654 lines
29 KiB
C++
654 lines
29 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-plugins.
|
|
*
|
|
* nymea-plugins is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-plugins is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "integrationpluginnetatmo.h"
|
|
#include "netatmoconnection.h"
|
|
#include "plugininfo.h"
|
|
|
|
#include <plugintimer.h>
|
|
#include <integrations/thing.h>
|
|
#include <network/networkaccessmanager.h>
|
|
|
|
#include <QDebug>
|
|
#include <QUrlQuery>
|
|
#include <QJsonDocument>
|
|
|
|
IntegrationPluginNetatmo::IntegrationPluginNetatmo()
|
|
{
|
|
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::init()
|
|
{
|
|
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::startPairing(ThingPairingInfo *info)
|
|
{
|
|
if (!loadClientCredentials()) {
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("No API key installed."));
|
|
return;
|
|
}
|
|
|
|
// Compose url
|
|
NetatmoConnection *connection = new NetatmoConnection(hardwareManager()->networkManager(), m_clientId, m_clientSecret, this);
|
|
QUrl loginUrl = connection->getLoginUrl();
|
|
|
|
// Checking the internet connect^ion
|
|
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(QUrl("https://api.netatmo.net")));
|
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
|
connect(reply, &QNetworkReply::finished, info, [this, reply, info, connection, loginUrl]() {
|
|
//The server replies usually 404 not found on this request
|
|
//int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError) {
|
|
qCWarning(dcNetatmo()) << "Netatmo server is not reachable";
|
|
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Netatmo server is not reachable."));
|
|
return;
|
|
}
|
|
|
|
ThingId thingId = info->thingId();
|
|
m_pendingSetups.insert(thingId, connection);
|
|
connect(info, &ThingPairingInfo::aborted, connection, [thingId, this]() {
|
|
qCWarning(dcNetatmo()) << "ThingPairingInfo aborted, cleaning up pending setup connection.";
|
|
m_pendingSetups.take(thingId)->deleteLater();
|
|
});
|
|
|
|
qCDebug(dcNetatmo()) << "Netatmo server is reachable. Start the OAuth pairing process";
|
|
info->setOAuthUrl(loginUrl);
|
|
info->finish(Thing::ThingErrorNoError);
|
|
});
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
|
|
{
|
|
Q_UNUSED(username)
|
|
|
|
qCDebug(dcNetatmo()) << "Confirm pairing" << info->thingName();
|
|
if (info->thingClassId() == netatmoConnectionThingClassId) {
|
|
QUrl url(secret);
|
|
QUrlQuery query(url);
|
|
QByteArray authorizationCode = query.queryItemValue("code").toLocal8Bit();
|
|
if (authorizationCode.isEmpty()) {
|
|
qCWarning(dcNetatmo()) << "Error while pairing to netatmo server. No authorization code received.";
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to log in to the Netatmo server."));
|
|
return;
|
|
}
|
|
|
|
NetatmoConnection *connection = m_pendingSetups.value(info->thingId());
|
|
if (!connection) {
|
|
qCWarning(dcNetatmo()) << "No NetatmoConnect connection found for device:" << info->thingName();
|
|
m_pendingSetups.remove(info->thingId());
|
|
info->finish(Thing::ThingErrorHardwareFailure);
|
|
return;
|
|
}
|
|
|
|
connect(connection, &NetatmoConnection::receivedRefreshToken, info, [info, this](const QByteArray &refreshToken){
|
|
qCDebug(dcNetatmo()) << "Received token:" << NetatmoConnection::censorDebugOutput(refreshToken);
|
|
pluginStorage()->beginGroup(info->thingId().toString());
|
|
pluginStorage()->setValue("refresh_token", refreshToken);
|
|
pluginStorage()->endGroup();
|
|
|
|
info->finish(Thing::ThingErrorNoError);
|
|
});
|
|
|
|
qCDebug(dcNetatmo()) << "Authorization code" << NetatmoConnection::censorDebugOutput(authorizationCode);
|
|
if (!connection->getAccessTokenFromAuthorizationCode(authorizationCode)) {
|
|
qCWarning(dcNetatmo()) << "Failed to get token from authorization code.";
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to log in to the Netatmo server."));
|
|
return;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::setupThing(ThingSetupInfo *info)
|
|
{
|
|
Thing *thing = info->thing();
|
|
|
|
if (thing->thingClassId() == netatmoConnectionThingClassId) {
|
|
qCDebug(dcNetatmo) << "Setup Netatmo connection" << thing->name() << thing->params();
|
|
|
|
if (!loadClientCredentials()) {
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("No API key installed."));
|
|
return;
|
|
}
|
|
|
|
NetatmoConnection *connection = nullptr;
|
|
|
|
// Handle reconfigure
|
|
if (m_connections.contains(thing)) {
|
|
qCDebug(dcNetatmo()) << "Setup after reconfiguration, cleaning up";
|
|
m_connections.take(thing)->deleteLater();
|
|
}
|
|
|
|
if (m_pendingSetups.keys().contains(thing->id())) {
|
|
// This thing setup is after a pairing process
|
|
qCDebug(dcNetatmo()) << "Netatmo available after successful OAuth2 setup. Using the existing object";
|
|
connection = m_pendingSetups.take(thing->id());
|
|
m_connections.insert(thing, connection);
|
|
info->finish(Thing::ThingErrorNoError);
|
|
|
|
thing->setStateValue("connected", true);
|
|
thing->setStateValue(netatmoConnectionLoggedInStateTypeId, true);
|
|
|
|
connect(connection, &NetatmoConnection::authenticatedChanged, thing, [thing](bool authenticated){
|
|
thing->setStateValue(netatmoConnectionLoggedInStateTypeId, authenticated);
|
|
});
|
|
|
|
} else {
|
|
// The thing should have been already set up before, lets refresh the token if we have a refresh token,
|
|
// otherwise the user has to perform a reconfigure and login using OAuth2... (also old username password user end up here)
|
|
|
|
if (doingLoginMigration(info))
|
|
return;
|
|
|
|
// No migration needed...
|
|
setupConnection(info);
|
|
return;
|
|
}
|
|
return;
|
|
} else if (thing->thingClassId() == indoorThingClassId) {
|
|
qCDebug(dcNetatmo) << "Setup Netatmo indoor base station" << thing->params();
|
|
info->finish(Thing::ThingErrorNoError);
|
|
return;
|
|
} else if (thing->thingClassId() == outdoorThingClassId) {
|
|
qCDebug(dcNetatmo) << "Setup netatmo outdoor module" << thing->params();
|
|
info->finish(Thing::ThingErrorNoError);
|
|
return;
|
|
} else if (thing->thingClassId() == windModuleThingClassId) {
|
|
qCDebug(dcNetatmo) << "Setup netatmo wind module" << thing->params();
|
|
info->finish(Thing::ThingErrorNoError);
|
|
return;
|
|
} else if (thing->thingClassId() == rainGaugeThingClassId) {
|
|
qCDebug(dcNetatmo) << "Setup netatmo wind module" << thing->params();
|
|
info->finish(Thing::ThingErrorNoError);
|
|
return;
|
|
} else if (thing->thingClassId() == indoorModuleThingClassId) {
|
|
qCDebug(dcNetatmo) << "Setup netatmo indoor module" << thing->params();
|
|
info->finish(Thing::ThingErrorNoError);
|
|
return;
|
|
} else {
|
|
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(info->thing()->thingClassId().toString()).toUtf8());
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::postSetupThing(Thing *thing)
|
|
{
|
|
if (thing->thingClassId() == netatmoConnectionThingClassId) {
|
|
refreshConnection(thing);
|
|
} else if (thing->thingClassId() == indoorThingClassId) {
|
|
QString stationId = thing->paramValue(indoorThingMacParamTypeId).toString();
|
|
if (m_temporaryInitData.contains(stationId)) {
|
|
updateModuleStates(thing, m_temporaryInitData.take(stationId));
|
|
}
|
|
} else if (thing->thingClassId() == outdoorThingClassId) {
|
|
QString stationId = thing->paramValue(outdoorThingMacParamTypeId).toString();
|
|
if (m_temporaryInitData.contains(stationId)) {
|
|
updateModuleStates(thing, m_temporaryInitData.take(stationId));
|
|
}
|
|
}
|
|
|
|
if (!m_pluginTimer) {
|
|
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(600);
|
|
connect(m_pluginTimer, &PluginTimer::timeout, this, [this](){
|
|
foreach (Thing *connectionThing, myThings().filterByThingClassId(netatmoConnectionThingClassId)) {
|
|
refreshConnection(connectionThing);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::thingRemoved(Thing *thing)
|
|
{
|
|
if (thing->thingClassId() == netatmoConnectionThingClassId) {
|
|
m_connections.take(thing)->deleteLater();
|
|
}
|
|
|
|
if (myThings().isEmpty() && m_pluginTimer) {
|
|
if (m_pluginTimer) {
|
|
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
|
|
m_pluginTimer = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::setupConnection(ThingSetupInfo *info)
|
|
{
|
|
Thing *thing = info->thing();
|
|
|
|
qCDebug(dcNetatmo()) << "Setup netatmo account" << thing->name();
|
|
|
|
// Load refresh token
|
|
pluginStorage()->beginGroup(thing->id().toString());
|
|
QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray();
|
|
pluginStorage()->endGroup();
|
|
if (refreshToken.isEmpty()) {
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Could not authenticate on the server. Please reconfigure the connection."));
|
|
return;
|
|
}
|
|
|
|
// Create new connection
|
|
NetatmoConnection *connection = new NetatmoConnection(hardwareManager()->networkManager(), m_clientId, m_clientSecret, thing);
|
|
connect(connection, &NetatmoConnection::authenticatedChanged, info, [this, info, thing, connection](bool authenticated){
|
|
if (authenticated) {
|
|
m_connections.insert(thing, connection);
|
|
qCDebug(dcNetatmo()) << "Authenticated successfully the netatmo connection.";
|
|
info->finish(Thing::ThingErrorNoError);
|
|
thing->setStateValue("connected", true);
|
|
} else {
|
|
qCDebug(dcNetatmo()) << "Authentication process failed.";
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Authentication failed. Please reconfigure the connection."));
|
|
}
|
|
});
|
|
|
|
connect(info, &ThingSetupInfo::aborted, connection, [this, thing, connection] {
|
|
if (m_connections.contains(thing))
|
|
m_connections.remove(thing);
|
|
|
|
connection->deleteLater();
|
|
});
|
|
|
|
connect(connection, &NetatmoConnection::authenticatedChanged, thing, [thing](bool authenticated){
|
|
thing->setStateValue(netatmoConnectionLoggedInStateTypeId, authenticated);
|
|
});
|
|
|
|
connection->getAccessTokenFromRefreshToken(refreshToken);
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::refreshConnection(Thing *thing)
|
|
{
|
|
qCDebug(dcNetatmo()) << "Refresh connection" << thing;
|
|
|
|
NetatmoConnection *connection = m_connections.value(thing);
|
|
if (!connection) {
|
|
qCWarning(dcNetatmo()) << "Failed to refresh data. The connection object does not exist";
|
|
return;
|
|
}
|
|
|
|
QNetworkReply *reply = connection->getStationData();
|
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply, thing]() {
|
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
qCWarning(dcNetatmo()) << "Failed to refresh station data. Network reply returned with error" << status << reply->errorString();
|
|
thing->setStateValue("connected", false);
|
|
return;
|
|
}
|
|
|
|
thing->setStateValue("connected", true);
|
|
|
|
QJsonParseError error;
|
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
|
|
if (error.error != QJsonParseError::NoError) {
|
|
qCWarning(dcNetatmo()) << "OAuth2: Failed to get token. Refresh data reply JSON error:" << error.errorString();
|
|
return;
|
|
}
|
|
|
|
QVariantMap responseMap = jsonDoc.toVariant().toMap();
|
|
if (responseMap.value("status") != "ok") {
|
|
qCWarning(dcNetatmo()) << "Refresh station data returned status not ok:" << responseMap.value("status").toString();
|
|
return;
|
|
}
|
|
|
|
qCDebug(dcNetatmo()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
|
QVariantMap bodyMap = responseMap.value("body").toMap();
|
|
thing->setStateValue("username", bodyMap.value("user").toMap().value("mail").toString());
|
|
processRefreshData(thing, bodyMap.value("devices").toList());
|
|
});
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::processRefreshData(Thing *connectionThing, const QVariantList &devices)
|
|
{
|
|
foreach (QVariant deviceVariant, devices) {
|
|
QVariantMap deviceMap = deviceVariant.toMap();
|
|
if (deviceMap.value("type").toString() == "NAMain") {
|
|
Thing *existingThing = findIndoorStationThing(deviceMap.value("_id").toString());
|
|
// Check if we have to create the thing (auto)
|
|
if (!existingThing) {
|
|
ThingDescriptor descriptor(indoorThingClassId, deviceMap.value("module_name").toString() + " " + deviceMap.value("station_name").toString(), QString(), connectionThing->id());
|
|
ParamList params;
|
|
params.append(Param(indoorThingMacParamTypeId, deviceMap.value("_id").toString()));
|
|
descriptor.setParams(params);
|
|
m_temporaryInitData.insert(deviceMap.value("_id").toString(), deviceMap);
|
|
emit autoThingsAppeared({descriptor});
|
|
} else {
|
|
// Update the indoor station states
|
|
updateModuleStates(existingThing, deviceMap);
|
|
}
|
|
}
|
|
|
|
/* Check modules of this station...
|
|
*
|
|
* NAMain: Indoor station ("Temperature", "CO2", "Humidity", "Noise", "Pressure")
|
|
* NAModule1: Outdoor module ("Temperature, Humidity")
|
|
* NAModule2: Wind module ("Wind")
|
|
* NAModule3: Rain gauge module ("Rain")
|
|
* NAModule4: Indoor module ("Temperature, Humidity, CO2")
|
|
*/
|
|
|
|
if (deviceMap.contains("modules")) {
|
|
QVariantList modulesList = deviceMap.value("modules").toList();
|
|
foreach (QVariant moduleVariant, modulesList) {
|
|
QVariantMap moduleMap = moduleVariant.toMap();
|
|
|
|
if (moduleMap.value("type").toString() == "NAModule1") {
|
|
Thing *existingThing = findOutdoorModuleThing(moduleMap.value("_id").toString());
|
|
if (!existingThing) {
|
|
ThingDescriptor descriptor(outdoorThingClassId, moduleMap.value("module_name").toString() + " " + moduleMap.value("station_name").toString(), QString(), connectionThing->id());
|
|
ParamList params;
|
|
params.append(Param(outdoorThingMacParamTypeId, moduleMap.value("_id").toString()));
|
|
params.append(Param(outdoorThingBaseStationParamTypeId, deviceMap.value("_id").toString()));
|
|
descriptor.setParams(params);
|
|
m_temporaryInitData.insert(moduleMap.value("_id").toString(), moduleMap);
|
|
emit autoThingsAppeared({descriptor});
|
|
} else {
|
|
updateModuleStates(existingThing, moduleMap);
|
|
}
|
|
} else if (moduleMap.value("type").toString() == "NAModule2") {
|
|
Thing *existingThing = findWindModuleThing(moduleMap.value("_id").toString());
|
|
if (!existingThing) {
|
|
ThingDescriptor descriptor(windModuleThingClassId, moduleMap.value("module_name").toString() + " " + moduleMap.value("station_name").toString(), QString(), connectionThing->id());
|
|
ParamList params;
|
|
params.append(Param(windModuleThingMacParamTypeId, moduleMap.value("_id").toString()));
|
|
params.append(Param(windModuleThingBaseStationParamTypeId, deviceMap.value("_id").toString()));
|
|
descriptor.setParams(params);
|
|
m_temporaryInitData.insert(moduleMap.value("_id").toString(), moduleMap);
|
|
emit autoThingsAppeared({descriptor});
|
|
} else {
|
|
updateModuleStates(existingThing, moduleMap);
|
|
}
|
|
} else if (moduleMap.value("type").toString() == "NAModule3") {
|
|
Thing *existingThing = findRainGaugeModuleThing(moduleMap.value("_id").toString());
|
|
if (!existingThing) {
|
|
ThingDescriptor descriptor(rainGaugeThingClassId, moduleMap.value("module_name").toString() + " " + moduleMap.value("station_name").toString(), QString(), connectionThing->id());
|
|
ParamList params;
|
|
params.append(Param(rainGaugeThingMacParamTypeId, moduleMap.value("_id").toString()));
|
|
params.append(Param(rainGaugeThingBaseStationParamTypeId, deviceMap.value("_id").toString()));
|
|
descriptor.setParams(params);
|
|
m_temporaryInitData.insert(moduleMap.value("_id").toString(), moduleMap);
|
|
emit autoThingsAppeared({descriptor});
|
|
} else {
|
|
updateModuleStates(existingThing, moduleMap);
|
|
}
|
|
} else if (moduleMap.value("type").toString() == "NAModule4") {
|
|
Thing *existingThing = findIndoorModuleThing(moduleMap.value("_id").toString());
|
|
if (!existingThing) {
|
|
ThingDescriptor descriptor(indoorModuleThingClassId, moduleMap.value("module_name").toString() + " " + moduleMap.value("station_name").toString(), QString(), connectionThing->id());
|
|
ParamList params;
|
|
params.append(Param(indoorModuleThingMacParamTypeId, moduleMap.value("_id").toString()));
|
|
params.append(Param(indoorModuleThingBaseStationParamTypeId, deviceMap.value("_id").toString()));
|
|
descriptor.setParams(params);
|
|
m_temporaryInitData.insert(moduleMap.value("_id").toString(), moduleMap);
|
|
emit autoThingsAppeared({descriptor});
|
|
} else {
|
|
updateModuleStates(existingThing, moduleMap);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Thing *IntegrationPluginNetatmo::findIndoorStationThing(const QString &macAddress)
|
|
{
|
|
foreach (Thing *thing, myThings().filterByThingClassId(indoorThingClassId)) {
|
|
if (thing->paramValue(indoorThingMacParamTypeId).toString() == macAddress) {
|
|
return thing;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Thing *IntegrationPluginNetatmo::findOutdoorModuleThing(const QString &macAddress)
|
|
{
|
|
foreach (Thing *thing, myThings().filterByThingClassId(outdoorThingClassId)) {
|
|
if (thing->paramValue(outdoorThingMacParamTypeId).toString() == macAddress) {
|
|
return thing;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Thing *IntegrationPluginNetatmo::findIndoorModuleThing(const QString &macAddress)
|
|
{
|
|
foreach (Thing *thing, myThings().filterByThingClassId(indoorModuleThingClassId)) {
|
|
if (thing->paramValue(indoorModuleThingMacParamTypeId).toString() == macAddress) {
|
|
return thing;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Thing *IntegrationPluginNetatmo::findWindModuleThing(const QString &macAddress)
|
|
{
|
|
foreach (Thing *thing, myThings().filterByThingClassId(windModuleThingClassId)) {
|
|
if (thing->paramValue(windModuleThingMacParamTypeId).toString() == macAddress) {
|
|
return thing;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Thing *IntegrationPluginNetatmo::findRainGaugeModuleThing(const QString &macAddress)
|
|
{
|
|
foreach (Thing *thing, myThings().filterByThingClassId(rainGaugeThingClassId)) {
|
|
if (thing->paramValue(rainGaugeThingMacParamTypeId).toString() == macAddress) {
|
|
return thing;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void IntegrationPluginNetatmo::updateModuleStates(Thing *thing, const QVariantMap &data)
|
|
{
|
|
// check data timestamp
|
|
if (data.contains("last_message"))
|
|
thing->setStateValue("updateTime", data.value("last_message").toInt());
|
|
|
|
// update dashboard data
|
|
if (data.contains("dashboard_data")) {
|
|
QVariantMap measurments = data.value("dashboard_data").toMap();
|
|
if (measurments.contains("Temperature"))
|
|
thing->setStateValue("temperature", measurments.value("Temperature").toDouble());
|
|
|
|
if (measurments.contains("min_temp"))
|
|
thing->setStateValue("temperatureMin", measurments.value("min_temp").toDouble());
|
|
|
|
if (measurments.contains("max_temp"))
|
|
thing->setStateValue("temperatureMax", measurments.value("max_temp").toDouble());
|
|
|
|
if (measurments.contains("Humidity"))
|
|
thing->setStateValue("humidity", measurments.value("Humidity").toInt());
|
|
|
|
if (measurments.contains("AbsolutePressure"))
|
|
thing->setStateValue("pressure", measurments.value("AbsolutePressure").toDouble());
|
|
|
|
if (measurments.contains("CO2"))
|
|
thing->setStateValue("co2", measurments.value("CO2").toInt());
|
|
|
|
if (measurments.contains("Noise"))
|
|
thing->setStateValue("noise", measurments.value("Noise").toInt());
|
|
|
|
// Wind speed (given in km/s, we need m/s)
|
|
if (measurments.contains("WindStrength"))
|
|
thing->setStateValue("windSpeed", measurments.value("WindStrength").toDouble() / 3.6);
|
|
|
|
if (measurments.contains("WindAngle"))
|
|
thing->setStateValue("windDirection", measurments.value("WindAngle").toInt());
|
|
|
|
// Rain
|
|
if (measurments.contains("sum_rain_1"))
|
|
thing->setStateValue("rainfallLastHour", measurments.value("sum_rain_1").toInt());
|
|
|
|
if (measurments.contains("sum_rain_24"))
|
|
thing->setStateValue("rainfallLastDay", measurments.value("sum_rain_24").toInt());
|
|
|
|
}
|
|
|
|
// Battery
|
|
if (thing->thingClass().hasStateType("batteryLevel")) {
|
|
if (data.contains("battery_percent")) {
|
|
thing->setStateValue("batteryLevel", data.value("battery_percent").toInt());
|
|
} else {
|
|
// Fallback for older versions, calculate from voltage
|
|
if (data.contains("battery_vp")) {
|
|
int battery = data.value("battery_vp").toInt();
|
|
if (battery >= 6000) {
|
|
thing->setStateValue("batteryLevel", 100);
|
|
} else if (battery <= 3600) {
|
|
thing->setStateValue("batteryLevel", 0);
|
|
} else {
|
|
int delta = battery - 3600;
|
|
thing->setStateValue("batteryLevel", qRound(100.0 * delta / 2400));
|
|
}
|
|
}
|
|
}
|
|
|
|
thing->setStateValue("batteryCritical", thing->stateValue("batteryLevel").toInt() < 10);
|
|
}
|
|
|
|
// Signal strength (RF, 90 low, 60 highest)
|
|
if (data.contains("rf_status")) {
|
|
int signalStrength = data.value("rf_status").toInt();
|
|
if (signalStrength <= 60) {
|
|
thing->setStateValue("signalStrength", 100);
|
|
} else if (signalStrength >= 90) {
|
|
thing->setStateValue("signalStrength", 0);
|
|
} else {
|
|
int delta = 30 - (signalStrength - 60);
|
|
thing->setStateValue("signalStrength", qRound(100.0 * delta / 30.0));
|
|
}
|
|
}
|
|
|
|
// Wifi status (86=bad, 56=good)"
|
|
|
|
if (data.contains("wifi_status")) {
|
|
int signalStrength = data.value("wifi_status").toInt();
|
|
if (signalStrength <= 56) {
|
|
thing->setStateValue("signalStrength", 100);
|
|
} else if (signalStrength >= 86) {
|
|
thing->setStateValue("signalStrength", 0);
|
|
} else {
|
|
int delta = 30 - (signalStrength - 56);
|
|
thing->setStateValue("signalStrength", qRound(100.0 * delta / 30.0));
|
|
}
|
|
}
|
|
|
|
// update reachable state
|
|
if (data.contains("reachable"))
|
|
thing->setStateValue("connected", data.value("reachable").toBool());
|
|
}
|
|
|
|
bool IntegrationPluginNetatmo::doingLoginMigration(ThingSetupInfo *info)
|
|
{
|
|
// Returns true if we need to perform a login migration
|
|
// 1. First we stored the username and password in thing params and then moved to the plugin storage
|
|
// 2. Username and password login does not work any more since mid 2022 if you change the password, we need to make a propper oauth2 login
|
|
|
|
Thing *thing = info->thing();
|
|
QString username; QString password;
|
|
if (pluginStorage()->childGroups().contains(thing->id().toString())) {
|
|
pluginStorage()->beginGroup(thing->id().toString());
|
|
username = pluginStorage()->value("username").toString();
|
|
password = pluginStorage()->value("password").toString();
|
|
pluginStorage()->endGroup();
|
|
} else {
|
|
/* username and password have been stored as thingParams,
|
|
* this is to migrate the params to plug-in storage. */
|
|
ParamTypeId usernameParamTypeId = ParamTypeId("763c2c10-dee5-41c8-9f7e-ded741945e73");
|
|
ParamTypeId passwordParamTypeId = ParamTypeId("c0d892d6-f359-4782-9d7d-8f74a3b53e3e");
|
|
|
|
username = thing->paramValue(usernameParamTypeId).toString();
|
|
password = thing->paramValue(passwordParamTypeId).toString();
|
|
|
|
// Delete username and password so it wont be visible in the things.conf file.
|
|
thing->setParamValue(ParamTypeId("763c2c10-dee5-41c8-9f7e-ded741945e73"), "");
|
|
thing->setParamValue(ParamTypeId("c0d892d6-f359-4782-9d7d-8f74a3b53e3e"), "");
|
|
}
|
|
|
|
if (username.isEmpty() || password.isEmpty()) {
|
|
// No username and no password found from previouse setting...nothing to migrate here
|
|
return false;
|
|
}
|
|
|
|
// We found username and password. If we would have done the migration, they would not be there any more
|
|
|
|
qCDebug(dcNetatmo()) << "Found deprecated username and password in the settings. Performing migration to plain OAuth2...";
|
|
|
|
// Create a connection, get the refresh token, delete the connection and continue with normal setup.
|
|
NetatmoConnection *connection = new NetatmoConnection(hardwareManager()->networkManager(), m_clientId, m_clientSecret, thing);
|
|
connect(info, &ThingSetupInfo::aborted, connection, &NetatmoConnection::deleteLater);
|
|
connect(connection, &NetatmoConnection::authenticatedChanged, info, [this, info, thing, connection](bool authenticated){
|
|
connection->deleteLater();
|
|
if (authenticated) {
|
|
pluginStorage()->beginGroup(thing->id().toString());
|
|
pluginStorage()->setValue("refresh_token", connection->refreshToken());
|
|
// Success, we can delete username and passord once and for all and keep only the refresh token
|
|
pluginStorage()->remove("username");
|
|
pluginStorage()->remove("password");
|
|
pluginStorage()->endGroup();
|
|
qCDebug(dcNetatmo()) << "Migration finished successfully. Continue with normal setup";
|
|
setupConnection(info);
|
|
} else {
|
|
qCDebug(dcNetatmo()) << "Authentication process failed.";
|
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Authentication failed. Please reconfigure the connection."));
|
|
}
|
|
});
|
|
|
|
connection->getAccessTokenFromUsernamePassword(username, password);
|
|
return true;
|
|
}
|
|
|
|
bool IntegrationPluginNetatmo::loadClientCredentials()
|
|
{
|
|
QByteArray clientId = configValue(netatmoPluginCustomClientIdParamTypeId).toByteArray();
|
|
QByteArray clientSecret = configValue(netatmoPluginCustomClientSecretParamTypeId).toByteArray();
|
|
if (clientId.isEmpty() || clientSecret.isEmpty()) {
|
|
clientId = apiKeyStorage()->requestKey("netatmo").data("clientId");
|
|
clientSecret = apiKeyStorage()->requestKey("netatmo").data("clientSecret");
|
|
} else {
|
|
qCDebug(dcNetatmo()) << "Using custom client id and secret from plugin configuration.";
|
|
}
|
|
|
|
if (clientId.isEmpty() || clientSecret.isEmpty()) {
|
|
qCWarning(dcNetatmo()) << "No API key installed. Please install a valid api key provider plugin.";
|
|
return false;
|
|
} else {
|
|
qCDebug(dcNetatmo()) << "Using API client secret and key from API key provider";
|
|
}
|
|
|
|
m_clientId = clientId;
|
|
m_clientSecret = clientSecret;
|
|
|
|
qCDebug(dcNetatmo()) << "API client ID" << NetatmoConnection::censorDebugOutput(m_clientId);
|
|
qCDebug(dcNetatmo()) << "API client secret" << NetatmoConnection::censorDebugOutput(m_clientSecret);
|
|
|
|
return true;
|
|
}
|