From 8f72d83369fd8952a9cbb625468547d71904598f 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 V2 and V3 energy storages --- debian-qt5/control | 11 + debian-qt5/nymea-plugin-senec.install.in | 2 + debian-qt6/nymea-plugin-senec.install.in | 2 + nymea-plugins.pro | 1 + senec/README.md | 11 + senec/integrationpluginsenec.cpp | 259 +++++++++++++ senec/integrationpluginsenec.h | 75 ++++ senec/integrationpluginsenec.json | 232 ++++++++++++ senec/meta.json | 13 + senec/senec.pro | 14 + senec/senec.svg | 40 ++ senec/senecdiscovery.cpp | 112 ++++++ senec/senecdiscovery.h | 79 ++++ senec/senecstoragelan.cpp | 351 ++++++++++++++++++ senec/senecstoragelan.h | 112 ++++++ ...3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts | 189 ++++++++++ ...55ea5-a883-445d-9556-675f5eda6c9a-en_US.ts | 189 ++++++++++ 17 files changed, 1692 insertions(+) create mode 100644 debian-qt5/nymea-plugin-senec.install.in create mode 100644 debian-qt6/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/senecdiscovery.cpp create mode 100644 senec/senecdiscovery.h create mode 100644 senec/senecstoragelan.cpp create mode 100644 senec/senecstoragelan.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-qt5/control b/debian-qt5/control index 49515d2f..99eb27e1 100644 --- a/debian-qt5/control +++ b/debian-qt5/control @@ -714,6 +714,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}, @@ -724,6 +725,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-qt5/nymea-plugin-senec.install.in b/debian-qt5/nymea-plugin-senec.install.in new file mode 100644 index 00000000..ef2a33b4 --- /dev/null +++ b/debian-qt5/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/debian-qt6/nymea-plugin-senec.install.in b/debian-qt6/nymea-plugin-senec.install.in new file mode 100644 index 00000000..ef2a33b4 --- /dev/null +++ b/debian-qt6/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 459ef528..0d32c124 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -56,6 +56,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..eba44c7e --- /dev/null +++ b/senec/README.md @@ -0,0 +1,11 @@ +# SENEC.Home + +Connects to SENEC.Home energy storages in the local network and integrates them into the system. Optionally the grid meter of the storage can be added. + +Currently supported and tested models: + +* SENEC.Home P4 + +## More + +https://senec.com diff --git a/senec/integrationpluginsenec.cpp b/senec/integrationpluginsenec.cpp new file mode 100644 index 00000000..bdfa0b7d --- /dev/null +++ b/senec/integrationpluginsenec.cpp @@ -0,0 +1,259 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "senecdiscovery.h" +#include "plugininfo.h" + +#include + +IntegrationPluginSenec::IntegrationPluginSenec() +{ + // Testing the convert methods + + // QString rawValue = "fl_42A2E5E4"; // 81.449 + // float value = SenecStorageLan::parseFloat(rawValue); + // qCWarning(dcSenec()) << rawValue << value; + + // QString rawValue = "st_foobar"; // foobar + // QString value = SenecStorageLan::parseString(rawValue); + // qCWarning(dcSenec()) << rawValue << value; + + // QString rawValue = "u8_64"; // 100 + // quint8 value = SenecStorageLan::parseUInt8(rawValue); + // qCWarning(dcSenec()) << rawValue << value; +} + +IntegrationPluginSenec::~IntegrationPluginSenec() +{ + +} + +void IntegrationPluginSenec::discoverThings(ThingDiscoveryInfo *info) +{ + if (info->thingClassId() == senecStorageLanThingClassId) { + + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcSenec()) << "Failed to discover network devices. The network device discovery is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network.")); + return; + } + + qCInfo(dcSenec()) << "Starting network discovery..."; + SenecDiscovery *discovery = new SenecDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &SenecDiscovery::discoveryFinished, info, [=](){ + ThingDescriptors descriptors; + qCInfo(dcSenec()) << "Discovery finished. Found" << discovery->results().count() << "devices"; + foreach (const SenecDiscovery::Result &result, discovery->results()) { + qCInfo(dcSenec()) << "Discovered device on" << result.networkDeviceInfo; + + QString title = "SENEC connection (" + result.deviceId + ")"; + QString description = result.networkDeviceInfo.address().toString(); + + ThingDescriptor descriptor(senecStorageLanThingClassId, title, description); + + ParamList params; + params << Param(senecStorageLanThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress()); + params << Param(senecStorageLanThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName()); + params << Param(senecStorageLanThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress()); + descriptor.setParams(params); + + // Check if we already have set up this device + Thing *existingThing = myThings().findByParams(params); + if (existingThing) { + qCDebug(dcSenec()) << "This thing already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + + discovery->startDiscovery(); + } +} + +void IntegrationPluginSenec::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcSenec()) << "Setting up" << thing->name() << thing->params(); + + if (thing->thingClassId() == senecStorageLanThingClassId) { + + // Handle reconfigure + if (m_monitors.contains(thing)) { + qCDebug(dcSenec()) << "Unregister existing monitor and recreate a new one..."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + + if (m_storages.contains(thing)) + m_storages.take(thing)->deleteLater(); + + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); + m_monitors.insert(thing, monitor); + + SenecStorageLan *storage = new SenecStorageLan(hardwareManager()->networkManager(), this); + m_storages.insert(thing, storage); + + storage->setAddress(monitor->networkDeviceInfo().address()); + connect(monitor, &NetworkDeviceMonitor::reachableChanged, storage, [monitor, storage](bool reachable){ + if (reachable) { + storage->setAddress(monitor->networkDeviceInfo().address()); + } + }); + + connect(storage, &SenecStorageLan::availableChanged, thing, [this, thing](bool available){ + setConnected(thing, available); + }); + + connect(storage, &SenecStorageLan::initializeFinished, thing, [storage, thing](bool success){ + if (success) { + thing->setStateValue(senecStorageLanCapacityStateTypeId, storage->capacity()); + } + }); + + connect(storage, &SenecStorageLan::updatedFinished, thing, [storage, thing, this](bool success){ + setConnected(thing, storage->available()); + + if (!success) + return; + + int batteryLevel = qRound(storage->batteryLevel()); + thing->setStateValue(senecStorageLanBatteryLevelStateTypeId, batteryLevel); + thing->setStateValue(senecStorageLanBatteryCriticalStateTypeId, batteryLevel < 10); + thing->setStateValue(senecStorageLanCurrentPowerStateTypeId, storage->batteryPower()); + if (storage->batteryPower() > 0) { + thing->setStateValue(senecStorageLanChargingStateStateTypeId, "charging"); + } else if (storage->batteryPower() < 0) { + thing->setStateValue(senecStorageLanChargingStateStateTypeId, "discharging"); + } else { + thing->setStateValue(senecStorageLanChargingStateStateTypeId, "idle"); + } + + Thing *meter = meterThing(thing); + if (meter) { + meter->setStateValue(senecMeterCurrentPowerStateTypeId, storage->gridPower()); + } + }); + + connect(thing, &Thing::settingChanged, this, [this, thing](const ParamTypeId ¶mTypeId, const QVariant &value){ + if (paramTypeId == senecStorageLanSettingsAddMeterParamTypeId) { + + if (value.toBool()) { + // Check if we have to add the meter + if (!meterThing(thing)) { + qCDebug(dcSenec()) << "Add meter for" << thing->name(); + emit autoThingsAppeared(ThingDescriptors() << ThingDescriptor(senecMeterThingClassId, "SENEC Meter", QString(), thing->id())); + } + } else { + // Check if we have to remove the meter + Thing *meter = meterThing(thing); + if (meter) { + qCDebug(dcSenec()) << "Remove meter thing for" << thing->name(); + emit autoThingDisappeared(meter->id()); + } + } + } + }); + + info->finish(Thing::ThingErrorNoError); + + } else if (thing->thingClassId() == senecMeterThingClassId) { + Thing *parentThing = myThings().findById(thing->parentId()); + if (parentThing) { + thing->setStateValue(senecMeterConnectedStateTypeId, parentThing->stateValue(senecStorageLanConnectedStateTypeId).toBool()); + } + + info->finish(Thing::ThingErrorNoError); + } +} + +void IntegrationPluginSenec::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == senecStorageLanThingClassId) { + m_storages.value(thing)->initialize(); + } + + // 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, &IntegrationPluginSenec::refresh); + m_refreshTimer->start(); + } +} + +void IntegrationPluginSenec::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == senecStorageLanThingClassId) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + m_storages.take(thing)->deleteLater(); + } + + if (myThings().isEmpty()) { + qCDebug(dcSenec()) << "Stopping refresh timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +Thing *IntegrationPluginSenec::meterThing(Thing *storageThing) const +{ + Things meterThings = myThings().filterByThingClassId(senecMeterThingClassId).filterByParentId(storageThing->id()); + if (meterThings.isEmpty()) + return nullptr; + + return meterThings.first(); +} + +void IntegrationPluginSenec::setConnected(Thing *storageThing, bool connected) +{ + storageThing->setStateValue(senecStorageLanConnectedStateTypeId, connected); + + Thing *meter = meterThing(storageThing); + if (meter) { + meter->setStateValue(senecMeterConnectedStateTypeId, connected); + } +} + +void IntegrationPluginSenec::refresh() +{ + foreach (Thing *storageThing, myThings().filterByThingClassId(senecStorageLanThingClassId)) { + SenecStorageLan *storage = m_storages.value(storageThing); + // Retry fetching the static information until the storage could be initialized once + if (storage->initialized()) { + storage->update(); + } else { + storage->initialize(); + } + } +} diff --git a/senec/integrationpluginsenec.h b/senec/integrationpluginsenec.h new file mode 100644 index 00000000..6a45f905 --- /dev/null +++ b/senec/integrationpluginsenec.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +#include + +#include "extern-plugininfo.h" + +#include "senecstoragelan.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 discoverThings(ThingDiscoveryInfo *info) override; + + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +private: + PluginTimer *m_refreshTimer = nullptr; + + QHash m_monitors; + QHash m_storages; + + Thing *meterThing(Thing *storageThing) const; + void setConnected(Thing *storageThing, bool connected); + +private slots: + void refresh(); + +}; + +#endif // INTEGRATIONPLUGINSENEC_H diff --git a/senec/integrationpluginsenec.json b/senec/integrationpluginsenec.json new file mode 100644 index 00000000..0bf83dbe --- /dev/null +++ b/senec/integrationpluginsenec.json @@ -0,0 +1,232 @@ +{ + "displayName": "SENEC", + "name": "Senec", + "id": "3f055ea5-a883-445d-9556-675f5eda6c9a", + "vendors": [ + { + "displayName": "SENEC", + "name": "senec", + "id": "1be922b9-4439-42cf-9caa-67f2ac7cc425", + "thingClasses": [ + { + "id": "345df3b1-d411-4db5-bbb2-3b14eb86c1ba", + "name": "senecStorageLan", + "displayName": "SENEC.Home storage", + "interfaces": ["energystorage", "networkdevice"], + "createMethods": ["Discovery", "User"], + "settingsTypes": [ + { + "id": "239d388c-f4a2-4055-88c3-decc6dace2b8", + "name": "addMeter", + "displayName": "Add meter", + "type": "bool", + "defaultValue": false + } + ], + "paramTypes": [ + { + "id": "a45cabe9-9f76-405e-b47c-0a3d00bdd44b", + "name": "address", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "51a1bbc0-28a5-43ac-9021-896acbf9413f", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "0790e1b1-c8c9-488f-9cda-2a0bbf14b4af", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "readOnly": true, + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "d3bdc64b-67fb-4d84-97ed-c5142e5db55d", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "3b00470d-11bc-4352-a885-52ab9443cee5", + "name": "currentPower", + "displayName": "Current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "cached": true + }, + { + "id": "66a3a7d2-1210-4891-a8f2-6fe4f9b8c6cc", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "a9aa13fb-3f51-40e3-9d29-ab882513a87a", + "name": "batteryCritical", + "displayName": "Battery critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "339f602f-c9f1-446d-bc96-1c7f9a7db922", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "d927e6cf-29f0-412f-b74d-7d803fd3e191", + "name": "capacity", + "displayName": "Capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + }, + { + "name": "senecMeter", + "displayName": "SENEC Meter", + "id": "303f454d-93b7-4f8f-b295-a4c22e09be6d", + "createMethods": [ "auto" ], + "interfaces": ["energymeter", "connectable"], + "stateTypes": [ + { + "id": "9aa4dbfb-255b-408b-b148-8fb145ba8c98", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "56e58d03-782d-40bb-8d32-fad396ffefdc", + "name": "currentPower", + "displayName": "Current power usage", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "f9920cfa-26a0-4247-ad50-b4fca91596f0", + "name": "totalEnergyProduced", + "displayName": "Energy returned", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "21728bf0-a566-4eb0-a163-5f108f34bb9b", + "name": "totalEnergyConsumed", + "displayName": "Energy consumed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "385df15e-4f1d-4842-a10a-597d087bdac0", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "3bab2b05-99d7-4b3a-b903-054d269a3527", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "01a63c94-b635-4e98-8037-d5d69fe0dd7c", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "db469573-bb11-48d2-84ba-2b8ff4b13413", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "176d5a01-220d-44da-b875-9a40aaa9c85a", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "e7ab02d3-016a-44ca-8da1-5d7ac9acbc6d", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "1923f7b0-5b54-46d8-a7e9-9364b7c9eeb8", + "name": "currentPhaseA", + "displayName": "Current phase A", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "4908abf8-f655-4e14-a007-3eefeda7088e", + "name": "currentPhaseB", + "displayName": "Current phase B", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "e8415152-0fac-4cd9-98ac-b0e7c006d637", + "name": "currentPhaseC", + "displayName": "Current phase C", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + } + ] + } + ] + } + ] +} diff --git a/senec/meta.json b/senec/meta.json new file mode 100644 index 00000000..46766d5a --- /dev/null +++ b/senec/meta.json @@ -0,0 +1,13 @@ +{ + "title": "SENEC", + "tagline": "Integrate SENEC.Home energy storages from the local network.", + "icon": "senec.svg", + "stability": "community", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/senec/senec.pro b/senec/senec.pro new file mode 100644 index 00000000..90c5d150 --- /dev/null +++ b/senec/senec.pro @@ -0,0 +1,14 @@ +include(../plugins.pri) + +QT+= network + +SOURCES += \ + integrationpluginsenec.cpp \ + senecdiscovery.cpp \ + senecstoragelan.cpp + +HEADERS += \ + integrationpluginsenec.h \ + senecdiscovery.h \ + senecstoragelan.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/senecdiscovery.cpp b/senec/senecdiscovery.cpp new file mode 100644 index 00000000..9166653a --- /dev/null +++ b/senec/senecdiscovery.cpp @@ -0,0 +1,112 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "senecdiscovery.h" +#include "extern-plugininfo.h" + +SenecDiscovery::SenecDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) + : QObject{parent}, + m_networkManager{networkManager}, + m_networkDeviceDiscovery{networkDeviceDiscovery} +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + finishDiscovery(); + }); +} + +void SenecDiscovery::startDiscovery() +{ + qCInfo(dcSenec()) << "Discovery: Searching SENEC energy storages in the local network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &SenecDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcSenec()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + m_gracePeriodTimer.start(); + discoveryReply->deleteLater(); + }); +} + +QList SenecDiscovery::results() const +{ + return m_results; +} + +void SenecDiscovery::checkNetworkDevice(const QHostAddress &address) +{ + qCDebug(dcSenec()) << "Discovery: Verifying" << address; + + SenecStorageLan *storage = new SenecStorageLan(m_networkManager, address, this); + m_storages.append(storage); + + connect(storage, &SenecStorageLan::initializeFinished, this, [this, storage, address](bool success){ + if (!success) { + qCDebug(dcSenec()) << "Discovery: Failed to initialize host" << address.toString() << "Continue ..."; + cleanupStorage(storage); + return; + } + + // Successfully initialized + Result result; + result.deviceId = storage->deviceId(); + result.address = address; + m_results.append(result); + + qCInfo(dcSenec()) << "Discovery: Found SENEC storage on" << address.toString() << storage->deviceId(); + cleanupStorage(storage); + }); + + storage->initialize(); +} + +void SenecDiscovery::cleanupStorage(SenecStorageLan *storage) +{ + m_storages.removeAll(storage); + storage->deleteLater(); +} + +void SenecDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Fill in all network device infos we have + for (int i = 0; i < m_results.count(); i++) + m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address); + + qCInfo(dcSenec()) << "Discovery: Finished the discovery process. Found" << m_results.count() + << "SENEC devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); +} diff --git a/senec/senecdiscovery.h b/senec/senecdiscovery.h new file mode 100644 index 00000000..6bfd99d6 --- /dev/null +++ b/senec/senecdiscovery.h @@ -0,0 +1,79 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 SENECDISCOVERY_H +#define SENECDISCOVERY_H + +#include +#include + +#include +#include + +#include "senecstoragelan.h" + +class SenecDiscovery : public QObject +{ + Q_OBJECT +public: + explicit SenecDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + + typedef struct Result { + QString deviceId; + QHostAddress address; + NetworkDeviceInfo networkDeviceInfo; + } Result; + + void startDiscovery(); + + QList results() const; + +signals: + void discoveryFinished(); + +private: + NetworkAccessManager *m_networkManager = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + NetworkDeviceInfos m_networkDeviceInfos; + QList m_storages; + QList m_results; + + void checkNetworkDevice(const QHostAddress &address); + void cleanupStorage(SenecStorageLan *storage); + + void finishDiscovery(); + +}; + +#endif // SENECDISCOVERY_H diff --git a/senec/senecstoragelan.cpp b/senec/senecstoragelan.cpp new file mode 100644 index 00000000..6c914ed0 --- /dev/null +++ b/senec/senecstoragelan.cpp @@ -0,0 +1,351 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "senecstoragelan.h" +#include "extern-plugininfo.h" + +#include +#include +#include + +SenecStorageLan::SenecStorageLan(NetworkAccessManager *networkManager, QObject *parent) + : QObject{parent}, + m_networkManager{networkManager} +{ + +} + +SenecStorageLan::SenecStorageLan(NetworkAccessManager *networkManager, const QHostAddress &address, QObject *parent) + : QObject{parent}, + m_networkManager{networkManager}, + m_address{address} +{ + updateUrl(); +} + +QHostAddress SenecStorageLan::address() const +{ + return m_address; +} + +void SenecStorageLan::setAddress(const QHostAddress &address) +{ + m_address = address; + updateUrl(); +} + +QUrl SenecStorageLan::url() const +{ + return m_url; +} + +bool SenecStorageLan::available() const +{ + return m_available; +} + +bool SenecStorageLan::initialized() const +{ + return m_initialized; +} + +QString SenecStorageLan::deviceId() const +{ + return m_deviceId; +} + +float SenecStorageLan::capacity() const +{ + return m_capacity; +} + +float SenecStorageLan::maxChargePower() const +{ + return m_maxChargePower; +} + +float SenecStorageLan::maxDischargePower() const +{ + return m_maxDischargePower; +} + +float SenecStorageLan::batteryLevel() const +{ + return m_batteryLevel; +} + +float SenecStorageLan::batteryPower() const +{ + return m_batteryPower; +} + +float SenecStorageLan::gridPower() const +{ + return m_gridPower; +} + +float SenecStorageLan::inverterPower() const +{ + return m_inverterPower; +} + +float SenecStorageLan::parseFloat(const QString &value) +{ + if (!value.startsWith("fl_")) { + qCWarning(dcSenec()) << "The given value does not seem to be a float, it is not starting with fl_" << value; + return 0; + } + + QByteArray rawData = QByteArray::fromHex(value.mid(3).toUtf8()); + if (rawData.size() != 4) { + qCWarning(dcSenec()) << "Invalid raw data size for converting value to float32" << value; + return 0; + } + + float result = 0; + quint32 dataValue = 0; + QDataStream stream(rawData); + stream >> dataValue; + memcpy(&result, &dataValue, sizeof(quint32)); + return result; +} + +QString SenecStorageLan::parseString(const QString &value) +{ + if (!value.startsWith("st_")) { + qCWarning(dcSenec()) << "The given value does not seem to be a string, it is not starting with st_" << value; + return QString(); + } + + return value.mid(3); +} + +// quint8 SenecStorageLan::parseUInt8(const QString &value) +// { +// Q_ASSERT_X(value.left(3) == "u8_", "SenecStorageLan", "The given value does not seem to be a uint8, it is not starting with u8_"); + +// } + + +void SenecStorageLan::initialize() +{ + if (!m_url.isValid()) { + qCWarning(dcSenec()) << "Cannot initialize the storage. The request URL is not valid. Maybe the IP is not known yet or invalid."; + emit initializeFinished(false); + return; + } + + // Verify the debug request is working + QVariantMap request; + request.insert("DEBUG", QVariantMap()); + + QNetworkReply *reply = m_networkManager->post(QNetworkRequest(m_url), QJsonDocument::fromVariant(request).toJson()); + connect(reply, &QNetworkReply::sslErrors, this, &SenecStorageLan::ignoreSslErrors); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Debug request finished with error. Status:" << status << "Error:" << reply->errorString(); + setAvailable(false); + emit initializeFinished(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()) << "Debug request finished successfully, but the response contains invalid JSON object:" << responseData; + setAvailable(false); + emit initializeFinished(false); + return; + } + + // Verify content, if the content is valid, we assume we have found a SENEC storage + if (!responseMap.contains("DEBUG")) { + qCWarning(dcSenec()) << "Unexpected reponse data from debug request. Aborting..."; + setAvailable(false); + emit initializeFinished(false); + return; + } + + qCDebug(dcSenec()) << "Debug request finished successfully" << qUtf8Printable(jsonDoc.toJson()); + + // Request basic information + + QVariantMap request; + QVariantMap factoryMap; + factoryMap.insert("DEVICE_ID", QString()); + factoryMap.insert("DESIGN_CAPACITY", QString()); + factoryMap.insert("MAX_CHARGE_POWER_DC", QString()); + factoryMap.insert("MAX_DISCHARGE_POWER_DC", QString()); + request.insert("FACTORY", factoryMap); + + QNetworkReply *reply = m_networkManager->post(QNetworkRequest(m_url), QJsonDocument::fromVariant(request).toJson()); + connect(reply, &QNetworkReply::sslErrors, this, &SenecStorageLan::ignoreSslErrors); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply] { + + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Factory request finished with error. Status:" << status << "Error:" << reply->errorString(); + setAvailable(false); + emit initializeFinished(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()) << "Factory request finished successfully, but the response contains invalid JSON object:" << responseData; + setAvailable(false); + emit initializeFinished(false); + return; + } + + qCDebug(dcSenec()) << "Factory request finished successfully" << qUtf8Printable(jsonDoc.toJson()); + + QVariantMap factoryResponseMap = responseMap.value("FACTORY").toMap(); + m_deviceId = parseString(factoryResponseMap.value("DEVICE_ID").toString()); + m_capacity = parseFloat(factoryResponseMap.value("DESIGN_CAPACITY").toString()); + m_maxChargePower = parseFloat(factoryResponseMap.value("MAX_CHARGE_POWER_DC").toString()); + m_maxDischargePower = parseFloat(factoryResponseMap.value("MAX_DISCHARGE_POWER_DC").toString()); + + m_initialized = true; + emit initializeFinished(true); + setAvailable(true); + + qCDebug(dcSenec()) << "Initialized successfully"; + }); + }); +} + +void SenecStorageLan::update() +{ + if (!m_url.isValid()) { + qCDebug(dcSenec()) << "Cannot update the storage. The request URL is not valid. Maybe the IP is not known yet or invalid."; + emit updatedFinished(false); + return; + } + + QVariantMap request; + + QVariantMap energyMap; + energyMap.insert("GUI_BAT_DATA_POWER", QString()); + energyMap.insert("GUI_INVERTER_POWER", QString()); + energyMap.insert("GUI_BAT_DATA_FUEL_CHARGE", QString()); + energyMap.insert("GUI_HOUSE_POW", QString()); + energyMap.insert("GUI_GRID_POW", QString()); + + request.insert("ENERGY", energyMap); + + QNetworkReply *reply = m_networkManager->post(QNetworkRequest(m_url), QJsonDocument::fromVariant(request).toJson()); + connect(reply, &QNetworkReply::sslErrors, this, &SenecStorageLan::ignoreSslErrors); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "Update request finished with error. Status:" << status << "Error:" << reply->errorString(); + setAvailable(false); + emit updatedFinished(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()) << "Update request finished successfully, but the response contains invalid JSON object:" << responseData; + setAvailable(false); + emit updatedFinished(false); + return; + } + + qCDebug(dcSenec()) << "Update request finished successfully" << qUtf8Printable(jsonDoc.toJson()); + + QVariantMap energyResponseMap = responseMap.value("ENERGY").toMap(); + m_batteryPower = parseFloat(energyResponseMap.value("GUI_BAT_DATA_POWER").toString()); + m_batteryLevel = parseFloat(energyResponseMap.value("GUI_BAT_DATA_FUEL_CHARGE").toString()); + m_gridPower = parseFloat(energyResponseMap.value("GUI_GRID_POW").toString()); + m_inverterPower = parseFloat(energyResponseMap.value("GUI_INVERTER_POWER").toString()); + + setAvailable(true); + + qCDebug(dcSenec()).nospace().noquote() << "Update values: Battery power: " << m_batteryPower + << "W, Battery level: " << m_batteryLevel + << "%, Grid power: " << m_gridPower + << "W, Inverter power: " << m_inverterPower << "W"; + + emit updatedFinished(true); + }); +} + +void SenecStorageLan::updateUrl() +{ + QUrl url; + if (m_address.isNull()) { + m_url = url; + return; + } + + url.setScheme("https"); + url.setHost(m_address.toString()); + url.setPath("/lala.cgi"); + m_url = url; +} + +void SenecStorageLan::setAvailable(bool available) +{ + if (m_available == available) + return; + + m_available = available; + emit availableChanged(m_available); +} + +void SenecStorageLan::ignoreSslErrors(const QList &errors) +{ + qCDebug(dcSenec()) << "SSL errors occurred" << errors << "but we are ignoring them in the LAN environment..."; + QNetworkReply *reply = qobject_cast(sender()); + reply->ignoreSslErrors(); +} diff --git a/senec/senecstoragelan.h b/senec/senecstoragelan.h new file mode 100644 index 00000000..e594d047 --- /dev/null +++ b/senec/senecstoragelan.h @@ -0,0 +1,112 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 SENECSTORAGELAN_H +#define SENECSTORAGELAN_H + +#include +#include +#include + +#include + +// https://blog.odenthal.cc/query-the-hidden-api-of-your-senec-photovoltaik-appliance/ + +class SenecStorageLan : public QObject +{ + Q_OBJECT +public: + + explicit SenecStorageLan(NetworkAccessManager *networkManager, QObject *parent = nullptr); + explicit SenecStorageLan(NetworkAccessManager *networkManager, const QHostAddress &address, QObject *parent = nullptr); + + QHostAddress address() const; + void setAddress(const QHostAddress &address); + + QUrl url() const; + + bool available() const; + bool initialized() const; + + // Init properties + QString deviceId() const; + float capacity() const; + float maxChargePower() const; + float maxDischargePower() const; + + // Update properties + float batteryLevel() const; + float batteryPower() const; + float gridPower() const; + float inverterPower() const; + + + static float parseFloat(const QString &value); // fl_ + static QString parseString(const QString &value); // st_ + //static quint8 parseUInt8(const QString &value); // u8_ + + +public slots: + void initialize(); + void update(); + +signals: + void initializeFinished(bool success); + void availableChanged(bool available); + void updatedFinished(bool success); + +private: + NetworkAccessManager *m_networkManager = nullptr; + + QUrl m_url; + QHostAddress m_address; + + bool m_available = false; + bool m_initialized = false; + + QString m_deviceId; + float m_capacity = 0; + float m_maxChargePower = 0; + float m_maxDischargePower = 0; + + float m_batteryLevel = 0; + float m_batteryPower = 0; + float m_gridPower = 0; + float m_inverterPower = 0; + + void updateUrl(); + void setAvailable(bool available); + +private slots: + void ignoreSslErrors(const QList &errors); + +}; + +#endif // SENECSTORAGELAN_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..f8061430 --- /dev/null +++ b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts @@ -0,0 +1,189 @@ + + + + + IntegrationPluginSenec + + + Unable to discover devices in your network. + Geräte im Netzwerk können nicht gesucht werden. + + + + Senec + + + Add meter + The name of the ParamType (ThingClass: senecStorageLan, Type: settings, ID: {239d388c-f4a2-4055-88c3-decc6dace2b8}) + Zähler hinzufügen + + + + Battery critical + The name of the StateType ({a9aa13fb-3f51-40e3-9d29-ab882513a87a}) of ThingClass senecStorageLan + Batterieladung kritisch + + + + Battery level + The name of the StateType ({66a3a7d2-1210-4891-a8f2-6fe4f9b8c6cc}) of ThingClass senecStorageLan + Ladestand + + + + Capacity + The name of the StateType ({d927e6cf-29f0-412f-b74d-7d803fd3e191}) of ThingClass senecStorageLan + Kapazität + + + + Charging state + The name of the StateType ({339f602f-c9f1-446d-bc96-1c7f9a7db922}) of ThingClass senecStorageLan + Ladezustand + + + + + Connected + The name of the StateType ({9aa4dbfb-255b-408b-b148-8fb145ba8c98}) of ThingClass senecMeter +---------- +The name of the StateType ({d3bdc64b-67fb-4d84-97ed-c5142e5db55d}) of ThingClass senecStorageLan + Verbunden + + + + Current phase A + The name of the StateType ({1923f7b0-5b54-46d8-a7e9-9364b7c9eeb8}) of ThingClass senecMeter + Strom Phase A + + + + Current phase B + The name of the StateType ({4908abf8-f655-4e14-a007-3eefeda7088e}) of ThingClass senecMeter + Strom Phase B + + + + Current phase C + The name of the StateType ({e8415152-0fac-4cd9-98ac-b0e7c006d637}) of ThingClass senecMeter + Strom Phase C + + + + Current power + The name of the StateType ({3b00470d-11bc-4352-a885-52ab9443cee5}) of ThingClass senecStorageLan + Aktuelle Leistung + + + + Current power phase A + The name of the StateType ({385df15e-4f1d-4842-a10a-597d087bdac0}) of ThingClass senecMeter + Leistung Phase A + + + + Current power phase B + The name of the StateType ({3bab2b05-99d7-4b3a-b903-054d269a3527}) of ThingClass senecMeter + Leistung Phase B + + + + Current power phase C + The name of the StateType ({01a63c94-b635-4e98-8037-d5d69fe0dd7c}) of ThingClass senecMeter + Leistung Phase C + + + + Current power usage + The name of the StateType ({56e58d03-782d-40bb-8d32-fad396ffefdc}) of ThingClass senecMeter + Aktuelle Leistung + + + + Energy consumed + The name of the StateType ({21728bf0-a566-4eb0-a163-5f108f34bb9b}) of ThingClass senecMeter + Bezogene Energie + + + + Energy returned + The name of the StateType ({f9920cfa-26a0-4247-ad50-b4fca91596f0}) of ThingClass senecMeter + Eingespeiste Energie + + + + Host address + The name of the ParamType (ThingClass: senecStorageLan, Type: thing, ID: {a45cabe9-9f76-405e-b47c-0a3d00bdd44b}) + IP-Adresse + + + + Host name + The name of the ParamType (ThingClass: senecStorageLan, Type: thing, ID: {51a1bbc0-28a5-43ac-9021-896acbf9413f}) + Hostname + + + + MAC address + The name of the ParamType (ThingClass: senecStorageLan, Type: thing, ID: {0790e1b1-c8c9-488f-9cda-2a0bbf14b4af}) + MAC-Adresse + + + + SENEC Meter + The name of the ThingClass ({303f454d-93b7-4f8f-b295-a4c22e09be6d}) + SENEC Zähler + + + + Voltage phase A + The name of the StateType ({db469573-bb11-48d2-84ba-2b8ff4b13413}) of ThingClass senecMeter + Spannung Phase A + + + + Voltage phase B + The name of the StateType ({176d5a01-220d-44da-b875-9a40aaa9c85a}) of ThingClass senecMeter + Spannung Phase B + + + + Voltage phase C + The name of the StateType ({e7ab02d3-016a-44ca-8da1-5d7ac9acbc6d}) of ThingClass senecMeter + Spannung Phase C + + + + + SENEC + The name of the vendor ({1be922b9-4439-42cf-9caa-67f2ac7cc425}) +---------- +The name of the plugin Senec ({3f055ea5-a883-445d-9556-675f5eda6c9a}) + SENEC + + + + SENEC.Home storage + The name of the ThingClass ({345df3b1-d411-4db5-bbb2-3b14eb86c1ba}) + SENEC.Home Speicher + + + + charging + The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan + lade + + + + discharging + The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan + entlade + + + + idle + The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan + 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..56f9b800 --- /dev/null +++ b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts @@ -0,0 +1,189 @@ + + + + + IntegrationPluginSenec + + + Unable to discover devices in your network. + + + + + Senec + + + Add meter + The name of the ParamType (ThingClass: senecStorageLan, Type: settings, ID: {239d388c-f4a2-4055-88c3-decc6dace2b8}) + + + + + Battery critical + The name of the StateType ({a9aa13fb-3f51-40e3-9d29-ab882513a87a}) of ThingClass senecStorageLan + + + + + Battery level + The name of the StateType ({66a3a7d2-1210-4891-a8f2-6fe4f9b8c6cc}) of ThingClass senecStorageLan + + + + + Capacity + The name of the StateType ({d927e6cf-29f0-412f-b74d-7d803fd3e191}) of ThingClass senecStorageLan + + + + + Charging state + The name of the StateType ({339f602f-c9f1-446d-bc96-1c7f9a7db922}) of ThingClass senecStorageLan + + + + + + Connected + The name of the StateType ({9aa4dbfb-255b-408b-b148-8fb145ba8c98}) of ThingClass senecMeter +---------- +The name of the StateType ({d3bdc64b-67fb-4d84-97ed-c5142e5db55d}) of ThingClass senecStorageLan + + + + + Current phase A + The name of the StateType ({1923f7b0-5b54-46d8-a7e9-9364b7c9eeb8}) of ThingClass senecMeter + + + + + Current phase B + The name of the StateType ({4908abf8-f655-4e14-a007-3eefeda7088e}) of ThingClass senecMeter + + + + + Current phase C + The name of the StateType ({e8415152-0fac-4cd9-98ac-b0e7c006d637}) of ThingClass senecMeter + + + + + Current power + The name of the StateType ({3b00470d-11bc-4352-a885-52ab9443cee5}) of ThingClass senecStorageLan + + + + + Current power phase A + The name of the StateType ({385df15e-4f1d-4842-a10a-597d087bdac0}) of ThingClass senecMeter + + + + + Current power phase B + The name of the StateType ({3bab2b05-99d7-4b3a-b903-054d269a3527}) of ThingClass senecMeter + + + + + Current power phase C + The name of the StateType ({01a63c94-b635-4e98-8037-d5d69fe0dd7c}) of ThingClass senecMeter + + + + + Current power usage + The name of the StateType ({56e58d03-782d-40bb-8d32-fad396ffefdc}) of ThingClass senecMeter + + + + + Energy consumed + The name of the StateType ({21728bf0-a566-4eb0-a163-5f108f34bb9b}) of ThingClass senecMeter + + + + + Energy returned + The name of the StateType ({f9920cfa-26a0-4247-ad50-b4fca91596f0}) of ThingClass senecMeter + + + + + Host address + The name of the ParamType (ThingClass: senecStorageLan, Type: thing, ID: {a45cabe9-9f76-405e-b47c-0a3d00bdd44b}) + + + + + Host name + The name of the ParamType (ThingClass: senecStorageLan, Type: thing, ID: {51a1bbc0-28a5-43ac-9021-896acbf9413f}) + + + + + MAC address + The name of the ParamType (ThingClass: senecStorageLan, Type: thing, ID: {0790e1b1-c8c9-488f-9cda-2a0bbf14b4af}) + + + + + SENEC Meter + The name of the ThingClass ({303f454d-93b7-4f8f-b295-a4c22e09be6d}) + + + + + Voltage phase A + The name of the StateType ({db469573-bb11-48d2-84ba-2b8ff4b13413}) of ThingClass senecMeter + + + + + Voltage phase B + The name of the StateType ({176d5a01-220d-44da-b875-9a40aaa9c85a}) of ThingClass senecMeter + + + + + Voltage phase C + The name of the StateType ({e7ab02d3-016a-44ca-8da1-5d7ac9acbc6d}) of ThingClass senecMeter + + + + + + SENEC + The name of the vendor ({1be922b9-4439-42cf-9caa-67f2ac7cc425}) +---------- +The name of the plugin Senec ({3f055ea5-a883-445d-9556-675f5eda6c9a}) + + + + + SENEC.Home storage + The name of the ThingClass ({345df3b1-d411-4db5-bbb2-3b14eb86c1ba}) + + + + + charging + The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan + + + + + discharging + The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan + + + + + idle + The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan + + + +