Merge PR #766: Tado: Update account authentication to device code grant

master
jenkins 2025-03-30 20:36:46 +02:00
commit 19cb1c497d
5 changed files with 395 additions and 316 deletions

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Copyright 2013 - 2025, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -43,67 +43,74 @@ IntegrationPluginTado::IntegrationPluginTado()
}
void IntegrationPluginTado::startPairing(ThingPairingInfo *info)
void IntegrationPluginTado::init()
{
qCDebug(dcTado()) << "Start pairing process, checking the internet connection ...";
NetworkAccessManager *network = hardwareManager()->networkManager();
QNetworkReply *reply = network->get(QNetworkRequest(QUrl("https://my.tado.com/api/v2")));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info] {
if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError) {
qCWarning(dcTado()) << "Tado server is not reachable, likely because of a missing internet connection.";
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Tado server is not reachable."));
} else {
qCDebug(dcTado()) << "Internet connection available";
info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter the login credentials for your Tado account."));
}
});
}
void IntegrationPluginTado::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password)
void IntegrationPluginTado::startPairing(ThingPairingInfo *info)
{
qCDebug(dcTado()) << "Confirm pairing" << username << "Network manager available" << hardwareManager()->networkManager()->available();
Tado *tado = new Tado(hardwareManager()->networkManager(), username, this);
qCDebug(dcTado()) << "Start pairing process ...";
Tado *tado = new Tado(hardwareManager()->networkManager(), this);
m_unfinishedTadoAccounts.insert(info->thingId(), tado);
connect(info, &ThingPairingInfo::aborted, this, [info, tado, this]() {
qCWarning(dcTado()) << "Thing pairing has been aborted, going to clean-up";
qCWarning(dcTado()) << "Thing pairing has been aborted, cleaning up...";
m_unfinishedTadoAccounts.remove(info->thingId());
tado->deleteLater();
});
connect(tado, &Tado::getLoginUrlFinished, info, [info, tado, this] (bool success) {
if (!success) {
info->finish(Thing::ThingErrorAuthenticationFailure);
return;
}
connect(info, &ThingPairingInfo::aborted, tado, [info, this]() {
qCWarning(dcTado()) << "ThingPairingInfo aborted, cleaning up pending setup connection.";
m_unfinishedTadoAccounts.take(info->thingId())->deleteLater();
});
qCDebug(dcTado()) << "Tado server is reachable. Starting the OAuth pairing process using" << tado->loginUrl();
info->setOAuthUrl(QUrl(tado->loginUrl()));
info->finish(Thing::ThingErrorNoError);
});
tado->getLoginUrl();
}
void IntegrationPluginTado::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password)
{
Q_UNUSED(username)
qCDebug(dcTado()) << "Confirm pairing" << password;
Tado *tado = m_unfinishedTadoAccounts.value(info->thingId());
connect(tado, &Tado::connectionError, info, [info] (QNetworkReply::NetworkError error){
if (error == QNetworkReply::NetworkError::ProtocolInvalidOperationError) {
qCWarning(dcTado()) << "Confirm pairing failed, wrong username or password";
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Wrong username or password."));
} else if (error != QNetworkReply::NetworkError::NoError){
if (error != QNetworkReply::NetworkError::NoError){
qCWarning(dcTado()) << "Confirm pairing failed" << error;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Connection error"));
}
// info->finish(success) will be called after the token has been received
});
connect(tado, &Tado::apiCredentialsReceived, info, [info, password, tado] (bool success) {
if (success) {
tado->getToken(password);
} else {
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Client credentials not found, the plug-in version might be outdated."));
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("A connection error occurred."));
}
});
connect(tado, &Tado::tokenReceived, info, [this, info, username, password](Tado::Token token) {
Q_UNUSED(token)
connect(tado, &Tado::startAuthenticationFinished, info, [info, tado, this](bool success) {
if (!success) {
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to authenticate with tado server."));
return;
}
qCDebug(dcTado()) << "Authentication finished successfully.";
pluginStorage()->beginGroup(info->thingId().toString());
pluginStorage()->setValue("username", username);
pluginStorage()->setValue("password", password);
pluginStorage()->setValue("refreshToken", tado->refreshToken());
pluginStorage()->endGroup();
info->finish(Thing::ThingErrorNoError);
});
tado->getApiCredentials();
tado->startAuthentication();
}
void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
@ -112,8 +119,9 @@ void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
if (thing->thingClassId() == tadoAccountThingClassId) {
qCDebug(dcTado) << "Setup Tado account" << thing->name() << thing->params();
Tado *tado;
qCDebug(dcTado) << "Setting up Tado account" << thing->name() << thing->params();
Tado *tado = nullptr;
if (m_tadoAccounts.contains(thing->id())) {
qCDebug(dcTado()) << "Setup after reconfigure, cleaning up";
@ -125,51 +133,65 @@ void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
tado = m_unfinishedTadoAccounts.take(thing->id());
m_tadoAccounts.insert(thing->id(), tado);
info->finish(Thing::ThingErrorNoError);
} else {
pluginStorage()->beginGroup(thing->id().toString());
QString username = pluginStorage()->value("username").toString();
QString password = pluginStorage()->value("password").toString();
pluginStorage()->setValue("refreshToken", tado->refreshToken());
pluginStorage()->endGroup();
} else {
// Load refresh token
pluginStorage()->beginGroup(thing->id().toString());
QString refreshToken = pluginStorage()->value("refreshToken").toString();
qCDebug(dcTado()) << "Loaded refresh token" << refreshToken;
pluginStorage()->endGroup();
tado = new Tado(hardwareManager()->networkManager(), username, this);
if (refreshToken.isEmpty()) {
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Could not authenticate on the server. Please reconfigure the connection."));
return;
}
tado = new Tado(hardwareManager()->networkManager(), this);
m_tadoAccounts.insert(thing->id(), tado);
connect(info, &ThingSetupInfo::aborted, [info, this] {
tado->setRefreshToken(refreshToken);
}
// Delete any leftover username password from deprecated password grant flow,
// storing passwords in plaintext does not correspond to good manners
pluginStorage()->beginGroup(thing->id().toString());
pluginStorage()->remove("username");
pluginStorage()->remove("password");
pluginStorage()->endGroup();
connect(info, &ThingSetupInfo::aborted, this, [info, this] {
if (m_tadoAccounts.contains(info->thing()->id())) {
m_tadoAccounts.take(info->thing()->id())->deleteLater();
}
});
connect(tado, &Tado::refreshTokenReceived, this, [thing, this](const QString &refreshToken){
pluginStorage()->beginGroup(thing->id().toString());
pluginStorage()->setValue("refreshToken", refreshToken);
pluginStorage()->endGroup();
});
connect(tado, &Tado::accessTokenReceived, info, [info]() {
qCDebug(dcTado()) << "Token received, account setup successfull";
info->finish(Thing::ThingErrorNoError);
});
connect(tado, &Tado::connectionError, info, [this, info] (QNetworkReply::NetworkError error) {
if (error != QNetworkReply::NetworkError::NoError){
if (m_tadoAccounts.contains(info->thing()->id())) {
Tado *tado = m_tadoAccounts.take(info->thing()->id());
tado->deleteLater();
}
});
connect(tado, &Tado::apiCredentialsReceived, info, [password, tado] {
tado->getToken(password);
});
qCWarning(dcTado()) << "Connection error during setup:" << error;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Connection error"));
}
});
connect(tado, &Tado::tokenReceived, info, [ info](Tado::Token token) {
Q_UNUSED(token)
qCDebug(dcTado()) << "Token received, account setup successfull";
info->finish(Thing::ThingErrorNoError);
});
connect(tado, &Tado::connectionError, info, [this, info] (QNetworkReply::NetworkError error) {
if (error != QNetworkReply::NetworkError::NoError){
if (m_tadoAccounts.contains(info->thing()->id())) {
Tado *tado = m_tadoAccounts.take(info->thing()->id());
tado->deleteLater();
}
if (error == QNetworkReply::NetworkError::ProtocolInvalidOperationError) {
qCWarning(dcTado()) << "Confirm pairing failed, wrong username or password";
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Wrong username or password."));
} else {
qCWarning(dcTado()) << "Confirm pairing failed" << error;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Connection error"));
}
}
});
tado->getApiCredentials();
}
connect(tado, &Tado::usernameChanged, this, &IntegrationPluginTado::onUsernameChanged);
connect(tado, &Tado::authenticationStatusChanged, this, &IntegrationPluginTado::onAuthenticationStatusChanged);
connect(tado, &Tado::requestExecuted, this, &IntegrationPluginTado::onRequestExecuted);
connect(tado, &Tado::connectionChanged, this, &IntegrationPluginTado::onConnectionChanged);
@ -177,6 +199,9 @@ void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
connect(tado, &Tado::zonesReceived, this, &IntegrationPluginTado::onZonesReceived);
connect(tado, &Tado::zoneStateReceived, this, &IntegrationPluginTado::onZoneStateReceived);
connect(tado, &Tado::overlayReceived, this, &IntegrationPluginTado::onOverlayReceived);
tado->getAccessToken();
return;
} else if (thing->thingClassId() == zoneThingClassId) {
@ -205,6 +230,9 @@ void IntegrationPluginTado::thingRemoved(Thing *thing)
tado->deleteLater();
}
// Clean up storage
pluginStorage()->remove(thing->id().toString());
if (myThings().isEmpty() && m_pluginTimer) {
m_pluginTimer->deleteLater();
m_pluginTimer = nullptr;
@ -220,7 +248,7 @@ void IntegrationPluginTado::postSetupThing(Thing *thing)
if (thing->thingClassId() == tadoAccountThingClassId) {
Tado *tado = m_tadoAccounts.value(thing->id());
thing->setStateValue(tadoAccountUserDisplayNameStateTypeId, tado->username());
//thing->setStateValue(tadoAccountUserDisplayNameStateTypeId, tado->username());
thing->setStateValue(tadoAccountLoggedInStateTypeId, true);
thing->setStateValue(tadoAccountConnectedStateTypeId, true);
tado->getHomes();
@ -260,7 +288,7 @@ void IntegrationPluginTado::executeAction(ThingActionInfo *info)
}
}
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, [requestId, this] {m_asyncActions.remove(requestId);});
connect(info, &ThingActionInfo::aborted, thing, [requestId, this] {m_asyncActions.remove(requestId);});
} else if (action.actionTypeId() == zoneTargetTemperatureActionTypeId) {
double temperature = action.param(zoneTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble();
@ -271,7 +299,7 @@ void IntegrationPluginTado::executeAction(ThingActionInfo *info)
requestId = tado->setOverlay(homeId, zoneId, true, temperature);
}
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, [requestId, this] {m_asyncActions.remove(requestId);});
connect(info, &ThingActionInfo::aborted, thing, [requestId, this] {m_asyncActions.remove(requestId);});
} else if (action.actionTypeId() == zonePowerActionTypeId) {
bool power = action.param(zonePowerActionPowerParamTypeId).value().toBool();
thing->setStateValue(zonePowerStateTypeId, power); // the actual power set response might be slow
@ -283,7 +311,7 @@ void IntegrationPluginTado::executeAction(ThingActionInfo *info)
requestId = tado->setOverlay(homeId, zoneId, true, temperature);
}
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, [requestId, this] {m_asyncActions.remove(requestId);});
connect(info, &ThingActionInfo::aborted, thing, [requestId, this] {m_asyncActions.remove(requestId);});
} else {
qCWarning(dcTado()) << "Execute action, unhandled actionTypeId" << action.actionTypeId();
info->finish(Thing::ThingErrorActionTypeNotFound);
@ -299,12 +327,9 @@ void IntegrationPluginTado::onPluginTimer()
Q_FOREACH(Tado *tado, m_tadoAccounts){
ThingId accountThingId = m_tadoAccounts.key(tado);
if (!tado->authenticated()) {
pluginStorage()->beginGroup(accountThingId.toString());
QString password = pluginStorage()->value("password").toString();
pluginStorage()->endGroup();
tado->getToken(password);
tado->getAccessToken();
} else {
Q_FOREACH(Thing *thing, myThings().filterByParentId(accountThingId)) {
foreach (Thing *thing, myThings().filterByParentId(accountThingId)) {
if (thing->thingClassId() == zoneThingClassId) {
QString homeId = thing->paramValue(zoneThingHomeIdParamTypeId).toString();
QString zoneId = thing->paramValue(zoneThingZoneIdParamTypeId).toString();
@ -326,7 +351,7 @@ void IntegrationPluginTado::onConnectionChanged(bool connected)
thing->setStateValue(tadoAccountConnectedStateTypeId, connected);
if (!connected) {
Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) {
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
if (child->thingClassId() == zoneThingClassId) {
child->setStateValue(zoneConnectedStateTypeId, connected);
}
@ -347,7 +372,7 @@ void IntegrationPluginTado::onAuthenticationStatusChanged(bool authenticated)
}
thing->setStateValue(tadoAccountLoggedInStateTypeId, authenticated);
if (!authenticated) {
Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) {
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
if (child->thingClassId() == zoneThingClassId) {
child->setStateValue(zoneConnectedStateTypeId, authenticated);
}
@ -356,6 +381,16 @@ void IntegrationPluginTado::onAuthenticationStatusChanged(bool authenticated)
}
}
void IntegrationPluginTado::onUsernameChanged(const QString &username)
{
Tado *tado = static_cast<Tado*>(sender());
if (m_tadoAccounts.values().contains(tado)){
Thing *thing = myThings().findById(m_tadoAccounts.key(tado));
thing->setStateValue(tadoAccountUserDisplayNameStateTypeId, username);
}
}
void IntegrationPluginTado::onRequestExecuted(QUuid requestId, bool success)
{
if (m_asyncActions.contains(requestId)) {

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Copyright 2013 - 2025, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -39,7 +39,6 @@
#include <network/oauth2.h>
#include <QHash>
#include <QTimer>
class IntegrationPluginTado : public IntegrationPlugin
@ -50,8 +49,12 @@ class IntegrationPluginTado : public IntegrationPlugin
public:
explicit IntegrationPluginTado();
void init() override;
void startPairing(ThingPairingInfo *info) override;
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
void setupThing(ThingSetupInfo *info) override;
void thingRemoved(Thing *thing) override;
void postSetupThing(Thing *thing) override;
@ -59,9 +62,9 @@ public:
private:
PluginTimer *m_pluginTimer = nullptr;
QHash<ThingId, Tado*> m_unfinishedTadoAccounts;
QHash<ThingId, Tado *> m_unfinishedTadoAccounts;
QHash<ThingId, Tado*> m_tadoAccounts;
QHash<ThingId, Tado *> m_tadoAccounts;
QHash<QUuid, ThingActionInfo *> m_asyncActions;
private slots:
@ -69,6 +72,7 @@ private slots:
void onConnectionChanged(bool connected);
void onAuthenticationStatusChanged(bool authenticated);
void onUsernameChanged(const QString &username);
void onRequestExecuted(QUuid requestId, bool success);
void onHomesReceived(QList<Tado::Home> homes);
void onZonesReceived(const QString &homeId, QList<Tado::Zone> zones);

View File

@ -14,13 +14,12 @@
"displayName": "Tado account",
"interfaces": ["account"],
"createMethods": ["user"],
"setupMethod": "userandpassword",
"setupMethod": "oauth",
"stateTypes": [
{
"id": "2f79bc1d-27ed-480a-b583-728363c83ea6",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false
},
@ -28,7 +27,6 @@
"id": "2aed240b-8c5c-418b-a9d1-0d75412c1c27",
"name": "loggedIn",
"displayName": "Logged in",
"displayNameEvent": "Logged in changed",
"type": "bool",
"defaultValue": false
},
@ -36,7 +34,6 @@
"id": "33f55afc-a673-47a4-9fb0-75fdac6a66f4",
"name": "userDisplayName",
"displayName": "Username",
"displayNameEvent": "Username changed",
"type": "QString",
"defaultValue": "-"
}
@ -78,7 +75,6 @@
"id": "9f45a703-6a15-447c-a77a-0df731cda48e",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false
},
@ -86,7 +82,6 @@
"id": "4cecf87c-8a5d-4bc4-a4ba-d2ee6103714b",
"name": "mode",
"displayName": "Mode",
"displayNameEvent": "Mode changed",
"displayNameAction": "Set mode",
"type": "QString",
"defaultValue": "Tado",
@ -101,7 +96,6 @@
"id": "8b800998-5c2d-4940-9d0e-036979cf49ca",
"name": "tadoMode",
"displayName": "Tado mode",
"displayNameEvent": "Tado mode changed",
"type": "QString",
"defaultValue": "Tado"
},
@ -109,7 +103,6 @@
"id": "e886377d-34b7-4908-ad0d-ed463fc6181d",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Power changed",
"displayNameAction": "Set power",
"type": "bool",
"writable": true,
@ -126,7 +119,6 @@
"id": "80098178-7d92-43dd-a216-23704cc0eaa2",
"name": "temperature",
"displayName": "Temperature",
"displayNameEvent": "Temperature changed",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 0
@ -135,7 +127,6 @@
"id": "684fcc62-f12b-4669-988e-4b79f153b0f2",
"name": "targetTemperature",
"displayName": "Target temperature",
"displayNameEvent": "Target temperature changed",
"displayNameAction": "Set target temperature",
"unit": "DegreeCelsius",
"type": "double",
@ -148,7 +139,6 @@
"id": "0faaaff1-2a33-44ec-b68d-d8855f584b02",
"name": "humidity",
"displayName": "Humidity",
"displayNameEvent": "Humidity changed",
"unit": "Percentage",
"type": "double",
"defaultValue": 0,

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Copyright 2013 - 2025, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -36,24 +36,27 @@
#include <QJsonArray>
#include <QUrlQuery>
Tado::Tado(NetworkAccessManager *networkManager, const QString &username, QObject *parent) :
Tado::Tado(NetworkAccessManager *networkManager, QObject *parent) :
QObject(parent),
m_networkManager(networkManager),
m_username(username)
m_networkManager(networkManager)
{
m_refreshTimer = new QTimer(this);
m_refreshTimer->setSingleShot(true);
connect(m_refreshTimer, &QTimer::timeout, this, &Tado::onRefreshTimer);
}
m_baseControlUrl = "https://my.tado.com/api/v2";
m_baseAuthorizationUrl = "https://login.tado.com/oauth2";
void Tado::setUsername(const QString &username)
{
m_username = username;
}
m_clientId = "1bb50063-6b0c-4d11-bd99-387f4a91cc46";
QString Tado::username()
{
return m_username;
m_refreshTimer.setSingleShot(true);
connect(&m_refreshTimer, &QTimer::timeout, this, [this](){
qCDebug(dcTado()) << "Refresh token...";
getAccessToken();
});
m_pollAuthenticationTimer.setSingleShot(true);
m_pollAuthenticationTimer.setInterval(2000);
connect(&m_pollAuthenticationTimer, &QTimer::timeout, this, [this](){
qCDebug(dcTado()) << "Checking authentication status...";
requestAuthenticationToken();
});
}
bool Tado::apiAvailable()
@ -71,138 +74,160 @@ bool Tado::connected()
return m_connectionStatus;
}
void Tado::getApiCredentials(const QString &url)
QString Tado::loginUrl() const
{
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
qCDebug(dcTado()) << "Sending request" << request.url();
return m_loginUrl;
}
QString Tado::username() const
{
return m_username;
}
QString Tado::refreshToken() const
{
return m_refreshToken;
}
void Tado::setRefreshToken(const QString &refreshToken)
{
m_refreshToken = refreshToken;
}
void Tado::startAuthentication()
{
qCDebug(dcTado()) << "Start authentication process...";
m_pollAuthenticationCount = 0;
requestAuthenticationToken();
}
void Tado::getLoginUrl()
{
QNetworkRequest request = QNetworkRequest(QUrl(m_baseAuthorizationUrl + "/device_authorize"));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
QUrlQuery query;
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("scope", "offline_access");
QByteArray payload = query.toString(QUrl::FullyEncoded).toUtf8();
qCDebug(dcTado()) << "Get login url request" << request.url() << payload;
QNetworkReply *reply = m_networkManager->post(request, payload);
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:*");
emit getLoginUrlFinished(false);
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);
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;
}
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);
setConnectionStatus(true);
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument responseJsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcTado()) << "Get Token: Received invalid JSON object:" << data;
emit getLoginUrlFinished(false);
return;
}
qCDebug(dcTado()) << "Get login url response" << qUtf8Printable(responseJsonDoc.toJson());
QVariantMap responseMap = responseJsonDoc.toVariant().toMap();
m_deviceCode = responseMap.value("device_code").toString();
m_loginUrl = responseMap.value("verification_uri_complete").toString();
uint pollInterval = responseMap.value("interval").toUInt();
qCDebug(dcTado()) << "Login url:" << m_loginUrl;
qCDebug(dcTado()) << "Device code:" << m_deviceCode;
qCDebug(dcTado()) << "Poll interval:" << pollInterval;
m_pollAuthenticationTimer.setInterval(pollInterval * 1000);
emit getLoginUrlFinished(true);
});
}
void Tado::getToken(const QString &password)
void Tado::getAccessToken()
{
if (!m_apiAvailable) {
qCWarning(dcTado()) << "Not sending request, get API credentials first";
return;
}
QNetworkRequest request;
request.setUrl(QUrl(m_baseAuthorizationUrl));
QNetworkRequest request = QNetworkRequest(QUrl(m_baseAuthorizationUrl + "/token"));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
QUrlQuery query;
query.setQueryItems({{"client_id", m_clientId},
{"client_secret", m_clientSecret},
{"grant_type", "password"},
{"scope", "home.user"},
{"username", m_username},
{"password", password}});
QNetworkReply *reply = m_networkManager->post(request, query.toString(QUrl::FullyEncoded).toUtf8());
// qCDebug(dcTado()) << "Sending request" << request.url() << query.toString(QUrl::FullyEncoded).toUtf8();
QUrlQuery query;
query.addQueryItem("grant_type", "refresh_token");
query.addQueryItem("refresh_token", m_refreshToken);
query.addQueryItem("client_id", m_clientId);
QByteArray payload = query.toString(QUrl::FullyEncoded).toUtf8();
qCDebug(dcTado()) << "Get access token request" << request.url() << payload;
QNetworkReply *reply = m_networkManager->post(request, payload);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
QByteArray data = reply->readAll();
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) {
if (reply->error() == QNetworkReply::HostNotFoundError)
setConnectionStatus(false);
}
if (status == 401 || status == 400) {
if (status == 401 || status == 400)
setAuthenticationStatus(false);
}
qCWarning(dcTado()) << "Request error:" << status << reply->errorString();
qCWarning(dcTado()) << "Request error:" << status << reply->errorString() << qUtf8Printable(data);
return;
}
m_apiAvailable = true;
setConnectionStatus(true);
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
QJsonDocument responseJsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcTado()) << "Get Token: Received invalid JSON object:" << data;
qDebug(dcTado()) << "Get access token received invalid JSON object:" << data;
emit getLoginUrlFinished(false);
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);
qCDebug(dcTado()) << "Get access token response" << qUtf8Printable(responseJsonDoc.toJson());
QVariantMap responseMap = responseJsonDoc.toVariant().toMap();
m_accessToken = responseMap.value("access_token").toString();
emit accessTokenReceived();
QString refreshToken = responseMap.value("refresh_token").toString();
if (m_refreshToken != refreshToken) {
m_refreshToken = refreshToken;
emit refreshTokenReceived(m_refreshToken);
}
setAuthenticationStatus(true);
// Refresh 10 sekonds before expiration
m_refreshTimer.setInterval((responseMap.value("expires_in").toUInt() - 10) * 1000);
m_refreshTimer.start();
});
}
@ -246,11 +271,21 @@ void Tado::getHomes()
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcTado()) << "Get Token: Recieved invalid JSON object";
qDebug(dcTado()) << "Get Homes: Recieved invalid JSON object";
return;
}
qCDebug(dcTado()) << "Get homes response" << qUtf8Printable(data.toJson());
QVariantMap responseMap = data.toVariant().toMap();
QString username = responseMap.value("username").toString();
if (m_username != username) {
m_username = username;
emit usernameChanged(m_username);
}
QList<Home> homes;
QVariantList homeList = data.toVariant().toMap().value("homes").toList();
QVariantList homeList = responseMap.value("homes").toList();
foreach (QVariant variant, homeList) {
QVariantMap obj = variant.toMap();
Home home;
@ -258,6 +293,7 @@ void Tado::getHomes()
home.name = obj["name"].toString();
homes.append(home);
}
emit homesReceived(homes);
});
}
@ -275,7 +311,7 @@ void Tado::getZones(const QString &homeId)
}
QNetworkRequest request;
request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones"));
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);
@ -332,7 +368,7 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId)
}
QNetworkRequest request;
request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones/"+zoneId+"/state"));
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);
@ -413,7 +449,7 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
QUuid requestId = QUuid::createUuid();
QNetworkRequest request;
request.setUrl(QUrl(m_baseControlUrl+"/homes/"+homeId+"/zones/"+zoneId+"/overlay"));
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());
@ -424,14 +460,14 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
else
powerString = "OFF";
body.append("{\"setting\":{\"type\":\"HEATING\",\"power\":\""+ powerString + "\",\"temperature\":{\"celsius\":" + QVariant(targetTemperature).toByteArray() + "}},\"termination\":{\"type\":\"MANUAL\"}}");
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());
@ -439,6 +475,7 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
if (reply->error() == QNetworkReply::HostNotFoundError) {
setConnectionStatus(false);
}
if (status == 401 || status == 400) { //Unauthorized
setAuthenticationStatus(false);
} else if (status == 422) { //Unprocessable Entity
@ -448,6 +485,7 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
}
return;
}
setAuthenticationStatus(true);
setConnectionStatus(true);
emit requestExecuted(requestId, true);
@ -488,21 +526,22 @@ 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.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
@ -510,9 +549,10 @@ QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId)
} else {
qCWarning(dcTado()) << "Request error:" << status << reply->errorString();
}
qCWarning(dcTado()) << "Request error:" << status << reply->errorString();
return;
}
setAuthenticationStatus(true);
setConnectionStatus(true);
emit requestExecuted(requestId, true);
@ -523,6 +563,7 @@ QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId)
qDebug(dcTado()) << "Get Token: Recieved invalid JSON object";
return;
}
QVariantMap map = data.toVariant().toMap();
Overlay overlay;
@ -539,6 +580,66 @@ QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId)
return requestId;
}
void Tado::requestAuthenticationToken()
{
QNetworkRequest request = QNetworkRequest(QUrl(m_baseAuthorizationUrl + "/token"));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
QUrlQuery query;
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("device_code", m_deviceCode);
query.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
QByteArray payload = query.toString(QUrl::FullyEncoded).toUtf8();
qCDebug(dcTado()) << "Request authentication token" << request.url() << payload;
QNetworkReply *reply = m_networkManager->post(request, payload);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status != 200 || reply->error() != QNetworkReply::NoError) {
qCDebug(dcTado()) << "Request error:" << status << "Retrying:" << m_pollAuthenticationCount << "/" << m_pollAuthenticationLimit;
if (m_pollAuthenticationCount >= m_pollAuthenticationLimit) {
qCWarning(dcTado()) << "Authentication request failed" << m_pollAuthenticationCount << "times. Giving up.";
emit startAuthenticationFinished(false);
setAuthenticationStatus(false);
return;
}
// We poll until the user finished the login or until we reached the limit
m_pollAuthenticationTimer.start();
m_pollAuthenticationCount++;
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument responseJsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcTado()) << "Authentication received invalid JSON object:" << data;
emit startAuthenticationFinished(false);
setAuthenticationStatus(false);
return;
}
qCDebug(dcTado()) << "Authentication finished successfully:" << qUtf8Printable(responseJsonDoc.toJson());
QVariantMap responseMap = responseJsonDoc.toVariant().toMap();
m_accessToken = responseMap.value("access_token").toString();
QString refreshToken = responseMap.value("refresh_token").toString();
if (m_refreshToken != refreshToken) {
m_refreshToken = refreshToken;
emit refreshTokenReceived(m_refreshToken);
}
emit startAuthenticationFinished(true);
setAuthenticationStatus(true);
});
}
void Tado::setAuthenticationStatus(bool status)
{
if (m_authenticationStatus != status) {
@ -546,9 +647,9 @@ void Tado::setAuthenticationStatus(bool status)
emit authenticationStatusChanged(status);
}
if (!status) {
m_refreshTimer->stop();
}
if (!status)
m_refreshTimer.stop();
}
void Tado::setConnectionStatus(bool status)
@ -558,62 +659,3 @@ void Tado::setConnectionStatus(bool 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");
QUrlQuery query;
query.setQueryItems({{"client_id", m_clientId},
{"client_secret", m_clientSecret},
{"grant_type", "refresh_token"},
{"refresh_token", m_refreshToken},
{"scope", "home.user"}});
QNetworkReply *reply = m_networkManager->post(request, query.toString(QUrl::FullyEncoded).toUtf8());
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);
});
}

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Copyright 2013 - 2025, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -31,8 +31,8 @@
#ifndef TADO_H
#define TADO_H
#include "network/networkaccessmanager.h"
#include "integrations/thing.h"
#include <network/networkaccessmanager.h>
#include <integrations/thing.h>
#include <QObject>
#include <QTimer>
@ -42,16 +42,6 @@ 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;
@ -89,16 +79,24 @@ public:
QString name;
};
explicit Tado(NetworkAccessManager *networkManager, const QString &username, QObject *parent = nullptr);
explicit Tado(NetworkAccessManager *networkManager, QObject *parent = nullptr);
void setUsername(const QString &username);
QString username();
bool apiAvailable();
bool authenticated();
bool connected();
void getApiCredentials(const QString &url = "https://app.tado.com/env.js");
void getToken(const QString &password);
QString loginUrl() const;
QString username() const;
QString refreshToken() const;
void setRefreshToken(const QString &refreshToken);
// Login process
void getLoginUrl();
void startAuthentication();
void getAccessToken();
void getHomes();
void getZones(const QString &homeId);
void getZoneState(const QString &homeId, const QString &zoneId);
@ -110,36 +108,46 @@ private:
bool m_apiAvailable = false;
QString m_baseAuthorizationUrl;
QString m_baseControlUrl;
QString m_clientSecret;
QString m_clientId;
QString m_deviceCode;
NetworkAccessManager *m_networkManager = nullptr;
QString m_username;
QString m_loginUrl;
QString m_accessToken;
QString m_refreshToken;
QTimer *m_refreshTimer = nullptr;
QString m_username;
QTimer m_refreshTimer;
QTimer m_pollAuthenticationTimer;
uint m_pollAuthenticationCount = 0;
uint m_pollAuthenticationLimit = 5;
bool m_authenticationStatus = false;
bool m_connectionStatus = false;
void requestAuthenticationToken();
void setAuthenticationStatus(bool status);
void setConnectionStatus(bool status);
signals:
void connectionChanged(bool connected);
void apiCredentialsReceived(bool success);
void getLoginUrlFinished(bool success);
void startAuthenticationFinished(bool success);
void accessTokenReceived();
void usernameChanged(const QString &username);
void refreshTokenReceived(const QString &refreshToken);
void authenticationStatusChanged(bool authenticated);
void requestExecuted(QUuid requestId, bool success);
void tokenReceived(Token token);
void homesReceived(QList<Home> homes);
void zonesReceived(const QString &homeId, QList<Zone> zones);
void zoneStateReceived(const QString &homeId,const QString &zoneId, ZoneState sate);
void overlayReceived(const QString &homeId, const QString &zoneId, const Overlay &overlay);
void homesReceived(QList<Tado::Home> homes);
void zonesReceived(const QString &homeId, QList<Tado::Zone> zones);
void zoneStateReceived(const QString &homeId,const QString &zoneId, Tado::ZoneState sate);
void overlayReceived(const QString &homeId, const QString &zoneId, const Tado::Overlay &overlay);
void connectionError(QNetworkReply::NetworkError error);
private slots:
void onRefreshTimer();
};
#endif // TADO_H