diff --git a/somfytahoma/README.md b/somfytahoma/README.md index 08542194..3ad0d32c 100644 --- a/somfytahoma/README.md +++ b/somfytahoma/README.md @@ -1,7 +1,11 @@ # Somfy TaHoma -This plugin adds support for Somfy smarthome devices through the Somfy TaHoma -API. +This plugin adds support for Somfy smarthome devices through the local +Somfy TaHoma API offered by Somfy Gateways with 'Developer Mode' +enabled. + +See and +for more information. ## Prerequisites @@ -18,5 +22,5 @@ entering your personal username + password for the Somfy TaHoma API. ## Supported devices Currently this plugin supports all roller shutters, blinds, garage -door, awning drives and lights that are connectable to the TaHoma gateway. -These are Somfy iO devices as well as RTS devices. +door, awning drives, lights and smoke detectors 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 index cfb6296a..cb2bc8b3 100644 --- a/somfytahoma/integrationpluginsomfytahoma.cpp +++ b/somfytahoma/integrationpluginsomfytahoma.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -35,237 +35,223 @@ #include #include "network/networkaccessmanager.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "platform/platformzeroconfcontroller.h" #include "plugininfo.h" #include "somfytahomarequests.h" +void IntegrationPluginSomfyTahoma::init() +{ + m_zeroConfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_kizboxdev._tcp"); +} + +void IntegrationPluginSomfyTahoma::discoverThings(ThingDiscoveryInfo *info) +{ + foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) { + qCDebug(dcSomfyTahoma()) << "Found local gateway:" << entry; + + ThingDescriptor descriptor(info->thingClassId(), "Somfy TaHoma Gateway", entry.hostAddress().toString()); + ParamList params; + params << Param(gatewayThingGatewayPinParamTypeId, entry.txt("gateway_pin")); + descriptor.setParams(params); + + Things existingThings = myThings().filterByParam(gatewayThingGatewayPinParamTypeId, entry.txt("gateway_pin")); + if (existingThings.count() == 1) { + qCDebug(dcSomfyTahoma()) << "This gateway already exists in the system!"; + descriptor.setThingId(existingThings.first()->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); +} + void IntegrationPluginSomfyTahoma::startPairing(ThingPairingInfo *info) { - info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter the login credentials for Somfy Tahoma.")); + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter the cloud login credentials for Somfy TaHoma in order to set up local access to the Gateway.")); } void IntegrationPluginSomfyTahoma::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password) { - SomfyTahomaRequest *request = createSomfyTahomaLoginRequest(hardwareManager()->networkManager(), username, password, this); + // Request local token from cloud account. + SomfyTahomaRequest *request = createCloudSomfyTahomaLoginRequest(hardwareManager()->networkManager(), username, password, this); connect(request, &SomfyTahomaRequest::error, info, [info](){ - info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to login to Somfy Tahoma.")); + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to login to Somfy TaHoma.")); }); connect(request, &SomfyTahomaRequest::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); + SomfyTahomaRequest *request = createCloudSomfyTahomaGetRequest(hardwareManager()->networkManager(), "/config/" + info->params().paramValue(gatewayThingGatewayPinParamTypeId).toString() + "/local/tokens/generate", this); + connect(request, &SomfyTahomaRequest::error, info, [info](){ + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to generate token.")); + }); + connect(request, &SomfyTahomaRequest::finished, info, [this, info, username, password](const QVariant &result){ + QString token = result.toMap()["token"].toString(); + QJsonDocument jsonRequest{QJsonObject{ + {"label", "nymea_" + info->thingId().toString()}, + {"token", token}, + {"scope", "devmode"}, + }}; + SomfyTahomaRequest *request = createCloudSomfyTahomaPostRequest(hardwareManager()->networkManager(), "/config/" + info->params().paramValue(gatewayThingGatewayPinParamTypeId).toString() + "/local/tokens", "application/json", jsonRequest.toJson(QJsonDocument::Compact), this); + connect(request, &SomfyTahomaRequest::error, info, [info](){ + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to activate token.")); + }); + connect(request, &SomfyTahomaRequest::finished, info, [this, info, username, password, token](const QVariant &/*result*/){ + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("username", username); + pluginStorage()->setValue("password", password); + pluginStorage()->setValue("token", token); + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); + }); + }); }); } void IntegrationPluginSomfyTahoma::setupThing(ThingSetupInfo *info) { - if (info->thing()->thingClassId() == tahomaThingClassId) { - SomfyTahomaRequest *request = createLoginRequestWithStoredCredentials(info->thing()); + // Compatibility to older cloud based versions of the plugin. + if (info->thing()->thingClassId() == tahomaThingClassId || + (info->thing()->thingClassId() == gatewayThingClassId && getToken(info->thing()).isEmpty())) { + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Somfy Plugin switched to local connection. Please enable 'Developer Mode' on somfy.com, remove the account from Nymea and re-setup the Somfy TaHoma Gateway.")); + return; + } + + if (info->thing()->thingClassId() == gatewayThingClassId) { + SomfyTahomaRequest *request = createLocalSomfyTahomaGetRequest(hardwareManager()->networkManager(), getHost(info->thing()), getToken(info->thing()), "/setup", this); connect(request, &SomfyTahomaRequest::error, info, [info](){ - info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Failed to login to Somfy Tahoma.")); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Failed to connect to gateway.")); }); - connect(request, &SomfyTahomaRequest::finished, info, [this, info](const QVariant &/*result*/){ - QUuid accountId = info->thing()->id(); - SomfyTahomaRequest *request = createSomfyTahomaGetRequest(hardwareManager()->networkManager(), "/setup", this); - connect(request, &SomfyTahomaRequest::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)); + connect(request, &SomfyTahomaRequest::finished, info, [info, this](const QVariant &result){ + QList unknownDevices; + QUuid gatewayId = info->thing()->id(); + + foreach (const QVariant &deviceVariant, result.toMap()["devices"].toList()) { + QVariantMap deviceMap = deviceVariant.toMap(); + QString type = deviceMap.value("controllableName").toString(); + QString deviceUrl = deviceMap.value("deviceURL").toString(); + QString label = deviceMap.value("label").toString(); + + if (type.startsWith(QStringLiteral("io:RollerShutter"))) { + Thing *thing = myThings().findByParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, deviceUrl)); if (thing) { - qCDebug(dcSomfyTahoma()) << "Found existing gateway:" << gatewayId; + qCDebug(dcSomfyTahoma()) << "Found existing roller shutter:" << label << deviceUrl; } else { - qCInfo(dcSomfyTahoma()) << "Found new gateway:" << gatewayId; - ThingDescriptor descriptor(gatewayThingClassId, "TaHoma Gateway", QString(), accountId); - descriptor.setParams(ParamList() << Param(gatewayThingGatewayIdParamTypeId, gatewayId)); + qCInfo(dcSomfyTahoma()) << "Found new roller shutter:" << label << deviceUrl; + ThingDescriptor descriptor(rollershutterThingClassId, label, QString(), gatewayId); + descriptor.setParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, deviceUrl)); 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 if (type == QStringLiteral("GarageDoor")) { - Thing *thing = myThings().findByParams(ParamList() << Param(garagedoorThingDeviceUrlParamTypeId, deviceUrl)); - if (thing) { - qCDebug(dcSomfyTahoma()) << "Found existing garage door:" << label << deviceUrl; - } else { - qCInfo(dcSomfyTahoma()) << "Found new garage door:" << label << deviceUrl; - ThingDescriptor descriptor(garagedoorThingClassId, label, QString(), accountId); - descriptor.setParams(ParamList() << Param(garagedoorThingDeviceUrlParamTypeId, deviceUrl)); - unknownDevices.append(descriptor); - } - } else if (type == QStringLiteral("Awning")) { - Thing *thing = myThings().findByParams(ParamList() << Param(awningThingDeviceUrlParamTypeId, deviceUrl)); - if (thing) { - qCDebug(dcSomfyTahoma()) << "Found existing awning:" << label << deviceUrl; - } else { - qCInfo(dcSomfyTahoma()) << "Found new awning:" << label << deviceUrl; - ThingDescriptor descriptor(awningThingClassId, label, QString(), accountId); - descriptor.setParams(ParamList() << Param(awningThingDeviceUrlParamTypeId, deviceUrl)); - unknownDevices.append(descriptor); - } - } else if (type == QStringLiteral("Light") && (deviceUrl.startsWith("io"))) { - Thing *thing = myThings().findByParams(ParamList() << Param(lightThingDeviceUrlParamTypeId, deviceUrl)); - if (thing) { - qCDebug(dcSomfyTahoma()) << "Found existing light:" << label << deviceUrl; - } else { - qCInfo(dcSomfyTahoma()) << "Found new light:" << label << deviceUrl; - ThingDescriptor descriptor(lightThingClassId, label, QString(), accountId); - descriptor.setParams(ParamList() << Param(lightThingDeviceUrlParamTypeId, deviceUrl)); - unknownDevices.append(descriptor); - } - } else if (type == QStringLiteral("ProtocolGateway") || - type == QStringLiteral("Alarm") || - (type == QStringLiteral("Light") && deviceUrl.startsWith("hue"))) { - continue; + } else if (type == QStringLiteral("io:ExteriorVenetianBlindIOComponent")) { + Thing *thing = myThings().findByParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing venetian blind:" << label << deviceUrl; } else { - qCInfo(dcSomfyTahoma()) << "Found unsupperted Somfy device:" << label << type << deviceUrl; + qCInfo(dcSomfyTahoma()) << "Found new venetian blind:" << label << deviceUrl; + ThingDescriptor descriptor(venetianblindThingClassId, label, QString(), gatewayId); + descriptor.setParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); } + } else if (type == QStringLiteral("io:GarageOpenerIOComponent")) { + Thing *thing = myThings().findByParams(ParamList() << Param(garagedoorThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing garage door:" << label << deviceUrl; + } else { + qCInfo(dcSomfyTahoma()) << "Found new garage door:" << label << deviceUrl; + ThingDescriptor descriptor(garagedoorThingClassId, label, QString(), gatewayId); + descriptor.setParams(ParamList() << Param(garagedoorThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); + } + } else if (type == QStringLiteral("io:HorizontalAwningIOComponent")) { + Thing *thing = myThings().findByParams(ParamList() << Param(awningThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing awning:" << label << deviceUrl; + } else { + qCInfo(dcSomfyTahoma()) << "Found new awning:" << label << deviceUrl; + ThingDescriptor descriptor(awningThingClassId, label, QString(), gatewayId); + descriptor.setParams(ParamList() << Param(awningThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); + } + } else if (type == QStringLiteral("io:DimmableLightIOComponent")) { + Thing *thing = myThings().findByParams(ParamList() << Param(lightThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing light:" << label << deviceUrl; + } else { + qCInfo(dcSomfyTahoma()) << "Found new light:" << label << deviceUrl; + ThingDescriptor descriptor(lightThingClassId, label, QString(), gatewayId); + descriptor.setParams(ParamList() << Param(lightThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); + } + } else if (type == QStringLiteral("io:SomfySmokeIOSystemSensor")) { + Thing *thing = myThings().findByParams(ParamList() << Param(smokedetectorThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + qCDebug(dcSomfyTahoma()) << "Found existing smoke detector:" << label << deviceUrl; + } else { + qCInfo(dcSomfyTahoma()) << "Found new smoke detector:" << label << deviceUrl; + ThingDescriptor descriptor(smokedetectorThingClassId, label, QString(), gatewayId); + descriptor.setParams(ParamList() << Param(smokedetectorThingDeviceUrlParamTypeId, deviceUrl)); + unknownDevices.append(descriptor); + } + } else if (type == QStringLiteral("io:StackComponent") || + type.startsWith("internal:")) { + continue; + } else { + qCInfo(dcSomfyTahoma()) << "Found unsupperted Somfy device:" << label << type << deviceUrl; } - if (!unknownDevices.isEmpty()) { - emit autoThingsAppeared(unknownDevices); - } - }); + } info->finish(Thing::ThingErrorNoError); + if (!unknownDevices.isEmpty()) { + emit autoThingsAppeared(unknownDevices); + } }); } - else if (info->thing()->thingClassId() == gatewayThingClassId || - info->thing()->thingClassId() == rollershutterThingClassId || + else if (info->thing()->thingClassId() == rollershutterThingClassId || info->thing()->thingClassId() == venetianblindThingClassId || info->thing()->thingClassId() == garagedoorThingClassId || info->thing()->thingClassId() == awningThingClassId || - info->thing()->thingClassId() == lightThingClassId) { + info->thing()->thingClassId() == lightThingClassId || + info->thing()->thingClassId() == smokedetectorThingClassId) { 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); + if (thing->thingClassId() != gatewayThingClassId) { + return; } - // 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()); - } else if (thing->thingClassId() == garagedoorThingClassId) { - deviceUrl = QUrl(thing->paramValue(garagedoorThingDeviceUrlParamTypeId).toString()); - } else if (thing->thingClassId() == awningThingClassId) { - deviceUrl = QUrl(thing->paramValue(awningThingDeviceUrlParamTypeId).toString()); - } else if (thing->thingClassId() == lightThingClassId) { - deviceUrl = QUrl(thing->paramValue(lightThingDeviceUrlParamTypeId).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]); - } - - SomfyTahomaRequest *setupRequest = createSomfyTahomaGetRequest(hardwareManager()->networkManager(), "/setup", this); + // Call /setup and update the state of all devices + SomfyTahomaRequest *setupRequest = createLocalSomfyTahomaGetRequest(hardwareManager()->networkManager(), getHost(thing), getToken(thing), "/setup", this); connect(setupRequest, &SomfyTahomaRequest::error, this, [this, thing](){ markDisconnected(thing); }); connect(setupRequest, &SomfyTahomaRequest::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(); - } - } + thing->setStateValue(gatewayConnectedStateTypeId, true); foreach (const QVariant &deviceVariant, result.toMap()["devices"].toList()) { updateThingStates(deviceVariant.toMap()["deviceURL"].toString(), deviceVariant.toMap()["states"].toList()); } }); - SomfyTahomaRequest *eventRegistrationRequest = createSomfyTahomaPostRequest(hardwareManager()->networkManager(), "/events/register", "application/json", QByteArray(), this); + // Register event handler + SomfyTahomaRequest *eventRegistrationRequest = createLocalSomfyTahomaPostRequest(hardwareManager()->networkManager(), getHost(thing), getToken(thing), "/events/register", "application/json", QByteArray(), this); connect(eventRegistrationRequest, &SomfyTahomaRequest::error, this, [this, thing](){ - qCWarning(dcSomfyTahoma()) << "Failed to register for events."; markDisconnected(thing); }); connect(eventRegistrationRequest, &SomfyTahomaRequest::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](){ - SomfyTahomaRequest *eventFetchRequest = createSomfyTahomaEventFetchRequest(hardwareManager()->networkManager(), eventListenerId, this); + SomfyTahomaRequest *eventFetchRequest = createLocalSomfyTahomaEventFetchRequest(hardwareManager()->networkManager(), getHost(thing), getToken(thing), eventListenerId, this); connect(eventFetchRequest, &SomfyTahomaRequest::error, thing, [this, thing](QNetworkReply::NetworkError error){ + qCWarning(dcSomfyTahoma()) << "Failed to fetch events:" << error; markDisconnected(thing); - if (error == QNetworkReply::AuthenticationRequiredError) { - qCInfo(dcSomfyTahoma()) << "Failed to fetch events: Authentication expired, reauthenticating"; - SomfyTahomaRequest *request = createLoginRequestWithStoredCredentials(thing); - connect(request, &SomfyTahomaRequest::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, &SomfyTahomaRequest::finished, this, [this, thing](const QVariant &/*result*/){ - qCInfo(dcSomfyTahoma()) << "Reauthentication successful"; - refreshAccount(thing); - }); - } else { - qCWarning(dcSomfyTahoma()) << "Failed to fetch events:" << error; - } }); connect(eventFetchRequest, &SomfyTahomaRequest::finished, thing, [this, thing](const QVariant &result){ - thing->setStateValue(tahomaConnectedStateTypeId, true); + thing->setStateValue(gatewayConnectedStateTypeId, true); restoreChildConnectedState(thing); - if (!result.toList().empty()) { - qCDebug(dcSomfyTahoma()) << "Got events:" << qUtf8Printable(QJsonDocument::fromVariant(result).toJson()); - } handleEvents(result.toList()); }); }); @@ -275,13 +261,78 @@ void IntegrationPluginSomfyTahoma::refreshAccount(Thing *thing) void IntegrationPluginSomfyTahoma::thingRemoved(Thing *thing) { m_eventPollTimer.remove(thing); + + if (thing->thingClassId() != gatewayThingClassId) { + return; + } + + // Unregister local token from cloud account. + pluginStorage()->beginGroup(thing->id().toString()); + QString username = pluginStorage()->value("username").toString(); + QString password = pluginStorage()->value("password").toString(); + QString gatewayPin = thing->paramValue(gatewayThingGatewayPinParamTypeId).toString(); + QString tokenName = "nymea_" + thing->id().toString(); + pluginStorage()->endGroup(); + + SomfyTahomaRequest *request = createCloudSomfyTahomaLoginRequest(hardwareManager()->networkManager(), username, password, this); + connect(request, &SomfyTahomaRequest::error, this, [](){ + qCWarning(dcSomfyTahoma()) << "Failed to login to Somfy TaHoma."; + }); + connect(request, &SomfyTahomaRequest::finished, this, [this, gatewayPin, tokenName](const QVariant &/*result*/){ + SomfyTahomaRequest *request = createCloudSomfyTahomaGetRequest(hardwareManager()->networkManager(), "/config/" + gatewayPin + "/local/tokens/devmode", this); + connect(request, &SomfyTahomaRequest::error, this, [](){ + qCWarning(dcSomfyTahoma()) << "Failed to get list of tokens."; + }); + connect(request, &SomfyTahomaRequest::finished, this, [this, gatewayPin, tokenName](const QVariant &result){ + foreach (const QVariant &tokenVariant, result.toList()) { + QVariantMap tokenMap = tokenVariant.toMap(); + QString label = tokenMap["label"].toString(); + QString uuid = tokenMap["uuid"].toString(); + if (label == tokenName) { + SomfyTahomaRequest *request = createCloudSomfyTahomaDeleteRequest(hardwareManager()->networkManager(), "/config/" + gatewayPin + "/local/tokens/" + uuid, this); + connect(request, &SomfyTahomaRequest::error, this, [](){ + qCWarning(dcSomfyTahoma()) << "Failed to remove token."; + }); + } + } + }); + }); } -void IntegrationPluginSomfyTahoma::handleEvents(const QVariantList &eventList) +void IntegrationPluginSomfyTahoma::handleEvents(const QVariantList &events) { Thing *thing; - foreach (const QVariant &eventVariant, eventList) { + foreach (const QVariant &eventVariant, events) { QVariantMap eventMap = eventVariant.toMap(); + + QString device = eventMap["deviceURL"].toString(); + thing = myThings().findByParams(ParamList() << Param(rollershutterThingDeviceUrlParamTypeId, eventMap["deviceURL"])); + if (thing) { + device = thing->name(); + } + thing = myThings().findByParams(ParamList() << Param(venetianblindThingDeviceUrlParamTypeId, eventMap["deviceURL"])); + if (thing) { + device = thing->name(); + } + thing = myThings().findByParams(ParamList() << Param(garagedoorThingDeviceUrlParamTypeId, eventMap["deviceURL"])); + if (thing) { + device = thing->name(); + } + thing = myThings().findByParams(ParamList() << Param(awningThingDeviceUrlParamTypeId, eventMap["deviceURL"])); + if (thing) { + device = thing->name(); + } + thing = myThings().findByParams(ParamList() << Param(lightThingDeviceUrlParamTypeId, eventMap["deviceURL"])); + if (thing) { + device = thing->name(); + } + thing = myThings().findByParams(ParamList() << Param(smokedetectorThingDeviceUrlParamTypeId, eventMap["deviceURL"])); + if (thing) { + device = thing->name(); + } + qCDebug(dcSomfyTahoma()) << "Got event" << eventMap["name"].toString() << "for device" << device; + qCDebug(dcSomfyTahoma()) << qUtf8Printable(QJsonDocument::fromVariant(eventVariant).toJson()); + if (eventMap["name"] == "DeviceStateChangedEvent") { updateThingStates(eventMap["deviceURL"].toString(), eventMap["deviceStates"].toList()); } else if (eventMap["name"] == "ExecutionRegisteredEvent") { @@ -340,30 +391,6 @@ void IntegrationPluginSomfyTahoma::handleEvents(const QVariantList &eventList) 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"]; - } } } } @@ -468,6 +495,47 @@ void IntegrationPluginSomfyTahoma::updateThingStates(const QString &deviceUrl, c } return; } + thing = myThings().findByParams(ParamList() << Param(smokedetectorThingDeviceUrlParamTypeId, deviceUrl)); + if (thing) { + foreach (const QVariant &stateVariant, stateList) { + QVariantMap stateMap = stateVariant.toMap(); + if (stateMap["name"] == "core:SmokeState") { + thing->setStateValue(smokedetectorFireDetectedStateTypeId, stateMap["value"] == "detected"); + } else if (stateMap["name"] == "core:MaintenanceRadioPartBatteryState") { + QString radioBattery = stateMap["value"].toString(); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("radioBatteryState", stateMap["value"]); + QString sensorBattery = pluginStorage()->value("sensorBatteryState", "normal").toString(); + pluginStorage()->endGroup(); + if (radioBattery == "normal" && sensorBattery == "normal") { + thing->setStateValue(smokedetectorBatteryCriticalStateTypeId, false); + } else { + qCWarning(dcSomfyTahoma()) << "Smoke Detector" << thing->name() << " radio battery is low!"; + thing->setStateValue(smokedetectorBatteryCriticalStateTypeId, true); + } + } else if (stateMap["name"] == "core:MaintenanceSensorPartBatteryState") { + QString sensorBattery = stateMap["value"].toString(); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("radioBatteryState", stateMap["value"]); + QString radioBattery = pluginStorage()->value("radioBatteryState", "normal").toString(); + pluginStorage()->endGroup(); + if (radioBattery == "normal" && sensorBattery == "normal") { + thing->setStateValue(smokedetectorBatteryCriticalStateTypeId, false); + } else { + qCWarning(dcSomfyTahoma()) << "Smoke Detector" << thing->name() << " sensor battery is low!"; + thing->setStateValue(smokedetectorBatteryCriticalStateTypeId, true); + } + } else if (stateMap["name"] == "core:StatusState") { + thing->setStateValue(smokedetectorConnectedStateTypeId, stateMap["value"] == "available"); + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("connected", stateMap["value"] == "available"); + pluginStorage()->endGroup(); + } else if (stateMap["name"] == "core:RSSILevelState") { + thing->setStateValue(smokedetectorSignalStrengthStateTypeId, stateMap["value"]); + } + } + return; + } } void IntegrationPluginSomfyTahoma::executeAction(ThingActionInfo *info) @@ -551,7 +619,7 @@ void IntegrationPluginSomfyTahoma::executeAction(ThingActionInfo *info) {"commands", QJsonArray{QJsonObject{{"name", actionName}, {"parameters", actionParameters}}}}}}} }}; - SomfyTahomaRequest *request = createSomfyTahomaPostRequest(hardwareManager()->networkManager(), "/exec/apply", "application/json", jsonRequest.toJson(QJsonDocument::Compact), this); + SomfyTahomaRequest *request = createLocalSomfyTahomaPostRequest(hardwareManager()->networkManager(), getHost(info->thing()), getToken(info->thing()), "/exec/apply", "application/json", jsonRequest.toJson(QJsonDocument::Compact), this); connect(request, &SomfyTahomaRequest::error, info, [info](){ info->finish(Thing::ThingErrorHardwareFailure); }); @@ -564,20 +632,9 @@ void IntegrationPluginSomfyTahoma::executeAction(ThingActionInfo *info) } } -SomfyTahomaRequest *IntegrationPluginSomfyTahoma::createLoginRequestWithStoredCredentials(Thing *thing) -{ - pluginStorage()->beginGroup(thing->id().toString()); - QString username = pluginStorage()->value("username").toString(); - QString password = pluginStorage()->value("password").toString(); - pluginStorage()->endGroup(); - return createSomfyTahomaLoginRequest(hardwareManager()->networkManager(), username, password, this); -} - void IntegrationPluginSomfyTahoma::markDisconnected(Thing *thing) { - if (thing->thingClassId() == tahomaThingClassId) { - thing->setStateValue(tahomaConnectedStateTypeId, false); - } else if (thing->thingClassId() == gatewayThingClassId) { + if (thing->thingClassId() == gatewayThingClassId) { thing->setStateValue(gatewayConnectedStateTypeId, false); } else if (thing->thingClassId() == rollershutterThingClassId) { thing->setStateValue(rollershutterConnectedStateTypeId, false); @@ -589,6 +646,8 @@ void IntegrationPluginSomfyTahoma::markDisconnected(Thing *thing) thing->setStateValue(awningConnectedStateTypeId, false); } else if (thing->thingClassId() == lightThingClassId) { thing->setStateValue(lightConnectedStateTypeId, false); + } else if (thing->thingClassId() == smokedetectorThingClassId) { + thing->setStateValue(smokedetectorConnectedStateTypeId, false); } foreach (Thing *child, myThings().filterByParentId(thing->id())) { markDisconnected(child); @@ -599,9 +658,7 @@ 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) { + if (thing->thingClassId() == rollershutterThingClassId) { thing->setStateValue(rollershutterConnectedStateTypeId, pluginStorage()->value("connected").toBool()); } else if (thing->thingClassId() == venetianblindThingClassId) { thing->setStateValue(venetianblindConnectedStateTypeId, pluginStorage()->value("connected").toBool()); @@ -611,6 +668,8 @@ void IntegrationPluginSomfyTahoma::restoreChildConnectedState(Thing *thing) thing->setStateValue(awningConnectedStateTypeId, pluginStorage()->value("connected").toBool()); } else if (thing->thingClassId() == lightThingClassId) { thing->setStateValue(lightConnectedStateTypeId, pluginStorage()->value("connected").toBool()); + } else if (thing->thingClassId() == smokedetectorThingClassId) { + thing->setStateValue(smokedetectorConnectedStateTypeId, pluginStorage()->value("connected").toBool()); } } pluginStorage()->endGroup(); @@ -618,3 +677,46 @@ void IntegrationPluginSomfyTahoma::restoreChildConnectedState(Thing *thing) restoreChildConnectedState(child); } } + +QString IntegrationPluginSomfyTahoma::getHost(Thing *thing) const +{ + Thing *gateway = thing; + if (!thing->parentId().isNull()) { + gateway = myThings().findById(thing->parentId()); + } + + QString gatewayPin = gateway->paramValue(gatewayThingGatewayPinParamTypeId).toString(); + ZeroConfServiceEntry zeroConfEntry; + foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) { + if (gatewayPin == entry.txt("gateway_pin")) { + zeroConfEntry = entry; + } + } + QString host; + pluginStorage()->beginGroup(gateway->id().toString()); + if (zeroConfEntry.isValid()) { + host = zeroConfEntry.hostAddress().toString() + ":" + QString::number(zeroConfEntry.port()); + pluginStorage()->setValue("cachedAddress", host); + } else if (pluginStorage()->contains("cachedAddress")){ + host = pluginStorage()->value("cachedAddress").toString(); + } else { + qCWarning(dcSomfyTahoma()) << "Unable to determine IP address for:" << gatewayPin; + } + pluginStorage()->endGroup(); + + return host; +} + +QString IntegrationPluginSomfyTahoma::getToken(Thing *thing) const +{ + Thing *gateway = thing; + if (!thing->parentId().isNull()) { + gateway = myThings().findById(thing->parentId()); + } + + QString token; + pluginStorage()->beginGroup(gateway->id().toString()); + token = pluginStorage()->value("token").toString(); + pluginStorage()->endGroup(); + return token; +} diff --git a/somfytahoma/integrationpluginsomfytahoma.h b/somfytahoma/integrationpluginsomfytahoma.h index e4323b88..330467c3 100644 --- a/somfytahoma/integrationpluginsomfytahoma.h +++ b/somfytahoma/integrationpluginsomfytahoma.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -34,6 +34,12 @@ #include "integrations/integrationplugin.h" #include "plugintimer.h" +#include "extern-plugininfo.h" + +class QHostAddress; + +class ZeroConfServiceBrowser; + class SomfyTahomaRequest; class IntegrationPluginSomfyTahoma : public IntegrationPlugin @@ -44,6 +50,9 @@ class IntegrationPluginSomfyTahoma : public IntegrationPlugin Q_INTERFACES(IntegrationPlugin) public: + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void startPairing(ThingPairingInfo *info) override; void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password) override; @@ -54,14 +63,16 @@ public: void executeAction(ThingActionInfo *info) override; private: - SomfyTahomaRequest *createLoginRequestWithStoredCredentials(Thing *thing); - void refreshAccount(Thing *thing); - void handleEvents(const QVariantList &eventList); + void refreshGateway(Thing *thing); + void handleEvents(const QVariantList &events); void updateThingStates(const QString &deviceUrl, const QVariantList &stateList); void markDisconnected(Thing *thing); void restoreChildConnectedState(Thing *thing); + QString getHost(Thing *thing) const; + QString getToken(Thing *thing) const; private: + ZeroConfServiceBrowser *m_zeroConfBrowser = nullptr; QMap m_eventPollTimer; QMap> m_pendingActions; QMap> m_currentExecutions; diff --git a/somfytahoma/integrationpluginsomfytahoma.json b/somfytahoma/integrationpluginsomfytahoma.json index bc6008c9..cf3edc2a 100644 --- a/somfytahoma/integrationpluginsomfytahoma.json +++ b/somfytahoma/integrationpluginsomfytahoma.json @@ -1,6 +1,6 @@ { "name": "SomfyTahoma", - "displayName": "Somfy Tahoma", + "displayName": "Somfy TaHoma", "id": "4e8be1c1-daa8-4e21-9e85-b2372ab1a450", "vendors": [ { @@ -11,9 +11,8 @@ { "id": "fedd72b8-547d-4e4f-b73e-71344a8ba0c1", "name": "tahoma", - "displayName": "Tahoma Account", - "createMethods": ["user"], - "setupMethod": "userandpassword", + "displayName": "TaHoma Account", + "createMethods": ["auto"], "interfaces": ["account"], "stateTypes": [ { @@ -45,15 +44,17 @@ { "id": "6c09e0b9-f0cc-4dea-9994-9e039eff78f1", "name": "gateway", - "displayName": "Tahoma Gateway", - "createMethods": ["auto"], + "displayName": "TaHoma Gateway", + "createMethods": ["discovery"], + "setupMethod": "userandpassword", "interfaces": ["gateway"], "paramTypes": [ { - "id": "e321a7d6-6dcb-4a37-baf1-c7008f2d5bdb", - "displayName": "Gateway Id", - "name": "gatewayId", - "type": "QString" + "id": "30b73244-e5bb-4c00-9332-702a60c03420", + "displayName": "Gateway pin", + "name": "gatewayPin", + "type": "QString", + "readOnly": true } ], "stateTypes": [ @@ -443,7 +444,61 @@ "defaultValue": false } ] + }, + { + "id": "e883a8ef-1fb7-4d7f-b40b-9cbc0133e58a", + "name": "smokedetector", + "displayName": "Smoke Detector", + "createMethods": ["auto"], + "interfaces": ["firesensor", "battery", "wirelessconnectable"], + "paramTypes": [ + { + "id": "3a41017b-87fe-4690-b148-1f69fac67f91", + "displayName": "Device URL", + "name": "deviceUrl", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "6c3ee0ce-fc0f-477c-9a01-be61e061463b", + "name": "fireDetected", + "displayName": "Fire detected", + "displayNameEvent": "Fire detected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "8f67b692-243b-4ce8-9983-0d1206439f16", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "47f8506c-af7f-4928-b0ce-8c52ce4f740b", + "name": "signalStrength", + "displayName": "Signal strength", + "type": "uint", + "unit": "Percentage", + "displayNameEvent": "Signal strength changed", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "98cf101c-cb0e-47a9-9c27-b6a0cdf9ba4b", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "displayNameEvent": "Connetion state changed", + "defaultValue": false + } + ] } + + ] } ] diff --git a/somfytahoma/somfytahomarequests.cpp b/somfytahoma/somfytahomarequests.cpp index bc115b58..617ab2e3 100644 --- a/somfytahoma/somfytahomarequests.cpp +++ b/somfytahoma/somfytahomarequests.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -38,7 +38,8 @@ #include "extern-plugininfo.h" -static const QString somfyTahomaUrl = QStringLiteral("https://tahomalink.com/enduser-mobile-web/enduserAPI"); +static const QString somfyTahomaWebUrl = QStringLiteral("https://ha101-1.overkiz.com/enduser-mobile-web/enduserAPI"); +static const QString localSomfyTahomaPath = QStringLiteral("/enduser-mobile-web/1/enduserAPI"); SomfyTahomaRequest::SomfyTahomaRequest(QNetworkReply *reply, QObject *parent) : QObject(parent) { @@ -46,7 +47,19 @@ SomfyTahomaRequest::SomfyTahomaRequest(QNetworkReply *reply, QObject *parent) : connect(reply, &QNetworkReply::finished, this, [this, reply] { deleteLater(); if (reply->error() != QNetworkReply::NoError) { - qCWarning(dcSomfyTahoma()) << "Request for" << reply->url().path() << "failed:" << reply->errorString(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QVariantMap requestHeaders; + foreach (const QByteArray &rawHeader, reply->request().rawHeaderList()) { + requestHeaders.insert(rawHeader, reply->request().rawHeader(rawHeader)); + } + QVariantMap replyHeaders; + foreach (const QByteArray &rawHeader, reply->rawHeaderList()) { + replyHeaders.insert(rawHeader, reply->rawHeader(rawHeader)); + } + qCWarning(dcSomfyTahoma()).noquote() << "Request error:" << status << "for URL:" << reply->url().toString() + << "on operation" << reply->operation() << "\n" << "Content:" << reply->readAll(); + qCDebug(dcSomfyTahoma()).noquote() << "Request headers:" << QJsonDocument::fromVariant(requestHeaders).toJson() + << "Reply headers:" << QJsonDocument::fromVariant(replyHeaders).toJson(); emit error(reply->error()); return; } @@ -55,7 +68,7 @@ SomfyTahomaRequest::SomfyTahomaRequest(QNetworkReply *reply, QObject *parent) : QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) { - qCWarning(dcSomfyTahoma()) << "Json parse error in reply for" << reply->url().path() << ":" << parseError.errorString(); + qCWarning(dcSomfyTahoma()) << "Json parse error:" << reply->url().toString() << ":" << parseError.error << parseError.errorString(); emit error(QNetworkReply::UnknownContentError); return; } @@ -64,32 +77,73 @@ SomfyTahomaRequest::SomfyTahomaRequest(QNetworkReply *reply, QObject *parent) : }); } -SomfyTahomaRequest *createSomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent) +SomfyTahomaRequest *createCloudSomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent) { - QUrl url(somfyTahomaUrl + path); + QUrl url(somfyTahomaWebUrl + path); QNetworkRequest request(url); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType); QNetworkReply *reply = networkManager->post(request, body); return new SomfyTahomaRequest(reply, parent); } -SomfyTahomaRequest *createSomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent) +SomfyTahomaRequest *createCloudSomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent) { - QUrl url(somfyTahomaUrl + path); + QUrl url(somfyTahomaWebUrl + path); QNetworkRequest request(url); QNetworkReply *reply = networkManager->get(request); return new SomfyTahomaRequest(reply, parent); } -SomfyTahomaRequest *createSomfyTahomaLoginRequest(NetworkAccessManager *networkManager, const QString &username, const QString &password, QObject *parent) +SomfyTahomaRequest *createCloudSomfyTahomaDeleteRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent) +{ + QUrl url(somfyTahomaWebUrl + path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::KnownHeaders::ContentLengthHeader, 0); + + QNetworkReply *reply = networkManager->deleteResource(request); + return new SomfyTahomaRequest(reply, parent); +} + +SomfyTahomaRequest *createCloudSomfyTahomaLoginRequest(NetworkAccessManager *networkManager, const QString &username, const QString &password, QObject *parent) { QUrlQuery postData; postData.addQueryItem("userId", username); postData.addQueryItem("userPassword", password); - return createSomfyTahomaPostRequest(networkManager, "/login", "application/x-www-form-urlencoded", postData.toString(QUrl::FullyEncoded).toUtf8(), parent); + return createCloudSomfyTahomaPostRequest(networkManager, "/login", "application/x-www-form-urlencoded", postData.toString(QUrl::FullyEncoded).toUtf8(), parent); } -SomfyTahomaRequest *createSomfyTahomaEventFetchRequest(NetworkAccessManager *networkManager, const QString &eventListenerId, QObject *parent) +SomfyTahomaRequest *createLocalSomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &host, const QString &token, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent) { - return createSomfyTahomaPostRequest(networkManager, "/events/" + eventListenerId + "/fetch", "application/json", QByteArray(), parent); + QUrl url("https://" + host + localSomfyTahomaPath + path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, contentType); + request.setRawHeader("Authorization", "Bearer " + token.toUtf8()); + + QSslConfiguration config = QSslConfiguration::defaultConfiguration(); + config.setPeerVerifyMode(QSslSocket::VerifyNone); + request.setSslConfiguration(config); + + QNetworkReply *reply = networkManager->post(request, body); + + return new SomfyTahomaRequest(reply, parent); +} + +SomfyTahomaRequest *createLocalSomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &host, const QString &token, const QString &path, QObject *parent) +{ + QUrl url("https://" + host + localSomfyTahomaPath + path); + QNetworkRequest request(url); + request.setRawHeader("Authorization", "Bearer " + token.toUtf8()); + + QSslConfiguration config = QSslConfiguration::defaultConfiguration(); + config.setPeerVerifyMode(QSslSocket::VerifyNone); + request.setSslConfiguration(config); + + QNetworkReply *reply = networkManager->get(request); + + return new SomfyTahomaRequest(reply, parent); +} + +SomfyTahomaRequest *createLocalSomfyTahomaEventFetchRequest(NetworkAccessManager *networkManager, const QString &host, const QString &token, const QString &eventListenerId, QObject *parent) +{ + return createLocalSomfyTahomaPostRequest(networkManager, host, token, "/events/" + eventListenerId + "/fetch", "application/json", QByteArray(), parent); } diff --git a/somfytahoma/somfytahomarequests.h b/somfytahoma/somfytahomarequests.h index a72fbccf..defad72e 100644 --- a/somfytahoma/somfytahomarequests.h +++ b/somfytahoma/somfytahomarequests.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -47,9 +47,14 @@ signals: void finished(const QVariant &results); }; -SomfyTahomaRequest *createSomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent); -SomfyTahomaRequest *createSomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent); -SomfyTahomaRequest *createSomfyTahomaLoginRequest(NetworkAccessManager *networkManager, const QString &username, const QString &password, QObject *parent); -SomfyTahomaRequest *createSomfyTahomaEventFetchRequest(NetworkAccessManager *networkManager, const QString &eventListenerId, QObject *parent); +SomfyTahomaRequest *createCloudSomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent); +SomfyTahomaRequest *createCloudSomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent); +SomfyTahomaRequest *createCloudSomfyTahomaDeleteRequest(NetworkAccessManager *networkManager, const QString &path, QObject *parent); +SomfyTahomaRequest *createCloudSomfyTahomaLoginRequest(NetworkAccessManager *networkManager, const QString &username, const QString &password, QObject *parent); + +SomfyTahomaRequest *createLocalSomfyTahomaPostRequest(NetworkAccessManager *networkManager, const QString &host, const QString &token, const QString &path, const QString &contentType, const QByteArray &body, QObject *parent); +SomfyTahomaRequest *createLocalSomfyTahomaGetRequest(NetworkAccessManager *networkManager, const QString &host, const QString &token, const QString &path, QObject *parent); +SomfyTahomaRequest *createLocalSomfyTahomaEventFetchRequest(NetworkAccessManager *networkManager, const QString &host, const QString &token, 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 index 61c319bc..bb44680c 100644 --- a/somfytahoma/translations/4e8be1c1-daa8-4e21-9e85-b2372ab1a450-en_US.ts +++ b/somfytahoma/translations/4e8be1c1-daa8-4e21-9e85-b2372ab1a450-en_US.ts @@ -4,66 +4,67 @@ IntegrationPluginSomfyTahoma - - Please enter the login credentials for Somfy Tahoma. + + Please enter the cloud login credentials for Somfy Tahoma in order to set up local access to the Gateway. - - + Failed to login to Somfy Tahoma. + + + Failed to generate token. + + + + + Failed to activate token. + + + + + The Somfy Plugin switched to local connection. Please enable 'Developer Mode' on somfy.com, remove the account from Nymea and re-setup the Somfy Tahoma Gateway. + + + + + Failed to connect to gateway. + + 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 - - - - + Awning The name of the ThingClass ({d3a3bb40-4b2d-4bdc-989f-5254f03b4c90}) - - - + + Brightness The name of the ParamType (ThingClass: light, ActionType: brightness, ID: {1b51ce68-1f7e-4f06-b68d-bfca2d61b353}) ---------- -The name of the ParamType (ThingClass: light, EventType: brightness, ID: {1b51ce68-1f7e-4f06-b68d-bfca2d61b353}) ----------- The name of the StateType ({1b51ce68-1f7e-4f06-b68d-bfca2d61b353}) of ThingClass light - - Brightness changed - The name of the EventType ({1b51ce68-1f7e-4f06-b68d-bfca2d61b353}) of ThingClass light - - - - - - - + + + + Close The name of the ActionType ({20cae53b-f36d-425b-b937-3e46519893a3}) of ThingClass awning ---------- @@ -75,82 +76,43 @@ The name of the ActionType ({baf377c6-9fba-44cf-9f14-af0101f874b5}) of ThingClas - - - - - - - - - - - - - - + + + + + + + + Connected - The name of the ParamType (ThingClass: light, EventType: connected, ID: {fb8dcd84-70ad-4f3e-97c4-93296608e33d}) + The name of the StateType ({98cf101c-cb0e-47a9-9c27-b6a0cdf9ba4b}) of ThingClass smokedetector ---------- The name of the StateType ({fb8dcd84-70ad-4f3e-97c4-93296608e33d}) of ThingClass light ---------- -The name of the ParamType (ThingClass: awning, EventType: connected, ID: {8f972969-10dd-4954-9c8b-de56070a6668}) ----------- The name of the StateType ({8f972969-10dd-4954-9c8b-de56070a6668}) of ThingClass awning ---------- -The name of the ParamType (ThingClass: garagedoor, EventType: connected, ID: {5a32cbd3-bc1c-4724-ae53-9f36cb75bf84}) ----------- The name of the StateType ({5a32cbd3-bc1c-4724-ae53-9f36cb75bf84}) of ThingClass garagedoor ---------- -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 ({fb8dcd84-70ad-4f3e-97c4-93296608e33d}) of ThingClass light ----------- -The name of the EventType ({8f972969-10dd-4954-9c8b-de56070a6668}) of ThingClass awning ----------- -The name of the EventType ({5a32cbd3-bc1c-4724-ae53-9f36cb75bf84}) of ThingClass garagedoor ----------- -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: light, Type: thing, ID: {9cd2e0f2-a02f-478a-9358-6ff0f5aba9f5}) + The name of the ParamType (ThingClass: smokedetector, Type: thing, ID: {3a41017b-87fe-4690-b148-1f69fac67f91}) +---------- +The name of the ParamType (ThingClass: light, Type: thing, ID: {9cd2e0f2-a02f-478a-9358-6ff0f5aba9f5}) ---------- The name of the ParamType (ThingClass: awning, Type: thing, ID: {ca60f12e-b9da-427a-a149-195922399fd5}) ---------- @@ -162,85 +124,49 @@ The name of the ParamType (ThingClass: rollershutter, Type: thing, ID: {b3d20d6a - + Garage Door The name of the ThingClass ({cb206d74-b13c-4466-98c6-070b19ebd23a}) - + Gateway Id The name of the ParamType (ThingClass: gateway, Type: thing, ID: {e321a7d6-6dcb-4a37-baf1-c7008f2d5bdb}) - + Light The name of the ThingClass ({e569a3cc-6e79-4e24-af35-c5fa327a7314}) - - + 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 + 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: awning, EventType: moving, ID: {2507ac5a-9658-42cb-80f6-73f673c32771}) ----------- -The name of the StateType ({2507ac5a-9658-42cb-80f6-73f673c32771}) of ThingClass awning ----------- -The name of the ParamType (ThingClass: garagedoor, EventType: moving, ID: {07175175-f95d-4cd9-a398-9aab8232c2a9}) + The name of the StateType ({2507ac5a-9658-42cb-80f6-73f673c32771}) of ThingClass awning ---------- The name of the StateType ({07175175-f95d-4cd9-a398-9aab8232c2a9}) of ThingClass garagedoor ---------- -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 ({2507ac5a-9658-42cb-80f6-73f673c32771}) of ThingClass awning ----------- -The name of the EventType ({07175175-f95d-4cd9-a398-9aab8232c2a9}) of ThingClass garagedoor ----------- -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 ({9612954c-02cb-4159-9a29-f36eaf1c7f6a}) of ThingClass awning ---------- @@ -252,100 +178,82 @@ The name of the ActionType ({a0460180-e799-4bc6-83ba-11731ef124a3}) of ThingClas - - - - - - - - - - - - + + + + + + + + Percentage The name of the ParamType (ThingClass: awning, ActionType: percentage, ID: {c409cb9b-82ef-4f59-ae89-eb783d4ebe97}) ---------- -The name of the ParamType (ThingClass: awning, EventType: percentage, ID: {c409cb9b-82ef-4f59-ae89-eb783d4ebe97}) ----------- The name of the StateType ({c409cb9b-82ef-4f59-ae89-eb783d4ebe97}) of ThingClass awning ---------- The name of the ParamType (ThingClass: garagedoor, ActionType: percentage, ID: {284816aa-842b-4a86-bb4e-ef5353b76762}) ---------- -The name of the ParamType (ThingClass: garagedoor, EventType: percentage, ID: {284816aa-842b-4a86-bb4e-ef5353b76762}) ----------- The name of the StateType ({284816aa-842b-4a86-bb4e-ef5353b76762}) of ThingClass garagedoor ---------- 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 ({c409cb9b-82ef-4f59-ae89-eb783d4ebe97}) of ThingClass awning ----------- -The name of the EventType ({284816aa-842b-4a86-bb4e-ef5353b76762}) of ThingClass garagedoor ----------- -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 - - - - - - + + Power The name of the ParamType (ThingClass: light, ActionType: power, ID: {654ddcdf-b0b7-4c38-a70d-878f0f3857a5}) ---------- -The name of the ParamType (ThingClass: light, EventType: power, ID: {654ddcdf-b0b7-4c38-a70d-878f0f3857a5}) ----------- The name of the StateType ({654ddcdf-b0b7-4c38-a70d-878f0f3857a5}) of ThingClass light - - Power changed - The name of the EventType ({654ddcdf-b0b7-4c38-a70d-878f0f3857a5}) of ThingClass light + + Gateway pin + The name of the ParamType (ThingClass: gateway, Type: thing, ID: {30b73244-e5bb-4c00-9332-702a60c03420}) - + + Battery critical + The name of the StateType ({8f67b692-243b-4ce8-9983-0d1206439f16}) of ThingClass smokedetector + + + + + Fire detected + The name of the StateType ({6c3ee0ce-fc0f-477c-9a01-be61e061463b}) of ThingClass smokedetector + + + + 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 brightness The name of the ActionType ({1b51ce68-1f7e-4f06-b68d-bfca2d61b353}) of ThingClass light - - - - + + + + Set percentage The name of the ActionType ({c409cb9b-82ef-4f59-ae89-eb783d4ebe97}) of ThingClass awning ---------- @@ -357,94 +265,61 @@ The name of the ActionType ({f954ffc7-a6aa-4d30-aee0-0484631c3344}) of ThingClas - + Set power The name of the ActionType ({654ddcdf-b0b7-4c38-a70d-878f0f3857a5}) of ThingClass light - - - - - - - - - - + + + + + + Signal strength - The name of the ParamType (ThingClass: light, EventType: signalStrength, ID: {cfaa5533-d26e-4545-9f44-6567c9d7888a}) + The name of the StateType ({47f8506c-af7f-4928-b0ce-8c52ce4f740b}) of ThingClass smokedetector ---------- The name of the StateType ({cfaa5533-d26e-4545-9f44-6567c9d7888a}) of ThingClass light ---------- -The name of the ParamType (ThingClass: awning, EventType: signalStrength, ID: {b2ad6f4a-c507-45c3-a951-b344603cc3fc}) ----------- The name of the StateType ({b2ad6f4a-c507-45c3-a951-b344603cc3fc}) of ThingClass awning ---------- -The name of the ParamType (ThingClass: garagedoor, EventType: signalStrength, ID: {0a194091-3073-4912-9d84-f1d52c8534bd}) ----------- The name of the StateType ({0a194091-3073-4912-9d84-f1d52c8534bd}) of ThingClass garagedoor ---------- -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 ({cfaa5533-d26e-4545-9f44-6567c9d7888a}) of ThingClass light ----------- -The name of the EventType ({b2ad6f4a-c507-45c3-a951-b344603cc3fc}) of ThingClass awning ----------- -The name of the EventType ({0a194091-3073-4912-9d84-f1d52c8534bd}) of ThingClass garagedoor ----------- -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 + + Smoke Detector + The name of the ThingClass ({e883a8ef-1fb7-4d7f-b40b-9cbc0133e58a}) - + Somfy The name of the vendor ({4e42a22a-ccfb-4677-89e3-f7fa16bf6be0}) - + Somfy Tahoma The name of the plugin SomfyTahoma ({4e8be1c1-daa8-4e21-9e85-b2372ab1a450}) - - + State - The name of the ParamType (ThingClass: garagedoor, EventType: state, ID: {12af28f1-475e-4d05-9bbb-adbb86dcd69c}) ----------- -The name of the StateType ({12af28f1-475e-4d05-9bbb-adbb86dcd69c}) of ThingClass garagedoor + The name of the StateType ({12af28f1-475e-4d05-9bbb-adbb86dcd69c}) of ThingClass garagedoor - - State changed - The name of the EventType ({12af28f1-475e-4d05-9bbb-adbb86dcd69c}) of ThingClass garagedoor - - - - - - - + + + + Stop The name of the ActionType ({33bec73b-4d15-493a-b553-bcee32c40ee1}) of ThingClass awning ---------- @@ -456,34 +331,25 @@ The name of the ActionType ({cbccf714-1188-4ac9-9c91-17fe2c99acb3}) of ThingClas - + 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 + 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})