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/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 eabf3196..ded23dbc 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,126 @@ 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; +} + +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() << "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(); + 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."; + result.error = Thing::ThingErrorInvalidParameter; + return result; + } + + Thing *outputThing = m_configuredThings.value(connection.outputThingId()); + if (!outputThing) { + 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(); + 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."; + 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(); + 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(); + result.error = Thing::ThingErrorInvalidParameter; + return result; + } + + // 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); + + qCDebug(dcThingManager()) << "IO connected added:" << inputThing << "->" << outputThing; + + // Sync initial state + syncIOConnection(inputThing, connection.inputStateTypeId()); + + result.error = Thing::ThingErrorNoError; + result.ioConnectionId = connection.id(); + return result; +} + +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); @@ -1428,6 +1555,8 @@ void ThingManagerImplementation::loadConfiguredThings() postSetupThing(info->thing()); }); } + + loadIOConnections(); } void ThingManagerImplementation::storeConfiguredThings() @@ -1616,6 +1745,127 @@ 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); + + 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!"; + continue; + } + IntegrationPlugin *plugin = m_integrationPlugins.value(outputThing->pluginId()); + if (!plugin) { + qCWarning(dcThingManager()) << "Plugin not found for IO connection's output action."; + continue; + } + StateType inputStateType = inputThing->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 = ioConnection.inverted() xor inputValue.toBool(); + + // 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(inputValue, inputStateType, outputStateType, ioConnection.inverted()); + + // We're already in sync (fuzzy, good enough)! Skipping action. + if (qFuzzyCompare(1.0 + outputThing->stateValue(outputStateType.id()).toDouble(), 1.0 + 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) { + inputThing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id())); + } else { + inputThing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType, ioConnection.inverted())); + } + } + }); + } + + // 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!"; + continue; + } + IntegrationPlugin *plugin = m_integrationPlugins.value(inputThing->pluginId()); + if (!plugin) { + qCWarning(dcThingManager()) << "Plugin not found for IO connection's input action."; + continue; + } + StateType outputStateType = outputThing->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 = ioConnection.inverted() xor outputValue.toBool(); + + // Prevent looping + if (inputThing->stateValue(inputStateType.id()) == inputValue) { + continue; + } + } else { + // Analog IOs are mapped within the according min/max ranges + 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(1.0 + inputThing->stateValue(inputStateType.id()).toDouble(), 1.0 + 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) @@ -1761,6 +2011,62 @@ 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.setValue("inverted", ioConnection.inverted()); + + 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(); + bool inverted = connectionSettings.value("inverted").toBool(); + 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(); +} + +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(); + double toMin = toStateType.minValue().toDouble(); + 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; +} + 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..51f51e4d 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; + 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; ParamType translateParamType(const PluginId &pluginId, const ParamType ¶mType, const QLocale &locale) override; ThingClass translateThingClass(const ThingClass &thingClass, const QLocale &locale) override; @@ -149,6 +154,11 @@ private: void postSetupThing(Thing *thing); void storeThingStates(Thing *thing); void loadThingStates(Thing *thing); + void storeIOConnections(); + void loadIOConnections(); + + 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; @@ -173,6 +183,8 @@ private: QString thingName; }; QHash m_pendingPairings; + + QHash m_ioConnections; }; #endif // THINGMANAGERIMPLEMENTATION_H 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/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..51ac9b1f 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,31 @@ 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. 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)); + params.insert("o:inverted", enumValueName(Bool)); + returns.insert("thingError", enumRef()); + returns.insert("o:ioConnectionId", enumValueName(Uuid)); + 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 +419,26 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa emit EventTriggered(params); }); + params.clear(); returns.clear(); + 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) { + QVariantMap params; + params.insert("ioConnection", pack(connection)); + emit IOConnectionAdded(params); + }); + + params.clear(); returns.clear(); + 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) { + 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 +977,38 @@ 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(); + 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); + } + return createReply(reply); +} + +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..67219449 --- /dev/null +++ b/libnymea/integrations/ioconnection.cpp @@ -0,0 +1,108 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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() +{ + +} + +/*! 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), + m_inputStateTypeId(inputState), + m_outputThingId(outputThing), + m_outputStateTypeId(outputState), + m_inverted(inverted) +{ + +} + +/*! 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; +} + +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..92022a78 --- /dev/null +++ b/libnymea/integrations/ioconnection.h @@ -0,0 +1,95 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 + +#include +#include +#include + +#include "typeutils.h" +#include "thing.h" + +struct IOConnectionResult { + Thing::ThingError error = Thing::ThingErrorNoError; + IOConnectionId ioConnectionId; +}; + +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) + Q_PROPERTY(bool inverted READ inverted) + +public: + IOConnection(); + IOConnection(const IOConnectionId &id, const ThingId &inputThingId, const StateTypeId &inputStateTypeId, const ThingId &outputThingId, const StateTypeId &outputStateTypeId, bool inverted = false); + + IOConnectionId id() const; + + ThingId inputThingId() const; + StateTypeId inputStateTypeId() const; + + 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 +{ + 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..ae02ed1a 100644 --- a/libnymea/integrations/thingmanager.cpp +++ b/libnymea/integrations/thingmanager.cpp @@ -50,3 +50,21 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent) qRegisterMetaType(); 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); + return connectIO(connection); +} diff --git a/libnymea/integrations/thingmanager.h b/libnymea/integrations/thingmanager.h index ed67a5ef..c846845e 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; + 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; 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 IOConnectionResult 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..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}. */ @@ -115,6 +119,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..82cacc52 100644 --- a/nymea.pro +++ b/nymea.pro @@ -5,9 +5,9 @@ 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_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/extern-plugininfo.h b/plugins/mock/extern-plugininfo.h index 79b79c29..3b92d668 100644 --- a/plugins/mock/extern-plugininfo.h +++ b/plugins/mock/extern-plugininfo.h @@ -246,5 +246,57 @@ 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 virtualIoTemperatureSensorMockInputStateTypeId; +extern StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId; +extern EventTypeId virtualIoTemperatureSensorMockInputEventTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockInputEventInputParamTypeId; +extern EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId; +extern ActionTypeId virtualIoTemperatureSensorMockInputActionTypeId; +extern ParamTypeId virtualIoTemperatureSensorMockInputActionInputParamTypeId; #endif // EXTERNPLUGININFO_H diff --git a/plugins/mock/integrationpluginmock.cpp b/plugins/mock/integrationpluginmock.cpp index a85d8c57..731b4135 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,61 @@ 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) { + 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().toBool()); + 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() == 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 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 +798,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..eb4713d3 100644 --- a/plugins/mock/integrationpluginmock.json +++ b/plugins/mock/integrationpluginmock.json @@ -868,6 +868,176 @@ "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": "fd341f72-6d9a-4812-9f66-47197c48a935", + "name": "input", + "displayName": "Input", + "displayNameEvent": "Input changed", + "displayNameAction": "Set input", + "type": "double", + "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": -20 + } + ] } ] } diff --git a/plugins/mock/plugininfo.h b/plugins/mock/plugininfo.h index 5bf42599..ef097a61 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") @@ -250,11 +250,108 @@ 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 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 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 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 +370,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,12 +466,33 @@ 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"), //: 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"), @@ -348,6 +508,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 +592,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 +667,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"), @@ -501,12 +694,18 @@ 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"), //: 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 ParamType (ThingClass: mock, Type: settings, ID: {367f7ba4-5039-47be-abd8-59cc8eaf4b9a}) QT_TRANSLATE_NOOP("mock", "Setting 1"), @@ -519,6 +718,15 @@ 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, 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..4645bbe9 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,29 @@ "thingError": "$ref:ThingError" } }, + "Integrations.ConnectIO": { + "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", + "o:inverted": "Bool", + "outputStateTypeId": "Uuid", + "outputThingId": "Uuid" + }, + "returns": { + "o:ioConnectionId": "Uuid", + "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 +1055,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 +2054,18 @@ "event": "$ref:Event" } }, + "Integrations.IOConnectionAdded": { + "description": "Emitted whenever an IO connection has been added.", + "params": { + "ioConnection": "$ref:IOConnection" + } + }, + "Integrations.IOConnectionRemoved": { + "description": "Emitted whenever an IO connection has been removed.", + "params": { + "ioConnectionId": "Uuid" + } + }, "Integrations.PluginConfigurationChanged": { "description": "Emitted whenever a plugin's configuration is changed.", "params": { @@ -2402,6 +2453,17 @@ "name": "String", "version": "String" }, + "IOConnection": { + "r:id": "Uuid", + "r:inputStateTypeId": "Uuid", + "r:inputThingId": "Uuid", + "r:inverted": "Bool", + "r:outputStateTypeId": "Uuid", + "r:outputThingId": "Uuid" + }, + "IOConnections": [ + "$ref:IOConnection" + ], "IntegrationPlugin": { "r:displayName": "String", "r:id": "Uuid", @@ -2586,6 +2648,7 @@ "displayName": "String", "index": "Int", "name": "String", + "o:ioType": "$ref:IOType", "o:maxValue": "Variant", "o:minValue": "Variant", "o:possibleValues": [ 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/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; } 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..13f1c5c7 --- /dev/null +++ b/tests/auto/ioconnections/testioconnections.cpp @@ -0,0 +1,335 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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_data(); + void testDigitalIO(); + + void testAnalogIO_data(); + void testAnalogIO(); +}; + +void TestIOConnections::initTestCase() +{ + NymeaTestBase::initTestCase(); + QLoggingCategory::setFilterRules("*.debug=false\n" + "Tests.debug=true\n" + "Mock.debug=true\n" + "ThingManager.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_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 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() == 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() == (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); + 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() == (expectedValue xor inverted), "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 + expectedValue = true; + 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() == (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); + params.insert("inverted", inverted); + + response = injectAndWait("Integrations.ConnectIO", params); + verifyThingError(response); + IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid(); + + // 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(), 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); + actionParam.clear(); + 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 + 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) +