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})
+
+
+
+