diff --git a/libnymea-core/jsonrpc/ruleshandler.cpp b/libnymea-core/jsonrpc/ruleshandler.cpp index 41643f45..bae4b307 100644 --- a/libnymea-core/jsonrpc/ruleshandler.cpp +++ b/libnymea-core/jsonrpc/ruleshandler.cpp @@ -84,9 +84,17 @@ RulesHandler::RulesHandler(QObject *parent) : setReturns("GetRuleDetails", returns); params.clear(); returns.clear(); - setDescription("AddRule", "Add a rule. You can describe rules by one or many EventDesciptors and a StateEvaluator. Note that only " - "one of either eventDescriptor or eventDescriptorList may be passed at a time. A rule can be created but left disabled, " - "meaning it won't actually be executed until set to enabled. If not given, enabled defaults to true."); + setDescription("AddRule", "Add a rule. You can describe rules by one or many EventDesciptors and a StateEvaluator. " + "Note that only one of either eventDescriptor or eventDescriptorList may be passed at a time. " + "A rule can be created but left disabled, meaning it won't actually be executed until set to enabled. " + "If not given, enabled defaults to true. A rule can have a list of actions and exitActions. " + "It must have at least one Action. For state based rules, actions will be executed when the system " + "enters a state matching the stateDescriptor. The exitActions will be executed when the system leaves " + "the described state again. For event based rules, actions will be executed when a matching event " + "happens and if the stateEvaluator matches the system's state. ExitActions for such rules will be " + "executed when a matching event happens and the stateEvaluator is not matching the system's state. " + "A rule marked as executable can be executed via the API using Rules.ExecuteRule, that means, its " + "actions will be executed regardless of the the eventDescriptor and stateEvaluators."); params.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); params.insert("actions", QVariantList() << JsonTypes::ruleActionRef()); params.insert("o:timeDescriptor", JsonTypes::timeDescriptorRef()); diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index b8890813..21e3e64d 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -593,8 +593,16 @@ void NymeaCore::gotEvent(const Event &event) // Event based if (!rule.eventDescriptors().isEmpty()) { m_logger->logRuleTriggered(rule); + QList tmp; + if (rule.statesActive()) { + qCDebug(dcRuleEngineDebug()) << "Executing actions"; + tmp = rule.actions(); + } else { + qCDebug(dcRuleEngineDebug()) << "Executing exitActions"; + tmp = rule.exitActions(); + } // check if we have an event based action or a normal action - foreach (const RuleAction &action, rule.actions()) { + foreach (const RuleAction &action, tmp) { if (action.isEventBased()) { eventBasedActions.append(action); } else { @@ -645,8 +653,10 @@ void NymeaCore::onDateTimeChanged(const QDateTime &dateTime) // TimeEvent based if (!rule.timeDescriptor().timeEventItems().isEmpty()) { m_logger->logRuleTriggered(rule); - foreach (const RuleAction &action, rule.actions()) { - actions.append(action); + if (rule.statesActive()) { + actions.append(rule.actions()); + } else { + actions.append(rule.exitActions()); } } else { // Calendar based rule diff --git a/libnymea-core/rule.cpp b/libnymea-core/rule.cpp index e4d11ddc..520a9aea 100644 --- a/libnymea-core/rule.cpp +++ b/libnymea-core/rule.cpp @@ -195,14 +195,14 @@ bool Rule::isValid() const bool Rule::isConsistent() const { // check if this rules is based on any event and contains exit actions - if (!eventDescriptors().isEmpty() && !exitActions().isEmpty()) { - qCWarning(dcRuleEngine) << "Rule not consistent. The exitActions will never be executed if the rule contains an eventDescriptor."; + if (!eventDescriptors().isEmpty() && stateEvaluator().isEmpty() && !exitActions().isEmpty()) { + qCWarning(dcRuleEngine) << "Rule not consistent. The exitActions will never be executed if the rule contains an eventDescriptor but no stateEvaluator."; return false; } // check if this rules is based on any time events and contains exit actions - if (!timeDescriptor().timeEventItems().isEmpty() && !exitActions().isEmpty()) { - qCWarning(dcRuleEngine) << "Rule not consistent. The exitActions will never be executed if the rule contains an timeEvents."; + if (!timeDescriptor().timeEventItems().isEmpty() && stateEvaluator().isEmpty() && !exitActions().isEmpty()) { + qCWarning(dcRuleEngine) << "Rule not consistent. The exitActions will never be executed if the rule contains a timeEvents but no stateEvaluator."; return false; } diff --git a/libnymea-core/ruleengine.cpp b/libnymea-core/ruleengine.cpp index cf835b8f..a82d31f4 100644 --- a/libnymea-core/ruleengine.cpp +++ b/libnymea-core/ruleengine.cpp @@ -390,7 +390,7 @@ QList RuleEngine::evaluateEvent(const Event &event) foreach (const RuleId &id, ruleIds()) { Rule rule = m_rules.value(id); if (!rule.enabled()) { - qCDebug(dcRuleEngineDebug()) << "Skipping rule" << rule.name() << "because it is disabled"; + qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Skipping rule " << rule.name() << " (" << rule.id().toString() << ") " << " because it is disabled."; continue; } @@ -404,7 +404,7 @@ QList RuleEngine::evaluateEvent(const Event &event) if (rule.eventDescriptors().isEmpty() && rule.timeDescriptor().timeEventItems().isEmpty()) { if (rule.timeActive() && rule.statesActive()) { if (!m_activeRules.contains(rule.id())) { - qCDebug(dcRuleEngine) << "Rule" << rule.id().toString() << "active."; + qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") active."; rule.setActive(true); m_rules[rule.id()] = rule; m_activeRules.append(rule.id()); @@ -412,7 +412,7 @@ QList RuleEngine::evaluateEvent(const Event &event) } } else { if (m_activeRules.contains(rule.id())) { - qCDebug(dcRuleEngine) << "Rule" << rule.id().toString() << "inactive."; + qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") inactive."; rule.setActive(false); m_rules[rule.id()] = rule; m_activeRules.removeAll(rule.id()); @@ -421,9 +421,15 @@ QList RuleEngine::evaluateEvent(const Event &event) } } else { // Event based rule - if (containsEvent(rule, event, device->deviceClassId()) && rule.statesActive() && rule.timeActive()) { - qCDebug(dcRuleEngine) << "Rule" << rule.id() << "contains event" << event.eventId() << "and all states match."; - rules.append(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); + } } } } @@ -486,8 +492,8 @@ QList RuleEngine::evaluateTime(const QDateTime &dateTime) // If we have timeEvent items if (!rule.timeDescriptor().timeEventItems().isEmpty()) { bool valid = rule.timeDescriptor().evaluate(m_lastEvaluationTime, dateTime); - if (valid && rule.statesActive() && rule.timeActive()) { - qCDebug(dcRuleEngine) << "Rule" << rule.id() << "time event triggert and all states match."; + if (valid && rule.timeActive()) { + qCDebug(dcRuleEngine) << "Rule" << rule.id() << "time event triggert."; rules.append(rule); } } diff --git a/libnymea-core/stateevaluator.cpp b/libnymea-core/stateevaluator.cpp index 922259ce..aa9a3321 100644 --- a/libnymea-core/stateevaluator.cpp +++ b/libnymea-core/stateevaluator.cpp @@ -346,6 +346,12 @@ bool StateEvaluator::isValid() const return true; } +/*! Returns true if the StateEvaluator is empty, that is, has no StateDescriptor and no ChildEvaluators */ +bool StateEvaluator::isEmpty() const +{ + return !m_stateDescriptor.isValid() && m_childEvaluators.isEmpty(); +} + /*! Print a StateEvaluator including childEvaluators recuresively to QDebug. */ QDebug operator<<(QDebug dbg, const StateEvaluator &stateEvaluator) { diff --git a/libnymea-core/stateevaluator.h b/libnymea-core/stateevaluator.h index 69a05fcb..273b98b9 100644 --- a/libnymea-core/stateevaluator.h +++ b/libnymea-core/stateevaluator.h @@ -56,6 +56,7 @@ public: static StateEvaluator loadFromSettings(NymeaSettings &settings, const QString &groupPrefix); bool isValid() const; + bool isEmpty() const; private: StateDescriptor m_stateDescriptor; diff --git a/nymea.pri b/nymea.pri index 78996a06..f6356cfa 100644 --- a/nymea.pri +++ b/nymea.pri @@ -6,7 +6,7 @@ NYMEA_PLUGINS_PATH=/usr/lib/$$system('dpkg-architecture -q DEB_HOST_MULTIARCH')/ # define protocol versions JSON_PROTOCOL_VERSION_MAJOR=1 -JSON_PROTOCOL_VERSION_MINOR=6 +JSON_PROTOCOL_VERSION_MINOR=7 REST_API_VERSION=1 DEFINES += NYMEA_VERSION_STRING=\\\"$${NYMEA_VERSION_STRING}\\\" \ diff --git a/tests/auto/api.json b/tests/auto/api.json index 128d3fd4..be5b4be2 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -1,4 +1,4 @@ -1.6 +1.7 { "methods": { "Actions.ExecuteAction": { @@ -658,7 +658,7 @@ } }, "Rules.AddRule": { - "description": "Add a rule. You can describe rules by one or many EventDesciptors and a StateEvaluator. Note that only one of either eventDescriptor or eventDescriptorList may be passed at a time. A rule can be created but left disabled, meaning it won't actually be executed until set to enabled. If not given, enabled defaults to true.", + "description": "Add a rule. You can describe rules by one or many EventDesciptors and a StateEvaluator. Note that only one of either eventDescriptor or eventDescriptorList may be passed at a time. A rule can be created but left disabled, meaning it won't actually be executed until set to enabled. If not given, enabled defaults to true. A rule can have a list of actions and exitActions. It must have at least one Action. For state based rules, actions will be executed when the system enters a state matching the stateDescriptor. The exitActions will be executed when the system leaves the described state again. For event based rules, actions will be executed when a matching event happens and if the stateEvaluator matches the system's state. ExitActions for such rules will be executed when a matching event happens and the stateEvaluator is not matching the system's state. A rule marked as executable can be executed via the API using Rules.ExecuteRule, that means, its actions will be executed regardless of the the eventDescriptor and stateEvaluators.", "params": { "actions": [ "$ref:RuleAction" diff --git a/tests/auto/rules/testrules.cpp b/tests/auto/rules/testrules.cpp index a6b904d5..00d5536a 100644 --- a/tests/auto/rules/testrules.cpp +++ b/tests/auto/rules/testrules.cpp @@ -98,6 +98,8 @@ private slots: void testEventBasedAction(); + void testEventBasedRuleWithExitAction(); + void removePolicyUpdate(); void removePolicyCascade(); @@ -492,8 +494,9 @@ void TestRules::addRemoveRules_data() QTest::newRow("valid rule. enabled, 1 Action, 1 Exit Action, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << QVariantMap() << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << true << "TestRule"; QTest::newRow("valid rule. disabled, 1 Action, 1 Exit Action, 1 StateEvaluator, name") << false << validActionNoParams << validExitActionNoParams << QVariantMap() << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << true << "TestRule"; QTest::newRow("invalid rule. disabled, 1 Action, 1 invalid Exit Action, 1 StateEvaluator, name") << false << validActionNoParams << invalidExitAction << QVariantMap() << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorActionTypeNotFound << false << "TestRule"; - QTest::newRow("invalid rule. 1 Action, 1 Exit Action, 1 EventDescriptor, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << validEventDescriptor1 << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorInvalidRuleFormat << false << "TestRule"; - QTest::newRow("invalid rule. 1 Action, 1 Exit Action, eventDescriptorList, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << QVariantMap() << eventDescriptorList << validStateEvaluator << RuleEngine::RuleErrorInvalidRuleFormat << false << "TestRule"; + QTest::newRow("valid rule. 1 Action, 1 Exit Action, 1 EventDescriptor, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << validEventDescriptor1 << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << false << "TestRule"; + QTest::newRow("invalid rule. 1 Action, 1 Exit Action, eventDescriptorList, NO StateEvaluator, name")<< true << validActionNoParams << validExitActionNoParams << QVariantMap() << eventDescriptorList << QVariantMap() << RuleEngine::RuleErrorInvalidRuleFormat << false << "TestRule"; + QTest::newRow("valid rule. 1 Action, 1 Exit Action, eventDescriptorList, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << QVariantMap() << eventDescriptorList << validStateEvaluator << RuleEngine::RuleErrorNoError << false << "TestRule"; // Rules without exit actions QTest::newRow("valid rule. enabled, 1 EventDescriptor, StateEvaluator, 1 Action, name") << true << validActionNoParams << QVariantMap() << validEventDescriptor1 << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << true << "TestRule"; @@ -732,8 +735,8 @@ void TestRules::editRules_data() // Rules with exit actions QTest::newRow("valid rule. enabled, 1 Action, 1 Exit Action, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << QVariantMap() << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << "TestRule"; QTest::newRow("valid rule. disabled, 1 Action, 1 Exit Action, 1 StateEvaluator, name") << false << validActionNoParams << validExitActionNoParams << QVariantMap() << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << "TestRule"; - QTest::newRow("invalid rule. 1 Action, 1 Exit Action, 1 EventDescriptor, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << validEventDescriptor1 << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorInvalidRuleFormat << "TestRule"; - QTest::newRow("invalid rule. 1 Action, 1 Exit Action, eventDescriptorList, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << QVariantMap() << eventDescriptorList << validStateEvaluator << RuleEngine::RuleErrorInvalidRuleFormat << "TestRule"; + QTest::newRow("valid rule. 1 Action, 1 Exit Action, 1 EventDescriptor, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << validEventDescriptor1 << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << "TestRule"; + QTest::newRow("valid rule. 1 Action, 1 Exit Action, eventDescriptorList, 1 StateEvaluator, name") << true << validActionNoParams << validExitActionNoParams << QVariantMap() << eventDescriptorList << validStateEvaluator << RuleEngine::RuleErrorNoError << "TestRule"; // Rules without exit actions QTest::newRow("valid rule. enabled, 1 EventDescriptor, StateEvaluator, 1 Action, name") << true << validActionNoParams << QVariantMap() << validEventDescriptor1 << QVariantList() << validStateEvaluator << RuleEngine::RuleErrorNoError << "TestRule"; @@ -2083,6 +2086,98 @@ void TestRules::testEventBasedAction() // TODO: check if this action was really executed with the int state value 42 } +void TestRules::testEventBasedRuleWithExitAction() +{ + QNetworkAccessManager nam; + QSignalSpy spy(&nam, SIGNAL(finished(QNetworkReply*))); + + // Init bool state to true + spy.clear(); + QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(m_mockDevice1Port).arg(mockBoolStateId.toString()).arg(true))); + QNetworkReply *reply = nam.get(request); + spy.wait(); + QCOMPARE(spy.count(), 1); + reply->deleteLater(); + + // Add a rule + QVariantMap addRuleParams; + QVariantMap eventDescriptor; + eventDescriptor.insert("eventTypeId", mockEvent1Id); + eventDescriptor.insert("deviceId", m_mockDeviceId); + addRuleParams.insert("eventDescriptors", QVariantList() << eventDescriptor); + addRuleParams.insert("name", "TestRule"); + addRuleParams.insert("enabled", true); + + QVariantMap stateEvaluator; + QVariantMap stateDescriptor; + stateDescriptor.insert("deviceId", m_mockDeviceId); + stateDescriptor.insert("stateTypeId", mockBoolStateId); + stateDescriptor.insert("operator", "ValueOperatorEquals"); + stateDescriptor.insert("value", true); + stateEvaluator.insert("stateDescriptor", stateDescriptor); + stateEvaluator.insert("operator", "StateOperatorAnd"); + addRuleParams.insert("stateEvaluator", stateEvaluator); + + QVariantList actions; + QVariantMap action; + QVariantList ruleActionParams; + QVariantMap param1; + param1.insert("paramTypeId", mockActionParam1ParamTypeId); + param1.insert("value", true); + QVariantMap param2; + param2.insert("paramTypeId", mockActionParam2ParamTypeId); + param2.insert("value", true); + ruleActionParams.append(param1); + ruleActionParams.append(param2); + + action.insert("actionTypeId", mockActionIdNoParams); + action.insert("deviceId", m_mockDeviceId); + actions.append(action); + addRuleParams.insert("actions", actions); + + actions.clear(); + action.insert("actionTypeId", mockActionIdWithParams); + action.insert("ruleActionParams", ruleActionParams); + actions.append(action); + addRuleParams.insert("exitActions", actions); + + qDebug() << addRuleParams; + + QVariant response = injectAndWait("Rules.AddRule", addRuleParams); + verifyRuleError(response); + + // trigger event + spy.clear(); + request = QNetworkRequest(QUrl(QString("http://localhost:%1/generateevent?eventtypeid=%2").arg(m_mockDevice1Port).arg(mockEvent1Id.toString()))); + reply = nam.get(request); + spy.wait(); + QCOMPARE(spy.count(), 1); + reply->deleteLater(); + + // Verify the actions got executed + verifyRuleExecuted(mockActionIdNoParams); + + // set bool state to false + spy.clear(); + request = QNetworkRequest(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(m_mockDevice1Port).arg(mockBoolStateId.toString()).arg(false))); + reply = nam.get(request); + spy.wait(); + QCOMPARE(spy.count(), 1); + reply->deleteLater(); + + // trigger event + spy.clear(); + request = QNetworkRequest(QUrl(QString("http://localhost:%1/generateevent?eventtypeid=%2").arg(m_mockDevice1Port).arg(mockEvent1Id.toString()))); + reply = nam.get(request); + spy.wait(); + QCOMPARE(spy.count(), 1); + reply->deleteLater(); + + // Verify the exit actions got executed + verifyRuleExecuted(mockActionIdNoParams); + +} + void TestRules::removePolicyUpdate() { // ADD parent device