diff --git a/debian/control b/debian/control index f57c6e7c..ce2d5528 100644 --- a/debian/control +++ b/debian/control @@ -857,6 +857,21 @@ Description: nymea.io plugin for simulated devices This package will install the nymea.io plugin for simulated devices +Package: nymea-plugin-somfytahoma +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for Somfy TaHoma + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for Somfy TaHoma devices + + Package: nymea-plugin-sonos Architecture: any Depends: ${shlibs:Depends}, @@ -1015,6 +1030,7 @@ Depends: nymea-plugin-anel, nymea-plugin-elgato, nymea-plugin-shelly, nymea-plugin-senic, + nymea-plugin-somfytahoma, nymea-plugin-sonos, nymea-plugin-solarlog, nymea-plugin-tado, diff --git a/debian/nymea-plugin-somfytahoma.install.in b/debian/nymea-plugin-somfytahoma.install.in new file mode 100644 index 00000000..f0e177e6 --- /dev/null +++ b/debian/nymea-plugin-somfytahoma.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsomfytahoma.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 0a670cd6..35224cb9 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -48,6 +48,7 @@ PLUGIN_DIRS = \ serialportcommander \ simulation \ snapd \ + somfytahoma \ sonos \ tado \ tasmota \ diff --git a/somfytahoma/README.md b/somfytahoma/README.md new file mode 100644 index 00000000..72627713 --- /dev/null +++ b/somfytahoma/README.md @@ -0,0 +1,22 @@ +# Somfy TaHoma + +This plugin adds support for Somfy smarthome devices through the Somfy TaHoma +API. + +## Prerequisites + +This plugin requires a Somfy TaHoma gateway to which your Somfy devices +are connected. The gateway needs to be registered to the Somfy API. +Follow the user guide of the gateway for detailed instructions. + +## Usage + +In order to interact with your Somfy devices, add your TaHoma gateway as new +'Thing' to nymea. All supported devices will show up automatically after +entering your personal username + password for the Somfy TaHoma API. + +## Supported devices + +Currently this plugin supports all roller shutters and blinds that are +connectable to the TaHoma gateway. These are Somfy iO devices as well as RTS +devices. diff --git a/somfytahoma/integrationpluginsomfytahoma.cpp b/somfytahoma/integrationpluginsomfytahoma.cpp new file mode 100644 index 00000000..ff1fd700 --- /dev/null +++ b/somfytahoma/integrationpluginsomfytahoma.cpp @@ -0,0 +1,458 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginsomfytahoma.h" + +#include +#include +#include + +#include "network/networkaccessmanager.h" + +#include "plugininfo.h" +#include "somfytahomarequests.h" + +void IntegrationPluginSomfyTahoma::startPairing(ThingPairingInfo *info) +{ + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter the login credentials for Somfy Tahoma.")); +} + +void IntegrationPluginSomfyTahoma::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password) +{ + SomfyTahomaLoginRequest *request = new SomfyTahomaLoginRequest(hardwareManager()->networkManager(), username, password, this); + connect(request, &SomfyTahomaLoginRequest::error, info, [info](){ + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to login to Somfy Tahoma.")); + }); + connect(request, &SomfyTahomaLoginRequest::finished, info, [this, info, username, password](const QVariant &/*result*/){ + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("username", username); + pluginStorage()->setValue("password", password); + pluginStorage()->endGroup(); + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginSomfyTahoma::setupThing(ThingSetupInfo *info) +{ + if (info->thing()->thingClassId() == tahomaThingClassId) { + SomfyTahomaLoginRequest *request = createLoginRequestWithStoredCredentials(info->thing()); + connect(request, &SomfyTahomaLoginRequest::error, info, [info](){ + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Failed to login to Somfy Tahoma.")); + }); + connect(request, &SomfyTahomaLoginRequest::finished, info, [this, info](const QVariant &/*result*/){ + QUuid accountId = info->thing()->id(); + SomfyTahomaGetRequest *request = new SomfyTahomaGetRequest(hardwareManager()->networkManager(), "/setup", this); + connect(request, &SomfyTahomaGetRequest::finished, this, [this, accountId](const QVariant &result){ + QList unknownDevices; + foreach (const QVariant &gatewayVariant, result.toMap()["gateways"].toList()) { + QVariantMap gatewayMap = gatewayVariant.toMap(); + QString gatewayId = gatewayMap.value("gatewayId").toString(); + Thing *thing = myThings().findByParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, gatewayId)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing gateway:" << gatewayId; + } else { + qCInfo(dcSomfyTahoma()) << "Found new gateway:" << gatewayId; + ThingDescriptor descriptor(gatewayThingClassId, "TaHoma Gateway", QString(), accountId); + descriptor.setParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, gatewayId)); + unknownDevices.append(descriptor); + } + } + foreach (const QVariant &deviceVariant, result.toMap()["devices"].toList()) { + QVariantMap deviceMap = deviceVariant.toMap(); + QString type = deviceMap.value("uiClass").toString(); + QString deviceUrl = deviceMap.value("deviceURL").toString(); + QString label = deviceMap.value("label").toString(); + if (type == QStringLiteral("RollerShutter")) { + Thing *thing = myThings().findByParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing roller shutter:" << label << deviceUrl; + } else { + qCInfo(dcSomfyTahoma()) << "Found new roller shutter:" << label << deviceUrl; + ThingDescriptor descriptor(rollershutterThingClassId, label, QString(), accountId); + descriptor.setParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); + } + } else if (type == QStringLiteral("ExteriorVenetianBlind")) { + Thing *thing = myThings().findByParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing venetian blind:" << label << deviceUrl; + } else { + qCInfo(dcSomfyTahoma()) << "Found new venetian blind:" << label << deviceUrl; + ThingDescriptor descriptor(venetianblindThingClassId, label, QString(), accountId); + descriptor.setParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); + } + } else { + qCInfo(dcSomfyTahoma()) << "Found unsupperted Somfy device:" << label << type << deviceUrl; + } + } + if (!unknownDevices.isEmpty()) { + emit autoThingsAppeared(unknownDevices); + } + }); + info->finish(Thing::ThingErrorNoError); + }); + } + + else if (info->thing()->thingClassId() == gatewayThingClassId || + info->thing()->thingClassId() == rollershutterThingClassId || + info->thing()->thingClassId() == venetianblindThingClassId) { + info->finish(Thing::ThingErrorNoError); + } +} + +void IntegrationPluginSomfyTahoma::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == tahomaThingClassId) { + pluginStorage()->beginGroup(thing->id().toString()); + thing->setStateValue(tahomaUserDisplayNameStateTypeId, pluginStorage()->value("username")); + pluginStorage()->endGroup(); + + refreshAccount(thing); + } + + // Set parent of all devices to the respective gateway. We create all devices in setup() of the account. + // But we don't have the ThingIds of the gateways, because they're created in setup() as well. + QUrl deviceUrl; + if (thing->thingClassId() == rollershutterThingClassId) { + deviceUrl = QUrl(thing->paramValue(rollershutterThingDeviceUrlParamTypeId).toString()); + } else if (thing->thingClassId() == venetianblindThingClassId) { + deviceUrl = QUrl(thing->paramValue(venetianblindThingDeviceUrlParamTypeId).toString()); + } + if (!deviceUrl.isEmpty()) { + Thing *gateway = myThings().findByParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, deviceUrl.host())); + if (gateway) { + thing->setParentId(gateway->parentId()); + } else { + qCWarning(dcSomfyTahoma()) << "Couldn't find gateway for thing" << thing; + } + } +} + +void IntegrationPluginSomfyTahoma::refreshAccount(Thing *thing) +{ + // Ensure that even't polling doesn't interfere the refreshing. + if (m_eventPollTimer.contains(thing)) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_eventPollTimer[thing]); + } + + SomfyTahomaGetRequest *setupRequest = new SomfyTahomaGetRequest(hardwareManager()->networkManager(), "/setup", this); + connect(setupRequest, &SomfyTahomaGetRequest::error, this, [this, thing](){ + markDisconnected(thing); + }); + connect(setupRequest, &SomfyTahomaGetRequest::finished, this, [this, thing](const QVariant &result){ + thing->setStateValue(tahomaLoggedInStateTypeId, true); + thing->setStateValue(tahomaConnectedStateTypeId, true); + foreach (const QVariant &gatewayVariant, result.toMap()["gateways"].toList()) { + QVariantMap gatewayMap = gatewayVariant.toMap(); + QString gatewayId = gatewayMap.value("gatewayId").toString(); + Thing *thing = myThings().findByParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, gatewayId)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Setting initial state for gateway:" << gatewayId; + thing->setStateValue(gatewayConnectedStateTypeId, gatewayMap["connectivity"].toMap()["status"] == "OK"); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("connected", gatewayMap["connectivity"].toMap()["status"] == "OK"); + pluginStorage()->endGroup(); + } + } + foreach (const QVariant &deviceVariant, result.toMap()["devices"].toList()) { + updateThingStates(deviceVariant.toMap()["deviceURL"].toString(), deviceVariant.toMap()["states"].toList()); + } + }); + + SomfyTahomaPostRequest *eventRegistrationRequest = new SomfyTahomaPostRequest(hardwareManager()->networkManager(), "/events/register", "application/json", QByteArray(), this); + connect(eventRegistrationRequest, &SomfyTahomaPostRequest::error, this, [this, thing](){ + qCWarning(dcSomfyTahoma()) << "Failed to register for events."; + markDisconnected(thing); + }); + connect(eventRegistrationRequest, &SomfyTahomaPostRequest::finished, this, [this, thing](const QVariant &result){ + thing->setStateValue(tahomaConnectedStateTypeId, true); + QString eventListenerId = result.toMap()["id"].toString(); + m_eventPollTimer[thing] = hardwareManager()->pluginTimerManager()->registerTimer(2 /*sec*/); + connect(m_eventPollTimer[thing], &PluginTimer::timeout, thing, [this, thing, eventListenerId](){ + SomfyTahomaEventFetchRequest *eventFetchRequest = new SomfyTahomaEventFetchRequest(hardwareManager()->networkManager(), eventListenerId, this); + connect(eventFetchRequest, &SomfyTahomaEventFetchRequest::error, thing, [this, thing](QNetworkReply::NetworkError error){ + markDisconnected(thing); + if (error == QNetworkReply::AuthenticationRequiredError) { + qCInfo(dcSomfyTahoma()) << "Failed to fetch events: Authentication expired, reauthenticating"; + SomfyTahomaLoginRequest *request = createLoginRequestWithStoredCredentials(thing); + connect(request, &SomfyTahomaLoginRequest::error, this, [this, thing](){ + // This is a fatal error. The user needs to reconfigure the account to provide new credentials. + qCWarning(dcSomfyTahoma()) << "Failed to reauthenticate"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_eventPollTimer[thing]); + m_eventPollTimer.remove(thing); + }); + connect(request, &SomfyTahomaLoginRequest::finished, this, [this, thing](const QVariant &/*result*/){ + qCInfo(dcSomfyTahoma()) << "Reauthentication successful"; + refreshAccount(thing); + }); + } else { + qCWarning(dcSomfyTahoma()) << "Failed to fetch events:" << error; + } + }); + connect(eventFetchRequest, &SomfyTahomaEventFetchRequest::finished, thing, [this, thing](const QVariant &result){ + thing->setStateValue(tahomaConnectedStateTypeId, true); + restoreChildConnectedState(thing); + if (!result.toList().empty()) { + qCDebug(dcSomfyTahoma()) << "Got events:" << qUtf8Printable(QJsonDocument::fromVariant(result).toJson()); + } + handleEvents(result.toList()); + }); + }); + }); +} + +void IntegrationPluginSomfyTahoma::thingRemoved(Thing *thing) +{ + m_eventPollTimer.remove(thing); +} + +void IntegrationPluginSomfyTahoma::handleEvents(const QVariantList &eventList) +{ + Thing *thing; + foreach (const QVariant &eventVariant, eventList) { + QVariantMap eventMap = eventVariant.toMap(); + if (eventMap["name"] == "DeviceStateChangedEvent") { + updateThingStates(eventMap["deviceURL"].toString(), eventMap["deviceStates"].toList()); + } else if (eventMap["name"] == "ExecutionRegisteredEvent") { + QList things; + foreach (const QVariant &action, eventMap["actions"].toList()) { + thing = myThings().findByParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, action.toMap()["deviceURL"])); + if (thing) { + thing->setStateValue(rollershutterMovingStateTypeId, true); + things.append(thing); + continue; + } + thing = myThings().findByParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, action.toMap()["deviceURL"])); + if (thing) { + thing->setStateValue(venetianblindMovingStateTypeId, true); + things.append(thing); + } + } + qCDebug(dcSomfyTahoma()) << "ExecutionRegisteredEvent" << eventMap["execId"]; + m_currentExecutions.insert(eventMap["execId"].toString(), things); + } else if (eventMap["name"] == "ExecutionStateChangedEvent" && + (eventMap["newState"] == "COMPLETED" || eventMap["newState"] == "FAILED")) { + QList things = m_currentExecutions.take(eventMap["execId"].toString()); + foreach (Thing *thing, things) { + if (thing->thingClassId() == rollershutterThingClassId) { + thing->setStateValue(rollershutterMovingStateTypeId, false); + } else if (thing->thingClassId() == venetianblindThingClassId) { + thing->setStateValue(venetianblindMovingStateTypeId, false); + } + } + + QPointer thingActionInfo = m_pendingActions.take(eventMap["execId"].toString()); + if (!thingActionInfo.isNull()) { + if (eventMap["newState"] == "COMPLETED") { + qCDebug(dcSomfyTahoma()) << "Action finished" << thingActionInfo->thing() << thingActionInfo->action().actionTypeId(); + thingActionInfo->finish(Thing::ThingErrorNoError); + } else if (eventMap["newState"] == "FAILED") { + qCWarning(dcSomfyTahoma()) << "Action failed" << thingActionInfo->thing() << thingActionInfo->action().actionTypeId(); + thingActionInfo->finish(Thing::ThingErrorHardwareFailure); + } else { + qCWarning(dcSomfyTahoma()) << "Action in unknown state" << thingActionInfo->thing() << thingActionInfo->action().actionTypeId() << eventMap["newState"].toString(); + thingActionInfo->finish(Thing::ThingErrorHardwareFailure); + } + } + } else if (eventMap["name"] == "GatewayAliveEvent") { + thing = myThings().findByParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, eventMap["gatewayId"])); + if (thing) { + qCInfo(dcSomfyTahoma()) << "Gateway connected event received:" << eventMap["gatewayId"]; + thing->setStateValue(gatewayConnectedStateTypeId, true); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("connected", true); + pluginStorage()->endGroup(); + restoreChildConnectedState(thing); + } else { + qCDebug(dcSomfyTahoma()) << "Ignoring gateway connected event for unknown gateway" << eventMap["gatewayId"]; + } + } else if (eventMap["name"] == "GatewayDownEvent") { + thing = myThings().findByParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, eventMap["gatewayId"])); + if (thing) { + qCInfo(dcSomfyTahoma()) << "Gateway disconnected event received:" << eventMap["gatewayId"]; + thing->setStateValue(gatewayConnectedStateTypeId, false); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("connected", false); + pluginStorage()->endGroup(); + markDisconnected(thing); + } else { + qCDebug(dcSomfyTahoma()) << "Ignoring gateway disconnected event for unknown gateway" << eventMap["gatewayId"]; + } + } + } +} + +void IntegrationPluginSomfyTahoma::updateThingStates(const QString &deviceUrl, const QVariantList &stateList) +{ + Thing *thing = myThings().findByParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + foreach (const QVariant &stateVariant, stateList) { + QVariantMap stateMap = stateVariant.toMap(); + if (stateMap["name"] == "core:ClosureState") { + thing->setStateValue(rollershutterPercentageStateTypeId, stateMap["value"]); + } else if (stateMap["name"] == "core:StatusState") { + thing->setStateValue(rollershutterConnectedStateTypeId, stateMap["value"] == "available"); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("connected", stateMap["value"] == "available"); + pluginStorage()->endGroup(); + } else if (stateMap["name"] == "core:RSSILevelState") { + thing->setStateValue(rollershutterSignalStrengthStateTypeId, stateMap["value"]); + } + } + return; + } + thing = myThings().findByParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + foreach (const QVariant &stateVariant, stateList) { + QVariantMap stateMap = stateVariant.toMap(); + if (stateMap["name"] == "core:ClosureState") { + thing->setStateValue(venetianblindPercentageStateTypeId, stateMap["value"]); + } else if (stateMap["name"] == "core:SlateOrientationState") { + // Convert percentage (0%/100%, 50%=open) into degree (-90/+90) + int degree = (stateMap["value"].toInt() * 1.8) - 90; + thing->setStateValue(venetianblindAngleStateTypeId, degree); + } else if (stateMap["name"] == "core:StatusState") { + thing->setStateValue(venetianblindConnectedStateTypeId, stateMap["value"] == "available"); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("connected", stateMap["value"] == "available"); + pluginStorage()->endGroup(); + } else if (stateMap["name"] == "core:RSSILevelState") { + thing->setStateValue(venetianblindSignalStrengthStateTypeId, stateMap["value"]); + } + } + return; + } +} + +void IntegrationPluginSomfyTahoma::executeAction(ThingActionInfo *info) +{ + qCInfo(dcSomfyTahoma()) << "Action request:" << info->thing() << info->action().actionTypeId() << info->action().params(); + + QString deviceUrl; + QString actionName; + QJsonArray actionParameters; + + if (info->thing()->thingClassId() == rollershutterThingClassId) { + deviceUrl = info->thing()->paramValue(rollershutterThingDeviceUrlParamTypeId).toString(); + if (info->action().actionTypeId() == rollershutterPercentageActionTypeId) { + actionName = "setClosureAndLinearSpeed"; + actionParameters = { info->action().param(rollershutterPercentageActionPercentageParamTypeId).value().toInt(), "lowspeed" }; + } else if (info->action().actionTypeId() == rollershutterOpenActionTypeId) { + actionName = "setClosureAndLinearSpeed"; + actionParameters = { 0, "lowspeed" }; + } else if (info->action().actionTypeId() == rollershutterCloseActionTypeId) { + actionName = "setClosureAndLinearSpeed"; + actionParameters = { 100, "lowspeed" }; + } else if (info->action().actionTypeId() == rollershutterStopActionTypeId) { + actionName = "stop"; + } + } else if (info->thing()->thingClassId() == venetianblindThingClassId) { + deviceUrl = info->thing()->paramValue(venetianblindThingDeviceUrlParamTypeId).toString(); + if (info->action().actionTypeId() == venetianblindPercentageActionTypeId) { + actionName = "setClosure"; + actionParameters = { info->action().param(venetianblindPercentageActionPercentageParamTypeId).value().toInt() }; + } else if (info->action().actionTypeId() == venetianblindAngleActionTypeId) { + actionName = "setOrientation"; + // Convert degree (-90/+90) into percentage (0%/100%, 50%=open) + int degree = (info->action().param(venetianblindAngleActionAngleParamTypeId).value().toInt() + 90) / 1.8; + actionParameters = { degree }; + } else if (info->action().actionTypeId() == venetianblindOpenActionTypeId) { + actionName = "open"; + } else if (info->action().actionTypeId() == venetianblindCloseActionTypeId) { + actionName = "close"; + } else if (info->action().actionTypeId() == venetianblindStopActionTypeId) { + actionName = "stop"; + } + } + + if (!actionName.isEmpty()) { + QJsonDocument jsonRequest{QJsonObject + { + {"label", info->thing()->name()}, + {"actions", QJsonArray{QJsonObject{{"deviceURL", deviceUrl}, + {"commands", QJsonArray{QJsonObject{{"name", actionName}, + {"parameters", actionParameters}}}}}}} + }}; + SomfyTahomaPostRequest *request = new SomfyTahomaPostRequest(hardwareManager()->networkManager(), "/exec/apply", "application/json", jsonRequest.toJson(QJsonDocument::Compact), this); + connect(request, &SomfyTahomaPostRequest::error, info, [info](){ + info->finish(Thing::ThingErrorHardwareFailure); + }); + connect(request, &SomfyTahomaPostRequest::finished, info, [this, info](const QVariant &result){ + qCInfo(dcSomfyTahoma()) << "Action started" << info->thing() << info->action().actionTypeId(); + m_pendingActions.insert(result.toMap()["execId"].toString(), info); + }); + } else { + info->finish(Thing::ThingErrorActionTypeNotFound); + } +} + +SomfyTahomaLoginRequest *IntegrationPluginSomfyTahoma::createLoginRequestWithStoredCredentials(Thing *thing) +{ + pluginStorage()->beginGroup(thing->id().toString()); + QString username = pluginStorage()->value("username").toString(); + QString password = pluginStorage()->value("password").toString(); + pluginStorage()->endGroup(); + return new SomfyTahomaLoginRequest(hardwareManager()->networkManager(), username, password, this); +} + +void IntegrationPluginSomfyTahoma::markDisconnected(Thing *thing) +{ + if (thing->thingClassId() == tahomaThingClassId) { + thing->setStateValue(tahomaConnectedStateTypeId, false); + } else if (thing->thingClassId() == gatewayThingClassId) { + thing->setStateValue(gatewayConnectedStateTypeId, false); + } else if (thing->thingClassId() == rollershutterThingClassId) { + thing->setStateValue(rollershutterConnectedStateTypeId, false); + } else if (thing->thingClassId() == venetianblindThingClassId) { + thing->setStateValue(venetianblindConnectedStateTypeId, false); + } + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + markDisconnected(child); + } +} + +void IntegrationPluginSomfyTahoma::restoreChildConnectedState(Thing *thing) +{ + pluginStorage()->beginGroup(thing->id().toString()); + if (pluginStorage()->contains("connected")) { + if (thing->thingClassId() == gatewayThingClassId) { + thing->setStateValue(gatewayConnectedStateTypeId, pluginStorage()->value("connected").toBool()); + } else if (thing->thingClassId() == rollershutterThingClassId) { + thing->setStateValue(rollershutterConnectedStateTypeId, pluginStorage()->value("connected").toBool()); + } else if (thing->thingClassId() == venetianblindThingClassId) { + thing->setStateValue(venetianblindConnectedStateTypeId, pluginStorage()->value("connected").toBool()); + } + } + pluginStorage()->endGroup(); + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + restoreChildConnectedState(child); + } +} diff --git a/somfytahoma/integrationpluginsomfytahoma.h b/somfytahoma/integrationpluginsomfytahoma.h new file mode 100644 index 00000000..9f46dc8a --- /dev/null +++ b/somfytahoma/integrationpluginsomfytahoma.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 INTEGRATIONPLUGINSOMFYTAHOMA_H +#define INTEGRATIONPLUGINSOMFYTAHOMA_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" + +class SomfyTahomaLoginRequest; + +class IntegrationPluginSomfyTahoma : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsomfytahoma.json") + Q_INTERFACES(IntegrationPlugin) + +public: + void startPairing(ThingPairingInfo *info) override; + void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password) override; + + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + + void executeAction(ThingActionInfo *info) override; + +private: + SomfyTahomaLoginRequest *createLoginRequestWithStoredCredentials(Thing *thing); + void refreshAccount(Thing *thing); + void handleEvents(const QVariantList &eventList); + void updateThingStates(const QString &deviceUrl, const QVariantList &stateList); + void markDisconnected(Thing *thing); + void restoreChildConnectedState(Thing *thing); + +private: + QMap m_eventPollTimer; + QMap> m_pendingActions; + QMap> m_currentExecutions; +}; + +#endif // INTEGRATIONPLUGINSOMFYTAHOMA_H diff --git a/somfytahoma/integrationpluginsomfytahoma.json b/somfytahoma/integrationpluginsomfytahoma.json new file mode 100644 index 00000000..c2bb55ba --- /dev/null +++ b/somfytahoma/integrationpluginsomfytahoma.json @@ -0,0 +1,232 @@ +{ + "name": "SomfyTahoma", + "displayName": "Somfy Tahoma", + "id": "4e8be1c1-daa8-4e21-9e85-b2372ab1a450", + "vendors": [ + { + "name": "Somfy", + "displayName": "Somfy", + "id": "4e42a22a-ccfb-4677-89e3-f7fa16bf6be0", + "thingClasses": [ + { + "id": "fedd72b8-547d-4e4f-b73e-71344a8ba0c1", + "name": "tahoma", + "displayName": "Tahoma Account", + "createMethods": ["user"], + "setupMethod": "userandpassword", + "interfaces": ["account"], + "stateTypes": [ + { + "id": "10ebf650-a93a-4ee3-945b-fba10d4e35a5", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "displayNameEvent": "Connetion state changed", + "defaultValue": false + }, + { + "id": "97fefa85-db79-4efd-8d83-4a15d72996e1", + "name": "loggedIn", + "displayName": "Logged in", + "type": "bool", + "displayNameEvent": "Login state changed", + "defaultValue": false + }, + { + "id": "75609987-be60-4932-94f6-ead791b5fa58", + "name": "userDisplayName", + "displayName": "User display name", + "type": "QString", + "displayNameEvent": "User display name changed", + "defaultValue": "" + } + ] + }, + { + "id": "6c09e0b9-f0cc-4dea-9994-9e039eff78f1", + "name": "gateway", + "displayName": "Tahoma Gateway", + "createMethods": ["auto"], + "interfaces": ["gateway"], + "paramTypes": [ + { + "id": "e321a7d6-6dcb-4a37-baf1-c7008f2d5bdb", + "displayName": "Gateway Id", + "name": "gatewayId", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "18ba7fb7-c9e8-4c61-86b3-a8d3b825ed00", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "displayNameEvent": "Connetion state changed", + "defaultValue": false + } + ] + }, + { + "id": "6b187fe0-a987-462d-90ac-c48efc0d0fc0", + "name": "rollershutter", + "displayName": "Roller Shutter", + "createMethods": ["auto"], + "interfaces": ["extendedshutter", "wirelessconnectable"], + "paramTypes": [ + { + "id": "b3d20d6a-f4e1-4959-ab06-3d271ba5c3dc", + "displayName": "Device URL", + "name": "deviceUrl", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "f954ffc7-a6aa-4d30-aee0-0484631c3344", + "name": "percentage", + "displayName": "Percentage", + "type": "int", + "unit": "Percentage", + "displayNameEvent": "Percentage changed", + "writable": true, + "displayNameAction": "Set percentage", + "defaultValue": 0 + }, + { + "id": "fa9446ba-da30-4d49-8fb6-f410ecc7dba0", + "name": "moving", + "type": "bool", + "defaultValue": false, + "displayName": "Moving", + "displayNameEvent": "Moving changed" + }, + { + "id": "67594d96-47a2-4360-a1b8-79e4f22f9ed0", + "name": "signalStrength", + "displayName": "Signal strength", + "type": "uint", + "unit": "Percentage", + "displayNameEvent": "Signal strength changed", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "7a49865d-5ea5-43be-b61f-4e454c48e87e", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "displayNameEvent": "Connetion state changed", + "defaultValue": false + } + ], + "actionTypes": [ + { + "id": "a0460180-e799-4bc6-83ba-11731ef124a3", + "name": "open", + "displayName": "Open" + }, + { + "id": "cbccf714-1188-4ac9-9c91-17fe2c99acb3", + "name": "stop", + "displayName": "Stop" + }, + { + "id": "baf377c6-9fba-44cf-9f14-af0101f874b5", + "name": "close", + "displayName": "Close" + } + ] + }, + { + "id": "c7160205-d864-4194-b418-060fff60f0cb", + "name": "venetianblind", + "displayName": "Venetian Blind", + "createMethods": ["auto"], + "interfaces": ["venetianblind", "wirelessconnectable"], + "paramTypes": [ + { + "id": "e2541b7b-fbfa-4659-87b1-35d8993714c9", + "displayName": "Device URL", + "name": "deviceUrl", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "77ca50db-42a7-4434-83e2-8b5fc4438924", + "name": "percentage", + "displayName": "Percentage", + "type": "int", + "unit": "Percentage", + "displayNameEvent": "Percentage changed", + "writable": true, + "displayNameAction": "Set percentage", + "defaultValue": 0 + }, + { + "id": "079c7a80-8a1c-4fd7-b40c-6800120c70fb", + "name": "angle", + "displayName": "Angle", + "type": "int", + "unit": "Degree", + "displayNameEvent": "Angle changed", + "writable": true, + "displayNameAction": "Set angle", + "defaultValue": 0, + "minValue": -90, + "maxValue": 90 + }, + { + "id": "48d5de0a-11ab-4801-94e4-a1dd458c341d", + "name": "moving", + "type": "bool", + "defaultValue": false, + "displayName": "Moving", + "displayNameEvent": "Moving changed" + }, + { + "id": "aee4f4e3-3445-441d-bdbb-631b0c5db942", + "name": "signalStrength", + "displayName": "Signal strength", + "type": "uint", + "unit": "Percentage", + "displayNameEvent": "Signal strength changed", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "57361115-edbe-49fb-9847-408b571d3108", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "displayNameEvent": "Connetion state changed", + "defaultValue": false + } + ], + "actionTypes": [ + { + "id": "004e7294-59e6-498b-a0aa-e58eaeefdf2b", + "name": "open", + "displayName": "Open" + }, + { + "id": "31b07407-65ef-4fd1-880b-b5d9f69a9d07", + "name": "stop", + "displayName": "Stop" + }, + { + "id": "1a9707e7-9d64-4237-b150-234edcfed12a", + "name": "close", + "displayName": "Close" + } + ] + } + + + ] + } + ] +} diff --git a/somfytahoma/meta.json b/somfytahoma/meta.json new file mode 100644 index 00000000..a965b2ff --- /dev/null +++ b/somfytahoma/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Somfy Tahoma", + "tagline": "Control Somfy smart home devices through the Somfy Tahoma box.", + "icon": "somfy.svg", + "stability": "community", + "offline": false, + "technologies": [ + "network" + ], + "categories": [ + ] +} diff --git a/somfytahoma/somfy.svg b/somfytahoma/somfy.svg new file mode 100644 index 00000000..8c43f91c --- /dev/null +++ b/somfytahoma/somfy.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/somfytahoma/somfytahoma.pro b/somfytahoma/somfytahoma.pro new file mode 100644 index 00000000..43fcfcb0 --- /dev/null +++ b/somfytahoma/somfytahoma.pro @@ -0,0 +1,11 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + integrationpluginsomfytahoma.cpp \ + somfytahomarequests.cpp + +HEADERS += \ + integrationpluginsomfytahoma.h \ + somfytahomarequests.h diff --git a/somfytahoma/somfytahomarequests.cpp b/somfytahoma/somfytahomarequests.cpp new file mode 100644 index 00000000..d82afa20 --- /dev/null +++ b/somfytahoma/somfytahomarequests.cpp @@ -0,0 +1,105 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "somfytahomarequests.h" + +#include + +#include "network/networkaccessmanager.h" + +#include "extern-plugininfo.h" + +SomfyTahomaPostRequest::SomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent): + QObject(parent) +{ + QUrl url("https://tahomalink.com/enduser-mobile-web/enduserAPI" + path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType); + QNetworkReply *reply = networkManager->post(request, body); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply, path] { + deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcSomfyTahoma()) << "Request for" << path << "failed:" << reply->errorString(); + emit error(reply->error()); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qCWarning(dcSomfyTahoma()) << "Json parse error in reply for" << path << ":" << parseError.errorString(); + emit error(QNetworkReply::UnknownContentError); + return; + } + + emit finished(jsonDoc.toVariant()); + }); +} + +SomfyTahomaGetRequest::SomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent): + QObject(parent) +{ + QUrl url("https://tahomalink.com/enduser-mobile-web/enduserAPI" + path); + QNetworkRequest request(url); + QNetworkReply *reply = networkManager->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply, path] { + deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcSomfyTahoma()) << "Request for" << path << "failed:" << reply->errorString(); + emit error(reply->error()); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qCWarning(dcSomfyTahoma()) << "Json parse error in reply for" << path << ":" << parseError.errorString(); + emit error(QNetworkReply::UnknownContentError); + return; + } + + emit finished(jsonDoc.toVariant()); + }); +} + +SomfyTahomaLoginRequest::SomfyTahomaLoginRequest(NetworkAccessManager *networkManager, const QString &username, const QString &password, QObject *parent): + SomfyTahomaPostRequest(networkManager, "/login", "application/x-www-form-urlencoded", QString("userId=" + username + "&userPassword=" + password).toUtf8(), parent) +{ +} + + +SomfyTahomaEventFetchRequest::SomfyTahomaEventFetchRequest(NetworkAccessManager *networkManager, const QString &eventListenerId, QObject *parent): + SomfyTahomaPostRequest(networkManager, "/events/" + eventListenerId + "/fetch", "application/json", QByteArray(), parent) +{ +} diff --git a/somfytahoma/somfytahomarequests.h b/somfytahoma/somfytahomarequests.h new file mode 100644 index 00000000..60a90627 --- /dev/null +++ b/somfytahoma/somfytahomarequests.h @@ -0,0 +1,78 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 SOMFYTAHOMAREQUESTS_H +#define SOMFYTAHOMAREQUESTS_H + +#include + +class NetworkAccessManager; + +class SomfyTahomaPostRequest : public QObject +{ + Q_OBJECT + +public: + SomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent); + +signals: + void error(QNetworkReply::NetworkError error); + void finished(const QVariant &results); +}; + +class SomfyTahomaGetRequest : public QObject +{ + Q_OBJECT + +public: + SomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent); + +signals: + void error(QNetworkReply::NetworkError error); + void finished(const QVariant &results); +}; + +class SomfyTahomaLoginRequest : public SomfyTahomaPostRequest +{ + Q_OBJECT + +public: + SomfyTahomaLoginRequest(NetworkAccessManager *networkManager, const QString &username, const QString &password, QObject *parent); +}; + +class SomfyTahomaEventFetchRequest : public SomfyTahomaPostRequest +{ + Q_OBJECT + +public: + SomfyTahomaEventFetchRequest(NetworkAccessManager *networkManager, const QString &eventListenerId, QObject *parent); +}; + +#endif // SOMFYTAHOMAREQUESTS_H diff --git a/somfytahoma/translations/4e8be1c1-daa8-4e21-9e85-b2372ab1a450-en_US.ts b/somfytahoma/translations/4e8be1c1-daa8-4e21-9e85-b2372ab1a450-en_US.ts new file mode 100644 index 00000000..a4e4a46b --- /dev/null +++ b/somfytahoma/translations/4e8be1c1-daa8-4e21-9e85-b2372ab1a450-en_US.ts @@ -0,0 +1,282 @@ + + + + + IntegrationPluginSomfyTahoma + + + Please enter the login credentials for Somfy Tahoma. + + + + + + Failed to login to Somfy Tahoma. + + + + + SomfyTahoma + + + + + Angle + The name of the ParamType (ThingClass: venetianblind, ActionType: angle, ID: {079c7a80-8a1c-4fd7-b40c-6800120c70fb}) +---------- +The name of the ParamType (ThingClass: venetianblind, EventType: angle, ID: {079c7a80-8a1c-4fd7-b40c-6800120c70fb}) +---------- +The name of the StateType ({079c7a80-8a1c-4fd7-b40c-6800120c70fb}) of ThingClass venetianblind + + + + + Angle changed + The name of the EventType ({079c7a80-8a1c-4fd7-b40c-6800120c70fb}) of ThingClass venetianblind + + + + + + Close + The name of the ActionType ({1a9707e7-9d64-4237-b150-234edcfed12a}) of ThingClass venetianblind +---------- +The name of the ActionType ({baf377c6-9fba-44cf-9f14-af0101f874b5}) of ThingClass rollershutter + + + + + + + + + + + + Connected + The name of the ParamType (ThingClass: venetianblind, EventType: connected, ID: {57361115-edbe-49fb-9847-408b571d3108}) +---------- +The name of the StateType ({57361115-edbe-49fb-9847-408b571d3108}) of ThingClass venetianblind +---------- +The name of the ParamType (ThingClass: rollershutter, EventType: connected, ID: {7a49865d-5ea5-43be-b61f-4e454c48e87e}) +---------- +The name of the StateType ({7a49865d-5ea5-43be-b61f-4e454c48e87e}) of ThingClass rollershutter +---------- +The name of the ParamType (ThingClass: gateway, EventType: connected, ID: {18ba7fb7-c9e8-4c61-86b3-a8d3b825ed00}) +---------- +The name of the StateType ({18ba7fb7-c9e8-4c61-86b3-a8d3b825ed00}) of ThingClass gateway +---------- +The name of the ParamType (ThingClass: tahoma, EventType: connected, ID: {10ebf650-a93a-4ee3-945b-fba10d4e35a5}) +---------- +The name of the StateType ({10ebf650-a93a-4ee3-945b-fba10d4e35a5}) of ThingClass tahoma + + + + + + + + Connetion state changed + The name of the EventType ({57361115-edbe-49fb-9847-408b571d3108}) of ThingClass venetianblind +---------- +The name of the EventType ({7a49865d-5ea5-43be-b61f-4e454c48e87e}) of ThingClass rollershutter +---------- +The name of the EventType ({18ba7fb7-c9e8-4c61-86b3-a8d3b825ed00}) of ThingClass gateway +---------- +The name of the EventType ({10ebf650-a93a-4ee3-945b-fba10d4e35a5}) of ThingClass tahoma + + + + + + Device URL + The name of the ParamType (ThingClass: venetianblind, Type: thing, ID: {e2541b7b-fbfa-4659-87b1-35d8993714c9}) +---------- +The name of the ParamType (ThingClass: rollershutter, Type: thing, ID: {b3d20d6a-f4e1-4959-ab06-3d271ba5c3dc}) + + + + + Gateway Id + The name of the ParamType (ThingClass: gateway, Type: thing, ID: {e321a7d6-6dcb-4a37-baf1-c7008f2d5bdb}) + + + + + + Logged in + The name of the ParamType (ThingClass: tahoma, EventType: loggedIn, ID: {97fefa85-db79-4efd-8d83-4a15d72996e1}) +---------- +The name of the StateType ({97fefa85-db79-4efd-8d83-4a15d72996e1}) of ThingClass tahoma + + + + + Login state changed + The name of the EventType ({97fefa85-db79-4efd-8d83-4a15d72996e1}) of ThingClass tahoma + + + + + + + + Moving + The name of the ParamType (ThingClass: venetianblind, EventType: moving, ID: {48d5de0a-11ab-4801-94e4-a1dd458c341d}) +---------- +The name of the StateType ({48d5de0a-11ab-4801-94e4-a1dd458c341d}) of ThingClass venetianblind +---------- +The name of the ParamType (ThingClass: rollershutter, EventType: moving, ID: {fa9446ba-da30-4d49-8fb6-f410ecc7dba0}) +---------- +The name of the StateType ({fa9446ba-da30-4d49-8fb6-f410ecc7dba0}) of ThingClass rollershutter + + + + + + Moving changed + The name of the EventType ({48d5de0a-11ab-4801-94e4-a1dd458c341d}) of ThingClass venetianblind +---------- +The name of the EventType ({fa9446ba-da30-4d49-8fb6-f410ecc7dba0}) of ThingClass rollershutter + + + + + + Open + The name of the ActionType ({004e7294-59e6-498b-a0aa-e58eaeefdf2b}) of ThingClass venetianblind +---------- +The name of the ActionType ({a0460180-e799-4bc6-83ba-11731ef124a3}) of ThingClass rollershutter + + + + + + + + + + Percentage + The name of the ParamType (ThingClass: venetianblind, ActionType: percentage, ID: {77ca50db-42a7-4434-83e2-8b5fc4438924}) +---------- +The name of the ParamType (ThingClass: venetianblind, EventType: percentage, ID: {77ca50db-42a7-4434-83e2-8b5fc4438924}) +---------- +The name of the StateType ({77ca50db-42a7-4434-83e2-8b5fc4438924}) of ThingClass venetianblind +---------- +The name of the ParamType (ThingClass: rollershutter, ActionType: percentage, ID: {f954ffc7-a6aa-4d30-aee0-0484631c3344}) +---------- +The name of the ParamType (ThingClass: rollershutter, EventType: percentage, ID: {f954ffc7-a6aa-4d30-aee0-0484631c3344}) +---------- +The name of the StateType ({f954ffc7-a6aa-4d30-aee0-0484631c3344}) of ThingClass rollershutter + + + + + + Percentage changed + The name of the EventType ({77ca50db-42a7-4434-83e2-8b5fc4438924}) of ThingClass venetianblind +---------- +The name of the EventType ({f954ffc7-a6aa-4d30-aee0-0484631c3344}) of ThingClass rollershutter + + + + + Roller Shutter + The name of the ThingClass ({6b187fe0-a987-462d-90ac-c48efc0d0fc0}) + + + + + Set angle + The name of the ActionType ({079c7a80-8a1c-4fd7-b40c-6800120c70fb}) of ThingClass venetianblind + + + + + + Set percentage + The name of the ActionType ({77ca50db-42a7-4434-83e2-8b5fc4438924}) of ThingClass venetianblind +---------- +The name of the ActionType ({f954ffc7-a6aa-4d30-aee0-0484631c3344}) of ThingClass rollershutter + + + + + + + + Signal strength + The name of the ParamType (ThingClass: venetianblind, EventType: signalStrength, ID: {aee4f4e3-3445-441d-bdbb-631b0c5db942}) +---------- +The name of the StateType ({aee4f4e3-3445-441d-bdbb-631b0c5db942}) of ThingClass venetianblind +---------- +The name of the ParamType (ThingClass: rollershutter, EventType: signalStrength, ID: {67594d96-47a2-4360-a1b8-79e4f22f9ed0}) +---------- +The name of the StateType ({67594d96-47a2-4360-a1b8-79e4f22f9ed0}) of ThingClass rollershutter + + + + + + Signal strength changed + The name of the EventType ({aee4f4e3-3445-441d-bdbb-631b0c5db942}) of ThingClass venetianblind +---------- +The name of the EventType ({67594d96-47a2-4360-a1b8-79e4f22f9ed0}) of ThingClass rollershutter + + + + + Somfy + The name of the vendor ({4e42a22a-ccfb-4677-89e3-f7fa16bf6be0}) + + + + + Somfy Tahoma + The name of the plugin SomfyTahoma ({4e8be1c1-daa8-4e21-9e85-b2372ab1a450}) + + + + + + Stop + The name of the ActionType ({31b07407-65ef-4fd1-880b-b5d9f69a9d07}) of ThingClass venetianblind +---------- +The name of the ActionType ({cbccf714-1188-4ac9-9c91-17fe2c99acb3}) of ThingClass rollershutter + + + + + Tahoma Account + The name of the ThingClass ({fedd72b8-547d-4e4f-b73e-71344a8ba0c1}) + + + + + Tahoma Gateway + The name of the ThingClass ({6c09e0b9-f0cc-4dea-9994-9e039eff78f1}) + + + + + + User display name + The name of the ParamType (ThingClass: tahoma, EventType: userDisplayName, ID: {75609987-be60-4932-94f6-ead791b5fa58}) +---------- +The name of the StateType ({75609987-be60-4932-94f6-ead791b5fa58}) of ThingClass tahoma + + + + + User display name changed + The name of the EventType ({75609987-be60-4932-94f6-ead791b5fa58}) of ThingClass tahoma + + + + + Venetian Blind + The name of the ThingClass ({c7160205-d864-4194-b418-060fff60f0cb}) + + + +