From 0c09e07112ffac9c6a64f679709ed3338cd2612e Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 24 May 2023 14:31:42 +0200 Subject: [PATCH] Add support for connection to actionExecuted in scripts --- libnymea-core/scriptengine/scriptaction.cpp | 29 ++++++++++++++++ libnymea-core/scriptengine/scriptaction.h | 5 +++ libnymea-core/scriptengine/scriptengine.cpp | 3 ++ libnymea-core/scriptengine/scriptthing.cpp | 16 +++++++++ libnymea-core/scriptengine/scriptthing.h | 3 ++ libnymea/types/action.h | 2 ++ tests/auto/scripts/testhelper.cpp | 5 +++ tests/auto/scripts/testhelper.h | 6 +++- tests/auto/scripts/testscripts.cpp | 37 ++++++++++++++++++++- 9 files changed, 104 insertions(+), 2 deletions(-) diff --git a/libnymea-core/scriptengine/scriptaction.cpp b/libnymea-core/scriptengine/scriptaction.cpp index 63cf5de3..804dd14e 100644 --- a/libnymea-core/scriptengine/scriptaction.cpp +++ b/libnymea-core/scriptengine/scriptaction.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include Q_DECLARE_LOGGING_CATEGORY(dcScriptEngine) @@ -56,6 +57,34 @@ void ScriptAction::classBegin() m_scriptId = qmlEngine(this)->contextForObject(this)->contextProperty("scriptId").toUuid(); m_logger = qmlEngine(this)->contextForObject(this)->contextProperty("logger").value(); + connect(m_thingManager, &ThingManager::actionExecuted, this, [=](const Action &action, Thing::ThingError status){ + if (ThingId(m_thingId) != action.thingId()) { + return; + } + + Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_thingId)); + if (!thing) { + return; + } + + ActionTypeId ourActionTypeId = ActionTypeId(m_actionTypeId); + if (ourActionTypeId.isNull()) { + ourActionTypeId = thing->thingClass().actionTypes().findByName(m_actionName).id(); + } + if (ourActionTypeId.isNull() || action.actionTypeId() != ourActionTypeId) { + return; + } + + QVariantMap params; + foreach (const Param ¶m, action.params()) { + params.insert(param.paramTypeId().toString().remove(QRegExp("[{}]")), param.value().toByteArray()); + QString paramName = thing->thingClass().actionTypes().findById(action.actionTypeId()).paramTypes().findById(param.paramTypeId()).name(); + params.insert(paramName, param.value().toByteArray()); + } + + // Note: Explicitly convert the params to a Json document because auto-casting from QVariantMap to the JS engine might drop some values. + emit executed(QJsonDocument::fromVariant(params).toVariant().toMap(), status, action.triggeredBy()); + }); } void ScriptAction::componentComplete() diff --git a/libnymea-core/scriptengine/scriptaction.h b/libnymea-core/scriptengine/scriptaction.h index eeada302..202f192a 100644 --- a/libnymea-core/scriptengine/scriptaction.h +++ b/libnymea-core/scriptengine/scriptaction.h @@ -36,6 +36,9 @@ #include #include +#include "integrations/thing.h" +#include "types/action.h" + class Logger; class ThingManager; @@ -77,6 +80,8 @@ signals: void actionTypeIdChanged(); void actionNameChanged(); + void executed(const QVariantMap ¶ms, Thing::ThingError status, Action::TriggeredBy triggeredBy); + public: ThingManager *m_thingManager = nullptr; Logger *m_logger = nullptr; diff --git a/libnymea-core/scriptengine/scriptengine.cpp b/libnymea-core/scriptengine/scriptengine.cpp index 6401f548..d0e1921b 100644 --- a/libnymea-core/scriptengine/scriptengine.cpp +++ b/libnymea-core/scriptengine/scriptengine.cpp @@ -40,6 +40,7 @@ #include "scriptinterfaceevent.h" #include "scriptthing.h" #include "scriptthings.h" +#include "types/action.h" #include "nymeasettings.h" #include "logging/logengine.h" @@ -76,6 +77,8 @@ ScriptEngine::ScriptEngine(ThingManager *thingManager, LogEngine *logEngine, QOb qmlRegisterType("nymea", 1, 0, "Alarm"); qmlRegisterType("nymea", 1, 0, "Thing"); qmlRegisterType("nymea", 1, 0, "Things"); + qmlRegisterUncreatableType("nymea", 1, 0, "Action", "Cannot create Actions. Use ThingAction instead."); + m_logger = logEngine->registerLogSource("scripts", {"id", "event"}); diff --git a/libnymea-core/scriptengine/scriptthing.cpp b/libnymea-core/scriptengine/scriptthing.cpp index 0d5edfe6..d85d320a 100644 --- a/libnymea-core/scriptengine/scriptthing.cpp +++ b/libnymea-core/scriptengine/scriptthing.cpp @@ -170,6 +170,22 @@ void ScriptThing::init(ThingManager *thingManager) // Note: Explicitly convert the params to a Json document because auto-casting from QVariantMap to the JS engine might drop some values. emit eventTriggered(thing->thingClass().eventTypes().findById(event.eventTypeId()).name(), QJsonDocument::fromVariant(params).toVariant().toMap()); }); + connect(m_thingManager, &ThingManager::actionExecuted, this, [=](const Action &action, Thing::ThingError status){ + if (m_thingId != action.thingId()) { + return; + } + + Thing *thing = m_thingManager->findConfiguredThing(action.thingId()); + QVariantMap params; + foreach (const Param ¶m, action.params()) { + params.insert(param.paramTypeId().toString().remove(QRegExp("[{}]")), param.value().toByteArray()); + QString paramName = thing->thingClass().actionTypes().findById(action.actionTypeId()).paramTypes().findById(param.paramTypeId()).name(); + params.insert(paramName, param.value().toByteArray()); + } + + // Note: Explicitly convert the params to a Json document because auto-casting from QVariantMap to the JS engine might drop some values. + emit actionExecuted(thing->thingClass().actionTypes().findById(action.actionTypeId()).name(), QJsonDocument::fromVariant(params).toVariant().toMap(), status, action.triggeredBy()); + }); } void ScriptThing::connectToThing() diff --git a/libnymea-core/scriptengine/scriptthing.h b/libnymea-core/scriptengine/scriptthing.h index 2f1ef79c..cd5f5558 100644 --- a/libnymea-core/scriptengine/scriptthing.h +++ b/libnymea-core/scriptengine/scriptthing.h @@ -45,6 +45,8 @@ class ScriptThing : public QObject, public QQmlParserStatus Q_PROPERTY(QString thingId READ thingId WRITE setThingId NOTIFY thingIdChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) public: + Q_ENUM(Thing::ThingError) + explicit ScriptThing(QObject *parent = nullptr); explicit ScriptThing(ThingManager *thingManager, QObject *parent = nullptr); void classBegin() override; @@ -66,6 +68,7 @@ signals: void stateValueChanged(const QString &stateName, const QVariant &value); void eventTriggered(const QString &eventName, const QVariantMap ¶ms); + void actionExecuted(const QString &actionName, const QVariantMap ¶ms, Thing::ThingError status, Action::TriggeredBy triggeredBy); private slots: void init(ThingManager *thingManager); diff --git a/libnymea/types/action.h b/libnymea/types/action.h index df9dc82b..eb499e80 100644 --- a/libnymea/types/action.h +++ b/libnymea/types/action.h @@ -77,4 +77,6 @@ private: TriggeredBy m_triggeredBy = TriggeredByUser; }; +Q_DECLARE_METATYPE(Action::TriggeredBy) + #endif // ACTION_H diff --git a/tests/auto/scripts/testhelper.cpp b/tests/auto/scripts/testhelper.cpp index fcf86ab9..23250a3d 100644 --- a/tests/auto/scripts/testhelper.cpp +++ b/tests/auto/scripts/testhelper.cpp @@ -50,6 +50,11 @@ void TestHelper::logStateChange(const QString &thingId, const QString &stateId, emit stateChangeLogged(ThingId(thingId), stateId, value); } +void TestHelper::logActionExecuted(const QString &thingId, const QString &actionId, const QVariantMap ¶ms, Thing::ThingError status, Action::TriggeredBy triggeredBy) +{ + emit actionExecutionLogged(ThingId(thingId), actionId, params, status, triggeredBy); +} + void TestHelper::setTestResult(bool success) { emit testResult(success); diff --git a/tests/auto/scripts/testhelper.h b/tests/auto/scripts/testhelper.h index 1f113e00..fe5cf331 100644 --- a/tests/auto/scripts/testhelper.h +++ b/tests/auto/scripts/testhelper.h @@ -24,6 +24,8 @@ #include #include "typeutils.h" +#include "integrations/thing.h" +#include "types/action.h" class TestHelper : public QObject { @@ -33,6 +35,7 @@ public: Q_INVOKABLE void logEvent(const QString &thingId, const QString &eventId, const QVariantMap ¶ms); Q_INVOKABLE void logStateChange(const QString &thingId, const QString &stateId, const QVariant &value); + Q_INVOKABLE void logActionExecuted(const QString &thingId, const QString &actionId, const QVariantMap ¶ms, Thing::ThingError status, Action::TriggeredBy triggeredBy); Q_INVOKABLE void setTestResult(bool success); @@ -41,7 +44,8 @@ signals: void executeAction(const QVariantMap ¶ms); void eventLogged(const ThingId &thingId, const QString &eventId, const QVariantMap ¶ms); - void stateChangeLogged(const ThingId &thingId, const QString stateId, const QVariant &value); + void stateChangeLogged(const ThingId &thingId, const QString &stateId, const QVariant &value); + void actionExecutionLogged(const ThingId &thingId, const QString &actionId, const QVariantMap ¶ms, Thing::ThingError status, Action::TriggeredBy triggeredBy); void testResult(bool success); diff --git a/tests/auto/scripts/testscripts.cpp b/tests/auto/scripts/testscripts.cpp index ae527573..f8c240e6 100644 --- a/tests/auto/scripts/testscripts.cpp +++ b/tests/auto/scripts/testscripts.cpp @@ -101,7 +101,9 @@ void TestScripts::init() // Set initial state values of mock device Action action(mockPowerActionTypeId, m_mockThingId); action.setParams(ParamList() << Param(mockPowerActionPowerParamTypeId, false)); - NymeaCore::instance()->thingManager()->executeAction(action); + ThingActionInfo *info = NymeaCore::instance()->thingManager()->executeAction(action); + QSignalSpy spy(info, &ThingActionInfo::finished); + spy.wait(); } void TestScripts::testScriptEventById() @@ -336,6 +338,9 @@ void TestScripts::testScriptActionById() " id: thingAction\n" " thingId: \"%1\"\n" " actionTypeId: \"%2\"\n" + " onExecuted: {\n" + " TestHelper.logActionExecuted(\"%1\", \"%2\", params, status, triggeredBy)\n" + " }\n" " }\n" " Connections {\n" " target: TestHelper\n" @@ -350,6 +355,7 @@ void TestScripts::testScriptActionById() QCOMPARE(reply.scriptError, ScriptEngine::ScriptErrorNoError); QSignalSpy spy(NymeaCore::instance()->thingManager(), &ThingManager::thingStateChanged); + QSignalSpy actionExecutedSpy(TestHelper::instance(), &TestHelper::actionExecutionLogged); QVariantMap params; params.insert(mockPowerActionPowerParamTypeId.toString(), true); @@ -361,6 +367,13 @@ void TestScripts::testScriptActionById() QCOMPARE(spy.first().at(0).value()->id(), m_mockThingId); QCOMPARE(spy.first().at(1).value(), mockPowerStateTypeId); QCOMPARE(spy.first().at(2).toBool(), true); + + QCOMPARE(actionExecutedSpy.count(), 1); + QCOMPARE(actionExecutedSpy.first().at(0).value(), m_mockThingId); + QCOMPARE(ActionTypeId(actionExecutedSpy.first().at(1).toString()), mockPowerActionTypeId); + QCOMPARE(actionExecutedSpy.first().at(2).toMap().value(mockPowerActionTypeId.toString().remove(QRegExp("[{}]"))).toBool(), true); + QCOMPARE(actionExecutedSpy.first().at(3).value(), Thing::ThingErrorNoError); + QCOMPARE(actionExecutedSpy.first().at(4).value(), Action::TriggeredByScript); } void TestScripts::testScriptActionByName() @@ -372,6 +385,9 @@ void TestScripts::testScriptActionByName() " id: thingAction\n" " thingId: \"%1\"\n" " actionName: \"%2\"\n" + " onExecuted: {\n" + " TestHelper.logActionExecuted(\"%1\", \"%2\", params, status, triggeredBy)\n" + " }\n" " }\n" " Connections {\n" " target: TestHelper\n" @@ -386,6 +402,7 @@ void TestScripts::testScriptActionByName() QCOMPARE(reply.scriptError, ScriptEngine::ScriptErrorNoError); QSignalSpy spy(NymeaCore::instance()->thingManager(), &ThingManager::thingStateChanged); + QSignalSpy actionExecutedSpy(TestHelper::instance(), &TestHelper::actionExecutionLogged); QVariantMap params; params.insert("power", true); @@ -397,6 +414,13 @@ void TestScripts::testScriptActionByName() QCOMPARE(spy.first().at(0).value()->id(), m_mockThingId); QCOMPARE(spy.first().at(1).value(), mockPowerStateTypeId); QCOMPARE(spy.first().at(2).toBool(), true); + + QCOMPARE(actionExecutedSpy.count(), 1); + QCOMPARE(actionExecutedSpy.first().at(0).value(), m_mockThingId); + QCOMPARE(actionExecutedSpy.first().at(1).toString(), "power"); + QCOMPARE(actionExecutedSpy.first().at(2).toMap().value("power").toBool(), true); + QCOMPARE(actionExecutedSpy.first().at(3).value(), Thing::ThingErrorNoError); + QCOMPARE(actionExecutedSpy.first().at(4).value(), Action::TriggeredByScript); } void TestScripts::testScriptAlarm_data() @@ -535,6 +559,9 @@ void TestScripts::testScriptThingAction() " Thing {\n" " id: thing\n" " thingId: \"%1\"\n" + " onActionExecuted: {\n" + " TestHelper.logActionExecuted(\"%1\", actionName, params, status, triggeredBy)\n" + " }\n" " }\n" " Connections {\n" " target: TestHelper\n" @@ -549,6 +576,7 @@ void TestScripts::testScriptThingAction() QCOMPARE(reply.scriptError, ScriptEngine::ScriptErrorNoError); QSignalSpy spy(NymeaCore::instance()->thingManager(), &ThingManager::thingStateChanged); + QSignalSpy actionExecutedSpy(TestHelper::instance(), &TestHelper::actionExecutionLogged); QVariantMap params; params.insert("power", true); @@ -561,6 +589,13 @@ void TestScripts::testScriptThingAction() QCOMPARE(spy.first().at(1).value(), mockPowerStateTypeId); QCOMPARE(spy.first().at(2).toBool(), true); + QCOMPARE(actionExecutedSpy.count(), 1); + QCOMPARE(actionExecutedSpy.first().at(0).value(), m_mockThingId); + QCOMPARE(actionExecutedSpy.first().at(1).toString(), "power"); + QCOMPARE(actionExecutedSpy.first().at(2).toMap().value("power").toBool(), true); + QCOMPARE(actionExecutedSpy.first().at(3).value(), Thing::ThingErrorNoError); + QCOMPARE(actionExecutedSpy.first().at(4).value(), Action::TriggeredByScript); + } void TestScripts::testScriptThingReadState()