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
+
+
+
+