diff --git a/.gitignore b/.gitignore index cf32e71..6a27cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,16 @@ moc_* *plugininfo.h *.so build/ +builddir/ +build-*/ + +# Sysroot local pour libnymea-sunspec-dev (extraction du .deb hors installation +# systeme). Artefact de build, jamais versionne. +.sunspec-sysroot/ + +# Documents de conception du chantier Fronius : ils vivent dans hems-system et +# sont copies ici pour travailler. Leur diffusion publique n'est pas tranchee +# (cf. section 5 du contrat), donc ils ne sont pas versionnes dans ce depot. +sunspec/CONTRAT-fronius-sunspec.md +sunspec/brief-agent-plugin.md +sunspec/CLAUDE-nymea-plugin-sunspec.md diff --git a/etm-powersync-plugins-modbus.pro b/etm-powersync-plugins-modbus.pro index 5c90608..4463590 100644 --- a/etm-powersync-plugins-modbus.pro +++ b/etm-powersync-plugins-modbus.pro @@ -7,6 +7,7 @@ PLUGIN_DIRS = \ eastron \ abbb2x \ abbterra \ + sunspec \ v2c diff --git a/sunspec.pri b/sunspec.pri index d39682d..d10c263 100644 --- a/sunspec.pri +++ b/sunspec.pri @@ -1,7 +1,29 @@ QT *= network serialbus -top_srcdir=$$PWD -top_builddir=$$shadowed($$PWD) +# libnymea-sunspec n'est PAS vendoree : on lie contre le paquet +# libnymea-sunspec-dev. Deux resolutions possibles, meme convention que +# modbus.pri / NYMEA_MODBUS_PATH : +# +# 1) paquet installe systeme -> pkg-config nymea-sunspec (defaut) +# 2) sysroot local -> qmake6 NYMEA_SUNSPEC_PATH=/chemin/vers/sysroot +# ou contient usr/include/nymea-sunspec et usr/lib/. +# +# Le paquet 1.15.2 installe les en-tetes des modeles sous +# /home/jenkins/nymea-plugins-modbus/libnymea-sunspec/models/ +# (le chemin de build Jenkins a fuite dans le .deb). Le plugin fait +# #include +# qui ne resout donc pas avec le seul -I. On ajoute le prefixe +# manquant quand il existe, sans casser une installation correcte. -INCLUDEPATH += $$top_srcdir/libnymea-sunspec -LIBS += -L$$top_builddir/libnymea-sunspec/ -lnymea-sunspec1 +isEmpty(NYMEA_SUNSPEC_PATH) { + CONFIG += link_pkgconfig + PKGCONFIG += nymea-sunspec + SUNSPEC_INCLUDEDIR = $$[QT_INSTALL_PREFIX]/include/nymea-sunspec +} else { + SUNSPEC_INCLUDEDIR = $$NYMEA_SUNSPEC_PATH/usr/include/nymea-sunspec + INCLUDEPATH += $$SUNSPEC_INCLUDEDIR + LIBS += -L$$NYMEA_SUNSPEC_PATH/usr/lib/$$QMAKE_HOST.arch-linux-gnu -lnymea-sunspec1 +} + +SUNSPEC_MODELS_WORKAROUND = $$SUNSPEC_INCLUDEDIR/home/jenkins/nymea-plugins-modbus/libnymea-sunspec +exists($$SUNSPEC_MODELS_WORKAROUND/models): INCLUDEPATH += $$SUNSPEC_MODELS_WORKAROUND diff --git a/sunspec/README.md b/sunspec/README.md new file mode 100644 index 0000000..682dd1a --- /dev/null +++ b/sunspec/README.md @@ -0,0 +1,29 @@ +# SunSpec + +Connect nymea to SunSpec devices over Modbus TCP + +## Supported Things + +* SunSpec Inverter + * Model ID 101 & 111 Single phase inverter + * Model ID 102 & 112 Split phase inverter + * Model ID 103 & 113 Three phase inverter +* SunSpec Meter + * Model ID 201 & 211 Single phase meter + * Model ID 202 & 212 Split phase meter + * Model ID 203 & 213 Three phase meter +* SunSpec Storage + * Model ID 124 + + +## Tested connections + +* SolarEdge (with custom battery) + +## Requirements + +* The package 'nymea-plugin-sunspec' must be installed. +* The SunSpec device must support SunSpec over modbus TCP + +## More +https://sunspec.org diff --git a/sunspec/VENDORED.md b/sunspec/VENDORED.md new file mode 100644 index 0000000..958ee22 --- /dev/null +++ b/sunspec/VENDORED.md @@ -0,0 +1,45 @@ +# Vendoring — sunspec + +Copié depuis : ETM/nymea-plugins-modbus (fork de nymea/nymea-plugins-modbus) +Commit source (HEAD au moment de la copie) : 2b18e84 +Dernier commit touchant les sources copiées : bf53a74 + ("Update license text and add SPDX identifier", 2025-11-18) +Base upstream : nymea-plugins-modbus 1.15.2 +Date copie : 2026-07-31 +Nom paquet cible : powersync-plugin-sunspec + +Raison : le chantier Fronius (voir `specs/fronius-sunspec.md`) ajoute un vendor +`fronius` et corrige des anomalies du vendor `sunspec` générique. Ces +changements vivent dans ce dépôt ETM, pas dans le mirror nymea. + +## Ce qui a été copié + +Sources uniquement. Exclus de la copie : `*.o`, `moc_*`, `*.so`, `Makefile`, +`plugininfo.h`, `extern-plugininfo.h`, `moc_predefs.h`, `*.qm` — tous des +artefacts de build régénérés par `qmake6` / `nymea-plugininfocompiler`. + +## Dépendance de build + +`libnymea-sunspec-dev` (paquet `repos.nymea.io`, 1.15.2). `sunspec.pri` le +résout par `pkg-config nymea-sunspec`. + +## Règle absolue + +**`libnymea-sunspec` n'est PAS vendorée et ne doit pas l'être.** Le plugin doit +rester rebasable sur l'upstream nymea : toute modification vit dans `sunspec/`. +Si un changement de la bibliothèque paraît inévitable, s'arrêter et remonter la +question plutôt que de la modifier. + +## Hygiène de commits + +Deux natures de changements, à ne jamais mélanger dans un même commit : + +- **correctifs upstream** (bugs présents dans nymea 1.15.2 — AC-1 à AC-4 de la + spec) : message en anglais, aucune référence à ETM / Fronius / PowerSync, pour + rester proposables en amont ; +- **spécifique ETM** (vendor `fronius`, réglages produit) : commits séparés. + +## SORTIE + +Si nymea intègre en amont les correctifs AC-1..AC-4 et un vendor Fronius +équivalent : rebaser sur la version amont et réduire ce dossier au delta ETM. diff --git a/sunspec/integrationpluginsunspec.cpp b/sunspec/integrationpluginsunspec.cpp new file mode 100644 index 0000000..2ef9d3d --- /dev/null +++ b/sunspec/integrationpluginsunspec.cpp @@ -0,0 +1,1759 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginsunspec.h" +#include "plugininfo.h" + +#include + +#include +#include +#include + +// Inverters +#include +#include +#include +#include +#include +#include + +// Meters +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "sunspecdiscovery.h" +#include "solaredgebattery.h" + +#include + +IntegrationPluginSunSpec::IntegrationPluginSunSpec() +{ + +} + +void IntegrationPluginSunSpec::init() +{ + // SunSpec connection params + m_connectionDiscoverySlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionDiscoverySlaveIdParamTypeId); + m_connectionDiscoverySlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionDiscoverySlaveIdParamTypeId); + + m_connectionMacAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingMacAddressParamTypeId); + m_connectionMacAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingMacAddressParamTypeId); + + m_connectionHostNameParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingHostNameParamTypeId); + m_connectionHostNameParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingHostNameParamTypeId); + + m_connectionAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingAddressParamTypeId); + m_connectionAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingAddressParamTypeId); + + m_connectionPortParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingPortParamTypeId); + m_connectionPortParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingPortParamTypeId); + + m_connectionSlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingSlaveIdParamTypeId); + m_connectionSlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingSlaveIdParamTypeId); + + // Params for sunspec things + m_modelIdParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingModelIdParamTypeId); + m_modelIdParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingModelIdParamTypeId); + m_modelIdParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingModelIdParamTypeId); + m_modelIdParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingModelIdParamTypeId); + m_modelIdParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingModelIdParamTypeId); + m_modelIdParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingModelIdParamTypeId); + m_modelIdParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingModelIdParamTypeId); + + m_modbusAddressParamTypeIds.insert(solarEdgeBatteryThingClassId, solarEdgeBatteryThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingModbusAddressParamTypeId); + m_modbusAddressParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingModbusAddressParamTypeId); + + m_manufacturerParamTypeIds.insert(solarEdgeBatteryThingClassId, solarEdgeBatteryThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingManufacturerParamTypeId); + m_manufacturerParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingManufacturerParamTypeId); + + m_deviceModelParamTypeIds.insert(solarEdgeBatteryThingClassId, solarEdgeBatteryThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingDeviceModelParamTypeId); + m_deviceModelParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingDeviceModelParamTypeId); + + m_serialNumberParamTypeIds.insert(solarEdgeBatteryThingClassId, solarEdgeBatteryThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecSinglePhaseInverterThingClassId, sunspecSinglePhaseInverterThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecSplitPhaseInverterThingClassId, sunspecSplitPhaseInverterThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecThreePhaseInverterThingClassId, sunspecThreePhaseInverterThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecStorageThingClassId, sunspecStorageThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecSinglePhaseMeterThingClassId, sunspecSinglePhaseMeterThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecSplitPhaseMeterThingClassId, sunspecSplitPhaseMeterThingSerialNumberParamTypeId); + m_serialNumberParamTypeIds.insert(sunspecThreePhaseMeterThingClassId, sunspecThreePhaseMeterThingSerialNumberParamTypeId); +} + +void IntegrationPluginSunSpec::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcSunSpec()) << "Failed to discover network devices. The network device discovery is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The discovery is not available.")); + return; + } + + + SunSpecDataPoint::ByteOrder byteOrder = SunSpecDataPoint::ByteOrderLittleEndian; + if (info->thingClassId() == solarEdgeConnectionThingClassId) + byteOrder = SunSpecDataPoint::ByteOrderBigEndian; + + quint16 discoverySlaveId = info->params().paramValue(m_connectionDiscoverySlaveIdParamTypeIds.value(info->thingClassId())).toUInt(); + + SunSpecDiscovery *discovery = new SunSpecDiscovery(hardwareManager()->networkDeviceDiscovery(), discoverySlaveId, byteOrder, info); + // Note: we could add here more + connect(discovery, &SunSpecDiscovery::discoveryFinished, info, [this, info, discovery](){ + foreach (const SunSpecDiscovery::Result &result, discovery->results()) { + + // Extract the manufacturer: we pick the first manufacturer name of the first common model having a manufacturer name for now + QString manufacturer; + if (!result.modelManufacturers.isEmpty()) + manufacturer = result.modelManufacturers.first(); + + qCDebug(dcSunSpec()) << "Found manufacturers on" << result.networkDeviceInfo << result.port; + qCDebug(dcSunSpec()) << "Manufacturers:" << result.modelManufacturers; + qCDebug(dcSunSpec()) << "Picking manufacturer for evaluation:" << manufacturer; + + // Filter for solar edge if we got one here + if (info->thingClassId() == solarEdgeConnectionThingClassId) { + if (!hasManufacturer(result.modelManufacturers, "solaredge") && !hasManufacturer(result.modelManufacturers, "solar edge")) { + // Solar edge...we must have the manufacturer in one common model + continue; + } else { + manufacturer = "SolarEdge"; + } + } else if (info->thingClassId() == sunspecConnectionThingClassId) { + // There are some issues regarding the sunspec implementation of kostal. + // Full support of meter, inverter and storage will be provided in the kostal plugin which makes + // use of the native modbus communication from kostal. + if (hasManufacturer(result.modelManufacturers, "kostal")) { + qCWarning(dcSunSpec()) << "Skipping Kostal manufacturer on SunSpec. Please use the native kostal integration plugin in order to add it to the system."; + continue; + } + } + + QString title; + if (!manufacturer.isEmpty()) { + title = manufacturer + " "; + } + title.append("SunSpec connection"); + + QString description; + MacAddressInfo macInfo; + switch (result.networkDeviceInfo.monitorMode()) { + case NetworkDeviceInfo::MonitorModeMac: + macInfo = result.networkDeviceInfo.macAddressInfos().constFirst(); + description = result.networkDeviceInfo.address().toString(); + if (!macInfo.vendorName().isEmpty()) + description += " - " + result.networkDeviceInfo.macAddressInfos().constFirst().vendorName(); + + break; + case NetworkDeviceInfo::MonitorModeHostName: + description = result.networkDeviceInfo.hostName(); + break; + case NetworkDeviceInfo::MonitorModeIp: + description = "Interface: " + result.networkDeviceInfo.networkInterface().name(); + break; + } + + ThingDescriptor descriptor(info->thingClassId(), title, description); + + ParamList params; + params << Param(m_connectionMacAddressParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.thingParamValueMacAddress()); + params << Param(m_connectionHostNameParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.thingParamValueHostName()); + params << Param(m_connectionAddressParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.thingParamValueAddress()); + params << Param(m_connectionPortParamTypeIds.value(info->thingClassId()), result.port); + params << Param(m_connectionSlaveIdParamTypeIds.value(info->thingClassId()), result.slaveId); + descriptor.setParams(params); + + // Check if we already have set up this device + Thing *existingThing = myThings().findByParams(params); + if (existingThing) { + qCDebug(dcSunSpec()) << "This thing already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + // Discovery done + info->finish(Thing::ThingErrorNoError); + }); + + discovery->startDiscovery(); +} + +void IntegrationPluginSunSpec::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcSunSpec()) << "Setup thing" << thing; + qCDebug(dcSunSpec()) << thing->params(); + + if (thing->thingClassId() == sunspecConnectionThingClassId || + thing->thingClassId() == solarEdgeConnectionThingClassId) { + + // Handle reconfigure + if (m_sunSpecConnections.contains(thing->id())) { + qCDebug(dcSunSpec()) << "Reconfiguring existing thing" << thing->name(); + m_sunSpecConnections.take(thing->id())->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + // Create the monitor + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); + if (!monitor) { + qCWarning(dcSunSpec()) << "Unable to register monitor with the given params" << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Unable to set up the connection with this configuration, please reconfigure the connection.")); + return; + } + + m_monitors.insert(thing, monitor); + + QHostAddress address = monitor->networkDeviceInfo().address(); + if (address.isNull() && info->isInitialSetup()) { + qCWarning(dcSunSpec()) << "Cannot set up thing. The host address is not known and this is an initial setup"; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); + return; + } + + // Clean up in case the setup gets aborted + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcSunSpec()) << "Unregister monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + SunSpecConnection *connection = createConnection(info->thing()); + connect(info, &ThingSetupInfo::aborted, connection, &SunSpecConnection::deleteLater); + + // If this is the first setup, we must connect successfully fo finishing the setup, otherwise we let the monitor do his work + if (info->isInitialSetup()) { + + // Only during setup + connect(connection, &SunSpecConnection::connectedChanged, info, [this, connection, info] (bool connected) { + if (connected) { + connect(connection, &SunSpecConnection::discoveryFinished, info, [this, connection, info] (bool success) { + if (success) { + qCDebug(dcSunSpec()) << "Discovery finished successfully during setup of" << connection << ". Found SunSpec data on base register" << connection->baseRegister(); + m_sunSpecConnections.insert(info->thing()->id(), connection); + info->finish(Thing::ThingErrorNoError); + processDiscoveryResult(info->thing(), connection); + } else { + qCWarning(dcSunSpec()) << "Discovery finished with errors during setup of" << connection; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The SunSpec discovery finished with errors. Please make sure this is a SunSpec device.")); + connection->deleteLater(); + } + }); + + // Perform initial discovery, finish if a valid base register has been found + connection->startDiscovery(); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable); + connection->deleteLater(); + } + }); + + connect(info, &ThingSetupInfo::aborted, connection, &SunSpecConnection::deleteLater); + + if (!connection->connectDevice()) { + qCWarning(dcSunSpec()) << "Error connecting to SunSpec device" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + connection->deleteLater(); + return; + } + + } else { + m_sunSpecConnections.insert(thing->id(), connection); + + // Finish the setup in any case, not the initial setup and the monitor takes care about connecting + info->finish(Thing::ThingErrorNoError); + + if (monitor->reachable()) { + // Thing already reachable...let's continue with the setup + connection->connectDevice(); + } else { + // Wait for the monitor to be ready + qCDebug(dcSunSpec()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "..."; + } + } + + } else if (thing->thingClassId() == sunspecThreePhaseInverterThingClassId || + thing->thingClassId() == sunspecSplitPhaseInverterThingClassId || + thing->thingClassId() == sunspecSinglePhaseInverterThingClassId ) { + + Thing *thing = info->thing(); + uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt(); + int modbusStartRegister = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toInt(); + SunSpecConnection *connection = m_sunSpecConnections.value(thing->parentId()); + if (connection) { + // Get the model from the connection if already available + foreach (SunSpecModel *model, connection->models()) { + if (model->modelId() == modelId && model->modbusStartRegister() == modbusStartRegister) { + connect(model, &SunSpecModel::blockUpdated, this, &IntegrationPluginSunSpec::onInverterBlockUpdated); + m_sunSpecInverters.insert(thing, model); + qCDebug(dcSunSpec()) << "Model initialized successfully for" << thing; + } + } + } + info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == sunspecThreePhaseMeterThingClassId + || thing->thingClassId() == sunspecSplitPhaseMeterThingClassId + || thing->thingClassId() == sunspecSinglePhaseMeterThingClassId ) { + + Thing *thing = info->thing(); + uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt(); + int modbusStartRegister = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toInt(); + SunSpecConnection *connection = m_sunSpecConnections.value(thing->parentId()); + if (connection) { + // Get the model from the connection + foreach (SunSpecModel *model, connection->models()) { + if (model->modelId() == modelId && model->modbusStartRegister() == modbusStartRegister) { + m_sunSpecMeters.insert(thing, model); + connect(model, &SunSpecModel::blockUpdated, this, &IntegrationPluginSunSpec::onMeterBlockUpdated); + qCDebug(dcSunSpec()) << "Model initialized successfully for" << thing; + } + } + } else { + // Note: will be initialized once the connection is available and discovered. + qCDebug(dcSunSpec()) << "Model not available yet for" << thing; + } + + info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == sunspecStorageThingClassId) { + Thing *thing = info->thing(); + uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt(); + int modbusStartRegister = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toInt(); + SunSpecConnection *connection = m_sunSpecConnections.value(thing->parentId()); + if (connection) { + // Get the model from the connection + foreach (SunSpecModel *model, connection->models()) { + if (model->modelId() == modelId && model->modbusStartRegister() == modbusStartRegister) { + m_sunSpecStorages.insert(thing, model); + connect(model, &SunSpecModel::blockUpdated, this, &IntegrationPluginSunSpec::onStorageBlockUpdated); + qCDebug(dcSunSpec()) << "Model initialized successfully for" << thing; + } + } + } else { + // Note: will be initialized once the connection is available and discovered. + qCDebug(dcSunSpec()) << "Model not available yet for" << thing; + } + + info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == solarEdgeBatteryThingClassId) { + + Thing *parentThing = myThings().findById(thing->parentId()); + if (parentThing->setupStatus() == Thing::ThingSetupStatusComplete) { + setupSolarEdgeBattery(info); + } else { + connect(parentThing, &Thing::setupStatusChanged, info, [this, info] { + setupSolarEdgeBattery(info); + }); + } + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(info->thing()->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginSunSpec::postSetupThing(Thing *thing) +{ + qCDebug(dcSunSpec()) << "Post setup thing" << thing->name(); + + if (thing->thingClassId() == solarEdgeConnectionThingClassId) { + SunSpecConnection *connection = m_sunSpecConnections.value(thing->id()); + if (connection) { + searchSolarEdgeBatteries(connection); + } + } + + // Create the refresh timer if not already set up + if (!m_refreshTimer) { + qCDebug(dcSunSpec()) << "Starting refresh timer"; + int refreshTime = configValue(sunSpecPluginUpdateIntervalParamTypeId).toInt(); + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime); + connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSunSpec::onRefreshTimer); + } +} + +void IntegrationPluginSunSpec::thingRemoved(Thing *thing) +{ + qCDebug(dcSunSpec()) << "Thing removed" << thing->name(); + + if (m_sunSpecConnections.contains(thing->id())) { + m_sunSpecConnections.take(thing->id())->deleteLater(); + } else if (m_sunSpecThings.contains(thing)) { + m_sunSpecThings.take(thing)->deleteLater(); + } else if (m_sunSpecInverters.contains(thing)) { + m_sunSpecInverters.remove(thing); + } else if (m_sunSpecMeters.contains(thing)) { + m_sunSpecMeters.remove(thing); + } else if (m_sunSpecStorages.contains(thing)) { + m_sunSpecStorages.remove(thing); + } else { + Q_ASSERT_X(false, "thingRemoved", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } + + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + + if (myThings().isEmpty()) { + qCDebug(dcSunSpec()) << "Stopping refresh timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +void IntegrationPluginSunSpec::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == sunspecStorageThingClassId) { + SunSpecStorageModel *storage = qobject_cast(m_sunSpecStorages.value(thing)); + if (!storage) { + qWarning(dcSunSpec()) << "Could not find sunspec model instance for thing" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (!thing->stateValue(sunspecStorageConnectedStateTypeId).toBool()) { + qWarning(dcSunSpec()) << "Could not execute action for" << thing << "because the SunSpec connection is not connected."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The SunSpec connection is not connected.")); + return; + } + + if (action.actionTypeId() == sunspecStorageGridChargingActionTypeId) { + bool gridCharging = action.param(sunspecStorageGridChargingActionGridChargingParamTypeId).value().toBool(); + QModbusReply *reply = storage->setChaGriSet(gridCharging ? SunSpecStorageModel::ChagrisetGrid : SunSpecStorageModel::ChagrisetPv); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + info->finish(Thing::ThingErrorNoError); + }); + } else if (action.actionTypeId() == sunspecStorageEnableChargingActionTypeId) { + SunSpecStorageModel::Storctl_modFlags controlModeFlags = storage->storCtlMod(); + bool enabled = action.param(sunspecStorageEnableChargingActionEnableChargingParamTypeId).value().toBool(); + controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modCharge, enabled); + QModbusReply *reply = storage->setStorCtlMod(controlModeFlags); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, enabled]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + info->thing()->setStateValue(sunspecStorageEnableChargingStateTypeId, enabled); + info->finish(Thing::ThingErrorNoError); + }); + } else if (action.actionTypeId() == sunspecStorageEnableDischargingActionTypeId) { + SunSpecStorageModel::Storctl_modFlags controlModeFlags = storage->storCtlMod(); + bool enabled = action.param(sunspecStorageEnableDischargingActionEnableDischargingParamTypeId).value().toBool(); + controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modDiScharge, enabled); + QModbusReply *reply = storage->setStorCtlMod(controlModeFlags); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply, enabled]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + info->thing()->setStateValue(sunspecStorageEnableDischargingStateTypeId, enabled); + info->finish(Thing::ThingErrorNoError); + }); + } else if (action.actionTypeId() == sunspecStorageChargingRateActionTypeId) { + QModbusReply *reply = storage->setInWRte(action.param(sunspecStorageChargingRateActionChargingRateParamTypeId).value().toInt()); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + info->finish(Thing::ThingErrorNoError); + }); + } else if (action.actionTypeId() == sunspecStorageDischargingRateActionTypeId) { + QModbusReply *reply = storage->setOutWRte(action.param(sunspecStorageDischargingRateActionDischargingRateParamTypeId).value().toInt()); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(info->thing()->thingClassId().toString()).toUtf8()); + } +} + +Thing *IntegrationPluginSunSpec::getThingForSunSpecModel(uint modelId, uint modbusAddress, const ThingId &parentId) +{ + foreach (Thing *thing, myThings()) { + if (!m_modelIdParamTypeIds.contains(thing->thingClassId())) + continue; + + uint thingModelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toUInt(); + uint thingModbusAddress = thing->paramValue(m_modbusAddressParamTypeIds.value(thing->thingClassId())).toUInt(); + if (thingModelId == modelId && thingModbusAddress == modbusAddress && thing->parentId() == parentId) { + return thing; + } + } + + return nullptr; +} + +bool IntegrationPluginSunSpec::sunspecThingAlreadyAdded(uint modelId, uint modbusAddress, const ThingId &parentId) +{ + return getThingForSunSpecModel(modelId, modbusAddress, parentId)!= nullptr; +} + +void IntegrationPluginSunSpec::processDiscoveryResult(Thing *thing, SunSpecConnection *connection) +{ + qCInfo(dcSunSpec()) << "Processing discovery result from" << thing->name() << connection; + + foreach (SunSpecModel *model, connection->models()) { + Thing *modelThing = getThingForSunSpecModel(model->modelId(), model->modbusStartRegister(), thing->id()); + if (modelThing) { + + // We already set up a thing for this model. Make sure we are actually monitoring and updating it + + qCDebug(dcSunSpec()) << "Found" << modelThing << "for" << model; + + if (modelThing->thingClassId() == sunspecSinglePhaseInverterThingClassId + || modelThing->thingClassId() == sunspecSplitPhaseInverterThingClassId + || modelThing->thingClassId() == sunspecThreePhaseInverterThingClassId) { + + if (!m_sunSpecInverters.contains(modelThing)) { + m_sunSpecInverters.insert(modelThing, model); + connect(model, &SunSpecModel::blockUpdated, this, &IntegrationPluginSunSpec::onInverterBlockUpdated); + qCDebug(dcSunSpec()) << "Model initialized successfully for" << modelThing; + } + } else if (modelThing->thingClassId() == sunspecSinglePhaseMeterThingClassId + || modelThing->thingClassId() == sunspecSplitPhaseMeterThingClassId + || modelThing->thingClassId() == sunspecThreePhaseMeterThingClassId) { + + if (!m_sunSpecMeters.contains(modelThing)) { + m_sunSpecMeters.insert(modelThing, model); + connect(model, &SunSpecModel::blockUpdated, this, &IntegrationPluginSunSpec::onMeterBlockUpdated); + qCDebug(dcSunSpec()) << "Model initialized successfully for" << modelThing; + } + } else if (modelThing->thingClassId() == sunspecStorageThingClassId) { + + if (!m_sunSpecStorages.contains(modelThing)) { + m_sunSpecStorages.insert(modelThing, model); + connect(model, &SunSpecModel::blockUpdated, this, &IntegrationPluginSunSpec::onStorageBlockUpdated); + qCDebug(dcSunSpec()) << "Model initialized successfully for" << modelThing; + } + } + } else { + + // No thing for this model, let's see if we can create one + + switch (model->modelId()) { + case SunSpecModelFactory::ModelIdCommon: + // Skip the common model, we already handled this one for each thing model + break; + case SunSpecModelFactory::ModelIdInverterSinglePhase: + case SunSpecModelFactory::ModelIdInverterSinglePhaseFloat: + autocreateSunSpecModelThing(sunspecSinglePhaseInverterThingClassId, QT_TR_NOOP("Single Phase Inverter"), thing->id(), model); + break; + case SunSpecModelFactory::ModelIdInverterSplitPhase: + case SunSpecModelFactory::ModelIdInverterSplitPhaseFloat: + autocreateSunSpecModelThing(sunspecSplitPhaseInverterThingClassId, QT_TR_NOOP("Split Phase Inverter"), thing->id(), model); + break; + case SunSpecModelFactory::ModelIdInverterThreePhase: + case SunSpecModelFactory::ModelIdInverterThreePhaseFloat: + autocreateSunSpecModelThing(sunspecThreePhaseInverterThingClassId, QT_TR_NOOP("Three Phase Inverter"), thing->id(), model); + break; + case SunSpecModelFactory::ModelIdMeterSinglePhase: + case SunSpecModelFactory::ModelIdMeterSinglePhaseFloat: + autocreateSunSpecModelThing(sunspecSinglePhaseMeterThingClassId, QT_TR_NOOP("Single Phase Meter"), thing->id(), model); + break; + case SunSpecModelFactory::ModelIdMeterSplitSinglePhaseAbn: + case SunSpecModelFactory::ModelIdMeterSplitSinglePhaseFloat: + autocreateSunSpecModelThing(sunspecSplitPhaseMeterThingClassId, QT_TR_NOOP("Split Phase Meter"), thing->id(), model); + break; + case SunSpecModelFactory::ModelIdMeterThreePhase: + case SunSpecModelFactory::ModelIdDeltaConnectThreePhaseAbcMeter: + case SunSpecModelFactory::ModelIdMeterThreePhaseWyeConnect: + case SunSpecModelFactory::ModelIdMeterThreePhaseDeltaConnect: + autocreateSunSpecModelThing(sunspecThreePhaseMeterThingClassId, QT_TR_NOOP("Three Phase Meter"), thing->id(), model); + break; + case SunSpecModelFactory::ModelIdStorage: + autocreateSunSpecModelThing(sunspecStorageThingClassId, QT_TR_NOOP("Storage"), thing->id(), model); + break; + default: + qCWarning(dcSunSpec()) << "Plugin has no implementation for detected" << model; + break; + } + } + } + + // Check if we have a model for all child devices, otherwise we let the device dissappear + // As of now there might be following situation where this code is required: + // - A setup has changed and something has been removed or replaced + // - Some SunSpec device seem to communicate different model id depending on the startup phase + // i.e. they communicate a SinglePhase Meter on register x, few minutes later it is a 3 phase meter on x + // This code should handle such weird setups... + + if (connection->models().isEmpty()) + return; + + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + if (!m_modelIdParamTypeIds.contains(child->thingClassId()) || !m_modbusAddressParamTypeIds.contains(child->thingClassId())) + continue; + + uint childModelId = child->paramValue(m_modelIdParamTypeIds.value(child->thingClassId())).toUInt(); + uint childModbusAddress = child->paramValue(m_modbusAddressParamTypeIds.value(child->thingClassId())).toUInt(); + + bool modelFoundForChild = false; + foreach (SunSpecModel *model, connection->models()) { + if (childModelId == model->modelId() && childModbusAddress == model->modbusStartRegister()) { + modelFoundForChild = true; + break; + } + } + + if (!modelFoundForChild) { + qCInfo(dcSunSpec()) << "The model for" << child << "does not seem to be available any more on" << connection << "Removing the device since it does not seem to exist ony more on this connection."; + emit autoThingDisappeared(child->id()); + } + } +} + + +SunSpecConnection *IntegrationPluginSunSpec::createConnection(Thing *thing) +{ + QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); + int port = thing->paramValue(m_connectionPortParamTypeIds.value(thing->thingClassId())).toInt(); + int slaveId = thing->paramValue(m_connectionSlaveIdParamTypeIds.value(thing->thingClassId())).toInt(); + + if (m_sunSpecConnections.contains(thing->id())) { + qCDebug(dcSunSpec()) << "Reconfigure SunSpec connection" << thing; + m_sunSpecConnections.take(thing->id())->deleteLater(); + } + + SunSpecConnection *connection = nullptr; + if (thing->thingClassId() == solarEdgeConnectionThingClassId) { + // Note: for some reason, solar edge is using big endian register order instead + // of little endian as specified in sunspec and even solar edge documentation + connection = new SunSpecConnection(address, port, slaveId, SunSpecDataPoint::ByteOrderBigEndian, this); + } else { + SunSpecDataPoint::ByteOrder endianness = SunSpecDataPoint::ByteOrderLittleEndian; + QString endiannessParam = thing->paramValue("endianness").toString(); + if (endiannessParam == "Big Endian") + endianness = SunSpecDataPoint::ByteOrderBigEndian; + + connection = new SunSpecConnection(address, port, slaveId, endianness, this); + } + connection->setTimeout(configValue(sunSpecPluginTimeoutParamTypeId).toUInt()); + connection->setNumberOfRetries(configValue(sunSpecPluginNumberOfRetriesParamTypeId).toUInt()); + + // Reconnect on monitor reachable changed + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcSunSpec()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (!thing->setupComplete()) + return; + + if (reachable && !thing->stateValue("connected").toBool()) { + qCDebug(dcSunSpec()) << "The monitor is reachable. Set the host address to" << monitor->networkDeviceInfo().address() << "and start connecting..."; + connection->setHostAddress(monitor->networkDeviceInfo().address()); + connection->connectDevice(); + } + }); + + m_sunSpecConnections.insert(thing->id(), connection); + + // Update all child things connected states for this connection + connect(connection, &SunSpecConnection::connectedChanged, thing, [this, connection, thing] (bool connected) { + if (connected) { + qCDebug(dcSunSpec()) << connection << "connected"; + } else { + qCWarning(dcSunSpec()) << connection << "disconnected"; + } + + thing->setStateValue("connected", connected); + + if (connected) { + if (thing->setupComplete()) { + // We rediscovery all models on every connect to keep the child things in sync + connection->startDiscovery(); + } + } + // Update connected state of child things + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + if (connected) { + child->setStateValue("connected", true); + } else { + markThingStatesDisconnected(child); + } + } + }); + + connect(connection, &SunSpecConnection::discoveryFinished, thing, [this, connection, thing] (bool success) { + if (success) { + qCDebug(dcSunSpec()) << "Discovery finished successfully of" << connection; + processDiscoveryResult(thing, connection); + } else { + qCWarning(dcSunSpec()) << "Discovery finished with errors on" << connection; + } + }); + + return connection; +} + +void IntegrationPluginSunSpec::setupSolarEdgeBattery(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + int modbusStartRegister = thing->paramValue(solarEdgeBatteryThingModbusAddressParamTypeId).toUInt(); + SunSpecConnection *connection = m_sunSpecConnections.value(thing->parentId()); + if (!connection) { + qCWarning(dcSunSpec()) << "Could not find SunSpec parent connection for sunspec battery" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + qCDebug(dcSunSpec()) << "Setting up SolarEdge battery..."; + SolarEdgeBattery *battery = new SolarEdgeBattery(thing, connection, modbusStartRegister, connection); + + m_sunSpecThings.insert(thing, battery); + connect(battery, &SolarEdgeBattery::blockDataUpdated, this, &IntegrationPluginSunSpec::onSolarEdgeBatteryBlockUpdated); + info->finish(Thing::ThingErrorNoError); + + // Start initializing battery data + if (connection->connected()) + battery->init(); +} + +void IntegrationPluginSunSpec::searchSolarEdgeBatteries(SunSpecConnection *connection) +{ + if (!connection->connected()) { + qCDebug(dcSunSpec()) << "Could not search for SolarEdge batteries, the connection does not seem to be available at the moment."; + return; + } + + qCDebug(dcSunSpec()) << "Searching for connected SolarEdge batteries..."; + ThingId parentThingId = m_sunSpecConnections.key(connection); + if (parentThingId.isNull()) { + qCWarning(dcSunSpec()) << "Could not search for SolarEdge batteries because of find parent ThingId connection for" << connection->hostAddress().toString(); + return; + } + + // Batteries are not mapped to the sunspec layer, so we have to treat them as normal modbus registers. + // Read the battery id to verify if the battery is connected. + // Battery 1: start register 0xE100, device id register 0xE140 + // Battery 2: start register 0xE200, device id register 0xE240 + searchSolarEdgeBattery(connection, parentThingId, 0xE100); + searchSolarEdgeBattery(connection, parentThingId, 0xE200); +} + +void IntegrationPluginSunSpec::searchSolarEdgeBattery(SunSpecConnection *connection, const ThingId &parentThingId, quint16 startRegister) +{ + // Read the battery data, if init failed there is no battery connected, + // otherwise there is a battery and we should check if we need to setup the battery thing + + // Create a temporary battery object without thing for init + qCDebug(dcSunSpec()) << "Checking presence of SolarEdge battery on modbus register" << startRegister; + SolarEdgeBattery *battery = new SolarEdgeBattery(nullptr, connection, startRegister, connection); + connect(battery, &SolarEdgeBattery::initFinished, connection, [=](bool success) { + // Delete this object since we used it only for set up + battery->deleteLater(); + + // If init failed, no battery connected + if (!success) { + qCDebug(dcSunSpec()) << "No SolarEdge battery connected on register" << startRegister << "- not creating thing."; + return; + } + + qCDebug(dcSunSpec()) << "Battery initialized successfully." << battery->batteryData().manufacturerName << battery->batteryData().model; + // Check if we already created this battery + if (!myThings().filterByParam(solarEdgeBatteryThingSerialNumberParamTypeId, battery->batteryData().serialNumber).isEmpty()) { + qCDebug(dcSunSpec()) << "Battery already set up" << battery->batteryData().serialNumber; + } else { + // Create new battery device in the system + ThingDescriptor descriptor(solarEdgeBatteryThingClassId, battery->batteryData().manufacturerName + " - " + battery->batteryData().model, QString(), parentThingId); + ParamList params; + params.append(Param(solarEdgeBatteryThingModbusAddressParamTypeId, startRegister)); + params.append(Param(solarEdgeBatteryThingManufacturerParamTypeId, battery->batteryData().manufacturerName)); + params.append(Param(solarEdgeBatteryThingDeviceModelParamTypeId, battery->batteryData().model)); + params.append(Param(solarEdgeBatteryThingSerialNumberParamTypeId, battery->batteryData().serialNumber)); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); + } + }); + + // Try to initialize battery data + battery->init(); +} + +double IntegrationPluginSunSpec::calculateSolarEdgePvProduction(Thing *thing, double acPower, double dcPower) +{ + // Note: for solar edge, we need to calculate pv power from battery and AC/DC current depending + // on loading or discharging of the battery + + double pvPower = acPower; + Thing *parentThing = myThings().findById(thing->parentId()); + if (parentThing && parentThing->thingClassId() == solarEdgeConnectionThingClassId) { + SolarEdgeBattery *battery = nullptr; + // This is a solar edge, let's see if we have a battery for this connection + foreach (Thing *sunspecThing, m_sunSpecThings.keys()) { + if (sunspecThing->thingClassId() == solarEdgeBatteryThingClassId && thing->parentId() == parentThing->id()) { + battery = qobject_cast(m_sunSpecThings.value(sunspecThing)); + break; + } + } + + // Check if we have a meter... + Thing *meterThing = nullptr; + foreach (Thing *sunspecThing, m_sunSpecMeters.keys()) { + if (thing->parentId() == parentThing->id()) { + meterThing = sunspecThing; + break; + } + } + + // This is a solar edge, let's see if we have a battery for this connection + if (battery) { + double meterCurrentPower = meterThing ? meterThing->stateValue("currentPower").toDouble() : 0; + qCDebug(dcSunSpec()) << "SolarEdge: found battery for inverter: calculate actual PV power from battery DC power and inverter DC power..."; + qCDebug(dcSunSpec()) << "--> SolarEdge: -------------------------------------------------------"; + qCDebug(dcSunSpec()) << "--> SolarEdge: meter power:" << meterCurrentPower << "W"; + qCDebug(dcSunSpec()) << "--> SolarEdge: inverter AC power:" << acPower << "W"; + qCDebug(dcSunSpec()) << "--> SolarEdge: inverter DC power:" << dcPower << "W"; + qCDebug(dcSunSpec()) << "--> SolarEdge: battery DC power:" << battery->batteryData().instantaneousPower << "W"; + qCDebug(dcSunSpec()) << "--> SolarEdge: battery DC state:" << battery->batteryData().batteryStatus; + + switch (battery->batteryData().batteryStatus) { + case SolarEdgeBattery::BatteryStatus::Charge: { + // Actual PV = inverter AC power - battery power + pvPower = acPower - battery->batteryData().instantaneousPower; + qCDebug(dcSunSpec()) << "--> SolarEdge: calculate actual PV power: inverter AC power - battery power:" << acPower << "-" << battery->batteryData().instantaneousPower << "=" << pvPower; + break; + } + case SolarEdgeBattery::BatteryStatus::Discharge: { + // Actual PV = inverter DC power + battery power + pvPower = dcPower - battery->batteryData().instantaneousPower; + qCDebug(dcSunSpec()) << "--> SolarEdge: calculate actual PV power: inverter DC power - battery power:" << dcPower << "-" << battery->batteryData().instantaneousPower << "=" << pvPower; + if (pvPower > 0) { + qCDebug(dcSunSpec()) << "--> SolarEdge: actual PV power: 0W | loss:" << pvPower << "W"; + pvPower = 0; + } + break; + } + default: + // Idle: No change required, AC power is actual PV energy + pvPower = acPower; + break; + } + + // Calculate self consumption + double selfConsumption = pvPower - meterCurrentPower + battery->batteryData().instantaneousPower; + qCDebug(dcSunSpec()) << "--> SolarEdge: self consumption" << selfConsumption << "W"; + } + } + + return pvPower; +} + +void IntegrationPluginSunSpec::autocreateSunSpecModelThing(const ThingClassId &thingClassId, const QString &thingName, const ThingId &parentId, SunSpecModel *model) +{ + /* On some systems we get duplicated things having the same serial number but are on different registers. + * Those devices normally dissapear after some time again. For now, we need to prevent to even create those + * devices by filtering out those who already have been added using the serialnumber, not only the combination + * of model number and start address. */ + + QString serialNumber = model->commonModelInfo().serialNumber; + if (!serialNumber.isEmpty()) { + // If we have any model with this exact serial number, we skip adding this model + foreach (Thing *thing, myThings()) { + if (m_serialNumberParamTypeIds.contains(thing->thingClassId())) { + QString thingSerialNumber = thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString(); + if (serialNumber == thingSerialNumber) { + qCWarning(dcSunSpec()) << "Trying to add a new thing for a SunSpec model, but we already have a thing with this serial number. Not adding it to the system..."; + qCWarning(dcSunSpec()) << "--> Already added:" << thing << thing->params(); + qCWarning(dcSunSpec()) << "--> Attempt to add:" << thingName << model + << model->commonModelInfo().modelName + << model->commonModelInfo().manufacturerName + << model->commonModelInfo().serialNumber + << model->commonModelInfo().versionString; + + return; + } + } + } + } + + ThingDescriptor descriptor(thingClassId); + descriptor.setParentId(parentId); + QString finalThingName; + if (model->commonModelInfo().manufacturerName.isEmpty()) { + finalThingName = thingName; + } else { + finalThingName = model->commonModelInfo().manufacturerName + " " + thingName; + } + + descriptor.setTitle(finalThingName); + + ParamList params; + params.append(Param(m_modelIdParamTypeIds.value(descriptor.thingClassId()), model->modelId())); + params.append(Param(m_modbusAddressParamTypeIds.value(descriptor.thingClassId()), model->modbusStartRegister())); + params.append(Param(m_manufacturerParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().manufacturerName)); + params.append(Param(m_deviceModelParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().modelName)); + params.append(Param(m_serialNumberParamTypeIds.value(descriptor.thingClassId()), model->commonModelInfo().serialNumber)); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); +} + +QString IntegrationPluginSunSpec::getInverterStateString(quint16 state) +{ + // Note: this works because the models have all the same value + QString stateString; + switch (state) { + case SunSpecInverterSinglePhaseModel::StOff: + stateString = "Off"; + break; + case SunSpecInverterSinglePhaseModel::StSleeping: + stateString = "Sleeping"; + break; + case SunSpecInverterSinglePhaseModel::StStarting: + stateString = "Starting"; + break; + case SunSpecInverterSinglePhaseModel::StMppt: + stateString = "MPPT"; + break; + case SunSpecInverterSinglePhaseModel::StThrottled: + stateString = "Throttled"; + break; + case SunSpecInverterSinglePhaseModel::StShuttingDown: + stateString = "Shutting down"; + break; + case SunSpecInverterSinglePhaseModel::StFault: + stateString = "Fault"; + break; + case SunSpecInverterSinglePhaseModel::StStandby: + stateString = "Standby"; + break; + } + return stateString; +} + +QString IntegrationPluginSunSpec::getInverterErrorString(quint32 flag) +{ + // Note: this works because the models have all the same value + QStringList errorStrings; + SunSpecInverterSinglePhaseModel::Evt1Flags event1Flag = static_cast(flag); + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1GroundFault)) { + errorStrings.append(QT_TR_NOOP("Ground fault")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1DcOverVolt)) { + errorStrings.append(QT_TR_NOOP("DC over voltage")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1AcDisconnect)) { + errorStrings.append(QT_TR_NOOP("AC disconnect")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1DcDisconnect)) { + errorStrings.append(QT_TR_NOOP("DC disconnect")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1GridDisconnect)) { + errorStrings.append(QT_TR_NOOP("Grid disconnect")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1CabinetOpen)) { + errorStrings.append(QT_TR_NOOP("Cabinet open")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1ManualShutdown)) { + errorStrings.append(QT_TR_NOOP("Manual shutdown")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1OverTemp)) { + errorStrings.append(QT_TR_NOOP("Over temperature")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1OverFrequency)) { + errorStrings.append(QT_TR_NOOP("Over frequency")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1UnderFrequency)) { + errorStrings.append(QT_TR_NOOP("Under frequency")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1AcOverVolt)) { + errorStrings.append(QT_TR_NOOP("AC over voltage")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1AcUnderVolt)) { + errorStrings.append(QT_TR_NOOP("AC under voltage")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1BlownStringFuse)) { + errorStrings.append(QT_TR_NOOP("Blown string fuse")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1UnderTemp)) { + errorStrings.append(QT_TR_NOOP("Under temperature")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1MemoryLoss)) { + errorStrings.append(QT_TR_NOOP("Memory loss")); + } + + if (event1Flag.testFlag(SunSpecInverterSinglePhaseModel::Evt1HwTestFailure)) { + errorStrings.append(QT_TR_NOOP("Hardware test failure")); + } + + if (errorStrings.isEmpty()) { + return QT_TR_NOOP("No error"); + } else { + return errorStrings.join(", "); + } +} + +double IntegrationPluginSunSpec::fixValueSign(double targetValue, double powerValue) +{ + // Some sunspec devices (i.e. SolarEdge return an absolute value on the phase current. + // This method makes sure the phase current has the same sign as the phase power value. + bool sameSign = ((targetValue < 0) == (powerValue < 0)); + return (sameSign ? targetValue : -targetValue); +} + +bool IntegrationPluginSunSpec::hasManufacturer(const QStringList &manufacturers, const QString &manufacturer) +{ + foreach (const QString &name, manufacturers) { + if (name.toLower().contains(manufacturer.toLower())) { + return true; + } + } + + return false; +} + +void IntegrationPluginSunSpec::markThingStatesDisconnected(Thing *thing) +{ + qCDebug(dcSunSpec()) << thing << "is now disconnected. Setting energy live data to 0."; + if (thing->thingClassId() == sunspecSinglePhaseInverterThingClassId) { + thing->setStateValue(sunspecSinglePhaseInverterConnectedStateTypeId, false); + thing->setStateValue(sunspecSinglePhaseInverterCurrentPowerStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseInverterTotalCurrentStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseInverterFrequencyStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseInverterPhaseVoltageStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseInverterVoltageDcStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseInverterCurrentDcStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseInverterCurrentPowerDcStateTypeId, 0); + } else if (thing->thingClassId() == sunspecSplitPhaseInverterThingClassId) { + thing->setStateValue(sunspecSplitPhaseInverterConnectedStateTypeId, false); + thing->setStateValue(sunspecSplitPhaseInverterCurrentPowerStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterTotalCurrentStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterFrequencyStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterPhaseANVoltageStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterPhaseBNVoltageStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterPhaseACurrentStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterPhaseBCurrentStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterVoltageDcStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterCurrentDcStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseInverterCurrentPowerDcStateTypeId, 0); + } else if (thing->thingClassId() == sunspecThreePhaseInverterThingClassId) { + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecThreePhaseInverterConnectedStateTypeId, false); + thing->setStateValue(sunspecThreePhaseInverterCurrentPowerStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterTotalCurrentStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterFrequencyStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterPhaseANVoltageStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterPhaseBNVoltageStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterPhaseCNVoltageStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterPhaseACurrentStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterPhaseBCurrentStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterPhaseCCurrentStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterVoltageDcStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterCurrentDcStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseInverterCurrentPowerDcStateTypeId, 0); + } else if (thing->thingClassId() == sunspecSinglePhaseMeterThingClassId) { + thing->setStateValue(sunspecSinglePhaseMeterConnectedStateTypeId, false); + thing->setStateValue(sunspecSinglePhaseMeterCurrentPowerStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseMeterCurrentPhaseAStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseMeterVoltagePhaseAStateTypeId, 0); + thing->setStateValue(sunspecSinglePhaseMeterFrequencyStateTypeId, 0); + } else if (thing->thingClassId() == sunspecSplitPhaseMeterThingClassId) { + thing->setStateValue(sunspecSplitPhaseMeterConnectedStateTypeId, false); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterTotalCurrentStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerPhaseAStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerPhaseBStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPhaseAStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPhaseBStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterLnACVoltageStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterVoltagePhaseAStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterVoltagePhaseBStateTypeId, 0); + thing->setStateValue(sunspecSplitPhaseMeterFrequencyStateTypeId,0 ); + } else if (thing->thingClassId() == sunspecThreePhaseMeterThingClassId) { + thing->setStateValue(sunspecThreePhaseMeterConnectedStateTypeId, false); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseAStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseBStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseCStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseAStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseBStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseCStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseAStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseBStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseCStateTypeId, 0); + thing->setStateValue(sunspecThreePhaseMeterFrequencyStateTypeId, 0); + } else if (thing->thingClassId() == sunspecStorageThingClassId) { + thing->setStateValue(sunspecStorageConnectedStateTypeId, false); + thing->setStateValue(sunspecStorageGridChargingStateTypeId, false); + thing->setStateValue(sunspecStorageChargingRateStateTypeId, 0); + thing->setStateValue(sunspecStorageDischargingRateStateTypeId, 0); + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Off"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + } else if (thing->thingClassId() == solarEdgeBatteryThingClassId) { + thing->setStateValue(solarEdgeBatteryConnectedStateTypeId, false); + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Idle"); + thing->setStateValue(solarEdgeBatteryChargingStateStateTypeId, "idle"); + thing->setStateValue(solarEdgeBatteryInstantaneousVoltageStateTypeId, 0); + thing->setStateValue(solarEdgeBatteryInstantaneousCurrentStateTypeId, 0); + thing->setStateValue(solarEdgeBatteryCurrentPowerStateTypeId, 0); + } + +} + +void IntegrationPluginSunSpec::onRefreshTimer() +{ + // Update meters + foreach (SunSpecModel *model, m_sunSpecMeters.values()) { + if (model->connection()->connected()) { + model->readBlockData(); + } + } + + // Update storage + foreach (SunSpecModel *model, m_sunSpecStorages.values()) { + if (model->connection()->connected()) { + model->readBlockData(); + } + } + + // Update all other sunspec thing blocks + foreach (SunSpecThing *sunSpecThing, m_sunSpecThings) { + if (sunSpecThing->connection()->connected()) { + sunSpecThing->readBlockData(); + } + } + + // Update inverters + foreach (SunSpecModel *model, m_sunSpecInverters.values()) { + if (model->connection()->connected()) { + model->readBlockData(); + } + } +} + +void IntegrationPluginSunSpec::onPluginConfigurationChanged(const ParamTypeId ¶mTypeId, const QVariant &value) +{ + // Check refresh schedule + if (paramTypeId == sunSpecPluginUpdateIntervalParamTypeId) { + qCDebug(dcSunSpec()) << "Update interval has changed" << value.toInt(); + if (m_refreshTimer) { + int refreshTime = value.toInt(); + m_refreshTimer->stop(); + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime); + connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSunSpec::onRefreshTimer); + m_refreshTimer->start(); + } + } else if (paramTypeId == sunSpecPluginNumberOfRetriesParamTypeId) { + qCDebug(dcSunSpec()) << "Updating number of retries" << value.toUInt(); + foreach (SunSpecConnection *connection, m_sunSpecConnections) { + connection->setNumberOfRetries(value.toUInt()); + } + } else if (paramTypeId == sunSpecPluginTimeoutParamTypeId) { + qCDebug(dcSunSpec()) << "Updating timeout" << value.toUInt() << "[ms]"; + foreach (SunSpecConnection *connection, m_sunSpecConnections) { + connection->setTimeout(value.toUInt()); + } + } else { + qCWarning(dcSunSpec()) << "Unknown plugin configuration" << paramTypeId << "Value" << value; + } +} + +void IntegrationPluginSunSpec::onInverterBlockUpdated() +{ + SunSpecModel *model = qobject_cast(sender()); + Thing *thing = m_sunSpecInverters.key(model); + if (!thing) return; + + // Get parent thing + Thing *parentThing = myThings().findById(thing->parentId()); + if (!parentThing) + return; + + switch (model->modelId()) { + case SunSpecModelFactory::ModelIdInverterSinglePhase: { + SunSpecInverterSinglePhaseModel *inverter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecSinglePhaseInverterConnectedStateTypeId, true); + thing->setStateValue(sunspecSinglePhaseInverterVersionStateTypeId, model->commonModelInfo().versionString); + + // Note: solar edge needs some calculations for the current pv power + double currentPower = calculateSolarEdgePvProduction(thing, -inverter->watts(), -inverter->dcWatts()); + thing->setStateValue(sunspecSinglePhaseInverterCurrentPowerStateTypeId, currentPower); + evaluateEnergyProducedValue(thing, inverter->wattHours() / 1000.0); + thing->setStateValue(sunspecSinglePhaseInverterTotalCurrentStateTypeId, inverter->amps()); + thing->setStateValue(sunspecSinglePhaseInverterFrequencyStateTypeId, inverter->hz()); + thing->setStateValue(sunspecSinglePhaseInverterCabinetTemperatureStateTypeId, inverter->cabinetTemperature()); + thing->setStateValue(sunspecSinglePhaseInverterPhaseVoltageStateTypeId, inverter->phaseVoltageAn()); + thing->setStateValue(sunspecSinglePhaseInverterOperatingStateStateTypeId, getInverterStateString(inverter->operatingState())); + thing->setStateValue(sunspecSinglePhaseInverterErrorStateTypeId, getInverterErrorString(inverter->event1())); + thing->setStateValue(sunspecSinglePhaseInverterVoltageDcStateTypeId, inverter->dcVoltage()); + thing->setStateValue(sunspecSinglePhaseInverterCurrentDcStateTypeId, inverter->dcAmps()); + thing->setStateValue(sunspecSinglePhaseInverterCurrentPowerDcStateTypeId, -inverter->dcWatts()); + break; + } + case SunSpecModelFactory::ModelIdInverterSinglePhaseFloat: { + SunSpecInverterSinglePhaseFloatModel *inverter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecSinglePhaseInverterConnectedStateTypeId, true); + thing->setStateValue(sunspecSinglePhaseInverterVersionStateTypeId, model->commonModelInfo().versionString); + + // Note: solar edge needs some calculations for the current pv power + double currentPower = calculateSolarEdgePvProduction(thing, -inverter->watts(), -inverter->dcWatts()); + thing->setStateValue(sunspecSinglePhaseInverterCurrentPowerStateTypeId, currentPower); + evaluateEnergyProducedValue(thing, inverter->wattHours() / 1000.0); + thing->setStateValue(sunspecSinglePhaseInverterTotalCurrentStateTypeId, inverter->amps()); + thing->setStateValue(sunspecSinglePhaseInverterFrequencyStateTypeId, inverter->hz()); + thing->setStateValue(sunspecSinglePhaseInverterCabinetTemperatureStateTypeId, inverter->cabinetTemperature()); + thing->setStateValue(sunspecSinglePhaseInverterPhaseVoltageStateTypeId, inverter->phaseVoltageAn()); + thing->setStateValue(sunspecSinglePhaseInverterOperatingStateStateTypeId, getInverterStateString(inverter->operatingState())); + thing->setStateValue(sunspecSinglePhaseInverterErrorStateTypeId, getInverterErrorString(inverter->event1())); + thing->setStateValue(sunspecSinglePhaseInverterVoltageDcStateTypeId, inverter->dcVoltage()); + thing->setStateValue(sunspecSinglePhaseInverterCurrentDcStateTypeId, inverter->dcAmps()); + thing->setStateValue(sunspecSinglePhaseInverterCurrentPowerDcStateTypeId, -inverter->dcWatts()); + break; + } + case SunSpecModelFactory::ModelIdInverterSplitPhase: { + SunSpecInverterSplitPhaseModel *inverter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecSplitPhaseInverterConnectedStateTypeId, true); + thing->setStateValue(sunspecSplitPhaseInverterVersionStateTypeId, model->commonModelInfo().versionString); + + double currentPower = calculateSolarEdgePvProduction(thing, -inverter->watts(), -inverter->dcWatts()); + thing->setStateValue(sunspecSplitPhaseInverterCurrentPowerStateTypeId, currentPower); + evaluateEnergyProducedValue(thing, inverter->wattHours() / 1000.0); + thing->setStateValue(sunspecSplitPhaseInverterTotalCurrentStateTypeId, inverter->amps()); + thing->setStateValue(sunspecSplitPhaseInverterFrequencyStateTypeId, inverter->hz()); + thing->setStateValue(sunspecSplitPhaseInverterCabinetTemperatureStateTypeId, inverter->cabinetTemperature()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseANVoltageStateTypeId, inverter->phaseVoltageAn()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseBNVoltageStateTypeId, inverter->phaseVoltageBn()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseACurrentStateTypeId, inverter->ampsPhaseA()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseBCurrentStateTypeId, inverter->ampsPhaseB()); + thing->setStateValue(sunspecSplitPhaseInverterOperatingStateStateTypeId, getInverterStateString(inverter->operatingState())); + thing->setStateValue(sunspecSplitPhaseInverterErrorStateTypeId, getInverterErrorString(inverter->event1())); + thing->setStateValue(sunspecSplitPhaseInverterVoltageDcStateTypeId, inverter->dcVoltage()); + thing->setStateValue(sunspecSplitPhaseInverterCurrentDcStateTypeId, inverter->dcAmps()); + thing->setStateValue(sunspecSplitPhaseInverterCurrentPowerDcStateTypeId, -inverter->dcWatts()); + break; + } + case SunSpecModelFactory::ModelIdInverterSplitPhaseFloat: { + SunSpecInverterSplitPhaseFloatModel *inverter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecSplitPhaseInverterConnectedStateTypeId, true); + thing->setStateValue(sunspecSplitPhaseInverterVersionStateTypeId, model->commonModelInfo().versionString); + + double currentPower = calculateSolarEdgePvProduction(thing, -inverter->watts(), -inverter->dcWatts()); + thing->setStateValue(sunspecSplitPhaseInverterCurrentPowerStateTypeId, currentPower); + evaluateEnergyProducedValue(thing, inverter->wattHours() / 1000.0); + thing->setStateValue(sunspecSplitPhaseInverterTotalCurrentStateTypeId, inverter->amps()); + thing->setStateValue(sunspecSplitPhaseInverterFrequencyStateTypeId, inverter->hz()); + thing->setStateValue(sunspecSplitPhaseInverterCabinetTemperatureStateTypeId, inverter->cabinetTemperature()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseANVoltageStateTypeId, inverter->phaseVoltageAn()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseBNVoltageStateTypeId, inverter->phaseVoltageBn()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseACurrentStateTypeId, inverter->ampsPhaseA()); + thing->setStateValue(sunspecSplitPhaseInverterPhaseBCurrentStateTypeId, inverter->ampsPhaseB()); + thing->setStateValue(sunspecSplitPhaseInverterOperatingStateStateTypeId, getInverterStateString(inverter->operatingState())); + thing->setStateValue(sunspecSplitPhaseInverterErrorStateTypeId, getInverterErrorString(inverter->event1())); + thing->setStateValue(sunspecSplitPhaseInverterVoltageDcStateTypeId, inverter->dcVoltage()); + thing->setStateValue(sunspecSplitPhaseInverterCurrentDcStateTypeId, inverter->dcAmps()); + thing->setStateValue(sunspecSplitPhaseInverterCurrentPowerDcStateTypeId, -inverter->dcWatts()); + break; + } + case SunSpecModelFactory::ModelIdInverterThreePhase: { + SunSpecInverterThreePhaseModel *inverter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecThreePhaseInverterConnectedStateTypeId, true); + thing->setStateValue(sunspecThreePhaseInverterVersionStateTypeId, model->commonModelInfo().versionString); + + double currentPower = calculateSolarEdgePvProduction(thing, -inverter->watts(), -inverter->dcWatts()); + thing->setStateValue(sunspecThreePhaseInverterCurrentPowerStateTypeId, currentPower); + evaluateEnergyProducedValue(thing, inverter->wattHours() / 1000.0); + thing->setStateValue(sunspecThreePhaseInverterTotalCurrentStateTypeId, inverter->amps()); + thing->setStateValue(sunspecThreePhaseInverterFrequencyStateTypeId, inverter->hz()); + thing->setStateValue(sunspecThreePhaseInverterCabinetTemperatureStateTypeId, inverter->cabinetTemperature()); + thing->setStateValue(sunspecThreePhaseInverterPhaseANVoltageStateTypeId, inverter->phaseVoltageAn()); + thing->setStateValue(sunspecThreePhaseInverterPhaseBNVoltageStateTypeId, inverter->phaseVoltageBn()); + thing->setStateValue(sunspecThreePhaseInverterPhaseCNVoltageStateTypeId, inverter->phaseVoltageCn()); + thing->setStateValue(sunspecThreePhaseInverterPhaseACurrentStateTypeId, inverter->ampsPhaseA()); + thing->setStateValue(sunspecThreePhaseInverterPhaseBCurrentStateTypeId, inverter->ampsPhaseB()); + thing->setStateValue(sunspecThreePhaseInverterPhaseCCurrentStateTypeId, inverter->ampsPhaseC()); + thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, getInverterStateString(inverter->operatingState())); + thing->setStateValue(sunspecThreePhaseInverterErrorStateTypeId, getInverterErrorString(inverter->event1())); + thing->setStateValue(sunspecThreePhaseInverterVoltageDcStateTypeId, inverter->dcVoltage()); + thing->setStateValue(sunspecThreePhaseInverterCurrentDcStateTypeId, inverter->dcAmps()); + thing->setStateValue(sunspecThreePhaseInverterCurrentPowerDcStateTypeId, -inverter->dcWatts()); + break; + } + case SunSpecModelFactory::ModelIdInverterThreePhaseFloat: { + SunSpecInverterThreePhaseFloatModel *inverter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << inverter; + thing->setStateValue(sunspecThreePhaseInverterConnectedStateTypeId, true); + thing->setStateValue(sunspecThreePhaseInverterVersionStateTypeId, model->commonModelInfo().versionString); + + double currentPower = calculateSolarEdgePvProduction(thing, -inverter->watts(), -inverter->dcWatts()); + thing->setStateValue(sunspecThreePhaseInverterCurrentPowerStateTypeId, currentPower); + evaluateEnergyProducedValue(thing, inverter->wattHours() / 1000.0); + thing->setStateValue(sunspecThreePhaseInverterTotalCurrentStateTypeId, inverter->amps()); + thing->setStateValue(sunspecThreePhaseInverterFrequencyStateTypeId, inverter->hz()); + thing->setStateValue(sunspecThreePhaseInverterCabinetTemperatureStateTypeId, inverter->cabinetTemperature()); + thing->setStateValue(sunspecThreePhaseInverterPhaseANVoltageStateTypeId, inverter->phaseVoltageAn()); + thing->setStateValue(sunspecThreePhaseInverterPhaseBNVoltageStateTypeId, inverter->phaseVoltageBn()); + thing->setStateValue(sunspecThreePhaseInverterPhaseCNVoltageStateTypeId, inverter->phaseVoltageCn()); + thing->setStateValue(sunspecThreePhaseInverterPhaseACurrentStateTypeId, inverter->ampsPhaseA()); + thing->setStateValue(sunspecThreePhaseInverterPhaseBCurrentStateTypeId, inverter->ampsPhaseB()); + thing->setStateValue(sunspecThreePhaseInverterPhaseCCurrentStateTypeId, inverter->ampsPhaseC()); + thing->setStateValue(sunspecThreePhaseInverterOperatingStateStateTypeId, getInverterStateString(inverter->operatingState())); + thing->setStateValue(sunspecThreePhaseInverterErrorStateTypeId, getInverterErrorString(inverter->event1())); + thing->setStateValue(sunspecThreePhaseInverterVoltageDcStateTypeId, inverter->dcVoltage()); + thing->setStateValue(sunspecThreePhaseInverterCurrentDcStateTypeId, inverter->dcAmps()); + thing->setStateValue(sunspecThreePhaseInverterCurrentPowerDcStateTypeId, -inverter->dcWatts()); + break; + } + default: + qCWarning(dcSunSpec()) << "Received block data from unhandled model" << model; + break; + } +} + +void IntegrationPluginSunSpec::onMeterBlockUpdated() +{ + SunSpecModel *model = qobject_cast(sender()); + Thing *thing = m_sunSpecMeters.key(model); + if (!thing) return; + + // Get parent thing + Thing *parentThing = myThings().findById(thing->parentId()); + if (!parentThing) + return; + + switch (model->modelId()) { + case SunSpecModelFactory::ModelIdMeterSinglePhase: { + SunSpecMeterSinglePhaseModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecSinglePhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecSinglePhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecSinglePhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecSinglePhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecSinglePhaseMeterCurrentPhaseAStateTypeId, -meter->ampsPhaseA()); + thing->setStateValue(sunspecSinglePhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecSinglePhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecSinglePhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdMeterSinglePhaseFloat: { + SunSpecMeterSinglePhaseFloatModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecSinglePhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecSinglePhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecSinglePhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecSinglePhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecSinglePhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->watts())); + thing->setStateValue(sunspecSinglePhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecSinglePhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecSinglePhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdMeterSplitSinglePhaseAbn: { + SunSpecMeterSplitSinglePhaseAbnModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecSplitPhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecSplitPhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyConsumedPhaseAStateTypeId, meter->totalWattHoursImportedPhaseA() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyConsumedPhaseBStateTypeId, meter->totalWattHoursImportedPhaseB() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyProducedPhaseAStateTypeId, meter->totalWattHoursExportedPhaseA() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyProducedPhaseBStateTypeId, meter->totalWattHoursExportedPhaseB() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecSplitPhaseMeterTotalCurrentStateTypeId, fixValueSign(meter->amps(), -meter->watts())); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerPhaseAStateTypeId, -meter->wattsPhaseA()); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerPhaseBStateTypeId, -meter->wattsPhaseB()); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->wattsPhaseA())); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPhaseBStateTypeId, fixValueSign(meter->ampsPhaseB(), -meter->wattsPhaseB())); + thing->setStateValue(sunspecSplitPhaseMeterLnACVoltageStateTypeId, meter->voltageLn()); + thing->setStateValue(sunspecSplitPhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecSplitPhaseMeterVoltagePhaseBStateTypeId, meter->phaseVoltageBn()); + thing->setStateValue(sunspecSplitPhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecSplitPhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdMeterSplitSinglePhaseFloat: { + SunSpecMeterSplitSinglePhaseFloatModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecSplitPhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecSplitPhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyConsumedPhaseAStateTypeId, meter->totalWattHoursImportedPhaseA() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyConsumedPhaseBStateTypeId, meter->totalWattHoursImportedPhaseB() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyProducedPhaseAStateTypeId, meter->totalWattHoursExportedPhaseA() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterEnergyProducedPhaseBStateTypeId, meter->totalWattHoursExportedPhaseB() / 1000.0); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecSplitPhaseMeterTotalCurrentStateTypeId, fixValueSign(meter->amps(), -meter->watts())); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerPhaseAStateTypeId, -meter->wattsPhaseA()); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPowerPhaseBStateTypeId, -meter->wattsPhaseB()); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->wattsPhaseA())); + thing->setStateValue(sunspecSplitPhaseMeterCurrentPhaseBStateTypeId, fixValueSign(meter->ampsPhaseB(), -meter->wattsPhaseB())); + thing->setStateValue(sunspecSplitPhaseMeterLnACVoltageStateTypeId, meter->voltageLn()); + thing->setStateValue(sunspecSplitPhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecSplitPhaseMeterVoltagePhaseBStateTypeId, meter->phaseVoltageBn()); + thing->setStateValue(sunspecSplitPhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecSplitPhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdMeterThreePhase: { + SunSpecMeterThreePhaseModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecThreePhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseAStateTypeId, meter->totalWattHoursImportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseBStateTypeId, meter->totalWattHoursImportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseCStateTypeId, meter->totalWattHoursImportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseAStateTypeId, meter->totalWattHoursExportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseBStateTypeId, meter->totalWattHoursExportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseCStateTypeId, meter->totalWattHoursExportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseAStateTypeId, -meter->wattsPhaseA()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseBStateTypeId, -meter->wattsPhaseB()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseCStateTypeId, -meter->wattsPhaseC()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->wattsPhaseA())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseBStateTypeId, fixValueSign(meter->ampsPhaseB(), -meter->wattsPhaseB())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseCStateTypeId, fixValueSign(meter->ampsPhaseC(), -meter->wattsPhaseC())); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseBStateTypeId, meter->phaseVoltageBn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseCStateTypeId, meter->phaseVoltageCn()); + thing->setStateValue(sunspecThreePhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecThreePhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdDeltaConnectThreePhaseAbcMeter: { + SunSpecDeltaConnectThreePhaseAbcMeterModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecThreePhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseAStateTypeId, meter->totalWattHoursImportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseBStateTypeId, meter->totalWattHoursImportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseCStateTypeId, meter->totalWattHoursImportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseAStateTypeId, meter->totalWattHoursExportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseBStateTypeId, meter->totalWattHoursExportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseCStateTypeId, meter->totalWattHoursExportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseAStateTypeId, -meter->wattsPhaseA()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseBStateTypeId, -meter->wattsPhaseB()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseCStateTypeId, -meter->wattsPhaseC()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->wattsPhaseA())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseBStateTypeId, fixValueSign(meter->ampsPhaseB(), -meter->wattsPhaseB())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseCStateTypeId, fixValueSign(meter->ampsPhaseC(), -meter->wattsPhaseC())); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseBStateTypeId, meter->phaseVoltageBn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseCStateTypeId, meter->phaseVoltageCn()); + thing->setStateValue(sunspecThreePhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecThreePhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdMeterThreePhaseWyeConnect: { + SunSpecMeterThreePhaseWyeConnectModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecThreePhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseAStateTypeId, meter->totalWattHoursImportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseBStateTypeId, meter->totalWattHoursImportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseCStateTypeId, meter->totalWattHoursImportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseAStateTypeId, meter->totalWattHoursExportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseBStateTypeId, meter->totalWattHoursExportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseCStateTypeId, meter->totalWattHoursExportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseAStateTypeId, -meter->wattsPhaseA()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseBStateTypeId, -meter->wattsPhaseB()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseCStateTypeId, -meter->wattsPhaseC()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->wattsPhaseA())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseBStateTypeId, fixValueSign(meter->ampsPhaseB(), -meter->wattsPhaseB())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseCStateTypeId, fixValueSign(meter->ampsPhaseC(), -meter->wattsPhaseC())); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseBStateTypeId, meter->phaseVoltageBn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseCStateTypeId, meter->phaseVoltageCn()); + thing->setStateValue(sunspecThreePhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecThreePhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + case SunSpecModelFactory::ModelIdMeterThreePhaseDeltaConnect: { + SunSpecMeterThreePhaseDeltaConnectModel *meter = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << meter; + thing->setStateValue(sunspecThreePhaseMeterConnectedStateTypeId, true); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyProducedStateTypeId, meter->totalWattHoursExported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterTotalEnergyConsumedStateTypeId, meter->totalWattHoursImported() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseAStateTypeId, meter->totalWattHoursImportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseBStateTypeId, meter->totalWattHoursImportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyConsumedPhaseCStateTypeId, meter->totalWattHoursImportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseAStateTypeId, meter->totalWattHoursExportedPhaseA() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseBStateTypeId, meter->totalWattHoursExportedPhaseB() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterEnergyProducedPhaseCStateTypeId, meter->totalWattHoursExportedPhaseC() / 1000.0); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerStateTypeId, -meter->watts()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseAStateTypeId, -meter->wattsPhaseA()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseBStateTypeId, -meter->wattsPhaseB()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPowerPhaseCStateTypeId, -meter->wattsPhaseC()); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseAStateTypeId, fixValueSign(meter->ampsPhaseA(), -meter->wattsPhaseA())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseBStateTypeId, fixValueSign(meter->ampsPhaseB(), -meter->wattsPhaseB())); + thing->setStateValue(sunspecThreePhaseMeterCurrentPhaseCStateTypeId, fixValueSign(meter->ampsPhaseC(), -meter->wattsPhaseC())); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseAStateTypeId, meter->phaseVoltageAn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseBStateTypeId, meter->phaseVoltageBn()); + thing->setStateValue(sunspecThreePhaseMeterVoltagePhaseCStateTypeId, meter->phaseVoltageCn()); + thing->setStateValue(sunspecThreePhaseMeterFrequencyStateTypeId, meter->hz()); + thing->setStateValue(sunspecThreePhaseMeterVersionStateTypeId, model->commonModelInfo().versionString); + break; + } + default: + qCWarning(dcSunSpec()) << "Received block data from unhandled model" << model; + break; + } +} + +void IntegrationPluginSunSpec::onStorageBlockUpdated() +{ + SunSpecModel *model = qobject_cast(sender()); + Thing *thing = m_sunSpecStorages.key(model); + if (!thing) return; + + SunSpecStorageModel *storage = qobject_cast(model); + qCDebug(dcSunSpec()) << thing->name() << "block data updated";// << storage; + + thing->setStateValue(sunspecStorageConnectedStateTypeId, true); + thing->setStateValue(sunspecStorageVersionStateTypeId, model->commonModelInfo().versionString); + + thing->setStateValue(sunspecStorageBatteryCriticalStateTypeId, storage->chaState() < 5); + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, qRound(storage->chaState())); + thing->setStateValue(sunspecStorageGridChargingStateTypeId, storage->chaGriSet() == SunSpecStorageModel::ChagrisetGrid); + thing->setStateValue(sunspecStorageEnableChargingStateTypeId, storage->storCtlMod().testFlag(SunSpecStorageModel::Storctl_modCharge)); + thing->setStateValue(sunspecStorageChargingRateStateTypeId, storage->wChaGra()); + thing->setStateValue(sunspecStorageDischargingRateStateTypeId, storage->wDisChaGra()); + + switch (storage->chaSt()) { + case SunSpecStorageModel::ChastOff: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Off"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastEmpty: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Empty"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastDischarging: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Discharging"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "discharging"); + break; + case SunSpecStorageModel::ChastCharging: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Charging"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "charging"); + break; + case SunSpecStorageModel::ChastFull: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Full"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastHolding: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Holding"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastTesting: + thing->setStateValue(sunspecStorageStorageStatusStateTypeId, "Testing"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + } +} + +void IntegrationPluginSunSpec::onSolarEdgeBatteryBlockUpdated() +{ + SolarEdgeBattery *battery = qobject_cast(sender()); + Thing *thing = battery->thing(); + + qCDebug(dcSunSpec()) << "SolarEdgeBattery: block data updated";// << battery->batteryData(); + + thing->setStateValue(solarEdgeBatteryConnectedStateTypeId, true); + + QString chargingState = "idle"; + switch (battery->batteryData().batteryStatus) { + case SolarEdgeBattery::Off: + chargingState = "idle"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Off"); + break; + case SolarEdgeBattery::Standby: + chargingState = "idle"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Standby"); + break; + case SolarEdgeBattery::Init: + chargingState = "idle"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Init"); + break; + case SolarEdgeBattery::Charge: + chargingState = "charging"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Charging"); + break; + case SolarEdgeBattery::Discharge: + chargingState = "discharging"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Discharging"); + break; + case SolarEdgeBattery::Fault: + chargingState = "idle"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Fault"); + break; + case SolarEdgeBattery::Holding: + chargingState = "idle"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Holding"); + break; + case SolarEdgeBattery::Idle: + chargingState = "idle"; + thing->setStateValue(solarEdgeBatteryBatteryStatusStateTypeId, "Idle"); + break; + } + + thing->setStateValue(solarEdgeBatteryBatteryCriticalStateTypeId, (battery->batteryData().stateOfEnergy < 5) && chargingState != "charging"); + thing->setStateValue(solarEdgeBatteryBatteryLevelStateTypeId, battery->batteryData().stateOfEnergy); + thing->setStateValue(solarEdgeBatteryChargingStateStateTypeId, chargingState); + thing->setStateValue(solarEdgeBatteryRatedEnergyStateTypeId, battery->batteryData().ratedEnergy / 1000.0); // kWh + thing->setStateValue(solarEdgeBatteryAverageTemperatureStateTypeId, battery->batteryData().averageTemperature); + thing->setStateValue(solarEdgeBatteryInstantaneousVoltageStateTypeId, battery->batteryData().instantaneousVoltage); + thing->setStateValue(solarEdgeBatteryInstantaneousCurrentStateTypeId, battery->batteryData().instantaneousCurrent); + thing->setStateValue(solarEdgeBatteryCurrentPowerStateTypeId, battery->batteryData().instantaneousPower); + thing->setStateValue(solarEdgeBatteryMaxEnergyStateTypeId, battery->batteryData().maxEnergy / 1000.0); // kWh + thing->setStateValue(solarEdgeBatteryCapacityStateTypeId, battery->batteryData().availableEnergy / 1000.0); // kWh + thing->setStateValue(solarEdgeBatteryStateOfHealthStateTypeId, battery->batteryData().stateOfHealth); + thing->setStateValue(solarEdgeBatteryVersionStateTypeId, battery->batteryData().firmwareVersion); +} + +void IntegrationPluginSunSpec::evaluateEnergyProducedValue(Thing *inverterThing, float energyProduced) +{ + /* Note: on some systems the inverter sends for a longer period an absurdly + * high and wrong value for the produced energy (seen so far with SolarEdge inverters). + * + * In order to catch such situations, we need to verify if the state changed makes sense, + * or if the difference is to big for a regular produced energy value. + * + * Following scenarios need to be considered: + * - This is the first data value, we have no history to verify if this values makes sense + * - The system might be switched off for some time, the energy produced could be much more than the last known value + * - More than one value in a row could occure, not only single garbage data value + */ + + StateTypeId energyProducedStateTypeId; + if (inverterThing->thingClassId() == sunspecSinglePhaseInverterThingClassId) { + energyProducedStateTypeId = sunspecSinglePhaseInverterTotalEnergyProducedStateTypeId; + } else if (inverterThing->thingClassId() == sunspecSplitPhaseInverterThingClassId) { + energyProducedStateTypeId = sunspecSplitPhaseInverterTotalEnergyProducedStateTypeId; + } else if (inverterThing->thingClassId() == sunspecThreePhaseInverterThingClassId) { + energyProducedStateTypeId = sunspecThreePhaseInverterTotalEnergyProducedStateTypeId; + } else { + qCWarning(dcSunSpec()) << "Could not evaluate energy produced value for ThingClassId" << inverterThing->thingClassId() << "The value will not be updated."; + return; + } + + double currentEnergyValue = inverterThing->stateValue(energyProducedStateTypeId).toDouble(); + if (currentEnergyValue <= 0) { + // Probably the initial value, no fancy data handling here + inverterThing->setStateValue(energyProducedStateTypeId, energyProduced); + } else { + double producedDiff = energyProduced - currentEnergyValue; + if (producedDiff > 10000 /*kWh*/) { + // The new energy value is way to high in order to be a reglar energy produced change... + qCWarning(dcSunSpec()) << "The energy produced value for" << inverterThing << "is way to high compared to the previouse value:" + << currentEnergyValue << "kWh. Ignoring the value:" << energyProduced << "kWh."; + return; + } else { + // Not a huge jump, just set the value + inverterThing->setStateValue(energyProducedStateTypeId, energyProduced); + } + } +} diff --git a/sunspec/integrationpluginsunspec.h b/sunspec/integrationpluginsunspec.h new file mode 100644 index 0000000..d6a09b4 --- /dev/null +++ b/sunspec/integrationpluginsunspec.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINSUNSPEC_H +#define INTEGRATIONPLUGINSUNSPEC_H + +#include +#include +#include + +#include +#include + +#include "sunspecthing.h" +#include "extern-plugininfo.h" + +#include + +class IntegrationPluginSunSpec: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsunspec.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginSunSpec(); + + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + // SunSpec Connection params map + QHash m_connectionDiscoverySlaveIdParamTypeIds; + QHash m_connectionMacAddressParamTypeIds; + QHash m_connectionAddressParamTypeIds; + QHash m_connectionHostNameParamTypeIds; + QHash m_connectionPortParamTypeIds; + QHash m_connectionSlaveIdParamTypeIds; + + // SunSpec thing params map + QHash m_modelIdParamTypeIds; + QHash m_modbusAddressParamTypeIds; + QHash m_manufacturerParamTypeIds; + QHash m_deviceModelParamTypeIds; + QHash m_serialNumberParamTypeIds; + + PluginTimer *m_refreshTimer = nullptr; + + QHash m_monitors; + + QHash m_sunSpecConnections; + QHash m_sunSpecThings; + + QHash m_sunSpecInverters; + QHash m_sunSpecMeters; + QHash m_sunSpecStorages; + + + Thing *getThingForSunSpecModel(uint modelId, uint modbusAddress, const ThingId &parentId); + bool sunspecThingAlreadyAdded(uint modelId, uint modbusAddress, const ThingId &parentId); + void processDiscoveryResult(Thing *thing, SunSpecConnection *connection); + + // SunSpec things + SunSpecConnection *createConnection(Thing *thing); + + // Custom types + void setupSolarEdgeBattery(ThingSetupInfo *info); + + // Discover SolarEdge batteries + void searchSolarEdgeBatteries(SunSpecConnection *connection); + void searchSolarEdgeBattery(SunSpecConnection *connection, const ThingId &parentThingId, quint16 startRegister); + + // SolarEdge + double calculateSolarEdgePvProduction(Thing *thing, double acPower, double dcPower); + + void autocreateSunSpecModelThing(const ThingClassId &thingClassId, const QString &thingName, const ThingId &parentId, SunSpecModel *model); + + QString getInverterStateString(quint16 state); + QString getInverterErrorString(quint32 flag); + + double fixValueSign(double targetValue, double powerValue); + bool hasManufacturer(const QStringList &manufacturers, const QString &manufacturer); + void markThingStatesDisconnected(Thing *thing); + +private slots: + void onRefreshTimer(); + void onPluginConfigurationChanged(const ParamTypeId ¶mTypeId, const QVariant &value); + + void onInverterBlockUpdated(); + void onMeterBlockUpdated(); + void onStorageBlockUpdated(); + void onSolarEdgeBatteryBlockUpdated(); + + void evaluateEnergyProducedValue(Thing *inverterThing, float energyProduced); + + +}; +#endif // INTEGRATIONPLUGINSUNSPEC_H + diff --git a/sunspec/integrationpluginsunspec.json b/sunspec/integrationpluginsunspec.json new file mode 100644 index 0000000..6696477 --- /dev/null +++ b/sunspec/integrationpluginsunspec.json @@ -0,0 +1,1728 @@ +{ + "name": "SunSpec", + "displayName": "sunspec", + "id": "cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b", + "paramTypes":[ + { + "id": "52da5222-9a94-47a2-9adc-004541d2f5ed", + "name": "updateInterval", + "displayName": "Update interval", + "type": "int", + "unit": "Seconds", + "defaultValue": 1 + }, + { + "id": "1a8895a0-c746-48af-9307-3a4636f24cc2", + "name": "timeout", + "displayName": "Timout", + "type": "uint", + "unit": "MilliSeconds", + "defaultValue": 1000 + }, + { + "id": "9a4bfe01-315f-4ee7-98a9-f16b08ba12ad", + "name": "numberOfRetries", + "displayName": "Number of retries", + "type": "uint", + "defaultValue": 3, + "minValue": 1, + "maxValue": 10 + } + ], + "vendors": [ + { + "name": "sunspec", + "displayName": "SunSpec", + "id": "c143a7b4-a16c-4fff-86a3-9ffab3d6841d", + "thingClasses": [ + { + "name": "sunspecConnection", + "displayName": "SunSpec Generic", + "id": "f51853f3-8815-4cf3-b337-45cda1f3e6d5", + "createMethods": [ "discovery", "user" ], + "interfaces": [ "gateway", "networkdevice" ], + "providedInterfaces": [ "solarinverter", "energymeter", "energystorage"], + "discoveryParamTypes": [ + { + "id": "50e027aa-ab15-44df-80d3-71964aee44af", + "name":"slaveId", + "displayName": "Slave ID", + "type": "int", + "defaultValue": 1 + } + ], + "paramTypes": [ + { + "id": "3567b389-9d42-48f9-a29b-d18388fb36a1", + "name": "address", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "1667b0ed-9a2b-47c6-a01f-690caee55ffa", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "f65d6c36-1672-44cb-b52a-62d71837ae67", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + }, + { + "id": "1fa4fc9c-f6be-47c7-928a-bcefc1142eec", + "name":"port", + "displayName": "Port", + "type": "int", + "defaultValue": 502 + }, + { + "id": "953064e0-4675-4538-a9a2-fa22ce2f347c", + "name":"slaveId", + "displayName": "Slave ID", + "type": "int", + "defaultValue": 1 + }, + { + "id": "38082ecf-23bf-47d1-beba-f816c978c823", + "name":"endianness", + "displayName": "Endianness", + "type": "QString", + "allowedValues": [ + "Big Endian", + "Little Endian" + ], + "defaultValue": "Big Endian" + } + ], + "stateTypes":[ + { + "id": "3e767ad0-b4b3-4398-94c1-00579ea09ca8", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + } + ] + }, + { + "name": "sunspecSinglePhaseInverter", + "displayName": "SunSpec single phase inverter", + "id": "c5d5204e-3375-4b92-8128-fab77a671fed", + "createMethods": [ "Auto" ], + "interfaces": [ "solarinverter", "connectable" ], + "paramTypes": [ + { + "id": "41715d00-a947-4f43-a475-cea05790e01d", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true + }, + { + "id": "26ae9050-7090-453a-85a3-307bfebe6fed", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "5fbb0b62-e0f9-4aee-aba7-fd00ae8b0a5d", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "12f1b02c-033d-423f-a217-955ddb995dd5", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "59b85218-f144-4ce8-95f7-19a1dc2e2a8d", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "48bf31c4-fda7-41e5-a3ef-3011bf96e104", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "f02d1c99-9b43-45a6-8a06-2ed4d6e5d497", + "name": "totalCurrent", + "displayName": "Total AC current", + "displayNameEvent": "Total AC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "4ca086e9-82b9-461c-b168-1d61b542b884", + "name": "phaseVoltage", + "displayName": "Phase voltage", + "displayNameEvent": "Phase volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "f49591d6-d759-4be3-bafc-b6a7a72cf023", + "name": "currentPower", + "displayName": "AC power", + "displayNameEvent": "AC power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "611df2ce-2b9c-49f3-9fa7-5706776e812c", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, + { + "id": "4c0407b3-5cd5-438d-bfa8-9a8d6695b458", + "name": "totalEnergyProduced", + "displayName": "AC energy", + "displayNameEvent": "AC energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "157376b8-19ae-415f-b693-cb0a9ba88863", + "name": "voltageDc", + "displayName": "DC voltage", + "displayNameEvent": "DC volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "79bd1748-b540-467d-b6fa-b0cc56d9775a", + "name": "currentDc", + "displayName": "DC current", + "displayNameEvent": "DC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "852a8c28-b14d-492e-b2b3-f22219f4ed36", + "name": "currentPowerDc", + "displayName": "DC power", + "displayNameEvent": "DC power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "51461bde-1a6b-4aa1-94cc-59829ea0a7c8", + "name": "cabinetTemperature", + "displayName": "Cabinet temperature", + "displayNameEvent": "Cabinet temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00 + }, + { + "id": "47543a7f-425f-406b-a458-b79c36b65f6c", + "name": "operatingState", + "displayName": "Operating state", + "displayNameEvent": "Operating state changed", + "type": "QString", + "possibleValues": [ + "Off", + "Sleeping", + "Starting", + "MPPT", + "Throttled", + "Shutting down", + "Fault", + "Standby" + ], + "defaultValue": "Off" + }, + { + "id": "49240259-d82a-4fe6-b3f5-1cd6a67c87a7", + "name": "error", + "displayName": "Error", + "displayNameEvent": "Error changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "f4439de3-a461-42d6-b4c9-aa35f696a420", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "sunspecSplitPhaseInverter", + "displayName": "SunSpec split phase inverter", + "id": "61b38f93-d331-42bf-b1ef-d3fb16ad1230", + "createMethods": [ "Auto" ], + "interfaces": [ "solarinverter", "connectable" ], + "paramTypes": [ + { + "id": "c42fb50e-210f-4b53-88eb-fa216e15f88f", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true + }, + { + "id": "37582a96-f2f2-4845-abef-973c7dd0ad57", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "c2c35b68-1c0f-47cc-b09f-49d9cf67c0ff", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "d40b1b9d-994d-422c-8a41-b98cf03c1678", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "8afdd256-0393-4610-9477-7c4541c92ffe", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "27b49640-f58b-466e-a225-a4663cf3ed96", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "9dbd8da7-dc22-4c3a-b941-47520fde705f", + "name": "totalCurrent", + "displayName": "Total AC current", + "displayNameEvent": "Total AC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "be7b86b4-aeeb-49ba-9b6b-9792dceed6b5", + "name": "phaseACurrent", + "displayName": "Phase A current", + "displayNameEvent": "Phase A current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "fc5df18d-cf2f-4944-97b7-e57dabef8778", + "name": "phaseBCurrent", + "displayName": "Phase B current", + "displayNameEvent": "Phase B current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "17c24cfc-cb41-4873-82b4-19a20d6be146", + "name": "phaseANVoltage", + "displayName": "Phase AN voltage", + "displayNameEvent": "Phase AN volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "f8645ee2-a1e6-4d09-8c20-f6fd02a9e896", + "name": "phaseBNVoltage", + "displayName": "Phase BN voltage", + "displayNameEvent": "Phase BN voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "9235eb4b-906c-4557-8e18-bca268a367cc", + "name": "currentPower", + "displayName": "AC power", + "displayNameEvent": "AC power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "874f5e4a-a009-4c28-b211-2af90a24b2ac", + "name": "frequency", + "displayName": "Line frequency", + "displayNameEvent": "Line frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, + { + "id": "fe3f8a65-121a-4ae1-b22a-ae325dc3e7e6", + "name": "totalEnergyProduced", + "displayName": "AC energy", + "displayNameEvent": "AC energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "d36bcaba-1e77-431e-89dc-af8ac6172d7d", + "name": "voltageDc", + "displayName": "DC voltage", + "displayNameEvent": "DC volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "68e7239f-d7b2-4ab2-9cf3-e769fb5ddfc0", + "name": "currentDc", + "displayName": "DC current", + "displayNameEvent": "DC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "560b5ec8-1b2c-47d3-b55e-40f4d0aebfe0", + "name": "currentPowerDc", + "displayName": "DC power", + "displayNameEvent": "DC power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "6d314d50-b990-4a58-a37f-4a3da42c4407", + "name": "cabinetTemperature", + "displayName": "Cabinet temperature", + "displayNameEvent": "Cabinet temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00 + }, + { + "id": "6c1e2929-bc9a-4ce9-a405-6df2633a5131", + "name": "operatingState", + "displayName": "Operating state", + "displayNameEvent": "Operating state changed", + "type": "QString", + "possibleValues": [ + "Off", + "Sleeping", + "Starting", + "MPPT", + "Throttled", + "Shutting down", + "Fault", + "Standby" + ], + "defaultValue": "Off" + }, + { + "id": "5cbfccc9-6afb-404c-a85e-e0323659a25f", + "name": "error", + "displayName": "Error", + "displayNameEvent": "Error changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "2986cac4-f4ac-4299-ac9e-48339e2a54ef", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "sunspecThreePhaseInverter", + "displayName": "SunSpec three phase inverter", + "id": "2e4122ea-96a5-415c-b5e2-7d6012265a83", + "createMethods": [ "Auto" ], + "interfaces": [ "solarinverter", "connectable" ], + "paramTypes": [ + { + "id": "8d5b2b58-ce46-406d-844e-f53136afcf09", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true + }, + { + "id": "e5465ede-9d3d-4558-b614-40dda743ddae", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "640b71ba-2ccd-40d6-bf45-8271a9046f81", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "938b6555-e0af-4222-b85b-90bc1bc78c8f", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "ee169df5-475c-4dfb-870a-0785899b7dc1", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "4401468c-0385-40a9-b436-daf7ed6a50d5", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "26560dd8-6de4-445e-ba55-391d7241c370", + "name": "totalCurrent", + "displayName": "Total AC current", + "displayNameEvent": "Total AC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "3140ccd3-40cf-46c8-8bb2-8c3ea4582f84", + "name": "phaseACurrent", + "displayName": "Phase A current", + "displayNameEvent": "Phase A current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "7ea1a53a-6fd9-4914-8283-b57aa1aaaebf", + "name": "phaseBCurrent", + "displayName": "Phase B current", + "displayNameEvent": "Phase B current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "aa4b4cf5-43d0-4be5-9505-403918b5371d", + "name": "phaseCCurrent", + "displayName": "Phase C current", + "displayNameEvent": "Phase C current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "f08521aa-9c38-4c31-95e1-acb616f6e9c6", + "name": "phaseANVoltage", + "displayName": "Phase AN voltage", + "displayNameEvent": "Phase AN volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "739b8805-d522-4406-bede-d1e4200a3aa9", + "name": "phaseBNVoltage", + "displayName": "Phase BN voltage", + "displayNameEvent": "Phase BN voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "949b797d-5566-4667-8982-e430d23548e2", + "name": "phaseCNVoltage", + "displayName": "Phase CN voltage", + "displayNameEvent": "Phase CN voltage changed", + "type": "int", + "unit": "Volt", + "defaultValue": 0, + "filter": "adaptive" + }, + { + "id": "14036f44-25fd-4e93-8e8c-c677b06a2c34", + "name": "currentPower", + "displayName": "AC power", + "displayNameEvent": "AC power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "faa45cae-ed28-4150-9036-fceddf9d6776", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, + { + "id": "d493880d-eb58-4530-8010-8ea4f6d63387", + "name": "totalEnergyProduced", + "displayName": "AC energy", + "displayNameEvent": "AC energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "8c1c71c4-28fd-472a-83a6-3e96ab03d677", + "name": "voltageDc", + "displayName": "DC voltage", + "displayNameEvent": "DC volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "81a989aa-4104-438e-bfc5-4f44ce5ceeaf", + "name": "currentDc", + "displayName": "DC current", + "displayNameEvent": "DC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "1397d935-1206-4ceb-883d-1fcf41ba6214", + "name": "currentPowerDc", + "displayName": "DC power", + "displayNameEvent": "DC power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "44b0320f-89a7-4248-bad4-288ef898a5cc", + "name": "cabinetTemperature", + "displayName": "Cabinet temperature", + "displayNameEvent": "Cabinet temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00 + }, + { + "id": "cebdce98-42d1-4a28-8834-8960efc0e83f", + "name": "operatingState", + "displayName": "Operating state", + "displayNameEvent": "Operating state changed", + "type": "QString", + "possibleValues": [ + "Off", + "Sleeping", + "Starting", + "MPPT", + "Throttled", + "Shutting down", + "Fault", + "Standby" + ], + "defaultValue": "Off" + }, + { + "id": "4479af96-c499-4f15-abd6-4afdb18a9e09", + "name": "error", + "displayName": "Error", + "displayNameEvent": "Error changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "3b543b60-d8f6-4bc0-a79f-cb9cb20e4bb8", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "sunspecSinglePhaseMeter", + "displayName": "SunSpec single phase meter", + "id": "7ffa43b8-b56f-4435-8509-980e9d81dfa8", + "createMethods": [ "Auto" ], + "interfaces": [ "energymeter", "connectable" ], + "paramTypes": [ + { + "id": "7d6fcafb-c62e-4a21-aae2-f4041c487149", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "30b90ec0-429b-4e6c-88e9-155aa4bcad47", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "811de380-848a-448c-8446-daa0e0fd8e50", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "e31333c8-746d-4955-af0a-ae18d7100de7", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "581fa623-0005-448f-a5b1-8db1043795a3", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "d960e7b1-d4aa-4cab-8f54-6bcfdbb8be36", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "93cf8c6a-2620-42ed-9070-e0726d7b1dbc", + "name": "currentPower", + "displayName": "Total real power", + "displayNameEvent": "Total real power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "ba275bdf-f418-4ef0-afbe-ac425c6f6783", + "name": "totalEnergyProduced", + "displayName": "Total real energy exported", + "displayNameEvent": "Total real energy exported changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "c51dc6cb-5c05-4078-b11a-26afb2f85541", + "name": "totalEnergyConsumed", + "displayName": "Total real energy imported", + "displayNameEvent": "Total real energy imported changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "4a058e36-0b45-4388-9a26-0615f7aafa0d", + "name": "currentPhaseA", + "displayName": "Phase A current", + "displayNameEvent": "Phase A current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "5e7a65a5-b740-4395-87fa-ffbe8fe1c823", + "name": "voltagePhaseA", + "displayName": "Phase A voltage", + "displayNameEvent": "Phase A voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "3a2ce51d-7fa0-4188-bbd6-00d25de90e15", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, + { + "id": "94adff6f-f672-4e4e-a72c-cd6cd7e8721a", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "sunspecSplitPhaseMeter", + "displayName": "SunSpec split phase meter", + "id": "b8a18e45-5ff5-4f43-915f-04ee216c809d", + "createMethods": [ "Auto" ], + "interfaces": [ "energymeter", "connectable" ], + "paramTypes": [ + { + "id": "89aeec6d-abeb-48b5-9594-214ad5db2d03", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "a56f198d-ed86-429f-b839-8e11a32da8c1", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "ecbdee01-8be7-42bf-9e1a-a0f700669b09", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "6bc978cb-a779-4f5f-87a4-3cbbe2a55ca8", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "fb7cc04b-91cb-4f3d-a444-201cca57934a", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "34e34ec9-dab0-438c-9493-a3068bc401de", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "8a63bd73-0546-4636-8da2-23238cc06fb2", + "name": "totalEnergyProduced", + "displayName": "Total real energy exported", + "displayNameEvent": "Total real energy exported changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "51ffb2ae-3920-40df-8290-bbf5b6e1a68f", + "name": "totalEnergyConsumed", + "displayName": "Total real energy imported", + "displayNameEvent": "Total real energy imported changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "ef4bc0f8-f516-49b7-aba8-d5f987485aca", + "name": "currentPower", + "displayName": "Total real power", + "displayNameEvent": "Total real power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "481b9895-1c3e-4e74-9418-ee51dae80a63", + "name": "energyConsumedPhaseA", + "displayName": "Energy consumed phase A", + "displayNameEvent": "Energy consumed phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "10245c6c-4b0d-44b4-be10-72026b842c86", + "name": "energyConsumedPhaseB", + "displayName": "Energy consumed phase B", + "displayNameEvent": "Energy consumed phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "2dc76576-561c-4bce-8799-fadc55e84aee", + "name": "energyProducedPhaseA", + "displayName": "Energy produced phase A", + "displayNameEvent": "Energy produced phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "b1489efd-ece0-419e-ab1c-d3d25ab6b958", + "name": "energyProducedPhaseB", + "displayName": "Energy produced phase B", + "displayNameEvent": "Energy produced phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "e85024af-5376-4ff1-813e-5a56990c11cc", + "name": "totalCurrent", + "displayName": "Total AC current", + "displayNameEvent": "Total AC current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "57c0f5ba-a104-424b-b8d4-8bab90acf009", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "456f7cdf-9756-49b8-9084-f212199d4014", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "e8c0f4bf-a704-46f2-80a0-cf490bd7871b", + "name": "currentPhaseA", + "displayName": "Phase A current", + "displayNameEvent": "Phase A current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "4281f6fc-d5a0-4a22-ac61-6bec88efbc80", + "name": "currentPhaseB", + "displayName": "Phase B current", + "displayNameEvent": "Phase B current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "0ac79508-07c3-4d01-97a3-6edf121bdf32", + "name": "lnACVoltage", + "displayName": "Line to Neutral AC Voltage", + "displayNameEvent": "Line to Neutral AC Voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "1bd7e53e-abf8-4d62-b87c-2c84c283567b", + "name": "voltagePhaseA", + "displayName": "Phase A voltage", + "displayNameEvent": "Phase A volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "377b5279-ddb6-451d-8377-a9389c749393", + "name": "voltagePhaseB", + "displayName": "Phase B voltage", + "displayNameEvent": "Phase B voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "db977c04-a3e1-436f-a0cd-9ce5b7bc6b89", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, + { + "id": "85c5087c-77b4-4ac8-bdff-020f6242bd57", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "sunspecThreePhaseMeter", + "displayName": "SunSpec three phase meter", + "id": "68f822f9-ff30-4275-b229-39a3674fead7", + "createMethods": [ "Auto" ], + "interfaces": [ "energymeter", "connectable" ], + "paramTypes": [ + { + "id": "a1960821-155c-4176-86fa-974429039182", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "6d5dbd35-1bf6-46db-bee9-90c679421b89", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "462552de-af84-4893-9132-81c1adce67ad", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "00d308dc-e9a3-40e4-acad-3cad2ca3590f", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "97bbcc98-f4ec-4060-bac2-31b40720862c", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "36f861c7-afc1-4725-b41f-67113200d78f", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "01a69b2c-cc96-4925-9b1c-a2f204ef0837", + "name": "totalEnergyProduced", + "displayName": "Total real energy exported", + "displayNameEvent": "Total real energy exported changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "0a98b229-7625-4811-8bf8-ed22be2db3fb", + "name": "totalEnergyConsumed", + "displayName": "Total real energy imported", + "displayNameEvent": "Total real energy imported changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "47780b0a-68b7-46c3-bb98-79ec9a57fd69", + "name": "currentPower", + "displayName": "Total real power", + "displayNameEvent": "Total real power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "cc0fd197-22ed-4461-8bba-47460396798a", + "name": "energyConsumedPhaseA", + "displayName": "Energy consumed phase A", + "displayNameEvent": "Energy consumed phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "e928fde3-b35b-4884-9810-1cd083725e1c", + "name": "energyConsumedPhaseB", + "displayName": "Energy consumed phase B", + "displayNameEvent": "Energy consumed phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "2a12a991-d6c8-4665-bcc8-ea2685f7f0d4", + "name": "energyConsumedPhaseC", + "displayName": "Energy consumed phase C", + "displayNameEvent": "Energy consumed phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "fe7caa7e-cf37-4550-833e-7f9d35001fdd", + "name": "energyProducedPhaseA", + "displayName": "Energy produced phase A", + "displayNameEvent": "Energy produced phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "edbe64a5-121e-4c96-9dc8-bbc2a33dfdb7", + "name": "energyProducedPhaseB", + "displayName": "Energy produced phase B", + "displayNameEvent": "Energy produced phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "a26790c0-7378-471c-80ba-67021352415d", + "name": "energyProducedPhaseC", + "displayName": "Energy produced phase C", + "displayNameEvent": "Energy produced phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "0e3e8870-3d59-4ee8-b7aa-33bda68b6ae3", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "453d046c-3a66-4590-882c-1283882918fc", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "ba4e94de-9136-48a7-aad8-3d5cd0e7d968", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "displayNameEvent": "Current power phase C changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "46ae5c40-f389-4f7e-a59f-8947d7dc9f3f", + "name": "currentPhaseA", + "displayName": "Phase A current", + "displayNameEvent": "Phase A current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "efc75436-ac51-4fee-81f1-65a726416980", + "name": "currentPhaseB", + "displayName": "Phase B current", + "displayNameEvent": "Phase B current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "618cbcc5-2efe-49d2-9c5c-53fb3eaa3a9d", + "name": "currentPhaseC", + "displayName": "Phase C current", + "displayNameEvent": "Phase C current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "4ff342de-f0b7-475a-99e4-3a926326d1b7", + "name": "voltagePhaseA", + "displayName": "Phase A voltage", + "displayNameEvent": "Phase A volatage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "cc8434f1-02fe-41ad-bc8a-94e57293f8fc", + "name": "voltagePhaseB", + "displayName": "Phase B voltage", + "displayNameEvent": "Phase B voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "fbd673cf-8891-4aef-b112-2829f5fcfe06", + "name": "voltagePhaseC", + "displayName": "Phase C voltage", + "displayNameEvent": "Phase C voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "filter": "adaptive" + }, + { + "id": "66734fc7-b3e4-48d4-8cb7-f0a9f7cef88a", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, + { + "id": "3d908dd2-4a93-4b62-8cbb-2fa9ef4d28fa", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "sunspecStorage", + "displayName": "SunSpec Storage", + "id": "9a643ba8-346c-4127-a2f8-956a7133d75e", + "createMethods": [ "Auto" ], + "interfaces": ["battery", "connectable"], + "paramTypes": [ + { + "id": "219beb96-b9fe-4dd2-a386-ecfbbab8786d", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "3f107844-00c5-4f39-86e5-485b3d1f5c1a", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "7ee31b97-caec-4cdb-80c9-71e232c5fb22", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "2abf786d-18be-46cb-b4a3-7e0934a4c3fd", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "5cf43b80-3e65-4f69-8bf5-91810da0ead4", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown" + } + ], + "stateTypes":[ + { + "id": "25a1fb10-a6b9-4037-b7cf-ad481a65beb4", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "3171a6e0-43a7-4de8-8e20-f748e44af7ac", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "0bf53f80-97f8-488b-b514-58f9fe08c183", + "name": "batteryLevel", + "displayName": "Battery level", + "displayNameEvent": "Battery level changed", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "da2b19c5-0f48-49d1-93f0-abdc0051407d", + "name": "storageStatus", + "displayName": "Status", + "displayNameEvent": "Status changed", + "type": "QString", + "possibleValues": [ + "Off", + "Empty", + "Discharging", + "Charging", + "Full", + "Holding", + "Testing" + ], + "defaultValue": "Off" + }, + { + "id": "f6178427-8e04-45c7-abec-27d04dd5ff04", + "name": "chargingState", + "displayName": "Charging state", + "displayNameEvent": "Charging state changed", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "221a2ef6-0a92-4ff0-87fe-7bd920dbec0b", + "name": "gridCharging", + "displayName": "Grid charging", + "displayNameEvent": "Grid charging changed", + "type": "bool", + "defaultValue": false, + "writable": true, + "displayNameAction": "Set grid charging" + }, + { + "id": "1f530f79-c0d2-466b-90e1-79149e34d92f", + "name": "enableCharging", + "displayName": "Charging", + "displayNameEvent": "Charging changed", + "displayNameAction": "Enable charging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "bc99a159-815a-40ab-a6e8-b46f315305f7", + "name": "enableDischarging", + "displayName": "Discharging", + "displayNameEvent": "Discharging changed", + "displayNameAction": "Enable discharging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "7f469bbc-64a5-4045-8d5f-9a1a85039851", + "name": "chargingRate", + "displayName": "Charging rate", + "displayNameEvent": "Charging rate changed", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": false, + "writable": true, + "displayNameAction": "Set charging rate" + }, + { + "id": "6068f030-acce-44a2-b95f-bd00dd5ca760", + "name": "dischargingRate", + "displayName": "Discharging rate", + "displayNameEvent": "Discharging rate changed", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": false, + "writable": true, + "displayNameAction": "Set discharging rate" + }, + { + "id": "fca0c151-d8fa-4d6e-a5d7-b3bc965abc09", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + }, + { + "name": "solarEdge", + "displayName": "SolarEdge", + "id": "addd7dd8-13e1-405b-ba61-b2d82eaf0426", + "thingClasses": [ + { + "name": "solarEdgeConnection", + "displayName": "SolarEdge", + "id": "7a92bf65-b443-4491-a012-2bec35eb5bf0", + "createMethods": [ "discovery", "user" ], + "interfaces": [ "gateway", "networkdevice" ], + "providedInterfaces": [ "solarinverter", "energymeter", "energystorage" ], + "discoveryParamTypes": [ + { + "id": "9025a4e9-d994-45c7-9a00-62f7abf47107", + "name":"slaveId", + "displayName": "Slave ID", + "type": "int", + "defaultValue": 1 + } + ], + "paramTypes": [ + { + "id": "9c2bafd0-6d56-42e0-8ef3-c4940b4f18b5", + "name": "address", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "2e22c369-8476-4908-9cdc-186472f4d472", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "bb395c12-54d6-4139-b0a6-e31b28bc4d2e", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + }, + { + "id": "1bcede48-d167-4ced-8f1b-bea6302dd43f", + "name":"port", + "displayName": "Port", + "type": "int", + "defaultValue": 502 + }, + { + "id": "b9cc58cf-ad15-447a-a94d-17b9204313bc", + "name":"slaveId", + "displayName": "Slave ID", + "type": "int", + "defaultValue": 1 + } + ], + "stateTypes":[ + { + "id": "f51d4e5a-24ea-41f9-a453-d2bdd68d804c", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + } + ] + }, + { + "name": "solarEdgeBattery", + "displayName": "SolarEdge battery", + "id": "7900471a-da3f-4f03-98be-3e3d9c841546", + "createMethods": [ "Auto" ], + "interfaces": ["energystorage", "connectable"], + "paramTypes": [ + { + "id": "7413cce6-48c1-4924-89bc-915beed67b57", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "8a4b19a9-ee14-42aa-9146-fa84e15970ee", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "e90a3833-1eb6-43ed-a72a-0cf7cb8015ca", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown" + }, + { + "id": "efd99fe4-3a48-4208-829d-14e062015ee7", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes":[ + { + "id": "8addf8aa-f879-4afa-935d-4d2ffa38d05e", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "897616db-e815-4cda-b890-13b37a94ad17", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "a4b0c8c9-44f0-43e2-ab25-63fdc9772fb8", + "name": "batteryLevel", + "displayName": "Battery level", + "displayNameEvent": "Battery level changed", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "815b0826-c392-4249-a271-19e92ebe46e0", + "name": "chargingState", + "displayName": "Charging state", + "displayNameEvent": "Charging state changed", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "f6c6f548-8a12-4a62-a641-08bb003c55fa", + "name": "ratedEnergy", + "displayName": "Rated energy", + "displayNameEvent": "Rated energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "f46298e5-d06c-4ebe-869a-d9feff1a4689", + "name": "averageTemperature", + "displayName": "Average temperature", + "displayNameEvent": "Average temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00 + }, + { + "id": "9d17defb-fc6e-4c10-af53-14b796222a14", + "name": "instantaneousVoltage", + "displayName": "Instantaneous voltage", + "displayNameEvent": "Instantaneous voltage changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00 + }, + { + "id": "89f58155-ca0c-48a8-aa7f-6e4d1b1bad36", + "name": "instantaneousCurrent", + "displayName": "Instantaneous current", + "displayNameEvent": "Instantaneous current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00 + }, + { + "id": "caccf9fd-0f83-4dd5-ae48-a497f13d2a2e", + "name": "currentPower", + "displayName": "Instantaneous power", + "displayNameEvent": "Instantaneous power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00 + }, + { + "id": "d8cc799d-9709-4342-8766-52ff5f12aecc", + "name": "maxEnergy", + "displayName": "Maximum energy", + "displayNameEvent": "Maximum energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "7ac7ccd1-b5d1-488b-a99b-98ef000b7d03", + "name": "capacity", + "displayName": "Available energy", + "displayNameEvent": "Available energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "24b501db-f301-422d-ad58-f16ca025f6ee", + "name": "stateOfHealth", + "displayName": "State of health (SOH)", + "displayNameEvent": "State of health (SOH) changed", + "type": "double", + "unit": "Percentage", + "defaultValue": 0.00 + }, + { + "id": "9c24b27a-3ea1-4db1-8868-6c999aad0a18", + "name": "batteryStatus", + "displayName": "Battery status", + "displayNameEvent": "Battery status changed", + "type": "QString", + "possibleValues": [ + "Off", + "Standby", + "Init", + "Charging", + "Discharging", + "Fault", + "Holding", + "Idle" + ], + "defaultValue": "Idle" + }, + { + "id": "db2e0304-4f0e-461f-8bab-c63fcb5deb1b", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} diff --git a/sunspec/meta.json b/sunspec/meta.json new file mode 100644 index 0000000..8d08966 --- /dev/null +++ b/sunspec/meta.json @@ -0,0 +1,14 @@ +{ + "title": "SunSpec", + "tagline": "Connect to SunSpec devices.", + "icon": "sunspec.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network", + "modbus" + ], + "categories": [ + "energy" + ] +} diff --git a/sunspec/solaredgebattery.cpp b/sunspec/solaredgebattery.cpp new file mode 100644 index 0000000..22e8373 --- /dev/null +++ b/sunspec/solaredgebattery.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "solaredgebattery.h" +#include "extern-plugininfo.h" + +#include +#include + +SolarEdgeBattery::SolarEdgeBattery(Thing *thing, SunSpecConnection *connection, int modbusStartRegister, QObject *parent) : + SunSpecThing(thing, nullptr, parent), + m_connection(connection), + m_modbusStartRegister(modbusStartRegister) +{ + m_timer.setSingleShot(true); + m_timer.setInterval(10000); + connect(&m_timer, &QTimer::timeout, this, [this](){ + if (!m_initFinishedSuccess) { + emit initFinished(false); + } + }); +} + +SunSpecConnection *SolarEdgeBattery::connection() const +{ + return m_connection; +} + +quint16 SolarEdgeBattery::modbusStartRegister() const +{ + return m_modbusStartRegister; +} + +SolarEdgeBattery::BatteryData SolarEdgeBattery::batteryData() const +{ + return m_batteryData; +} + +void SolarEdgeBattery::init() +{ + qCDebug(dcSunSpec()) << "Initializing battery on" << m_modbusStartRegister; + m_initFinishedSuccess = false; + readBlockData(); + m_timer.start(); +} + +void SolarEdgeBattery::readBlockData() +{ + // Read the data in 2 block requests + qCDebug(dcSunSpec()) << "SolarEdgeBattery: Read block 1 from modbus address" << m_modbusStartRegister << "length" << 107<< ", Slave ID" << m_connection->slaveId(); + + // Total possible block size is 0xE19A - 0xE100 = 0x9A = 153 registers + + // First block request 0x00 - 0x4C + QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, m_modbusStartRegister, 0x4C); + if (QModbusReply *reply = m_connection->modbusTcpClient()->sendReadRequest(request, m_connection->slaveId())) { + if (!reply->isFinished()) { + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [=]() { + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Read response error:" << reply->error(); + if (!m_initFinishedSuccess) { + m_timer.stop(); + emit initFinished(false); + } + return; + } + + // Example data: + // "(0x3438, 0x565f, 0x4c47, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x4c47, 0x4320, 0x5245, 0x5355, 0x2031, 0x3000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00ff, 0x0000, 0xffff, 0xff7f, 0xffff, 0xff7f, 0xffff, 0xff7f, 0xffff, 0xff7f, 0xffff, 0xff7f)" + // 255 "48V_LG" "LGC RESU 10" "" "" + // "(0x3438, 0x565f, 0x4c47, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x4c47, 0x4320, 0x5245, 0x5355, 0x2031, 0x3000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x3438, 0x5620, 0x4443, 0x4443, 0x2032, 0x2e32, 0x2e39, 0x3120, 0x424d, 0x5320, 0x302e, 0x302e, 0x3000, 0x0000, 0x0000, 0x0000, 0x3745, 0x3034, 0x3432, 0x4543, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0070, 0x0000, 0x2000, 0x4619, 0x4000, 0x459c, 0x4000, 0x459c, 0x4000, 0x44ce, 0x4000, 0x459c)" + // 112 "48V_LG" "LGC RESU 10" "48V DCDC 2.2.91 BMS 0.0.0" "7E0442EC" + + + const QModbusDataUnit unit = reply->result(); + QVector values = unit.values(); + qCDebug(dcSunSpec()) << "SolarEdgeBattery: Received first block data" << m_modbusStartRegister << values.length(); + qCDebug(dcSunSpec()) << "SolarEdgeBattery:" << SunSpecDataPoint::registersToString(values); + + m_batteryData.manufacturerName = SunSpecDataPoint::convertToString(values.mid(ManufacturerName, 16)); + m_batteryData.model = SunSpecDataPoint::convertToString(values.mid(Model, 16)); + m_batteryData.firmwareVersion = SunSpecDataPoint::convertToString(values.mid(FirmwareVersion, 16)); + m_batteryData.serialNumber = SunSpecDataPoint::convertToString(values.mid(SerialNumber, 16)); + m_batteryData.batteryDeviceId = values[BatteryDeviceId]; + qCDebug(dcSunSpec()) << "SolarEdgeBattery:" << m_batteryData.batteryDeviceId << m_batteryData.manufacturerName << m_batteryData.model << m_batteryData.firmwareVersion << m_batteryData.serialNumber; + + // Check if there is a battery connected, if so, one of the string must contain vaild data... + if (m_batteryData.manufacturerName.isEmpty() && m_batteryData.model.isEmpty() && m_batteryData.serialNumber.isEmpty() && m_batteryData.firmwareVersion.isEmpty()) { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: No valid information detected about the battery. Probably no battery connected at register" << m_modbusStartRegister; + if (!m_initFinishedSuccess) { + m_timer.stop(); + emit initFinished(false); + } + return; + } + + // For some reason, there might be even data in there but no battery connected, let's check if there are invalid registers + + // Check if there is a battery connected, if so, one of the string must contain vaild data... + const QVector invalidRegisters = { 0xffff, 0xff7f }; + if (values.mid(RatedEnergy, 2) == invalidRegisters && values.mid(MaxChargeContinuesPower, 2) == invalidRegisters && + values.mid(MaxDischargeContinuesPower, 2) == invalidRegisters && values.mid(MaxChargePeakPower, 2) == invalidRegisters && + values.mid(MaxDischargePeakPower, 2) == invalidRegisters) { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: No valid information detected about the battery. Probably no battery connected at register" << m_modbusStartRegister; + if (!m_initFinishedSuccess) { + m_timer.stop(); + emit initFinished(false); + } + return; + } + + m_batteryData.ratedEnergy = SunSpecDataPoint::convertToFloat32(values.mid(RatedEnergy, 2)); + m_batteryData.maxChargeContinuesPower = SunSpecDataPoint::convertToFloat32(values.mid(MaxChargeContinuesPower, 2)); + m_batteryData.maxDischargeContinuesPower = SunSpecDataPoint::convertToFloat32(values.mid(MaxDischargeContinuesPower, 2)); + m_batteryData.maxChargePeakPower = SunSpecDataPoint::convertToFloat32(values.mid(MaxChargePeakPower, 2)); + m_batteryData.maxDischargePeakPower = SunSpecDataPoint::convertToFloat32(values.mid(MaxDischargePeakPower, 2)); + + // First block looks good, continue with second block + + // 8192 17945 536888857 536888857 1.08652e-19 + // 0x2000 0x4619 + + // Read from 0x6c to 0x86 + int offset = 0x6c; + QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, m_modbusStartRegister + offset, 28); + if (QModbusReply *reply = m_connection->modbusTcpClient()->sendReadRequest(request, m_connection->slaveId())) { + if (!reply->isFinished()) { + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [=]() { + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Read response error:" << reply->error(); + if (!m_initFinishedSuccess) { + m_timer.stop(); + emit initFinished(false); + } + return; + } + + const QModbusDataUnit unit = reply->result(); + QVector values = unit.values(); + + qCDebug(dcSunSpec()) << "SolarEdgeBattery: Received second block data" << m_modbusStartRegister + offset << values.length(); + qCDebug(dcSunSpec()) << "SolarEdgeBattery:" << SunSpecDataPoint::registersToString(values); + QVector valueRegisters; + + valueRegisters = values.mid(BatteryAverageTemperature - offset, 2); + m_batteryData.averageTemperature = SunSpecDataPoint::convertToFloat32(valueRegisters); + qCDebug(dcSunSpec()) << "SolarEdgeBattery: Average temperature:" << SunSpecDataPoint::registersToString(valueRegisters) << m_batteryData.averageTemperature; + + m_batteryData.maxTemperature = SunSpecDataPoint::convertToFloat32(values.mid(BatteryMaxTemperature - offset, 2)); + m_batteryData.instantaneousVoltage = SunSpecDataPoint::convertToFloat32(values.mid(InstantaneousVoltage - offset, 2)); + m_batteryData.instantaneousCurrent = SunSpecDataPoint::convertToFloat32(values.mid(InstantaneousCurrent - offset, 2)); + m_batteryData.instantaneousPower = SunSpecDataPoint::convertToFloat32(values.mid(InstantaneousPower - offset, 2)); + m_batteryData.maxEnergy = SunSpecDataPoint::convertToFloat32(values.mid(MaxEnergy - offset, 2)); + + valueRegisters = values.mid(AvailableEnergy - offset, 2); + m_batteryData.availableEnergy = SunSpecDataPoint::convertToFloat32(valueRegisters); + qCDebug(dcSunSpec()) << "SolarEdgeBattery: Available energy:" << (AvailableEnergy - offset) << SunSpecDataPoint::registersToString(valueRegisters) << m_batteryData.availableEnergy; + + m_batteryData.stateOfHealth = SunSpecDataPoint::convertToFloat32(values.mid(StateOfHealth - offset, 2)); + m_batteryData.stateOfEnergy = SunSpecDataPoint::convertToFloat32(values.mid(StateOfEnergy - offset, 2)); + m_batteryData.batteryStatus = static_cast(SunSpecDataPoint::convertToUInt32(values.mid(Status - offset, 2))); + + if (!m_initFinishedSuccess) { + m_timer.stop(); + m_initFinishedSuccess = true; + emit initFinished(true); + } + + emit blockDataUpdated(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [] (QModbusDevice::Error error) { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Modbus reply error:" << error; + }); + } else { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Read error: " << m_connection->modbusTcpClient()->errorString(); + reply->deleteLater(); // broadcast replies return immediately + if (!m_initFinishedSuccess) { + m_timer.stop(); + emit initFinished(false); + } + return; + } + } else { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Read error: " << m_connection->modbusTcpClient()->errorString(); + return; + } + }); + + connect(reply, &QModbusReply::errorOccurred, this, [] (QModbusDevice::Error error) { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Modbus reply error:" << error; + }); + } else { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Read error: " << m_connection->modbusTcpClient()->errorString(); + reply->deleteLater(); // broadcast replies return immediately + if (!m_initFinishedSuccess) { + m_timer.stop(); + emit initFinished(false); + } + return; + } + } else { + qCWarning(dcSunSpec()) << "SolarEdgeBattery: Read error: " << m_connection->modbusTcpClient()->errorString(); + return; + } +} + +QDebug operator<<(QDebug debug, const SolarEdgeBattery::BatteryData &batteryData) +{ + debug << "SolarEdgeBatteryData(" << batteryData.manufacturerName << "-" << batteryData.model << ")" << "\n"; + debug << " - Battery Device ID" << batteryData.batteryDeviceId << "\n"; + debug << " - Firmware version" << batteryData.firmwareVersion << "\n"; + debug << " - Serial number" << batteryData.serialNumber << "\n"; + debug << " - Rated Energy" << batteryData.ratedEnergy << "W * H" << "\n"; + debug << " - Max charging continues power" << batteryData.maxChargeContinuesPower << "W" << "\n"; + debug << " - Max discharging continues power" << batteryData.maxDischargeContinuesPower << "W" << "\n"; + debug << " - Max charging peak power" << batteryData.maxChargePeakPower << "W" << "\n"; + debug << " - Max discharging peak power" << batteryData.maxDischargePeakPower << "W" << "\n"; + debug << " - Average temperature" << batteryData.averageTemperature << "°C" << "\n"; + debug << " - Max temperature" << batteryData.maxTemperature << "°C" << "\n"; + debug << " - Instantuouse Voltage" << batteryData.instantaneousVoltage << "V" << "\n"; + debug << " - Instantuouse Current" << batteryData.instantaneousCurrent << "A" << "\n"; + debug << " - Instantuouse Power" << batteryData.instantaneousPower << "W" << "\n"; + debug << " - Max energy" << batteryData.maxEnergy << "W * H" << "\n"; + debug << " - Available energy" << batteryData.availableEnergy << "W * H" << "\n"; + debug << " - State of health" << batteryData.stateOfHealth << "%" << "\n"; + debug << " - State of energy" << batteryData.stateOfEnergy << "%" << "\n"; + debug << " - Battery status" << batteryData.batteryStatus << "\n"; + return debug; +} diff --git a/sunspec/solaredgebattery.h b/sunspec/solaredgebattery.h new file mode 100644 index 0000000..824f7cf --- /dev/null +++ b/sunspec/solaredgebattery.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SOLAREDGEBATTERY_H +#define SOLAREDGEBATTERY_H + +#include +#include +#include + +#include "sunspecthing.h" + +/* Note: this is actually not a sunspec thing, since SolarEdge provide battery information using a non SunSpec modbus register section. + * For keeping the code as simple as possible we make it a SunSpec thing */ + +class SolarEdgeBattery : public SunSpecThing +{ + Q_OBJECT +public: + enum BatteryRegister { + ManufacturerName = 0x00, + Model = 0x10, + FirmwareVersion = 0x20, + SerialNumber = 0x30, + BatteryDeviceId = 0x40, + RatedEnergy = 0x42, // W * H + MaxChargeContinuesPower = 0x44, // W + MaxDischargeContinuesPower = 0x46, // W + MaxChargePeakPower = 0x48, // W + MaxDischargePeakPower = 0x4A, // W + // 32 byte reserved + BatteryAverageTemperature = 0x6c, // °C + BatteryMaxTemperature = 0x6E, + InstantaneousVoltage = 0x70, // V + InstantaneousCurrent = 0x72, // A + InstantaneousPower = 0x74, // W + LifetimeExportEnergyCounter = 0x76, // W * H + LifetimeImportEnergyCounter = 0x7A, // W * H + MaxEnergy = 0x7E, // W * H + AvailableEnergy = 0x80, // W * H + StateOfHealth = 0x82, + StateOfEnergy = 0x84, + Status = 0x86 + }; + Q_ENUM(BatteryRegister) + + enum BatteryStatus { + Off = 0, + Standby = 1, + Init = 2, + Charge = 3, + Discharge = 4, + Fault = 5, + Holding = 6, + Idle = 7 + }; + Q_ENUM(BatteryStatus) + + struct BatteryData { + QString manufacturerName; + QString model; + QString firmwareVersion; + QString serialNumber; + quint16 batteryDeviceId; + float ratedEnergy; + float maxChargeContinuesPower; + float maxDischargeContinuesPower; + float maxChargePeakPower; + float maxDischargePeakPower; + float averageTemperature; + float maxTemperature; + float instantaneousVoltage; + float instantaneousCurrent; + float instantaneousPower; + quint64 lifetimeExportEnergyCounter; + quint64 lifetimeImportEnergyCounter; + float maxEnergy; + float availableEnergy; + float stateOfHealth; + float stateOfEnergy; + BatteryStatus batteryStatus; + }; + + explicit SolarEdgeBattery(Thing *thing, SunSpecConnection *connection, int modbusStartRegister, QObject *parent = nullptr); + + SunSpecConnection *connection() const override; + + quint16 modbusStartRegister() const override; + + BatteryData batteryData() const; + + void init(); + void readBlockData() override; + +signals: + void initFinished(bool success); + void blockDataUpdated(); + +private: + QTimer m_timer; + SunSpecConnection *m_connection = nullptr; + int m_modbusStartRegister; + bool m_initFinishedSuccess = false; + BatteryData m_batteryData; + +}; + +QDebug operator<<(QDebug debug, const SolarEdgeBattery::BatteryData &batteryData); + +#endif // SOLAREDGEBATTERY_H diff --git a/sunspec/sunspec.png b/sunspec/sunspec.png new file mode 100644 index 0000000..7a52a45 Binary files /dev/null and b/sunspec/sunspec.png differ diff --git a/sunspec/sunspec.pro b/sunspec/sunspec.pro new file mode 100644 index 0000000..df3837a --- /dev/null +++ b/sunspec/sunspec.pro @@ -0,0 +1,14 @@ +include(../plugins.pri) +include(../sunspec.pri) + +SOURCES += \ + integrationpluginsunspec.cpp \ + solaredgebattery.cpp \ + sunspecdiscovery.cpp \ + sunspecthing.cpp + +HEADERS += \ + integrationpluginsunspec.h \ + solaredgebattery.h \ + sunspecdiscovery.h \ + sunspecthing.h diff --git a/sunspec/sunspecdiscovery.cpp b/sunspec/sunspecdiscovery.cpp new file mode 100644 index 0000000..a38acaf --- /dev/null +++ b/sunspec/sunspecdiscovery.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "sunspecdiscovery.h" + +#include +#include +#include + +SunSpecDiscovery::SunSpecDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 slaveId, SunSpecDataPoint::ByteOrder byteOrder, QObject *parent) + : QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery}, + m_slaveId{slaveId}, + m_byteOrder{byteOrder} +{ + m_scanPorts.append(502); + m_scanPorts.append(1502); +} + +QList SunSpecDiscovery::results() const +{ + return m_results; +} + +void SunSpecDiscovery::addCustomDiscoveryPort(quint16 port) +{ + if (m_scanPorts.contains(port)) + return; + + m_scanPorts.append(port); +} + +void SunSpecDiscovery::startDiscovery() +{ + qCInfo(dcSunSpec()) << "Discovery: Start searching for SunSpec devices in the network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + m_startDateTime = QDateTime::currentDateTime(); + + // Imedialty check any new device gets discovered + connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &SunSpecDiscovery::checkNetworkDevice); + + // Check what might be left on finished + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [this, discoveryReply](){ + qCDebug(dcSunSpec()) << "Discovery: Network discovery finished. Give some time for pending discovery checks to finish..."; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + + // Give the last connections added right before the network discovery finished a chance to check the device... + QTimer::singleShot(3000, this, [this](){ + finishDiscovery(); + }); + }); +} + +void SunSpecDiscovery::testNextConnection(const QHostAddress &address) +{ + if (!m_pendingConnectionAttempts.contains(address)) + return; + + SunSpecConnection *connection = m_pendingConnectionAttempts[address].dequeue(); + if (m_pendingConnectionAttempts.value(address).isEmpty()) + m_pendingConnectionAttempts.remove(address); + + qCDebug(dcSunSpec()) << "Discovery: Start searching on" << QString("%1:%2").arg(address.toString()).arg(connection->port()) << "slave ID:" << connection->slaveId(); + // Try to connect, maybe it works, maybe not... + if (!connection->connectDevice()) { + qCDebug(dcSunSpec()) << "Discovery: Failed to connect to" << QString("%1:%2").arg(address.toString()).arg(connection->port()) << "slave ID:" << connection->slaveId() << "Continue...";; + cleanupConnection(connection); + return; + } + + /* Some connection will block the connection process and the connection will pending in the + * connecting state for over a minute before failing. + * This behavior blocks further connection tests for during the disdovery process and the discovery failes to detect the device. + * This timer will make sure the connection can be established within 5 seconds, otherwise we continue with the next connection. + */ + + QTimer *connectionTimer = new QTimer(connection); + connectionTimer->setInterval(5000); + connectionTimer->setSingleShot(true); + + m_connectionTimers.insert(connection, connectionTimer); + + connect(connectionTimer, &QTimer::timeout, connection, [this, connection, connectionTimer](){ + qCDebug(dcSunSpec()) << "Discovery: Could not establish a connection within" << connectionTimer->interval() << "ms. Continue..."; + m_connectionTimers.remove(connection); + connection->disconnectDevice(); + // Note: since the connection is the parent of the timer, it will be deleted with the connection + cleanupConnection(connection); + }); + + // Once connected, the timer will be stopped. + connectionTimer->start(); +} + +void SunSpecDiscovery::checkNetworkDevice(const QHostAddress &address) +{ + // Create a connection queue for this network device + + QQueue connectionQueue; + + // Check all ports for this host + foreach (quint16 port, m_scanPorts) { + + + SunSpecConnection *connection = new SunSpecConnection(address, port, m_slaveId, m_byteOrder, this); + connection->setNumberOfRetries(1); + connection->setTimeout(500); + m_connections.append(connection); + connectionQueue.enqueue(connection); + + connect(connection, &SunSpecConnection::connectedChanged, this, [this, connection, connectionQueue, address](bool connected){ + if (!connected) { + // Disconnected ... done with this connection + cleanupConnection(connection); + return; + } + + // Successfully connected, we can stop the connection timer which takes care about blocking connection attempts + if (m_connectionTimers.contains(connection)) { + QTimer *connectionTimer = m_connectionTimers.take(connection); + connectionTimer->stop(); + connectionTimer->deleteLater(); + } + + // Modbus TCP connected, try to discovery sunspec models... + connect(connection, &SunSpecConnection::discoveryFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcSunSpec()) << "Discovery: SunSpec discovery failed on" + << QString("%1:%2").arg(address.toString()).arg(connection->port()) + << "slave ID:" << connection->slaveId() << "Continue..."; + cleanupConnection(connection); + return; + } + + // Success, we found some sunspec models here, let's read some infomation from the models + + Result result; + result.address = address; + result.port = connection->port(); + result.slaveId = connection->slaveId(); + + qCDebug(dcSunSpec()) << "Discovery: --> Found SunSpec devices on" << result.networkDeviceInfo << "port" << result.port << "slave ID:" << result.slaveId; + foreach (SunSpecModel *model, connection->models()) { + if (model->modelId() == SunSpecModelFactory::ModelIdCommon) { + SunSpecCommonModel *commonModel = qobject_cast(model); + QString manufacturer = commonModel->manufacturer(); + if (!manufacturer.isEmpty() && !result.modelManufacturers.contains(manufacturer)) { + result.modelManufacturers.append(manufacturer); + } + } + } + + m_results.append(result); + + // Done with this connection + cleanupConnection(connection); + }); + + // Run SunSpec discovery on connection... + if (!connection->startDiscovery()) { + qCDebug(dcSunSpec()) << "Discovery: Unable to discover SunSpec data on connection" + << QString("%1:%2").arg(address.toString()).arg(connection->port()) + << "slave ID:" << connection->slaveId() << "Continue..."; + cleanupConnection(connection); + } + }); + + // If we get any error...skip this host... + connect(connection->modbusTcpClient(), &QModbusTcpClient::errorOccurred, this, [=](QModbusDevice::Error error){ + if (error != QModbusDevice::NoError) { + qCDebug(dcSunSpec()) << "Discovery: Connection error on" + << QString("%1:%2").arg(address.toString()).arg(connection->port()) + << "slave ID:" << connection->slaveId() << "Continue..."; + cleanupConnection(connection); + } + }); + } + + m_pendingConnectionAttempts[address] = connectionQueue; + testNextConnection(address); +} + +void SunSpecDiscovery::cleanupConnection(SunSpecConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); + + if (m_connectionTimers.contains(connection)) { + QTimer *connectionTimer = m_connectionTimers.take(connection); + connectionTimer->stop(); + } + + testNextConnection(connection->hostAddress()); +} + +void SunSpecDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Fill in all network device infos we have + for (int i = 0; i < m_results.length(); i++) + m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address); + + // Cleanup any leftovers...we don't care any more + foreach (SunSpecConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcSunSpec()) << "Discovery: Finished the discovery process. Found" << m_results.length() + << "SunSpec devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit discoveryFinished(); +} diff --git a/sunspec/sunspecdiscovery.h b/sunspec/sunspecdiscovery.h new file mode 100644 index 0000000..0037c95 --- /dev/null +++ b/sunspec/sunspecdiscovery.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SUNSPECDISCOVERY_H +#define SUNSPECDISCOVERY_H + +#include +#include +#include + +#include +#include + +class SunSpecDiscovery : public QObject +{ + Q_OBJECT +public: + explicit SunSpecDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 slaveId, SunSpecDataPoint::ByteOrder byteOrder = SunSpecDataPoint::ByteOrderLittleEndian, QObject *parent = nullptr); + typedef struct Result { + QHostAddress address; + NetworkDeviceInfo networkDeviceInfo; + quint16 port; + quint16 slaveId; + QStringList modelManufacturers; + } Result; + + QList results() const; + + void addCustomDiscoveryPort(quint16 port); + void startDiscovery(); + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QList m_scanPorts; + quint16 m_slaveId = 1; + SunSpecDataPoint::ByteOrder m_byteOrder; + + QDateTime m_startDateTime; + QHash> m_pendingConnectionAttempts; + QHash m_connectionTimers; + QList m_connections; + NetworkDeviceInfos m_networkDeviceInfos; + QList m_results; + + void testNextConnection(const QHostAddress &address); + + void checkNetworkDevice(const QHostAddress &address); + void cleanupConnection(SunSpecConnection *connection); + + void finishDiscovery(); +}; + +#endif // SUNSPECDISCOVERY_H diff --git a/sunspec/sunspecthing.cpp b/sunspec/sunspecthing.cpp new file mode 100644 index 0000000..f8b27cb --- /dev/null +++ b/sunspec/sunspecthing.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "sunspecthing.h" + +SunSpecThing::SunSpecThing(Thing *thing, SunSpecModel *model, QObject *parent) : + QObject(parent), + m_thing(thing), + m_model(model) +{ + +} + +SunSpecConnection *SunSpecThing::connection() const +{ + if (!m_model) + return nullptr; + + return m_model->connection(); +} + +SunSpecModel *SunSpecThing::model() const +{ + return m_model; +} + +Thing *SunSpecThing::thing() const +{ + return m_thing; +} + +quint16 SunSpecThing::modbusStartRegister() const +{ + if (!m_model) + return 0xFFFF; + + return m_model->modbusStartRegister(); +} + +void SunSpecThing::executeAction(ThingActionInfo *info) +{ + Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(info->action().actionTypeId().toString()).toUtf8()); + info->finish(Thing::ThingErrorActionTypeNotFound); +} + +void SunSpecThing::onBlockDataUpdated() +{ + // Reimplement if required +} diff --git a/sunspec/sunspecthing.h b/sunspec/sunspecthing.h new file mode 100644 index 0000000..b21455c --- /dev/null +++ b/sunspec/sunspecthing.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2013 - 2024, nymea GmbH +* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* +* This file is part of nymea-plugins-modbus. +* +* nymea-plugins-modbus is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* nymea-plugins-modbus 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 General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with nymea-plugins-modbus. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SUNSPECTHING_H +#define SUNSPECTHING_H + +#include + +#include "extern-plugininfo.h" + +#include + +#include +#include + +class SunSpecThing : public QObject +{ + Q_OBJECT +public: + explicit SunSpecThing(Thing *thing, SunSpecModel *model, QObject *parent = nullptr); + virtual ~SunSpecThing() = default; + + virtual SunSpecConnection *connection() const; + SunSpecModel *model() const; + Thing *thing() const; + + virtual quint16 modbusStartRegister() const; + + virtual void readBlockData() = 0; + + virtual void executeAction(ThingActionInfo *info); + +protected: + Thing *m_thing = nullptr; + SunSpecModel *m_model = nullptr; + +private slots: + virtual void onBlockDataUpdated(); + +}; + +#endif // SUNSPECTHING_H diff --git a/sunspec/test/sunspec_server.sh b/sunspec/test/sunspec_server.sh new file mode 100755 index 0000000..fb09118 --- /dev/null +++ b/sunspec/test/sunspec_server.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +if ! command -v suns &> /dev/null +then + echo "suns could not be found" + echo "... installing" + sudo apt update + sudo apt install libmodbus-dev flex bison git + + if [ ! -d "sunspec"]; then + git clone https://github.com/Boernsman/sunspec.git + fi + cd ./sunspec/src + make + sudo make install +fi + +echo "Startin sunspec test server" +sudo suns -s -vvvv -m models/test/composite_superdevice.model diff --git a/sunspec/translations/cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b-en_US.ts b/sunspec/translations/cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b-en_US.ts new file mode 100644 index 0000000..62d6250 --- /dev/null +++ b/sunspec/translations/cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b-en_US.ts @@ -0,0 +1,976 @@ + + + + + IntegrationPluginSunSpec + + + The discovery is not available. + + + + + The SunSpec connection is not connected. + + + + + Single Phase Inverter + + + + + Split Phase Inverter + + + + + Three Phase Inverter + + + + + Single Phase Meter + + + + + Split Phase Meter + + + + + Three Phase Meter + + + + + Storage + + + + + The SunSpec discovery finished with errors. Please make sure this is a SunSpec device. + + + + + Ground fault + + + + + DC over voltage + + + + + AC disconnect + + + + + DC disconnect + + + + + Grid disconnect + + + + + Cabinet open + + + + + Manual shutdown + + + + + Over temperature + + + + + Over frequency + + + + + Under frequency + + + + + AC over voltage + + + + + AC under voltage + + + + + Blown string fuse + + + + + Under temperature + + + + + Memory loss + + + + + Hardware test failure + + + + + No error + + + + + SunSpec + + + + + AC energy + The name of the StateType ({d493880d-eb58-4530-8010-8ea4f6d63387}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({fe3f8a65-121a-4ae1-b22a-ae325dc3e7e6}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({4c0407b3-5cd5-438d-bfa8-9a8d6695b458}) of ThingClass sunspecSinglePhaseInverter + + + + + + + AC power + The name of the StateType ({14036f44-25fd-4e93-8e8c-c677b06a2c34}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({9235eb4b-906c-4557-8e18-bca268a367cc}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({f49591d6-d759-4be3-bafc-b6a7a72cf023}) of ThingClass sunspecSinglePhaseInverter + + + + + Available energy + The name of the StateType ({7ac7ccd1-b5d1-488b-a99b-98ef000b7d03}) of ThingClass solarEdgeBattery + + + + + Average temperature + The name of the StateType ({f46298e5-d06c-4ebe-869a-d9feff1a4689}) of ThingClass solarEdgeBattery + + + + + + Battery critical + The name of the StateType ({897616db-e815-4cda-b890-13b37a94ad17}) of ThingClass solarEdgeBattery +---------- +The name of the StateType ({3171a6e0-43a7-4de8-8e20-f748e44af7ac}) of ThingClass sunspecStorage + + + + + + Battery level + The name of the StateType ({a4b0c8c9-44f0-43e2-ab25-63fdc9772fb8}) of ThingClass solarEdgeBattery +---------- +The name of the StateType ({0bf53f80-97f8-488b-b514-58f9fe08c183}) of ThingClass sunspecStorage + + + + + Battery status + The name of the StateType ({9c24b27a-3ea1-4db1-8868-6c999aad0a18}) of ThingClass solarEdgeBattery + + + + + + + Cabinet temperature + The name of the StateType ({44b0320f-89a7-4248-bad4-288ef898a5cc}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({6d314d50-b990-4a58-a37f-4a3da42c4407}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({51461bde-1a6b-4aa1-94cc-59829ea0a7c8}) of ThingClass sunspecSinglePhaseInverter + + + + + + Charging + The name of the ParamType (ThingClass: sunspecStorage, ActionType: enableCharging, ID: {1f530f79-c0d2-466b-90e1-79149e34d92f}) +---------- +The name of the StateType ({1f530f79-c0d2-466b-90e1-79149e34d92f}) of ThingClass sunspecStorage + + + + + + Charging rate + The name of the ParamType (ThingClass: sunspecStorage, ActionType: chargingRate, ID: {7f469bbc-64a5-4045-8d5f-9a1a85039851}) +---------- +The name of the StateType ({7f469bbc-64a5-4045-8d5f-9a1a85039851}) of ThingClass sunspecStorage + + + + + + Charging state + The name of the StateType ({815b0826-c392-4249-a271-19e92ebe46e0}) of ThingClass solarEdgeBattery +---------- +The name of the StateType ({f6178427-8e04-45c7-abec-27d04dd5ff04}) of ThingClass sunspecStorage + + + + + + + + + + + + + + Connected + The name of the StateType ({8addf8aa-f879-4afa-935d-4d2ffa38d05e}) of ThingClass solarEdgeBattery +---------- +The name of the StateType ({f51d4e5a-24ea-41f9-a453-d2bdd68d804c}) of ThingClass solarEdgeConnection +---------- +The name of the StateType ({25a1fb10-a6b9-4037-b7cf-ad481a65beb4}) of ThingClass sunspecStorage +---------- +The name of the StateType ({36f861c7-afc1-4725-b41f-67113200d78f}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({34e34ec9-dab0-438c-9493-a3068bc401de}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({d960e7b1-d4aa-4cab-8f54-6bcfdbb8be36}) of ThingClass sunspecSinglePhaseMeter +---------- +The name of the StateType ({4401468c-0385-40a9-b436-daf7ed6a50d5}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({27b49640-f58b-466e-a225-a4663cf3ed96}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({48bf31c4-fda7-41e5-a3ef-3011bf96e104}) of ThingClass sunspecSinglePhaseInverter +---------- +The name of the StateType ({3e767ad0-b4b3-4398-94c1-00579ea09ca8}) of ThingClass sunspecConnection + + + + + + Current power phase A + The name of the StateType ({0e3e8870-3d59-4ee8-b7aa-33bda68b6ae3}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({57c0f5ba-a104-424b-b8d4-8bab90acf009}) of ThingClass sunspecSplitPhaseMeter + + + + + + Current power phase B + The name of the StateType ({453d046c-3a66-4590-882c-1283882918fc}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({456f7cdf-9756-49b8-9084-f212199d4014}) of ThingClass sunspecSplitPhaseMeter + + + + + Current power phase C + The name of the StateType ({ba4e94de-9136-48a7-aad8-3d5cd0e7d968}) of ThingClass sunspecThreePhaseMeter + + + + + + + DC current + The name of the StateType ({81a989aa-4104-438e-bfc5-4f44ce5ceeaf}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({68e7239f-d7b2-4ab2-9cf3-e769fb5ddfc0}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({79bd1748-b540-467d-b6fa-b0cc56d9775a}) of ThingClass sunspecSinglePhaseInverter + + + + + + + DC power + The name of the StateType ({1397d935-1206-4ceb-883d-1fcf41ba6214}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({560b5ec8-1b2c-47d3-b55e-40f4d0aebfe0}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({852a8c28-b14d-492e-b2b3-f22219f4ed36}) of ThingClass sunspecSinglePhaseInverter + + + + + + + DC voltage + The name of the StateType ({8c1c71c4-28fd-472a-83a6-3e96ab03d677}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({d36bcaba-1e77-431e-89dc-af8ac6172d7d}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({157376b8-19ae-415f-b693-cb0a9ba88863}) of ThingClass sunspecSinglePhaseInverter + + + + + + + + + + + + Device model + The name of the ParamType (ThingClass: solarEdgeBattery, Type: thing, ID: {e90a3833-1eb6-43ed-a72a-0cf7cb8015ca}) +---------- +The name of the ParamType (ThingClass: sunspecStorage, Type: thing, ID: {2abf786d-18be-46cb-b4a3-7e0934a4c3fd}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseMeter, Type: thing, ID: {00d308dc-e9a3-40e4-acad-3cad2ca3590f}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseMeter, Type: thing, ID: {6bc978cb-a779-4f5f-87a4-3cbbe2a55ca8}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseMeter, Type: thing, ID: {e31333c8-746d-4955-af0a-ae18d7100de7}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseInverter, Type: thing, ID: {938b6555-e0af-4222-b85b-90bc1bc78c8f}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseInverter, Type: thing, ID: {d40b1b9d-994d-422c-8a41-b98cf03c1678}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseInverter, Type: thing, ID: {12f1b02c-033d-423f-a217-955ddb995dd5}) + + + + + + Discharging + The name of the ParamType (ThingClass: sunspecStorage, ActionType: enableDischarging, ID: {bc99a159-815a-40ab-a6e8-b46f315305f7}) +---------- +The name of the StateType ({bc99a159-815a-40ab-a6e8-b46f315305f7}) of ThingClass sunspecStorage + + + + + + Discharging rate + The name of the ParamType (ThingClass: sunspecStorage, ActionType: dischargingRate, ID: {6068f030-acce-44a2-b95f-bd00dd5ca760}) +---------- +The name of the StateType ({6068f030-acce-44a2-b95f-bd00dd5ca760}) of ThingClass sunspecStorage + + + + + Enable charging + The name of the ActionType ({1f530f79-c0d2-466b-90e1-79149e34d92f}) of ThingClass sunspecStorage + + + + + Enable discharging + The name of the ActionType ({bc99a159-815a-40ab-a6e8-b46f315305f7}) of ThingClass sunspecStorage + + + + + + Energy consumed phase A + The name of the StateType ({cc0fd197-22ed-4461-8bba-47460396798a}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({481b9895-1c3e-4e74-9418-ee51dae80a63}) of ThingClass sunspecSplitPhaseMeter + + + + + + Energy consumed phase B + The name of the StateType ({e928fde3-b35b-4884-9810-1cd083725e1c}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({10245c6c-4b0d-44b4-be10-72026b842c86}) of ThingClass sunspecSplitPhaseMeter + + + + + Energy consumed phase C + The name of the StateType ({2a12a991-d6c8-4665-bcc8-ea2685f7f0d4}) of ThingClass sunspecThreePhaseMeter + + + + + + Energy produced phase A + The name of the StateType ({fe7caa7e-cf37-4550-833e-7f9d35001fdd}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({2dc76576-561c-4bce-8799-fadc55e84aee}) of ThingClass sunspecSplitPhaseMeter + + + + + + Energy produced phase B + The name of the StateType ({edbe64a5-121e-4c96-9dc8-bbc2a33dfdb7}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({b1489efd-ece0-419e-ab1c-d3d25ab6b958}) of ThingClass sunspecSplitPhaseMeter + + + + + Energy produced phase C + The name of the StateType ({a26790c0-7378-471c-80ba-67021352415d}) of ThingClass sunspecThreePhaseMeter + + + + + + + Error + The name of the StateType ({4479af96-c499-4f15-abd6-4afdb18a9e09}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({5cbfccc9-6afb-404c-a85e-e0323659a25f}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({49240259-d82a-4fe6-b3f5-1cd6a67c87a7}) of ThingClass sunspecSinglePhaseInverter + + + + + + + + + Frequency + The name of the StateType ({66734fc7-b3e4-48d4-8cb7-f0a9f7cef88a}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({db977c04-a3e1-436f-a0cd-9ce5b7bc6b89}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({3a2ce51d-7fa0-4188-bbd6-00d25de90e15}) of ThingClass sunspecSinglePhaseMeter +---------- +The name of the StateType ({faa45cae-ed28-4150-9036-fceddf9d6776}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({611df2ce-2b9c-49f3-9fa7-5706776e812c}) of ThingClass sunspecSinglePhaseInverter + + + + + + Grid charging + The name of the ParamType (ThingClass: sunspecStorage, ActionType: gridCharging, ID: {221a2ef6-0a92-4ff0-87fe-7bd920dbec0b}) +---------- +The name of the StateType ({221a2ef6-0a92-4ff0-87fe-7bd920dbec0b}) of ThingClass sunspecStorage + + + + + + IP address + The name of the ParamType (ThingClass: solarEdgeConnection, Type: thing, ID: {94f6ba3c-6b8b-47a9-a5cb-73a091ae4cf7}) +---------- +The name of the ParamType (ThingClass: sunspecConnection, Type: thing, ID: {6be6abc4-e2b2-4687-9343-0e5164ed0ab2}) + + + + + Instantaneous current + The name of the StateType ({89f58155-ca0c-48a8-aa7f-6e4d1b1bad36}) of ThingClass solarEdgeBattery + + + + + Instantaneous power + The name of the StateType ({caccf9fd-0f83-4dd5-ae48-a497f13d2a2e}) of ThingClass solarEdgeBattery + + + + + Instantaneous voltage + The name of the StateType ({9d17defb-fc6e-4c10-af53-14b796222a14}) of ThingClass solarEdgeBattery + + + + + Line frequency + The name of the StateType ({874f5e4a-a009-4c28-b211-2af90a24b2ac}) of ThingClass sunspecSplitPhaseInverter + + + + + Line to Neutral AC Voltage + The name of the StateType ({0ac79508-07c3-4d01-97a3-6edf121bdf32}) of ThingClass sunspecSplitPhaseMeter + + + + + + MAC address + The name of the ParamType (ThingClass: solarEdgeConnection, Type: thing, ID: {bb395c12-54d6-4139-b0a6-e31b28bc4d2e}) +---------- +The name of the ParamType (ThingClass: sunspecConnection, Type: thing, ID: {f65d6c36-1672-44cb-b52a-62d71837ae67}) + + + + + + + + + + + + Manufacturer + The name of the ParamType (ThingClass: solarEdgeBattery, Type: thing, ID: {8a4b19a9-ee14-42aa-9146-fa84e15970ee}) +---------- +The name of the ParamType (ThingClass: sunspecStorage, Type: thing, ID: {7ee31b97-caec-4cdb-80c9-71e232c5fb22}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseMeter, Type: thing, ID: {462552de-af84-4893-9132-81c1adce67ad}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseMeter, Type: thing, ID: {ecbdee01-8be7-42bf-9e1a-a0f700669b09}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseMeter, Type: thing, ID: {811de380-848a-448c-8446-daa0e0fd8e50}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseInverter, Type: thing, ID: {640b71ba-2ccd-40d6-bf45-8271a9046f81}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseInverter, Type: thing, ID: {c2c35b68-1c0f-47cc-b09f-49d9cf67c0ff}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseInverter, Type: thing, ID: {5fbb0b62-e0f9-4aee-aba7-fd00ae8b0a5d}) + + + + + Maximum energy + The name of the StateType ({d8cc799d-9709-4342-8766-52ff5f12aecc}) of ThingClass solarEdgeBattery + + + + + + + + + + + + Modbus address + The name of the ParamType (ThingClass: solarEdgeBattery, Type: thing, ID: {7413cce6-48c1-4924-89bc-915beed67b57}) +---------- +The name of the ParamType (ThingClass: sunspecStorage, Type: thing, ID: {3f107844-00c5-4f39-86e5-485b3d1f5c1a}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseMeter, Type: thing, ID: {6d5dbd35-1bf6-46db-bee9-90c679421b89}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseMeter, Type: thing, ID: {a56f198d-ed86-429f-b839-8e11a32da8c1}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseMeter, Type: thing, ID: {30b90ec0-429b-4e6c-88e9-155aa4bcad47}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseInverter, Type: thing, ID: {e5465ede-9d3d-4558-b614-40dda743ddae}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseInverter, Type: thing, ID: {37582a96-f2f2-4845-abef-973c7dd0ad57}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseInverter, Type: thing, ID: {26ae9050-7090-453a-85a3-307bfebe6fed}) + + + + + + + + + + + Model + The name of the ParamType (ThingClass: sunspecStorage, Type: thing, ID: {219beb96-b9fe-4dd2-a386-ecfbbab8786d}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseMeter, Type: thing, ID: {a1960821-155c-4176-86fa-974429039182}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseMeter, Type: thing, ID: {89aeec6d-abeb-48b5-9594-214ad5db2d03}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseMeter, Type: thing, ID: {7d6fcafb-c62e-4a21-aae2-f4041c487149}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseInverter, Type: thing, ID: {8d5b2b58-ce46-406d-844e-f53136afcf09}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseInverter, Type: thing, ID: {c42fb50e-210f-4b53-88eb-fa216e15f88f}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseInverter, Type: thing, ID: {41715d00-a947-4f43-a475-cea05790e01d}) + + + + + Number of retries + The name of the ParamType (ThingClass: sunSpec, Type: plugin, ID: {9a4bfe01-315f-4ee7-98a9-f16b08ba12ad}) + + + + + + + Operating state + The name of the StateType ({cebdce98-42d1-4a28-8834-8960efc0e83f}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({6c1e2929-bc9a-4ce9-a405-6df2633a5131}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({47543a7f-425f-406b-a458-b79c36b65f6c}) of ThingClass sunspecSinglePhaseInverter + + + + + + + + + Phase A current + The name of the StateType ({46ae5c40-f389-4f7e-a59f-8947d7dc9f3f}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({e8c0f4bf-a704-46f2-80a0-cf490bd7871b}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({4a058e36-0b45-4388-9a26-0615f7aafa0d}) of ThingClass sunspecSinglePhaseMeter +---------- +The name of the StateType ({3140ccd3-40cf-46c8-8bb2-8c3ea4582f84}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({be7b86b4-aeeb-49ba-9b6b-9792dceed6b5}) of ThingClass sunspecSplitPhaseInverter + + + + + + + Phase A voltage + The name of the StateType ({4ff342de-f0b7-475a-99e4-3a926326d1b7}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({1bd7e53e-abf8-4d62-b87c-2c84c283567b}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({5e7a65a5-b740-4395-87fa-ffbe8fe1c823}) of ThingClass sunspecSinglePhaseMeter + + + + + + Phase AN voltage + The name of the StateType ({f08521aa-9c38-4c31-95e1-acb616f6e9c6}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({17c24cfc-cb41-4873-82b4-19a20d6be146}) of ThingClass sunspecSplitPhaseInverter + + + + + + + + Phase B current + The name of the StateType ({efc75436-ac51-4fee-81f1-65a726416980}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({4281f6fc-d5a0-4a22-ac61-6bec88efbc80}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({7ea1a53a-6fd9-4914-8283-b57aa1aaaebf}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({fc5df18d-cf2f-4944-97b7-e57dabef8778}) of ThingClass sunspecSplitPhaseInverter + + + + + + Phase B voltage + The name of the StateType ({cc8434f1-02fe-41ad-bc8a-94e57293f8fc}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({377b5279-ddb6-451d-8377-a9389c749393}) of ThingClass sunspecSplitPhaseMeter + + + + + + Phase BN voltage + The name of the StateType ({739b8805-d522-4406-bede-d1e4200a3aa9}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({f8645ee2-a1e6-4d09-8c20-f6fd02a9e896}) of ThingClass sunspecSplitPhaseInverter + + + + + + Phase C current + The name of the StateType ({618cbcc5-2efe-49d2-9c5c-53fb3eaa3a9d}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({aa4b4cf5-43d0-4be5-9505-403918b5371d}) of ThingClass sunspecThreePhaseInverter + + + + + Phase C voltage + The name of the StateType ({fbd673cf-8891-4aef-b112-2829f5fcfe06}) of ThingClass sunspecThreePhaseMeter + + + + + Phase CN voltage + The name of the StateType ({949b797d-5566-4667-8982-e430d23548e2}) of ThingClass sunspecThreePhaseInverter + + + + + Phase voltage + The name of the StateType ({4ca086e9-82b9-461c-b168-1d61b542b884}) of ThingClass sunspecSinglePhaseInverter + + + + + + Port + The name of the ParamType (ThingClass: solarEdgeConnection, Type: thing, ID: {1bcede48-d167-4ced-8f1b-bea6302dd43f}) +---------- +The name of the ParamType (ThingClass: sunspecConnection, Type: thing, ID: {1fa4fc9c-f6be-47c7-928a-bcefc1142eec}) + + + + + Rated energy + The name of the StateType ({f6c6f548-8a12-4a62-a641-08bb003c55fa}) of ThingClass solarEdgeBattery + + + + + + + + + + + + Serial number + The name of the ParamType (ThingClass: solarEdgeBattery, Type: thing, ID: {efd99fe4-3a48-4208-829d-14e062015ee7}) +---------- +The name of the ParamType (ThingClass: sunspecStorage, Type: thing, ID: {5cf43b80-3e65-4f69-8bf5-91810da0ead4}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseMeter, Type: thing, ID: {97bbcc98-f4ec-4060-bac2-31b40720862c}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseMeter, Type: thing, ID: {fb7cc04b-91cb-4f3d-a444-201cca57934a}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseMeter, Type: thing, ID: {581fa623-0005-448f-a5b1-8db1043795a3}) +---------- +The name of the ParamType (ThingClass: sunspecThreePhaseInverter, Type: thing, ID: {ee169df5-475c-4dfb-870a-0785899b7dc1}) +---------- +The name of the ParamType (ThingClass: sunspecSplitPhaseInverter, Type: thing, ID: {8afdd256-0393-4610-9477-7c4541c92ffe}) +---------- +The name of the ParamType (ThingClass: sunspecSinglePhaseInverter, Type: thing, ID: {59b85218-f144-4ce8-95f7-19a1dc2e2a8d}) + + + + + Set charging rate + The name of the ActionType ({7f469bbc-64a5-4045-8d5f-9a1a85039851}) of ThingClass sunspecStorage + + + + + Set discharging rate + The name of the ActionType ({6068f030-acce-44a2-b95f-bd00dd5ca760}) of ThingClass sunspecStorage + + + + + Set grid charging + The name of the ActionType ({221a2ef6-0a92-4ff0-87fe-7bd920dbec0b}) of ThingClass sunspecStorage + + + + + + Slave ID + The name of the ParamType (ThingClass: solarEdgeConnection, Type: thing, ID: {b9cc58cf-ad15-447a-a94d-17b9204313bc}) +---------- +The name of the ParamType (ThingClass: sunspecConnection, Type: thing, ID: {953064e0-4675-4538-a9a2-fa22ce2f347c}) + + + + + + SolarEdge + The name of the ThingClass ({7a92bf65-b443-4491-a012-2bec35eb5bf0}) +---------- +The name of the vendor ({addd7dd8-13e1-405b-ba61-b2d82eaf0426}) + + + + + SolarEdge battery + The name of the ThingClass ({7900471a-da3f-4f03-98be-3e3d9c841546}) + + + + + State of health (SOH) + The name of the StateType ({24b501db-f301-422d-ad58-f16ca025f6ee}) of ThingClass solarEdgeBattery + + + + + Status + The name of the StateType ({da2b19c5-0f48-49d1-93f0-abdc0051407d}) of ThingClass sunspecStorage + + + + + SunSpec + The name of the vendor ({c143a7b4-a16c-4fff-86a3-9ffab3d6841d}) + + + + + SunSpec Generic + The name of the ThingClass ({f51853f3-8815-4cf3-b337-45cda1f3e6d5}) + + + + + SunSpec Storage + The name of the ThingClass ({9a643ba8-346c-4127-a2f8-956a7133d75e}) + + + + + SunSpec single phase inverter + The name of the ThingClass ({c5d5204e-3375-4b92-8128-fab77a671fed}) + + + + + SunSpec single phase meter + The name of the ThingClass ({7ffa43b8-b56f-4435-8509-980e9d81dfa8}) + + + + + SunSpec split phase inverter + The name of the ThingClass ({61b38f93-d331-42bf-b1ef-d3fb16ad1230}) + + + + + SunSpec split phase meter + The name of the ThingClass ({b8a18e45-5ff5-4f43-915f-04ee216c809d}) + + + + + SunSpec three phase inverter + The name of the ThingClass ({2e4122ea-96a5-415c-b5e2-7d6012265a83}) + + + + + SunSpec three phase meter + The name of the ThingClass ({68f822f9-ff30-4275-b229-39a3674fead7}) + + + + + Timout + The name of the ParamType (ThingClass: sunSpec, Type: plugin, ID: {1a8895a0-c746-48af-9307-3a4636f24cc2}) + + + + + + + + Total AC current + The name of the StateType ({e85024af-5376-4ff1-813e-5a56990c11cc}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({26560dd8-6de4-445e-ba55-391d7241c370}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({9dbd8da7-dc22-4c3a-b941-47520fde705f}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({f02d1c99-9b43-45a6-8a06-2ed4d6e5d497}) of ThingClass sunspecSinglePhaseInverter + + + + + + + Total real energy exported + The name of the StateType ({01a69b2c-cc96-4925-9b1c-a2f204ef0837}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({8a63bd73-0546-4636-8da2-23238cc06fb2}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({ba275bdf-f418-4ef0-afbe-ac425c6f6783}) of ThingClass sunspecSinglePhaseMeter + + + + + + + Total real energy imported + The name of the StateType ({0a98b229-7625-4811-8bf8-ed22be2db3fb}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({51ffb2ae-3920-40df-8290-bbf5b6e1a68f}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({c51dc6cb-5c05-4078-b11a-26afb2f85541}) of ThingClass sunspecSinglePhaseMeter + + + + + + + Total real power + The name of the StateType ({47780b0a-68b7-46c3-bb98-79ec9a57fd69}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({ef4bc0f8-f516-49b7-aba8-d5f987485aca}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({93cf8c6a-2620-42ed-9070-e0726d7b1dbc}) of ThingClass sunspecSinglePhaseMeter + + + + + Update interval + The name of the ParamType (ThingClass: sunSpec, Type: plugin, ID: {52da5222-9a94-47a2-9adc-004541d2f5ed}) + + + + + + + + + + + + Version + The name of the StateType ({db2e0304-4f0e-461f-8bab-c63fcb5deb1b}) of ThingClass solarEdgeBattery +---------- +The name of the StateType ({fca0c151-d8fa-4d6e-a5d7-b3bc965abc09}) of ThingClass sunspecStorage +---------- +The name of the StateType ({3d908dd2-4a93-4b62-8cbb-2fa9ef4d28fa}) of ThingClass sunspecThreePhaseMeter +---------- +The name of the StateType ({85c5087c-77b4-4ac8-bdff-020f6242bd57}) of ThingClass sunspecSplitPhaseMeter +---------- +The name of the StateType ({94adff6f-f672-4e4e-a72c-cd6cd7e8721a}) of ThingClass sunspecSinglePhaseMeter +---------- +The name of the StateType ({3b543b60-d8f6-4bc0-a79f-cb9cb20e4bb8}) of ThingClass sunspecThreePhaseInverter +---------- +The name of the StateType ({2986cac4-f4ac-4299-ac9e-48339e2a54ef}) of ThingClass sunspecSplitPhaseInverter +---------- +The name of the StateType ({f4439de3-a461-42d6-b4c9-aa35f696a420}) of ThingClass sunspecSinglePhaseInverter + + + + + sunspec + The name of the plugin SunSpec ({cb4bdec6-cf2c-4a0f-9709-42d951ca2d8b}) + + + +