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

This commit is contained in:
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 * Contact: contact@nymea.io
* *
* This file is part of nymea. * 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(); qCDebug(dcTado()) << "Start pairing process ...";
Tado *tado = new Tado(hardwareManager()->networkManager(), username, this);
Tado *tado = new Tado(hardwareManager()->networkManager(), this);
m_unfinishedTadoAccounts.insert(info->thingId(), tado); m_unfinishedTadoAccounts.insert(info->thingId(), tado);
connect(info, &ThingPairingInfo::aborted, this, [info, tado, this]() { 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()); m_unfinishedTadoAccounts.remove(info->thingId());
tado->deleteLater(); 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){ connect(tado, &Tado::connectionError, info, [info] (QNetworkReply::NetworkError error){
if (error != QNetworkReply::NetworkError::NoError){
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){
qCWarning(dcTado()) << "Confirm pairing failed" << error; qCWarning(dcTado()) << "Confirm pairing failed" << error;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Connection error")); info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("A connection error occurred."));
}
// 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."));
} }
}); });
connect(tado, &Tado::tokenReceived, info, [this, info, username, password](Tado::Token token) { connect(tado, &Tado::startAuthenticationFinished, info, [info, tado, this](bool success) {
Q_UNUSED(token) 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()->beginGroup(info->thingId().toString());
pluginStorage()->setValue("username", username); pluginStorage()->setValue("refreshToken", tado->refreshToken());
pluginStorage()->setValue("password", password);
pluginStorage()->endGroup(); pluginStorage()->endGroup();
info->finish(Thing::ThingErrorNoError); info->finish(Thing::ThingErrorNoError);
}); });
tado->getApiCredentials();
tado->startAuthentication();
} }
void IntegrationPluginTado::setupThing(ThingSetupInfo *info) void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
@ -112,8 +119,9 @@ void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
if (thing->thingClassId() == tadoAccountThingClassId) { if (thing->thingClassId() == tadoAccountThingClassId) {
qCDebug(dcTado) << "Setup Tado account" << thing->name() << thing->params(); qCDebug(dcTado) << "Setting up Tado account" << thing->name() << thing->params();
Tado *tado;
Tado *tado = nullptr;
if (m_tadoAccounts.contains(thing->id())) { if (m_tadoAccounts.contains(thing->id())) {
qCDebug(dcTado()) << "Setup after reconfigure, cleaning up"; qCDebug(dcTado()) << "Setup after reconfigure, cleaning up";
@ -125,51 +133,65 @@ void IntegrationPluginTado::setupThing(ThingSetupInfo *info)
tado = m_unfinishedTadoAccounts.take(thing->id()); tado = m_unfinishedTadoAccounts.take(thing->id());
m_tadoAccounts.insert(thing->id(), tado); m_tadoAccounts.insert(thing->id(), tado);
info->finish(Thing::ThingErrorNoError); info->finish(Thing::ThingErrorNoError);
} else {
pluginStorage()->beginGroup(thing->id().toString()); pluginStorage()->beginGroup(thing->id().toString());
QString username = pluginStorage()->value("username").toString(); pluginStorage()->setValue("refreshToken", tado->refreshToken());
QString password = pluginStorage()->value("password").toString(); 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(); 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); 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())) { if (m_tadoAccounts.contains(info->thing()->id())) {
Tado *tado = m_tadoAccounts.take(info->thing()->id()); Tado *tado = m_tadoAccounts.take(info->thing()->id());
tado->deleteLater(); tado->deleteLater();
} }
});
connect(tado, &Tado::apiCredentialsReceived, info, [password, tado] { qCWarning(dcTado()) << "Connection error during setup:" << error;
tado->getToken(password); info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Connection error"));
}); }
});
connect(tado, &Tado::tokenReceived, info, [ info](Tado::Token token) { connect(tado, &Tado::usernameChanged, this, &IntegrationPluginTado::onUsernameChanged);
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::authenticationStatusChanged, this, &IntegrationPluginTado::onAuthenticationStatusChanged); connect(tado, &Tado::authenticationStatusChanged, this, &IntegrationPluginTado::onAuthenticationStatusChanged);
connect(tado, &Tado::requestExecuted, this, &IntegrationPluginTado::onRequestExecuted); connect(tado, &Tado::requestExecuted, this, &IntegrationPluginTado::onRequestExecuted);
connect(tado, &Tado::connectionChanged, this, &IntegrationPluginTado::onConnectionChanged); 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::zonesReceived, this, &IntegrationPluginTado::onZonesReceived);
connect(tado, &Tado::zoneStateReceived, this, &IntegrationPluginTado::onZoneStateReceived); connect(tado, &Tado::zoneStateReceived, this, &IntegrationPluginTado::onZoneStateReceived);
connect(tado, &Tado::overlayReceived, this, &IntegrationPluginTado::onOverlayReceived); connect(tado, &Tado::overlayReceived, this, &IntegrationPluginTado::onOverlayReceived);
tado->getAccessToken();
return; return;
} else if (thing->thingClassId() == zoneThingClassId) { } else if (thing->thingClassId() == zoneThingClassId) {
@ -205,6 +230,9 @@ void IntegrationPluginTado::thingRemoved(Thing *thing)
tado->deleteLater(); tado->deleteLater();
} }
// Clean up storage
pluginStorage()->remove(thing->id().toString());
if (myThings().isEmpty() && m_pluginTimer) { if (myThings().isEmpty() && m_pluginTimer) {
m_pluginTimer->deleteLater(); m_pluginTimer->deleteLater();
m_pluginTimer = nullptr; m_pluginTimer = nullptr;
@ -220,7 +248,7 @@ void IntegrationPluginTado::postSetupThing(Thing *thing)
if (thing->thingClassId() == tadoAccountThingClassId) { if (thing->thingClassId() == tadoAccountThingClassId) {
Tado *tado = m_tadoAccounts.value(thing->id()); Tado *tado = m_tadoAccounts.value(thing->id());
thing->setStateValue(tadoAccountUserDisplayNameStateTypeId, tado->username()); //thing->setStateValue(tadoAccountUserDisplayNameStateTypeId, tado->username());
thing->setStateValue(tadoAccountLoggedInStateTypeId, true); thing->setStateValue(tadoAccountLoggedInStateTypeId, true);
thing->setStateValue(tadoAccountConnectedStateTypeId, true); thing->setStateValue(tadoAccountConnectedStateTypeId, true);
tado->getHomes(); tado->getHomes();
@ -260,7 +288,7 @@ void IntegrationPluginTado::executeAction(ThingActionInfo *info)
} }
} }
m_asyncActions.insert(requestId, 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) { } else if (action.actionTypeId() == zoneTargetTemperatureActionTypeId) {
double temperature = action.param(zoneTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble(); double temperature = action.param(zoneTargetTemperatureActionTargetTemperatureParamTypeId).value().toDouble();
@ -271,7 +299,7 @@ void IntegrationPluginTado::executeAction(ThingActionInfo *info)
requestId = tado->setOverlay(homeId, zoneId, true, temperature); requestId = tado->setOverlay(homeId, zoneId, true, temperature);
} }
m_asyncActions.insert(requestId, 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() == zonePowerActionTypeId) { } else if (action.actionTypeId() == zonePowerActionTypeId) {
bool power = action.param(zonePowerActionPowerParamTypeId).value().toBool(); bool power = action.param(zonePowerActionPowerParamTypeId).value().toBool();
thing->setStateValue(zonePowerStateTypeId, power); // the actual power set response might be slow 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); requestId = tado->setOverlay(homeId, zoneId, true, temperature);
} }
m_asyncActions.insert(requestId, 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 { } else {
qCWarning(dcTado()) << "Execute action, unhandled actionTypeId" << action.actionTypeId(); qCWarning(dcTado()) << "Execute action, unhandled actionTypeId" << action.actionTypeId();
info->finish(Thing::ThingErrorActionTypeNotFound); info->finish(Thing::ThingErrorActionTypeNotFound);
@ -299,12 +327,9 @@ void IntegrationPluginTado::onPluginTimer()
Q_FOREACH(Tado *tado, m_tadoAccounts){ Q_FOREACH(Tado *tado, m_tadoAccounts){
ThingId accountThingId = m_tadoAccounts.key(tado); ThingId accountThingId = m_tadoAccounts.key(tado);
if (!tado->authenticated()) { if (!tado->authenticated()) {
pluginStorage()->beginGroup(accountThingId.toString()); tado->getAccessToken();
QString password = pluginStorage()->value("password").toString();
pluginStorage()->endGroup();
tado->getToken(password);
} else { } else {
Q_FOREACH(Thing *thing, myThings().filterByParentId(accountThingId)) { foreach (Thing *thing, myThings().filterByParentId(accountThingId)) {
if (thing->thingClassId() == zoneThingClassId) { if (thing->thingClassId() == zoneThingClassId) {
QString homeId = thing->paramValue(zoneThingHomeIdParamTypeId).toString(); QString homeId = thing->paramValue(zoneThingHomeIdParamTypeId).toString();
QString zoneId = thing->paramValue(zoneThingZoneIdParamTypeId).toString(); QString zoneId = thing->paramValue(zoneThingZoneIdParamTypeId).toString();
@ -326,7 +351,7 @@ void IntegrationPluginTado::onConnectionChanged(bool connected)
thing->setStateValue(tadoAccountConnectedStateTypeId, connected); thing->setStateValue(tadoAccountConnectedStateTypeId, connected);
if (!connected) { if (!connected) {
Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) { foreach (Thing *child, myThings().filterByParentId(thing->id())) {
if (child->thingClassId() == zoneThingClassId) { if (child->thingClassId() == zoneThingClassId) {
child->setStateValue(zoneConnectedStateTypeId, connected); child->setStateValue(zoneConnectedStateTypeId, connected);
} }
@ -347,7 +372,7 @@ void IntegrationPluginTado::onAuthenticationStatusChanged(bool authenticated)
} }
thing->setStateValue(tadoAccountLoggedInStateTypeId, authenticated); thing->setStateValue(tadoAccountLoggedInStateTypeId, authenticated);
if (!authenticated) { if (!authenticated) {
Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) { foreach (Thing *child, myThings().filterByParentId(thing->id())) {
if (child->thingClassId() == zoneThingClassId) { if (child->thingClassId() == zoneThingClassId) {
child->setStateValue(zoneConnectedStateTypeId, authenticated); 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) void IntegrationPluginTado::onRequestExecuted(QUuid requestId, bool success)
{ {
if (m_asyncActions.contains(requestId)) { 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 * Contact: contact@nymea.io
* *
* This file is part of nymea. * This file is part of nymea.
@ -39,7 +39,6 @@
#include <network/oauth2.h> #include <network/oauth2.h>
#include <QHash> #include <QHash>
#include <QTimer> #include <QTimer>
class IntegrationPluginTado : public IntegrationPlugin class IntegrationPluginTado : public IntegrationPlugin
@ -50,8 +49,12 @@ class IntegrationPluginTado : public IntegrationPlugin
public: public:
explicit IntegrationPluginTado(); explicit IntegrationPluginTado();
void init() override;
void startPairing(ThingPairingInfo *info) override; void startPairing(ThingPairingInfo *info) override;
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override; void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
void setupThing(ThingSetupInfo *info) override; void setupThing(ThingSetupInfo *info) override;
void thingRemoved(Thing *thing) override; void thingRemoved(Thing *thing) override;
void postSetupThing(Thing *thing) override; void postSetupThing(Thing *thing) override;
@ -59,9 +62,9 @@ public:
private: private:
PluginTimer *m_pluginTimer = nullptr; 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; QHash<QUuid, ThingActionInfo *> m_asyncActions;
private slots: private slots:
@ -69,6 +72,7 @@ private slots:
void onConnectionChanged(bool connected); void onConnectionChanged(bool connected);
void onAuthenticationStatusChanged(bool authenticated); void onAuthenticationStatusChanged(bool authenticated);
void onUsernameChanged(const QString &username);
void onRequestExecuted(QUuid requestId, bool success); void onRequestExecuted(QUuid requestId, bool success);
void onHomesReceived(QList<Tado::Home> homes); void onHomesReceived(QList<Tado::Home> homes);
void onZonesReceived(const QString &homeId, QList<Tado::Zone> zones); void onZonesReceived(const QString &homeId, QList<Tado::Zone> zones);

View File

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

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright 2013 - 2020, nymea GmbH * Copyright 2013 - 2025, nymea GmbH
* Contact: contact@nymea.io * Contact: contact@nymea.io
* *
* This file is part of nymea. * This file is part of nymea.
@ -36,24 +36,27 @@
#include <QJsonArray> #include <QJsonArray>
#include <QUrlQuery> #include <QUrlQuery>
Tado::Tado(NetworkAccessManager *networkManager, const QString &username, QObject *parent) : Tado::Tado(NetworkAccessManager *networkManager, QObject *parent) :
QObject(parent), QObject(parent),
m_networkManager(networkManager), m_networkManager(networkManager)
m_username(username)
{ {
m_refreshTimer = new QTimer(this); m_baseControlUrl = "https://my.tado.com/api/v2";
m_refreshTimer->setSingleShot(true); m_baseAuthorizationUrl = "https://login.tado.com/oauth2";
connect(m_refreshTimer, &QTimer::timeout, this, &Tado::onRefreshTimer);
}
void Tado::setUsername(const QString &username) m_clientId = "1bb50063-6b0c-4d11-bd99-387f4a91cc46";
{
m_username = username;
}
QString Tado::username() m_refreshTimer.setSingleShot(true);
{ connect(&m_refreshTimer, &QTimer::timeout, this, [this](){
return m_username; 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() bool Tado::apiAvailable()
@ -71,138 +74,160 @@ bool Tado::connected()
return m_connectionStatus; return m_connectionStatus;
} }
void Tado::getApiCredentials(const QString &url)
QString Tado::loginUrl() const
{ {
QNetworkRequest request; return m_loginUrl;
request.setUrl(url); }
QNetworkReply *reply = m_networkManager->get(request);
qCDebug(dcTado()) << "Sending request" << request.url(); 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, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] { connect(reply, &QNetworkReply::finished, this, [reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Check HTTP status code // Check HTTP status code
if (status != 200 || reply->error() != QNetworkReply::NoError) { if (status != 200 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); emit getLoginUrlFinished(false);
emit apiCredentialsReceived(false);
return;
}
QRegExp filter;
filter.setPatternSyntax(QRegExp::Wildcard);
filter.setPattern("*tgaRestApiV2Endpoint:*");
QStringList list = QString(reply->readAll()).split('\n'); emit connectionError(reply->error());
int index = list.indexOf(filter);
if (index == -1) { if (reply->error() == QNetworkReply::HostNotFoundError)
qCWarning(dcTado()) << "GetApiCredenitals: Could not find the API url"; setConnectionStatus(false);
emit apiCredentialsReceived(false);
if (status == 401 || status == 400)
setAuthenticationStatus(false);
qCWarning(dcTado()) << "Request error:" << status << reply->errorString();
return; 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; 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) { QNetworkRequest request = QNetworkRequest(QUrl(m_baseAuthorizationUrl + "/token"));
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"); 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()); QUrlQuery query;
// qCDebug(dcTado()) << "Sending request" << request.url() << query.toString(QUrl::FullyEncoded).toUtf8(); 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, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] { connect(reply, &QNetworkReply::finished, this, [reply, this] {
QByteArray data = reply->readAll();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Check HTTP status code // Check HTTP status code
if (status != 200 || reply->error() != QNetworkReply::NoError) { if (status != 200 || reply->error() != QNetworkReply::NoError) {
emit connectionError(reply->error()); emit connectionError(reply->error());
if (reply->error() == QNetworkReply::HostNotFoundError) {
if (reply->error() == QNetworkReply::HostNotFoundError)
setConnectionStatus(false); setConnectionStatus(false);
}
if (status == 401 || status == 400) { if (status == 401 || status == 400)
setAuthenticationStatus(false); setAuthenticationStatus(false);
}
qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); qCWarning(dcTado()) << "Request error:" << status << reply->errorString() << qUtf8Printable(data);
return; return;
} }
m_apiAvailable = true;
setConnectionStatus(true); setConnectionStatus(true);
QJsonParseError error; QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); QJsonDocument responseJsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { 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; 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(); qCDebug(dcTado()) << "Get access token response" << qUtf8Printable(responseJsonDoc.toJson());
token.refreshToken = obj["refresh_token"].toString(); QVariantMap responseMap = responseJsonDoc.toVariant().toMap();
m_refreshToken = token.refreshToken;
if (obj.contains("expires_in")) { m_accessToken = responseMap.value("access_token").toString();
token.expires = obj["expires_in"].toInt(); emit accessTokenReceived();
m_refreshTimer->start((token.expires - 10)*1000);
} else { QString refreshToken = responseMap.value("refresh_token").toString();
qCWarning(dcTado()) << "Received response doesn't contain an expire time"; if (m_refreshToken != refreshToken) {
} m_refreshToken = refreshToken;
token.scope = obj["scope"].toString(); emit refreshTokenReceived(m_refreshToken);
token.jti = obj["jti"].toString();
setAuthenticationStatus(true);
emit tokenReceived(token);
} else {
qCWarning(dcTado()) << "Received response isn't an object" << data.toJson();
setAuthenticationStatus(false);
} }
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; QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qDebug(dcTado()) << "Get Token: Recieved invalid JSON object"; qDebug(dcTado()) << "Get Homes: Recieved invalid JSON object";
return; 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; QList<Home> homes;
QVariantList homeList = data.toVariant().toMap().value("homes").toList(); QVariantList homeList = responseMap.value("homes").toList();
foreach (QVariant variant, homeList) { foreach (QVariant variant, homeList) {
QVariantMap obj = variant.toMap(); QVariantMap obj = variant.toMap();
Home home; Home home;
@ -258,6 +293,7 @@ void Tado::getHomes()
home.name = obj["name"].toString(); home.name = obj["name"].toString();
homes.append(home); homes.append(home);
} }
emit homesReceived(homes); emit homesReceived(homes);
}); });
} }
@ -275,7 +311,7 @@ void Tado::getZones(const QString &homeId)
} }
QNetworkRequest request; 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.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit());
QNetworkReply *reply = m_networkManager->get(request); QNetworkReply *reply = m_networkManager->get(request);
@ -332,7 +368,7 @@ void Tado::getZoneState(const QString &homeId, const QString &zoneId)
} }
QNetworkRequest request; 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.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit());
QNetworkReply *reply = m_networkManager->get(request); 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(); QUuid requestId = QUuid::createUuid();
QNetworkRequest request; 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.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json;charset=utf-8");
request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit()); request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit());
@ -424,14 +460,14 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
else else
powerString = "OFF"; 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; //qCDebug(dcTado()) << "Sending request" << body;
QNetworkReply *reply = m_networkManager->put(request, body); QNetworkReply *reply = m_networkManager->put(request, body);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Check HTTP status code
if (status != 200 || reply->error() != QNetworkReply::NoError) { if (status != 200 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false); emit requestExecuted(requestId, false);
emit connectionError(reply->error()); emit connectionError(reply->error());
@ -439,6 +475,7 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
if (reply->error() == QNetworkReply::HostNotFoundError) { if (reply->error() == QNetworkReply::HostNotFoundError) {
setConnectionStatus(false); setConnectionStatus(false);
} }
if (status == 401 || status == 400) { //Unauthorized if (status == 401 || status == 400) { //Unauthorized
setAuthenticationStatus(false); setAuthenticationStatus(false);
} else if (status == 422) { //Unprocessable Entity } else if (status == 422) { //Unprocessable Entity
@ -448,6 +485,7 @@ QUuid Tado::setOverlay(const QString &homeId, const QString &zoneId, bool power,
} }
return; return;
} }
setAuthenticationStatus(true); setAuthenticationStatus(true);
setConnectionStatus(true); setConnectionStatus(true);
emit requestExecuted(requestId, true); emit requestExecuted(requestId, true);
@ -488,21 +526,22 @@ QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId)
QUuid requestId = QUuid::createUuid(); QUuid requestId = QUuid::createUuid();
QNetworkRequest request; 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()); request.setRawHeader("Authorization", "Bearer " + m_accessToken.toLocal8Bit());
QNetworkReply *reply = m_networkManager->deleteResource(request); QNetworkReply *reply = m_networkManager->deleteResource(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] { connect(reply, &QNetworkReply::finished, this, [homeId, zoneId, requestId, reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Check HTTP status code
if (status < 200 || status > 210 || reply->error() != QNetworkReply::NoError) { if (status < 200 || status > 210 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId ,false); emit requestExecuted(requestId ,false);
emit connectionError(reply->error()); emit connectionError(reply->error());
if (reply->error() == QNetworkReply::HostNotFoundError) { if (reply->error() == QNetworkReply::HostNotFoundError) {
setConnectionStatus(false); setConnectionStatus(false);
} }
if (status == 401 || status == 400) { //Unauthorized if (status == 401 || status == 400) { //Unauthorized
setAuthenticationStatus(false); setAuthenticationStatus(false);
} else if (status == 422) { //Unprocessable Entity } else if (status == 422) { //Unprocessable Entity
@ -510,9 +549,10 @@ QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId)
} else { } else {
qCWarning(dcTado()) << "Request error:" << status << reply->errorString(); qCWarning(dcTado()) << "Request error:" << status << reply->errorString();
} }
qCWarning(dcTado()) << "Request error:" << status << reply->errorString();
return; return;
} }
setAuthenticationStatus(true); setAuthenticationStatus(true);
setConnectionStatus(true); setConnectionStatus(true);
emit requestExecuted(requestId, 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"; qDebug(dcTado()) << "Get Token: Recieved invalid JSON object";
return; return;
} }
QVariantMap map = data.toVariant().toMap(); QVariantMap map = data.toVariant().toMap();
Overlay overlay; Overlay overlay;
@ -539,6 +580,66 @@ QUuid Tado::deleteOverlay(const QString &homeId, const QString &zoneId)
return requestId; 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) void Tado::setAuthenticationStatus(bool status)
{ {
if (m_authenticationStatus != status) { if (m_authenticationStatus != status) {
@ -546,9 +647,9 @@ void Tado::setAuthenticationStatus(bool status)
emit authenticationStatusChanged(status); emit authenticationStatusChanged(status);
} }
if (!status) { if (!status)
m_refreshTimer->stop(); m_refreshTimer.stop();
}
} }
void Tado::setConnectionStatus(bool status) void Tado::setConnectionStatus(bool status)
@ -558,62 +659,3 @@ void Tado::setConnectionStatus(bool status)
emit connectionChanged(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 * Contact: contact@nymea.io
* *
* This file is part of nymea. * This file is part of nymea.
@ -31,8 +31,8 @@
#ifndef TADO_H #ifndef TADO_H
#define TADO_H #define TADO_H
#include "network/networkaccessmanager.h" #include <network/networkaccessmanager.h>
#include "integrations/thing.h" #include <integrations/thing.h>
#include <QObject> #include <QObject>
#include <QTimer> #include <QTimer>
@ -42,16 +42,6 @@ class Tado : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
struct Token {
QString accesToken;
QString tokenType;
QString refreshToken;
int expires;
QString scope;
QString jti;
};
struct Zone { struct Zone {
QString id; QString id;
QString name; QString name;
@ -89,16 +79,24 @@ public:
QString name; 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 apiAvailable();
bool authenticated(); bool authenticated();
bool connected(); bool connected();
void getApiCredentials(const QString &url = "https://app.tado.com/env.js"); QString loginUrl() const;
void getToken(const QString &password);
QString username() const;
QString refreshToken() const;
void setRefreshToken(const QString &refreshToken);
// Login process
void getLoginUrl();
void startAuthentication();
void getAccessToken();
void getHomes(); void getHomes();
void getZones(const QString &homeId); void getZones(const QString &homeId);
void getZoneState(const QString &homeId, const QString &zoneId); void getZoneState(const QString &homeId, const QString &zoneId);
@ -110,36 +108,46 @@ private:
bool m_apiAvailable = false; bool m_apiAvailable = false;
QString m_baseAuthorizationUrl; QString m_baseAuthorizationUrl;
QString m_baseControlUrl; QString m_baseControlUrl;
QString m_clientSecret;
QString m_clientId; QString m_clientId;
QString m_deviceCode;
NetworkAccessManager *m_networkManager = nullptr; NetworkAccessManager *m_networkManager = nullptr;
QString m_username; QString m_loginUrl;
QString m_accessToken; QString m_accessToken;
QString m_refreshToken; 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_authenticationStatus = false;
bool m_connectionStatus = false; bool m_connectionStatus = false;
void requestAuthenticationToken();
void setAuthenticationStatus(bool status); void setAuthenticationStatus(bool status);
void setConnectionStatus(bool status); void setConnectionStatus(bool status);
signals: signals:
void connectionChanged(bool connected); 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 authenticationStatusChanged(bool authenticated);
void requestExecuted(QUuid requestId, bool success); void requestExecuted(QUuid requestId, bool success);
void tokenReceived(Token token); void homesReceived(QList<Tado::Home> homes);
void homesReceived(QList<Home> homes); void zonesReceived(const QString &homeId, QList<Tado::Zone> zones);
void zonesReceived(const QString &homeId, QList<Zone> zones); void zoneStateReceived(const QString &homeId,const QString &zoneId, Tado::ZoneState sate);
void zoneStateReceived(const QString &homeId,const QString &zoneId, ZoneState sate); void overlayReceived(const QString &homeId, const QString &zoneId, const Tado::Overlay &overlay);
void overlayReceived(const QString &homeId, const QString &zoneId, const Overlay &overlay);
void connectionError(QNetworkReply::NetworkError error); void connectionError(QNetworkReply::NetworkError error);
private slots:
void onRefreshTimer();
}; };
#endif // TADO_H #endif // TADO_H