From 9f856f3ce9395193d623cec5ece1643364fdc226 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 5 Apr 2020 11:53:30 +0200 Subject: [PATCH 1/5] Add support for generic IO connections --- debian/changelog | 4 + libnymea-core/cloud/cloudnotifications.cpp | 2 +- .../thingmanagerimplementation.cpp | 267 ++++++++++++++++++ .../integrations/thingmanagerimplementation.h | 11 + libnymea-core/jsonrpc/devicehandler.cpp | 1 + libnymea-core/jsonrpc/integrationshandler.cpp | 71 +++++ libnymea-core/jsonrpc/integrationshandler.h | 6 + libnymea-core/nymeacore.cpp | 8 +- libnymea-core/ruleengine/ruleaction.cpp | 8 - libnymea-core/ruleengine/ruleaction.h | 2 - libnymea-core/ruleengine/ruleengine.cpp | 6 +- libnymea/integrations/ioconnection.cpp | 51 ++++ libnymea/integrations/ioconnection.h | 55 ++++ libnymea/integrations/pluginmetadata.cpp | 62 ++++ libnymea/integrations/thingmanager.cpp | 6 + libnymea/integrations/thingmanager.h | 11 + libnymea/interfaces/temperaturesensor.json | 3 +- libnymea/libnymea.pro | 2 + libnymea/nymeasettings.cpp | 3 + libnymea/nymeasettings.h | 1 + libnymea/types/action.cpp | 9 - libnymea/types/action.h | 3 - libnymea/types/browseraction.cpp | 10 +- libnymea/types/browseraction.h | 3 - libnymea/types/browseritemaction.cpp | 10 +- libnymea/types/browseritemaction.h | 3 - libnymea/types/event.cpp | 11 +- libnymea/types/event.h | 3 - libnymea/types/state.cpp | 7 - libnymea/types/state.h | 3 - libnymea/types/statetype.cpp | 36 ++- libnymea/types/statetype.h | 10 + libnymea/typeutils.h | 13 +- nymea.pro | 2 +- plugins/mock/extern-plugininfo.h | 49 ++++ plugins/mock/integrationpluginmock.cpp | 73 ++++- plugins/mock/integrationpluginmock.json | 162 +++++++++++ plugins/mock/plugininfo.h | 196 +++++++++++++ tests/auto/api.json | 62 +++- tests/auto/integrations/testintegrations.cpp | 4 +- 40 files changed, 1160 insertions(+), 89 deletions(-) create mode 100644 libnymea/integrations/ioconnection.cpp create mode 100644 libnymea/integrations/ioconnection.h diff --git a/debian/changelog b/debian/changelog index 0e799ac4..17d522f2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,7 @@ +nymea (0.21.0) UNRELEASED; urgency=medium + + -- Michael Zanetti Thu, 09 Apr 2020 18:19:52 +0200 + nymea (0.20.0) xenial; urgency=medium [ Michael Zanetti ] diff --git a/libnymea-core/cloud/cloudnotifications.cpp b/libnymea-core/cloud/cloudnotifications.cpp index 3dd581a0..38a426e5 100644 --- a/libnymea-core/cloud/cloudnotifications.cpp +++ b/libnymea-core/cloud/cloudnotifications.cpp @@ -168,7 +168,7 @@ void CloudNotifications::startMonitoringAutoThings() void CloudNotifications::executeAction(ThingActionInfo *info) { - qCDebug(dcCloud()) << "executeAction" << info->thing() << info->action().id() << info->action().params(); + qCDebug(dcCloud()) << "executeAction" << info->thing() << info->action().params(); QString userId = info->thing()->paramValue(cloudNotificationsThingClassUserParamId).toString(); QString endpointId = info->thing()->paramValue(cloudNotificationsThingClassEndpointParamId).toString(); int id = m_awsConnector->sendPushNotification(userId, endpointId, info->action().param(notifyActionParamTitleId).value().toString(), info->action().param(notifyActionParamBodyId).value().toString()); diff --git a/libnymea-core/integrations/thingmanagerimplementation.cpp b/libnymea-core/integrations/thingmanagerimplementation.cpp index 4f0d4380..8bba515a 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -760,6 +760,13 @@ Thing::ThingError ThingManagerImplementation::removeConfiguredThing(const ThingI NymeaSettings stateCache(NymeaSettings::SettingsRoleThingStates); stateCache.remove(thingId.toString()); + foreach (const IOConnectionId &ioConnectionId, m_ioConnections.keys()) { + IOConnection ioConnection = m_ioConnections.value(ioConnectionId); + if (ioConnection.inputThingId() == thing->id() || ioConnection.outputThingId() == thing->id()) { + disconnectIO(ioConnectionId); + } + } + emit thingRemoved(thingId); return Thing::ThingErrorNoError; @@ -907,6 +914,108 @@ BrowserItemActionInfo* ThingManagerImplementation::executeBrowserItemAction(cons return info; } +IOConnections ThingManagerImplementation::ioConnections(const ThingId &thingId) const +{ + if (thingId.isNull()) { + return m_ioConnections.values(); + } + IOConnections ioConnections; + foreach (const IOConnection &ioConnection, m_ioConnections) { + if (ioConnection.inputThingId() == thingId || ioConnection.outputThingId() == thingId) { + ioConnections.append(ioConnection); + } + } + return ioConnections; +} + +Thing::ThingError ThingManagerImplementation::connectIO(const IOConnection &connection) +{ + // Do some sanity checks + Thing *inputThing = m_configuredThings.value(connection.inputThingId()); + if (!inputThing) { + qCWarning(dcThingManager()) << "Could not find inputThing" << connection.inputThingId() << "configured things. Not adding IO connection."; + return Thing::ThingErrorThingNotFound; + } + if (!inputThing->thingClass().stateTypes().contains(connection.inputStateTypeId())) { + qCWarning(dcThingManager()) << "Input thing" << inputThing->name() << "does not have a state with id" << connection.inputStateTypeId(); + return Thing::ThingErrorStateTypeNotFound; + } + StateType inputStateType = inputThing->thingClass().stateTypes().findById(connection.inputStateTypeId()); + + // Check if this is actually an input + if (inputStateType.ioType() != Types::IOTypeDigitalInput && inputStateType.ioType() != Types::IOTypeAnalogInput) { + qCWarning(dcThingManager()) << "The given input state is neither a digital nor an analog input."; + return Thing::ThingErrorInvalidParameter; + } + + Thing *outputThing = m_configuredThings.value(connection.outputThingId()); + if (!outputThing) { + qCWarning(dcThingManager()) << "Could not find outputThing" << connection.outputThingId() << "configured things. Not adding IO connection."; + return Thing::ThingErrorThingNotFound; + } + if (!outputThing->thingClass().stateTypes().contains(connection.outputStateTypeId())) { + qCWarning(dcThingManager()) << "Output thing" << outputThing->name() << "does not have a state with id" << connection.outputStateTypeId(); + return Thing::ThingErrorStateTypeNotFound; + } + StateType outputStateType = outputThing->thingClass().stateTypes().findById(connection.outputStateTypeId()); + + // Check if this is actually an output + if (outputStateType.ioType() != Types::IOTypeDigitalOutput && outputStateType.ioType() != Types::IOTypeAnalogOutput) { + qCWarning(dcThingManager()) << "The given output state is neither a digital nor an analog output."; + return Thing::ThingErrorInvalidParameter; + } + + // Check if io types are compatible + if (inputStateType.ioType() == Types::IOTypeDigitalInput && outputStateType.ioType() != Types::IOTypeDigitalOutput) { + qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType(); + return Thing::ThingErrorInvalidParameter; + } + if (inputStateType.ioType() == Types::IOTypeAnalogInput && outputStateType.ioType() != Types::IOTypeAnalogOutput) { + qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType(); + return Thing::ThingErrorInvalidParameter; + } + + // Check if either input or output is already connected + foreach (const IOConnectionId &id, m_ioConnections.keys()) { + if (m_ioConnections.value(id).inputThingId() == connection.inputThingId() && m_ioConnections.value(id).inputStateTypeId() == connection.inputStateTypeId()) { + qCDebug(dcThingManager()).nospace() << "Thing " << inputThing->name() << " already has an IO connection on " << inputStateType.displayName() << ". Replacing old connection."; + disconnectIO(id); + continue; + } + if (m_ioConnections.value(id).outputThingId() == connection.outputThingId() && m_ioConnections.value(id).outputStateTypeId() == connection.outputStateTypeId()) { + qCDebug(dcThingManager()).nospace() << "Thing " << inputThing->name() << " already has an IO connection on " << inputStateType.displayName() << ". Replacing old connection."; + disconnectIO(id); + } + } + + // Finally add the connection + m_ioConnections.insert(connection.id(), connection); + + storeIOConnections(); + + emit ioConnectionAdded(connection); + return Thing::ThingErrorNoError; +} + +Thing::ThingError ThingManagerImplementation::disconnectIO(const IOConnectionId &ioConnectionId) +{ + if (!m_ioConnections.contains(ioConnectionId)) { + qCWarning(dcThingManager()) << "IO connection" << ioConnectionId << "not found. Cannot disconnect."; + return Thing::ThingErrorItemNotFound; + } + m_ioConnections.remove(ioConnectionId); + + NymeaSettings settings(NymeaSettings::SettingsRoleIOConnections); + settings.beginGroup("IOConnections"); + settings.remove(ioConnectionId.toString()); + settings.endGroup(); + + qCDebug(dcThingManager()) << "IO connection disconnected:" << ioConnectionId; + + emit ioConnectionRemoved(ioConnectionId); + return Thing::ThingErrorNoError; +} + QString ThingManagerImplementation::translate(const PluginId &pluginId, const QString &string, const QLocale &locale) { return m_translator->translate(pluginId, string, locale); @@ -1423,6 +1532,8 @@ void ThingManagerImplementation::loadConfiguredThings() postSetupThing(info->thing()); }); } + + loadIOConnections(); } void ThingManagerImplementation::storeConfiguredThings() @@ -1611,6 +1722,115 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s Param valueParam(ParamTypeId(stateTypeId.toString()), value); Event event(EventTypeId(stateTypeId.toString()), thing->id(), ParamList() << valueParam, true); emit eventTriggered(event); + + foreach (const IOConnection &ioConnection, m_ioConnections) { + // Check if this state is an input to an IO connection. + if (ioConnection.inputThingId() == thing->id() && ioConnection.inputStateTypeId() == stateTypeId) { + Thing *outputThing = m_configuredThings.value(ioConnection.outputThingId()); + if (!outputThing) { + qCWarning(dcThingManager()) << "IO connection contains invalid output thing!"; + continue; + } + IntegrationPlugin *plugin = m_integrationPlugins.value(outputThing->pluginId()); + if (!plugin) { + qCWarning(dcThingManager()) << "Plugin not found for IO connection's output action."; + continue; + } + StateType inputStateType = thing->thingClass().getStateType(stateTypeId); + + StateType outputStateType = outputThing->thingClass().getStateType(ioConnection.outputStateTypeId()); + if (outputStateType.id().isNull()) { + qCWarning(dcThingManager()) << "Could not find output state type for IO connection."; + continue; + } + QVariant outputValue; + if (outputStateType.ioType() == Types::IOTypeDigitalOutput) { + // Digital IOs are mapped as-is + outputValue = value; + + // We're already in sync! Skipping action. + if (outputThing->stateValue(outputStateType.id()) == outputValue) { + continue; + } + } else { + // Analog IOs are mapped within the according min/max ranges + outputValue = mapValue(value, inputStateType, outputStateType); + + // We're already in sync (fuzzy, good enough)! Skipping action. + if (qFuzzyCompare(outputThing->stateValue(outputStateType.id()).toDouble(), outputValue.toDouble())) { + continue; + } + } + Action outputAction(ActionTypeId(ioConnection.outputStateTypeId()), ioConnection.outputThingId()); + + Param outputParam(ioConnection.outputStateTypeId(), outputValue); + outputAction.setParams(ParamList() << outputParam); + qCDebug(dcThingManager()) << "Executing IO connection action on" << outputThing->name() << outputParam; + ThingActionInfo* info = executeAction(outputAction); + connect(info, &ThingActionInfo::finished, this, [=](){ + if (info->status() != Thing::ThingErrorNoError) { + // An error happened... let's switch the input back to be in sync with the output + qCWarning(dcThingManager()) << "Error syncing IO connection state. Reverting input back to old value."; + if (inputStateType.ioType() == Types::IOTypeDigitalInput) { + thing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id())); + } else { + thing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType)); + } + } + }); + } + + // Now check if this is an output state type and - if possible - update the inputs for bidirectional connections + if (ioConnection.outputThingId() == thing->id() && ioConnection.outputStateTypeId() == stateTypeId) { + Thing *inputThing = m_configuredThings.value(ioConnection.inputThingId()); + if (!inputThing) { + qCWarning(dcThingManager()) << "IO connection contains invalid input thing!"; + continue; + } + IntegrationPlugin *plugin = m_integrationPlugins.value(inputThing->pluginId()); + if (!plugin) { + qCWarning(dcThingManager()) << "Plugin not found for IO connection's input action."; + continue; + } + StateType outputStateType = thing->thingClass().getStateType(stateTypeId); + + StateType inputStateType = inputThing->thingClass().getStateType(ioConnection.inputStateTypeId()); + if (inputStateType.id().isNull()) { + qCWarning(dcThingManager()) << "Could not find input state type for IO connection."; + continue; + } + + if (!inputStateType.writable()) { + qCDebug(dcThingManager()) << "Input state is not writable. This connection is unidirectional."; + continue; + } + + QVariant inputValue; + if (inputStateType.ioType() == Types::IOTypeDigitalInput) { + // Digital IOs are mapped as-is + inputValue = value; + + // Prevent looping + if (inputThing->stateValue(inputStateType.id()) == inputValue) { + continue; + } + } else { + // Analog IOs are mapped within the according min/max ranges + inputValue = mapValue(value, outputStateType, inputStateType); + + // Prevent looping even if the above calculation has rounding errors... Just skip this action if we're close enough already + if (qFuzzyCompare(inputThing->stateValue(inputStateType.id()).toDouble(), inputValue.toDouble())) { + continue; + } + } + Action inputAction(ActionTypeId(ioConnection.inputStateTypeId()), ioConnection.inputThingId()); + + Param inputParam(ioConnection.inputStateTypeId(), inputValue); + inputAction.setParams(ParamList() << inputParam); + qCDebug(dcThingManager()) << "Executing reverse IO connection action on" << inputThing->name() << inputParam; + executeAction(inputAction); + } + } } void ThingManagerImplementation::slotThingSettingChanged(const ParamTypeId ¶mTypeId, const QVariant &value) @@ -1756,6 +1976,53 @@ void ThingManagerImplementation::loadThingStates(Thing *thing) settings.endGroup(); } +void ThingManagerImplementation::storeIOConnections() +{ + NymeaSettings connectionSettings(NymeaSettings::SettingsRoleIOConnections); + connectionSettings.beginGroup("IOConnections"); + foreach (const IOConnection &ioConnection, m_ioConnections) { + connectionSettings.beginGroup(ioConnection.id().toString()); + + connectionSettings.setValue("inputThingId", ioConnection.inputThingId().toString()); + connectionSettings.setValue("inputStateTypeId", ioConnection.inputStateTypeId().toString()); + connectionSettings.setValue("outputThingId", ioConnection.outputThingId().toString()); + connectionSettings.setValue("outputStateTypeId", ioConnection.outputStateTypeId().toString()); + + connectionSettings.endGroup(); + } + connectionSettings.endGroup(); +} + +void ThingManagerImplementation::loadIOConnections() +{ + NymeaSettings connectionSettings(NymeaSettings::SettingsRoleIOConnections); + connectionSettings.beginGroup("IOConnections"); + foreach (const QString &idString, connectionSettings.childGroups()) { + connectionSettings.beginGroup(idString); + IOConnectionId id(idString); + ThingId inputThingId = connectionSettings.value("inputThingId").toUuid(); + StateTypeId inputStateTypeId = connectionSettings.value("inputStateTypeId").toUuid(); + ThingId outputThingId = connectionSettings.value("outputThingId").toUuid(); + StateTypeId outputStateTypeId = connectionSettings.value("outputStateTypeId").toUuid(); + IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId); + m_ioConnections.insert(id, ioConnection); + connectionSettings.endGroup(); + } + connectionSettings.endGroup(); +} + +QVariant ThingManagerImplementation::mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType) const +{ + double fromMin = fromStateType.minValue().toDouble(); + double fromMax = fromStateType.maxValue().toDouble(); + double toMin = toStateType.minValue().toDouble(); + double toMax = toStateType.maxValue().toDouble(); + double fromValue = value.toDouble(); + double fromPercent = (fromValue - fromMin) / (fromMax - fromMin); + double toValue = toMin + (toMax - toMin) * fromPercent; + return toValue; +} + void ThingManagerImplementation::storeThingStates(Thing *thing) { NymeaSettings settings(NymeaSettings::SettingsRoleThingStates); diff --git a/libnymea-core/integrations/thingmanagerimplementation.h b/libnymea-core/integrations/thingmanagerimplementation.h index f9b0f093..8cb343ef 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.h +++ b/libnymea-core/integrations/thingmanagerimplementation.h @@ -36,6 +36,7 @@ #include "integrations/thing.h" #include "integrations/thingdescriptor.h" #include "integrations/pluginmetadata.h" +#include "integrations/ioconnection.h" #include "types/thingclass.h" #include "types/interface.h" @@ -113,6 +114,10 @@ public: BrowserActionInfo *executeBrowserItem(const BrowserAction &browserAction) override; BrowserItemActionInfo *executeBrowserItemAction(const BrowserItemAction &browserItemAction) override; + IOConnections ioConnections(const ThingId &thingId = ThingId()) const override; + Thing::ThingError connectIO(const IOConnection &connection) override; + Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) override; + QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) override; ParamType translateParamType(const PluginId &pluginId, const ParamType ¶mType, const QLocale &locale) override; ThingClass translateThingClass(const ThingClass &thingClass, const QLocale &locale) override; @@ -149,6 +154,10 @@ private: void postSetupThing(Thing *thing); void storeThingStates(Thing *thing); void loadThingStates(Thing *thing); + void storeIOConnections(); + void loadIOConnections(); + + QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType) const; private: HardwareManager *m_hardwareManager; @@ -173,6 +182,8 @@ private: QString thingName; }; QHash m_pendingPairings; + + QHash m_ioConnections; }; #endif // THINGMANAGERIMPLEMENTATION_H diff --git a/libnymea-core/jsonrpc/devicehandler.cpp b/libnymea-core/jsonrpc/devicehandler.cpp index f13b9f70..120992b1 100644 --- a/libnymea-core/jsonrpc/devicehandler.cpp +++ b/libnymea-core/jsonrpc/devicehandler.cpp @@ -60,6 +60,7 @@ DeviceHandler::DeviceHandler(QObject *parent) : registerEnum(); registerEnum(); registerEnum(); + registerEnum(); registerEnum(); registerEnum(); registerEnum(); diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index 639af631..0aa6b400 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -59,6 +59,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa registerEnum(); registerEnum(); registerEnum(); + registerEnum(); registerEnum(); registerEnum(); registerEnum(); @@ -77,6 +78,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa registerObject(); registerObject(); registerUncreatableObject(); + registerObject(); // Registering browseritem manually for now. Not sure how to deal with the // polymorphism in it (e.g MediaBrowserItem) @@ -346,6 +348,28 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("o:displayMessage", enumValueName(String)); registerMethod("ExecuteBrowserItemAction", description, params, returns); + params.clear(); returns.clear(); + description = "Fetch IO connections. Optionally filtered by thingId and stateTypeId."; + params.insert("o:thingId", enumValueName(Uuid)); + returns.insert("ioConnections", objectRef()); + registerMethod("GetIOConnections", description, params, returns); + + params.clear(); returns.clear(); + description = "Connect two generic IO states."; + params.insert("inputThingId", enumValueName(Uuid)); + params.insert("inputStateTypeId", enumValueName(Uuid)); + params.insert("outputThingId", enumValueName(Uuid)); + params.insert("outputStateTypeId", enumValueName(Uuid)); + returns.insert("thingError", enumRef()); + registerMethod("ConnectIO", description, params, returns); + + params.clear(); returns.clear(); + description = "Disconnect an existing IO connection."; + params.insert("ioConnectionId", enumValueName(Uuid)); + returns.insert("thingError", enumRef()); + registerMethod("DisconnectIO", description, params, returns); + + // Notifications params.clear(); returns.clear(); description = "Emitted whenever a state of a thing changes."; @@ -392,6 +416,26 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa emit EventTriggered(params); }); + params.clear(); returns.clear(); + description = "Emitted whenever a IO connection is added."; + params.insert("ioConnection", objectRef()); + registerNotification("IOConnectionAdded", description, params); + connect(m_thingManager, &ThingManager::ioConnectionAdded, this, [this](const IOConnection &connection) { + QVariantMap params; + params.insert("ioConnection", pack(connection)); + emit IOConnectionAdded(params); + }); + + params.clear(); returns.clear(); + description = "Emitted whenever a IO connection is removed."; + params.insert("ioConnectionId", enumValueName(Uuid)); + registerNotification("IOConnectionRemoved", description, params); + connect(m_thingManager, &ThingManager::ioConnectionRemoved, this, [this](const IOConnectionId &ioConnectionId) { + QVariantMap params; + params.insert("ioConnectionId", ioConnectionId); + emit IOConnectionRemoved(params); + }); + connect(NymeaCore::instance(), &NymeaCore::pluginConfigChanged, this, &IntegrationsHandler::pluginConfigChanged); connect(NymeaCore::instance(), &NymeaCore::thingStateChanged, this, &IntegrationsHandler::thingStateChanged); connect(NymeaCore::instance(), &NymeaCore::thingRemoved, this, &IntegrationsHandler::thingRemovedNotification); @@ -930,6 +974,33 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap ¶ return jsonReply; } +JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap ¶ms) +{ + ThingId thingId = params.value("thingId").toUuid(); + IOConnections ioConnections = m_thingManager->ioConnections(thingId); + QVariantMap returns; + QVariant bla = pack(ioConnections); + returns.insert("ioConnections", pack(ioConnections)); + return createReply(returns); +} + +JsonReply *IntegrationsHandler::ConnectIO(const QVariantMap ¶ms) +{ + ThingId inputThingId = params.value("inputThingId").toUuid(); + StateTypeId inputStateTypeId = params.value("inputStateTypeId").toUuid(); + ThingId outputThingId = params.value("outputThingId").toUuid(); + StateTypeId outputStateTypeId = params.value("outputStateTypeId").toUuid(); + Thing::ThingError error = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId); + return createReply(statusToReply(error)); +} + +JsonReply *IntegrationsHandler::DisconnectIO(const QVariantMap ¶ms) +{ + IOConnectionId ioConnectionId = params.value("ioConnectionId").toUuid(); + Thing::ThingError error = m_thingManager->disconnectIO(ioConnectionId); + return createReply(statusToReply(error)); +} + QVariantMap IntegrationsHandler::packBrowserItem(const BrowserItem &item) { QVariantMap ret; diff --git a/libnymea-core/jsonrpc/integrationshandler.h b/libnymea-core/jsonrpc/integrationshandler.h index 47f88fab..cad64521 100644 --- a/libnymea-core/jsonrpc/integrationshandler.h +++ b/libnymea-core/jsonrpc/integrationshandler.h @@ -72,6 +72,10 @@ public: Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *GetIOConnections(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *ConnectIO(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *DisconnectIO(const QVariantMap ¶ms); + static QVariantMap packBrowserItem(const BrowserItem &item); signals: @@ -82,6 +86,8 @@ signals: void ThingChanged(const QVariantMap ¶ms); void ThingSettingChanged(const QVariantMap ¶ms); void EventTriggered(const QVariantMap ¶ms); + void IOConnectionAdded(const QVariantMap ¶ms); + void IOConnectionRemoved(const QVariantMap ¶ms); private slots: void pluginConfigChanged(const PluginId &id, const ParamList &config); diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index ae246218..ee4b8144 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -424,7 +424,7 @@ void NymeaCore::executeRuleActions(const QList ruleActions) } else if (ruleActionParam.isStateBased()) { Thing *stateThing = m_thingManager->findConfiguredThing(ruleActionParam.stateThingId()); if (!stateThing) { - qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action" << ruleAction.id(); + qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action"; ok = false; break; } @@ -438,7 +438,7 @@ void NymeaCore::executeRuleActions(const QList ruleActions) } } if (!ok) { - qCWarning(dcRuleEngine()) << "Not executing rule action" << ruleAction.id(); + qCWarning(dcRuleEngine()) << "Not executing rule action"; continue; } Action action(actionTypeId, thing->id()); @@ -476,7 +476,7 @@ void NymeaCore::executeRuleActions(const QList ruleActions) } else if (ruleActionParam.isStateBased()) { Thing *stateThing = m_thingManager->findConfiguredThing(ruleActionParam.stateThingId()); if (!stateThing) { - qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action" << ruleAction.id(); + qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action"; ok = false; break; } @@ -490,7 +490,7 @@ void NymeaCore::executeRuleActions(const QList ruleActions) } } if (!ok) { - qCWarning(dcRuleEngine()) << "Not executing rule action" << ruleAction.id(); + qCWarning(dcRuleEngine()) << "Not executing rule action"; continue; } diff --git a/libnymea-core/ruleengine/ruleaction.cpp b/libnymea-core/ruleengine/ruleaction.cpp index 385fa1c0..29b3fc77 100644 --- a/libnymea-core/ruleengine/ruleaction.cpp +++ b/libnymea-core/ruleengine/ruleaction.cpp @@ -31,7 +31,6 @@ #include "ruleaction.h" RuleAction::RuleAction(const ActionTypeId &actionTypeId, const ThingId &thingId, const RuleActionParams ¶ms): - m_id(ActionId::createActionId()), m_thingId(thingId), m_actionTypeId(actionTypeId), m_ruleActionParams(params) @@ -55,7 +54,6 @@ RuleAction::RuleAction(const ThingId &thingId, const QString &browserItemId): } RuleAction::RuleAction(const RuleAction &other) : - m_id(other.id()), m_thingId(other.thingId()), m_actionTypeId(other.actionTypeId()), m_browserItemId(other.browserItemId()), @@ -66,11 +64,6 @@ RuleAction::RuleAction(const RuleAction &other) : } -ActionId RuleAction::id() const -{ - return m_id; -} - bool RuleAction::isValid() const { return (!m_actionTypeId.isNull() && !m_thingId.isNull()) @@ -211,7 +204,6 @@ RuleActionParam RuleAction::ruleActionParam(const QString &ruleActionParamName) void RuleAction::operator=(const RuleAction &other) { - m_id = other.id(); m_actionTypeId = other.actionTypeId(); m_ruleActionParams = other.ruleActionParams(); } diff --git a/libnymea-core/ruleengine/ruleaction.h b/libnymea-core/ruleengine/ruleaction.h index 29e1a96a..017a05ab 100644 --- a/libnymea-core/ruleengine/ruleaction.h +++ b/libnymea-core/ruleengine/ruleaction.h @@ -58,7 +58,6 @@ public: explicit RuleAction(const ThingId &thingId, const QString &browserItemId); RuleAction(const RuleAction &other); - ActionId id() const; bool isValid() const; Type type() const; @@ -92,7 +91,6 @@ public: void operator=(const RuleAction &other); private: - ActionId m_id; ThingId m_thingId; ActionTypeId m_actionTypeId; QString m_browserItemId; diff --git a/libnymea-core/ruleengine/ruleengine.cpp b/libnymea-core/ruleengine/ruleengine.cpp index 72f15482..9a75797d 100644 --- a/libnymea-core/ruleengine/ruleengine.cpp +++ b/libnymea-core/ruleengine/ruleengine.cpp @@ -201,12 +201,12 @@ QList RuleEngine::evaluateEvent(const Event &event) } else { // Event based rule if (containsEvent(rule, event, thing->thingClassId())) { - qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") contains event " << event.eventId(); + qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") contains event"; if (rule.statesActive() && rule.timeActive()) { - qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event" << event.eventId() << "and all states match."; + qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event 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."; + qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event but state are not matching."; rules.append(rule); } } diff --git a/libnymea/integrations/ioconnection.cpp b/libnymea/integrations/ioconnection.cpp new file mode 100644 index 00000000..e408df54 --- /dev/null +++ b/libnymea/integrations/ioconnection.cpp @@ -0,0 +1,51 @@ +#include "ioconnection.h" + +IOConnection::IOConnection() +{ + +} + +IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState): + m_id(id), + m_inputThingId(inputThing), + m_inputStateTypeId(inputState), + m_outputThingId(outputThing), + m_outputStateTypeId(outputState) +{ + +} + +IOConnectionId IOConnection::id() const +{ + return m_id; +} + +ThingId IOConnection::inputThingId() const +{ + return m_inputThingId; +} + +StateTypeId IOConnection::inputStateTypeId() const +{ + return m_inputStateTypeId; +} + +ThingId IOConnection::outputThingId() const +{ + return m_outputThingId; +} + +StateTypeId IOConnection::outputStateTypeId() const +{ + return m_outputStateTypeId; +} + +QVariant IOConnections::get(int index) const +{ + return QVariant::fromValue(at(index)); +} + +void IOConnections::put(const QVariant &variant) +{ + append(variant.value()); +} diff --git a/libnymea/integrations/ioconnection.h b/libnymea/integrations/ioconnection.h new file mode 100644 index 00000000..144abc89 --- /dev/null +++ b/libnymea/integrations/ioconnection.h @@ -0,0 +1,55 @@ +#ifndef IOCONNECTION_H +#define IOCONNECTION_H + +#include +#include +#include + +#include "typeutils.h" + +class IOConnection +{ + Q_GADGET + Q_PROPERTY(QUuid id READ id) + Q_PROPERTY(QUuid inputThingId READ inputThingId) + Q_PROPERTY(QUuid inputStateTypeId READ inputStateTypeId) + Q_PROPERTY(QUuid outputThingId READ outputThingId) + Q_PROPERTY(QUuid outputStateTypeId READ outputStateTypeId) + +public: + IOConnection(); + IOConnection(const IOConnectionId &id, const ThingId &inputThingId, const StateTypeId &inputStateTypeId, const ThingId &outputThingId, const StateTypeId &outputStateTypeId); + + IOConnectionId id() const; + + ThingId inputThingId() const; + StateTypeId inputStateTypeId() const; + + ThingId outputThingId() const; + StateTypeId outputStateTypeId() const; + +private: + IOConnectionId m_id; + ThingId m_inputThingId; + StateTypeId m_inputStateTypeId; + ThingId m_outputThingId; + StateTypeId m_outputStateTypeId; +}; + +class IOConnections: public QList +{ + Q_GADGET + Q_PROPERTY(int count READ count) +public: + IOConnections() {} + inline IOConnections(std::initializer_list args): QList(args) {} + IOConnections(const QList &other): QList(other) {} + Q_INVOKABLE QVariant get(int index) const; + Q_INVOKABLE void put(const QVariant &variant); +}; + +Q_DECLARE_METATYPE(IOConnection) +Q_DECLARE_METATYPE(IOConnections) + + +#endif // IOCONNECTION_H diff --git a/libnymea/integrations/pluginmetadata.cpp b/libnymea/integrations/pluginmetadata.cpp index 65af11dd..35570c96 100644 --- a/libnymea/integrations/pluginmetadata.cpp +++ b/libnymea/integrations/pluginmetadata.cpp @@ -381,6 +381,68 @@ void PluginMetadata::parse(const QJsonObject &jsonObject) if (st.contains("cached")) { stateType.setCached(st.value("cached").toBool()); } + if (st.contains("writable")) { + stateType.setWritable(st.value("writable").toBool()); + } + + if (st.contains("ioType")) { + QString ioTypeString = st.value("ioType").toString(); + Types::IOType ioType = Types::IOTypeNone; + if (ioTypeString == "digitalInput") { + if (stateType.type() != QVariant::Bool) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital input but type is not \"bool\""); + hasError = true; + break; + } + ioType = Types::IOTypeDigitalInput; + } else if (ioTypeString == "digitalOutput") { + if (stateType.type() != QVariant::Bool) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital output but type is not \"bool\""); + hasError = true; + break; + } + if (!stateType.writable()) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital output but is not writable"); + hasError = true; + break; + } + ioType = Types::IOTypeDigitalOutput; + } else if (ioTypeString == "analogInput") { + if (stateType.type() != QVariant::Double) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog input but type is not \"double\""); + hasError = true; + break; + } + if (stateType.minValue().isNull() || stateType.maxValue().isNull()) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog input but it does not define \"minValue\" and \"maxValue\""); + hasError = true; + break; + } + ioType = Types::IOTypeAnalogInput; + } else if (ioTypeString == "analogOutput") { + if (stateType.type() != QVariant::Double) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but type is not \"double\""); + hasError = true; + break; + } + if (!stateType.writable()) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but is not writable"); + hasError = true; + break; + } + if (stateType.minValue().isNull() || stateType.maxValue().isNull()) { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but it does not define \"minValue\" and \"maxValue\""); + hasError = true; + break; + } + ioType = Types::IOTypeAnalogOutput; + } else { + m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid ioType value \"" + ioType + "\" which is not any of \"digitalInput\", \"digitalOutput\", \"analogInput\" or \"analogOutput\""); + hasError = true; + break; + } + stateType.setIOType(ioType); + } stateTypes.append(stateType); // Events for state changed (Not checking for duplicate UUID, this is expected to be the same as the state!) diff --git a/libnymea/integrations/thingmanager.cpp b/libnymea/integrations/thingmanager.cpp index d3d39c72..21751eb5 100644 --- a/libnymea/integrations/thingmanager.cpp +++ b/libnymea/integrations/thingmanager.cpp @@ -50,3 +50,9 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent) qRegisterMetaType(); qRegisterMetaType(); } + +Thing::ThingError ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState) +{ + IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState); + return connectIO(connection); +} diff --git a/libnymea/integrations/thingmanager.h b/libnymea/integrations/thingmanager.h index ed67a5ef..7c1dc417 100644 --- a/libnymea/integrations/thingmanager.h +++ b/libnymea/integrations/thingmanager.h @@ -35,6 +35,7 @@ #include "thing.h" #include "integrationplugin.h" +#include "ioconnection.h" #include "types/interface.h" #include "types/vendor.h" #include "types/browseritem.h" @@ -89,10 +90,18 @@ public: virtual BrowserActionInfo* executeBrowserItem(const BrowserAction &browserAction) = 0; virtual BrowserItemActionInfo* executeBrowserItemAction(const BrowserItemAction &browserItemAction) = 0; + virtual IOConnections ioConnections(const ThingId &thingId = ThingId()) const = 0; + Thing::ThingError connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState); + virtual Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) = 0; + virtual QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) = 0; virtual ParamType translateParamType(const PluginId &pluginId, const ParamType ¶mType, const QLocale &locale) = 0; virtual ThingClass translateThingClass(const ThingClass &thingClass, const QLocale &locale) = 0; virtual Vendor translateVendor(const Vendor &vendor, const QLocale &locale) = 0; + +protected: + virtual Thing::ThingError connectIO(const IOConnection &connection) = 0; + signals: void pluginConfigChanged(const PluginId &id, const ParamList &config); void eventTriggered(const Event &event); @@ -102,6 +111,8 @@ signals: void thingAdded(Thing *thing); void thingChanged(Thing *device); void thingSettingChanged(const ThingId &thingId, const ParamTypeId &settingParamTypeId, const QVariant &value); + void ioConnectionAdded(const IOConnection &ioConnection); + void ioConnectionRemoved(const IOConnectionId &ioConnectionId); }; #endif // THINGMANAGER_H diff --git a/libnymea/interfaces/temperaturesensor.json b/libnymea/interfaces/temperaturesensor.json index 5b480c3a..04fafa42 100644 --- a/libnymea/interfaces/temperaturesensor.json +++ b/libnymea/interfaces/temperaturesensor.json @@ -3,7 +3,8 @@ "states": [ { "name": "temperature", - "type": "double" + "type": "double", + "unit": "DegreeCelsius" } ] } diff --git a/libnymea/libnymea.pro b/libnymea/libnymea.pro index 6654c457..b8662168 100644 --- a/libnymea/libnymea.pro +++ b/libnymea/libnymea.pro @@ -14,6 +14,7 @@ HEADERS += \ integrations/browseritemactioninfo.h \ integrations/browseritemresult.h \ integrations/integrationplugin.h \ + integrations/ioconnection.h \ integrations/pluginmetadata.h \ integrations/browseresult.h \ integrations/thing.h \ @@ -97,6 +98,7 @@ SOURCES += \ integrations/browseritemactioninfo.cpp \ integrations/browseritemresult.cpp \ integrations/integrationplugin.cpp \ + integrations/ioconnection.cpp \ integrations/pluginmetadata.cpp \ integrations/browseresult.cpp \ integrations/thing.cpp \ diff --git a/libnymea/nymeasettings.cpp b/libnymea/nymeasettings.cpp index 8e607aa4..5b404857 100644 --- a/libnymea/nymeasettings.cpp +++ b/libnymea/nymeasettings.cpp @@ -115,6 +115,9 @@ NymeaSettings::NymeaSettings(const SettingsRole &role, QObject *parent): case SettingsRoleMqttPolicies: fileName = "mqttpolicies.conf"; break; + case SettingsRoleIOConnections: + fileName = "ioconnections.conf"; + break; } m_settings = new QSettings(basePath + settingsPrefix + fileName, QSettings::IniFormat, this); } diff --git a/libnymea/nymeasettings.h b/libnymea/nymeasettings.h index 61984e53..8c810e1d 100644 --- a/libnymea/nymeasettings.h +++ b/libnymea/nymeasettings.h @@ -51,6 +51,7 @@ public: SettingsRoleThingStates, SettingsRoleTags, SettingsRoleMqttPolicies, + SettingsRoleIOConnections, }; explicit NymeaSettings(const SettingsRole &role = SettingsRoleNone, QObject *parent = nullptr); diff --git a/libnymea/types/action.cpp b/libnymea/types/action.cpp index 5db75322..7df1246f 100644 --- a/libnymea/types/action.cpp +++ b/libnymea/types/action.cpp @@ -47,7 +47,6 @@ /*! Construct an Action with the given \a deviceId and \a actionTypeId. */ Action::Action(const ActionTypeId &actionTypeId, const ThingId &thingId) : - m_id(ActionId::createActionId()), m_actionTypeId(actionTypeId), m_thingId(thingId) { @@ -55,19 +54,12 @@ Action::Action(const ActionTypeId &actionTypeId, const ThingId &thingId) : /*! Construct a copy of an \a other Action. */ Action::Action(const Action &other): - m_id(other.id()), m_actionTypeId(other.actionTypeId()), m_thingId(other.thingId()), m_params(other.params()) { } -/*! Returns the actionId for this Action. */ -ActionId Action::id() const -{ - return m_id; -} - /*! An Action is valid if actionTypeId and deviceId are valid uuids. Returns true if valid, false if not. */ bool Action::isValid() const { @@ -123,7 +115,6 @@ Param Action::param(const ParamTypeId ¶mTypeId) const /*! Copy the data to an \l{Action} from an \a other action. */ void Action::operator =(const Action &other) { - m_id = other.id(); m_actionTypeId = other.actionTypeId(); m_params = other.params(); } diff --git a/libnymea/types/action.h b/libnymea/types/action.h index 8a1c249a..358d3c87 100644 --- a/libnymea/types/action.h +++ b/libnymea/types/action.h @@ -49,8 +49,6 @@ public: explicit Action(const ActionTypeId &actionTypeId = ActionTypeId(), const ThingId &thingId = ThingId()); Action(const Action &other); - ActionId id() const; - bool isValid() const; ActionTypeId actionTypeId() const; @@ -64,7 +62,6 @@ public: void operator=(const Action &other); private: - ActionId m_id; ActionTypeId m_actionTypeId; ThingId m_thingId; ParamList m_params; diff --git a/libnymea/types/browseraction.cpp b/libnymea/types/browseraction.cpp index 1c0075b0..21726544 100644 --- a/libnymea/types/browseraction.cpp +++ b/libnymea/types/browseraction.cpp @@ -31,7 +31,6 @@ #include "browseraction.h" BrowserAction::BrowserAction(const ThingId &thingId, const QString &itemId): - m_id(ActionId::createActionId()), m_thingId(thingId), m_itemId(itemId) { @@ -39,21 +38,15 @@ BrowserAction::BrowserAction(const ThingId &thingId, const QString &itemId): } BrowserAction::BrowserAction(const BrowserAction &other): - m_id(other.id()), m_thingId(other.thingId()), m_itemId(other.itemId()) { } -ActionId BrowserAction::id() const -{ - return m_id; -} - bool BrowserAction::isValid() const { - return !m_id.isNull() && !m_thingId.isNull() && !m_itemId.isNull(); + return !m_thingId.isNull() && !m_itemId.isNull(); } ThingId BrowserAction::thingId() const @@ -68,7 +61,6 @@ QString BrowserAction::itemId() const void BrowserAction::operator=(const BrowserAction &other) { - m_id = other.id(); m_thingId = other.thingId(); m_itemId = other.itemId(); } diff --git a/libnymea/types/browseraction.h b/libnymea/types/browseraction.h index 7dd5e0b6..2caf991d 100644 --- a/libnymea/types/browseraction.h +++ b/libnymea/types/browseraction.h @@ -39,8 +39,6 @@ public: explicit BrowserAction(const ThingId &thingId = ThingId(), const QString &itemId = QString()); BrowserAction(const BrowserAction &other); - ActionId id() const; - bool isValid() const; ThingId thingId() const; @@ -48,7 +46,6 @@ public: void operator=(const BrowserAction &other); private: - ActionId m_id; ThingId m_thingId; QString m_itemId; }; diff --git a/libnymea/types/browseritemaction.cpp b/libnymea/types/browseritemaction.cpp index ea625527..40d419e4 100644 --- a/libnymea/types/browseritemaction.cpp +++ b/libnymea/types/browseritemaction.cpp @@ -32,7 +32,6 @@ BrowserItemAction::BrowserItemAction(const ThingId &thingId, const QString &itemId, const ActionTypeId &actionTypeId, const ParamList ¶ms): - m_id(ActionId::createActionId()), m_thingId(thingId), m_itemId(itemId), m_actionTypeId(actionTypeId), @@ -42,7 +41,6 @@ BrowserItemAction::BrowserItemAction(const ThingId &thingId, const QString &item } BrowserItemAction::BrowserItemAction(const BrowserItemAction &other): - m_id(other.id()), m_thingId(other.thingId()), m_itemId(other.itemId()), m_actionTypeId(other.actionTypeId()), @@ -51,14 +49,9 @@ BrowserItemAction::BrowserItemAction(const BrowserItemAction &other): } -ActionId BrowserItemAction::id() const -{ - return m_id; -} - bool BrowserItemAction::isValid() const { - return !m_id.isNull() && !m_thingId.isNull() && !m_itemId.isNull(); + return !m_thingId.isNull() && !m_itemId.isNull(); } ThingId BrowserItemAction::thingId() const @@ -83,7 +76,6 @@ ParamList BrowserItemAction::params() const void BrowserItemAction::operator=(const BrowserItemAction &other) { - m_id = other.id(); m_thingId = other.thingId(); m_itemId = other.itemId(); m_actionTypeId = other.actionTypeId(); diff --git a/libnymea/types/browseritemaction.h b/libnymea/types/browseritemaction.h index 64ba5aae..534c9212 100644 --- a/libnymea/types/browseritemaction.h +++ b/libnymea/types/browseritemaction.h @@ -40,8 +40,6 @@ public: explicit BrowserItemAction(const ThingId &thingId = ThingId(), const QString &itemId = QString(), const ActionTypeId &actionTypeId = ActionTypeId(), const ParamList ¶ms = ParamList()); BrowserItemAction(const BrowserItemAction &other); - ActionId id() const; - bool isValid() const; ThingId thingId() const; @@ -54,7 +52,6 @@ public: void operator=(const BrowserItemAction &other); private: - ActionId m_id; ThingId m_thingId; QString m_itemId; ActionTypeId m_actionTypeId; diff --git a/libnymea/types/event.cpp b/libnymea/types/event.cpp index 71089d8d..c38655d5 100644 --- a/libnymea/types/event.cpp +++ b/libnymea/types/event.cpp @@ -46,8 +46,7 @@ #include "event.h" /*! Constructs an Event. */ -Event::Event(): - m_id(EventId::createEventId()) +Event::Event() { } @@ -56,7 +55,6 @@ Event::Event(): * specifies if the \l{Event} will be autogeneratet or not. The parameters must * match the description in the reflecting \l{Event}. */ Event::Event(const EventTypeId &eventTypeId, const ThingId &thingId, const ParamList ¶ms, bool isStateChangeEvent): - m_id(EventId::createEventId()), m_eventTypeId(eventTypeId), m_thingId(thingId), m_params(params), @@ -64,13 +62,6 @@ Event::Event(const EventTypeId &eventTypeId, const ThingId &thingId, const Param { } -/*! Returns the Id of this Event. Each newly created Event will have a new UUID generated. The id will be copied - * in the copy constructor. */ -EventId Event::eventId() const -{ - return m_id; -} - /*! Returns the id of the \l{EventType} which describes this Event. */ EventTypeId Event::eventTypeId() const { diff --git a/libnymea/types/event.h b/libnymea/types/event.h index 463a2a8e..cb323633 100644 --- a/libnymea/types/event.h +++ b/libnymea/types/event.h @@ -50,8 +50,6 @@ public: Event(); Event(const EventTypeId &eventTypeId, const ThingId &thingId, const ParamList ¶ms = ParamList(), bool isStateChangeEvent = false); - EventId eventId() const; - EventTypeId eventTypeId() const; void setEventTypeId(const EventTypeId &eventTypeId); @@ -67,7 +65,6 @@ public: bool isStateChangeEvent() const; private: - EventId m_id; EventTypeId m_eventTypeId; ThingId m_thingId; ParamList m_params; diff --git a/libnymea/types/state.cpp b/libnymea/types/state.cpp index f06f5be1..8366511f 100644 --- a/libnymea/types/state.cpp +++ b/libnymea/types/state.cpp @@ -51,18 +51,11 @@ State::State() /*! Constructs a State reflecting the \l{StateType} given by \a stateTypeId * and associated with the \l{Device} given by \a deviceId */ State::State(const StateTypeId &stateTypeId, const ThingId &deviceId): - m_id(StateId::createStateId()), m_stateTypeId(stateTypeId), m_thingId(deviceId) { } -/*! Returns the id of this State. */ -StateId State::id() const -{ - return m_id; -} - /*! Returns the id of the StateType describing this State. */ StateTypeId State::stateTypeId() const { diff --git a/libnymea/types/state.h b/libnymea/types/state.h index 55f7a3ac..165c2fb9 100644 --- a/libnymea/types/state.h +++ b/libnymea/types/state.h @@ -47,8 +47,6 @@ public: State(); State(const StateTypeId &stateTypeId, const ThingId &deviceId); - StateId id() const; - StateTypeId stateTypeId() const; ThingId thingId() const; @@ -56,7 +54,6 @@ public: void setValue(const QVariant &value); private: - StateId m_id; StateTypeId m_stateTypeId; ThingId m_thingId; QVariant m_value; diff --git a/libnymea/types/statetype.cpp b/libnymea/types/statetype.cpp index ef20c147..ab505a90 100644 --- a/libnymea/types/statetype.cpp +++ b/libnymea/types/statetype.cpp @@ -172,6 +172,30 @@ void StateType::setUnit(const Types::Unit &unit) m_unit = unit; } +/*! Returns the IO type of this StateType. */ +Types::IOType StateType::ioType() const +{ + return m_ioType; +} + +/*! Sets the IO type of this StateType. */ +void StateType::setIOType(Types::IOType ioType) +{ + m_ioType = ioType; +} + +/*! Returns whether the StateType is writable or not. A writable StateType will have an according ActionType defined.*/ +bool StateType::writable() const +{ + return m_writable; +} + +/*! Sets the writable property to true */ +void StateType::setWritable(bool writable) +{ + m_writable = writable; +} + /*! Returns true if this StateType is to be cached. This means, the last state value will be stored to disk upon shutdown and restored on reboot. If this is false, states will be initialized with the default value on each boot. By default all states are cached by the system. */ bool StateType::cached() const { @@ -189,7 +213,7 @@ QStringList StateType::typeProperties() { return QStringList() << "id" << "name" << "displayName" << "displayNameEvent" << "type" << "defaultValue" << "cached" << "unit" << "minValue" << "maxValue" << "possibleValues" << "writable" - << "displayNameAction"; + << "displayNameAction" << "ioType"; } /*! Returns a list of mandatory properties a DeviceClass definition must have. */ @@ -205,6 +229,16 @@ StateTypes::StateTypes(const QList &other) } } +bool StateTypes::contains(const StateTypeId &stateTypeId) +{ + foreach (const StateType &stateType, *this) { + if (stateType.id() == stateTypeId) { + return true; + } + } + return false; +} + QVariant StateTypes::get(int index) const { return QVariant::fromValue(at(index)); diff --git a/libnymea/types/statetype.h b/libnymea/types/statetype.h index edb6a6bd..0bfdd6ee 100644 --- a/libnymea/types/statetype.h +++ b/libnymea/types/statetype.h @@ -46,6 +46,7 @@ class LIBNYMEA_EXPORT StateType Q_PROPERTY(int index READ index WRITE setIndex) Q_PROPERTY(QVariant defaultValue READ defaultValue WRITE setDefaultValue) Q_PROPERTY(Types::Unit unit READ unit WRITE setUnit USER true) + Q_PROPERTY(Types::IOType ioType READ ioType WRITE setIOType USER true) Q_PROPERTY(QVariant minValue READ minValue WRITE setMinValue USER true) Q_PROPERTY(QVariant maxValue READ maxValue WRITE setMaxValue USER true) Q_PROPERTY(QVariantList possibleValues READ possibleValues WRITE setPossibleValues USER true) @@ -83,6 +84,12 @@ public: Types::Unit unit() const; void setUnit(const Types::Unit &unit); + Types::IOType ioType() const; + void setIOType(Types::IOType ioType); + + bool writable() const; + void setWritable(bool writable); + bool cached() const; void setCached(bool cached); @@ -100,6 +107,8 @@ private: QVariant m_maxValue; QVariantList m_possibleValues; Types::Unit m_unit = Types::UnitNone; + Types::IOType m_ioType = Types::IOTypeNone; + bool m_writable = false; bool m_cached = true; }; Q_DECLARE_METATYPE(StateType) @@ -111,6 +120,7 @@ class StateTypes: public QList public: StateTypes() = default; StateTypes(const QList &other); + bool contains(const StateTypeId &stateTypeId); Q_INVOKABLE QVariant get(int index) const; Q_INVOKABLE void put(const QVariant &variant); StateType findByName(const QString &name); diff --git a/libnymea/typeutils.h b/libnymea/typeutils.h index 619b5a54..b76a5c6b 100644 --- a/libnymea/typeutils.h +++ b/libnymea/typeutils.h @@ -56,14 +56,12 @@ DECLARE_TYPE_ID(ThingDescriptor) DECLARE_TYPE_ID(ParamType) DECLARE_TYPE_ID(Param) DECLARE_TYPE_ID(EventType) -DECLARE_TYPE_ID(Event) DECLARE_TYPE_ID(StateType) -DECLARE_TYPE_ID(State) DECLARE_TYPE_ID(ActionType) -DECLARE_TYPE_ID(Action) DECLARE_TYPE_ID(Plugin) DECLARE_TYPE_ID(Rule) DECLARE_TYPE_ID(Browser) +DECLARE_TYPE_ID(IOConnection) DECLARE_TYPE_ID(PairingTransaction) @@ -165,6 +163,15 @@ public: BrowserTypeGeneric, }; Q_ENUM(BrowserType) + + enum IOType { + IOTypeNone, + IOTypeDigitalInput, + IOTypeDigitalOutput, + IOTypeAnalogInput, + IOTypeAnalogOutput + }; + Q_ENUM(IOType) }; Q_DECLARE_METATYPE(Types::InputType) diff --git a/nymea.pro b/nymea.pro index 94a7cd35..1f3b20ef 100644 --- a/nymea.pro +++ b/nymea.pro @@ -5,7 +5,7 @@ NYMEA_VERSION_STRING=$$system('dpkg-parsechangelog | sed -n -e "s/^Version: //p" # define protocol versions JSON_PROTOCOL_VERSION_MAJOR=5 -JSON_PROTOCOL_VERSION_MINOR=0 +JSON_PROTOCOL_VERSION_MINOR=1 JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}" LIBNYMEA_API_VERSION_MAJOR=5 LIBNYMEA_API_VERSION_MINOR=0 diff --git a/plugins/mock/extern-plugininfo.h b/plugins/mock/extern-plugininfo.h index 79b79c29..aca86972 100644 --- a/plugins/mock/extern-plugininfo.h +++ b/plugins/mock/extern-plugininfo.h @@ -246,5 +246,54 @@ extern ParamTypeId inputTypeMockWritableTimestampUIntActionWritableTimestampUInt extern ThingClassId oAuthGoogleMockThingClassId; extern ThingClassId oAuthSonosMockThingClassId; extern ThingClassId userAndPassMockThingClassId; +extern ThingClassId genericIoMockThingClassId; +extern StateTypeId genericIoMockDigitalInput1StateTypeId; +extern StateTypeId genericIoMockDigitalInput2StateTypeId; +extern StateTypeId genericIoMockDigitalOutput1StateTypeId; +extern StateTypeId genericIoMockDigitalOutput2StateTypeId; +extern StateTypeId genericIoMockAnalogInput1StateTypeId; +extern StateTypeId genericIoMockAnalogInput2StateTypeId; +extern StateTypeId genericIoMockAnalogOutput1StateTypeId; +extern StateTypeId genericIoMockAnalogOutput2StateTypeId; +extern EventTypeId genericIoMockDigitalInput1EventTypeId; +extern ParamTypeId genericIoMockDigitalInput1EventDigitalInput1ParamTypeId; +extern EventTypeId genericIoMockDigitalInput2EventTypeId; +extern ParamTypeId genericIoMockDigitalInput2EventDigitalInput2ParamTypeId; +extern EventTypeId genericIoMockDigitalOutput1EventTypeId; +extern ParamTypeId genericIoMockDigitalOutput1EventDigitalOutput1ParamTypeId; +extern EventTypeId genericIoMockDigitalOutput2EventTypeId; +extern ParamTypeId genericIoMockDigitalOutput2EventDigitalOutput2ParamTypeId; +extern EventTypeId genericIoMockAnalogInput1EventTypeId; +extern ParamTypeId genericIoMockAnalogInput1EventAnalogInput1ParamTypeId; +extern EventTypeId genericIoMockAnalogInput2EventTypeId; +extern ParamTypeId genericIoMockAnalogInput2EventAnalogInput2ParamTypeId; +extern EventTypeId genericIoMockAnalogOutput1EventTypeId; +extern ParamTypeId genericIoMockAnalogOutput1EventAnalogOutput1ParamTypeId; +extern EventTypeId genericIoMockAnalogOutput2EventTypeId; +extern ParamTypeId genericIoMockAnalogOutput2EventAnalogOutput2ParamTypeId; +extern ActionTypeId genericIoMockDigitalOutput1ActionTypeId; +extern ParamTypeId genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId; +extern ActionTypeId genericIoMockDigitalOutput2ActionTypeId; +extern ParamTypeId genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId; +extern ActionTypeId genericIoMockAnalogInput1ActionTypeId; +extern ParamTypeId genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId; +extern ActionTypeId genericIoMockAnalogOutput1ActionTypeId; +extern ParamTypeId genericIoMockAnalogOutput1ActionAnalogOutput1ParamTypeId; +extern ActionTypeId genericIoMockAnalogOutput2ActionTypeId; +extern ParamTypeId genericIoMockAnalogOutput2ActionAnalogOutput2ParamTypeId; +extern ThingClassId virtualIoLightMockThingClassId; +extern StateTypeId virtualIoLightMockPowerStateTypeId; +extern EventTypeId virtualIoLightMockPowerEventTypeId; +extern ParamTypeId virtualIoLightMockPowerEventPowerParamTypeId; +extern ActionTypeId virtualIoLightMockPowerActionTypeId; +extern ParamTypeId virtualIoLightMockPowerActionPowerParamTypeId; +extern ThingClassId virtualIoTemperatureSensorMockThingClassId; +extern ParamTypeId virtualIoTemperatureSensorMockSettingsMinTempParamTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId; +extern StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId; +extern EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId; +extern ActionTypeId virtualIoTemperatureSensorMockTemperatureActionTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockTemperatureActionTemperatureParamTypeId; #endif // EXTERNPLUGININFO_H diff --git a/plugins/mock/integrationpluginmock.cpp b/plugins/mock/integrationpluginmock.cpp index a85d8c57..bbf0ff8b 100644 --- a/plugins/mock/integrationpluginmock.cpp +++ b/plugins/mock/integrationpluginmock.cpp @@ -233,6 +233,24 @@ void IntegrationPluginMock::setupThing(ThingSetupInfo *info) return; } + if (info->thing()->thingClassId() == genericIoMockThingClassId) { + qCDebug(dcMock()) << "Generic IO mock setup complete"; + info->finish(Thing::ThingErrorNoError); + return; + } + + if (info->thing()->thingClassId() == virtualIoLightMockThingClassId) { + qCDebug(dcMock()) << "Virtual IO mock light setup complete"; + info->finish(Thing::ThingErrorNoError); + return; + } + + if (info->thing()->thingClassId() == virtualIoTemperatureSensorMockThingClassId) { + qCDebug(dcMock()) << "Virtual IO mock temperature sensor setup complete"; + info->finish(Thing::ThingErrorNoError); + return; + } + qCWarning(dcMock()) << "Unhandled thing class" << info->thing()->thingClass(); info->finish(Thing::ThingErrorThingClassNotFound); } @@ -689,9 +707,59 @@ void IntegrationPluginMock::executeAction(ThingActionInfo *info) info->thing()->setStateValue(inputTypeMockWritableTimestampUIntStateTypeId, info->action().param(inputTypeMockWritableTimestampUIntActionWritableTimestampUIntParamTypeId).value().toULongLong()); } return; - } - info->finish(Thing::ThingErrorThingClassNotFound); + + if (info->thing()->thingClassId() == virtualIoLightMockThingClassId) { + if (info->action().actionTypeId() == virtualIoLightMockPowerActionTypeId) { + qCDebug(dcMock()) << "ExecuteAction for virtual light power action with param" << info->action().param(virtualIoLightMockPowerActionPowerParamTypeId).value(); + info->thing()->setStateValue(virtualIoLightMockPowerStateTypeId, info->action().param(virtualIoLightMockPowerActionPowerParamTypeId).value()); + info->finish(Thing::ThingErrorNoError); + return; + } + } + + if (info->thing()->thingClassId() == genericIoMockThingClassId) { + if (info->action().actionTypeId() == genericIoMockDigitalOutput1ActionTypeId) { + info->thing()->setStateValue(genericIoMockDigitalOutput1StateTypeId, info->action().param(genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId).value()); + info->finish(Thing::ThingErrorNoError); + return; + } + if (info->action().actionTypeId() == genericIoMockDigitalOutput2ActionTypeId) { + info->thing()->setStateValue(genericIoMockDigitalOutput2StateTypeId, info->action().param(genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId).value()); + info->finish(Thing::ThingErrorNoError); + return; + } + if (info->action().actionTypeId() == genericIoMockAnalogInput1ActionTypeId) { + qCDebug(dcMock()) << "ExecuteAction for virtual io analog in 1 action with param" << info->action().param(genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId).value(); + info->thing()->setStateValue(genericIoMockAnalogInput1StateTypeId, info->action().param(genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId).value()); + info->finish(Thing::ThingErrorNoError); + return; + } + if (info->action().actionTypeId() == genericIoMockAnalogOutput1ActionTypeId) { + info->thing()->setStateValue(genericIoMockAnalogOutput1StateTypeId, info->action().param(genericIoMockAnalogOutput1ActionAnalogOutput1ParamTypeId).value()); + info->finish(Thing::ThingErrorNoError); + return; + } + if (info->action().actionTypeId() == genericIoMockAnalogOutput2ActionTypeId) { + info->thing()->setStateValue(genericIoMockAnalogOutput2StateTypeId, info->action().param(genericIoMockAnalogOutput2ActionAnalogOutput2ParamTypeId).value()); + info->finish(Thing::ThingErrorNoError); + return; + } + } + + if (info->thing()->thingClassId() == virtualIoTemperatureSensorMockThingClassId) { + if (info->action().actionTypeId() == virtualIoTemperatureSensorMockTemperatureActionTypeId) { + double minTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMinTempParamTypeId).toDouble(); + double maxTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId).toDouble(); + double value = info->action().param(virtualIoTemperatureSensorMockTemperatureActionTemperatureParamTypeId).value().toDouble(); + double temp = minTemp + (maxTemp - minTemp) * value; + info->thing()->setStateValue(virtualIoTemperatureSensorMockTemperatureStateTypeId, temp); + info->finish(Thing::ThingErrorNoError); + return; + } + } + + qCWarning(dcMock()) << "Unhandled executeAction call in mock plugin!"; } void IntegrationPluginMock::executeBrowserItem(BrowserActionInfo *info) @@ -728,7 +796,6 @@ void IntegrationPluginMock::executeBrowserItem(BrowserActionInfo *info) void IntegrationPluginMock::executeBrowserItemAction(BrowserItemActionInfo *info) { - qCDebug(dcMock()) << "TODO" << info << info->browserItemAction().id(); if (info->browserItemAction().actionTypeId() == mockAddToFavoritesBrowserItemActionTypeId) { VirtualFsNode *node = m_virtualFs->findNode(info->browserItemAction().itemId()); diff --git a/plugins/mock/integrationpluginmock.json b/plugins/mock/integrationpluginmock.json index b590e091..0931a01e 100644 --- a/plugins/mock/integrationpluginmock.json +++ b/plugins/mock/integrationpluginmock.json @@ -868,6 +868,168 @@ "displayName": "Mocked Thing (User & Password)", "createMethods": ["discovery", "user"], "setupMethod": "userandpassword" + }, + { + "id": "7cbd729a-465b-4ccb-b59c-5733039dbbed", + "name": "genericIoMock", + "displayName": "Generic IO pins", + "createMethods": ["user"], + "setupMethod": "justAdd", + "stateTypes": [ + { + "id": "07165c12-4d53-45c0-8bf1-34618443b706", + "name": "digitalInput1", + "displayName": "Digital input 1", + "displayNameEvent": "Digital input 1 changed", + "type": "bool", + "defaultValue": false, + "ioType": "digitalInput" + }, + { + "id": "0a4362ba-a086-4540-84ba-107ef7b99ed8", + "name": "digitalInput2", + "displayName": "Digital input 2", + "displayNameEvent": "Digital input 2 changed", + "type": "bool", + "defaultValue": false, + "ioType": "digitalInput" + }, + { + "id": "d6fcdb52-f7c3-423b-b9f5-1e29f164c42e", + "name": "digitalOutput1", + "displayName": "Digital Output 1", + "displayNameEvent": "Digital Output 1 changed", + "displayNameAction": "Set Digital Output 1", + "type": "bool", + "defaultValue": false, + "ioType": "digitalOutput", + "writable": true + }, + { + "id": "35de8b68-0cf3-4850-a27d-cf9c4a26921f", + "name": "digitalOutput2", + "displayName": "Digital Output 2", + "displayNameEvent": "Digital Output 2 changed", + "displayNameAction": "Set Digital Output 2", + "type": "bool", + "defaultValue": false, + "ioType": "digitalOutput", + "writable": true + }, + { + "id": "ac56977c-cbba-47c6-a827-5735d8b0aed6", + "name": "analogInput1", + "displayName": "Analog Input 1", + "displayNameEvent": "Analog Input 1 changed", + "type": "double", + "defaultValue": 0, + "ioType": "analogInput", + "minValue": 0, + "maxValue": 3.3, + "writable": true, + "displayNameAction": "Set analog input 1" + }, + { + "id": "8e07e57e-ba4e-42df-81ee-5b72ed074532", + "name": "analogInput2", + "displayName": "Analog Input 2", + "displayNameEvent": "Analog Input 2 changed", + "type": "double", + "defaultValue": 0, + "ioType": "analogInput", + "minValue": 0, + "maxValue": 5 + }, + { + "id": "70cf053e-4abc-4d88-8e1e-2bd9a62256c7", + "name": "analogOutput1", + "displayName": "Analog Output 1", + "displayNameEvent": "Analog Output 1 changed", + "displayNameAction": "Set Output Input 1", + "type": "double", + "defaultValue": 0, + "ioType": "analogOutput", + "minValue": 0, + "maxValue": 3.3, + "writable": true + }, + { + "id": "e40bcf7d-47b8-41fa-b213-3652a905b376", + "name": "analogOutput2", + "displayName": "Analog Output 2", + "displayNameEvent": "Analog Output 2 changed", + "displayNameAction": "Set Output Input 2", + "type": "double", + "defaultValue": 0, + "ioType": "analogOutput", + "minValue": 0, + "maxValue": 5, + "writable": true + } + ] + }, + { + "id": "98ab137e-757e-43f8-9d9b-5d50d990242a", + "name": "virtualIoLightMock", + "displayName": "Generic Light (Mock)", + "createMethods": ["user"], + "setupMethod": "justAdd", + "interfaces": ["light"], + "stateTypes": [ + { + "id": "d1917b3d-1530-4cf9-90f7-263ee88e714b", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "ioType": "digitalInput", + "writable": true + } + ] + }, + { + "id": "f8917e12-c9cb-4ea1-a06e-1ce6db2194f3", + "name": "virtualIoTemperatureSensorMock", + "displayName": "Generic Temperature Sensor (Mock)", + "createMethods": ["user"], + "setupMethod": "justAdd", + "interfaces": ["temperaturesensor"], + "settingsTypes": [ + { + "id": "803cddbf-94c7-4f35-bc7a-18698b03b942", + "name": "minTemp", + "displayName": "Minimum temperature", + "type": "double", + "defaultValue": -20, + "unit": "DegreeCelsius" + }, + { + "id": "7077c56f-c35b-4252-8c15-8fb549be04ce", + "name": "maxTemp", + "displayName": "Maximum temperature", + "type": "double", + "defaultValue": 50, + "unit": "DegreeCelsius" + } + ], + "stateTypes": [ + { + "id": "db9cc518-1012-47e2-8212-6e616fed07a6", + "name": "temperature", + "displayName": "Temperature", + "displayNameEvent": "Temperature changed", + "displayNameAction": "Set temperature", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0, + "ioType": "analogOutput", + "writable": true, + "minValue": 0, + "maxValue": 1 + } + ] } ] } diff --git a/plugins/mock/plugininfo.h b/plugins/mock/plugininfo.h index 5bf42599..e1b03571 100644 --- a/plugins/mock/plugininfo.h +++ b/plugins/mock/plugininfo.h @@ -250,11 +250,105 @@ ParamTypeId inputTypeMockWritableTimestampUIntActionWritableTimestampUIntParamTy ThingClassId oAuthGoogleMockThingClassId = ThingClassId("{805d1692-7bd0-449a-9d5c-43a332ff58f4}"); ThingClassId oAuthSonosMockThingClassId = ThingClassId("{783c615b-7bd6-49a4-98b0-8d1deb3c7156}"); ThingClassId userAndPassMockThingClassId = ThingClassId("{6fe07a77-9c07-4736-81e2-d504314bbcb9}"); +ThingClassId genericIoMockThingClassId = ThingClassId("{7cbd729a-465b-4ccb-b59c-5733039dbbed}"); +StateTypeId genericIoMockDigitalInput1StateTypeId = StateTypeId("{07165c12-4d53-45c0-8bf1-34618443b706}"); +StateTypeId genericIoMockDigitalInput2StateTypeId = StateTypeId("{0a4362ba-a086-4540-84ba-107ef7b99ed8}"); +StateTypeId genericIoMockDigitalOutput1StateTypeId = StateTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}"); +StateTypeId genericIoMockDigitalOutput2StateTypeId = StateTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}"); +StateTypeId genericIoMockAnalogInput1StateTypeId = StateTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}"); +StateTypeId genericIoMockAnalogInput2StateTypeId = StateTypeId("{8e07e57e-ba4e-42df-81ee-5b72ed074532}"); +StateTypeId genericIoMockAnalogOutput1StateTypeId = StateTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}"); +StateTypeId genericIoMockAnalogOutput2StateTypeId = StateTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}"); +EventTypeId genericIoMockDigitalInput1EventTypeId = EventTypeId("{07165c12-4d53-45c0-8bf1-34618443b706}"); +ParamTypeId genericIoMockDigitalInput1EventDigitalInput1ParamTypeId = ParamTypeId("{07165c12-4d53-45c0-8bf1-34618443b706}"); +EventTypeId genericIoMockDigitalInput2EventTypeId = EventTypeId("{0a4362ba-a086-4540-84ba-107ef7b99ed8}"); +ParamTypeId genericIoMockDigitalInput2EventDigitalInput2ParamTypeId = ParamTypeId("{0a4362ba-a086-4540-84ba-107ef7b99ed8}"); +EventTypeId genericIoMockDigitalOutput1EventTypeId = EventTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}"); +ParamTypeId genericIoMockDigitalOutput1EventDigitalOutput1ParamTypeId = ParamTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}"); +EventTypeId genericIoMockDigitalOutput2EventTypeId = EventTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}"); +ParamTypeId genericIoMockDigitalOutput2EventDigitalOutput2ParamTypeId = ParamTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}"); +EventTypeId genericIoMockAnalogInput1EventTypeId = EventTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}"); +ParamTypeId genericIoMockAnalogInput1EventAnalogInput1ParamTypeId = ParamTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}"); +EventTypeId genericIoMockAnalogInput2EventTypeId = EventTypeId("{8e07e57e-ba4e-42df-81ee-5b72ed074532}"); +ParamTypeId genericIoMockAnalogInput2EventAnalogInput2ParamTypeId = ParamTypeId("{8e07e57e-ba4e-42df-81ee-5b72ed074532}"); +EventTypeId genericIoMockAnalogOutput1EventTypeId = EventTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}"); +ParamTypeId genericIoMockAnalogOutput1EventAnalogOutput1ParamTypeId = ParamTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}"); +EventTypeId genericIoMockAnalogOutput2EventTypeId = EventTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}"); +ParamTypeId genericIoMockAnalogOutput2EventAnalogOutput2ParamTypeId = ParamTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}"); +ActionTypeId genericIoMockDigitalOutput1ActionTypeId = ActionTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}"); +ParamTypeId genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId = ParamTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}"); +ActionTypeId genericIoMockDigitalOutput2ActionTypeId = ActionTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}"); +ParamTypeId genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId = ParamTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}"); +ActionTypeId genericIoMockAnalogInput1ActionTypeId = ActionTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}"); +ParamTypeId genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId = ParamTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}"); +ActionTypeId genericIoMockAnalogOutput1ActionTypeId = ActionTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}"); +ParamTypeId genericIoMockAnalogOutput1ActionAnalogOutput1ParamTypeId = ParamTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}"); +ActionTypeId genericIoMockAnalogOutput2ActionTypeId = ActionTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}"); +ParamTypeId genericIoMockAnalogOutput2ActionAnalogOutput2ParamTypeId = ParamTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}"); +ThingClassId virtualIoLightMockThingClassId = ThingClassId("{98ab137e-757e-43f8-9d9b-5d50d990242a}"); +StateTypeId virtualIoLightMockPowerStateTypeId = StateTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}"); +EventTypeId virtualIoLightMockPowerEventTypeId = EventTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}"); +ParamTypeId virtualIoLightMockPowerEventPowerParamTypeId = ParamTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}"); +ActionTypeId virtualIoLightMockPowerActionTypeId = ActionTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}"); +ParamTypeId virtualIoLightMockPowerActionPowerParamTypeId = ParamTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}"); +ThingClassId virtualIoTemperatureSensorMockThingClassId = ThingClassId("{f8917e12-c9cb-4ea1-a06e-1ce6db2194f3}"); +ParamTypeId virtualIoTemperatureSensorMockSettingsMinTempParamTypeId = ParamTypeId("{803cddbf-94c7-4f35-bc7a-18698b03b942}"); +ParamTypeId virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId = ParamTypeId("{7077c56f-c35b-4252-8c15-8fb549be04ce}"); +StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId = StateTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); +EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId = EventTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); +ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId = ParamTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); +ActionTypeId virtualIoTemperatureSensorMockTemperatureActionTypeId = ActionTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); +ParamTypeId virtualIoTemperatureSensorMockTemperatureActionTemperatureParamTypeId = ParamTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); const QString translations[] { //: The name of the Browser Item ActionType ({00b8f0a8-99ca-4aa4-833d-59eb8d4d6de3}) of ThingClass mock QT_TRANSLATE_NOOP("mock", "Add to favorites"), + //: The name of the ParamType (ThingClass: genericIoMock, ActionType: analogInput1, ID: {ac56977c-cbba-47c6-a827-5735d8b0aed6}) + QT_TRANSLATE_NOOP("mock", "Analog Input 1"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: analogInput1, ID: {ac56977c-cbba-47c6-a827-5735d8b0aed6}) + QT_TRANSLATE_NOOP("mock", "Analog Input 1"), + + //: The name of the StateType ({ac56977c-cbba-47c6-a827-5735d8b0aed6}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Input 1"), + + //: The name of the EventType ({ac56977c-cbba-47c6-a827-5735d8b0aed6}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Input 1 changed"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: analogInput2, ID: {8e07e57e-ba4e-42df-81ee-5b72ed074532}) + QT_TRANSLATE_NOOP("mock", "Analog Input 2"), + + //: The name of the StateType ({8e07e57e-ba4e-42df-81ee-5b72ed074532}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Input 2"), + + //: The name of the EventType ({8e07e57e-ba4e-42df-81ee-5b72ed074532}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Input 2 changed"), + + //: The name of the ParamType (ThingClass: genericIoMock, ActionType: analogOutput1, ID: {70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) + QT_TRANSLATE_NOOP("mock", "Analog Output 1"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: analogOutput1, ID: {70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) + QT_TRANSLATE_NOOP("mock", "Analog Output 1"), + + //: The name of the StateType ({70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Output 1"), + + //: The name of the EventType ({70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Output 1 changed"), + + //: The name of the ParamType (ThingClass: genericIoMock, ActionType: analogOutput2, ID: {e40bcf7d-47b8-41fa-b213-3652a905b376}) + QT_TRANSLATE_NOOP("mock", "Analog Output 2"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: analogOutput2, ID: {e40bcf7d-47b8-41fa-b213-3652a905b376}) + QT_TRANSLATE_NOOP("mock", "Analog Output 2"), + + //: The name of the StateType ({e40bcf7d-47b8-41fa-b213-3652a905b376}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Output 2"), + + //: The name of the EventType ({e40bcf7d-47b8-41fa-b213-3652a905b376}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Analog Output 2 changed"), + //: The name of the ParamType (ThingClass: inputTypeMock, EventType: bool, ID: {3bad3a09-5826-4ed7-a832-10e3e2ee2a7d}) QT_TRANSLATE_NOOP("mock", "Bool"), @@ -273,6 +367,48 @@ const QString translations[] { //: The name of the EventType ({4507d5c6-b692-4bd6-87f2-00364bc0cb4d}) of ThingClass inputTypeMock QT_TRANSLATE_NOOP("mock", "Color changed"), + //: The name of the ParamType (ThingClass: genericIoMock, ActionType: digitalOutput1, ID: {d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) + QT_TRANSLATE_NOOP("mock", "Digital Output 1"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalOutput1, ID: {d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) + QT_TRANSLATE_NOOP("mock", "Digital Output 1"), + + //: The name of the StateType ({d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital Output 1"), + + //: The name of the EventType ({d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital Output 1 changed"), + + //: The name of the ParamType (ThingClass: genericIoMock, ActionType: digitalOutput2, ID: {35de8b68-0cf3-4850-a27d-cf9c4a26921f}) + QT_TRANSLATE_NOOP("mock", "Digital Output 2"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalOutput2, ID: {35de8b68-0cf3-4850-a27d-cf9c4a26921f}) + QT_TRANSLATE_NOOP("mock", "Digital Output 2"), + + //: The name of the StateType ({35de8b68-0cf3-4850-a27d-cf9c4a26921f}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital Output 2"), + + //: The name of the EventType ({35de8b68-0cf3-4850-a27d-cf9c4a26921f}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital Output 2 changed"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalInput1, ID: {07165c12-4d53-45c0-8bf1-34618443b706}) + QT_TRANSLATE_NOOP("mock", "Digital input 1"), + + //: The name of the StateType ({07165c12-4d53-45c0-8bf1-34618443b706}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital input 1"), + + //: The name of the EventType ({07165c12-4d53-45c0-8bf1-34618443b706}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital input 1 changed"), + + //: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalInput2, ID: {0a4362ba-a086-4540-84ba-107ef7b99ed8}) + QT_TRANSLATE_NOOP("mock", "Digital input 2"), + + //: The name of the StateType ({0a4362ba-a086-4540-84ba-107ef7b99ed8}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital input 2"), + + //: The name of the EventType ({0a4362ba-a086-4540-84ba-107ef7b99ed8}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Digital input 2 changed"), + //: The name of the ParamType (ThingClass: inputTypeMock, EventType: double, ID: {f7d2063d-959e-46ac-8568-8b99722d3b22}) QT_TRANSLATE_NOOP("mock", "Double"), @@ -327,6 +463,15 @@ const QString translations[] { //: The name of the EventType ({80baec19-54de-4948-ac46-31eabfaceb83}) of ThingClass mock QT_TRANSLATE_NOOP("mock", "Dummy int state changed"), + //: The name of the ThingClass ({7cbd729a-465b-4ccb-b59c-5733039dbbed}) + QT_TRANSLATE_NOOP("mock", "Generic IO pins"), + + //: The name of the ThingClass ({98ab137e-757e-43f8-9d9b-5d50d990242a}) + QT_TRANSLATE_NOOP("mock", "Generic Light (Mock)"), + + //: The name of the ThingClass ({f8917e12-c9cb-4ea1-a06e-1ce6db2194f3}) + QT_TRANSLATE_NOOP("mock", "Generic Temperature Sensor (Mock)"), + //: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {9e5f86a0-4bb3-4892-bff8-3fc4032af6e2}) QT_TRANSLATE_NOOP("mock", "IPv4 address"), @@ -348,6 +493,12 @@ const QString translations[] { //: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {a8494faf-3a0f-4cf3-84b7-4b39148a838d}) QT_TRANSLATE_NOOP("mock", "Mail address"), + //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, Type: settings, ID: {7077c56f-c35b-4252-8c15-8fb549be04ce}) + QT_TRANSLATE_NOOP("mock", "Maximum temperature"), + + //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, Type: settings, ID: {803cddbf-94c7-4f35-bc7a-18698b03b942}) + QT_TRANSLATE_NOOP("mock", "Minimum temperature"), + //: The name of the ActionType ({07cd8d5f-2f65-4955-b1f9-05d7f4da488a}) of ThingClass autoMock QT_TRANSLATE_NOOP("mock", "Mock Action 1 (with params)"), @@ -426,12 +577,36 @@ const QString translations[] { //: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {e5c0d14b-c9f1-4aca-a56e-85bfa6977150}) QT_TRANSLATE_NOOP("mock", "Password text"), + //: The name of the ParamType (ThingClass: virtualIoLightMock, ActionType: power, ID: {d1917b3d-1530-4cf9-90f7-263ee88e714b}) + QT_TRANSLATE_NOOP("mock", "Power"), + + //: The name of the ParamType (ThingClass: virtualIoLightMock, EventType: power, ID: {d1917b3d-1530-4cf9-90f7-263ee88e714b}) + QT_TRANSLATE_NOOP("mock", "Power"), + + //: The name of the StateType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock + QT_TRANSLATE_NOOP("mock", "Power"), + + //: The name of the EventType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock + QT_TRANSLATE_NOOP("mock", "Power changed"), + //: The name of the Browser Item ActionType ({da6faef8-2816-430e-93bb-57e8f9582d29}) of ThingClass mock QT_TRANSLATE_NOOP("mock", "Remove from favorites"), //: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {22add8c9-ee4f-43ad-8931-58e999313ac3}) QT_TRANSLATE_NOOP("mock", "Search text"), + //: The name of the ActionType ({d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Set Digital Output 1"), + + //: The name of the ActionType ({35de8b68-0cf3-4850-a27d-cf9c4a26921f}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Set Digital Output 2"), + + //: The name of the ActionType ({70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Set Output Input 1"), + + //: The name of the ActionType ({e40bcf7d-47b8-41fa-b213-3652a905b376}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Set Output Input 2"), + //: The name of the ActionType ({a7c11774-f31f-4d64-99d1-e0ae5fb35a5c}) of ThingClass inputTypeMock QT_TRANSLATE_NOOP("mock", "Set Writable Bool"), @@ -477,6 +652,9 @@ const QString translations[] { //: The name of the ActionType ({05f63f9c-f61e-4dcf-ad55-3f13fde2765b}) of ThingClass pushButtonMock QT_TRANSLATE_NOOP("mock", "Set allowed values"), + //: The name of the ActionType ({ac56977c-cbba-47c6-a827-5735d8b0aed6}) of ThingClass genericIoMock + QT_TRANSLATE_NOOP("mock", "Set analog input 1"), + //: The name of the ActionType ({80ba1449-b485-47d4-a067-6bf306e2a568}) of ThingClass childMock QT_TRANSLATE_NOOP("mock", "Set bool value"), @@ -507,6 +685,12 @@ const QString translations[] { //: The name of the ActionType ({72981c04-267a-4ba0-a59e-9921d2f3af9c}) of ThingClass pushButtonMock QT_TRANSLATE_NOOP("mock", "Set percentage"), + //: The name of the ActionType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock + QT_TRANSLATE_NOOP("mock", "Set power"), + + //: The name of the ActionType ({db9cc518-1012-47e2-8212-6e616fed07a6}) of ThingClass virtualIoTemperatureSensorMock + QT_TRANSLATE_NOOP("mock", "Set temperature"), + //: The name of the ParamType (ThingClass: mock, Type: settings, ID: {367f7ba4-5039-47be-abd8-59cc8eaf4b9a}) QT_TRANSLATE_NOOP("mock", "Setting 1"), @@ -519,6 +703,18 @@ const QString translations[] { //: The name of the EventType ({27f69ca9-a321-40ff-bfee-4b0272a671b4}) of ThingClass inputTypeMock QT_TRANSLATE_NOOP("mock", "String changed"), + //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, ActionType: temperature, ID: {db9cc518-1012-47e2-8212-6e616fed07a6}) + QT_TRANSLATE_NOOP("mock", "Temperature"), + + //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, EventType: temperature, ID: {db9cc518-1012-47e2-8212-6e616fed07a6}) + QT_TRANSLATE_NOOP("mock", "Temperature"), + + //: The name of the StateType ({db9cc518-1012-47e2-8212-6e616fed07a6}) of ThingClass virtualIoTemperatureSensorMock + QT_TRANSLATE_NOOP("mock", "Temperature"), + + //: The name of the EventType ({db9cc518-1012-47e2-8212-6e616fed07a6}) of ThingClass virtualIoTemperatureSensorMock + QT_TRANSLATE_NOOP("mock", "Temperature changed"), + //: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {716f0994-bc01-42b0-b64d-59236f7320d2}) QT_TRANSLATE_NOOP("mock", "Text area"), diff --git a/tests/auto/api.json b/tests/auto/api.json index 92665af5..b3a4757c 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -1,4 +1,4 @@ -5.0 +5.1 { "enums": { "BasicType": [ @@ -82,6 +82,13 @@ "DeviceSetupStatusComplete", "DeviceSetupStatusFailed" ], + "IOType": [ + "IOTypeNone", + "IOTypeDigitalInput", + "IOTypeDigitalOutput", + "IOTypeAnalogInput", + "IOTypeAnalogOutput" + ], "InputType": [ "InputTypeNone", "InputTypeTextLine", @@ -937,6 +944,27 @@ "thingError": "$ref:ThingError" } }, + "Integrations.ConnectIO": { + "description": "Connect two generic IO states.", + "params": { + "inputStateTypeId": "Uuid", + "inputThingId": "Uuid", + "outputStateTypeId": "Uuid", + "outputThingId": "Uuid" + }, + "returns": { + "thingError": "$ref:ThingError" + } + }, + "Integrations.DisconnectIO": { + "description": "Disconnect an existing IO connection.", + "params": { + "ioConnectionId": "Uuid" + }, + "returns": { + "thingError": "$ref:ThingError" + } + }, "Integrations.DiscoverThings": { "description": "Performs a thing discovery for things of the given thingClassId and returns the results. This function may take a while to return. Note that this method will include all the found things, that is, including things that may already have been added. Those things will have thingId set to the id of the already added thing. Such results may be used to reconfigure existing things and might be filtered in cases where only unknown things are of interest.", "params": { @@ -1025,6 +1053,15 @@ "eventTypes": "$ref:EventTypes" } }, + "Integrations.GetIOConnections": { + "description": "Fetch IO connections. Optionally filtered by thingId and stateTypeId.", + "params": { + "o:thingId": "Uuid" + }, + "returns": { + "ioConnections": "$ref:IOConnections" + } + }, "Integrations.GetPluginConfiguration": { "description": "Get a plugin's params.", "params": { @@ -2015,6 +2052,18 @@ "event": "$ref:Event" } }, + "Integrations.IOConnectionAdded": { + "description": "Emitted whenever a IO connection is added.", + "params": { + "ioConnection": "$ref:IOConnection" + } + }, + "Integrations.IOConnectionRemoved": { + "description": "Emitted whenever a IO connection is removed.", + "params": { + "ioConnectionId": "Uuid" + } + }, "Integrations.PluginConfigurationChanged": { "description": "Emitted whenever a plugin's configuration is changed.", "params": { @@ -2402,6 +2451,16 @@ "name": "String", "version": "String" }, + "IOConnection": { + "r:id": "Uuid", + "r:inputStateTypeId": "Uuid", + "r:inputThingId": "Uuid", + "r:outputStateTypeId": "Uuid", + "r:outputThingId": "Uuid" + }, + "IOConnections": [ + "$ref:IOConnection" + ], "IntegrationPlugin": { "r:displayName": "String", "r:id": "Uuid", @@ -2586,6 +2645,7 @@ "displayName": "String", "index": "Int", "name": "String", + "o:ioType": "$ref:IOType", "o:maxValue": "Variant", "o:minValue": "Variant", "o:possibleValues": [ diff --git a/tests/auto/integrations/testintegrations.cpp b/tests/auto/integrations/testintegrations.cpp index 801b5a9b..0f82a748 100644 --- a/tests/auto/integrations/testintegrations.cpp +++ b/tests/auto/integrations/testintegrations.cpp @@ -287,8 +287,8 @@ void TestIntegrations::getThingClasses_data() QTest::addColumn("vendorId"); QTest::addColumn("resultCount"); - QTest::newRow("vendor nymea") << nymeaVendorId << 11; - QTest::newRow("no filter") << VendorId() << 11; + QTest::newRow("vendor nymea") << nymeaVendorId << 14; + QTest::newRow("no filter") << VendorId() << 14; QTest::newRow("invalid vendor") << VendorId("93e7d361-8025-4354-b17e-b68406c800bc") << 0; } From bee39045081c0973503546d07f27398f81c691ad Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 28 Apr 2020 17:35:17 +0200 Subject: [PATCH 2/5] Bump lib version as we're breaking the ABI! --- nymea.pro | 2 +- plugins/mock/plugininfo.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nymea.pro b/nymea.pro index 1f3b20ef..82cacc52 100644 --- a/nymea.pro +++ b/nymea.pro @@ -7,7 +7,7 @@ NYMEA_VERSION_STRING=$$system('dpkg-parsechangelog | sed -n -e "s/^Version: //p" JSON_PROTOCOL_VERSION_MAJOR=5 JSON_PROTOCOL_VERSION_MINOR=1 JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}" -LIBNYMEA_API_VERSION_MAJOR=5 +LIBNYMEA_API_VERSION_MAJOR=6 LIBNYMEA_API_VERSION_MINOR=0 LIBNYMEA_API_VERSION_PATCH=0 LIBNYMEA_API_VERSION="$${LIBNYMEA_API_VERSION_MAJOR}.$${LIBNYMEA_API_VERSION_MINOR}.$${LIBNYMEA_API_VERSION_PATCH}" diff --git a/plugins/mock/plugininfo.h b/plugins/mock/plugininfo.h index e1b03571..9f4c7dad 100644 --- a/plugins/mock/plugininfo.h +++ b/plugins/mock/plugininfo.h @@ -9,7 +9,7 @@ #include #include -extern "C" const QString libnymea_api_version() { return QString("5.0.0");} +extern "C" const QString libnymea_api_version() { return QString("6.0.0");} Q_DECLARE_LOGGING_CATEGORY(dcMock) Q_LOGGING_CATEGORY(dcMock, "Mock") From 4e509d75f83409bf4626e3c07fe57a1f53524bc3 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 5 May 2020 19:05:38 +0200 Subject: [PATCH 3/5] Add tests, some fixes/improvements --- .../thingmanagerimplementation.cpp | 37 ++- .../integrations/thingmanagerimplementation.h | 2 +- libnymea-core/jsonrpc/integrationshandler.cpp | 12 +- libnymea/integrations/ioconnection.h | 6 + libnymea/integrations/thingmanager.cpp | 2 +- libnymea/integrations/thingmanager.h | 4 +- plugins/mock/extern-plugininfo.h | 7 +- plugins/mock/integrationpluginmock.cpp | 5 +- plugins/mock/integrationpluginmock.json | 20 +- plugins/mock/plugininfo.h | 28 +- tests/auto/api.json | 3 +- tests/auto/auto.pro | 31 +- tests/auto/ioconnections/ioconnections.pro | 5 + .../auto/ioconnections/testioconnections.cpp | 299 ++++++++++++++++++ 14 files changed, 408 insertions(+), 53 deletions(-) create mode 100644 tests/auto/ioconnections/ioconnections.pro create mode 100644 tests/auto/ioconnections/testioconnections.cpp diff --git a/libnymea-core/integrations/thingmanagerimplementation.cpp b/libnymea-core/integrations/thingmanagerimplementation.cpp index 8bba515a..3516c998 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -928,51 +928,61 @@ IOConnections ThingManagerImplementation::ioConnections(const ThingId &thingId) return ioConnections; } -Thing::ThingError ThingManagerImplementation::connectIO(const IOConnection &connection) +IOConnectionResult ThingManagerImplementation::connectIO(const IOConnection &connection) { + IOConnectionResult result; + // Do some sanity checks Thing *inputThing = m_configuredThings.value(connection.inputThingId()); if (!inputThing) { - qCWarning(dcThingManager()) << "Could not find inputThing" << connection.inputThingId() << "configured things. Not adding IO connection."; - return Thing::ThingErrorThingNotFound; + qCWarning(dcThingManager()) << "Could not find inputThing" << connection.inputThingId() << "in configured things. Not adding IO connection."; + result.error = Thing::ThingErrorThingNotFound; + return result; } if (!inputThing->thingClass().stateTypes().contains(connection.inputStateTypeId())) { qCWarning(dcThingManager()) << "Input thing" << inputThing->name() << "does not have a state with id" << connection.inputStateTypeId(); - return Thing::ThingErrorStateTypeNotFound; + result.error = Thing::ThingErrorStateTypeNotFound; + return result; } StateType inputStateType = inputThing->thingClass().stateTypes().findById(connection.inputStateTypeId()); // Check if this is actually an input if (inputStateType.ioType() != Types::IOTypeDigitalInput && inputStateType.ioType() != Types::IOTypeAnalogInput) { qCWarning(dcThingManager()) << "The given input state is neither a digital nor an analog input."; - return Thing::ThingErrorInvalidParameter; + result.error = Thing::ThingErrorInvalidParameter; + return result; } Thing *outputThing = m_configuredThings.value(connection.outputThingId()); if (!outputThing) { - qCWarning(dcThingManager()) << "Could not find outputThing" << connection.outputThingId() << "configured things. Not adding IO connection."; - return Thing::ThingErrorThingNotFound; + qCWarning(dcThingManager()) << "Could not find outputThing" << connection.outputThingId() << "in configured things. Not adding IO connection."; + result.error = Thing::ThingErrorThingNotFound; + return result; } if (!outputThing->thingClass().stateTypes().contains(connection.outputStateTypeId())) { qCWarning(dcThingManager()) << "Output thing" << outputThing->name() << "does not have a state with id" << connection.outputStateTypeId(); - return Thing::ThingErrorStateTypeNotFound; + result.error = Thing::ThingErrorStateTypeNotFound; + return result; } StateType outputStateType = outputThing->thingClass().stateTypes().findById(connection.outputStateTypeId()); // Check if this is actually an output if (outputStateType.ioType() != Types::IOTypeDigitalOutput && outputStateType.ioType() != Types::IOTypeAnalogOutput) { qCWarning(dcThingManager()) << "The given output state is neither a digital nor an analog output."; - return Thing::ThingErrorInvalidParameter; + result.error = Thing::ThingErrorInvalidParameter; + return result; } // Check if io types are compatible if (inputStateType.ioType() == Types::IOTypeDigitalInput && outputStateType.ioType() != Types::IOTypeDigitalOutput) { qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType(); - return Thing::ThingErrorInvalidParameter; + result.error = Thing::ThingErrorInvalidParameter; + return result; } if (inputStateType.ioType() == Types::IOTypeAnalogInput && outputStateType.ioType() != Types::IOTypeAnalogOutput) { qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType(); - return Thing::ThingErrorInvalidParameter; + result.error = Thing::ThingErrorInvalidParameter; + return result; } // Check if either input or output is already connected @@ -994,7 +1004,10 @@ Thing::ThingError ThingManagerImplementation::connectIO(const IOConnection &conn storeIOConnections(); emit ioConnectionAdded(connection); - return Thing::ThingErrorNoError; + + result.error = Thing::ThingErrorNoError; + result.ioConnectionId = connection.id(); + return result; } Thing::ThingError ThingManagerImplementation::disconnectIO(const IOConnectionId &ioConnectionId) diff --git a/libnymea-core/integrations/thingmanagerimplementation.h b/libnymea-core/integrations/thingmanagerimplementation.h index 8cb343ef..8f579f8b 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.h +++ b/libnymea-core/integrations/thingmanagerimplementation.h @@ -115,7 +115,7 @@ public: BrowserItemActionInfo *executeBrowserItemAction(const BrowserItemAction &browserItemAction) override; IOConnections ioConnections(const ThingId &thingId = ThingId()) const override; - Thing::ThingError connectIO(const IOConnection &connection) override; + IOConnectionResult connectIO(const IOConnection &connection) override; Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) override; QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) override; diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index 0aa6b400..bab65155 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -355,12 +355,14 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa registerMethod("GetIOConnections", description, params, returns); params.clear(); returns.clear(); - description = "Connect two generic IO states."; + description = "Connect two generic IO states. Input and output need to be compatible, that is, either a digital input " + "and a digital output, or an analog input and an analog output. If successful, the connectionId will be returned."; params.insert("inputThingId", enumValueName(Uuid)); params.insert("inputStateTypeId", enumValueName(Uuid)); params.insert("outputThingId", enumValueName(Uuid)); params.insert("outputStateTypeId", enumValueName(Uuid)); returns.insert("thingError", enumRef()); + returns.insert("o:ioConnectionId", enumValueName(Uuid)); registerMethod("ConnectIO", description, params, returns); params.clear(); returns.clear(); @@ -990,8 +992,12 @@ JsonReply *IntegrationsHandler::ConnectIO(const QVariantMap ¶ms) StateTypeId inputStateTypeId = params.value("inputStateTypeId").toUuid(); ThingId outputThingId = params.value("outputThingId").toUuid(); StateTypeId outputStateTypeId = params.value("outputStateTypeId").toUuid(); - Thing::ThingError error = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId); - return createReply(statusToReply(error)); + IOConnectionResult result = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId); + QVariantMap reply = statusToReply(result.error); + if (result.error == Thing::ThingErrorNoError) { + reply.insert("ioConnectionId", result.ioConnectionId); + } + return createReply(reply); } JsonReply *IntegrationsHandler::DisconnectIO(const QVariantMap ¶ms) diff --git a/libnymea/integrations/ioconnection.h b/libnymea/integrations/ioconnection.h index 144abc89..207cbd71 100644 --- a/libnymea/integrations/ioconnection.h +++ b/libnymea/integrations/ioconnection.h @@ -6,6 +6,12 @@ #include #include "typeutils.h" +#include "thing.h" + +struct IOConnectionResult { + Thing::ThingError error = Thing::ThingErrorNoError; + IOConnectionId ioConnectionId; +}; class IOConnection { diff --git a/libnymea/integrations/thingmanager.cpp b/libnymea/integrations/thingmanager.cpp index 21751eb5..53d41a47 100644 --- a/libnymea/integrations/thingmanager.cpp +++ b/libnymea/integrations/thingmanager.cpp @@ -51,7 +51,7 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent) qRegisterMetaType(); } -Thing::ThingError ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState) +IOConnectionResult ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState) { IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState); return connectIO(connection); diff --git a/libnymea/integrations/thingmanager.h b/libnymea/integrations/thingmanager.h index 7c1dc417..0b234591 100644 --- a/libnymea/integrations/thingmanager.h +++ b/libnymea/integrations/thingmanager.h @@ -91,7 +91,7 @@ public: virtual BrowserItemActionInfo* executeBrowserItemAction(const BrowserItemAction &browserItemAction) = 0; virtual IOConnections ioConnections(const ThingId &thingId = ThingId()) const = 0; - Thing::ThingError connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState); + IOConnectionResult connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState); virtual Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) = 0; virtual QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) = 0; @@ -100,7 +100,7 @@ public: virtual Vendor translateVendor(const Vendor &vendor, const QLocale &locale) = 0; protected: - virtual Thing::ThingError connectIO(const IOConnection &connection) = 0; + virtual IOConnectionResult connectIO(const IOConnection &connection) = 0; signals: void pluginConfigChanged(const PluginId &id, const ParamList &config); diff --git a/plugins/mock/extern-plugininfo.h b/plugins/mock/extern-plugininfo.h index aca86972..3b92d668 100644 --- a/plugins/mock/extern-plugininfo.h +++ b/plugins/mock/extern-plugininfo.h @@ -290,10 +290,13 @@ extern ParamTypeId virtualIoLightMockPowerActionPowerParamTypeId; extern ThingClassId virtualIoTemperatureSensorMockThingClassId; extern ParamTypeId virtualIoTemperatureSensorMockSettingsMinTempParamTypeId; extern ParamTypeId virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId; +extern StateTypeId virtualIoTemperatureSensorMockInputStateTypeId; extern StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId; +extern EventTypeId virtualIoTemperatureSensorMockInputEventTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockInputEventInputParamTypeId; extern EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId; extern ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId; -extern ActionTypeId virtualIoTemperatureSensorMockTemperatureActionTypeId; -extern ParamTypeId virtualIoTemperatureSensorMockTemperatureActionTemperatureParamTypeId; +extern ActionTypeId virtualIoTemperatureSensorMockInputActionTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockInputActionInputParamTypeId; #endif // EXTERNPLUGININFO_H diff --git a/plugins/mock/integrationpluginmock.cpp b/plugins/mock/integrationpluginmock.cpp index bbf0ff8b..409cb268 100644 --- a/plugins/mock/integrationpluginmock.cpp +++ b/plugins/mock/integrationpluginmock.cpp @@ -748,11 +748,12 @@ void IntegrationPluginMock::executeAction(ThingActionInfo *info) } if (info->thing()->thingClassId() == virtualIoTemperatureSensorMockThingClassId) { - if (info->action().actionTypeId() == virtualIoTemperatureSensorMockTemperatureActionTypeId) { + if (info->action().actionTypeId() == virtualIoTemperatureSensorMockInputActionTypeId) { double minTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMinTempParamTypeId).toDouble(); double maxTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId).toDouble(); - double value = info->action().param(virtualIoTemperatureSensorMockTemperatureActionTemperatureParamTypeId).value().toDouble(); + double value = info->action().param(virtualIoTemperatureSensorMockInputActionInputParamTypeId).value().toDouble(); double temp = minTemp + (maxTemp - minTemp) * value; + qCDebug(dcMock()) << "Min:" << minTemp << "Max:" << maxTemp << "value:" << value << "temp:" << temp; info->thing()->setStateValue(virtualIoTemperatureSensorMockTemperatureStateTypeId, temp); info->finish(Thing::ThingErrorNoError); return; diff --git a/plugins/mock/integrationpluginmock.json b/plugins/mock/integrationpluginmock.json index 0931a01e..cd67c5e0 100644 --- a/plugins/mock/integrationpluginmock.json +++ b/plugins/mock/integrationpluginmock.json @@ -1016,18 +1016,26 @@ ], "stateTypes": [ { - "id": "db9cc518-1012-47e2-8212-6e616fed07a6", - "name": "temperature", - "displayName": "Temperature", - "displayNameEvent": "Temperature changed", - "displayNameAction": "Set temperature", + "id": "fd341f72-6d9a-4812-9f66-47197c48a935", + "name": "input", + "displayName": "Input", + "displayNameEvent": "Input changed", + "displayNameAction": "Set input", "type": "double", - "unit": "DegreeCelsius", "defaultValue": 0, "ioType": "analogOutput", "writable": true, "minValue": 0, "maxValue": 1 + }, + { + "id": "db9cc518-1012-47e2-8212-6e616fed07a6", + "name": "temperature", + "displayName": "Temperature", + "displayNameEvent": "Temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0 } ] } diff --git a/plugins/mock/plugininfo.h b/plugins/mock/plugininfo.h index 9f4c7dad..ef097a61 100644 --- a/plugins/mock/plugininfo.h +++ b/plugins/mock/plugininfo.h @@ -294,11 +294,14 @@ ParamTypeId virtualIoLightMockPowerActionPowerParamTypeId = ParamTypeId("{d1917b ThingClassId virtualIoTemperatureSensorMockThingClassId = ThingClassId("{f8917e12-c9cb-4ea1-a06e-1ce6db2194f3}"); ParamTypeId virtualIoTemperatureSensorMockSettingsMinTempParamTypeId = ParamTypeId("{803cddbf-94c7-4f35-bc7a-18698b03b942}"); ParamTypeId virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId = ParamTypeId("{7077c56f-c35b-4252-8c15-8fb549be04ce}"); +StateTypeId virtualIoTemperatureSensorMockInputStateTypeId = StateTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}"); StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId = StateTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); +EventTypeId virtualIoTemperatureSensorMockInputEventTypeId = EventTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}"); +ParamTypeId virtualIoTemperatureSensorMockInputEventInputParamTypeId = ParamTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}"); EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId = EventTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId = ParamTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); -ActionTypeId virtualIoTemperatureSensorMockTemperatureActionTypeId = ActionTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); -ParamTypeId virtualIoTemperatureSensorMockTemperatureActionTemperatureParamTypeId = ParamTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}"); +ActionTypeId virtualIoTemperatureSensorMockInputActionTypeId = ActionTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}"); +ParamTypeId virtualIoTemperatureSensorMockInputActionInputParamTypeId = ParamTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}"); const QString translations[] { //: The name of the Browser Item ActionType ({00b8f0a8-99ca-4aa4-833d-59eb8d4d6de3}) of ThingClass mock @@ -478,6 +481,18 @@ const QString translations[] { //: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {43bf3832-dd48-4090-a836-656e8b60216e}) QT_TRANSLATE_NOOP("mock", "IPv6 address"), + //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, ActionType: input, ID: {fd341f72-6d9a-4812-9f66-47197c48a935}) + QT_TRANSLATE_NOOP("mock", "Input"), + + //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, EventType: input, ID: {fd341f72-6d9a-4812-9f66-47197c48a935}) + QT_TRANSLATE_NOOP("mock", "Input"), + + //: The name of the StateType ({fd341f72-6d9a-4812-9f66-47197c48a935}) of ThingClass virtualIoTemperatureSensorMock + QT_TRANSLATE_NOOP("mock", "Input"), + + //: The name of the EventType ({fd341f72-6d9a-4812-9f66-47197c48a935}) of ThingClass virtualIoTemperatureSensorMock + QT_TRANSLATE_NOOP("mock", "Input changed"), + //: The name of the ParamType (ThingClass: inputTypeMock, EventType: int, ID: {d0fc56ae-5791-4e91-b76c-dadfbc7e7dbb}) QT_TRANSLATE_NOOP("mock", "Int"), @@ -679,6 +694,9 @@ const QString translations[] { //: The name of the ActionType ({53cd7c55-49b7-441b-b970-9048f20f0e2c}) of ThingClass pushButtonMock QT_TRANSLATE_NOOP("mock", "Set double value"), + //: The name of the ActionType ({fd341f72-6d9a-4812-9f66-47197c48a935}) of ThingClass virtualIoTemperatureSensorMock + QT_TRANSLATE_NOOP("mock", "Set input"), + //: The name of the ActionType ({527f0687-0b28-4c26-852c-25b8f83e4797}) of ThingClass displayPinMock QT_TRANSLATE_NOOP("mock", "Set percentage"), @@ -688,9 +706,6 @@ const QString translations[] { //: The name of the ActionType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock QT_TRANSLATE_NOOP("mock", "Set power"), - //: The name of the ActionType ({db9cc518-1012-47e2-8212-6e616fed07a6}) of ThingClass virtualIoTemperatureSensorMock - QT_TRANSLATE_NOOP("mock", "Set temperature"), - //: The name of the ParamType (ThingClass: mock, Type: settings, ID: {367f7ba4-5039-47be-abd8-59cc8eaf4b9a}) QT_TRANSLATE_NOOP("mock", "Setting 1"), @@ -703,9 +718,6 @@ const QString translations[] { //: The name of the EventType ({27f69ca9-a321-40ff-bfee-4b0272a671b4}) of ThingClass inputTypeMock QT_TRANSLATE_NOOP("mock", "String changed"), - //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, ActionType: temperature, ID: {db9cc518-1012-47e2-8212-6e616fed07a6}) - QT_TRANSLATE_NOOP("mock", "Temperature"), - //: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, EventType: temperature, ID: {db9cc518-1012-47e2-8212-6e616fed07a6}) QT_TRANSLATE_NOOP("mock", "Temperature"), diff --git a/tests/auto/api.json b/tests/auto/api.json index b3a4757c..3785c079 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -945,7 +945,7 @@ } }, "Integrations.ConnectIO": { - "description": "Connect two generic IO states.", + "description": "Connect two generic IO states. Input and output need to be compatible, that is, either a digital input and a digital output, or an analog input and an analog output. If successful, the connectionId will be returned.", "params": { "inputStateTypeId": "Uuid", "inputThingId": "Uuid", @@ -953,6 +953,7 @@ "outputThingId": "Uuid" }, "returns": { + "o:ioConnectionId": "Uuid", "thingError": "$ref:ThingError" } }, diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index 3c5e589b..c6431395 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,26 +1,27 @@ TEMPLATE = subdirs SUBDIRS = \ - versioning \ - devices \ - integrations \ - jsonrpc \ - events \ - states \ actions \ - rules \ - plugins \ - webserver \ - websocketserver \ + configurations \ + devices \ + events \ + integrations \ + ioconnections \ + jsonrpc \ logging \ loggingdirect \ loggingloading \ - #coap \ # temporary removed until fixed - configurations \ + mqttbroker \ + plugins \ + rules \ + scripts \ + states \ + tags \ timemanager \ userloading \ usermanager \ - mqttbroker \ - tags \ - scripts \ + versioning \ + webserver \ + websocketserver \ + #coap \ # temporary removed until fixed diff --git a/tests/auto/ioconnections/ioconnections.pro b/tests/auto/ioconnections/ioconnections.pro new file mode 100644 index 00000000..0ea01980 --- /dev/null +++ b/tests/auto/ioconnections/ioconnections.pro @@ -0,0 +1,5 @@ +include(../../../nymea.pri) +include(../autotests.pri) + +TARGET = testioconnections +SOURCES += testioconnections.cpp diff --git a/tests/auto/ioconnections/testioconnections.cpp b/tests/auto/ioconnections/testioconnections.cpp new file mode 100644 index 00000000..1bda1c73 --- /dev/null +++ b/tests/auto/ioconnections/testioconnections.cpp @@ -0,0 +1,299 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "nymeatestbase.h" +#include "nymeacore.h" +#include "nymeasettings.h" + +#include "integrations/thingdiscoveryinfo.h" +#include "integrations/thingsetupinfo.h" + +#include "servers/mocktcpserver.h" +#include "jsonrpc/integrationshandler.h" + +using namespace nymeaserver; + +class TestIOConnections : public NymeaTestBase +{ + Q_OBJECT + +private: + ThingId m_ioThingId; + ThingId m_lightThingId; + ThingId m_tempSensorThingId; + + inline void verifyThingError(const QVariant &response, Thing::ThingError error = Thing::ThingErrorNoError) { + verifyError(response, "thingError", enumValueName(error)); + } + +private slots: + + void initTestCase(); + + void testConnectionCompatibility_data(); + void testConnectionCompatibility(); + + void testDigitalIO(); + + void testAnalogIO(); +}; + +void TestIOConnections::initTestCase() +{ + NymeaTestBase::initTestCase(); + QLoggingCategory::setFilterRules("*.debug=false\n" + "Tests.debug=true\n" + "Mock.debug=true\n" + ); + + // Adding generic IO mock + QVariantMap params; + params.insert("thingClassId", genericIoMockThingClassId); + params.insert("name", "Generic IO mock"); + QVariant response = injectAndWait("Integrations.AddThing", params); + m_ioThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString()); + QVERIFY2(!m_ioThingId.isNull(), "Creating generic IO mock failed"); + qCDebug(dcTests()) << "Created IO mock with ID" << m_ioThingId; + + // Adding virtual light (digital input) + params.clear(); + params.insert("thingClassId", virtualIoLightMockThingClassId); + params.insert("name", "light"); + response = injectAndWait("Integrations.AddThing", params); + m_lightThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toUuid()); + QVERIFY2(!m_lightThingId.isNull(), "Creating virtual light failed"); + + // Adding virtual temp sensor (analog output) + params.clear(); + params.insert("thingClassId", virtualIoTemperatureSensorMockThingClassId); + params.insert("name", "temp sensor"); + response = injectAndWait("Integrations.AddThing", params); + m_tempSensorThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toUuid()); + QVERIFY2(!m_tempSensorThingId.isNull(), "Creating virtual temp sensor failed"); +} + +void TestIOConnections::testConnectionCompatibility_data() +{ + QTest::addColumn("inputThingId"); + QTest::addColumn("inputStateTypeId"); + QTest::addColumn("outputThingId"); + QTest::addColumn("outputStateTypeId"); + QTest::addColumn("expectedError"); + + QTest::newRow("digital in, digital in") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("digital in, digital out") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorNoError; + QTest::newRow("digital in, analog in") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("digital in, analog out") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorInvalidParameter; + + QTest::newRow("digital out, digital in") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("digital out, digital out") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("digital out, analog in") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("digital out, analot out") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorInvalidParameter; + + QTest::newRow("analog in, digital in") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("analog in, digital out") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("analog in, analog in") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("analog in, analog out") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorNoError; + + QTest::newRow("analog out, digital in") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("analog out, digital out") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("analog out, analog in") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter; + QTest::newRow("analog out, analog out") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorInvalidParameter; + + QTest::newRow("valid input, invalid output thing") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << ThingId("707d5093-4915-499e-8e69-10c11972bb34") << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorThingNotFound; + QTest::newRow("valid input, invalid output stateType") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << StateTypeId("51534cd7-8adf-4bdc-a4c1-042d0a9d4faa") << Thing::ThingErrorStateTypeNotFound; + QTest::newRow("invalid input thing, valid output") << ThingId("99843693-8615-416a-8e59-a47b050f5c1a") << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorThingNotFound; + QTest::newRow("invalid input stateType, valid output") << m_ioThingId << StateTypeId("04657948-e349-4f43-bf20-13f9986ad1b4") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorStateTypeNotFound; +} + +void TestIOConnections::testConnectionCompatibility() +{ + QFETCH(ThingId, inputThingId); + QFETCH(StateTypeId, inputStateTypeId); + QFETCH(ThingId, outputThingId); + QFETCH(StateTypeId, outputStateTypeId); + QFETCH(Thing::ThingError, expectedError); + + QVariantMap params; + params.insert("inputThingId", inputThingId); + params.insert("inputStateTypeId", inputStateTypeId); + params.insert("outputThingId", outputThingId); + params.insert("outputStateTypeId", outputStateTypeId); + QVariant response = injectAndWait("Integrations.ConnectIO", params); + verifyThingError(response, expectedError); + +} + +void TestIOConnections::testDigitalIO() +{ + QVariantMap params; + params.insert("inputThingId", m_lightThingId); + params.insert("inputStateTypeId", virtualIoLightMockPowerStateTypeId); + params.insert("outputThingId", m_ioThingId); + params.insert("outputStateTypeId", genericIoMockDigitalOutput1StateTypeId); + QVariant response = injectAndWait("Integrations.ConnectIO", params); + verifyThingError(response); + IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid(); + + // verify both, input and out are off + params.clear(); + params.insert("thingId", m_lightThingId); + params.insert("stateTypeId", virtualIoLightMockPowerStateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == false, "Light isn't turned off"); + + params.clear(); + params.insert("thingId", m_ioThingId); + params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == false, "Digital output isn't turned off"); + + // Turn on light and verify digital output went on + params.clear(); + params.insert("thingId", m_lightThingId); + params.insert("actionTypeId", virtualIoLightMockPowerActionTypeId); + QVariantMap actionParam; + actionParam.insert("paramTypeId", virtualIoLightMockPowerActionPowerParamTypeId); + actionParam.insert("value", true); + params.insert("params", QVariantList() << actionParam); + response = injectAndWait("Integrations.ExecuteAction", params); + verifyThingError(response); + + params.clear(); + params.insert("thingId", m_ioThingId); + params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == true, "Digital output isn't turned on"); + + // Disconnect IO again + params.clear(); + params.insert("ioConnectionId", ioConnectionId); + response = injectAndWait("Integrations.DisconnectIO", params); + verifyThingError(response); + + // Turn off the light and verify digital output is still on + params.clear(); + params.insert("thingId", m_lightThingId); + params.insert("actionTypeId", virtualIoLightMockPowerActionTypeId); + actionParam.clear(); + actionParam.insert("paramTypeId", virtualIoLightMockPowerActionPowerParamTypeId); + actionParam.insert("value", false); + params.insert("params", QVariantList() << actionParam); + response = injectAndWait("Integrations.ExecuteAction", params); + verifyThingError(response); + + params.clear(); + params.insert("thingId", m_ioThingId); + params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == true, "Digital output turned off while it should not"); +} + +void TestIOConnections::testAnalogIO() +{ + QVariantMap params; + params.insert("inputThingId", m_ioThingId); + params.insert("inputStateTypeId", genericIoMockAnalogInput1StateTypeId); + params.insert("outputThingId", m_tempSensorThingId); + params.insert("outputStateTypeId", virtualIoTemperatureSensorMockInputStateTypeId); + QVariant response = injectAndWait("Integrations.ConnectIO", params); + verifyThingError(response); + IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid(); + + // verify input is at 0, temp at 0 too + params.clear(); + params.insert("thingId", m_ioThingId); + params.insert("stateTypeId", genericIoMockAnalogInput1StateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), 0), "Input isn't at 0"); + + params.clear(); + params.insert("thingId", m_tempSensorThingId); + params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), 0), QString("Temp sensor is not at 0 but at %1").arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8()); + + + // set analog input to 0.5 and verify temp aligned + params.clear(); + params.insert("thingId", m_ioThingId); + params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId); + QVariantMap actionParam; + actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId); + actionParam.insert("value", 1.65); // goes from 0 to 3.3 + params.insert("params", QVariantList() << actionParam); + response = injectAndWait("Integrations.ExecuteAction", params); + verifyThingError(response); + + params.clear(); + params.insert("thingId", m_tempSensorThingId); + params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + // generic IO output goes from 0 to 3.3. We're setting 1.65V which 50% + // temp goes from -20 to 50. A input of 1.65 should output a temperature of 15°C + double expectedTemp = 70.0 / 2 - 20; + QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8()); + + // Disconnect IO again + params.clear(); + params.insert("ioConnectionId", ioConnectionId); + response = injectAndWait("Integrations.DisconnectIO", params); + verifyThingError(response); + + // set analog input to 3 and verify temp is still at the old expectedTemp + params.clear(); + params.insert("thingId", m_ioThingId); + params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId); + actionParam.clear(); + actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId); + actionParam.insert("value", 3); // goes from 0 to 3.3 + params.insert("params", QVariantList() << actionParam); + response = injectAndWait("Integrations.ExecuteAction", params); + verifyThingError(response); + + params.clear(); + params.insert("thingId", m_tempSensorThingId); + params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId); + response = injectAndWait("Integrations.GetStateValue", params); + verifyThingError(response); + QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8()); + +} + +#include "testioconnections.moc" +QTEST_MAIN(TestIOConnections) + From 322bcf56a600f56e6320b212102b89c3aa7f493a Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 9 May 2020 15:14:11 +0200 Subject: [PATCH 4/5] Add support for inverting the connections --- .../thingmanagerimplementation.cpp | 44 ++++++++---- .../integrations/thingmanagerimplementation.h | 3 +- libnymea-core/jsonrpc/actionhandler.cpp | 2 +- libnymea-core/jsonrpc/integrationshandler.cpp | 4 +- libnymea/integrations/ioconnection.cpp | 10 ++- libnymea/integrations/ioconnection.h | 6 +- libnymea/integrations/thingmanager.cpp | 4 +- libnymea/integrations/thingmanager.h | 2 +- plugins/mock/integrationpluginmock.cpp | 4 +- plugins/mock/integrationpluginmock.json | 2 +- tests/auto/api.json | 2 + .../auto/ioconnections/testioconnections.cpp | 70 ++++++++++++++----- 12 files changed, 112 insertions(+), 41 deletions(-) diff --git a/libnymea-core/integrations/thingmanagerimplementation.cpp b/libnymea-core/integrations/thingmanagerimplementation.cpp index 3516c998..568c8cd9 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -1005,6 +1005,11 @@ IOConnectionResult ThingManagerImplementation::connectIO(const IOConnection &con emit ioConnectionAdded(connection); + qCDebug(dcThingManager()) << "IO connected added:" << inputThing << "->" << outputThing; + + // Sync initial state + syncIOConnection(inputThing, connection.inputStateTypeId()); + result.error = Thing::ThingErrorNoError; result.ioConnectionId = connection.id(); return result; @@ -1736,9 +1741,18 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s Event event(EventTypeId(stateTypeId.toString()), thing->id(), ParamList() << valueParam, true); emit eventTriggered(event); + syncIOConnection(thing, stateTypeId); +} + +void ThingManagerImplementation::syncIOConnection(Thing *thing, const StateTypeId &stateTypeId) +{ + foreach (const IOConnection &ioConnection, m_ioConnections) { // Check if this state is an input to an IO connection. if (ioConnection.inputThingId() == thing->id() && ioConnection.inputStateTypeId() == stateTypeId) { + Thing *inputThing = thing; + QVariant inputValue = inputThing->stateValue(stateTypeId); + Thing *outputThing = m_configuredThings.value(ioConnection.outputThingId()); if (!outputThing) { qCWarning(dcThingManager()) << "IO connection contains invalid output thing!"; @@ -1749,7 +1763,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s qCWarning(dcThingManager()) << "Plugin not found for IO connection's output action."; continue; } - StateType inputStateType = thing->thingClass().getStateType(stateTypeId); + StateType inputStateType = inputThing->thingClass().getStateType(stateTypeId); StateType outputStateType = outputThing->thingClass().getStateType(ioConnection.outputStateTypeId()); if (outputStateType.id().isNull()) { @@ -1759,7 +1773,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s QVariant outputValue; if (outputStateType.ioType() == Types::IOTypeDigitalOutput) { // Digital IOs are mapped as-is - outputValue = value; + outputValue = ioConnection.inverted() xor inputValue.toBool(); // We're already in sync! Skipping action. if (outputThing->stateValue(outputStateType.id()) == outputValue) { @@ -1767,10 +1781,10 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s } } else { // Analog IOs are mapped within the according min/max ranges - outputValue = mapValue(value, inputStateType, outputStateType); + outputValue = mapValue(inputValue, inputStateType, outputStateType, ioConnection.inverted()); // We're already in sync (fuzzy, good enough)! Skipping action. - if (qFuzzyCompare(outputThing->stateValue(outputStateType.id()).toDouble(), outputValue.toDouble())) { + if (qFuzzyCompare(1.0 + outputThing->stateValue(outputStateType.id()).toDouble(), 1.0 + outputValue.toDouble())) { continue; } } @@ -1785,9 +1799,9 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s // An error happened... let's switch the input back to be in sync with the output qCWarning(dcThingManager()) << "Error syncing IO connection state. Reverting input back to old value."; if (inputStateType.ioType() == Types::IOTypeDigitalInput) { - thing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id())); + inputThing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id())); } else { - thing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType)); + inputThing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType, ioConnection.inverted())); } } }); @@ -1795,6 +1809,9 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s // Now check if this is an output state type and - if possible - update the inputs for bidirectional connections if (ioConnection.outputThingId() == thing->id() && ioConnection.outputStateTypeId() == stateTypeId) { + Thing *outputThing = thing; + QVariant outputValue = outputThing->stateValue(stateTypeId); + Thing *inputThing = m_configuredThings.value(ioConnection.inputThingId()); if (!inputThing) { qCWarning(dcThingManager()) << "IO connection contains invalid input thing!"; @@ -1805,7 +1822,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s qCWarning(dcThingManager()) << "Plugin not found for IO connection's input action."; continue; } - StateType outputStateType = thing->thingClass().getStateType(stateTypeId); + StateType outputStateType = outputThing->thingClass().getStateType(stateTypeId); StateType inputStateType = inputThing->thingClass().getStateType(ioConnection.inputStateTypeId()); if (inputStateType.id().isNull()) { @@ -1821,7 +1838,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s QVariant inputValue; if (inputStateType.ioType() == Types::IOTypeDigitalInput) { // Digital IOs are mapped as-is - inputValue = value; + inputValue = ioConnection.inverted() xor outputValue.toBool(); // Prevent looping if (inputThing->stateValue(inputStateType.id()) == inputValue) { @@ -1829,10 +1846,10 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s } } else { // Analog IOs are mapped within the according min/max ranges - inputValue = mapValue(value, outputStateType, inputStateType); + inputValue = mapValue(outputValue, outputStateType, inputStateType, ioConnection.inverted()); // Prevent looping even if the above calculation has rounding errors... Just skip this action if we're close enough already - if (qFuzzyCompare(inputThing->stateValue(inputStateType.id()).toDouble(), inputValue.toDouble())) { + if (qFuzzyCompare(1.0 + inputThing->stateValue(inputStateType.id()).toDouble(), 1.0 + inputValue.toDouble())) { continue; } } @@ -2000,6 +2017,7 @@ void ThingManagerImplementation::storeIOConnections() connectionSettings.setValue("inputStateTypeId", ioConnection.inputStateTypeId().toString()); connectionSettings.setValue("outputThingId", ioConnection.outputThingId().toString()); connectionSettings.setValue("outputStateTypeId", ioConnection.outputStateTypeId().toString()); + connectionSettings.setValue("inverted", ioConnection.inverted()); connectionSettings.endGroup(); } @@ -2017,14 +2035,15 @@ void ThingManagerImplementation::loadIOConnections() StateTypeId inputStateTypeId = connectionSettings.value("inputStateTypeId").toUuid(); ThingId outputThingId = connectionSettings.value("outputThingId").toUuid(); StateTypeId outputStateTypeId = connectionSettings.value("outputStateTypeId").toUuid(); - IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId); + bool inverted = connectionSettings.value("inverted").toBool(); + IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted); m_ioConnections.insert(id, ioConnection); connectionSettings.endGroup(); } connectionSettings.endGroup(); } -QVariant ThingManagerImplementation::mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType) const +QVariant ThingManagerImplementation::mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const { double fromMin = fromStateType.minValue().toDouble(); double fromMax = fromStateType.maxValue().toDouble(); @@ -2032,6 +2051,7 @@ QVariant ThingManagerImplementation::mapValue(const QVariant &value, const State double toMax = toStateType.maxValue().toDouble(); double fromValue = value.toDouble(); double fromPercent = (fromValue - fromMin) / (fromMax - fromMin); + fromPercent = inverted ? 1 - fromPercent : fromPercent; double toValue = toMin + (toMax - toMin) * fromPercent; return toValue; } diff --git a/libnymea-core/integrations/thingmanagerimplementation.h b/libnymea-core/integrations/thingmanagerimplementation.h index 8f579f8b..51f51e4d 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.h +++ b/libnymea-core/integrations/thingmanagerimplementation.h @@ -157,7 +157,8 @@ private: void storeIOConnections(); void loadIOConnections(); - QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType) const; + void syncIOConnection(Thing *inputThing, const StateTypeId &stateTypeId); + QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const; private: HardwareManager *m_hardwareManager; diff --git a/libnymea-core/jsonrpc/actionhandler.cpp b/libnymea-core/jsonrpc/actionhandler.cpp index 21915436..4a6721fe 100644 --- a/libnymea-core/jsonrpc/actionhandler.cpp +++ b/libnymea-core/jsonrpc/actionhandler.cpp @@ -176,7 +176,7 @@ JsonReply *ActionHandler::ExecuteBrowserItemAction(const QVariantMap ¶ms) BrowserItemActionInfo *info = NymeaCore::instance()->executeBrowserItemAction(browserItemAction); connect(info, &BrowserItemActionInfo::finished, jsonReply, [info, jsonReply](){ QVariantMap data; - data.insert("deviceError", enumValueName(info->status()).replace("ThingError", "DeviceError")); + data.insert("deviceError", enumValueName(info->status()).replace("Thing", "Device")); jsonReply->setData(data); jsonReply->finished(); }); diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index bab65155..158c4f8c 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -361,6 +361,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("inputStateTypeId", enumValueName(Uuid)); params.insert("outputThingId", enumValueName(Uuid)); params.insert("outputStateTypeId", enumValueName(Uuid)); + params.insert("o:inverted", enumValueName(Bool)); returns.insert("thingError", enumRef()); returns.insert("o:ioConnectionId", enumValueName(Uuid)); registerMethod("ConnectIO", description, params, returns); @@ -992,7 +993,8 @@ JsonReply *IntegrationsHandler::ConnectIO(const QVariantMap ¶ms) StateTypeId inputStateTypeId = params.value("inputStateTypeId").toUuid(); ThingId outputThingId = params.value("outputThingId").toUuid(); StateTypeId outputStateTypeId = params.value("outputStateTypeId").toUuid(); - IOConnectionResult result = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId); + bool inverted = params.value("inverted", false).toBool(); + IOConnectionResult result = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted); QVariantMap reply = statusToReply(result.error); if (result.error == Thing::ThingErrorNoError) { reply.insert("ioConnectionId", result.ioConnectionId); diff --git a/libnymea/integrations/ioconnection.cpp b/libnymea/integrations/ioconnection.cpp index e408df54..b56aa53b 100644 --- a/libnymea/integrations/ioconnection.cpp +++ b/libnymea/integrations/ioconnection.cpp @@ -5,12 +5,13 @@ IOConnection::IOConnection() } -IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState): +IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted): m_id(id), m_inputThingId(inputThing), m_inputStateTypeId(inputState), m_outputThingId(outputThing), - m_outputStateTypeId(outputState) + m_outputStateTypeId(outputState), + m_inverted(inverted) { } @@ -40,6 +41,11 @@ StateTypeId IOConnection::outputStateTypeId() const return m_outputStateTypeId; } +bool IOConnection::inverted() const +{ + return m_inverted; +} + QVariant IOConnections::get(int index) const { return QVariant::fromValue(at(index)); diff --git a/libnymea/integrations/ioconnection.h b/libnymea/integrations/ioconnection.h index 207cbd71..7cb124de 100644 --- a/libnymea/integrations/ioconnection.h +++ b/libnymea/integrations/ioconnection.h @@ -21,10 +21,11 @@ class IOConnection Q_PROPERTY(QUuid inputStateTypeId READ inputStateTypeId) Q_PROPERTY(QUuid outputThingId READ outputThingId) Q_PROPERTY(QUuid outputStateTypeId READ outputStateTypeId) + Q_PROPERTY(bool inverted READ inverted) public: IOConnection(); - IOConnection(const IOConnectionId &id, const ThingId &inputThingId, const StateTypeId &inputStateTypeId, const ThingId &outputThingId, const StateTypeId &outputStateTypeId); + IOConnection(const IOConnectionId &id, const ThingId &inputThingId, const StateTypeId &inputStateTypeId, const ThingId &outputThingId, const StateTypeId &outputStateTypeId, bool inverted = false); IOConnectionId id() const; @@ -34,12 +35,15 @@ public: ThingId outputThingId() const; StateTypeId outputStateTypeId() const; + bool inverted() const; + private: IOConnectionId m_id; ThingId m_inputThingId; StateTypeId m_inputStateTypeId; ThingId m_outputThingId; StateTypeId m_outputStateTypeId; + bool m_inverted = false; }; class IOConnections: public QList diff --git a/libnymea/integrations/thingmanager.cpp b/libnymea/integrations/thingmanager.cpp index 53d41a47..c427cda3 100644 --- a/libnymea/integrations/thingmanager.cpp +++ b/libnymea/integrations/thingmanager.cpp @@ -51,8 +51,8 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent) qRegisterMetaType(); } -IOConnectionResult ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState) +IOConnectionResult ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted) { - IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState); + IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState, inverted); return connectIO(connection); } diff --git a/libnymea/integrations/thingmanager.h b/libnymea/integrations/thingmanager.h index 0b234591..c846845e 100644 --- a/libnymea/integrations/thingmanager.h +++ b/libnymea/integrations/thingmanager.h @@ -91,7 +91,7 @@ public: virtual BrowserItemActionInfo* executeBrowserItemAction(const BrowserItemAction &browserItemAction) = 0; virtual IOConnections ioConnections(const ThingId &thingId = ThingId()) const = 0; - IOConnectionResult connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState); + IOConnectionResult connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted = false); virtual Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) = 0; virtual QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) = 0; diff --git a/plugins/mock/integrationpluginmock.cpp b/plugins/mock/integrationpluginmock.cpp index 409cb268..42c5cc6b 100644 --- a/plugins/mock/integrationpluginmock.cpp +++ b/plugins/mock/integrationpluginmock.cpp @@ -749,11 +749,11 @@ void IntegrationPluginMock::executeAction(ThingActionInfo *info) if (info->thing()->thingClassId() == virtualIoTemperatureSensorMockThingClassId) { if (info->action().actionTypeId() == virtualIoTemperatureSensorMockInputActionTypeId) { + double value = info->action().param(virtualIoTemperatureSensorMockInputActionInputParamTypeId).value().toDouble(); + info->thing()->setStateValue(virtualIoTemperatureSensorMockInputStateTypeId, value); double minTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMinTempParamTypeId).toDouble(); double maxTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId).toDouble(); - double value = info->action().param(virtualIoTemperatureSensorMockInputActionInputParamTypeId).value().toDouble(); double temp = minTemp + (maxTemp - minTemp) * value; - qCDebug(dcMock()) << "Min:" << minTemp << "Max:" << maxTemp << "value:" << value << "temp:" << temp; info->thing()->setStateValue(virtualIoTemperatureSensorMockTemperatureStateTypeId, temp); info->finish(Thing::ThingErrorNoError); return; diff --git a/plugins/mock/integrationpluginmock.json b/plugins/mock/integrationpluginmock.json index cd67c5e0..eb4713d3 100644 --- a/plugins/mock/integrationpluginmock.json +++ b/plugins/mock/integrationpluginmock.json @@ -1035,7 +1035,7 @@ "displayNameEvent": "Temperature changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 0 + "defaultValue": -20 } ] } diff --git a/tests/auto/api.json b/tests/auto/api.json index 3785c079..3b5ffff8 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -949,6 +949,7 @@ "params": { "inputStateTypeId": "Uuid", "inputThingId": "Uuid", + "o:inverted": "Bool", "outputStateTypeId": "Uuid", "outputThingId": "Uuid" }, @@ -2456,6 +2457,7 @@ "r:id": "Uuid", "r:inputStateTypeId": "Uuid", "r:inputThingId": "Uuid", + "r:inverted": "Bool", "r:outputStateTypeId": "Uuid", "r:outputThingId": "Uuid" }, diff --git a/tests/auto/ioconnections/testioconnections.cpp b/tests/auto/ioconnections/testioconnections.cpp index 1bda1c73..13f1c5c7 100644 --- a/tests/auto/ioconnections/testioconnections.cpp +++ b/tests/auto/ioconnections/testioconnections.cpp @@ -60,8 +60,10 @@ private slots: void testConnectionCompatibility_data(); void testConnectionCompatibility(); + void testDigitalIO_data(); void testDigitalIO(); + void testAnalogIO_data(); void testAnalogIO(); }; @@ -71,6 +73,7 @@ void TestIOConnections::initTestCase() QLoggingCategory::setFilterRules("*.debug=false\n" "Tests.debug=true\n" "Mock.debug=true\n" + "ThingManager.debug=true\n" ); // Adding generic IO mock @@ -151,33 +154,47 @@ void TestIOConnections::testConnectionCompatibility() } +void TestIOConnections::testDigitalIO_data() +{ + QTest::addColumn("inverted"); + + QTest::newRow("normal") << false; + QTest::newRow("inverted") << true; +} + void TestIOConnections::testDigitalIO() { + QFETCH(bool, inverted); + QVariantMap params; params.insert("inputThingId", m_lightThingId); params.insert("inputStateTypeId", virtualIoLightMockPowerStateTypeId); params.insert("outputThingId", m_ioThingId); params.insert("outputStateTypeId", genericIoMockDigitalOutput1StateTypeId); + params.insert("inverted", inverted); QVariant response = injectAndWait("Integrations.ConnectIO", params); verifyThingError(response); IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid(); - // verify both, input and out are off + // verify input is off + bool expectedValue = false; params.clear(); params.insert("thingId", m_lightThingId); params.insert("stateTypeId", virtualIoLightMockPowerStateTypeId); response = injectAndWait("Integrations.GetStateValue", params); verifyThingError(response); - QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == false, "Light isn't turned off"); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == expectedValue, "Light isn't turned off"); + // verify output is off (or inverted) params.clear(); params.insert("thingId", m_ioThingId); params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId); response = injectAndWait("Integrations.GetStateValue", params); verifyThingError(response); - QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == false, "Digital output isn't turned off"); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output isn't turned off"); // Turn on light and verify digital output went on + expectedValue = true; params.clear(); params.insert("thingId", m_lightThingId); params.insert("actionTypeId", virtualIoLightMockPowerActionTypeId); @@ -193,7 +210,7 @@ void TestIOConnections::testDigitalIO() params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId); response = injectAndWait("Integrations.GetStateValue", params); verifyThingError(response); - QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == true, "Digital output isn't turned on"); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output isn't turned on"); // Disconnect IO again params.clear(); @@ -202,6 +219,7 @@ void TestIOConnections::testDigitalIO() verifyThingError(response); // Turn off the light and verify digital output is still on + expectedValue = true; params.clear(); params.insert("thingId", m_lightThingId); params.insert("actionTypeId", virtualIoLightMockPowerActionTypeId); @@ -217,41 +235,59 @@ void TestIOConnections::testDigitalIO() params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId); response = injectAndWait("Integrations.GetStateValue", params); verifyThingError(response); - QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == true, "Digital output turned off while it should not"); + QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output turned off while it should not"); +} + +void TestIOConnections::testAnalogIO_data() +{ + QTest::addColumn("inverted"); + + QTest::newRow("normal") << false; + QTest::newRow("inverted") << true; } void TestIOConnections::testAnalogIO() { + QFETCH(bool, inverted); + + // Set input to 0 QVariantMap params; + params.insert("thingId", m_ioThingId); + params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId); + QVariantMap actionParam; + actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId); + actionParam.insert("value", 0); // goes from 0 to 3.3 + params.insert("params", QVariantList() << actionParam); + QVariant response = injectAndWait("Integrations.ExecuteAction", params); + verifyThingError(response); + + // Connect IO to it + params.clear(); params.insert("inputThingId", m_ioThingId); params.insert("inputStateTypeId", genericIoMockAnalogInput1StateTypeId); params.insert("outputThingId", m_tempSensorThingId); params.insert("outputStateTypeId", virtualIoTemperatureSensorMockInputStateTypeId); - QVariant response = injectAndWait("Integrations.ConnectIO", params); + params.insert("inverted", inverted); + + response = injectAndWait("Integrations.ConnectIO", params); verifyThingError(response); IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid(); - // verify input is at 0, temp at 0 too - params.clear(); - params.insert("thingId", m_ioThingId); - params.insert("stateTypeId", genericIoMockAnalogInput1StateTypeId); - response = injectAndWait("Integrations.GetStateValue", params); - verifyThingError(response); - QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), 0), "Input isn't at 0"); - + // and check temp senser + double expectedTemp = inverted ? 50 : -20; params.clear(); params.insert("thingId", m_tempSensorThingId); params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId); response = injectAndWait("Integrations.GetStateValue", params); verifyThingError(response); - QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), 0), QString("Temp sensor is not at 0 but at %1").arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8()); + QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8()); // set analog input to 0.5 and verify temp aligned params.clear(); params.insert("thingId", m_ioThingId); params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId); - QVariantMap actionParam; + actionParam.clear(); actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId); actionParam.insert("value", 1.65); // goes from 0 to 3.3 params.insert("params", QVariantList() << actionParam); @@ -265,7 +301,7 @@ void TestIOConnections::testAnalogIO() verifyThingError(response); // generic IO output goes from 0 to 3.3. We're setting 1.65V which 50% // temp goes from -20 to 50. A input of 1.65 should output a temperature of 15°C - double expectedTemp = 70.0 / 2 - 20; + expectedTemp = 70.0 / 2 - 20; QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8()); // Disconnect IO again From 82703de061df7aeb401dfff5de69d97a5bec72ad Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 27 May 2020 17:16:50 +0200 Subject: [PATCH 5/5] Fixes after testing/review --- libnymea-core/debugserverhandler.cpp | 79 +++++++++++++++++++ .../thingmanagerimplementation.cpp | 6 ++ libnymea-core/jsonrpc/integrationshandler.cpp | 4 +- libnymea/integrations/ioconnection.cpp | 51 ++++++++++++ libnymea/integrations/ioconnection.h | 30 +++++++ libnymea/integrations/thingmanager.cpp | 12 +++ libnymea/nymeasettings.cpp | 4 + plugins/mock/integrationpluginmock.cpp | 5 +- tests/auto/api.json | 4 +- 9 files changed, 189 insertions(+), 6 deletions(-) diff --git a/libnymea-core/debugserverhandler.cpp b/libnymea-core/debugserverhandler.cpp index 48222228..a402ec2d 100644 --- a/libnymea-core/debugserverhandler.cpp +++ b/libnymea-core/debugserverhandler.cpp @@ -343,6 +343,35 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath, c return reply; } + if (requestPath.startsWith("/debug/settings/ioconnections")) { + QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleIOConnections).fileName(); + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; + QFile settingsFile(settingsFileName); + if (!settingsFile.exists()) { + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; + HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound); + reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); + reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); + return reply; + } + + if (!settingsFile.open(QFile::ReadOnly)) { + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; + HttpReply *reply = HttpReply::createErrorReply(HttpReply::Forbidden); + reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); + reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); + return reply; + } + + QByteArray settingsFileData = settingsFile.readAll(); + settingsFile.close(); + + HttpReply *reply = HttpReply::createSuccessReply(); + reply->setHeader(HttpReply::ContentTypeHeader, "text/plain"); + reply->setPayload(settingsFileData); + return reply; + } + if (requestPath.startsWith("/debug/ping")) { // Only one ping process should run if (m_pingProcess || m_pingReply) @@ -1608,6 +1637,56 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeEndElement(); // div download-row + + // Download row IO connections + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-row"); + + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-name-column"); + //: The MQTT policies download description of the debug interface + writer.writeTextElement("p", tr("IO Connections")); + writer.writeEndElement(); // div download-name-column + + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-path-column"); + writer.writeTextElement("p", NymeaSettings(NymeaSettings::SettingsRoleIOConnections).fileName()); + writer.writeEndElement(); // div download-path-column + + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-button-column"); + writer.writeStartElement("form"); + writer.writeAttribute("class", "download-button"); + writer.writeStartElement("button"); + writer.writeAttribute("class", "button"); + writer.writeAttribute("type", "button"); + if (!QFile::exists(NymeaSettings(NymeaSettings::SettingsRoleMqttPolicies).fileName())) { + writer.writeAttribute("disabled", "true"); + } + writer.writeAttribute("onClick", "downloadFile('/debug/settings/ioconnections', 'ioconnections.conf')"); + writer.writeCharacters(tr("Download")); + writer.writeEndElement(); // button + writer.writeEndElement(); // form + writer.writeEndElement(); // div download-button-column + + writer.writeStartElement("div"); + writer.writeAttribute("class", "show-button-column"); + writer.writeStartElement("form"); + writer.writeAttribute("class", "show-button"); + writer.writeStartElement("button"); + writer.writeAttribute("class", "button"); + writer.writeAttribute("type", "button"); + if (!QFile::exists(NymeaSettings(NymeaSettings::SettingsRoleMqttPolicies).fileName())) { + writer.writeAttribute("disabled", "true"); + } + writer.writeAttribute("onClick", "showFile('/debug/settings/ioconnections')"); + writer.writeCharacters(tr("Show")); + writer.writeEndElement(); // button + writer.writeEndElement(); // form + writer.writeEndElement(); // div show-button-column + + writer.writeEndElement(); // div download-row + writer.writeEndElement(); // downloads-section diff --git a/libnymea-core/integrations/thingmanagerimplementation.cpp b/libnymea-core/integrations/thingmanagerimplementation.cpp index 568c8cd9..7ec37118 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -2039,6 +2039,12 @@ void ThingManagerImplementation::loadIOConnections() IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted); m_ioConnections.insert(id, ioConnection); connectionSettings.endGroup(); + + Thing *inputThing = m_configuredThings.value(inputThingId); + if (!inputThing) { + continue; + } + syncIOConnection(inputThing, inputStateTypeId); } connectionSettings.endGroup(); } diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index 158c4f8c..51ac9b1f 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -420,7 +420,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa }); params.clear(); returns.clear(); - description = "Emitted whenever a IO connection is added."; + description = "Emitted whenever an IO connection has been added."; params.insert("ioConnection", objectRef()); registerNotification("IOConnectionAdded", description, params); connect(m_thingManager, &ThingManager::ioConnectionAdded, this, [this](const IOConnection &connection) { @@ -430,7 +430,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa }); params.clear(); returns.clear(); - description = "Emitted whenever a IO connection is removed."; + description = "Emitted whenever an IO connection has been removed."; params.insert("ioConnectionId", enumValueName(Uuid)); registerNotification("IOConnectionRemoved", description, params); connect(m_thingManager, &ThingManager::ioConnectionRemoved, this, [this](const IOConnectionId &ioConnectionId) { diff --git a/libnymea/integrations/ioconnection.cpp b/libnymea/integrations/ioconnection.cpp index b56aa53b..67219449 100644 --- a/libnymea/integrations/ioconnection.cpp +++ b/libnymea/integrations/ioconnection.cpp @@ -1,3 +1,47 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/*! + \class IOConnection + \brief The IOConnection class stores information about generic IO connections. + + \ingroup things + \inmodule libnymea + + Generic IO connections allow a user to connect states of two devices so they'll be synced whenever + those states change. + + \sa ThingManager::connectIO + +*/ + #include "ioconnection.h" IOConnection::IOConnection() @@ -5,6 +49,7 @@ IOConnection::IOConnection() } +/*! Constructs a new IOConnection object. */ IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted): m_id(id), m_inputThingId(inputThing), @@ -16,31 +61,37 @@ IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, } +/*! Returns the ID of this connection object. */ IOConnectionId IOConnection::id() const { return m_id; } +/*! Returns the ID of the input thing for this connection. */ ThingId IOConnection::inputThingId() const { return m_inputThingId; } +/*! Returns the input state type ID for this connection. */ StateTypeId IOConnection::inputStateTypeId() const { return m_inputStateTypeId; } +/*! Returns the ID of the output thing for this connection. */ ThingId IOConnection::outputThingId() const { return m_outputThingId; } +/*! Returns the output state type ID for this connection. */ StateTypeId IOConnection::outputStateTypeId() const { return m_outputStateTypeId; } +/*! Returns whether the connection is inverted or not. */ bool IOConnection::inverted() const { return m_inverted; diff --git a/libnymea/integrations/ioconnection.h b/libnymea/integrations/ioconnection.h index 7cb124de..92022a78 100644 --- a/libnymea/integrations/ioconnection.h +++ b/libnymea/integrations/ioconnection.h @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef IOCONNECTION_H #define IOCONNECTION_H diff --git a/libnymea/integrations/thingmanager.cpp b/libnymea/integrations/thingmanager.cpp index c427cda3..ae02ed1a 100644 --- a/libnymea/integrations/thingmanager.cpp +++ b/libnymea/integrations/thingmanager.cpp @@ -51,6 +51,18 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent) qRegisterMetaType(); } +/*! Connect two states. + When two states are connected, any state changes will be synced between those. A connection + is made from an \a inputThing and its \a inputState to an \a outputThing and its \a outputState. + Whenever the input state changes, the output state is set accordingly. If the input state is + writable, the connection will be bidirectional, that is, a change of the output state will also + reflect on the input state. + Connections can be logically inverted. + Connections need to be compatible. This means, only states which have a defined ioState of type "input" + can be connected to states which habe a defined ioState of type "output". Additionally, the digital/analog + type needs to match. In other words, states with ioType "digitalInput" can be connected to states with ioType + "digitaOutput" and states with ioType "analogInput" can be connected to states with ioType "analogOutput". + */ IOConnectionResult ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted) { IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState, inverted); diff --git a/libnymea/nymeasettings.cpp b/libnymea/nymeasettings.cpp index 5b404857..f7f2324e 100644 --- a/libnymea/nymeasettings.cpp +++ b/libnymea/nymeasettings.cpp @@ -60,6 +60,10 @@ This role will create the \b{device-states.conf} file and is used to store the configured \l{Device} \l{State}{States}. \value SettingsRoleTags This role will create the \b{tags.conf} file and is used to store the \l{Tag}{Tags}. + \value SettingsRoleMqttPolicies + This role will create the \b{mqttpolicies.conf} file and is used to store the \l{MqttPolicy}{MqttPolicies}. + \value SettingsRoleIOConnections + This role will create the \b{ioconnections.conf} file and is used to store the \l{IOConnection}{IOConnections}. */ diff --git a/plugins/mock/integrationpluginmock.cpp b/plugins/mock/integrationpluginmock.cpp index 42c5cc6b..731b4135 100644 --- a/plugins/mock/integrationpluginmock.cpp +++ b/plugins/mock/integrationpluginmock.cpp @@ -720,12 +720,13 @@ void IntegrationPluginMock::executeAction(ThingActionInfo *info) if (info->thing()->thingClassId() == genericIoMockThingClassId) { if (info->action().actionTypeId() == genericIoMockDigitalOutput1ActionTypeId) { - info->thing()->setStateValue(genericIoMockDigitalOutput1StateTypeId, info->action().param(genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId).value()); + qCDebug(dcMock()) << "Setting digital output 1 to" << info->action().param(genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId).value().toBool(); + info->thing()->setStateValue(genericIoMockDigitalOutput1StateTypeId, info->action().param(genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId).value().toBool()); info->finish(Thing::ThingErrorNoError); return; } if (info->action().actionTypeId() == genericIoMockDigitalOutput2ActionTypeId) { - info->thing()->setStateValue(genericIoMockDigitalOutput2StateTypeId, info->action().param(genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId).value()); + info->thing()->setStateValue(genericIoMockDigitalOutput2StateTypeId, info->action().param(genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId).value().toBool()); info->finish(Thing::ThingErrorNoError); return; } diff --git a/tests/auto/api.json b/tests/auto/api.json index 3b5ffff8..4645bbe9 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -2055,13 +2055,13 @@ } }, "Integrations.IOConnectionAdded": { - "description": "Emitted whenever a IO connection is added.", + "description": "Emitted whenever an IO connection has been added.", "params": { "ioConnection": "$ref:IOConnection" } }, "Integrations.IOConnectionRemoved": { - "description": "Emitted whenever a IO connection is removed.", + "description": "Emitted whenever an IO connection has been removed.", "params": { "ioConnectionId": "Uuid" }