diff --git a/senec/integrationpluginsenec.cpp b/senec/integrationpluginsenec.cpp index 908cdaac..03544dde 100644 --- a/senec/integrationpluginsenec.cpp +++ b/senec/integrationpluginsenec.cpp @@ -37,6 +37,8 @@ #include +#include "senecconnection.h" + IntegrationPluginSenec::IntegrationPluginSenec() { // Testing the convert methods @@ -52,6 +54,8 @@ IntegrationPluginSenec::IntegrationPluginSenec() // QString rawValue = "u8_64"; // 100 // quint8 value = SenecStorageLan::parseUInt8(rawValue); // qCWarning(dcSenec()) << rawValue << value; + + } IntegrationPluginSenec::~IntegrationPluginSenec() @@ -107,7 +111,31 @@ void IntegrationPluginSenec::discoverThings(ThingDiscoveryInfo *info) void IntegrationPluginSenec::startPairing(ThingPairingInfo *info) { - info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter username and password for your SENEC.home account.")); + if (info->thingClassId() == senecConnectionThingClassId) { + qCDebug(dcSenec()) << "Start pairing process ..."; + + SenecConnection *connection = new SenecConnection(hardwareManager()->networkManager(), this); + //m_unfinishedTadoAccounts.insert(info->thingId(), tado); + + connect(info, &ThingPairingInfo::aborted, this, [connection]() { + qCWarning(dcSenec()) << "Thing pairing has been aborted, cleaning up..."; + //m_unfinishedTadoAccounts.remove(info->thingId()); + connection->deleteLater(); + }); + + connect(connection, &SenecConnection::initFinished, info, [info, connection] (bool success) { + if (!success) { + info->finish(Thing::ThingErrorAuthenticationFailure); + return; + } + + qCDebug(dcSenec()) << "Senec server is reachable. Starting the OpenID auth process" << connection->authEndpoint().toString(QUrl::FullyEncodedy); + info->setOAuthUrl(connection->authEndpoint()); + info->finish(Thing::ThingErrorNoError); + }); + + connection->initialize(); + } } void IntegrationPluginSenec::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) diff --git a/senec/integrationpluginsenec.json b/senec/integrationpluginsenec.json index 3916ede0..506e024e 100644 --- a/senec/integrationpluginsenec.json +++ b/senec/integrationpluginsenec.json @@ -40,6 +40,43 @@ } ] }, + { + "id": "290e4abb-d000-43a4-9e23-e298e5c36707", + "name": "senecConnection", + "displayName": "SENEC connection", + "interfaces": ["account"], + "createMethods": ["user"], + "setupMethod": "oauth", + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "1180576a-1de2-4815-b442-877b572ce586", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + }, + { + "id": "ff40d5c7-3095-4b3e-9e10-4c0774336764", + "name": "loggedIn", + "displayName": "Logged in", + "displayNameEvent": "Logged in changed", + "defaultValue": true, + "type": "bool" + }, + { + "id": "5d3b2396-6528-47c2-b5a4-f751531bccea", + "name": "userDisplayName", + "displayName": "User name", + "displayNameEvent": "User name changed", + "defaultValue": "", + "type": "QString" + } + ] + }, { "id": "345df3b1-d411-4db5-bbb2-3b14eb86c1ba", "name": "senecStorageLan", diff --git a/senec/senec.pro b/senec/senec.pro index 5d1de98f..5c09c426 100644 --- a/senec/senec.pro +++ b/senec/senec.pro @@ -5,12 +5,14 @@ QT+= network SOURCES += \ integrationpluginsenec.cpp \ senecaccount.cpp \ + senecconnection.cpp \ senecdiscovery.cpp \ senecstoragelan.cpp HEADERS += \ integrationpluginsenec.h \ senecaccount.h \ + senecconnection.h \ senecdiscovery.h \ senecstoragelan.h diff --git a/senec/senecconnection.cpp b/senec/senecconnection.cpp new file mode 100644 index 00000000..e3b18300 --- /dev/null +++ b/senec/senecconnection.cpp @@ -0,0 +1,184 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "senecconnection.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include + +SenecConnection::SenecConnection(NetworkAccessManager *networkManager, QObject *parent) + : QObject{parent}, + m_networkManager{networkManager} +{ + m_configUrl = QUrl("https://sso.senec.com/realms/senec/.well-known/openid-configuration"); + m_systemsUrl = QUrl("https://senec-app-systems-proxy.prod.senec.dev"); + m_measurementsUrl = QUrl("https://senec-app-measurements-proxy.prod.senec.dev"); + + m_clientId = QString("endcustomer-app-frontend"); + m_redirectUrl =/* QString("http://127.0.0.1:8888");*/ QUrl("senec-app-auth://keycloak.prod"); + m_scopes << "roles" << "meinsenec" << "openid"; + m_userAgent = QString("nymea (+https://github.com/nymea/nymea)"); +} + +void SenecConnection::initialize() +{ + qCDebug(dcSenec()) << "OpenID: Initialize and get configuration"; + + QNetworkRequest request(m_configUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent.toUtf8()); + + QNetworkReply *reply = m_networkManager->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply, request](){ + + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "OpenID: Failed to get configuration" << request.url().toString() << reply->errorString(); + if (!data.isEmpty()) + qCWarning(dcSenec()) << "OpenID: Response data:" << qUtf8Printable(data); + + emit initFinished(false); + return; + } + + //qCDebug(dcPushNotifications()) << "OAuth2: Request access token response" << qUtf8Printable(data); + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSenec()) << "OpenID: Failed to get configuration. Response data JSON error:" << error.errorString(); + emit initFinished(false); + return; + } + + QVariantMap configMap = jsonDoc.toVariant().toMap(); + m_authEndpoint = QUrl(configMap.value("authorization_endpoint").toString()); + + QUrlQuery queryParams; + queryParams.addQueryItem("client_id", m_clientId); + queryParams.addQueryItem("redirect_uri", m_redirectUrl.toString()); + queryParams.addQueryItem("response_type", "code"); + queryParams.addQueryItem("scope", m_scopes.join("+")); + + // queryParams.addQueryItem("nonce", QUuid::createUuid().toString()); + // m_codeChallenge = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + + // queryParams.addQueryItem("code_challenge", m_codeChallenge); + // queryParams.addQueryItem("code_challenge_method", "S256"); + m_authEndpoint.setQuery(queryParams); + emit initFinished(true); + }); +} + +QUrl SenecConnection::authEndpoint() const +{ + return m_authEndpoint; +} + +// void SenecConnection::login(const QString &username, const QString &password) +// { +// Q_UNUSED(username, password) +// QNetworkRequest request(m_configUrl); +// request.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent.toUtf8()); + +// QNetworkReply *reply = m_networkManager->get(request); +// connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); +// connect(reply, &QNetworkReply::finished, this, [this, reply, request](){ + +// QByteArray data = reply->readAll(); +// if (reply->error() != QNetworkReply::NoError) { +// qCWarning(dcSenec()) << "OpenID: Failed to get configuration" << request.url().toString() << reply->errorString(); +// if (!data.isEmpty()) +// qCWarning(dcSenec()) << "OpenID: Response data:" << qUtf8Printable(data); + +// // setAuthenticated(false); + +// // // Retry in 30 seconds, maybe we are not online yet +// // m_refreshTimer.start(60 * 1000); +// return; +// } + +// //qCDebug(dcPushNotifications()) << "OAuth2: Request access token response" << qUtf8Printable(data); + +// QJsonParseError error; +// QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); +// if (error.error != QJsonParseError::NoError) { +// qCWarning(dcSenec()) << "OpenID: Failed to get configuration. Response data JSON error:" << error.errorString(); +// //setAuthenticated(false); +// return; +// } + +// QVariantMap configMap = jsonDoc.toVariant().toMap(); +// m_authEndpoint = QUrl(configMap.value("authorization_endpoint").toString()); + + + +// // QUrl url(m_baseTokenUrl); +// // QUrlQuery query; +// // query.clear(); +// // query.addQueryItem("grant_type", "refresh_token"); +// // query.addQueryItem("refresh_token", refreshToken); +// // query.addQueryItem("client_secret", m_clientSecret); + +// // QNetworkRequest request(m_authEndpoint); +// // request.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent.toUtf8()); +// // request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + +// // // QUrlQuery queryParams; +// // // queryParams.addQueryItem("client_id", m_clientId); +// // // queryParams.addQueryItem("redirect_uri", m_redirectUrl); +// // // queryParams.addQueryItem("response_type", "code"); +// // // queryParams.addQueryItem("scope", m_scopes.join(" ")); +// // // queryParams.addQueryItem("nonce", QUuid::createUuid().toString()); +// // // m_codeChallenge = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + +// // // queryParams.addQueryItem("code_challenge", m_codeChallenge); +// // // queryParams.addQueryItem("code_challenge_method", "S256"); +// // // url.setQuery(queryParams); + + +// // // QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); +// // // connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); +// // // connect(reply, &QNetworkReply::finished, this, [this, reply](){ + +// // redirect_uri: REDIRECT_URI, +// // scope: SCOPE, +// // code_challenge:, +// // code_challenge_method: 'S256', + + +// }); + +// }); +// } diff --git a/senec/senecconnection.h b/senec/senecconnection.h new file mode 100644 index 00000000..8d717464 --- /dev/null +++ b/senec/senecconnection.h @@ -0,0 +1,72 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SENECCONNECTION_H +#define SENECCONNECTION_H + +#include +#include + +#include + +class SenecConnection : public QObject +{ + Q_OBJECT +public: + explicit SenecConnection(NetworkAccessManager *networkManager, QObject *parent = nullptr); + + void initialize(); + + QUrl authEndpoint() const; + + // void login(const QString &username, const QString &password); + +signals: + void initFinished(bool success); + +private: + NetworkAccessManager *m_networkManager = nullptr; + + QUrl m_configUrl; + QUrl m_systemsUrl; + QUrl m_measurementsUrl; + + QString m_clientId; + QUrl m_redirectUrl; + QStringList m_scopes; + QString m_userAgent; + + // Fetch from config url + QUrl m_authEndpoint; + QString m_codeChallenge; + +}; + +#endif // SENECCONNECTION_H diff --git a/usbrly82/usbrly82.pro b/usbrly82/usbrly82.pro index 0c808803..b9cc891e 100644 --- a/usbrly82/usbrly82.pro +++ b/usbrly82/usbrly82.pro @@ -1,6 +1,6 @@ include(../plugins.pri) -QT *= serialport +QT += serialport PKGCONFIG += libudev