Merge PR #766: Tado: Update account authentication to device code grant
commit
19cb1c497d
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
410
tado/tado.cpp
410
tado/tado.cpp
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
68
tado/tado.h
68
tado/tado.h
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue