diff --git a/debian/control b/debian/control index 813808a2..5d6b1dd9 100644 --- a/debian/control +++ b/debian/control @@ -845,6 +845,21 @@ Description: nymea.io plugin to monitor the system This package will install the nymea.io plugin for system monitoring +Package: nymea-plugin-tado +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin to connect to your Tado account + 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 Tado + + Package: nymea-plugins-translations Section: misc Architecture: all @@ -888,6 +903,7 @@ Depends: nymea-plugin-anel, nymea-plugin-shelly, nymea-plugin-senic, nymea-plugin-sonos, + nymea-plugin-tado, nymea-plugin-keba, Replaces: guh-plugins Description: Plugins for nymea IoT server - the default plugin collection diff --git a/debian/nymea-plugin-tado.install.in b/debian/nymea-plugin-tado.install.in new file mode 100644 index 00000000..2a7d8bab --- /dev/null +++ b/debian/nymea-plugin-tado.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugintado.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 1a61f501..168cef4a 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -43,6 +43,7 @@ PLUGIN_DIRS = \ simulation \ snapd \ sonos \ + tado \ tasmota \ tcpcommander \ texasinstruments \ diff --git a/tado/README.md b/tado/README.md new file mode 100644 index 00000000..dd065d92 --- /dev/null +++ b/tado/README.md @@ -0,0 +1,14 @@ +# Tado + +Let's you connect to your Tado account. All configured zones will appear in nymea automatically. +You will get all relevant data and can set the mode and target temperature. + +## Device Setup + +nymea will connect to your Tado account. After the account is connected all associated devices +will appear automatically. The Tado app is required to create a Tado account and connect the +devices to it. + +More about Tado: +https://www.tado.com + diff --git a/tado/deviceplugintado.cpp b/tado/deviceplugintado.cpp new file mode 100644 index 00000000..f9d6088d --- /dev/null +++ b/tado/deviceplugintado.cpp @@ -0,0 +1,333 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* This project may also contain libraries licensed under the open source software license GNU GPL v.3. +* 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 "deviceplugintado.h" +#include "devices/device.h" +#include "plugininfo.h" +#include "network/networkaccessmanager.h" + +#include +#include +#include + +DevicePluginTado::DevicePluginTado() +{ + +} + +void DevicePluginTado::startPairing(DevicePairingInfo *info) +{ + info->finish(Device::DeviceErrorNoError, QT_TR_NOOP("Please enter the login credentials.")); +} + +void DevicePluginTado::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &password) +{ + Tado *tado = new Tado(hardwareManager()->networkManager(), username, this); + connect(tado, &Tado::tokenReceived, this, &DevicePluginTado::onTokenReceived); + connect(tado, &Tado::authenticationStatusChanged, this, &DevicePluginTado::onAuthenticationStatusChanged); + connect(tado, &Tado::connectionChanged, this, &DevicePluginTado::onConnectionChanged); + connect(tado, &Tado::homesReceived, this, &DevicePluginTado::onHomesReceived); + connect(tado, &Tado::zonesReceived, this, &DevicePluginTado::onZonesReceived); + connect(tado, &Tado::zoneStateReceived, this, &DevicePluginTado::onZoneStateReceived); + connect(tado, &Tado::overlayReceived, this, &DevicePluginTado::onOverlayReceived); + m_unfinishedTadoAccounts.insert(info->deviceId(), tado); + m_unfinishedDevicePairings.insert(info->deviceId(), info); + tado->getToken(password); + + pluginStorage()->beginGroup(info->deviceId().toString()); + pluginStorage()->setValue("username", username); + pluginStorage()->setValue("password", password); + pluginStorage()->endGroup(); +} + +void DevicePluginTado::setupDevice(DeviceSetupInfo *info) +{ + Device *device = info->device(); + + if (device->deviceClassId() == tadoConnectionDeviceClassId) { + + qCDebug(dcTado) << "Setup tado connection" << device->name() << device->params(); + pluginStorage()->beginGroup(device->id().toString()); + QString username = pluginStorage()->value("username").toString(); + QString password = pluginStorage()->value("password").toString(); + pluginStorage()->endGroup(); + + Tado *tado; + if (m_unfinishedTadoAccounts.contains(device->id())) { + tado = m_unfinishedTadoAccounts.take(device->id()); + m_tadoAccounts.insert(device->id(), tado); + return info->finish(Device::DeviceErrorNoError); + } else { + tado = new Tado(hardwareManager()->networkManager(), username, this); + connect(tado, &Tado::tokenReceived, this, &DevicePluginTado::onTokenReceived); + connect(tado, &Tado::authenticationStatusChanged, this, &DevicePluginTado::onAuthenticationStatusChanged); + connect(tado, &Tado::connectionChanged, this, &DevicePluginTado::onConnectionChanged); + connect(tado, &Tado::homesReceived, this, &DevicePluginTado::onHomesReceived); + connect(tado, &Tado::zonesReceived, this, &DevicePluginTado::onZonesReceived); + connect(tado, &Tado::zoneStateReceived, this, &DevicePluginTado::onZoneStateReceived); + connect(tado, &Tado::overlayReceived, this, &DevicePluginTado::onOverlayReceived); + tado->getToken(password); + m_tadoAccounts.insert(device->id(), tado); + m_asyncDeviceSetup.insert(tado, info); + return; + } + + } else if (device->deviceClassId() == zoneDeviceClassId) { + qCDebug(dcTado) << "Setup tado thermostat" << device->params(); + return info->finish(Device::DeviceErrorNoError); + } + qCWarning(dcTado()) << "Unhandled device class in setupDevice"; +} + +void DevicePluginTado::deviceRemoved(Device *device) +{ + if (device->deviceClassId() == tadoConnectionDeviceClassId) { + Tado *tado = m_tadoAccounts.take(device->id()); + tado->deleteLater(); + } + + if (myDevices().isEmpty() && m_pluginTimer) { + m_pluginTimer->deleteLater(); + m_pluginTimer = nullptr; + } +} + +void DevicePluginTado::postSetupDevice(Device *device) +{ + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginTado::onPluginTimer); + } + + if (device->deviceClassId() == tadoConnectionDeviceClassId) { + Tado *tado = m_tadoAccounts.value(device->id()); + device->setStateValue(tadoConnectionUserDisplayNameStateTypeId, tado->username()); + device->setStateValue(tadoConnectionLoggedInStateTypeId, true); + device->setStateValue(tadoConnectionConnectedStateTypeId, true); + tado->getHomes(); + + } else if (device->deviceClassId() == zoneDeviceClassId) { + if (m_tadoAccounts.contains(device->parentId())) { + Tado *tado = m_tadoAccounts.value(device->parentId()); + tado->getZoneState(device->paramValue(zoneDeviceHomeIdParamTypeId).toString(), device->paramValue(zoneDeviceZoneIdParamTypeId).toString()); + } + } +} + +void DevicePluginTado::executeAction(DeviceActionInfo *info) +{ + Device *device = info->device(); + Action action = info->action(); + + if (device->deviceClassId() == zoneDeviceClassId) { + Tado *tado = m_tadoAccounts.value(device->parentId()); + if (!tado) + return; + QString homeId = device->paramValue(zoneDeviceHomeIdParamTypeId).toString(); + QString zoneId = device->paramValue(zoneDeviceZoneIdParamTypeId).toString(); + if (action.actionTypeId() == zoneModeActionTypeId) { + + if (action.param(zoneModeActionModeParamTypeId).value().toString() == "Tado") { + tado->deleteOverlay(homeId, zoneId); + } else if (action.param(zoneModeActionModeParamTypeId).value().toString() == "Off") { + tado->setOverlay(homeId, zoneId, false, device->stateValue(zoneTargetTemperatureStateTypeId).toDouble()); + } else { + if(device->stateValue(zoneTargetTemperatureStateTypeId).toDouble() <= 5.0) { + tado->setOverlay(homeId, zoneId, true, 5); + } else { + tado->setOverlay(homeId, zoneId, true, device->stateValue(zoneTargetTemperatureStateTypeId).toDouble()); + } + } + info->finish(Device::DeviceErrorNoError); + } else if (action.actionTypeId() == zoneTargetTemperatureActionTypeId) { + + double temperature = action.param(zoneTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble(); + if (temperature <= 0) { + QUuid requestId = tado->setOverlay(homeId, zoneId, false, 0); + m_asyncActions.insert(requestId, info); + } else { + tado->setOverlay(homeId, zoneId, true, temperature); + } + info->finish(Device::DeviceErrorNoError); + } + } +} + +void DevicePluginTado::onPluginTimer() +{ + foreach (Device *device, myDevices().filterByDeviceClassId(zoneDeviceClassId)) { + Tado *tado = m_tadoAccounts.value(device->parentId()); + if (!tado) + continue; + + QString homeId = device->paramValue(zoneDeviceHomeIdParamTypeId).toString(); + QString zoneId = device->paramValue(zoneDeviceZoneIdParamTypeId).toString(); + tado->getZoneState(homeId, zoneId); + } +} + +void DevicePluginTado::onConnectionChanged(bool connected) +{ + Tado *tado = static_cast(sender()); + + if (m_tadoAccounts.values().contains(tado)){ + Device *device = myDevices().findById(m_tadoAccounts.key(tado)); + device->setStateValue(tadoConnectionConnectedStateTypeId, connected); + + foreach(Device *zoneDevice, myDevices().filterByParentDeviceId(device->id())) { + zoneDevice->setStateValue(zoneConnectedStateTypeId, connected); + } + } +} + +void DevicePluginTado::onAuthenticationStatusChanged(bool authenticated) +{ + Tado *tado = static_cast(sender()); + + if (m_unfinishedTadoAccounts.values().contains(tado) && !authenticated){ + DeviceId id = m_unfinishedTadoAccounts.key(tado); + m_unfinishedTadoAccounts.remove(id); + DevicePairingInfo *info = m_unfinishedDevicePairings.take(id); + info->finish(Device::DeviceErrorSetupFailed); + } + + if (m_tadoAccounts.values().contains(tado)){ + Device *device = myDevices().findById(m_tadoAccounts.key(tado)); + device->setStateValue(tadoConnectionLoggedInStateTypeId, authenticated); + } +} + +void DevicePluginTado::onTokenReceived(Tado::Token token) +{ + Q_UNUSED(token); + + qCDebug(dcTado()) << "Token received"; + Tado *tado = static_cast(sender()); + + if (m_asyncDeviceSetup.contains(tado)) { + DeviceSetupInfo *info = m_asyncDeviceSetup.take(tado); + info->finish(Device::DeviceErrorNoError); + } + + if (m_unfinishedTadoAccounts.values().contains(tado)) { + DeviceId id = m_unfinishedTadoAccounts.key(tado); + DevicePairingInfo *info = m_unfinishedDevicePairings.take(id); + info->finish(Device::DeviceErrorNoError); + } +} + +void DevicePluginTado::onHomesReceived(QList homes) +{ + qCDebug(dcTado()) << "Homes received"; + Tado *tado = static_cast(sender()); + foreach (Tado::Home home, homes) { + tado->getZones(home.id); + } +} + +void DevicePluginTado::onZonesReceived(const QString &homeId, QList zones) +{ + Tado *tado = static_cast(sender()); + + if (m_tadoAccounts.values().contains(tado)) { + + Device *parentDevice = myDevices().findById(m_tadoAccounts.key(tado)); + qCDebug(dcTado()) << "Zones received:" << zones.count() << parentDevice->name(); + + DeviceDescriptors descriptors; + foreach (Tado::Zone zone, zones) { + + DeviceDescriptor descriptor(zoneDeviceClassId, zone.name, "Type:" + zone.type, parentDevice->id()); + ParamList params; + params.append(Param(zoneDeviceHomeIdParamTypeId, homeId)); + params.append(Param(zoneDeviceZoneIdParamTypeId, zone.id)); + if (myDevices().findByParams(params)) + continue; + + params.append(Param(zoneDeviceTypeParamTypeId, zone.type)); + descriptor.setParams(params); + descriptors.append(descriptor); + } + emit autoDevicesAppeared(descriptors); + } else { + qCWarning(dcTado()) << "Tado connection not linked to a device Id" << m_tadoAccounts.size() << m_tadoAccounts.key(tado).toString(); + } +} + +void DevicePluginTado::onZoneStateReceived(const QString &homeId, const QString &zoneId, Tado::ZoneState state) +{ + Tado *tado = static_cast(sender()); + DeviceId parentId = m_tadoAccounts.key(tado); + ParamList params; + params.append(Param(zoneDeviceHomeIdParamTypeId, homeId)); + params.append(Param(zoneDeviceZoneIdParamTypeId, zoneId)); + Device *device = myDevices().filterByParentDeviceId(parentId).findByParams(params); + if (!device) + return; + + if (state.overlayIsSet) { + if (state.overlaySettingPower) { + device->setStateValue(zoneModeStateTypeId, "Manual"); + } else { + device->setStateValue(zoneModeStateTypeId, "Off"); + } + } else { + device->setStateValue(zoneModeStateTypeId, "Tado"); + } + + device->setStateValue(zonePowerStateTypeId, (state.heatingPowerPercentage > 0)); + device->setStateValue(zoneConnectedStateTypeId, state.connected); + device->setStateValue(zoneTargetTemperatureStateTypeId, state.settingTemperature); + device->setStateValue(zoneTemperatureStateTypeId, state.temperature); + device->setStateValue(zoneHumidityStateTypeId, state.humidity); + device->setStateValue(zoneWindowOpenStateTypeId, state.windowOpen); + device->setStateValue(zoneTadoModeStateTypeId, state.tadoMode); +} + +void DevicePluginTado::onOverlayReceived(const QString &homeId, const QString &zoneId, const Tado::Overlay &overlay) +{ + Tado *tado = static_cast(sender()); + DeviceId parentId = m_tadoAccounts.key(tado); + ParamList params; + params.append(Param(zoneDeviceHomeIdParamTypeId, homeId)); + params.append(Param(zoneDeviceZoneIdParamTypeId, zoneId)); + Device *device = myDevices().filterByParentDeviceId(parentId).findByParams(params); + if (!device) + return; + device->setStateValue(zoneTargetTemperatureStateTypeId, overlay.temperature); + + if (overlay.tadoMode == "MANUAL") { + if (overlay.power) { + device->setStateValue(zoneModeStateTypeId, "Manual"); + } else { + device->setStateValue(zoneModeStateTypeId, "Off"); + } + } else { + device->setStateValue(zoneModeStateTypeId, "Tado"); + } +} diff --git a/tado/deviceplugintado.h b/tado/deviceplugintado.h new file mode 100644 index 00000000..26972be4 --- /dev/null +++ b/tado/deviceplugintado.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* This project may also contain libraries licensed under the open source software license GNU GPL v.3. +* 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 DEVICEPLUGINTADO_H +#define DEVICEPLUGINTADO_H + +#include "plugintimer.h" +#include "devices/deviceplugin.h" +#include "network/oauth2.h" +#include "tado.h" + +#include +#include + +class DevicePluginTado : public DevicePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "deviceplugintado.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginTado(); + void startPairing(DevicePairingInfo *info) override; + void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override; + void setupDevice(DeviceSetupInfo *info) override; + void deviceRemoved(Device *device) override; + void postSetupDevice(Device *device) override; + void executeAction(DeviceActionInfo *info) override; + +private: + PluginTimer *m_pluginTimer = nullptr; + QHash m_unfinishedTadoAccounts; + QHash m_unfinishedDevicePairings; + + QHash m_tadoAccounts; + QHash m_asyncDeviceSetup; + QHash m_asyncActions; + +private slots: + void onPluginTimer(); + + void onConnectionChanged(bool connected); + void onAuthenticationStatusChanged(bool authenticated); + void onTokenReceived(Tado::Token token); + void onHomesReceived(QList homes); + void onZonesReceived(const QString &homeId, QList zones); + void onZoneStateReceived(const QString &homeId,const QString &zoneId, Tado::ZoneState sate); + void onOverlayReceived(const QString &homeId, const QString &zoneId, const Tado::Overlay &overlay); +}; + +#endif // DEVICEPLUGINTADO_H diff --git a/tado/deviceplugintado.json b/tado/deviceplugintado.json new file mode 100644 index 00000000..0814bd2c --- /dev/null +++ b/tado/deviceplugintado.json @@ -0,0 +1,162 @@ +{ + "displayName": "Tado", + "name": "Tado", + "id": "b4f2d2ee-50bb-4786-b7f5-261fed204fa5", + "vendors": [ + { + "displayName": "Tado", + "name": "tado", + "id": "23c8a19f-bd6a-4c90-bcc9-2f0c0d9292c5", + "deviceClasses": [ + { + "id": "69be7d15-5658-4442-872e-42abbd8bff81", + "name": "tadoConnection", + "displayName": "Tado Connection", + "interfaces": ["account"], + "createMethods": ["user"], + "setupMethod": "userandpassword", + "stateTypes": [ + { + "id": "2f79bc1d-27ed-480a-b583-728363c83ea6", + "name": "connected", + "displayName": "Available", + "displayNameEvent": "Available changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "2aed240b-8c5c-418b-a9d1-0d75412c1c27", + "name": "loggedIn", + "displayName": "Logged in", + "displayNameEvent": "Logged in changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "33f55afc-a673-47a4-9fb0-75fdac6a66f4", + "name": "userDisplayName", + "displayName": "Username", + "displayNameEvent": "Username changed", + "type": "QString", + "defaultValue": "-" + } + ] + }, + { + "id": "1a7bb944-fb9c-490a-8a4c-794b27282292", + "name": "zone", + "displayName": "Zone", + "interfaces": ["thermostat", "temperaturesensor", "connectable"], + "createMethods": ["auto"], + "paramTypes": [ + { + "id": "330cad74-6f07-42ad-b226-299927c3c4f0", + "name": "homeId", + "displayName": "Home id", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + }, + { + "id": "cd67476b-978d-4a22-a40e-50cbc941e09e", + "name": "zoneId", + "displayName": "Zone id", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + }, + { + "id": "8e86797e-5333-4428-9dba-9ed5ac243b44", + "name": "type", + "displayName": "Type", + "type": "bool", + "defaultValue": false + } + ], + "stateTypes": [ + { + "id": "9f45a703-6a15-447c-a77a-0df731cda48e", + "name": "connected", + "displayName": "Available", + "displayNameEvent": "Available changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b", + "name": "mode", + "displayName": "Mode", + "displayNameEvent": "Mode changed", + "displayNameAction": "Set mode", + "type": "QString", + "defaultValue": "Tado", + "possibleValues": [ + "Manual", + "Tado", + "Off" + ], + "writable": true + }, + { + "id": "8b800998-5c2d-4940-9d0e-036979cf49ca", + "name": "tadoMode", + "displayName": "Tado mode", + "displayNameEvent": "Tado mode changed", + "type": "QString", + "defaultValue": "Tado" + }, + { + "id": "e886377d-34b7-4908-ad0d-ed463fc6181d", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "c7a04e26-bb22-406e-b117-262bdb8b9c0e", + "name": "windowOpen", + "displayName": "Window open", + "displayNameEvent": "Window open changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "80098178-7d92-43dd-a216-23704cc0eaa2", + "name": "temperature", + "displayName": "Temperature", + "displayNameEvent": "Temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0 + }, + { + "id": "684fcc62-f12b-4669-988e-4b79f153b0f2", + "name": "targetTemperature", + "displayName": "Target temperature", + "displayNameEvent": "Target temperature changed", + "displayNameAction": "Set target temperature", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0, + "minValue": 5, + "maxValue": 25, + "writable": true + }, + { + "id": "0faaaff1-2a33-44ec-b68d-d8855f584b02", + "name": "humidity", + "displayName": "Humidity", + "displayNameEvent": "Humidity changed", + "unit": "Percentage", + "type": "double", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100 + } + ] + } + ] + } + ] +} diff --git a/tado/tado.cpp b/tado/tado.cpp new file mode 100644 index 00000000..0c88ddb1 --- /dev/null +++ b/tado/tado.cpp @@ -0,0 +1,447 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* This project may also contain libraries licensed under the open source software license GNU GPL v.3. +* 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 "tado.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include + +Tado::Tado(NetworkAccessManager *networkManager, const QString &username, QObject *parent) : + QObject(parent), + m_networkManager(networkManager), + m_username(username) +{ + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &Tado::onRefreshTimer); +} + +void Tado::setUsername(const QString &username) +{ + m_username = username; +} + +QString Tado::username() +{ + return m_username; +} + +void Tado::getToken(const QString &password) +{ + QNetworkRequest request; + request.setUrl(QUrl(m_baseAuthorizationUrl)); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded"); + QByteArray body; + body.append("client_id=" + m_clientId); + body.append("&client_secret=" + m_clientSecret); + body.append("&grant_type=password"); + body.append("&scope=home.user"); + body.append("&username=" + m_username); + body.append("&password=" + password); + + QNetworkReply *reply = m_networkManager->post(request, body); + //qCDebug(dcTado()) << "Sending request" << request.url() << body; + connect(reply, &QNetworkReply::finished, this, [reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Received invalide JSON object"; + return; + } + if (data.isObject()) { + Token token; + QVariantMap obj = data.toVariant().toMap(); + if (obj.contains("access_token")) { + token.accesToken = obj["access_token"].toString(); + m_accessToken = token.accesToken; + } else { + qCWarning(dcTado()) << "Received response doesnt contain an access token"; + } + + token.tokenType = obj["token_type"].toString(); + token.refreshToken = obj["refresh_token"].toString(); + m_refreshToken = token.refreshToken; + if (obj.contains("expires_in")) { + token.expires = obj["expires_in"].toInt(); + m_refreshTimer->start((token.expires - 10)*1000); + } else { + qCWarning(dcTado()) << "Received response doesn't contain an expire time"; + } + token.scope = obj["scope"].toString(); + token.jti = obj["jti"].toString(); + emit authenticationStatusChanged(true); + emit tokenReceived(token); + } else { + qCWarning(dcTado()) << "Received response isn't an object" << data.toJson(); + emit authenticationStatusChanged(false); + } + }); +} + +void Tado::getHomes() +{ + QNetworkRequest request; + request.setUrl(QUrl(m_baseControlUrl + "/me")); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); + QNetworkReply *reply = m_networkManager->get(request); + //qDebug(dcTado()) << "Sending request" << request.url() << request.rawHeaderList(); + connect(reply, &QNetworkReply::finished, this, [reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + emit authenticationStatusChanged(true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; + return; + } + QList homes; + QVariantList homeList = data.toVariant().toMap().value("homes").toList(); + foreach (QVariant variant, homeList) { + QVariantMap obj = variant.toMap(); + Home home; + home.id = obj["id"].toString(); + home.name = obj["name"].toString(); + homes.append(home); + } + emit homesReceived(homes); + }); +} + +void Tado::getZones(const QString &homeId) +{ + QNetworkRequest request; + request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones")); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); + QNetworkReply *reply = m_networkManager->get(request); + //qDebug(dcTado()) << "Sending request" << request.url() << request.rawHeaderList(); + connect(reply, &QNetworkReply::finished, this, [reply, homeId, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + emit authenticationStatusChanged(true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; + return; + } + QList zones; + QVariantList list = data.toVariant().toList(); + foreach (QVariant variant, list) { + QVariantMap obj = variant.toMap(); + Zone zone; + zone.id = obj["id"].toString(); + zone.name = obj["name"].toString(); + zone.type = obj["type"].toString(); + zones.append(zone); + } + emit zonesReceived(homeId, zones); + }); +} + +void Tado::getZoneState(const QString &homeId, const QString &zoneId) +{ + QNetworkRequest request; + request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones/"+zoneId+"/state")); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); + QNetworkReply *reply = m_networkManager->get(request); + //qDebug(dcTado()) << "Sending request" << request.url() << request.rawHeaderList(); + connect(reply, &QNetworkReply::finished, this, [reply, homeId, zoneId, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + + emit connectionChanged(true); + emit authenticationStatusChanged(true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; + return; + } + ZoneState state; + QVariantMap map = data.toVariant().toMap(); + state.tadoMode = map["tadoMode"].toString(); + state.windowOpen = map["openWindow"].toBool(); + + QVariantMap settingsMap = map["setting"].toMap(); + state.settingType = settingsMap["type"].toString(); + state.settingPower = (settingsMap["power"].toString() == "ON"); + state.settingTemperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); + state.connected = (map["link"].toMap().value("state").toString() == "ONLINE"); + + QVariantMap activityDataMap = map["activityDataPoints"].toMap(); + state.heatingPowerPercentage = activityDataMap["heatingPower"].toMap().value("percentage").toDouble(); + state.heatingPowerType = activityDataMap["heatingPower"].toMap().value("type").toString(); + + QVariantMap dataMap = map["sensorDataPoints"].toMap(); + state.temperature = dataMap["insideTemperature"].toMap().value("celsius").toDouble(); + state.humidity = dataMap["humidity"].toMap().value("percentage").toDouble(); + + if (!map["overlay"].toMap().isEmpty()){ + state.overlayIsSet = true; + QVariantMap overlayMap = map["overlay"].toMap(); + state.overlayType = map["overlayType"].toString(); + state.overlaySettingPower = (overlayMap["setting"].toMap().value("power").toString() == "ON"); + state.overlaySettingTemperature = overlayMap["setting"].toMap().value("temperature").toDouble(); + } else { + state.overlayIsSet = false; + } + emit zoneStateReceived(homeId, zoneId, state); + }); +} + +QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature) +{ + QUuid requestId = QUuid::createUuid(); + QNetworkRequest request; + request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones/"+zoneId+"/overlay")); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json;charset=utf-8"); + request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); + + QByteArray body; + QByteArray powerString; + if (power) + powerString = "ON"; + else + powerString = "OFF"; + + body.append("{\"setting\":{\"type\":\"HEATING\",\"power\":\""+ powerString + "\",\"temperature\":{\"celsius\":" + QVariant(targetTemperature).toByteArray() + "}},\"termination\":{\"type\":\"MANUAL\"}}"); + //qCDebug(dcTado()) << "Sending request" << body; + QNetworkReply *reply = m_networkManager->put(request, body); + connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + emit requestExecuted(requestId, false); + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 401) { //Unauthorized + emit authenticationStatusChanged(false); + } else if (status == 422) { //Unprocessable Entity + qCWarning(dcTado()) << "Unprocessable Entity, probably a value out of range"; + } else { + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + } + return; + } + emit authenticationStatusChanged(true); + emit connectionChanged(true); + emit requestExecuted(requestId, true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; + return; + } + QVariantMap map = data.toVariant().toMap(); + + Overlay overlay; + QVariantMap settingsMap = map["setting"].toMap(); + overlay.zoneType = settingsMap["type"].toString(); + overlay.power = (settingsMap["power"].toString() == "ON"); + overlay.temperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); + QVariantMap terminationMap = map["termination"].toMap(); + overlay.terminationType = terminationMap["type"].toString(); + + overlay.tadoMode = map["type"].toString(); + emit overlayReceived(homeId, zoneId, overlay); + }); + return requestId; +} + +QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId) +{ + QUuid requestId = QUuid::createUuid(); + QNetworkRequest request; + request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones/"+zoneId+"/overlay")); + request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); + QNetworkReply *reply = m_networkManager->deleteResource(request); + connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status < 200 || status > 210 || reply->error() != QNetworkReply::NoError) { + emit requestExecuted(requestId ,false); + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 401) { //Unauthorized + emit authenticationStatusChanged(false); + } else if (status == 422) { //Unprocessable Entity + qCWarning(dcTado()) << "Unprocessable Entity, probably a value out of range"; + } else { + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + emit authenticationStatusChanged(true); + emit connectionChanged(true); + emit requestExecuted(requestId, true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; + return; + } + QVariantMap map = data.toVariant().toMap(); + + Overlay overlay; + QVariantMap settingsMap = map["setting"].toMap(); + overlay.zoneType = settingsMap["type"].toString(); + overlay.power = (settingsMap["power"].toString() == "ON"); + overlay.temperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); + QVariantMap terminationMap = map["termination"].toMap(); + overlay.terminationType = terminationMap["type"].toString(); + + overlay.tadoMode = map["type"].toString(); + emit overlayReceived(homeId, zoneId, overlay); + }); + return requestId; +} + +void Tado::onRefreshTimer() +{ + QNetworkRequest request; + request.setUrl(QUrl(m_baseAuthorizationUrl)); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded"); + QByteArray body; + body.append("client_id=" + m_clientId); + body.append("&client_secret=" + m_clientSecret); + body.append("&grant_type=refresh_token"); + body.append("&refresh_token=" + m_refreshToken); + body.append("&scope=home.user"); + + QNetworkReply *reply = m_networkManager->post(request, body); + connect(reply, &QNetworkReply::finished, this, [reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 400 || status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + emit authenticationStatusChanged(true); + + QJsonParseError error; + QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; + return; + } + Token token; + QVariantMap obj = data.toVariant().toMap(); + token.accesToken = obj["access_token"].toString(); + m_accessToken = token.accesToken; + token.tokenType = obj["token_type"].toString(); + token.refreshToken = obj["refresh_token"].toString(); + m_refreshToken = token.refreshToken; + token.expires = obj["expires_in"].toInt(); + m_refreshTimer->start((token.expires - 10)*1000); + token.scope = obj["scope"].toString(); + token.jti = obj["jti"].toString(); + emit tokenReceived(token); + }); +} diff --git a/tado/tado.h b/tado/tado.h new file mode 100644 index 00000000..22320428 --- /dev/null +++ b/tado/tado.h @@ -0,0 +1,131 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* This project may also contain libraries licensed under the open source software license GNU GPL v.3. +* 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 TADO_H +#define TADO_H + +#include "network/networkaccessmanager.h" +#include "devices/device.h" + +#include +#include +#include + +class Tado : public QObject +{ + Q_OBJECT +public: + struct Token { + QString accesToken; + QString tokenType; + QString refreshToken; + int expires; + QString scope; + QString jti; + }; + + struct Zone { + QString id; + QString name; + QString type; + }; + + struct Overlay { + bool power; + double temperature; + QString zoneType; + QString terminationType; + QString tadoMode; + }; + + + struct ZoneState { + bool connected; + bool power; + QString tadoMode; + QString settingType; + double settingTemperature; + bool settingPower; + double temperature; + double humidity; + bool windowOpen; + double heatingPowerPercentage; + QString heatingPowerType; + bool overlayIsSet; + bool overlaySettingPower; + double overlaySettingTemperature; + QString overlayType; + }; + + struct Home { + QString id; + QString name; + }; + + explicit Tado(NetworkAccessManager *networkManager, const QString &username, QObject *parent = nullptr); + + void setUsername(const QString &username); + QString username(); + + void getToken(const QString &password); + void getHomes(); + void getZones(const QString &homeId); + void getZoneState(const QString &homeId, const QString &zoneId); + + QUuid setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature); + QUuid deleteOverlay(const QString &homeId, const QString &zoneId); + +private: + QByteArray m_baseAuthorizationUrl = "https://auth.tado.com/oauth/token"; + QByteArray m_baseControlUrl = "https://my.tado.com/api/v2"; + QByteArray m_clientSecret = "4HJGRffVR8xb3XdEUQpjgZ1VplJi6Xgw"; + QByteArray m_clientId = "public-api-preview"; + + NetworkAccessManager *m_networkManager = nullptr; + QString m_username; + QString m_accessToken; + QString m_refreshToken; + QTimer *m_refreshTimer = nullptr; + +signals: + void connectionChanged(bool connected); + void authenticationStatusChanged(bool authenticated); + void requestExecuted(QUuid requestId, bool success); + + void tokenReceived(Token token); + void homesReceived(QList homes); + void zonesReceived(const QString &homeId, QList zones); + void zoneStateReceived(const QString &homeId,const QString &zoneId, ZoneState sate); + void overlayReceived(const QString &homeId, const QString &zoneId, const Overlay &overlay); + +private slots: + void onRefreshTimer(); + +}; + +#endif // TADO_H diff --git a/tado/tado.pro b/tado/tado.pro new file mode 100644 index 00000000..6f088926 --- /dev/null +++ b/tado/tado.pro @@ -0,0 +1,14 @@ +include(../plugins.pri) + +QT += network + +TARGET = $$qtLibraryTarget(nymea_deviceplugintado) + +SOURCES += \ + deviceplugintado.cpp \ + tado.cpp + +HEADERS += \ + deviceplugintado.h \ + tado.h + diff --git a/tado/translations/b4f2d2ee-50bb-4786-b7f5-261fed204fa5-en_US.ts b/tado/translations/b4f2d2ee-50bb-4786-b7f5-261fed204fa5-en_US.ts new file mode 100644 index 00000000..990760b5 --- /dev/null +++ b/tado/translations/b4f2d2ee-50bb-4786-b7f5-261fed204fa5-en_US.ts @@ -0,0 +1,231 @@ + + + + + DevicePluginTado + + + Please enter the login credentials. + + + + + Tado + + + + + + Available + The name of the ParamType (DeviceClass: zone, EventType: connected, ID: {9f45a703-6a15-447c-a77a-0df731cda48e}) +---------- +The name of the StateType ({9f45a703-6a15-447c-a77a-0df731cda48e}) of DeviceClass zone +---------- +The name of the ParamType (DeviceClass: tadoConnection, EventType: connected, ID: {2f79bc1d-27ed-480a-b583-728363c83ea6}) +---------- +The name of the StateType ({2f79bc1d-27ed-480a-b583-728363c83ea6}) of DeviceClass tadoConnection + + + + + + Available changed + The name of the EventType ({9f45a703-6a15-447c-a77a-0df731cda48e}) of DeviceClass zone +---------- +The name of the EventType ({2f79bc1d-27ed-480a-b583-728363c83ea6}) of DeviceClass tadoConnection + + + + + Home id + The name of the ParamType (DeviceClass: zone, Type: device, ID: {330cad74-6f07-42ad-b226-299927c3c4f0}) + + + + + + Humidity + The name of the ParamType (DeviceClass: zone, EventType: humidity, ID: {0faaaff1-2a33-44ec-b68d-d8855f584b02}) +---------- +The name of the StateType ({0faaaff1-2a33-44ec-b68d-d8855f584b02}) of DeviceClass zone + + + + + Humidity changed + The name of the EventType ({0faaaff1-2a33-44ec-b68d-d8855f584b02}) of DeviceClass zone + + + + + + Logged in + The name of the ParamType (DeviceClass: tadoConnection, EventType: loggedIn, ID: {2aed240b-8c5c-418b-a9d1-0d75412c1c27}) +---------- +The name of the StateType ({2aed240b-8c5c-418b-a9d1-0d75412c1c27}) of DeviceClass tadoConnection + + + + + Logged in changed + The name of the EventType ({2aed240b-8c5c-418b-a9d1-0d75412c1c27}) of DeviceClass tadoConnection + + + + + + + Mode + The name of the ParamType (DeviceClass: zone, ActionType: mode, ID: {4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b}) +---------- +The name of the ParamType (DeviceClass: zone, EventType: mode, ID: {4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b}) +---------- +The name of the StateType ({4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b}) of DeviceClass zone + + + + + Mode changed + The name of the EventType ({4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b}) of DeviceClass zone + + + + + + Power + The name of the ParamType (DeviceClass: zone, EventType: power, ID: {e886377d-34b7-4908-ad0d-ed463fc6181d}) +---------- +The name of the StateType ({e886377d-34b7-4908-ad0d-ed463fc6181d}) of DeviceClass zone + + + + + Power changed + The name of the EventType ({e886377d-34b7-4908-ad0d-ed463fc6181d}) of DeviceClass zone + + + + + Set mode + The name of the ActionType ({4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b}) of DeviceClass zone + + + + + Set target temperature + The name of the ActionType ({684fcc62-f12b-4669-988e-4b79f153b0f2}) of DeviceClass zone + + + + + + Tado + The name of the vendor ({23c8a19f-bd6a-4c90-bcc9-2f0c0d9292c5}) +---------- +The name of the plugin Tado ({b4f2d2ee-50bb-4786-b7f5-261fed204fa5}) + + + + + Tado Connection + The name of the DeviceClass ({69be7d15-5658-4442-872e-42abbd8bff81}) + + + + + + Tado mode + The name of the ParamType (DeviceClass: zone, EventType: tadoMode, ID: {8b800998-5c2d-4940-9d0e-036979cf49ca}) +---------- +The name of the StateType ({8b800998-5c2d-4940-9d0e-036979cf49ca}) of DeviceClass zone + + + + + Tado mode changed + The name of the EventType ({8b800998-5c2d-4940-9d0e-036979cf49ca}) of DeviceClass zone + + + + + + + Target temperature + The name of the ParamType (DeviceClass: zone, ActionType: targetTemperature, ID: {684fcc62-f12b-4669-988e-4b79f153b0f2}) +---------- +The name of the ParamType (DeviceClass: zone, EventType: targetTemperature, ID: {684fcc62-f12b-4669-988e-4b79f153b0f2}) +---------- +The name of the StateType ({684fcc62-f12b-4669-988e-4b79f153b0f2}) of DeviceClass zone + + + + + Target temperature changed + The name of the EventType ({684fcc62-f12b-4669-988e-4b79f153b0f2}) of DeviceClass zone + + + + + + Temperature + The name of the ParamType (DeviceClass: zone, EventType: temperature, ID: {80098178-7d92-43dd-a216-23704cc0eaa2}) +---------- +The name of the StateType ({80098178-7d92-43dd-a216-23704cc0eaa2}) of DeviceClass zone + + + + + Temperature changed + The name of the EventType ({80098178-7d92-43dd-a216-23704cc0eaa2}) of DeviceClass zone + + + + + Type + The name of the ParamType (DeviceClass: zone, Type: device, ID: {8e86797e-5333-4428-9dba-9ed5ac243b44}) + + + + + + Username + The name of the ParamType (DeviceClass: tadoConnection, EventType: userDisplayName, ID: {33f55afc-a673-47a4-9fb0-75fdac6a66f4}) +---------- +The name of the StateType ({33f55afc-a673-47a4-9fb0-75fdac6a66f4}) of DeviceClass tadoConnection + + + + + Username changed + The name of the EventType ({33f55afc-a673-47a4-9fb0-75fdac6a66f4}) of DeviceClass tadoConnection + + + + + + Window open + The name of the ParamType (DeviceClass: zone, EventType: windowOpen, ID: {c7a04e26-bb22-406e-b117-262bdb8b9c0e}) +---------- +The name of the StateType ({c7a04e26-bb22-406e-b117-262bdb8b9c0e}) of DeviceClass zone + + + + + Window open changed + The name of the EventType ({c7a04e26-bb22-406e-b117-262bdb8b9c0e}) of DeviceClass zone + + + + + Zone + The name of the DeviceClass ({1a7bb944-fb9c-490a-8a4c-794b27282292}) + + + + + Zone id + The name of the ParamType (DeviceClass: zone, Type: device, ID: {cd67476b-978d-4a22-a40e-50cbc941e09e}) + + + +