diff --git a/debian-qt6/control b/debian-qt6/control index 5fc5822c..7afe3668 100644 --- a/debian-qt6/control +++ b/debian-qt6/control @@ -678,6 +678,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/senec/README.md b/senec/README.md index eba44c7e..77c43380 100644 --- a/senec/README.md +++ b/senec/README.md @@ -1,10 +1,20 @@ # 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. +Integrates SENEC.Home energy storages into the system. -Currently supported and tested models: +## Local network (SENEC.Home V2 / V3) -* SENEC.Home P4 +The storage is discovered in the local network and polled directly. Optionally the grid meter of the storage can be added using the "Add meter" setting. + +## SENEC.Connect (SENEC.Home 4, P4, E4) + +V4 systems do not provide a local API and are integrated using the official read-only SENEC.Connect cloud API. + +1. Sign in on https://developer.senec.com with your SENEC account. +2. Subscribe to the SENEC.Connect product (SENEC.Data 45000). +3. Copy the subscription key from your profile and enter it when adding the SENEC.Connect thing. + +All systems registered to the account will appear automatically, each with a grid meter and a PV inverter. The data is updated once per minute due to the request limit of the API. The energy totals of the meter and the inverter are calculated from the power values. ## More diff --git a/senec/integrationpluginsenec.cpp b/senec/integrationpluginsenec.cpp index bdfa0b7d..5d193240 100644 --- a/senec/integrationpluginsenec.cpp +++ b/senec/integrationpluginsenec.cpp @@ -102,6 +102,49 @@ void IntegrationPluginSenec::discoverThings(ThingDiscoveryInfo *info) } } +void IntegrationPluginSenec::startPairing(ThingPairingInfo *info) +{ + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please sign in on developer.senec.com with your SENEC account, subscribe to the SENEC.Connect product and enter the subscription key from your profile.")); +} + +void IntegrationPluginSenec::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + Q_UNUSED(username) + + QString subscriptionKey = secret.trimmed(); + if (subscriptionKey.isEmpty()) { + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Please enter a subscription key.")); + return; + } + + qCDebug(dcSenec()) << "Verifying SENEC.Connect subscription key" << subscriptionKey.left(4) + QString(subscriptionKey.length() - 4, '*'); + + SenecConnect *senecConnect = new SenecConnect(hardwareManager()->networkManager(), subscriptionKey, info); + QNetworkReply *reply = senecConnect->getDeviceData(); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [this, reply, info, subscriptionKey] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status == 401 || status == 403) { + qCWarning(dcSenec()) << "SENEC.Connect rejected the subscription key. Status:" << status; + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("The subscription key is not valid.")); + return; + } + + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "SENEC.Connect request finished with error. Status:" << status << "Error:" << reply->errorString(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to connect to SENEC.Connect. Please try again later.")); + return; + } + + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("subscriptionKey", subscriptionKey); + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); + }); +} + void IntegrationPluginSenec::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); @@ -159,7 +202,7 @@ void IntegrationPluginSenec::setupThing(ThingSetupInfo *info) thing->setStateValue(senecStorageLanChargingStateStateTypeId, "idle"); } - Thing *meter = meterThing(thing); + Thing *meter = childThing(thing, senecMeterThingClassId); if (meter) { meter->setStateValue(senecMeterCurrentPowerStateTypeId, storage->gridPower()); } @@ -170,13 +213,13 @@ void IntegrationPluginSenec::setupThing(ThingSetupInfo *info) if (value.toBool()) { // Check if we have to add the meter - if (!meterThing(thing)) { + if (!childThing(thing, senecMeterThingClassId)) { 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); + Thing *meter = childThing(thing, senecMeterThingClassId); if (meter) { qCDebug(dcSenec()) << "Remove meter thing for" << thing->name(); emit autoThingDisappeared(meter->id()); @@ -187,10 +230,41 @@ void IntegrationPluginSenec::setupThing(ThingSetupInfo *info) info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == senecConnectThingClassId) { + + pluginStorage()->beginGroup(thing->id().toString()); + QString subscriptionKey = pluginStorage()->value("subscriptionKey").toString(); + pluginStorage()->endGroup(); + + if (subscriptionKey.isEmpty()) { + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("The SENEC.Connect subscription key is missing. Please reconfigure the connection.")); + return; + } + + // Handle reconfigure + if (m_connects.contains(thing)) + m_connects.take(thing)->deleteLater(); + + m_connects.insert(thing, new SenecConnect(hardwareManager()->networkManager(), subscriptionKey, this)); + info->finish(Thing::ThingErrorNoError); + + } else if (thing->thingClassId() == senecStorageCloudThingClassId) { + info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == senecMeterThingClassId) { Thing *parentThing = myThings().findById(thing->parentId()); - if (parentThing) { + if (parentThing && parentThing->thingClassId() == senecStorageLanThingClassId) { thing->setStateValue(senecMeterConnectedStateTypeId, parentThing->stateValue(senecStorageLanConnectedStateTypeId).toBool()); + } else if (parentThing && parentThing->thingClassId() == senecStorageCloudThingClassId) { + thing->setStateValue(senecMeterConnectedStateTypeId, parentThing->stateValue(senecStorageCloudConnectedStateTypeId).toBool()); + } + + info->finish(Thing::ThingErrorNoError); + + } else if (thing->thingClassId() == senecInverterThingClassId) { + Thing *parentThing = myThings().findById(thing->parentId()); + if (parentThing) { + thing->setStateValue(senecInverterConnectedStateTypeId, parentThing->stateValue(senecStorageCloudConnectedStateTypeId).toBool()); } info->finish(Thing::ThingErrorNoError); @@ -201,14 +275,37 @@ 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(); + if (!m_refreshTimer) { + qCDebug(dcSenec()) << "Starting LAN refresh timer ..."; + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(5); + connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSenec::refresh); + m_refreshTimer->start(); + } + + } else if (thing->thingClassId() == senecConnectThingClassId) { + refreshCloud(thing); + + // SENEC.Connect allows 45000 requests per month, which is roughly one request per minute + if (!m_cloudRefreshTimer) { + qCDebug(dcSenec()) << "Starting SENEC.Connect refresh timer ..."; + m_cloudRefreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); + connect(m_cloudRefreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSenec::refreshClouds); + m_cloudRefreshTimer->start(); + } + + } else if (thing->thingClassId() == senecStorageCloudThingClassId) { + ThingDescriptors descriptors; + if (!childThing(thing, senecMeterThingClassId)) + descriptors.append(ThingDescriptor(senecMeterThingClassId, "SENEC Meter", thing->name(), thing->id())); + + if (!childThing(thing, senecInverterThingClassId)) + descriptors.append(ThingDescriptor(senecInverterThingClassId, "SENEC PV inverter", thing->name(), thing->id())); + + if (!descriptors.isEmpty()) { + qCDebug(dcSenec()) << "Adding" << descriptors.count() << "child things for" << thing->name(); + emit autoThingsAppeared(descriptors); + } } } @@ -219,27 +316,44 @@ void IntegrationPluginSenec::thingRemoved(Thing *thing) m_storages.take(thing)->deleteLater(); } - if (myThings().isEmpty()) { - qCDebug(dcSenec()) << "Stopping refresh timer"; + if (thing->thingClassId() == senecConnectThingClassId) { + if (m_connects.contains(thing)) + m_connects.take(thing)->deleteLater(); + + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->remove(""); + pluginStorage()->endGroup(); + } + + m_lastEnergyUpdates.remove(thing); + + if (m_refreshTimer && m_storages.isEmpty()) { + qCDebug(dcSenec()) << "Stopping LAN refresh timer"; hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); m_refreshTimer = nullptr; } + + if (m_cloudRefreshTimer && m_connects.isEmpty()) { + qCDebug(dcSenec()) << "Stopping SENEC.Connect refresh timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_cloudRefreshTimer); + m_cloudRefreshTimer = nullptr; + } } -Thing *IntegrationPluginSenec::meterThing(Thing *storageThing) const +Thing *IntegrationPluginSenec::childThing(Thing *parentThing, const ThingClassId &thingClassId) const { - Things meterThings = myThings().filterByThingClassId(senecMeterThingClassId).filterByParentId(storageThing->id()); - if (meterThings.isEmpty()) + Things childThings = myThings().filterByThingClassId(thingClassId).filterByParentId(parentThing->id()); + if (childThings.isEmpty()) return nullptr; - return meterThings.first(); + return childThings.first(); } void IntegrationPluginSenec::setConnected(Thing *storageThing, bool connected) { storageThing->setStateValue(senecStorageLanConnectedStateTypeId, connected); - Thing *meter = meterThing(storageThing); + Thing *meter = childThing(storageThing, senecMeterThingClassId); if (meter) { meter->setStateValue(senecMeterConnectedStateTypeId, connected); } @@ -257,3 +371,153 @@ void IntegrationPluginSenec::refresh() } } } + +void IntegrationPluginSenec::refreshClouds() +{ + foreach (Thing *connectThing, myThings().filterByThingClassId(senecConnectThingClassId)) { + refreshCloud(connectThing); + } +} + +void IntegrationPluginSenec::refreshCloud(Thing *connectThing) +{ + SenecConnect *senecConnect = m_connects.value(connectThing); + if (!senecConnect) + return; + + QNetworkReply *reply = senecConnect->getDeviceData(); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, connectThing, [this, reply, connectThing] { + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status == 401 || status == 403) { + qCWarning(dcSenec()) << "SENEC.Connect request has been rejected. The subscription key seems to be invalid. Status:" << status; + connectThing->setStateValue(senecConnectLoggedInStateTypeId, false); + setCloudConnected(connectThing, false); + return; + } + + if (status != 200 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcSenec()) << "SENEC.Connect request finished with error. Status:" << status << "Error:" << reply->errorString(); + setCloudConnected(connectThing, false); + return; + } + + QByteArray responseData = reply->readAll(); + qCDebug(dcSenec()) << "SENEC.Connect request finished successfully" << qUtf8Printable(responseData); + + bool ok = false; + QList systems = SenecConnect::parseDeviceData(responseData, &ok); + if (!ok) { + qCWarning(dcSenec()) << "SENEC.Connect request finished successfully, but the response contains invalid data:" << responseData; + setCloudConnected(connectThing, false); + return; + } + + connectThing->setStateValue(senecConnectLoggedInStateTypeId, true); + connectThing->setStateValue(senecConnectConnectedStateTypeId, true); + processCloudData(connectThing, systems); + }); +} + +void IntegrationPluginSenec::processCloudData(Thing *connectThing, const QList &systems) +{ + ThingDescriptors descriptors; + Things storageThings = myThings().filterByThingClassId(senecStorageCloudThingClassId).filterByParentId(connectThing->id()); + + foreach (const SenecConnect::SystemData &system, systems) { + Things existingThings = storageThings.filterByParam(senecStorageCloudThingSystemIdParamTypeId, system.systemId); + if (existingThings.isEmpty()) { + qCDebug(dcSenec()) << "Creating new storage for" << system.systemId << system.model; + ThingDescriptor descriptor(senecStorageCloudThingClassId, system.model.isEmpty() ? "SENEC.Home storage" : system.model, system.systemId, connectThing->id()); + descriptor.setParams(ParamList() << Param(senecStorageCloudThingSystemIdParamTypeId, system.systemId) + << Param(senecStorageCloudThingSerialNumberParamTypeId, system.serialNumber)); + descriptors.append(descriptor); + continue; + } + + Thing *storageThing = existingThings.first(); + Thing *meter = childThing(storageThing, senecMeterThingClassId); + Thing *inverter = childThing(storageThing, senecInverterThingClassId); + + storageThing->setStateValue(senecStorageCloudConnectedStateTypeId, system.active); + if (meter) + meter->setStateValue(senecMeterConnectedStateTypeId, system.active); + + if (inverter) + inverter->setStateValue(senecInverterConnectedStateTypeId, system.active); + + if (!system.active) { + qCDebug(dcSenec()) << "System" << system.systemId << "does not provide live data. Marking as disconnected."; + continue; + } + + int batteryLevel = qRound(system.batteryLevel); + storageThing->setStateValue(senecStorageCloudCapacityStateTypeId, system.capacity); + storageThing->setStateValue(senecStorageCloudBatteryLevelStateTypeId, batteryLevel); + storageThing->setStateValue(senecStorageCloudBatteryCriticalStateTypeId, batteryLevel < 10); + storageThing->setStateValue(senecStorageCloudCurrentPowerStateTypeId, system.batteryPower); + if (system.batteryPower > 0) { + storageThing->setStateValue(senecStorageCloudChargingStateStateTypeId, "charging"); + } else if (system.batteryPower < 0) { + storageThing->setStateValue(senecStorageCloudChargingStateStateTypeId, "discharging"); + } else { + storageThing->setStateValue(senecStorageCloudChargingStateStateTypeId, "idle"); + } + + if (meter) { + updateEnergy(meter, system.gridPower, senecMeterTotalEnergyConsumedStateTypeId, senecMeterTotalEnergyProducedStateTypeId); + meter->setStateValue(senecMeterCurrentPowerStateTypeId, system.gridPower); + } + + if (inverter) { + updateEnergy(inverter, -system.production, StateTypeId(), senecInverterTotalEnergyProducedStateTypeId); + inverter->setStateValue(senecInverterCurrentPowerStateTypeId, -system.production); + } + } + + if (!descriptors.isEmpty()) { + qCDebug(dcSenec()) << "Adding" << descriptors.count() << "new SENEC.Home" << (descriptors.count() > 1 ? "storages" : "storage"); + emit autoThingsAppeared(descriptors); + } +} + +void IntegrationPluginSenec::setCloudConnected(Thing *connectThing, bool connected) +{ + connectThing->setStateValue(senecConnectConnectedStateTypeId, connected); + + foreach (Thing *storageThing, myThings().filterByThingClassId(senecStorageCloudThingClassId).filterByParentId(connectThing->id())) { + storageThing->setStateValue(senecStorageCloudConnectedStateTypeId, connected); + + Thing *meter = childThing(storageThing, senecMeterThingClassId); + if (meter) + meter->setStateValue(senecMeterConnectedStateTypeId, connected); + + Thing *inverter = childThing(storageThing, senecInverterThingClassId); + if (inverter) + inverter->setStateValue(senecInverterConnectedStateTypeId, connected); + } +} + +void IntegrationPluginSenec::updateEnergy(Thing *thing, double currentPower, const StateTypeId &consumedStateTypeId, const StateTypeId &producedStateTypeId) +{ + // The cloud API only provides power values, so the energy totals are integrated between two samples + QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime lastUpdate = m_lastEnergyUpdates.value(thing); + m_lastEnergyUpdates.insert(thing, now); + + // Skip the first sample and gaps (restart, connection loss) instead of extrapolating + if (!lastUpdate.isValid() || lastUpdate.secsTo(now) > 300) + return; + + // Trapezoidal integration using the previous power value still stored in the state + double hoursPassed = lastUpdate.msecsTo(now) / 1000.0 / 3600.0; + double averagePower = (thing->stateValue("currentPower").toDouble() + currentPower) / 2.0; + double energy = qAbs(averagePower) / 1000.0 * hoursPassed; + + if (averagePower > 0 && !consumedStateTypeId.isNull()) { + thing->setStateValue(consumedStateTypeId, thing->stateValue(consumedStateTypeId).toDouble() + energy); + } else if (averagePower < 0 && !producedStateTypeId.isNull()) { + thing->setStateValue(producedStateTypeId, thing->stateValue(producedStateTypeId).toDouble() + energy); + } +} diff --git a/senec/integrationpluginsenec.h b/senec/integrationpluginsenec.h index 6a45f905..719ca83f 100644 --- a/senec/integrationpluginsenec.h +++ b/senec/integrationpluginsenec.h @@ -39,8 +39,11 @@ #include "extern-plugininfo.h" +#include "senecconnect.h" #include "senecstoragelan.h" +#include + class IntegrationPluginSenec : public IntegrationPlugin { Q_OBJECT @@ -54,21 +57,33 @@ public: void discoverThings(ThingDiscoveryInfo *info) override; + 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; private: PluginTimer *m_refreshTimer = nullptr; + PluginTimer *m_cloudRefreshTimer = nullptr; QHash m_monitors; QHash m_storages; + QHash m_connects; + QHash m_lastEnergyUpdates; - Thing *meterThing(Thing *storageThing) const; + Thing *childThing(Thing *parentThing, const ThingClassId &thingClassId) const; void setConnected(Thing *storageThing, bool connected); + void refreshCloud(Thing *connectThing); + void processCloudData(Thing *connectThing, const QList &systems); + void setCloudConnected(Thing *connectThing, bool connected); + void updateEnergy(Thing *thing, double currentPower, const StateTypeId &consumedStateTypeId, const StateTypeId &producedStateTypeId); + private slots: void refresh(); + void refreshClouds(); }; diff --git a/senec/integrationpluginsenec.json b/senec/integrationpluginsenec.json index 0bf83dbe..4885aa7b 100644 --- a/senec/integrationpluginsenec.json +++ b/senec/integrationpluginsenec.json @@ -8,6 +8,32 @@ "name": "senec", "id": "1be922b9-4439-42cf-9caa-67f2ac7cc425", "thingClasses": [ + { + "id": "d064f111-1332-4096-b5de-529d44f72963", + "name": "senecConnect", + "displayName": "SENEC.Connect", + "interfaces": ["account"], + "createMethods": ["User"], + "setupMethod": "EnterPin", + "providedInterfaces": ["energystorage", "energymeter", "solarinverter"], + "stateTypes": [ + { + "id": "ed8ef7d7-bf20-4780-870d-cf7171440952", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "73c4da2a-2167-4b68-8fbf-d03f5b7920a5", + "name": "loggedIn", + "displayName": "Logged in", + "type": "bool", + "defaultValue": false + } + ] + }, { "id": "345df3b1-d411-4db5-bbb2-3b14eb86c1ba", "name": "senecStorageLan", @@ -103,6 +129,83 @@ } ] }, + { + "id": "8de0eeca-a698-4aee-bc67-a586c8f10652", + "name": "senecStorageCloud", + "displayName": "SENEC.Home storage (SENEC.Connect)", + "interfaces": ["energystorage", "connectable"], + "createMethods": ["Auto"], + "paramTypes": [ + { + "id": "9172116e-e513-429b-b9a0-c73d7587bfa8", + "name": "systemId", + "displayName": "System ID", + "type": "QString", + "readOnly": true, + "defaultValue": "" + }, + { + "id": "5d1d61dd-a419-4ba5-992a-91e9a3f34d31", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "readOnly": true, + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "cd7d8a3c-44b4-4517-8aa0-25288d03f19a", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "eccdfa19-b033-445c-b196-ab41bb2e449f", + "name": "currentPower", + "displayName": "Current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "cached": true + }, + { + "id": "500b0df2-61d0-4895-91c7-c8f2616dd93d", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "8805be49-b88b-48b6-a0e3-7b6b251d3299", + "name": "batteryCritical", + "displayName": "Battery critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "8db60ec4-06cd-4fc3-a66e-ced915bf5c11", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "f282b051-a68c-4739-88c7-e5819d8424f9", + "name": "capacity", + "displayName": "Capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + }, { "name": "senecMeter", "displayName": "SENEC Meter", @@ -225,6 +328,40 @@ "cached": false } ] + }, + { + "id": "12a613fb-f33c-48b1-a9a4-caaee1041d25", + "name": "senecInverter", + "displayName": "SENEC PV inverter", + "interfaces": ["solarinverter", "connectable"], + "createMethods": ["Auto"], + "stateTypes": [ + { + "id": "67c15e22-2503-41e3-8318-2ccb973760c4", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "11e72bac-a531-4622-bbd0-da5b638e3257", + "name": "currentPower", + "displayName": "Current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "e5a22d6c-573a-46df-ba92-3d33bd0ad754", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] } ] } diff --git a/senec/meta.json b/senec/meta.json index 46766d5a..977aac22 100644 --- a/senec/meta.json +++ b/senec/meta.json @@ -1,11 +1,12 @@ { "title": "SENEC", - "tagline": "Integrate SENEC.Home energy storages from the local network.", + "tagline": "Integrate SENEC.Home energy storages from the local network or via SENEC.Connect.", "icon": "senec.svg", "stability": "community", - "offline": true, + "offline": false, "technologies": [ - "network" + "network", + "cloud" ], "categories": [ "energy" diff --git a/senec/senec.pro b/senec/senec.pro index 90c5d150..47a39fc6 100644 --- a/senec/senec.pro +++ b/senec/senec.pro @@ -4,11 +4,13 @@ QT+= network SOURCES += \ integrationpluginsenec.cpp \ + senecconnect.cpp \ senecdiscovery.cpp \ senecstoragelan.cpp HEADERS += \ integrationpluginsenec.h \ + senecconnect.h \ senecdiscovery.h \ senecstoragelan.h diff --git a/senec/senecconnect.cpp b/senec/senecconnect.cpp new file mode 100644 index 00000000..b588cd4b --- /dev/null +++ b/senec/senecconnect.cpp @@ -0,0 +1,104 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "senecconnect.h" +#include "extern-plugininfo.h" + +#include +#include +#include + +SenecConnect::SenecConnect(NetworkAccessManager *networkManager, const QString &subscriptionKey, QObject *parent) + : QObject{parent}, + m_networkManager{networkManager}, + m_subscriptionKey{subscriptionKey} +{ + +} + +QUrl SenecConnect::deviceDataUrl() +{ + QUrl url("https://apim-eds-gwc-prod.azure-api.net/senec-connect/v1/systems/device-data/general"); + QUrlQuery query; + query.addQueryItem("include", "battery,meter,bessNameplate"); + url.setQuery(query); + return url; +} + +QList SenecConnect::parseDeviceData(const QByteArray &data, bool *ok) +{ + QList systems; + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isArray()) { + if (ok) + *ok = false; + + return systems; + } + + foreach (const QVariant &systemVariant, jsonDoc.toVariant().toList()) { + QVariantMap systemMap = systemVariant.toMap(); + QVariantMap batteryMap = systemMap.value("battery").toMap(); + QVariantMap meterMap = systemMap.value("meter").toMap(); + QVariantMap nameplateMap = systemMap.value("bessNameplate").toMap(); + + SystemData system; + system.systemId = nameplateMap.value("system_id").toString(); + if (system.systemId.isEmpty()) + continue; + + system.serialNumber = nameplateMap.value("serial_number").toString(); + system.model = nameplateMap.value("model").toString(); + system.capacity = nameplateMap.value("design_capacity").toFloat() / 1000.0; + system.batteryPower = batteryMap.value("power").toFloat(); + system.batteryLevel = batteryMap.value("state_of_charge").toFloat(); + system.gridPower = meterMap.value("grid_power").toFloat(); + system.production = meterMap.value("production").toFloat(); + // Stale systems are still listed, but without live battery measurements + system.active = batteryMap.contains("voltage") || batteryMap.contains("current"); + systems.append(system); + } + + if (ok) + *ok = true; + + return systems; +} + +QNetworkReply *SenecConnect::getDeviceData() +{ + QNetworkRequest request(deviceDataUrl()); + request.setRawHeader("Ocp-Apim-Subscription-Key", m_subscriptionKey.toUtf8()); + request.setRawHeader("Accept", "application/json"); + qCDebug(dcSenec()) << "Get" << request.url().toString(); + return m_networkManager->get(request); +} diff --git a/senec/senecconnect.h b/senec/senecconnect.h new file mode 100644 index 00000000..69e9f179 --- /dev/null +++ b/senec/senecconnect.h @@ -0,0 +1,71 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 SENECCONNECT_H +#define SENECCONNECT_H + +#include +#include +#include + +#include + +// Official read-only SENEC cloud API (V4 systems only): https://developer.senec.com + +class SenecConnect : public QObject +{ + Q_OBJECT +public: + struct SystemData { + QString systemId; + QString serialNumber; + QString model; + float capacity = 0; // kWh + float batteryPower = 0; // W, positive charging + float batteryLevel = 0; // % + float gridPower = 0; // W, positive import + float production = 0; // W + bool active = false; + }; + + explicit SenecConnect(NetworkAccessManager *networkManager, const QString &subscriptionKey, QObject *parent = nullptr); + + static QUrl deviceDataUrl(); + static QList parseDeviceData(const QByteArray &data, bool *ok = nullptr); + + QNetworkReply *getDeviceData(); + +private: + NetworkAccessManager *m_networkManager = nullptr; + QString m_subscriptionKey; + +}; + +#endif // SENECCONNECT_H diff --git a/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts index f8061430..44273b5c 100644 --- a/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts +++ b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-de.ts @@ -8,153 +8,247 @@ Unable to discover devices in your network. Geräte im Netzwerk können nicht gesucht werden. + + + Please sign in on developer.senec.com with your SENEC account, subscribe to the SENEC.Connect product and enter the subscription key from your profile. + Bitte melden Sie sich auf developer.senec.com mit Ihrem SENEC-Konto an, abonnieren Sie das Produkt SENEC.Connect und geben Sie den Abonnementschlüssel aus Ihrem Profil ein. + + + + Please enter a subscription key. + Bitte geben Sie einen Abonnementschlüssel ein. + + + + The subscription key is not valid. + Der Abonnementschlüssel ist ungültig. + + + + Unable to connect to SENEC.Connect. Please try again later. + Verbindung zu SENEC.Connect nicht möglich. Bitte versuchen Sie es später erneut. + + + + The SENEC.Connect subscription key is missing. Please reconfigure the connection. + Der SENEC.Connect-Abonnementschlüssel fehlt. Bitte konfigurieren Sie die Verbindung neu. + 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 + The name of the StateType ({8805be49-b88b-48b6-a0e3-7b6b251d3299}) of ThingClass senecStorageCloud +---------- +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 + The name of the StateType ({500b0df2-61d0-4895-91c7-c8f2616dd93d}) of ThingClass senecStorageCloud +---------- +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 + The name of the StateType ({f282b051-a68c-4739-88c7-e5819d8424f9}) of ThingClass senecStorageCloud +---------- +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 + The name of the StateType ({8db60ec4-06cd-4fc3-a66e-ced915bf5c11}) of ThingClass senecStorageCloud +---------- +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 ({67c15e22-2503-41e3-8318-2ccb973760c4}) of ThingClass senecInverter ---------- -The name of the StateType ({d3bdc64b-67fb-4d84-97ed-c5142e5db55d}) of ThingClass senecStorageLan +The name of the StateType ({9aa4dbfb-255b-408b-b148-8fb145ba8c98}) of ThingClass senecMeter +---------- +The name of the StateType ({cd7d8a3c-44b4-4517-8aa0-25288d03f19a}) of ThingClass senecStorageCloud +---------- +The name of the StateType ({d3bdc64b-67fb-4d84-97ed-c5142e5db55d}) of ThingClass senecStorageLan +---------- +The name of the StateType ({ed8ef7d7-bf20-4780-870d-cf7171440952}) of ThingClass senecConnect 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 + The name of the StateType ({11e72bac-a531-4622-bbd0-da5b638e3257}) of ThingClass senecInverter +---------- +The name of the StateType ({eccdfa19-b033-445c-b196-ab41bb2e449f}) of ThingClass senecStorageCloud +---------- +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 - + + Logged in + The name of the StateType ({73c4da2a-2167-4b68-8fbf-d03f5b7920a5}) of ThingClass senecConnect + Angemeldet + + + 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 - + + SENEC PV inverter + The name of the ThingClass ({12a613fb-f33c-48b1-a9a4-caaee1041d25}) + SENEC PV-Wechselrichter + + + + SENEC.Connect + The name of the ThingClass ({d064f111-1332-4096-b5de-529d44f72963}) + SENEC.Connect + + + + SENEC.Home storage (SENEC.Connect) + The name of the ThingClass ({8de0eeca-a698-4aee-bc67-a586c8f10652}) + SENEC.Home Speicher (SENEC.Connect) + + + + Serial number + The name of the ParamType (ThingClass: senecStorageCloud, Type: thing, ID: {5d1d61dd-a419-4ba5-992a-91e9a3f34d31}) + Seriennummer + + + + System ID + The name of the ParamType (ThingClass: senecStorageCloud, Type: thing, ID: {9172116e-e513-429b-b9a0-c73d7587bfa8}) + System-ID + + + + Total energy produced + The name of the StateType ({e5a22d6c-573a-46df-ba92-3d33bd0ad754}) of ThingClass senecInverter + Gesamte erzeugte Energie + + + 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}) ---------- @@ -162,27 +256,36 @@ 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 + The name of a possible value of StateType {8db60ec4-06cd-4fc3-a66e-ced915bf5c11} of ThingClass senecStorageCloud +---------- +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 + The name of a possible value of StateType {8db60ec4-06cd-4fc3-a66e-ced915bf5c11} of ThingClass senecStorageCloud +---------- +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 + The name of a possible value of StateType {8db60ec4-06cd-4fc3-a66e-ced915bf5c11} of ThingClass senecStorageCloud +---------- +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 index 56f9b800..fc976fb1 100644 --- a/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts +++ b/senec/translations/3f055ea5-a883-445d-9556-675f5eda6c9a-en_US.ts @@ -8,153 +8,247 @@ Unable to discover devices in your network. + + + Please sign in on developer.senec.com with your SENEC account, subscribe to the SENEC.Connect product and enter the subscription key from your profile. + + + + + Please enter a subscription key. + + + + + The subscription key is not valid. + + + + + Unable to connect to SENEC.Connect. Please try again later. + + + + + The SENEC.Connect subscription key is missing. Please reconfigure the connection. + + 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 ({8805be49-b88b-48b6-a0e3-7b6b251d3299}) of ThingClass senecStorageCloud ---------- -The name of the StateType ({d3bdc64b-67fb-4d84-97ed-c5142e5db55d}) of ThingClass senecStorageLan +The name of the StateType ({a9aa13fb-3f51-40e3-9d29-ab882513a87a}) of ThingClass senecStorageLan - + + + Battery level + The name of the StateType ({500b0df2-61d0-4895-91c7-c8f2616dd93d}) of ThingClass senecStorageCloud +---------- +The name of the StateType ({66a3a7d2-1210-4891-a8f2-6fe4f9b8c6cc}) of ThingClass senecStorageLan + + + + + + Capacity + The name of the StateType ({f282b051-a68c-4739-88c7-e5819d8424f9}) of ThingClass senecStorageCloud +---------- +The name of the StateType ({d927e6cf-29f0-412f-b74d-7d803fd3e191}) of ThingClass senecStorageLan + + + + + + Charging state + The name of the StateType ({8db60ec4-06cd-4fc3-a66e-ced915bf5c11}) of ThingClass senecStorageCloud +---------- +The name of the StateType ({339f602f-c9f1-446d-bc96-1c7f9a7db922}) of ThingClass senecStorageLan + + + + + + + + + Connected + The name of the StateType ({67c15e22-2503-41e3-8318-2ccb973760c4}) of ThingClass senecInverter +---------- +The name of the StateType ({9aa4dbfb-255b-408b-b148-8fb145ba8c98}) of ThingClass senecMeter +---------- +The name of the StateType ({cd7d8a3c-44b4-4517-8aa0-25288d03f19a}) of ThingClass senecStorageCloud +---------- +The name of the StateType ({d3bdc64b-67fb-4d84-97ed-c5142e5db55d}) of ThingClass senecStorageLan +---------- +The name of the StateType ({ed8ef7d7-bf20-4780-870d-cf7171440952}) of ThingClass senecConnect + + + + 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 + The name of the StateType ({11e72bac-a531-4622-bbd0-da5b638e3257}) of ThingClass senecInverter +---------- +The name of the StateType ({eccdfa19-b033-445c-b196-ab41bb2e449f}) of ThingClass senecStorageCloud +---------- +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}) - + + Logged in + The name of the StateType ({73c4da2a-2167-4b68-8fbf-d03f5b7920a5}) of ThingClass senecConnect + + + + 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}) - + + SENEC PV inverter + The name of the ThingClass ({12a613fb-f33c-48b1-a9a4-caaee1041d25}) + + + + + SENEC.Connect + The name of the ThingClass ({d064f111-1332-4096-b5de-529d44f72963}) + + + + + SENEC.Home storage (SENEC.Connect) + The name of the ThingClass ({8de0eeca-a698-4aee-bc67-a586c8f10652}) + + + + + Serial number + The name of the ParamType (ThingClass: senecStorageCloud, Type: thing, ID: {5d1d61dd-a419-4ba5-992a-91e9a3f34d31}) + + + + + System ID + The name of the ParamType (ThingClass: senecStorageCloud, Type: thing, ID: {9172116e-e513-429b-b9a0-c73d7587bfa8}) + + + + + Total energy produced + The name of the StateType ({e5a22d6c-573a-46df-ba92-3d33bd0ad754}) of ThingClass senecInverter + + + + 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}) ---------- @@ -162,27 +256,36 @@ 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 + The name of a possible value of StateType {8db60ec4-06cd-4fc3-a66e-ced915bf5c11} of ThingClass senecStorageCloud +---------- +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 + The name of a possible value of StateType {8db60ec4-06cd-4fc3-a66e-ced915bf5c11} of ThingClass senecStorageCloud +---------- +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 + The name of a possible value of StateType {8db60ec4-06cd-4fc3-a66e-ced915bf5c11} of ThingClass senecStorageCloud +---------- +The name of a possible value of StateType {339f602f-c9f1-446d-bc96-1c7f9a7db922} of ThingClass senecStorageLan