diff --git a/doc/images/Rules_definition.svg b/doc/images/Rules_definition.svg new file mode 100644 index 00000000..4a652ef6 --- /dev/null +++ b/doc/images/Rules_definition.svg @@ -0,0 +1,692 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + State + + + + Event + + + + [>|<|!]= + + + + c + + + + | + + + + & + + + + State + + + + value + + + + State + + + + Action + + + + & + + + + + + + + + + + Trigger path 2 + Trigger path 1 + + + + + + + + + + + | + + + + + + + + + + + diff --git a/doc/rules.qdoc b/doc/rules.qdoc index 010027e2..c63f24f8 100644 --- a/doc/rules.qdoc +++ b/doc/rules.qdoc @@ -3,5 +3,92 @@ \title Rules \annotatedlist rules + + \section1 Introduction + + Rules define all the logic in guh. + A rule is composed by a set of \l{Event}{Events}, \l{State}{States} and \l{Action}{Actions}. + + This diagram shows how rules are composed and interpreted by + the \l{RuleEngine}. + + \image Rules_definition.svg "Rules definition" + + A basic concept of a \l{Rule} is the trigger path. This is the path that defines when a \l{Rule} + is triggered for evaluation. Each element added on a trigger path (either red or blue) has the possibility to trigger the + \l{RuleEngine} to evaluate the complete rule and ultimately execute the defined \l{Action}{Actions} if the + whole rule evaluates to true. Once the trigger path has been left (i.e. a black arrow is followed), + subsequent elements will still be evaluated when the rule is triggered, but they won't trigger the + evaluation themselves. + + There are two mechanisms that can trigger a rule, described by the two trigger paths in the Rules definition figure. + \list + \li State bindings based + \li Event based + \endlist + + \section2 Event based rules + + Event based rules (trigger path 1) contain either one or more \l{Event}{Events} or are triggered by a \l{State} change (disregarding what + the changed State's value actually is). Such rules may still evaluate \l{States}{State} for a certain value (leaving + Trigger path 1 and re-entering Trigger path 2), however, unless + otherwise explicitly defined, the Rule is not evaluated when such a State change happens (given the Trigger path has been + left by crossing a black arrow). Those rules are only executed for items on trigger path 1. + + \section3 Examples + + \list + \li This rule will be evaluated and executed whenever Remote_button1 is pressed: + + \tt {\span {id="color-blue"} {Event}} \tt {\span {id="color-black"} {-> Action}} + + \li This rule will be evaluated and executed whenever Remote_button1 is pressed or or Motion sensor 1 triggers. + + \tt {\span {id="color-blue"} {Event | Event}} \tt {\span {id="color-black"} {-> Action}} + + \li This rule will be evaluated and executed whenever Remote_button1 is pressed or the temperature changes. + + \tt {\span {id="color-blue"} {Event | State}} \tt {\span {id="color-black"} {-> Action}} + \endlist + + \section2 State bindings based rules + + State binding rules (Trigger path 2) are rules which only contains \l{State}{States} and \l{Action}{Actions}. Each time + a \l{State} on that path changes, the rule is evaluated. That means, all the States in the Rule are examined. If all + evaluations are fulfilled, the Rule's \l{Actions}{Action} are executed. Please note, that such Rules may only check + if states are equal to, unequal, less than or greater then some value. + + \section3 Examples + + \list + \li This rule will be evaluated whenever the temperature changes and executed when the temperature equals 10. + + \tt {\span {id="color-red"} {State==10}} \tt {\span {id="color-black"} {-> Action}} + + \li This rule will be evaluated whenever the temperature changes or the Light sensor changes and executed when the temperature is greater 20 and the light sensor reports values greater 10. + + \tt {\span {id="color-blue"} {State >20 & State >10}} \tt {\span {id="color-black"} {-> Action}} + + \li This rule will be evaluated whenever the temperature changes or the Light sensor changes and executed when the temperature is greater 20 or the light sensor reports values greater 10.. + + \tt {\span {id="color-blue"} {State >20 | State >10}} \tt {\span {id="color-black"} {-> Action}} + \endlist + + \section2 Mixing rules types + + Having both types of triggers in a single rule is only possible by leaving the trigger path 1, crossing a black arrow and + re-entering trigger path 2. This however, converts the rule to an Event based rule which means, only the elements + on trigger path 1 will be able to trigger the rule evaluation. + + \section3 Examples + + \list + \li This rule will be evaluated whenever the remote button1 is pressed and executed only if the temperature is greater 10. + + \tt {\span {id="color-blue"} {Event==10}} \tt {\span {id="color-black"} {->}} \tt {\span {id="color-red"} {State >10}} \tt {\span {id="color-black"} {-> Action}} + + \endlist + + */ diff --git a/doc/style.css b/doc/style.css index 18e37f2a..01562ff2 100644 --- a/doc/style.css +++ b/doc/style.css @@ -673,3 +673,6 @@ } } /* end of print media */ + +#color-blue { color: blue } +#color-red { color: red } diff --git a/libguh/device.cpp b/libguh/device.cpp index 4a421fc6..3534c057 100644 --- a/libguh/device.cpp +++ b/libguh/device.cpp @@ -124,6 +124,7 @@ void Device::setStateValue(const QUuid &stateTypeId, const QVariant &value) State newState(stateTypeId, m_id); newState.setValue(value); m_states[i] = newState; + emit stateValueChanged(stateTypeId, value); return; } } diff --git a/libguh/device.h b/libguh/device.h index 662f6f4a..9c822e80 100644 --- a/libguh/device.h +++ b/libguh/device.h @@ -48,6 +48,9 @@ public: QVariant stateValue(const QUuid &stateTypeId) const; void setStateValue(const QUuid &stateTypeId, const QVariant &value); +signals: + void stateValueChanged(const QUuid &stateTypeId, const QVariant &value); + private: Device(const QUuid &pluginId, const QUuid &id, const QUuid &deviceClassId, QObject *parent = 0); Device(const QUuid &pluginId, const QUuid &deviceClassId, QObject *parent = 0); diff --git a/libguh/devicemanager.cpp b/libguh/devicemanager.cpp index 174ea396..a085f1f1 100644 --- a/libguh/devicemanager.cpp +++ b/libguh/devicemanager.cpp @@ -289,6 +289,15 @@ void DeviceManager::storeConfiguredDevices() } } +void DeviceManager::slotDeviceStateValueChanged(const QUuid &stateTypeId, const QVariant &value) +{ + Device *device = qobject_cast(sender()); + if (!device) { + return; + } + emit deviceStateChanged(device, stateTypeId, value); +} + void DeviceManager::radio433SignalReceived(QList rawData) { foreach (Device *device, m_configuredDevices) { @@ -344,5 +353,6 @@ bool DeviceManager::setupDevice(Device *device) return false; } + connect(device, SIGNAL(stateValueChanged(QUuid,QVariant)), this, SLOT(slotDeviceStateValueChanged(QUuid,QVariant))); return true; } diff --git a/libguh/devicemanager.h b/libguh/devicemanager.h index 9e080159..e72bbe17 100644 --- a/libguh/devicemanager.h +++ b/libguh/devicemanager.h @@ -69,6 +69,7 @@ public: signals: void loaded(); void emitEvent(const Event &event); + void deviceStateChanged(Device *device, const QUuid &stateTypeId, const QVariant &value); public slots: DeviceError executeAction(const Action &action); @@ -78,6 +79,9 @@ private slots: void loadConfiguredDevices(); void storeConfiguredDevices(); + // Only connect this to Devices. It will query the sender() + void slotDeviceStateValueChanged(const QUuid &stateTypeId, const QVariant &value); + void radio433SignalReceived(QList rawData); void timerEvent(); diff --git a/plugins/deviceplugins/mock/devicepluginmock.cpp b/plugins/deviceplugins/mock/devicepluginmock.cpp index f8c07020..4986eb06 100644 --- a/plugins/deviceplugins/mock/devicepluginmock.cpp +++ b/plugins/deviceplugins/mock/devicepluginmock.cpp @@ -137,7 +137,13 @@ void DevicePluginMock::executeAction(Device *device, const Action &action) void DevicePluginMock::setState(const QUuid &stateTypeId, const QVariant &value) { - qDebug() << "should set state" << stateTypeId << value; + HttpDaemon *daemon = qobject_cast(sender()); + if (!daemon) { + return; + } + + Device *device = m_daemons.key(daemon); + device->setStateValue(stateTypeId, value); } void DevicePluginMock::triggerEvent(const QUuid &id) diff --git a/server/guhcore.cpp b/server/guhcore.cpp index 635f454a..3edc328d 100644 --- a/server/guhcore.cpp +++ b/server/guhcore.cpp @@ -31,6 +31,7 @@ #include "jsonrpcserver.h" #include "devicemanager.h" #include "ruleengine.h" +#include "device.h" #include @@ -78,15 +79,21 @@ GuhCore::GuhCore(QObject *parent) : qDebug() << "*****************************************"; m_jsonServer = new JsonRPCServer(this); - connect(m_deviceManager, &DeviceManager::emitEvent, this, &GuhCore::gotSignal); + connect(m_deviceManager, &DeviceManager::emitEvent, this, &GuhCore::gotEvent); + connect(m_deviceManager, &DeviceManager::deviceStateChanged, this, &GuhCore::deviceStateChanged); } /*! Connected to the DeviceManager's emitEvent signal. Events received in here will be evaluated by the \l{RuleEngine} and the according \l{Action}{Actions} are executed.*/ -void GuhCore::gotSignal(const Event &event) +void GuhCore::gotEvent(const Event &event) { foreach (const Action &action, m_ruleEngine->evaluateEvent(event)) { m_deviceManager->executeAction(action); } } + +void GuhCore::deviceStateChanged(Device *device, const QUuid &stateTypeId, const QVariant &value) +{ + m_jsonServer->emitStateChangeNotification(device, stateTypeId, value); +} diff --git a/server/guhcore.h b/server/guhcore.h index 3a86c932..669e5702 100644 --- a/server/guhcore.h +++ b/server/guhcore.h @@ -27,6 +27,7 @@ class JsonRPCServer; class DeviceManager; class RuleEngine; +class Device; class GuhCore : public QObject { @@ -46,7 +47,8 @@ private: RuleEngine *m_ruleEngine; private slots: - void gotSignal(const Event &event); + void gotEvent(const Event &event); + void deviceStateChanged(Device *device, const QUuid &stateTypeId, const QVariant &value); }; diff --git a/server/jsonrpc/jsonrpcserver.cpp b/server/jsonrpc/jsonrpcserver.cpp index 573a46c3..740a12fe 100644 --- a/server/jsonrpc/jsonrpcserver.cpp +++ b/server/jsonrpc/jsonrpcserver.cpp @@ -85,6 +85,23 @@ JsonRPCServer::JsonRPCServer(QObject *parent): registerHandler(new RulesHandler(this)); } +void JsonRPCServer::emitStateChangeNotification(Device *device, const QUuid &stateTypeId, const QVariant &value) +{ + QVariantMap notification; + notification.insert("notification", "Device.StateChanged"); + + QVariantMap params; + params.insert("deviceId", device->id()); + params.insert("stateTypeId", stateTypeId); + params.insert("value", value); + + notification.insert("params", params); + + QJsonDocument jsonDoc = QJsonDocument::fromVariant(notification); + + m_tcpServer->sendData(m_clients.keys(true), jsonDoc.toJson()); +} + QString JsonRPCServer::name() const { return QStringLiteral("JSONRPC"); @@ -92,6 +109,8 @@ QString JsonRPCServer::name() const QVariantMap JsonRPCServer::Introspect(const QVariantMap ¶ms) const { + Q_UNUSED(params) + QVariantMap data; data.insert("types", JsonTypes::allTypes()); QVariantMap methods; @@ -105,6 +124,8 @@ QVariantMap JsonRPCServer::Introspect(const QVariantMap ¶ms) const QVariantMap JsonRPCServer::Version(const QVariantMap ¶ms) const { + Q_UNUSED(params) + QVariantMap data; data.insert("version", "0.0.0"); return data; @@ -137,12 +158,14 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &jsonDat int commandId = message.value("id").toInt(&success); if (!success) { qWarning() << "Error parsing command. Missing \"id\":" << jsonData; + sendErrorResponse(clientId, commandId, "Error parsing command. Missing 'id'"); return; } QStringList commandList = message.value("method").toString().split('.'); if (commandList.count() != 2) { qWarning() << "Error parsing method.\nGot:" << message.value("method").toString() << "\nExpected: \"Namespace.method\""; + sendErrorResponse(clientId, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString())); return; } diff --git a/server/jsonrpc/jsonrpcserver.h b/server/jsonrpc/jsonrpcserver.h index ef0974fe..af2b6206 100644 --- a/server/jsonrpc/jsonrpcserver.h +++ b/server/jsonrpc/jsonrpcserver.h @@ -41,6 +41,8 @@ class JsonRPCServer: public JsonHandler public: JsonRPCServer(QObject *parent = 0); + void emitStateChangeNotification(Device *device, const QUuid &stateTypeId, const QVariant &value); + // JsonHandler API implementation QString name() const; Q_INVOKABLE QVariantMap Introspect(const QVariantMap ¶ms) const; diff --git a/tests/auto/mocktcpserver.cpp b/tests/auto/mocktcpserver.cpp index afdda221..172b4be3 100644 --- a/tests/auto/mocktcpserver.cpp +++ b/tests/auto/mocktcpserver.cpp @@ -18,6 +18,8 @@ #include "mocktcpserver.h" +#include + QList MockTcpServer::s_allServers; MockTcpServer::MockTcpServer(QObject *parent): @@ -36,6 +38,13 @@ void MockTcpServer::sendData(const QUuid &clientId, const QByteArray &data) emit outgoingData(clientId, data); } +void MockTcpServer::sendData(const QList &clients, const QByteArray &data) +{ + foreach (const QUuid &clientId, clients) { + sendData(clientId, data); + } +} + QList MockTcpServer::servers() { return s_allServers; diff --git a/tests/auto/mocktcpserver.h b/tests/auto/mocktcpserver.h index 1f85aba5..cd68c27a 100644 --- a/tests/auto/mocktcpserver.h +++ b/tests/auto/mocktcpserver.h @@ -31,6 +31,7 @@ public: ~MockTcpServer(); void sendData(const QUuid &clientId, const QByteArray &data); + void sendData(const QList &clients, const QByteArray &data); /************** Used for testing **************************/ static QList servers(); diff --git a/tests/scripts/addrule.sh b/tests/scripts/addrule.sh index 7d6aab7e..3ed30ae4 100755 --- a/tests/scripts/addrule.sh +++ b/tests/scripts/addrule.sh @@ -3,6 +3,7 @@ if test -z $5; then echo "usage: $0 host sourceDevice eventTypeId targetDeviceId actionTypeId" else - (echo '{"id":1, "method":"Rules.AddRule", "params":{"event": {"eventTypeId": "'$3'", "deviceId":"'$2'"}, "actions": [ { "deviceId":"'$4'", "actionTypeId":"'$5'", "params":{"power":"true"}}]}}'; sleep 1) | nc $1 1234 + (echo '{"id":1, "method":"Rules.AddRule", "params":{"event": {"eventTypeId": "$3", "deviceId":"'$2'"}, "actions": [ { "deviceId":"'$4'", "actionTypeId":"'$5'", "params":{"power":"true"}}]}}'; sleep 1) | nc $1 1234 +# (echo '{"id":1, "method":"Rules.AddRule", "params":{"event": {"eventTypeId": "'$3'", "deviceId":"'$2'"}, "actions": [ { "deviceId":"'$4'", "actionTypeId":"'$5'", "params":{"power":"true"}}]}}'; sleep 1) | nc $1 1234 # (echo '{"id":1, "method":"Rules.AddRule", "params":{"event": {"eventTypeId": "'$2'", "deviceId":"'$3'", "params":{"power":"false"}}, "actions": [ { "deviceId":"'$4'", "name":"rule 1", "params":{"power":"false"}},{ "deviceId":"'$5'", "name":"rule 1", "params":{"power":"true"}}]}}'; sleep 1) | nc $1 1234 fi