From 7e7a6b071c853816870fa54ff6e685a590036512 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 18 Dec 2019 19:15:30 +0100 Subject: [PATCH 1/8] added tado --- nymea-plugins.pro | 1 + tado/deviceplugintado.cpp | 263 +++++++++++++++++++++++++++++++++++++ tado/deviceplugintado.h | 71 ++++++++++ tado/deviceplugintado.json | 137 +++++++++++++++++++ tado/tado.cpp | 245 ++++++++++++++++++++++++++++++++++ tado/tado.h | 99 ++++++++++++++ tado/tado.pro | 14 ++ 7 files changed, 830 insertions(+) create mode 100644 tado/deviceplugintado.cpp create mode 100644 tado/deviceplugintado.h create mode 100644 tado/deviceplugintado.json create mode 100644 tado/tado.cpp create mode 100644 tado/tado.h create mode 100644 tado/tado.pro diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 9e1177a0..6157a086 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/deviceplugintado.cpp b/tado/deviceplugintado.cpp new file mode 100644 index 00000000..1a9528e7 --- /dev/null +++ b/tado/deviceplugintado.cpp @@ -0,0 +1,263 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "deviceplugintado.h" +#include "devices/device.h" +#include "plugininfo.h" +#include "network/networkaccessmanager.h" + +#include +#include +#include + +DevicePluginTado::DevicePluginTado() +{ + +} + +DevicePluginTado::~DevicePluginTado() +{ + +} + +void DevicePluginTado::init() +{ + +} + +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); + 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); + 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(); + + } else if (device->deviceClassId() == zoneDeviceClassId) { + + } + + if (myDevices().isEmpty() && m_pluginTimer) { + m_pluginTimer->deleteLater(); + m_pluginTimer = nullptr; + } +} + +void DevicePluginTado::postSetupDevice(Device *device) +{ + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(600); + 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(); + + if (device->deviceClassId() == zoneDeviceClassId) { + + + info->finish(Device::DeviceErrorNoError); + } +} + +void DevicePluginTado::onPluginTimer() +{ +} + +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); + + //TODO set connected state in child devices + } +} + +void DevicePluginTado::onAuthenticationStatusChanged(bool authenticated) +{ + Tado *tado = static_cast(sender()); + + if (m_unfinishedTadoAccounts.values().contains(tado) && !authenticated){ + DeviceId id = m_tadoAccounts.key(tado); + 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); + } + + if (m_tadoAccounts.values().contains(tado)) { + + } +} + +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)); + 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) +{ + qCDebug(dcTado()) << "Zone state received:"; + 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); + + device->setStateValue(zoneModeStateTypeId, state.tadoMode); + device->setStateValue(zonePowerStateTypeId, state.power); + device->setStateValue(zoneConnectedStateTypeId, state.connected); + device->setStateValue(zoneTargetTemperatureStateTypeId, state.targetTemperature); + device->setStateValue(zoneTemperatureStateTypeId, state.temperature); + device->setStateValue(zoneHumidityStateTypeId, state.humidity); +} diff --git a/tado/deviceplugintado.h b/tado/deviceplugintado.h new file mode 100644 index 00000000..61950459 --- /dev/null +++ b/tado/deviceplugintado.h @@ -0,0 +1,71 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2020 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#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(); + ~DevicePluginTado(); + + void init() override; + 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; + +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); +}; + +#endif // DEVICEPLUGINTADO_H diff --git a/tado/deviceplugintado.json b/tado/deviceplugintado.json new file mode 100644 index 00000000..d342c17f --- /dev/null +++ b/tado/deviceplugintado.json @@ -0,0 +1,137 @@ +{ + "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 + } + ], + "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": "Auto", + "possibleValues": [ + "Manual", + "Auto", + "Off" + ], + "writable": true + }, + { + "id": "e886377d-34b7-4908-ad0d-ed463fc6181d", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power 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, + "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..b983e93f --- /dev/null +++ b/tado/tado.cpp @@ -0,0 +1,245 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#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) +{ + +} + +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 == 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(); + token.expires = obj["expires_in"].toInt(); + token.scope = obj["scope"].toString(); + token.jti = obj["jti"].toString(); + emit tokenReceived(token); + }); +} + +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 == 400 || status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + + 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 == 400 || status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + + 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 == 400 || status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); + return; + } + + 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["tado_mode"].toString(); + + QVariantMap settingsMap = map["setting"].toMap(); + state.type = settingsMap["type"].toString(); + state.power = (settingsMap["power"].toString() == "ON"); + state.targetTemperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); + + state.connected = (map["link"].toMap().value("state").toString() == "ONLINE"); + + QVariantMap dataMap = map["sensorDataPoints"].toMap(); + state.temperature = dataMap["insideTemperature"].toMap().value("celsius").toDouble(); + state.humidity = dataMap["humidity"].toMap().value("percentage").toDouble(); + + emit zoneStateReceived(homeId, zoneId, state); + }); +} + +void Tado::setOverlay(const QString &homeId, const QString &zoneId, const QString &mode, double targetTemperature) +{ + Q_UNUSED(zoneId); + Q_UNUSED(homeId); + Q_UNUSED(mode); + Q_UNUSED(targetTemperature); +} diff --git a/tado/tado.h b/tado/tado.h new file mode 100644 index 00000000..77ff6d3a --- /dev/null +++ b/tado/tado.h @@ -0,0 +1,99 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TADO_H +#define TADO_H + +#include "network/networkaccessmanager.h" +#include "devices/device.h" + +#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 ZoneState { + bool connected; + bool power; + double targetTemperature; + QString tadoMode; + QString type; + double temperature; + double humidity; + }; + + 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); + + void setOverlay(const QString &homeId, const QString &zoneId, const QString &mode, double targetTemperature); + +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; + +signals: + void connectionChanged(bool connected); + void authenticationStatusChanged(bool authenticated); + + 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); + +}; + +#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 + From f55057ac6a89d69da27b325d08b5f089391a07cc Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 18 Dec 2019 19:18:57 +0100 Subject: [PATCH 2/8] added tado to debian --- debian/control | 16 ++++++++++++++++ debian/nymea-plugin-tado.install.in | 1 + 2 files changed, 17 insertions(+) create mode 100644 debian/nymea-plugin-tado.install.in diff --git a/debian/control b/debian/control index 8cedb1a3..6deed7fe 100644 --- a/debian/control +++ b/debian/control @@ -815,6 +815,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 @@ -857,6 +872,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 From 8d87047bf2bd498d986a64edd86a9e565614039e Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 18 Dec 2019 19:21:54 +0100 Subject: [PATCH 3/8] added Tado readme --- tado/README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tado/README.md diff --git a/tado/README.md b/tado/README.md new file mode 100644 index 00000000..8144c47d --- /dev/null +++ b/tado/README.md @@ -0,0 +1,4 @@ +# 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. From 0a8e8ef2804522846fbc75ccb18357b8ac677007 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 18 Dec 2019 22:41:32 +0100 Subject: [PATCH 4/8] added token refresh and overlay deletion --- tado/deviceplugintado.cpp | 61 ++++++++++++--- tado/deviceplugintado.h | 1 - tado/deviceplugintado.json | 35 ++++++--- tado/tado.cpp | 155 +++++++++++++++++++++++++++++++++++-- tado/tado.h | 18 ++++- 5 files changed, 238 insertions(+), 32 deletions(-) diff --git a/tado/deviceplugintado.cpp b/tado/deviceplugintado.cpp index 1a9528e7..8e6528c5 100644 --- a/tado/deviceplugintado.cpp +++ b/tado/deviceplugintado.cpp @@ -39,11 +39,6 @@ DevicePluginTado::~DevicePluginTado() } -void DevicePluginTado::init() -{ - -} - void DevicePluginTado::startPairing(DevicePairingInfo *info) { info->finish(Device::DeviceErrorNoError, QT_TR_NOOP("Please enter the login credentials.")); @@ -125,7 +120,7 @@ void DevicePluginTado::deviceRemoved(Device *device) void DevicePluginTado::postSetupDevice(Device *device) { if (!m_pluginTimer) { - m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(600); + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginTado::onPluginTimer); } @@ -148,16 +143,42 @@ void DevicePluginTado::postSetupDevice(Device *device) 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() == "Home") { + tado->deleteOverlay(homeId, zoneId); + } else { - info->finish(Device::DeviceErrorNoError); + } + info->finish(Device::DeviceErrorNoError); + } else if (action.actionTypeId() == zoneTargetTemperatureActionTypeId) { + + double temperature = action.param(zoneTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble(); + tado->setOverlay(homeId, zoneId, "MANUAL", 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) @@ -168,7 +189,9 @@ void DevicePluginTado::onConnectionChanged(bool connected) Device *device = myDevices().findById(m_tadoAccounts.key(tado)); device->setStateValue(tadoConnectionConnectedStateTypeId, connected); - //TODO set connected state in child devices + foreach(Device *zoneDevice, myDevices().filterByParentDeviceId(device->id())) { + zoneDevice->setStateValue(zoneConnectedStateTypeId, connected); + } } } @@ -231,10 +254,15 @@ void DevicePluginTado::onZonesReceived(const QString &homeId, QList 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); } @@ -246,18 +274,29 @@ void DevicePluginTado::onZonesReceived(const QString &homeId, QList void DevicePluginTado::onZoneStateReceived(const QString &homeId, const QString &zoneId, Tado::ZoneState state) { - qCDebug(dcTado()) << "Zone state received:"; 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, "Home"); + } - device->setStateValue(zoneModeStateTypeId, state.tadoMode); device->setStateValue(zonePowerStateTypeId, state.power); device->setStateValue(zoneConnectedStateTypeId, state.connected); - device->setStateValue(zoneTargetTemperatureStateTypeId, state.targetTemperature); + device->setStateValue(zoneTargetTemperatureStateTypeId, state.settingTemperature); device->setStateValue(zoneTemperatureStateTypeId, state.temperature); device->setStateValue(zoneHumidityStateTypeId, state.humidity); + device->setStateValue(zoneWindowOpenStateTypeId, state.windowOpen); } diff --git a/tado/deviceplugintado.h b/tado/deviceplugintado.h index 61950459..46bdfa83 100644 --- a/tado/deviceplugintado.h +++ b/tado/deviceplugintado.h @@ -41,7 +41,6 @@ public: explicit DevicePluginTado(); ~DevicePluginTado(); - void init() override; void startPairing(DevicePairingInfo *info) override; void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override; void setupDevice(DeviceSetupInfo *info) override; diff --git a/tado/deviceplugintado.json b/tado/deviceplugintado.json index d342c17f..865987bf 100644 --- a/tado/deviceplugintado.json +++ b/tado/deviceplugintado.json @@ -19,8 +19,8 @@ { "id": "2f79bc1d-27ed-480a-b583-728363c83ea6", "name": "connected", - "displayName": "available", - "displayNameEvent": "available changed", + "displayName": "Available", + "displayNameEvent": "Available changed", "type": "bool", "defaultValue": false }, @@ -64,14 +64,21 @@ "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", + "displayName": "Available", + "displayNameEvent": "Available changed", "type": "bool", "defaultValue": false }, @@ -82,10 +89,10 @@ "displayNameEvent": "Mode changed", "displayNameAction": "Set mode", "type": "QString", - "defaultValue": "Auto", + "defaultValue": "Home", "possibleValues": [ "Manual", - "Auto", + "Home", "Off" ], "writable": true @@ -98,11 +105,19 @@ "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", + "displayName": "Temperature", + "displayNameEvent": "Temperature changed", "unit": "DegreeCelsius", "type": "double", "defaultValue": 0 @@ -121,8 +136,8 @@ { "id": "0faaaff1-2a33-44ec-b68d-d8855f584b02", "name": "humidity", - "displayName": "humidity", - "displayNameEvent": "humidity changed", + "displayName": "Humidity", + "displayNameEvent": "Humidity changed", "unit": "Percentage", "type": "double", "defaultValue": 0, diff --git a/tado/tado.cpp b/tado/tado.cpp index b983e93f..554fc08d 100644 --- a/tado/tado.cpp +++ b/tado/tado.cpp @@ -33,7 +33,9 @@ Tado::Tado(NetworkAccessManager *networkManager, const QString &username, QObjec 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) @@ -91,7 +93,9 @@ void Tado::getToken(const QString &password) 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); @@ -216,30 +220,165 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; return; } - ZoneState state; QVariantMap map = data.toVariant().toMap(); - state.tadoMode = map["tado_mode"].toString(); + state.tadoMode = map["tadoMode"].toString(); + state.windowOpen = map["openWindow"].toBool(); QVariantMap settingsMap = map["setting"].toMap(); - state.type = settingsMap["type"].toString(); - state.power = (settingsMap["power"].toString() == "ON"); - state.targetTemperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); + 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["settings"].toMap().value("power").toString() == "ON"); + state.overlaySettingTemperature = overlayMap["settings"].toMap().value("temperature").toDouble(); + } else { + state.overlayIsSet = false; + } emit zoneStateReceived(homeId, zoneId, state); }); } void Tado::setOverlay(const QString &homeId, const QString &zoneId, const QString &mode, double targetTemperature) { - Q_UNUSED(zoneId); - Q_UNUSED(homeId); Q_UNUSED(mode); Q_UNUSED(targetTemperature); + + 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()); + QJsonDocument doc; + QJsonObject obj; + QJsonObject setting; + setting.insert("type", "HEATING"); + setting.insert("power", "ON"); + QJsonObject temperature; + temperature.insert("celsius", targetTemperature); + temperature.insert("fahrenheit", (targetTemperature * (9.0/5.0)) + 32.0); + setting.insert("temperature", temperature); + obj.insert("setting", setting); + QJsonObject termination; + termination.insert("type", "MANUAL"); + obj.insert("termination", termination); + doc.setObject(obj); + + QNetworkReply *reply = m_networkManager->put(request, doc.toJson()); + qCDebug(dcTado()) << "Sending request" << request.url() << doc.toJson(); + 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; + } + + 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(); + qCDebug(dcTado()) << map["type"].toString(); + }); +} + +void Tado::deleteOverlay(const QString &homeId, const QString &zoneId) +{ + 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, [reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status < 200 || status > 210 || 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; + } + }); +} + +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 index 77ff6d3a..1c8b4467 100644 --- a/tado/tado.h +++ b/tado/tado.h @@ -27,6 +27,7 @@ #include "devices/device.h" #include +#include class Tado : public QObject { @@ -50,11 +51,19 @@ public: struct ZoneState { bool connected; bool power; - double targetTemperature; QString tadoMode; - QString type; + 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 { @@ -73,6 +82,7 @@ public: void getZoneState(const QString &homeId, const QString &zoneId); void setOverlay(const QString &homeId, const QString &zoneId, const QString &mode, double targetTemperature); + void deleteOverlay(const QString &homeId, const QString &zoneId); private: QByteArray m_baseAuthorizationUrl = "https://auth.tado.com/oauth/token"; @@ -83,6 +93,8 @@ private: NetworkAccessManager *m_networkManager = nullptr; QString m_username; QString m_accessToken; + QString m_refreshToken; + QTimer *m_refreshTimer = nullptr; signals: void connectionChanged(bool connected); @@ -93,6 +105,8 @@ signals: void homesReceived(QList homes); void zonesReceived(const QString &homeId, QList zones); void zoneStateReceived(const QString &homeId,const QString &zoneId, ZoneState sate); +private slots: + void onRefreshTimer(); }; From dd14d4fcfcfd0d1ac04b8ea9abf0ce4861af7f55 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 18 Dec 2019 23:41:29 +0100 Subject: [PATCH 5/8] able to set mode and temperature --- tado/deviceplugintado.cpp | 14 ++++++++++---- tado/deviceplugintado.json | 4 ++-- tado/tado.cpp | 24 ++++++++++++++---------- tado/tado.h | 2 +- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/tado/deviceplugintado.cpp b/tado/deviceplugintado.cpp index 8e6528c5..73bffd6b 100644 --- a/tado/deviceplugintado.cpp +++ b/tado/deviceplugintado.cpp @@ -153,16 +153,22 @@ void DevicePluginTado::executeAction(DeviceActionInfo *info) QString zoneId = device->paramValue(zoneDeviceZoneIdParamTypeId).toString(); if (action.actionTypeId() == zoneModeActionTypeId) { - if (action.param(zoneModeActionModeParamTypeId).value().toString() == "Home") { + 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 { - + 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(); - tado->setOverlay(homeId, zoneId, "MANUAL", temperature); + if (temperature <= 0) { + tado->setOverlay(homeId, zoneId, false, 0); + } else { + tado->setOverlay(homeId, zoneId, true, temperature); + } info->finish(Device::DeviceErrorNoError); } } @@ -290,7 +296,7 @@ void DevicePluginTado::onZoneStateReceived(const QString &homeId, const QString device->setStateValue(zoneModeStateTypeId, "Off"); } } else { - device->setStateValue(zoneModeStateTypeId, "Home"); + device->setStateValue(zoneModeStateTypeId, "Tado"); } device->setStateValue(zonePowerStateTypeId, state.power); diff --git a/tado/deviceplugintado.json b/tado/deviceplugintado.json index 865987bf..6b55e19b 100644 --- a/tado/deviceplugintado.json +++ b/tado/deviceplugintado.json @@ -89,10 +89,10 @@ "displayNameEvent": "Mode changed", "displayNameAction": "Set mode", "type": "QString", - "defaultValue": "Home", + "defaultValue": "Tado", "possibleValues": [ "Manual", - "Home", + "Tado", "Off" ], "writable": true diff --git a/tado/tado.cpp b/tado/tado.cpp index 554fc08d..4390a5f0 100644 --- a/tado/tado.cpp +++ b/tado/tado.cpp @@ -253,32 +253,36 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) }); } -void Tado::setOverlay(const QString &homeId, const QString &zoneId, const QString &mode, double targetTemperature) +void Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature) { - Q_UNUSED(mode); - Q_UNUSED(targetTemperature); - 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()); - QJsonDocument doc; + /*QJsonDocument doc; QJsonObject obj; QJsonObject setting; - setting.insert("type", "HEATING"); setting.insert("power", "ON"); QJsonObject temperature; temperature.insert("celsius", targetTemperature); - temperature.insert("fahrenheit", (targetTemperature * (9.0/5.0)) + 32.0); + //temperature.insert("fahrenheit", (targetTemperature * (9.0/5.0)) + 32.0); + setting.insert("type", "HEATING"); setting.insert("temperature", temperature); obj.insert("setting", setting); QJsonObject termination; termination.insert("type", "MANUAL"); obj.insert("termination", termination); - doc.setObject(obj); + doc.setObject(obj);*/ - QNetworkReply *reply = m_networkManager->put(request, doc.toJson()); - qCDebug(dcTado()) << "Sending request" << request.url() << doc.toJson(); + 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\"}}"); + QNetworkReply *reply = m_networkManager->put(request, body); connect(reply, &QNetworkReply::finished, this, [reply, this] { reply->deleteLater(); diff --git a/tado/tado.h b/tado/tado.h index 1c8b4467..50f21c67 100644 --- a/tado/tado.h +++ b/tado/tado.h @@ -81,7 +81,7 @@ public: void getZones(const QString &homeId); void getZoneState(const QString &homeId, const QString &zoneId); - void setOverlay(const QString &homeId, const QString &zoneId, const QString &mode, double targetTemperature); + void setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature); void deleteOverlay(const QString &homeId, const QString &zoneId); private: From d47214ac14f6538770286b93cbb5f0e0aef04004 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Thu, 19 Dec 2019 00:02:05 +0100 Subject: [PATCH 6/8] added tado mode --- tado/deviceplugintado.cpp | 11 +++++++++-- tado/deviceplugintado.json | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tado/deviceplugintado.cpp b/tado/deviceplugintado.cpp index 73bffd6b..684c661c 100644 --- a/tado/deviceplugintado.cpp +++ b/tado/deviceplugintado.cpp @@ -158,7 +158,11 @@ void DevicePluginTado::executeAction(DeviceActionInfo *info) } else if (action.param(zoneModeActionModeParamTypeId).value().toString() == "Off") { tado->setOverlay(homeId, zoneId, false, device->stateValue(zoneTargetTemperatureStateTypeId).toDouble()); } else { - tado->setOverlay(homeId, zoneId, true, device->stateValue(zoneTargetTemperatureStateTypeId).toDouble()); + 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) { @@ -299,10 +303,13 @@ void DevicePluginTado::onZoneStateReceived(const QString &homeId, const QString device->setStateValue(zoneModeStateTypeId, "Tado"); } - device->setStateValue(zonePowerStateTypeId, state.power); + 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); } diff --git a/tado/deviceplugintado.json b/tado/deviceplugintado.json index 6b55e19b..53c2de30 100644 --- a/tado/deviceplugintado.json +++ b/tado/deviceplugintado.json @@ -97,6 +97,14 @@ ], "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", From 5c28eaf8716bee21fb104179c2a275aa21a4b717 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Fri, 3 Jan 2020 20:16:51 +0100 Subject: [PATCH 7/8] fixed state update --- tado/README.md | 10 ++ tado/deviceplugintado.cpp | 94 +++++++++++-------- tado/deviceplugintado.h | 52 ++++++----- tado/tado.cpp | 192 +++++++++++++++++++++++++------------- tado/tado.h | 66 ++++++++----- 5 files changed, 265 insertions(+), 149 deletions(-) diff --git a/tado/README.md b/tado/README.md index 8144c47d..dd065d92 100644 --- a/tado/README.md +++ b/tado/README.md @@ -2,3 +2,13 @@ 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 index 684c661c..f9d6088d 100644 --- a/tado/deviceplugintado.cpp +++ b/tado/deviceplugintado.cpp @@ -1,24 +1,30 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2019 Bernhard Trinnes * - * * - * This file is part of nymea. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Lesser General Public * - * License as published by the Free Software Foundation; either * - * version 2.1 of the License, or (at your option) any later version. * - * * - * This library 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 library; If not, see * - * . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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" @@ -34,11 +40,6 @@ DevicePluginTado::DevicePluginTado() } -DevicePluginTado::~DevicePluginTado() -{ - -} - void DevicePluginTado::startPairing(DevicePairingInfo *info) { info->finish(Device::DeviceErrorNoError, QT_TR_NOOP("Please enter the login credentials.")); @@ -53,6 +54,7 @@ void DevicePluginTado::confirmPairing(DevicePairingInfo *info, const QString &us 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); @@ -88,6 +90,7 @@ void DevicePluginTado::setupDevice(DeviceSetupInfo *info) 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); @@ -106,9 +109,6 @@ void DevicePluginTado::deviceRemoved(Device *device) if (device->deviceClassId() == tadoConnectionDeviceClassId) { Tado *tado = m_tadoAccounts.take(device->id()); tado->deleteLater(); - - } else if (device->deviceClassId() == zoneDeviceClassId) { - } if (myDevices().isEmpty() && m_pluginTimer) { @@ -129,7 +129,6 @@ void DevicePluginTado::postSetupDevice(Device *device) device->setStateValue(tadoConnectionUserDisplayNameStateTypeId, tado->username()); device->setStateValue(tadoConnectionLoggedInStateTypeId, true); device->setStateValue(tadoConnectionConnectedStateTypeId, true); - tado->getHomes(); } else if (device->deviceClassId() == zoneDeviceClassId) { @@ -169,7 +168,8 @@ void DevicePluginTado::executeAction(DeviceActionInfo *info) double temperature = action.param(zoneTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble(); if (temperature <= 0) { - tado->setOverlay(homeId, zoneId, false, 0); + QUuid requestId = tado->setOverlay(homeId, zoneId, false, 0); + m_asyncActions.insert(requestId, info); } else { tado->setOverlay(homeId, zoneId, true, temperature); } @@ -210,7 +210,8 @@ void DevicePluginTado::onAuthenticationStatusChanged(bool authenticated) Tado *tado = static_cast(sender()); if (m_unfinishedTadoAccounts.values().contains(tado) && !authenticated){ - DeviceId id = m_tadoAccounts.key(tado); + DeviceId id = m_unfinishedTadoAccounts.key(tado); + m_unfinishedTadoAccounts.remove(id); DevicePairingInfo *info = m_unfinishedDevicePairings.take(id); info->finish(Device::DeviceErrorSetupFailed); } @@ -238,10 +239,6 @@ void DevicePluginTado::onTokenReceived(Tado::Token token) DevicePairingInfo *info = m_unfinishedDevicePairings.take(id); info->finish(Device::DeviceErrorNoError); } - - if (m_tadoAccounts.values().contains(tado)) { - - } } void DevicePluginTado::onHomesReceived(QList homes) @@ -304,8 +301,6 @@ void DevicePluginTado::onZoneStateReceived(const QString &homeId, const QString } device->setStateValue(zonePowerStateTypeId, (state.heatingPowerPercentage > 0)); - - device->setStateValue(zoneConnectedStateTypeId, state.connected); device->setStateValue(zoneTargetTemperatureStateTypeId, state.settingTemperature); device->setStateValue(zoneTemperatureStateTypeId, state.temperature); @@ -313,3 +308,26 @@ void DevicePluginTado::onZoneStateReceived(const QString &homeId, const QString 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 index 46bdfa83..26972be4 100644 --- a/tado/deviceplugintado.h +++ b/tado/deviceplugintado.h @@ -1,24 +1,30 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2020 Bernhard Trinnes * - * * - * This file is part of nymea. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Lesser General Public * - * License as published by the Free Software Foundation; either * - * version 2.1 of the License, or (at your option) any later version. * - * * - * This library 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 library; If not, see * - * . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 @@ -39,8 +45,6 @@ class DevicePluginTado : public DevicePlugin public: explicit DevicePluginTado(); - ~DevicePluginTado(); - void startPairing(DevicePairingInfo *info) override; void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override; void setupDevice(DeviceSetupInfo *info) override; @@ -55,6 +59,7 @@ private: QHash m_tadoAccounts; QHash m_asyncDeviceSetup; + QHash m_asyncActions; private slots: void onPluginTimer(); @@ -65,6 +70,7 @@ private slots: 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/tado.cpp b/tado/tado.cpp index 4390a5f0..80e60608 100644 --- a/tado/tado.cpp +++ b/tado/tado.cpp @@ -1,24 +1,30 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2019 Bernhard Trinnes * - * * - * This file is part of nymea. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Lesser General Public * - * License as published by the Free Software Foundation; either * - * version 2.1 of the License, or (at your option) any later version. * - * * - * This library 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 library; If not, see * - * . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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" @@ -72,33 +78,47 @@ void Tado::getToken(const QString &password) if (reply->error() == QNetworkReply::HostNotFoundError) { emit connectionChanged(false); } - if (status == 400 || status == 401) { + 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"; + qDebug(dcTado()) << "Get Token: Received 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); + 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); + } }); } @@ -119,12 +139,14 @@ void Tado::getHomes() if (reply->error() == QNetworkReply::HostNotFoundError) { emit connectionChanged(false); } - if (status == 400 || status == 401) { + 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); @@ -163,12 +185,14 @@ void Tado::getZones(const QString &homeId) if (reply->error() == QNetworkReply::HostNotFoundError) { emit connectionChanged(false); } - if (status == 400 || status == 401) { + 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); @@ -207,19 +231,24 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) if (reply->error() == QNetworkReply::HostNotFoundError) { emit connectionChanged(false); } - if (status == 400 || status == 401) { + 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; } + + qDebug(dcTado()) << "Get zone stat" << data; ZoneState state; QVariantMap map = data.toVariant().toMap(); state.tadoMode = map["tadoMode"].toString(); @@ -244,8 +273,9 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) state.overlayIsSet = true; QVariantMap overlayMap = map["overlay"].toMap(); state.overlayType = map["overlayType"].toString(); - state.overlaySettingPower = (overlayMap["settings"].toMap().value("power").toString() == "ON"); - state.overlaySettingTemperature = overlayMap["settings"].toMap().value("temperature").toDouble(); + qCDebug(dcTado()) << "Overlay power" << overlayMap["setting"].toMap().value("power").toString() << overlayMap["setting"].toMap().value("temperature").toDouble(); + state.overlaySettingPower = (overlayMap["setting"].toMap().value("power").toString() == "ON"); + state.overlaySettingTemperature = overlayMap["setting"].toMap().value("temperature").toDouble(); } else { state.overlayIsSet = false; } @@ -253,26 +283,13 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) }); } -void Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature) +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()); - /*QJsonDocument doc; - QJsonObject obj; - QJsonObject setting; - setting.insert("power", "ON"); - QJsonObject temperature; - temperature.insert("celsius", targetTemperature); - //temperature.insert("fahrenheit", (targetTemperature * (9.0/5.0)) + 32.0); - setting.insert("type", "HEATING"); - setting.insert("temperature", temperature); - obj.insert("setting", setting); - QJsonObject termination; - termination.insert("type", "MANUAL"); - obj.insert("termination", termination); - doc.setObject(obj);*/ QByteArray body; QByteArray powerString; @@ -282,23 +299,30 @@ void Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, 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, [reply, this] { + 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 == 400 || status == 401) { + 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); @@ -307,32 +331,72 @@ void Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, return; } QVariantMap map = data.toVariant().toMap(); - qCDebug(dcTado()) << map["type"].toString(); + + 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::deleteOverlay(const QString &homeId, const QString &zoneId) +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, [reply, this] { + 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 == 400 || status == 401) { + 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() diff --git a/tado/tado.h b/tado/tado.h index 50f21c67..22320428 100644 --- a/tado/tado.h +++ b/tado/tado.h @@ -1,24 +1,30 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * Copyright (C) 2019 Bernhard Trinnes * - * * - * This file is part of nymea. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Lesser General Public * - * License as published by the Free Software Foundation; either * - * version 2.1 of the License, or (at your option) any later version. * - * * - * This library 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 library; If not, see * - * . * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 @@ -28,6 +34,7 @@ #include #include +#include class Tado : public QObject { @@ -48,6 +55,15 @@ public: QString type; }; + struct Overlay { + bool power; + double temperature; + QString zoneType; + QString terminationType; + QString tadoMode; + }; + + struct ZoneState { bool connected; bool power; @@ -81,8 +97,8 @@ public: void getZones(const QString &homeId); void getZoneState(const QString &homeId, const QString &zoneId); - void setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature); - void deleteOverlay(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"; @@ -99,12 +115,14 @@ private: 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(); From c7cdcaea0f6d90ef01235c2956fd60a51817cb30 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Sat, 4 Jan 2020 18:58:23 +0100 Subject: [PATCH 8/8] removed debug output --- tado/deviceplugintado.json | 2 + tado/tado.cpp | 15 +- ...2d2ee-50bb-4786-b7f5-261fed204fa5-en_US.ts | 231 ++++++++++++++++++ 3 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 tado/translations/b4f2d2ee-50bb-4786-b7f5-261fed204fa5-en_US.ts diff --git a/tado/deviceplugintado.json b/tado/deviceplugintado.json index 53c2de30..0814bd2c 100644 --- a/tado/deviceplugintado.json +++ b/tado/deviceplugintado.json @@ -139,6 +139,8 @@ "unit": "DegreeCelsius", "type": "double", "defaultValue": 0, + "minValue": 5, + "maxValue": 25, "writable": true }, { diff --git a/tado/tado.cpp b/tado/tado.cpp index 80e60608..0c88ddb1 100644 --- a/tado/tado.cpp +++ b/tado/tado.cpp @@ -68,7 +68,7 @@ void Tado::getToken(const QString &password) body.append("&password=" + password); QNetworkReply *reply = m_networkManager->post(request, body); - qCDebug(dcTado()) << "Sending request" << request.url() << body; + //qCDebug(dcTado()) << "Sending request" << request.url() << body; connect(reply, &QNetworkReply::finished, this, [reply, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -129,7 +129,7 @@ void Tado::getHomes() 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(); + //qDebug(dcTado()) << "Sending request" << request.url() << request.rawHeaderList(); connect(reply, &QNetworkReply::finished, this, [reply, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -161,7 +161,6 @@ void Tado::getHomes() Home home; home.id = obj["id"].toString(); home.name = obj["name"].toString(); - homes.append(home); } emit homesReceived(homes); @@ -175,7 +174,7 @@ void Tado::getZones(const QString &homeId) 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(); + //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(); @@ -221,7 +220,7 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) 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(); + //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(); @@ -247,8 +246,6 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) qDebug(dcTado()) << "Get Token: Recieved invalide JSON object"; return; } - - qDebug(dcTado()) << "Get zone stat" << data; ZoneState state; QVariantMap map = data.toVariant().toMap(); state.tadoMode = map["tadoMode"].toString(); @@ -258,7 +255,6 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) 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(); @@ -273,7 +269,6 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId) state.overlayIsSet = true; QVariantMap overlayMap = map["overlay"].toMap(); state.overlayType = map["overlayType"].toString(); - qCDebug(dcTado()) << "Overlay power" << overlayMap["setting"].toMap().value("power").toString() << overlayMap["setting"].toMap().value("temperature").toDouble(); state.overlaySettingPower = (overlayMap["setting"].toMap().value("power").toString() == "ON"); state.overlaySettingTemperature = overlayMap["setting"].toMap().value("temperature").toDouble(); } else { @@ -299,7 +294,7 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, powerString = "OFF"; body.append("{\"setting\":{\"type\":\"HEATING\",\"power\":\""+ powerString + "\",\"temperature\":{\"celsius\":" + QVariant(targetTemperature).toByteArray() + "}},\"termination\":{\"type\":\"MANUAL\"}}"); - qCDebug(dcTado()) << "Sending request" << body; + //qCDebug(dcTado()) << "Sending request" << body; QNetworkReply *reply = m_networkManager->put(request, body); connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { reply->deleteLater(); 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}) + + + +