/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2015 Simon Stürz * * Copyright (C) 2014 Michael Zanetti * * * * This file is part of nymea. * * * * nymea is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, version 2 of the License. * * * * nymea is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with nymea. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /*! \class nymeaserver::RuleEngine \brief The Engine that evaluates \l{Rule}{Rules} and finds \l{Action}{Actions} to be executed. \ingroup rules \inmodule core You can add, remove and update rules and query the engine for actions to be executed for a given \l{Event} described by an \l{EventDescriptor}. \sa Event, EventDescriptor, Rule, RuleAction */ /*! \fn void nymeaserver::RuleEngine::ruleAdded(const Rule &rule) Will be emitted whenever a new \l{Rule} is added to this Engine. The \a rule parameter holds the entire new rule.*/ /*! \fn void nymeaserver::RuleEngine::ruleRemoved(const RuleId &ruleId) Will be emitted whenever a \l{Rule} is removed from this Engine. \a ruleId holds the id of the removed rule. You should remove any references or copies you hold for this rule.*/ /*! \fn void nymeaserver::RuleEngine::ruleConfigurationChanged(const Rule &rule) Will be emitted whenever a \l{Rule} changed his enable/disable status. The parameter \a rule holds the changed rule.*/ /*! \enum nymeaserver::RuleEngine::RuleError \value RuleErrorNoError No error happened. Everything is fine. \value RuleErrorInvalidRuleId The given RuleId is not valid. \value RuleErrorRuleNotFound Couldn't find a \l{Rule} with the given id. \value RuleErrorDeviceNotFound Couldn't find a \l{Device} with the given id. \value RuleErrorEventTypeNotFound Couldn't find a \l{EventType} with the given id. \value RuleErrorStateTypeNotFound Couldn't find a \l{StateType} with the given id. \value RuleErrorActionTypeNotFound Couldn't find a \l{ActionType} with the given id. \value RuleErrorInvalidParameter The given \l{Param} is not valid. \value RuleErrorInvalidRuleFormat The format of the rule is not valid. (i.e. add \l{Rule} with exitActions and eventDescriptors) \value RuleErrorMissingParameter One of the given \l{Param}{Params} is missing. \value RuleErrorInvalidRuleActionParameter One of the given \l{RuleActionParam}{RuleActionParams} is not valid. \value RuleErrorInvalidStateEvaluatorValue One of the given \l{StateEvaluator}{StateEvaluators} has an invalid \l{State} value. \value RuleErrorTypesNotMatching The types of the \l{RuleActionParam} and the corresponding \l{Event} \l{Param} do not match. \value RuleErrorNotExecutable This rule is not executable. \value RuleErrorInvalidRepeatingOption One of the given \l{RepeatingOption}{RepeatingOption} is not valid. \value RuleErrorInvalidCalendarItem One of the given \l{CalendarItem}{CalendarItems} is not valid. \value RuleErrorInvalidTimeDescriptor One of the given \l{TimeDescriptor}{TimeDescriptors} is not valid. \value RuleErrorInvalidTimeEventItem One of the given \l{TimeEventItem}{TimeEventItems} is not valid. \value RuleErrorContainsEventBasesAction This rule contains an \l{Action} which depends on an \l{Event} value. This \l{Rule} cannot execute the \l{Action}{Actions} without the \l{Event} value. \value RuleErrorNoExitActions This rule does not have any ExitActions which means they cannot be executed. \value RuleErrorInterfaceNotFound There is no interface for the given string. */ /*! \enum nymeaserver::RuleEngine::RemovePolicy \value RemovePolicyCascade Remove the whole \l{Rule}. \value RemovePolicyUpdate Remove a \l{Device} from a rule. */ #include "ruleengine.h" #include "nymeacore.h" #include "loggingcategories.h" #include "time/calendaritem.h" #include "time/repeatingoption.h" #include "time/timeeventitem.h" #include "types/eventdescriptor.h" #include "types/paramdescriptor.h" #include "nymeasettings.h" #include "devices/devicemanager.h" #include "devices/device.h" #include #include #include #include namespace nymeaserver { /*! Constructs the RuleEngine with the given \a parent. Although it wouldn't harm to have multiple RuleEngines, there is one instance available from \l{NymeaCore}. This one should be used instead of creating multiple ones. */ RuleEngine::RuleEngine(QObject *parent) : QObject(parent) { } /*! Destructor of the \l{RuleEngine}. */ RuleEngine::~RuleEngine() { } /*! Ask the Engine to evaluate all the rules for the given \a event. This will search all the \l{Rule}{Rules} triggered by the given \a event and evaluate their states in the system. It will return a list of all \l{Rule}{Rules} that are triggered or change its active state because of this \a event. */ QList RuleEngine::evaluateEvent(const Event &event) { Device *device = NymeaCore::instance()->deviceManager()->findConfiguredDevice(event.deviceId()); if (!device) { qCWarning(dcRuleEngine()) << "Invalid event. DeviceID does not reference a valid device"; return QList(); } DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(device->deviceClassId()); EventType eventType = deviceClass.eventTypes().findById(event.eventTypeId()); if (event.params().count() == 0) { qCDebug(dcRuleEngineDebug).nospace().noquote() << "Evaluate event: " << device->name() << " - " << eventType.name() << " (DeviceId:" << device->id().toString() << ", EventTypeId:" << eventType.id().toString() << ")"; } else { qCDebug(dcRuleEngineDebug).nospace().noquote() << "Evaluate event: " << device->name() << " - " << eventType.name() << " (DeviceId:" << device->id().toString() << ", EventTypeId:" << eventType.id().toString() << ")" << endl << " " << event.params(); } QList rules; foreach (const RuleId &id, ruleIds()) { Rule rule = m_rules.value(id); if (!rule.enabled()) { qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Skipping rule " << rule.name() << " (" << rule.id().toString() << ") " << " because it is disabled."; continue; } // If we have a state based on this event if (containsState(rule.stateEvaluator(), event)) { rule.setStatesActive(rule.stateEvaluator().evaluate()); m_rules[rule.id()] = rule; } // If this rule does not base on an event, evaluate the rule if (rule.eventDescriptors().isEmpty() && rule.timeDescriptor().timeEventItems().isEmpty() && !rule.stateEvaluator().isEmpty()) { if (rule.timeActive() && rule.statesActive()) { if (!m_activeRules.contains(rule.id())) { qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") active."; rule.setActive(true); m_rules[rule.id()] = rule; m_activeRules.append(rule.id()); rules.append(rule); } } else { if (m_activeRules.contains(rule.id())) { qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") inactive."; rule.setActive(false); m_rules[rule.id()] = rule; m_activeRules.removeAll(rule.id()); rules.append(rule); } } } else { // Event based rule if (containsEvent(rule, event, device->deviceClassId())) { qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") contains event " << event.eventId(); if (rule.statesActive() && rule.timeActive()) { qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event" << event.eventId() << "and all states match."; rules.append(rule); } else { qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event" << event.eventId() << "but state are not matching."; rules.append(rule); } } } } return rules; } /*! Ask the Engine to evaluate all the rules for the given \a dateTime. This will search all the \l{Rule}{Rules} triggered by the given \a dateTime and evaluate their \l{CalendarItem}{CalendarItems} and \l{TimeEventItem}{TimeEventItems}. It will return a list of all \l{Rule}{Rules} that are triggered or change its active state. */ QList RuleEngine::evaluateTime(const QDateTime &dateTime) { // Initialize the last datetime if not already set (current time -1 second) if (!m_lastEvaluationTime.isValid()) { m_lastEvaluationTime = dateTime; m_lastEvaluationTime = m_lastEvaluationTime.addSecs(-1); } QList rules; foreach (const Rule &r, m_rules.values()) { Rule rule = m_rules.value(r.id()); if (!rule.enabled()) { qCDebug(dcRuleEngineDebug()) << "Skipping rule" + rule.name() + "because it is disabled"; continue; } // If no timeDescriptor, do nothing if (rule.timeDescriptor().isEmpty()) continue; // Check if this rule is based on calendarItems if (!rule.timeDescriptor().calendarItems().isEmpty()) { rule.setTimeActive(rule.timeDescriptor().evaluate(m_lastEvaluationTime, dateTime)); m_rules[rule.id()] = rule; if (rule.timeDescriptor().timeEventItems().isEmpty() && rule.eventDescriptors().isEmpty()) { if (rule.timeActive() && rule.statesActive()) { if (!m_activeRules.contains(rule.id())) { qCDebug(dcRuleEngine) << "Rule" << rule.id().toString() << "active."; rule.setActive(true); m_rules[rule.id()] = rule; m_activeRules.append(rule.id()); rules.append(rule); } } else { if (m_activeRules.contains(rule.id())) { qCDebug(dcRuleEngine) << "Rule" << rule.id().toString() << "inactive."; rule.setActive(false); m_rules[rule.id()] = rule; m_activeRules.removeAll(rule.id()); rules.append(rule); } } } } // If we have timeEvent items if (!rule.timeDescriptor().timeEventItems().isEmpty()) { bool valid = rule.timeDescriptor().evaluate(m_lastEvaluationTime, dateTime); if (valid && rule.timeActive()) { qCDebug(dcRuleEngine) << "Rule" << rule.id() << "time event triggert."; rules.append(rule); } } } m_lastEvaluationTime = dateTime; if (rules.count() > 0) { // Don't spam the log qCDebug(dcRuleEngine()) << "EvaluateTimeEvent evaluated" << rules.count() << "to be executed"; } return rules; } /*! Add the given \a rule to the system. If the rule will be added from an edit request, the parameter \a fromEdit will be true. */ RuleEngine::RuleError RuleEngine::addRule(const Rule &rule, bool fromEdit) { if (rule.id().isNull()) return RuleErrorInvalidRuleId; if (!findRule(rule.id()).id().isNull()) { qCWarning(dcRuleEngine) << "Already have a rule with this id."; return RuleErrorInvalidRuleId; } if (!rule.isConsistent()) { qCWarning(dcRuleEngine) << "Rule inconsistent."; return RuleErrorInvalidRuleFormat; } // Check IDs in each EventDescriptor foreach (const EventDescriptor &eventDescriptor, rule.eventDescriptors()) { if (!eventDescriptor.isValid()) { qWarning(dcRuleEngine()) << "EventDescriptor is incomplete. It must have either eventTypeId and deviceId, or interface and interfaceEvent"; return RuleErrorEventTypeNotFound; } if (eventDescriptor.type() == EventDescriptor::TypeDevice) { // check deviceId Device *device = NymeaCore::instance()->deviceManager()->findConfiguredDevice(eventDescriptor.deviceId()); if (!device) { qCWarning(dcRuleEngine) << "Cannot create rule. No configured device for eventTypeId" << eventDescriptor.eventTypeId(); return RuleErrorDeviceNotFound; } // Check eventTypeId for this deivce DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(device->deviceClassId()); bool eventTypeFound = false; foreach (const EventType &eventType, deviceClass.eventTypes()) { if (eventType.id() == eventDescriptor.eventTypeId()) { eventTypeFound = true; } } if (!eventTypeFound) { qCWarning(dcRuleEngine) << "Cannot create rule. Device " + device->name() + " has no event type:" << eventDescriptor.eventTypeId(); return RuleErrorEventTypeNotFound; } } else { // Interface based event Interface iface = NymeaCore::instance()->deviceManager()->supportedInterfaces().findByName(eventDescriptor.interface()); if (!iface.isValid()) { qWarning(dcRuleEngine()) << "No such interface:" << eventDescriptor.interface(); return RuleErrorInterfaceNotFound; } if (iface.eventTypes().findByName(eventDescriptor.interfaceEvent()).name().isEmpty()) { qWarning(dcRuleEngine()) << "Interface" << iface.name() << "has no such event:" << eventDescriptor.interfaceEvent(); return RuleErrorEventTypeNotFound; } } } // Check state evaluator if (!rule.stateEvaluator().isValid()) { qCWarning(dcRuleEngine) << "Cannot create rule. Got an invalid StateEvaluator."; return RuleErrorInvalidStateEvaluatorValue; } // Check time descriptor if (!rule.timeDescriptor().isEmpty()) { if (!rule.timeDescriptor().isValid()) { qCDebug(dcRuleEngine()) << "Cannot create rule. Got invalid timeDescriptor."; return RuleErrorInvalidTimeDescriptor; } // validate CalendarItems if (!rule.timeDescriptor().calendarItems().isEmpty()) { foreach (const CalendarItem &calendarItem, rule.timeDescriptor().calendarItems()) { if (!calendarItem.isValid()) { qCDebug(dcRuleEngine()) << "Cannot create rule. Got invalid calendarItem."; return RuleErrorInvalidCalendarItem; } // validate RepeatingOptions if (!calendarItem.repeatingOption().isEmtpy() && !calendarItem.repeatingOption().isValid()) { qCDebug(dcRuleEngine()) << "Cannot create rule. Got invalid repeatingOption in calendarItem."; return RuleErrorInvalidRepeatingOption; } } } // validate TimeEventItems if (!rule.timeDescriptor().timeEventItems().isEmpty()) { foreach (const TimeEventItem &timeEventItem, rule.timeDescriptor().timeEventItems()) { if (!timeEventItem.isValid()) { qCDebug(dcRuleEngine()) << "Cannot create rule. Got invalid timeEventItem."; return RuleErrorInvalidTimeEventItem; } // validate RepeatingOptions if (!timeEventItem.repeatingOption().isEmtpy() && !timeEventItem.repeatingOption().isValid()) { qCDebug(dcRuleEngine()) << "Cannot create rule. Got invalid repeatingOption in timeEventItem."; return RuleErrorInvalidRepeatingOption; } } } } // Check actions foreach (const RuleAction &ruleAction, rule.actions()) { RuleError ruleActionError = checkRuleAction(ruleAction, rule); if (ruleActionError != RuleErrorNoError) { return ruleActionError; } } // Check exit actions foreach (const RuleAction &ruleExitAction, rule.exitActions()) { RuleError ruleActionError = checkRuleAction(ruleExitAction, rule); if (ruleActionError != RuleErrorNoError) { return ruleActionError; } } appendRule(rule); saveRule(rule); if (!fromEdit) emit ruleAdded(rule); qCDebug(dcRuleEngine()) << "Rule" << rule.name() << rule.id().toString() << "added successfully."; return RuleErrorNoError; } /*! Edit the given \a rule in the system. The rule with the \l{RuleId} from the given \a rule will be removed from the system and readded with the new parameters in the given \a rule. */ RuleEngine::RuleError RuleEngine::editRule(const Rule &rule) { if (rule.id().isNull()) return RuleErrorInvalidRuleId; // Store rule in case the add new rule fails Rule oldRule = findRule(rule.id()); if (oldRule.id().isNull()) { qCWarning(dcRuleEngine) << "Cannot edit rule. There is no rule with id:" << rule.id().toString(); return RuleErrorRuleNotFound; } // First remove old rule with this id RuleError removeResult = removeRule(oldRule.id(), true); if (removeResult != RuleErrorNoError) { qCWarning(dcRuleEngine) << "Cannot edit rule. Could not remove the old rule."; // no need to restore, rule is still in system return removeResult; } // The rule is removed, now add the new one RuleError addResult = addRule(rule, true); if (addResult != RuleErrorNoError) { qCWarning(dcRuleEngine) << "Cannot edit rule. Could not add the new rule. Restoring the old rule."; // restore old rule appendRule(oldRule); return addResult; } // Successfully changed the rule emit ruleConfigurationChanged(rule); qCDebug(dcRuleEngine()) << "Rule" << rule.id().toString() << "updated."; return RuleErrorNoError; } /*! Returns a list of all \l{Rule}{Rules} loaded in this Engine. Be aware that this does not necessarily reflect the order of the rules in the engine. Use ruleIds() if you need the correct order. */ QList RuleEngine::rules() const { return m_rules.values(); } /*! Returns a list of all ruleIds loaded in this Engine. */ QList RuleEngine::ruleIds() const { return m_ruleIds; } /*! Removes the \l{Rule} with the given \a ruleId from the Engine. Returns \l{RuleError} which describes whether the operation was successful or not. If \a fromEdit is true, the notification Rules.RuleRemoved will not be emitted. */ RuleEngine::RuleError RuleEngine::removeRule(const RuleId &ruleId, bool fromEdit) { int index = m_ruleIds.indexOf(ruleId); if (index < 0) { return RuleErrorRuleNotFound; } m_ruleIds.takeAt(index); m_rules.remove(ruleId); m_activeRules.removeAll(ruleId); NymeaSettings settings(NymeaSettings::SettingsRoleRules); settings.beginGroup(ruleId.toString()); settings.remove(""); settings.endGroup(); if (!fromEdit) emit ruleRemoved(ruleId); qCDebug(dcRuleEngine()) << "Rule" << ruleId.toString() << "removed."; return RuleErrorNoError; } /*! Enables the rule with the given \a ruleId that has been previously disabled. \sa disableRule() */ RuleEngine::RuleError RuleEngine::enableRule(const RuleId &ruleId) { if (!m_rules.contains(ruleId)) { qCWarning(dcRuleEngine) << "Rule not found. Can't enable it"; return RuleErrorRuleNotFound; } Rule rule = m_rules.value(ruleId); if (rule.enabled()) return RuleErrorNoError; rule.setEnabled(true); m_rules[ruleId] = rule; saveRule(rule); emit ruleConfigurationChanged(rule); NymeaCore::instance()->logEngine()->logRuleEnabledChanged(rule, true); qCDebug(dcRuleEngine()) << "Rule" << rule.name() << rule.id() << "enabled."; return RuleErrorNoError; } /*! Disables the rule with the given \a ruleId. Disabled rules won't be triggered. \sa enableRule() */ RuleEngine::RuleError RuleEngine::disableRule(const RuleId &ruleId) { if (!m_rules.contains(ruleId)) { qCWarning(dcRuleEngine) << "Rule not found. Can't disable it"; return RuleErrorRuleNotFound; } Rule rule = m_rules.value(ruleId); if (!rule.enabled()) return RuleErrorNoError; rule.setEnabled(false); m_rules[ruleId] = rule; saveRule(rule); emit ruleConfigurationChanged(rule); NymeaCore::instance()->logEngine()->logRuleEnabledChanged(rule, false); qCDebug(dcRuleEngine()) << "Rule" << rule.name() << rule.id() << "disabled."; return RuleErrorNoError; } /*! Executes the list of \l{Action}{Actions} of the rule with the given \a ruleId. Returns the corresponding RuleEngine::RuleError to inform about the result. \sa executeExitActions() */ RuleEngine::RuleError RuleEngine::executeActions(const RuleId &ruleId) { // check if rule exists if (!m_rules.contains(ruleId)) { qCWarning(dcRuleEngine) << "Not executing rule actions: Rule not found."; return RuleErrorRuleNotFound; } Rule rule = m_rules.value(ruleId); // check if rule is executable if (!rule.executable()) { qCWarning(dcRuleEngine) << "Not executing rule actions: rule is not executable."; return RuleErrorNotExecutable; } // check if an Action is eventBased foreach (const RuleAction &ruleAction, rule.actions()) { if (ruleAction.isEventBased()) { qCWarning(dcRuleEngine) << "Not executing rule actions: rule action depends on an event:" << ruleAction.actionTypeId() << ruleAction.ruleActionParams(); return RuleErrorContainsEventBasesAction; } } qCDebug(dcRuleEngine) << "Executing rule actions of rule" << rule.name() << rule.id(); NymeaCore::instance()->logEngine()->logRuleActionsExecuted(rule); NymeaCore::instance()->executeRuleActions(rule.actions()); return RuleErrorNoError; } /*! Executes the list of \l{Action}{ExitActions} of the rule with the given \a ruleId. Returns the corresponding RuleEngine::RuleError to inform about the result. \sa executeActions() */ RuleEngine::RuleError RuleEngine::executeExitActions(const RuleId &ruleId) { // check if rule exits if (!m_rules.contains(ruleId)) { qCWarning(dcRuleEngine) << "Not executing rule exit actions: rule not found."; return RuleErrorRuleNotFound; } Rule rule = m_rules.value(ruleId); // check if rule is executable if (!rule.executable()) { qCWarning(dcRuleEngine) << "Not executing rule exit actions: rule is not executable."; return RuleErrorNotExecutable; } if (rule.exitActions().isEmpty()) { qCWarning(dcRuleEngine) << "Not executing rule exit actions: rule has no exit actions."; return RuleErrorNoExitActions; } qCDebug(dcRuleEngine) << "Executing rule exit actions of rule" << rule.name() << rule.id(); NymeaCore::instance()->logEngine()->logRuleExitActionsExecuted(rule); NymeaCore::instance()->executeRuleActions(rule.exitActions()); return RuleErrorNoError; } /*! Returns the \l{Rule} with the given \a ruleId. If the \l{Rule} does not exist, it will return \l{Rule::Rule()} */ Rule RuleEngine::findRule(const RuleId &ruleId) { if (!m_rules.contains(ruleId)) return Rule(); return m_rules.value(ruleId); } /*! Returns a list of all \l{Rule}{Rules} loaded in this Engine, which contains a \l{Device} with the given \a deviceId. */ QList RuleEngine::findRules(const DeviceId &deviceId) const { // Find all offending rules QList offendingRules; foreach (const Rule &rule, m_rules) { bool offending = false; foreach (const EventDescriptor &eventDescriptor, rule.eventDescriptors()) { if (eventDescriptor.deviceId() == deviceId) { offending = true; break; } } if (!offending && rule.stateEvaluator().containsDevice(deviceId)) offending = true; if (!offending) { foreach (const RuleAction &action, rule.actions()) { if (action.deviceId() == deviceId) { offending = true; break; } foreach (const RuleActionParam &ruleActionParam, action.ruleActionParams()) { if (ruleActionParam.stateDeviceId() == deviceId) { offending = true; break; } } if (offending) { break; } } } if (!offending) { foreach (const RuleAction &action, rule.exitActions()) { if (action.deviceId() == deviceId) { offending = true; break; } foreach (const RuleActionParam &ruleActionParam, action.ruleActionParams()) { if (ruleActionParam.stateDeviceId() == deviceId) { offending = true; break; } } if (offending) { break; } } } if (offending) offendingRules.append(rule.id()); } return offendingRules; } /*! Returns all devices that are somehow contained in a rule */ QList RuleEngine::devicesInRules() const { QList tmp; foreach (const Rule &rule, m_rules) { foreach (const EventDescriptor &descriptor, rule.eventDescriptors()) { if (!tmp.contains(descriptor.deviceId()) && !descriptor.deviceId().isNull()) { tmp.append(descriptor.deviceId()); } } foreach (const DeviceId &deviceId, rule.stateEvaluator().containedDevices()) { if (!tmp.contains(deviceId) && !deviceId.isNull()) { tmp.append(deviceId); } } foreach (const RuleAction &action, rule.actions()) { if (!tmp.contains(action.deviceId()) && !action.deviceId().isNull()) { tmp.append(action.deviceId()); } } foreach (const RuleAction &exitAction, rule.exitActions()) { if (!tmp.contains(exitAction.deviceId()) && !exitAction.deviceId().isNull()) { tmp.append(exitAction.deviceId()); } } } return tmp; } /*! Removes a \l{Device} from a \l{Rule} with the given \a id and \a deviceId. */ void RuleEngine::removeDeviceFromRule(const RuleId &id, const DeviceId &deviceId) { if (!m_rules.contains(id)) return; Rule rule = m_rules.value(id); // remove device from eventDescriptors QList eventDescriptors = rule.eventDescriptors(); QList removeIndexes; for (int i = 0; i < eventDescriptors.count(); i++) { if (eventDescriptors.at(i).deviceId() == deviceId) { removeIndexes.append(i); } } while (removeIndexes.count() > 0) { eventDescriptors.takeAt(removeIndexes.takeLast()); } // remove device from state evaluators StateEvaluator stateEvalatuator = rule.stateEvaluator(); stateEvalatuator.removeDevice(deviceId); // remove device from actions QList actions = rule.actions(); for (int i = 0; i < actions.count(); i++) { if (actions.at(i).deviceId() == deviceId) { removeIndexes.append(i); continue; } foreach (const RuleActionParam ¶m, actions.at(i).ruleActionParams()) { if (param.stateDeviceId() == deviceId) { removeIndexes.append(i); break; } } } while (removeIndexes.count() > 0) { actions.takeAt(removeIndexes.takeLast()); } // remove device from exit actions QList exitActions = rule.exitActions(); for (int i = 0; i < exitActions.count(); i++) { if (exitActions.at(i).deviceId() == deviceId) { removeIndexes.append(i); continue; } foreach (const RuleActionParam ¶m, exitActions.at(i).ruleActionParams()) { if (param.stateDeviceId() == deviceId) { removeIndexes.append(i); break; } } } while (removeIndexes.count() > 0) { exitActions.takeAt(removeIndexes.takeLast()); } // remove the rule from settings NymeaSettings settings(NymeaSettings::SettingsRoleRules); settings.beginGroup(id.toString()); settings.remove(""); settings.endGroup(); if (actions.isEmpty() && exitActions.isEmpty()) { // The rule doesn't have any actions any more and is useless at this point... let's remove it altogether qCDebug(dcRuleEngine()) << "Rule" << rule.name() << "(" + rule.id().toString() + ")" << "does not have any actions any more. Removing it."; m_rules.take(id); emit ruleRemoved(id); return; } Rule newRule; newRule.setId(id); newRule.setName(rule.name()); newRule.setEventDescriptors(eventDescriptors); newRule.setStateEvaluator(stateEvalatuator); newRule.setTimeDescriptor(rule.timeDescriptor()); newRule.setActions(actions); newRule.setExitActions(exitActions); m_rules[id] = newRule; // save it saveRule(newRule); emit ruleConfigurationChanged(newRule); } bool RuleEngine::containsEvent(const Rule &rule, const Event &event, const DeviceClassId &deviceClassId) { foreach (const EventDescriptor &eventDescriptor, rule.eventDescriptors()) { // If this is a device based rule, eventTypeId and deviceId must match if (eventDescriptor.type() == EventDescriptor::TypeDevice) { if (eventDescriptor.eventTypeId() != event.eventTypeId() || eventDescriptor.deviceId() != event.deviceId()) { continue; } } // If this is a interface based rule, the device must implement the interface if (eventDescriptor.type() == EventDescriptor::TypeInterface) { DeviceClass dc = NymeaCore::instance()->deviceManager()->findDeviceClass(deviceClassId); if (!dc.interfaces().contains(eventDescriptor.interface())) { // DeviceClass for this event doesn't implement the interface for this eventDescriptor continue; } EventType et = dc.eventTypes().findById(event.eventTypeId()); if (et.name() != eventDescriptor.interfaceEvent()) { // The fired event name does not match with the eventDescriptor's interfaceEvent continue; } } // Ok, either device/eventTypeId or interface/interfaceEvent are matching. Compare the paramdescriptor bool allOK = true; foreach (const ParamDescriptor ¶mDescriptor, eventDescriptor.paramDescriptors()) { QVariant paramValue; if (!paramDescriptor.paramTypeId().isNull()) { paramValue = event.param(paramDescriptor.paramTypeId()).value(); } else { if (paramDescriptor.paramName().isEmpty()) { qWarning(dcRuleEngine()) << "ParamDescriptor invalid. Either paramTypeId or paramName are required"; allOK = false; continue; } DeviceClass dc = NymeaCore::instance()->deviceManager()->findDeviceClass(deviceClassId); EventType et = dc.eventTypes().findById(event.eventTypeId()); ParamType pt = et.paramTypes().findByName(paramDescriptor.paramName()); paramValue = event.param(pt.id()).value(); } switch (paramDescriptor.operatorType()) { case Types::ValueOperatorEquals: if (paramValue != paramDescriptor.value()) { allOK = false; } break; case Types::ValueOperatorNotEquals: if (paramValue == paramDescriptor.value()) { allOK = false; } break; case Types::ValueOperatorGreater: if (event.param(paramDescriptor.paramTypeId()).value() <= paramDescriptor.value()) { allOK = false; } break; case Types::ValueOperatorGreaterOrEqual: if (event.param(paramDescriptor.paramTypeId()).value() < paramDescriptor.value()) { allOK = false; } break; case Types::ValueOperatorLess: if (event.param(paramDescriptor.paramTypeId()).value() >= paramDescriptor.value()) { allOK = false; } break; case Types::ValueOperatorLessOrEqual: if (event.param(paramDescriptor.paramTypeId()).value() < paramDescriptor.value()) { allOK = false; } break; } } // All matching! if (allOK) { return true; } } qCDebug(dcRuleEngineDebug()) << "Rule" << rule.name() << "does not match event descriptors"; return false; } bool RuleEngine::containsState(const StateEvaluator &stateEvaluator, const Event &stateChangeEvent) { if (stateEvaluator.stateDescriptor().isValid()) { if (stateEvaluator.stateDescriptor().type() == StateDescriptor::TypeDevice) { if (stateEvaluator.stateDescriptor().stateTypeId().toString() == stateChangeEvent.eventTypeId().toString()) { return true; } } else { Device *device = NymeaCore::instance()->deviceManager()->findConfiguredDevice(stateChangeEvent.deviceId()); DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(device->deviceClassId()); if (deviceClass.interfaces().contains(stateEvaluator.stateDescriptor().interface())) { return true; } } } foreach (const StateEvaluator &childEvaluator, stateEvaluator.childEvaluators()) { if (containsState(childEvaluator, stateChangeEvent)) { return true; } } return false; } RuleEngine::RuleError RuleEngine::checkRuleAction(const RuleAction &ruleAction, const Rule &rule) { if (!ruleAction.isValid()) { qWarning(dcRuleEngine()) << "Action is incomplete. It must have either deviceId and actionTypeId/browserItemId, or interface and interfaceAction:" << ruleAction; return RuleErrorActionTypeNotFound; } ActionType actionType; if (ruleAction.type() == RuleAction::TypeDevice) { Device *device = NymeaCore::instance()->deviceManager()->findConfiguredDevice(ruleAction.deviceId()); if (!device) { qCWarning(dcRuleEngine) << "Cannot create rule. No configured device with ID" << ruleAction.deviceId(); return RuleErrorDeviceNotFound; } DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(device->deviceClassId()); if (!deviceClass.hasActionType(ruleAction.actionTypeId())) { qCWarning(dcRuleEngine) << "Cannot create rule. Device " + device->name() + " has no action type:" << ruleAction.actionTypeId(); return RuleErrorActionTypeNotFound; } actionType = deviceClass.actionTypes().findById(ruleAction.actionTypeId()); } else if (ruleAction.type() == RuleAction::TypeInterface) { Interface iface = NymeaCore::instance()->deviceManager()->supportedInterfaces().findByName(ruleAction.interface()); if (!iface.isValid()) { qCWarning(dcRuleEngine()) << "Cannot create rule. No such interface:" << ruleAction.interface(); return RuleError::RuleErrorInterfaceNotFound; } actionType = iface.actionTypes().findByName(ruleAction.interfaceAction()); if (actionType.name().isEmpty()) { qCWarning(dcRuleEngine()) << "Cannot create rule. Interface" << iface.name() << "does not implement action" << ruleAction.interfaceAction(); return RuleError::RuleErrorActionTypeNotFound; } } else if (ruleAction.type() == RuleAction::TypeBrowser) { Device *device = NymeaCore::instance()->deviceManager()->findConfiguredDevice(ruleAction.deviceId()); if (!device) { qCWarning(dcRuleEngine) << "Cannot create rule. No configured device with ID" << ruleAction.deviceId(); return RuleErrorDeviceNotFound; } if (ruleAction.browserItemId().isEmpty()) { qCWarning(dcRuleEngine()) << "Cannot create rule with empty browserItemId"; return RuleErrorInvalidRuleActionParameter; } } else { return RuleErrorActionTypeNotFound; } // Not all rule actions might have an actiontype (e.g. browser item executions) if (!actionType.id().isNull()) { // Verify given params foreach (const RuleActionParam &ruleActionParam, ruleAction.ruleActionParams()) { RuleError ruleActionParamError = checkRuleActionParam(ruleActionParam, actionType, rule); if (ruleActionParamError != RuleErrorNoError) { return ruleActionParamError; } } // Verify all required params are given foreach (const ParamType ¶mType, actionType.paramTypes()) { bool found = false; foreach (const RuleActionParam &ruleActionParam, ruleAction.ruleActionParams()) { if (ruleActionParam.paramTypeId() == paramType.id() || ruleActionParam.paramName() == paramType.name()) { found = true; break; } } if (!found) { return RuleErrorMissingParameter; } } } return RuleErrorNoError; } RuleEngine::RuleError RuleEngine::checkRuleActionParam(const RuleActionParam &ruleActionParam, const ActionType &actionType, const Rule &rule) { // Check param identifier (either paramTypeId or paramName) ParamType paramType; if (!ruleActionParam.paramTypeId().isNull()) { paramType = actionType.paramTypes().findById(ruleActionParam.paramTypeId()); } else if (!ruleActionParam.paramName().isEmpty()) { paramType = actionType.paramTypes().findByName(ruleActionParam.paramName()); } else { return RuleErrorInvalidRuleActionParameter; } if (ruleActionParam.isEventBased()) { // We have an eventTypeId, see if the rule actually has such a event bool found = false; foreach (const EventDescriptor &ed, rule.eventDescriptors()) { if (ed.eventTypeId() == ruleActionParam.eventTypeId()) { found = true; } } if (!found) { qCWarning(dcRuleEngine) << "Cannot create rule. EventTypeId" << ruleActionParam.eventTypeId() << "not found in rule's eventDescriptors."; return RuleErrorInvalidRuleActionParameter; } // check if the param type of the event and the action match QVariant::Type eventParamType = getEventParamType(ruleActionParam.eventTypeId(), ruleActionParam.eventParamTypeId()); QVariant v(eventParamType); if (eventParamType != paramType.type() && !v.canConvert(static_cast(paramType.type()))) { qCWarning(dcRuleEngine) << "Cannot create rule. RuleActionParam" << ruleActionParam.paramTypeId().toString() << " and given event param " << ruleActionParam.eventParamTypeId().toString() << "have not the same type:"; qCWarning(dcRuleEngine) << " -> actionParamType:" << paramType.type(); qCWarning(dcRuleEngine) << " -> eventParamType:" << eventParamType; return RuleErrorTypesNotMatching; } } else if (ruleActionParam.isStateBased()) { Device *d = NymeaCore::instance()->deviceManager()->findConfiguredDevice(ruleActionParam.stateDeviceId()); if (!d) { qCWarning(dcRuleEngine()) << "Cannot create Rule. DeviceId from RuleActionParam" << ruleActionParam.paramTypeId() << "not found in system."; return RuleErrorDeviceNotFound; } DeviceClass stateDeviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(d->deviceClassId()); StateType stateType = stateDeviceClass.stateTypes().findById(ruleActionParam.stateTypeId()); QVariant::Type actionParamType = getActionParamType(actionType.id(), ruleActionParam.paramTypeId()); QVariant v(stateType.type()); if (actionParamType != stateType.type() && !v.canConvert(static_cast(actionParamType))) { qCWarning(dcRuleEngine) << "Cannot create rule. RuleActionParam" << ruleActionParam.paramTypeId().toString() << " and given state based param " << ruleActionParam.stateTypeId().toString() << "have not the same type:"; qCWarning(dcRuleEngine) << " -> actionParamType:" << actionParamType; qCWarning(dcRuleEngine) << " -> stateType:" << stateType.type(); return RuleErrorTypesNotMatching; } } else { // Is value based if (ruleActionParam.value().isNull()) { qCDebug(dcRuleEngine()) << "Cannot create rule. No param value given for action:" << ruleActionParam.paramTypeId().toString(); return RuleErrorInvalidRuleActionParameter; } if (paramType.type() != ruleActionParam.value().type() && !ruleActionParam.value().canConvert(static_cast(paramType.type()))) { qCWarning(dcRuleEngine) << "Cannot create rule. RuleActionParam" << ruleActionParam.paramTypeId().toString() << " and given state based param " << ruleActionParam.stateTypeId().toString() << "have not the same type:"; qCWarning(dcRuleEngine) << " -> actionParamType:" << paramType.type(); qCWarning(dcRuleEngine) << " -> stateType:" << ruleActionParam.value().type(); return RuleErrorTypesNotMatching; } } return RuleErrorNoError; } QVariant::Type RuleEngine::getActionParamType(const ActionTypeId &actionTypeId, const ParamTypeId ¶mTypeId) { foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) { foreach (const ActionType &actionType, deviceClass.actionTypes()) { if (actionType.id() == actionTypeId) { foreach (const ParamType ¶mType, actionType.paramTypes()) { if (paramType.id() == paramTypeId) { return paramType.type(); } } } } } return QVariant::Invalid; } QVariant::Type RuleEngine::getEventParamType(const EventTypeId &eventTypeId, const ParamTypeId ¶mTypeId) { foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) { foreach (const EventType &eventType, deviceClass.eventTypes()) { if (eventType.id() == eventTypeId) { foreach (const ParamType ¶mType, eventType.paramTypes()) { if (paramType.id() == paramTypeId) { return paramType.type(); } } } } } return QVariant::Invalid; } void RuleEngine::appendRule(const Rule &rule) { Rule newRule = rule; newRule.setStatesActive(newRule.stateEvaluator().evaluate()); qCDebug(dcRuleEngine()) << "Adding Rule:" << newRule; m_rules.insert(rule.id(), newRule); m_ruleIds.append(rule.id()); } void RuleEngine::saveRule(const Rule &rule) { NymeaSettings settings(NymeaSettings::SettingsRoleRules); settings.beginGroup(rule.id().toString()); settings.setValue("name", rule.name()); settings.setValue("enabled", rule.enabled()); settings.setValue("executable", rule.executable()); // Save timeDescriptor settings.beginGroup("timeDescriptor"); if (!rule.timeDescriptor().isEmpty()) { settings.beginGroup("calendarItems"); for (int i = 0; i < rule.timeDescriptor().calendarItems().count(); i++) { settings.beginGroup("CalendarItem-" + QString::number(i)); const CalendarItem &calendarItem = rule.timeDescriptor().calendarItems().at(i); if (calendarItem.dateTime().isValid()) settings.setValue("dateTime", calendarItem.dateTime().toTime_t()); if (calendarItem.startTime().isValid()) settings.setValue("startTime", calendarItem.startTime().toString("hh:mm")); settings.setValue("duration", calendarItem.duration()); settings.setValue("mode", calendarItem.repeatingOption().mode()); // Save weekDays settings.beginWriteArray("weekDays"); for (int i = 0; i < calendarItem.repeatingOption().weekDays().count(); ++i) { settings.setArrayIndex(i); settings.setValue("weekDay", calendarItem.repeatingOption().weekDays().at(i)); } settings.endArray(); // Save monthDays settings.beginWriteArray("monthDays"); for (int i = 0; i < calendarItem.repeatingOption().monthDays().count(); ++i) { settings.setArrayIndex(i); settings.setValue("monthDay", calendarItem.repeatingOption().monthDays().at(i)); } settings.endArray(); settings.endGroup(); } settings.endGroup(); settings.beginGroup("timeEventItems"); for (int i = 0; i < rule.timeDescriptor().timeEventItems().count(); i++) { settings.beginGroup("TimeEventItem-" + QString::number(i)); const TimeEventItem &timeEventItem = rule.timeDescriptor().timeEventItems().at(i); if (timeEventItem.dateTime().isValid()) settings.setValue("dateTime", timeEventItem.dateTime().toTime_t()); if (timeEventItem.time().isValid()) settings.setValue("time", timeEventItem.time().toString("hh:mm")); settings.setValue("mode", timeEventItem.repeatingOption().mode()); // Save weekDays settings.beginWriteArray("weekDays"); for (int i = 0; i < timeEventItem.repeatingOption().weekDays().count(); ++i) { settings.setArrayIndex(i); settings.setValue("weekDay", timeEventItem.repeatingOption().weekDays().at(i)); } settings.endArray(); // Save monthDays settings.beginWriteArray("monthDays"); for (int i = 0; i < timeEventItem.repeatingOption().monthDays().count(); ++i) { settings.setArrayIndex(i); settings.setValue("monthDay", timeEventItem.repeatingOption().monthDays().at(i)); } settings.endArray(); settings.endGroup(); } settings.endGroup(); } settings.endGroup(); // Save Events / EventDescriptors settings.beginGroup("events"); for (int i = 0; i < rule.eventDescriptors().count(); i++) { const EventDescriptor &eventDescriptor = rule.eventDescriptors().at(i); settings.beginGroup("EventDescriptor-" + QString::number(i)); settings.setValue("deviceId", eventDescriptor.deviceId().toString()); settings.setValue("eventTypeId", eventDescriptor.eventTypeId().toString()); settings.setValue("interface", eventDescriptor.interface()); settings.setValue("interfaceEvent", eventDescriptor.interfaceEvent()); foreach (const ParamDescriptor ¶mDescriptor, eventDescriptor.paramDescriptors()) { if (!paramDescriptor.paramTypeId().isNull()) { settings.beginGroup("ParamDescriptor-" + paramDescriptor.paramTypeId().toString()); } else { settings.beginGroup("ParamDescriptor-" + paramDescriptor.paramName()); } settings.setValue("valueType", static_cast(paramDescriptor.value().type())); settings.setValue("value", paramDescriptor.value()); settings.setValue("operator", paramDescriptor.operatorType()); settings.endGroup(); } settings.endGroup(); } settings.endGroup(); // Save StateEvaluator rule.stateEvaluator().dumpToSettings(settings, "stateEvaluator"); // Save ruleActions settings.beginGroup("ruleActions"); saveRuleActions(&settings, rule.actions()); settings.endGroup(); // Save ruleExitActions settings.beginGroup("ruleExitActions"); saveRuleActions(&settings, rule.exitActions()); settings.endGroup(); qCDebug(dcRuleEngineDebug()) << "Saved rule to config:" << rule; } void RuleEngine::saveRuleActions(NymeaSettings *settings, const QList &ruleActions) { int i = 0; foreach (const RuleAction &action, ruleActions) { settings->beginGroup(QString::number(i)); if (action.type() == RuleAction::TypeDevice) { settings->setValue("deviceId", action.deviceId().toString()); settings->setValue("actionTypeId", action.actionTypeId().toString()); } else if (action.type() == RuleAction::TypeBrowser) { settings->setValue("deviceId", action.deviceId().toString()); settings->setValue("browserItemId", action.browserItemId()); } else if (action.type() == RuleAction::TypeInterface){ settings->setValue("interface", action.interface()); settings->setValue("interfaceAction", action.interfaceAction()); } else { Q_ASSERT_X(false, "RuleEngine::saveRule", "Unhandled rule action type."); } foreach (const RuleActionParam ¶m, action.ruleActionParams()) { if (!param.paramTypeId().isNull()) { settings->beginGroup("RuleActionParam-" + param.paramTypeId().toString()); } else { settings->beginGroup("RuleActionParam-" + param.paramName()); } settings->setValue("valueType", static_cast(param.value().type())); settings->setValue("value", param.value()); if (param.isEventBased()) { settings->setValue("eventTypeId", param.eventTypeId().toString()); settings->setValue("eventParamTypeId", param.eventParamTypeId()); } else if (param.isStateBased()) { settings->setValue("stateDeviceId", param.stateDeviceId().toString()); settings->setValue("stateTypeId", param.stateTypeId()); } settings->endGroup(); } i++; settings->endGroup(); } } QList RuleEngine::loadRuleActions(NymeaSettings *settings) { QList actions; foreach (const QString &actionNumber, settings->childGroups()) { settings->beginGroup(actionNumber); RuleActionParamList params; foreach (QString paramTypeIdString, settings->childGroups()) { if (paramTypeIdString.startsWith("RuleActionParam-")) { settings->beginGroup(paramTypeIdString); QString strippedParamTypeIdString = paramTypeIdString.remove(QRegExp("^RuleActionParam-")); EventTypeId eventTypeId = EventTypeId(settings->value("eventTypeId", EventTypeId()).toString()); ParamTypeId eventParamTypeId = ParamTypeId(settings->value("eventParamTypeId", ParamTypeId()).toString()); DeviceId stateDeviceId = DeviceId(settings->value("stateDeviceId", DeviceId()).toString()); StateTypeId stateTypeId = StateTypeId(settings->value("stateTypeId", StateTypeId()).toString()); QVariant value = settings->value("value"); if (settings->contains("valueType")) { QVariant::Type valueType = static_cast(settings->value("valueType").toInt()); // Note: only warn, and continue with the QVariant guessed type if (valueType == QVariant::Invalid) { qCWarning(dcRuleEngine()) << "Could not load the value type of the rule action param " << strippedParamTypeIdString << ". The value type will be guessed by QVariant."; } else if (!value.canConvert(static_cast(valueType))) { qCWarning(dcRuleEngine()) << "Error loading rule action. Could not convert the rule action param value" << value << "to the stored type" << valueType; } else { value.convert(static_cast(valueType)); } } RuleActionParam param; if (!ParamTypeId(strippedParamTypeIdString).isNull()) { // By ParamTypeId param = RuleActionParam(ParamTypeId(strippedParamTypeIdString), value); } else { // By param name param = RuleActionParam(strippedParamTypeIdString, value); } param.setEventTypeId(eventTypeId); param.setEventParamTypeId(eventParamTypeId); param.setStateDeviceId(stateDeviceId); param.setStateTypeId(stateTypeId); params.append(param); settings->endGroup(); } } if (settings->contains("actionTypeId") && settings->contains("deviceId")) { RuleAction action = RuleAction(ActionTypeId(settings->value("actionTypeId").toString()), DeviceId(settings->value("deviceId").toString())); action.setRuleActionParams(params); actions.append(action); } else if (settings->contains("deviceId") && settings->contains("browserItemId")) { RuleAction action = RuleAction(DeviceId(settings->value("deviceId").toString()), settings->value("browserItemId").toString()); actions.append(action); } else if (settings->contains("interface") && settings->contains("interfaceAction")){ RuleAction action = RuleAction(settings->value("interface").toString(), settings->value("interfaceAction").toString()); action.setRuleActionParams(params); actions.append(action); } settings->endGroup(); } return actions; } void RuleEngine::init() { NymeaSettings settings(NymeaSettings::SettingsRoleRules); qCDebug(dcRuleEngine) << "Loading rules from" << settings.fileName(); foreach (const QString &idString, settings.childGroups()) { settings.beginGroup(idString); QString name = settings.value("name", idString).toString(); bool enabled = settings.value("enabled", true).toBool(); bool executable = settings.value("executable", true).toBool(); qCDebug(dcRuleEngine) << "Loading rule" << name << idString; // Load timeDescriptor TimeDescriptor timeDescriptor; QList calendarItems; QList timeEventItems; settings.beginGroup("timeDescriptor"); settings.beginGroup("calendarItems"); foreach (const QString &childGroup, settings.childGroups()) { settings.beginGroup(childGroup); CalendarItem calendarItem; calendarItem.setDateTime(QDateTime::fromTime_t(settings.value("dateTime", 0).toUInt())); calendarItem.setStartTime(QTime::fromString(settings.value("startTime").toString())); calendarItem.setDuration(settings.value("duration", 0).toUInt()); QList weekDays; QList monthDays; RepeatingOption::RepeatingMode mode = static_cast(settings.value("mode", 0).toInt()); // Load weekDays int weekDaysCount = settings.beginReadArray("weekDays"); for (int i = 0; i < weekDaysCount; ++i) { settings.setArrayIndex(i); weekDays.append(settings.value("weekDay", 0).toInt()); } settings.endArray(); // Load weekDays int monthDaysCount = settings.beginReadArray("monthDays"); for (int i = 0; i < monthDaysCount; ++i) { settings.setArrayIndex(i); monthDays.append(settings.value("monthDay", 0).toInt()); } settings.endArray(); settings.endGroup(); calendarItem.setRepeatingOption(RepeatingOption(mode, weekDays, monthDays)); calendarItems.append(calendarItem); } settings.endGroup(); timeDescriptor.setCalendarItems(calendarItems); settings.beginGroup("timeEventItems"); foreach (const QString &childGroup, settings.childGroups()) { settings.beginGroup(childGroup); TimeEventItem timeEventItem; timeEventItem.setDateTime(settings.value("dateTime", 0).toUInt()); timeEventItem.setTime(QTime::fromString(settings.value("time").toString())); QList weekDays; QList monthDays; RepeatingOption::RepeatingMode mode = static_cast(settings.value("mode", 0).toInt()); // Load weekDays int weekDaysCount = settings.beginReadArray("weekDays"); for (int i = 0; i < weekDaysCount; ++i) { settings.setArrayIndex(i); weekDays.append(settings.value("weekDay", 0).toInt()); } settings.endArray(); // Load weekDays int monthDaysCount = settings.beginReadArray("monthDays"); for (int i = 0; i < monthDaysCount; ++i) { settings.setArrayIndex(i); monthDays.append(settings.value("monthDay", 0).toInt()); } settings.endArray(); settings.endGroup(); timeEventItem.setRepeatingOption(RepeatingOption(mode, weekDays, monthDays)); timeEventItems.append(timeEventItem); } settings.endGroup(); settings.endGroup(); timeDescriptor.setTimeEventItems(timeEventItems); // Load events QList eventDescriptorList; settings.beginGroup("events"); foreach (QString eventGroupName, settings.childGroups()) { if (eventGroupName.startsWith("EventDescriptor-")) { settings.beginGroup(eventGroupName); EventTypeId eventTypeId(settings.value("eventTypeId").toString()); DeviceId deviceId(settings.value("deviceId").toString()); QString interface = settings.value("interface").toString(); QString interfaceEvent = settings.value("interfaceEvent").toString(); QList params; foreach (QString groupName, settings.childGroups()) { if (groupName.startsWith("ParamDescriptor-")) { settings.beginGroup(groupName); QString strippedGroupName = groupName.remove(QRegExp("^ParamDescriptor-")); QVariant value = settings.value("value"); if (settings.contains("valueType")) { QVariant::Type valueType = static_cast(settings.value("valueType").toInt()); // Note: only warn, and continue with the QVariant guessed type if (valueType == QVariant::Invalid) { qCWarning(dcRuleEngine()) << name << idString << "Could not load the value type of the param descriptor" << strippedGroupName << ". The value type will be guessed by QVariant."; } else if (!value.canConvert(static_cast(valueType))) { qCWarning(dcRuleEngine()) << "Error loading rule" << name << idString << ". Could not convert the param descriptor value" << value << "to the stored type" << valueType; } else { value.convert(static_cast(valueType)); } } if (!ParamTypeId(strippedGroupName).isNull()) { ParamDescriptor paramDescriptor(ParamTypeId(strippedGroupName), value); paramDescriptor.setOperatorType(static_cast(settings.value("operator").toInt())); params.append(paramDescriptor); } else { ParamDescriptor paramDescriptor(strippedGroupName, value); paramDescriptor.setOperatorType(static_cast(settings.value("operator").toInt())); params.append(paramDescriptor); } settings.endGroup(); } } if (!eventTypeId.isNull()) { EventDescriptor eventDescriptor(eventTypeId, deviceId, params); eventDescriptorList.append(eventDescriptor); } else { EventDescriptor eventDescriptor(interface, interfaceEvent, params); eventDescriptorList.append(eventDescriptor); } settings.endGroup(); } } settings.endGroup(); // Load stateEvaluator StateEvaluator stateEvaluator = StateEvaluator::loadFromSettings(settings, "stateEvaluator"); // Load actions QList actions; settings.beginGroup("ruleActions"); actions = loadRuleActions(&settings); settings.endGroup(); // Load exit actions QList exitActions; settings.beginGroup("ruleExitActions"); exitActions = loadRuleActions(&settings); settings.endGroup(); Rule rule; rule.setId(RuleId(idString)); rule.setName(name); rule.setTimeDescriptor(timeDescriptor); rule.setEventDescriptors(eventDescriptorList); rule.setStateEvaluator(stateEvaluator); rule.setActions(actions); rule.setExitActions(exitActions); rule.setEnabled(enabled); rule.setExecutable(executable); rule.setStatesActive(rule.stateEvaluator().evaluate()); appendRule(rule); settings.endGroup(); } } }