From e9a0fe1f08068af7e1c32b8d3938bb2078888ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 8 Jul 2025 17:08:11 +0200 Subject: [PATCH] New plugin: Senec: Add support for SENEC.home P4 energy storages --- debian/control | 11 + debian/nymea-plugin-senec.install.in | 2 + nymea-plugins.pro | 1 + senec/README.md | 11 + senec/integrationpluginsenec.cpp | 328 ++++++++++++++++++ senec/integrationpluginsenec.h | 70 ++++ senec/integrationpluginsenec.json | 115 ++++++ senec/meta.json | 13 + senec/senec.pro | 12 + senec/senec.svg | 40 +++ senec/senecaccount.cpp | 105 ++++++ senec/senecaccount.h | 70 ++++ ...3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts | 116 +++++++ ...55ea5-a883-445d-9556-675f5eda6c9a-en_US.ts | 116 +++++++ 14 files changed, 1010 insertions(+) create mode 100644 debian/nymea-plugin-senec.install.in create mode 100644 senec/README.md create mode 100644 senec/integrationpluginsenec.cpp create mode 100644 senec/integrationpluginsenec.h create mode 100644 senec/integrationpluginsenec.json create mode 100644 senec/meta.json create mode 100644 senec/senec.pro create mode 100644 senec/senec.svg create mode 100644 senec/senecaccount.cpp create mode 100644 senec/senecaccount.h create mode 100644 senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts create mode 100644 senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts diff --git a/debian/control b/debian/control index 99ef84a8..b29e6afb 100644 --- a/debian/control +++ b/debian/control @@ -703,6 +703,7 @@ Description: nymea integration plugin for TCP commander This package contains the nymea integration plugin for sending arbitrary TCP packets from and to nymea. + Package: nymea-plugin-httpcommander Architecture: any Depends: ${shlibs:Depends}, @@ -713,6 +714,16 @@ Description: nymea integration plugin for HTTP commander HTTP requests from and to nymea. +Package: nymea-plugin-senec +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Conflicts: nymea-plugins-translations (<< 1.0.1) +Description: nymea integration plugin for SENEC.home + This package contains the nymea integration plugin for the SENEC.home + energy storages. + + Package: nymea-plugin-senic Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-senec.install.in b/debian/nymea-plugin-senec.install.in new file mode 100644 index 00000000..ef2a33b4 --- /dev/null +++ b/debian/nymea-plugin-senec.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsenec.so +senec/translations/*qm usr/share/nymea/translations/ diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 5e12641e..c90991ae 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -60,6 +60,7 @@ PLUGIN_DIRS = \ pushbullet \ pushnotifications \ reversessh \ + senec \ senic \ serialportcommander \ sgready \ diff --git a/senec/README.md b/senec/README.md new file mode 100644 index 00000000..78733ec0 --- /dev/null +++ b/senec/README.md @@ -0,0 +1,11 @@ +# SENEC.Home + +Connects to the SENEC cloud and integrates the energy storages into the system. + +Currently supported and tested models: + +* SENEC.Home P4 + +## More + +https://senec.com \ No newline at end of file diff --git a/senec/integrationpluginsenec.cpp b/senec/integrationpluginsenec.cpp new file mode 100644 index 00000000..566c6a44 --- /dev/null +++ b/senec/integrationpluginsenec.cpp @@ -0,0 +1,328 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "integrationpluginsenec.h" +#include "plugininfo.h" + +#include +#include + +IntegrationPluginSenec::IntegrationPluginSenec() +{ + +} + +IntegrationPluginSenec::~IntegrationPluginSenec() +{ + +} + +void IntegrationPluginSenec::startPairing(ThingPairingInfo *info) +{ + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter username and password for your SENEC.home account.")); +} + +void IntegrationPluginSenec::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + qCDebug(dcSenec()) << "Start logging in" << username << secret.left(2) + QString(secret.length() - 2, '*'); + + QVariantMap requestMap; + requestMap.insert("username", username); + requestMap.insert("password", secret); + + QNetworkRequest request(SenecAccount::loginUrl()); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Indented)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [reply, info, username, this] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Login request finished with error. Status:" << status << "Error:" << reply->errorString(); + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Username or password is invalid.")); + return; + } + + // Note: as of now (API 4.4.3) the login seems to return a static token, which does not require any refresh. + // Not as bad as saving user and password on the device, but almost ... + + // https://documenter.getpostman.com/view/932140/2s9YXib2td + + QByteArray responseData = reply->readAll(); + + QJsonParseError jsonError; + QVariantMap responseMap = QJsonDocument::fromJson(responseData, &jsonError).toVariant().toMap(); + if (jsonError.error != QJsonParseError::NoError) { + qCWarning(dcSenec()) << "Login request finished successfully, but the response contains invalid JSON object:" << responseData; + info->finish(Thing::ThingErrorAuthenticationFailure); + return; + } + + if (!responseMap.contains("token") || !responseMap.contains("refreshToken")) { + qCWarning(dcSenec()) << "Login request finished successfully, but the response JSON does not contain the expected properties" << qUtf8Printable(responseData); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + QString token = responseMap.value("token").toString(); + QString refreshToken = responseMap.value("refreshToken").toString(); + + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("username", username); + pluginStorage()->setValue("token", token); + pluginStorage()->setValue("refreshToken", refreshToken); + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginSenec::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcSenec()) << "Setting up" << thing->name() << thing->params(); + + if (thing->thingClassId() == senecAccountThingClassId) { + // Load login information + pluginStorage()->beginGroup(thing->id().toString()); + QString token = pluginStorage()->value("token").toString(); + QString refreshToken = pluginStorage()->value("refreshToken").toString(); + QString username = pluginStorage()->value("username").toString(); + pluginStorage()->endGroup(); + + SenecAccount *account = new SenecAccount(hardwareManager()->networkManager(), username, token, refreshToken, this); + m_accounts.insert(thing, account); + info->finish(Thing::ThingErrorNoError); + + thing->setStateValue(senecAccountUserDisplayNameStateTypeId, username); + + } else if (thing->thingClassId() == senecStorageThingClassId) { + info->finish(Thing::ThingErrorNoError); + } +} + +void IntegrationPluginSenec::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == senecAccountThingClassId) { + + // Search for now things, first poll the + SenecAccount *account = m_accounts.value(thing); + + // Check installation, create things if not already created + QNetworkReply *reply = account->getSystems(); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, account, [reply, thing, this] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Systems request finished with error. Status:" << status << "Error:" << reply->errorString(); + if (status == 401) { + qCWarning(dcSenec()) << "Authentication error, reconfigure and re-login should fix this problem."; + thing->setStateValue(senecAccountLoggedInStateTypeId, false); + } + + thing->setStateValue(senecAccountConnectedStateTypeId, false); + return; + } + + QByteArray responseData = reply->readAll(); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &jsonError); + QVariant responseVariant = jsonDoc.toVariant(); + if (jsonError.error != QJsonParseError::NoError) { + qCWarning(dcSenec()) << "Systems request finished successfully, but the response contains invalid JSON object:" << responseData; + thing->setStateValue(senecAccountConnectedStateTypeId, false); + return; + } + + qCDebug(dcSenec()) << "Systems request finished successfully" << qUtf8Printable(jsonDoc.toJson()); + thing->setStateValue(senecAccountLoggedInStateTypeId, true); + thing->setStateValue(senecAccountConnectedStateTypeId, true); + + ThingDescriptors descriptors; + foreach (const QVariant &installation, responseVariant.toList()) { + QVariantMap installationMap = installation.toMap(); + + // Note: for now we only support V4 systems + if (!installationMap.value("systemType").toString().toLower().contains("v4")) + continue; + + // This is a V4 storage, let's check if we already created one + QString id = installationMap.value("id").toString(); + Things existingThings = myThings().filterByThingClassId(senecStorageThingClassId).filterByParam(senecStorageThingIdParamTypeId, id); + if (existingThings.isEmpty()) { + qCDebug(dcSenec()) << "Creating new storage for" << id; + ThingDescriptor descriptor(senecStorageThingClassId, "SENEC.Home P4", id); + descriptor.setParentId(thing->id()); + ParamList params; + params.append(Param(senecStorageThingIdParamTypeId, id)); + descriptor.setParams(params); + descriptors.append(descriptor); + } else { + qCDebug(dcSenec()) << "Thing for storage" << id << "already created."; + } + } + + if (!descriptors.isEmpty()) { + qCDebug(dcSenec()) << "Adding" << descriptors.count() << "new SENEC.Home" << (descriptors.count() > 1 ? "storages" : "storage"); + emit autoThingsAppeared(descriptors); + } + }); + } else if (thing->thingClassId() == senecStorageThingClassId) { + + SenecAccount *account = m_accounts.value(myThings().findById(thing->parentId())); + QString id = thing->paramValue(senecStorageThingIdParamTypeId).toString(); + QNetworkReply *reply = account->getTechnicalData(id); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, account, [reply, thing, this] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Technical data request finished with error. Status:" << status << "Error:" << reply->errorString(); + thing->setStateValue(senecStorageConnectedStateTypeId, false); + return; + } + + QByteArray responseData = reply->readAll(); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &jsonError); + //QVariantMap responseMap = jsonDoc.toVariant().toMap(); + if (jsonError.error != QJsonParseError::NoError) { + qCWarning(dcSenec()) << "Technical data request finished successfully, but the response contains invalid JSON object:" << responseData; + return; + } + + qCDebug(dcSenec()) << "Technical data request finished successfully" << qUtf8Printable(jsonDoc.toJson()); + thing->setStateValue(senecStorageConnectedStateTypeId, true); + + refresh(thing); + }); + } + + // Create the refresh timer if not already set up + if (!m_refreshTimer) { + qCDebug(dcSenec()) << "Starting refresh timer ..."; + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(5); + connect(m_refreshTimer, &PluginTimer::timeout, this, [this](){ + refresh(); + }); + m_refreshTimer->start(); + } +} + +void IntegrationPluginSenec::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == senecAccountThingClassId) { + if (m_accounts.contains(thing)) + m_accounts.take(thing)->deleteLater(); + + // Wipe any stored login information + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->remove(""); + pluginStorage()->endGroup(); + } + + if (myThings().isEmpty()) { + qCDebug(dcSenec()) << "Stopping refresh timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +void IntegrationPluginSenec::executeAction(ThingActionInfo *info) +{ + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginSenec::refresh(Thing *thing) +{ + if (thing) { + SenecAccount *account = m_accounts.value(myThings().findById(thing->parentId())); + QString id = thing->paramValue(senecStorageThingIdParamTypeId).toString(); + QNetworkReply *reply = account->getDashboard(id); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, account, [reply, thing] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Dashboard request finished with error. Status:" << status << "Error:" << reply->errorString(); + thing->setStateValue(senecStorageConnectedStateTypeId, false); + return; + } + + QByteArray responseData = reply->readAll(); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &jsonError); + QVariantMap responseMap = jsonDoc.toVariant().toMap(); + if (jsonError.error != QJsonParseError::NoError) { + qCWarning(dcSenec()) << "Dashboard request finished successfully, but the response contains invalid JSON object:" << responseData; + return; + } + + qCDebug(dcSenec()) << "Dashboard request finished successfully" << qUtf8Printable(jsonDoc.toJson()); + thing->setStateValue(senecStorageConnectedStateTypeId, true); + + QVariantMap currentDataMap = responseMap.value("currently").toMap(); + float batteryCharge = qRound(currentDataMap.value("batteryChargeInW").toFloat() * 100) / 100.0; + float batteryDischarge = qRound(currentDataMap.value("batteryDischargeInW").toFloat() * 100) / 100.0; + int batteryLevel = currentDataMap.value("batteryLevelInPercent").toInt(); + + // qCDebug(dcSenec()) << "charge:" << batteryCharge << "W" << "discharge:" << batteryDischarge << "W" << "level" << batteryLevel << "%"; + + float currentPower = 0; + + if (batteryCharge != 0) { + currentPower = batteryCharge; + thing->setStateValue(senecStorageChargingStateStateTypeId, "charging"); + } else if (batteryDischarge != 0) { + currentPower = -batteryDischarge; + thing->setStateValue(senecStorageChargingStateStateTypeId, "discharging"); + } else { + thing->setStateValue(senecStorageChargingStateStateTypeId, "idle"); + } + + thing->setStateValue(senecStorageCurrentPowerStateTypeId, currentPower); + thing->setStateValue(senecStorageBatteryLevelStateTypeId, batteryLevel); + thing->setStateValue(senecStorageBatteryCriticalStateTypeId, batteryLevel < 10); + }); + } else { + foreach (Thing *storageThing, myThings().filterByThingClassId(senecStorageThingClassId)) { + refresh(storageThing); + } + } +} diff --git a/senec/integrationpluginsenec.h b/senec/integrationpluginsenec.h new file mode 100644 index 00000000..31440b0f --- /dev/null +++ b/senec/integrationpluginsenec.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 INTEGRATIONPLUGINSENEC_H +#define INTEGRATIONPLUGINSENEC_H + +#include +#include + +#include "extern-plugininfo.h" + +#include "senecaccount.h" + +class IntegrationPluginSenec : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsenec.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginSenec(); + ~IntegrationPluginSenec(); + + void startPairing(ThingPairingInfo *info) override; + void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override; + + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + + void executeAction(ThingActionInfo *info) override; + +private: + PluginTimer *m_refreshTimer = nullptr; + QHash m_accounts; + +private slots: + void refresh(Thing *thing = nullptr); + +}; + +#endif // INTEGRATIONPLUGINSENEC_H diff --git a/senec/integrationpluginsenec.json b/senec/integrationpluginsenec.json new file mode 100644 index 00000000..1ed328b7 --- /dev/null +++ b/senec/integrationpluginsenec.json @@ -0,0 +1,115 @@ +{ + "displayName": "SENEC", + "name": "Senec", + "id": "3f055ea5-a883-445d-9556-675f5eda6c9a", + "vendors": [ + { + "displayName": "SENEC", + "name": "senec", + "id": "1be922b9-4439-42cf-9caa-67f2ac7cc425", + "thingClasses": [ + { + "id": "f60497ea-a15a-4237-86bc-935182475e47", + "name": "senecAccount", + "displayName": "SENEC account", + "interfaces": ["account"], + "createMethods": ["user"], + "setupMethod": "userandpassword", + "providedInterfaces": ["energystorage"], + "stateTypes": [ + { + "id": "2a508d04-3183-4c0b-9e92-90ff1ebce19e", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false + }, + { + "id": "6842e3b3-ff25-4f34-b1c4-ff3f83c23746", + "name": "loggedIn", + "displayName": "Logged in", + "type": "bool", + "defaultValue": false + }, + { + "id": "7670bfb4-5dfc-47f1-9c99-80b120f2aa8b", + "name": "userDisplayName", + "displayName": "Username", + "type": "QString", + "defaultValue": "-" + } + ] + }, + { + "name": "senecStorage", + "displayName": "SENEC.Home storage", + "id": "a983c5ee-1c58-4cad-87fb-1bc612cbe6e4", + "createMethods": [ "Auto" ], + "interfaces": ["energystorage", "connectable"], + "paramTypes": [ + { + "id": "9a0fa7b1-bc35-44d4-b987-5ecb87fd8b00", + "name":"id", + "displayName": "System ID", + "type": "QString", + "readOnly": true, + "defaultValue": "" + } + ], + "stateTypes":[ + { + "id": "320cb3f0-d2c5-4a30-817f-474f0cc87253", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "09225a15-f2d0-46cf-a84f-4a91f389d488", + "name": "currentPower", + "displayName": "Current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "cached": true + }, + { + "id": "d6dd533a-beb2-4f57-a3d3-12c48a83306c", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "cba01bf1-5daf-4466-8ae3-ea864593bdc6", + "name": "batteryCritical", + "displayName": "Battery critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "cef34773-02c2-4eb1-8624-4e595847f674", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "4f571731-85f7-492e-ba51-eb6bf224776f", + "name": "capacity", + "displayName": "Capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + } + ] + } + ] +} diff --git a/senec/meta.json b/senec/meta.json new file mode 100644 index 00000000..3ee6a8e7 --- /dev/null +++ b/senec/meta.json @@ -0,0 +1,13 @@ +{ + "title": "SENEC", + "tagline": "Connect nymea to your SENEC account.", + "icon": "senec.svg", + "stability": "community", + "offline": false, + "technologies": [ + "cloud" + ], + "categories": [ + "energy" + ] +} diff --git a/senec/senec.pro b/senec/senec.pro new file mode 100644 index 00000000..2a1e45fe --- /dev/null +++ b/senec/senec.pro @@ -0,0 +1,12 @@ +include(../plugins.pri) + +QT+= network + +SOURCES += \ + integrationpluginsenec.cpp \ + senecaccount.cpp + +HEADERS += \ + integrationpluginsenec.h \ + senecaccount.h + diff --git a/senec/senec.svg b/senec/senec.svg new file mode 100644 index 00000000..f179ab31 --- /dev/null +++ b/senec/senec.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/senec/senecaccount.cpp b/senec/senecaccount.cpp new file mode 100644 index 00000000..b8aca2c8 --- /dev/null +++ b/senec/senecaccount.cpp @@ -0,0 +1,105 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "senecaccount.h" +#include "extern-plugininfo.h" + +SenecAccount::SenecAccount(NetworkAccessManager *networkManager, const QString &username, const QString &token, const QString &refreshToken, QObject *parent) + : QObject{parent}, + m_networkManager{networkManager}, + m_username{username}, + m_token{token}, + m_refreshToken{refreshToken} +{} + +QUrl SenecAccount::baseUrl() +{ + return QUrl("https://app-gateway.prod.senec.dev"); +} + +QUrl SenecAccount::loginUrl() +{ + QUrl url = SenecAccount::baseUrl(); + url.setPath("/v1/senec/login"); + return url; +} + +QUrl SenecAccount::systemsUrl() +{ + QUrl url = SenecAccount::baseUrl(); + url.setPath("/v1/senec/systems"); + return url; +} + +bool SenecAccount::available() const +{ + return m_available; +} + +QNetworkReply *SenecAccount::getSystems() +{ + QNetworkRequest request(SenecAccount::systemsUrl()); + request.setRawHeader("authorization", m_token.toUtf8()); + return m_networkManager->get(request); +} + +QNetworkReply *SenecAccount::getDashboard(const QString &id) +{ + QUrl url = SenecAccount::baseUrl(); + url.setPath("/v2/senec/systems/" + id +"/dashboard"); + + QNetworkRequest request(url); + request.setRawHeader("authorization", m_token.toUtf8()); + qCDebug(dcSenec()) << "Get" << url.toString(); + return m_networkManager->get(request); +} + +QNetworkReply *SenecAccount::getAbilities(const QString &id) +{ + QUrl url = SenecAccount::baseUrl(); + url.setPath("/v1/senec/systems/" + id +"/abilities"); + + QNetworkRequest request(url); + request.setRawHeader("authorization", m_token.toUtf8()); + qCDebug(dcSenec()) << "Get" << url.toString(); + return m_networkManager->get(request); +} + +QNetworkReply *SenecAccount::getTechnicalData(const QString &id) +{ + QUrl url = SenecAccount::baseUrl(); + url.setPath("/v1/senec/systems/" + id +"/technical-data"); + + QNetworkRequest request(url); + request.setRawHeader("authorization", m_token.toUtf8()); + qCDebug(dcSenec()) << "Get" << url.toString(); + return m_networkManager->get(request); +} + diff --git a/senec/senecaccount.h b/senec/senecaccount.h new file mode 100644 index 00000000..32513195 --- /dev/null +++ b/senec/senecaccount.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 SENECACCOUNT_H +#define SENECACCOUNT_H + +#include +#include +#include + +#include + +class SenecAccount : public QObject +{ + Q_OBJECT +public: + explicit SenecAccount(NetworkAccessManager *networkManager, const QString &username, const QString &token, const QString &refreshToken, QObject *parent = nullptr); + + static QUrl baseUrl(); + static QUrl loginUrl(); + static QUrl systemsUrl(); + + bool available() const; + + QNetworkReply *getSystems(); + QNetworkReply *getDashboard(const QString &id); + QNetworkReply *getAbilities(const QString &id); + QNetworkReply *getTechnicalData(const QString &id); + +signals: + bool availableChanged(bool available); + +private: + NetworkAccessManager *m_networkManager = nullptr; + + QString m_username; + QString m_token; + QString m_refreshToken; + + bool m_available = false; +}; + +#endif // SENECACCOUNT_H diff --git a/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts new file mode 100644 index 00000000..d21218ed --- /dev/null +++ b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts @@ -0,0 +1,116 @@ + + + + + IntegrationPluginSenec + + + Please enter username and password for your SENEC.home account. + Logge dich mit deinem SENEC.home account ein. + + + + Username or password is invalid. + Benutzername oder Password ungültig. + + + + Senec + + + Battery critical + The name of the StateType ({cba01bf1-5daf-4466-8ae3-ea864593bdc6}) of ThingClass senecStorage + Batterieladung kritisch + + + + Battery level + The name of the StateType ({d6dd533a-beb2-4f57-a3d3-12c48a83306c}) of ThingClass senecStorage + Ladestand + + + + Capacity + The name of the StateType ({4f571731-85f7-492e-ba51-eb6bf224776f}) of ThingClass senecStorage + Kapazität + + + + Charging state + The name of the StateType ({cef34773-02c2-4eb1-8624-4e595847f674}) of ThingClass senecStorage + Ladezustand + + + + + Connected + The name of the StateType ({320cb3f0-d2c5-4a30-817f-474f0cc87253}) of ThingClass senecStorage +---------- +The name of the StateType ({2a508d04-3183-4c0b-9e92-90ff1ebce19e}) of ThingClass senecAccount + Verbunden + + + + Current power + The name of the StateType ({09225a15-f2d0-46cf-a84f-4a91f389d488}) of ThingClass senecStorage + Aktuelle Leistung + + + + Logged in + The name of the StateType ({6842e3b3-ff25-4f34-b1c4-ff3f83c23746}) of ThingClass senecAccount + Eingelogged + + + + + SENEC + The name of the vendor ({1be922b9-4439-42cf-9caa-67f2ac7cc425}) +---------- +The name of the plugin Senec ({3f055ea5-a883-445d-9556-675f5eda6c9a}) + SENEC + + + + SENEC account + The name of the ThingClass ({f60497ea-a15a-4237-86bc-935182475e47}) + SENEC Account + + + + SENEC.Home storage + The name of the ThingClass ({a983c5ee-1c58-4cad-87fb-1bc612cbe6e4}) + SENEC.Home Speicher + + + + System ID + The name of the ParamType (ThingClass: senecStorage, Type: thing, ID: {9a0fa7b1-bc35-44d4-b987-5ecb87fd8b00}) + System ID + + + + Username + The name of the StateType ({7670bfb4-5dfc-47f1-9c99-80b120f2aa8b}) of ThingClass senecAccount + Benutzername + + + + charging + The name of a possible value of StateType {cef34773-02c2-4eb1-8624-4e595847f674} of ThingClass senecStorage + lade + + + + discharging + The name of a possible value of StateType {cef34773-02c2-4eb1-8624-4e595847f674} of ThingClass senecStorage + entlade + + + + idle + The name of a possible value of StateType {cef34773-02c2-4eb1-8624-4e595847f674} of ThingClass senecStorage + leerlauf + + + diff --git a/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts new file mode 100644 index 00000000..7e7d98f8 --- /dev/null +++ b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts @@ -0,0 +1,116 @@ + + + + + IntegrationPluginSenec + + + Please enter username and password for your SENEC.home account. + + + + + Username or password is invalid. + + + + + Senec + + + Battery critical + The name of the StateType ({cba01bf1-5daf-4466-8ae3-ea864593bdc6}) of ThingClass senecStorage + + + + + Battery level + The name of the StateType ({d6dd533a-beb2-4f57-a3d3-12c48a83306c}) of ThingClass senecStorage + + + + + Capacity + The name of the StateType ({4f571731-85f7-492e-ba51-eb6bf224776f}) of ThingClass senecStorage + + + + + Charging state + The name of the StateType ({cef34773-02c2-4eb1-8624-4e595847f674}) of ThingClass senecStorage + + + + + + Connected + The name of the StateType ({320cb3f0-d2c5-4a30-817f-474f0cc87253}) of ThingClass senecStorage +---------- +The name of the StateType ({2a508d04-3183-4c0b-9e92-90ff1ebce19e}) of ThingClass senecAccount + + + + + Current power + The name of the StateType ({09225a15-f2d0-46cf-a84f-4a91f389d488}) of ThingClass senecStorage + + + + + Logged in + The name of the StateType ({6842e3b3-ff25-4f34-b1c4-ff3f83c23746}) of ThingClass senecAccount + + + + + + SENEC + The name of the vendor ({1be922b9-4439-42cf-9caa-67f2ac7cc425}) +---------- +The name of the plugin Senec ({3f055ea5-a883-445d-9556-675f5eda6c9a}) + + + + + SENEC account + The name of the ThingClass ({f60497ea-a15a-4237-86bc-935182475e47}) + + + + + SENEC.Home storage + The name of the ThingClass ({a983c5ee-1c58-4cad-87fb-1bc612cbe6e4}) + + + + + System ID + The name of the ParamType (ThingClass: senecStorage, Type: thing, ID: {9a0fa7b1-bc35-44d4-b987-5ecb87fd8b00}) + + + + + Username + The name of the StateType ({7670bfb4-5dfc-47f1-9c99-80b120f2aa8b}) of ThingClass senecAccount + + + + + charging + The name of a possible value of StateType {cef34773-02c2-4eb1-8624-4e595847f674} of ThingClass senecStorage + + + + + discharging + The name of a possible value of StateType {cef34773-02c2-4eb1-8624-4e595847f674} of ThingClass senecStorage + + + + + idle + The name of a possible value of StateType {cef34773-02c2-4eb1-8624-4e595847f674} of ThingClass senecStorage + + + +