/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; version 3. This project is distributed in the hope that * it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "tado.h" #include "extern-plugininfo.h" #include #include #include #include Tado::Tado(NetworkAccessManager *networkManager, const QString &username, QObject *parent) : QObject(parent), m_networkManager(networkManager), m_username(username) { m_refreshTimer = new QTimer(this); m_refreshTimer->setSingleShot(true); connect(m_refreshTimer, &QTimer::timeout, this, &Tado::onRefreshTimer); } void Tado::setUsername(const QString &username) { m_username = username; } QString Tado::username() { return m_username; } bool Tado::apiAvailable() { return m_apiAvailable; } bool Tado::authenticated() { return m_authenticationStatus; } bool Tado::connected() { return m_connectionStatus; } void Tado::getApiCredentials(const QString &url) { QNetworkRequest request; request.setUrl(url); QNetworkReply *reply = m_networkManager->get(request); qCDebug(dcTado()) << "Sending request" << request.url(); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); emit apiCredentialsReceived(false); return; } QRegExp filter; filter.setPatternSyntax(QRegExp::Wildcard); filter.setPattern("*tgaRestApiV2Endpoint:*"); QStringList list = QString(reply->readAll()).split('\n'); int index = list.indexOf(filter); if (index == -1) { qCWarning(dcTado()) << "GetApiCredenitals: Could not find the API url"; emit apiCredentialsReceived(false); return; } m_baseControlUrl = list.value(index).split(": ").last().remove(QRegExp("[,']"));; qCDebug(dcTado()) << "Received control url" << m_baseControlUrl; filter.setPattern("*apiEndpoint*"); index = list.indexOf(filter); if (index == -1) { qCWarning(dcTado()) << "GetApiCredenitals: Could not find the authorization url"; emit apiCredentialsReceived(false); return; } m_baseAuthorizationUrl = list.value(index).split(": ").last().remove(QRegExp("[,']"))+"/token"; qCDebug(dcTado()) << "Received auth url" << m_baseAuthorizationUrl; filter.setPattern("*clientId*"); index = list.indexOf(filter); if (index == -1) { emit apiCredentialsReceived(false); qCWarning(dcTado()) << "GetApiCredenitals: Could not find the client Id"; return; } m_clientId = list.value(index).split(": ").last().remove(QRegExp("[,']")); qCDebug(dcTado()) << "Received client id" << m_clientId.mid(0, 4)+"*****"; filter.setPattern("*clientSecret*"); index = list.indexOf(filter); if (index == -1) { qCWarning(dcTado()) << "GetApiCredenitals: Could not find the client secret"; emit apiCredentialsReceived(false); return; } m_clientSecret = list.value(index).split(": ").last().remove(QRegExp("[,']")); qCDebug(dcTado()) << "Received client secret" << m_clientSecret.mid(0, 4)+"*****"; m_apiAvailable = true; emit apiCredentialsReceived(true); }); } void Tado::getToken(const QString &password) { if (!m_apiAvailable) { qCWarning(dcTado()) << "Not sending request, get API credentials first"; return; } 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, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { emit connectionError(reply->error()); if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 401 || status == 400) { setAuthenticationStatus(false); } qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); return; } setConnectionStatus(true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Received invalid JSON object:" << data; return; } if (data.isObject()) { Token token; QVariantMap obj = data.toVariant().toMap(); if (obj.contains("access_token")) { token.accesToken = obj["access_token"].toString(); m_accessToken = token.accesToken; } else { qCWarning(dcTado()) << "Received response doesnt contain an access token"; } token.tokenType = obj["token_type"].toString(); token.refreshToken = obj["refresh_token"].toString(); m_refreshToken = token.refreshToken; if (obj.contains("expires_in")) { token.expires = obj["expires_in"].toInt(); m_refreshTimer->start((token.expires - 10)*1000); } else { qCWarning(dcTado()) << "Received response doesn't contain an expire time"; } token.scope = obj["scope"].toString(); token.jti = obj["jti"].toString(); setAuthenticationStatus(true); emit tokenReceived(token); } else { qCWarning(dcTado()) << "Received response isn't an object" << data.toJson(); setAuthenticationStatus(false); } }); } void Tado::getHomes() { if (!m_apiAvailable) { qCWarning(dcTado()) << "Not sending request, get API credentials first"; return; } if(m_accessToken.isEmpty()) { qCWarning(dcTado()) << "Not sending request, get the access token first"; return; } 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, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { emit connectionError(reply->error()); if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 401 || status == 400) { setAuthenticationStatus(false); } qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); return; } setConnectionStatus(true); setAuthenticationStatus(true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Recieved invalid 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) { if (!m_apiAvailable) { qCWarning(dcTado()) << "Not sending request, get API credentials first"; return; } if(m_accessToken.isEmpty()) { qCWarning(dcTado()) << "Not sending request, get the access token first"; return; } 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, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, homeId, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 401 || status == 400) { setAuthenticationStatus(false); } qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); return; } setConnectionStatus(true); setAuthenticationStatus(true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Recieved invalid 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) { if (!m_apiAvailable) { qCWarning(dcTado()) << "Not sending request, get API credentials first"; return; } if(m_accessToken.isEmpty()) { qCWarning(dcTado()) << "Not sending request, get the access token first"; return; } 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, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, homeId, zoneId, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { emit connectionError(reply->error()); if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 401 || status == 400) { setAuthenticationStatus(false); } qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); return; } setConnectionStatus(true); setAuthenticationStatus(true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Recieved invalid JSON object"; return; } ZoneState state; QVariantMap map = data.toVariant().toMap(); state.tadoMode = map["tadoMode"].toString(); state.windowOpen = map["openWindow"].toBool(); QVariantMap settingsMap = map["setting"].toMap(); state.settingType = settingsMap["type"].toString(); state.settingPower = (settingsMap["power"].toString() == "ON"); state.settingTemperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); state.connected = (map["link"].toMap().value("state").toString() == "ONLINE"); QVariantMap activityDataMap = map["activityDataPoints"].toMap(); state.heatingPowerPercentage = activityDataMap["heatingPower"].toMap().value("percentage").toDouble(); state.heatingPowerType = activityDataMap["heatingPower"].toMap().value("type").toString(); QVariantMap dataMap = map["sensorDataPoints"].toMap(); state.temperature = dataMap["insideTemperature"].toMap().value("celsius").toDouble(); state.humidity = dataMap["humidity"].toMap().value("percentage").toDouble(); if (!map["overlay"].toMap().isEmpty()){ state.overlayIsSet = true; QVariantMap overlayMap = map["overlay"].toMap(); state.overlayType = map["overlayType"].toString(); state.overlaySettingPower = (overlayMap["setting"].toMap().value("power").toString() == "ON"); state.overlaySettingTemperature = overlayMap["setting"].toMap().value("temperature").toDouble(); } else { state.overlayIsSet = false; } emit zoneStateReceived(homeId, zoneId, state); }); } QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power, double targetTemperature) { if (!m_apiAvailable) { qCWarning(dcTado()) << "Not sending request, get API credentials first"; return ""; } if(m_accessToken.isEmpty()) { qCWarning(dcTado()) << "Not sending request, get the access token first"; return ""; } QUuid requestId = QUuid::createUuid(); QNetworkRequest request; request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones/"+zoneId+"/overlay")); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json;charset=utf-8"); request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); QByteArray body; QByteArray powerString; if (power) powerString = "ON"; else powerString = "OFF"; body.append("{\"setting\":{\"type\":\"HEATING\",\"power\":\""+ powerString + "\",\"temperature\":{\"celsius\":" + QVariant(targetTemperature).toByteArray() + "}},\"termination\":{\"type\":\"MANUAL\"}}"); //qCDebug(dcTado()) << "Sending request" << body; QNetworkReply *reply = m_networkManager->put(request, body); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { emit requestExecuted(requestId, false); emit connectionError(reply->error()); if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 401 || status == 400) { //Unauthorized setAuthenticationStatus(false); } else if (status == 422) { //Unprocessable Entity qCWarning(dcTado()) << "Unprocessable Entity, probably a value out of range"; } else { qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); } return; } setAuthenticationStatus(true); setConnectionStatus(true); emit requestExecuted(requestId, true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Recieved invalid JSON object"; return; } QVariantMap map = data.toVariant().toMap(); Overlay overlay; QVariantMap settingsMap = map["setting"].toMap(); overlay.zoneType = settingsMap["type"].toString(); overlay.power = (settingsMap["power"].toString() == "ON"); overlay.temperature = settingsMap["temperature"].toMap().value("celsius").toDouble(); QVariantMap terminationMap = map["termination"].toMap(); overlay.terminationType = terminationMap["type"].toString(); overlay.tadoMode = map["type"].toString(); emit overlayReceived(homeId, zoneId, overlay); }); return requestId; } QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId) { if (!m_apiAvailable) { qCWarning(dcTado()) << "Not sending request, get API credentials first"; return ""; } if(m_accessToken.isEmpty()) { qCWarning(dcTado()) << "Not sending request, get the access token first"; return ""; } 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, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status < 200 || status > 210 || reply->error() != QNetworkReply::NoError) { emit requestExecuted(requestId ,false); emit connectionError(reply->error()); if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 401 || status == 400) { //Unauthorized setAuthenticationStatus(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; } setAuthenticationStatus(true); setConnectionStatus(true); emit requestExecuted(requestId, true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Recieved invalid 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::setAuthenticationStatus(bool status) { if (m_authenticationStatus != status) { m_authenticationStatus = status; emit authenticationStatusChanged(status); } if (!status) { m_refreshTimer->stop(); } } void Tado::setConnectionStatus(bool status) { if (m_connectionStatus != status) { m_connectionStatus = status; emit connectionChanged(status); } } void Tado::onRefreshTimer() { if(m_refreshToken.isEmpty()) { qCWarning(dcTado()) << "Not sending request, get the access token first"; return; } 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, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [reply, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Check HTTP status code if (status != 200 || reply->error() != QNetworkReply::NoError) { emit connectionError(reply->error()); if (reply->error() == QNetworkReply::HostNotFoundError) { setConnectionStatus(false); } if (status == 400 || status == 401) { setAuthenticationStatus(false); } qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); return; } setConnectionStatus(true); setAuthenticationStatus(true); QJsonParseError error; QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug(dcTado()) << "Get Token: Recieved invalid 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); }); }