diff --git a/debian/control b/debian/control
index 05f8045..cfb9d34 100644
--- a/debian/control
+++ b/debian/control
@@ -227,6 +227,14 @@ Description: nymea.io plugin for Stiebel Eltron heat pumps
This package will install the nymea.io plugin for Stiebel Eltron heat pumps.
+Package: nymea-plugin-sungrow
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+Description: nymea integration plugin for Sungrow devices
+ This package contains the nymea integration plugin for Sungrow solar inverters, meters and batteries.
+
+
Package: nymea-plugin-sunspec
Architecture: any
Depends: ${shlibs:Depends},
diff --git a/debian/nymea-plugin-sungrow.install.in b/debian/nymea-plugin-sungrow.install.in
new file mode 100644
index 0000000..eaa9dd7
--- /dev/null
+++ b/debian/nymea-plugin-sungrow.install.in
@@ -0,0 +1,2 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsungrow.so
+sungrow/translations/*qm usr/share/nymea/translations/
diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro
index bc1a5c8..f5af517 100644
--- a/nymea-plugins-modbus.pro
+++ b/nymea-plugins-modbus.pro
@@ -24,6 +24,7 @@ PLUGIN_DIRS = \
sma \
solax \
stiebeleltron \
+ sungrow \
sunspec \
unipi \
vestel \
diff --git a/sungrow/README.md b/sungrow/README.md
new file mode 100644
index 0000000..a236ad7
--- /dev/null
+++ b/sungrow/README.md
@@ -0,0 +1,26 @@
+# Sungrow
+
+Connects Sungrow inverters to nymea.
+
+Currently supported models:
+
+* SH3K6
+* SH4K6
+* SH5K-20
+* SH5K-V13
+* SH3K6-30
+* SH4K6-30
+* SH5K-30
+* SH3.0RS
+* SH3.6RS
+* SH4.0RS
+* SH5.0RS
+* SH6.0RS
+* SH5.0RT
+* SH6.0RT
+* SH8.0RT
+* SH10RT
+
+# Requirements
+
+nymea uses the modbus TCP connection in order to connect to the Sungrow inverter. Therefore the inverter must be reachable using the local network.
diff --git a/sungrow/integrationpluginsungrow.cpp b/sungrow/integrationpluginsungrow.cpp
new file mode 100644
index 0000000..5fe2cb1
--- /dev/null
+++ b/sungrow/integrationpluginsungrow.cpp
@@ -0,0 +1,390 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "integrationpluginsungrow.h"
+#include "plugininfo.h"
+#include "sungrowdiscovery.h"
+
+#include
+#include
+
+IntegrationPluginSungrow::IntegrationPluginSungrow()
+{
+
+}
+
+void IntegrationPluginSungrow::discoverThings(ThingDiscoveryInfo *info)
+{
+ if (!hardwareManager()->networkDeviceDiscovery()->available()) {
+ qCWarning(dcSungrow()) << "The network discovery is not available on this platform.";
+ info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available."));
+ return;
+ }
+
+ // Create a discovery with the info as parent for auto deleting the object once the discovery info is done
+ SungrowDiscovery *discovery = new SungrowDiscovery(hardwareManager()->networkDeviceDiscovery(), m_modbusTcpPort, m_modbusSlaveAddress, info);
+ connect(discovery, &SungrowDiscovery::discoveryFinished, discovery, &SungrowDiscovery::deleteLater);
+ connect(discovery, &SungrowDiscovery::discoveryFinished, info, [=](){
+ foreach (const SungrowDiscovery::SungrowDiscoveryResult &result, discovery->discoveryResults()) {
+ QString title = "Sungrow " + QString::number(result.nominalOutputPower) + "kW Inverter";
+
+ if (!result.serialNumber.isEmpty())
+ title.append(" " + result.serialNumber);
+
+ ThingDescriptor descriptor(sungrowInverterTcpThingClassId, title, result.networkDeviceInfo.address().toString() + " " + result.networkDeviceInfo.macAddress());
+ qCInfo(dcSungrow()) << "Discovered:" << descriptor.title() << descriptor.description();
+
+ // Check if we already have set up this device
+ Things existingThings = myThings().filterByParam(sungrowInverterTcpThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
+ if (existingThings.count() == 1) {
+ qCDebug(dcSungrow()) << "This Sungrow inverter already exists in the system:" << result.networkDeviceInfo;
+ descriptor.setThingId(existingThings.first()->id());
+ }
+
+ ParamList params;
+ params << Param(sungrowInverterTcpThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
+ descriptor.setParams(params);
+ info->addThingDescriptor(descriptor);
+ }
+
+ info->finish(Thing::ThingErrorNoError);
+ });
+
+ // Start the discovery process
+ discovery->startDiscovery();
+}
+
+void IntegrationPluginSungrow::setupThing(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+ qCInfo(dcSungrow()) << "Setup" << thing << thing->params();
+
+ if (thing->thingClassId() == sungrowInverterTcpThingClassId) {
+
+ // Handle reconfiguration
+ if (m_tcpConnections.contains(thing)) {
+ qCDebug(dcSungrow()) << "Reconfiguring existing thing" << thing->name();
+ m_tcpConnections.take(thing)->deleteLater();
+ if (m_monitors.contains(thing)) {
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+ }
+ }
+
+ MacAddress macAddress = MacAddress(thing->paramValue(sungrowInverterTcpThingMacAddressParamTypeId).toString());
+ if (!macAddress.isValid()) {
+ qCWarning(dcSungrow()) << "The configured MAC address is not valid" << thing->params();
+ info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure this inverter."));
+ return;
+ }
+
+ // Create the monitor
+ NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
+ m_monitors.insert(thing, monitor);
+ connect(info, &ThingSetupInfo::aborted, monitor, [=](){
+ // Clean up in case the setup gets aborted
+ if (m_monitors.contains(thing)) {
+ qCDebug(dcSungrow()) << "Unregister monitor because the setup has been aborted.";
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+ }
+ });
+
+ QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address();
+
+ qCInfo(dcSungrow()) << "Setting up Sungrow on" << address.toString();
+ auto sungrowConnection = new SungrowModbusTcpConnection(address, m_modbusTcpPort , m_modbusSlaveAddress, this);
+ connect(info, &ThingSetupInfo::aborted, sungrowConnection, &SungrowModbusTcpConnection::deleteLater);
+
+ // Reconnect on monitor reachable changed
+ connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
+ qCDebug(dcSungrow()) << "Network device monitor reachable changed for" << thing->name() << reachable;
+ if (!thing->setupComplete())
+ return;
+
+ if (reachable && !thing->stateValue("connected").toBool()) {
+ sungrowConnection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address());
+ sungrowConnection->reconnectDevice();
+ } else if (!reachable) {
+ // Note: Auto reconnect is disabled explicitly and
+ // the device will be connected once the monitor says it is reachable again
+ sungrowConnection->disconnectDevice();
+ }
+ });
+
+ connect(sungrowConnection, &SungrowModbusTcpConnection::reachableChanged, thing, [this, thing, sungrowConnection](bool reachable){
+ qCInfo(dcSungrow()) << "Reachable changed to" << reachable << "for" << thing;
+ if (reachable) {
+ // Connected true will be set after successfull init
+ sungrowConnection->initialize();
+ } else {
+ thing->setStateValue("connected", false);
+ thing->setStateValue(sungrowInverterTcpCurrentPowerStateTypeId, 0);
+
+ foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
+ childThing->setStateValue("connected", false);
+ }
+
+ Thing *child = getMeterThing(thing);
+ if (child) {
+ child->setStateValue(sungrowMeterCurrentPowerStateTypeId, 0);
+ child->setStateValue(sungrowMeterCurrentPhaseAStateTypeId, 0);
+ child->setStateValue(sungrowMeterCurrentPhaseBStateTypeId, 0);
+ child->setStateValue(sungrowMeterCurrentPhaseCStateTypeId, 0);
+ child->setStateValue(sungrowMeterApparentPowerPhaseAStateTypeId, 0);
+ child->setStateValue(sungrowMeterApparentPowerPhaseBStateTypeId, 0);
+ child->setStateValue(sungrowMeterApparentPowerPhaseCStateTypeId, 0);
+ }
+
+ child = getBatteryThing(thing);
+ if (child) {
+ child->setStateValue(sungrowBatteryCurrentPowerStateTypeId, 0);
+ }
+ }
+ });
+
+ connect(sungrowConnection, &SungrowModbusTcpConnection::initializationFinished, thing, [=](bool success){
+ thing->setStateValue("connected", success);
+
+ foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
+ childThing->setStateValue("connected", success);
+ }
+
+ if (!success) {
+ // Try once to reconnect the device
+ sungrowConnection->reconnectDevice();
+ } else {
+ qCInfo(dcSungrow()) << "Connection initialized successfully for" << thing;
+ sungrowConnection->update();
+ }
+ });
+
+ connect(sungrowConnection, &SungrowModbusTcpConnection::updateFinished, thing, [=](){
+ qCDebug(dcSungrow()) << "Updated" << sungrowConnection;
+
+ if (myThings().filterByParentId(thing->id()).filterByThingClassId(sungrowMeterThingClassId).isEmpty()) {
+ qCDebug(dcSungrow()) << "There is no meter set up for this inverter. Creating a meter for" << thing << sungrowConnection->modbusTcpMaster();
+ ThingClass meterThingClass = thingClass(sungrowMeterThingClassId);
+ ThingDescriptor descriptor(sungrowMeterThingClassId, meterThingClass.displayName() + " " + sungrowConnection->serialNumber(), QString(), thing->id());
+ emit autoThingsAppeared(ThingDescriptors() << descriptor);
+ }
+
+ // Check if a battery is connected to this Sungrow inverter
+ if (sungrowConnection->batteryType() != SungrowModbusTcpConnection::BatteryTypeNoBattery &&
+ myThings().filterByParentId(thing->id()).filterByThingClassId(sungrowBatteryThingClassId).isEmpty()) {
+ qCDebug(dcSungrow()) << "There is a battery connected but not set up yet. Creating a battery.";
+ ThingClass batteryThingClass = thingClass(sungrowBatteryThingClassId);
+ ThingDescriptor descriptor(sungrowBatteryThingClassId, batteryThingClass.displayName() + " " + sungrowConnection->serialNumber(), QString(), thing->id());
+ emit autoThingsAppeared(ThingDescriptors() << descriptor);
+ }
+
+ // Update inverter states
+ thing->setStateValue(sungrowInverterTcpCurrentPowerStateTypeId, static_cast(sungrowConnection->totalPVPower()) * -1);
+ thing->setStateValue(sungrowInverterTcpTemperatureStateTypeId, sungrowConnection->inverterTemperature());
+ thing->setStateValue(sungrowInverterTcpFrequencyStateTypeId, sungrowConnection->gridFrequency());
+ thing->setStateValue(sungrowInverterTcpTotalEnergyProducedStateTypeId, sungrowConnection->totalPVGeneration());
+
+ // Update the meter if available
+ Thing *meterThing = getMeterThing(thing);
+ if (meterThing) {
+ auto runningState = sungrowConnection->runningState();
+ qCDebug(dcSungrow()) << "Power generated from PV:" << (runningState & (0x1 << 0) ? "true" : "false");
+ qCDebug(dcSungrow()) << "Battery charging:" << (runningState & (0x1 << 1) ? "true" : "false");
+ qCDebug(dcSungrow()) << "Battery discharging:" << (runningState & (0x1 << 2) ? "true" : "false");
+ qCDebug(dcSungrow()) << "Positive load power:" << (runningState & (0x1 << 3) ? "true" : "false");
+ qCDebug(dcSungrow()) << "Feed-in power:" << (runningState & (0x1 << 4) ? "true" : "false");
+ qCDebug(dcSungrow()) << "Import power from grid:" << (runningState & (0x1 << 5) ? "true" : "false");
+ qCDebug(dcSungrow()) << "Negative load power:" << (runningState & (0x1 << 7) ? "true" : "false");
+ meterThing->setStateValue(sungrowMeterCurrentPowerStateTypeId, sungrowConnection->totalActivePower() * -1);
+ meterThing->setStateValue(sungrowMeterTotalEnergyConsumedStateTypeId, sungrowConnection->totalImportEnergy());
+ meterThing->setStateValue(sungrowMeterTotalEnergyProducedStateTypeId, sungrowConnection->totalExportEnergy());
+ meterThing->setStateValue(sungrowMeterCurrentPhaseAStateTypeId, sungrowConnection->phaseACurrent() * -1);
+ meterThing->setStateValue(sungrowMeterCurrentPhaseBStateTypeId, sungrowConnection->phaseBCurrent() * -1);
+ meterThing->setStateValue(sungrowMeterCurrentPhaseCStateTypeId, sungrowConnection->phaseCCurrent() * -1);
+ meterThing->setStateValue(sungrowMeterVoltagePhaseAStateTypeId, sungrowConnection->phaseAVoltage());
+ meterThing->setStateValue(sungrowMeterVoltagePhaseBStateTypeId, sungrowConnection->phaseBVoltage());
+ meterThing->setStateValue(sungrowMeterVoltagePhaseCStateTypeId, sungrowConnection->phaseCVoltage());
+ meterThing->setStateValue(sungrowMeterApparentPowerPhaseAStateTypeId, sungrowConnection->phaseAVoltage() * sungrowConnection->phaseACurrent() * -1);
+ meterThing->setStateValue(sungrowMeterApparentPowerPhaseBStateTypeId, sungrowConnection->phaseBVoltage() * sungrowConnection->phaseBCurrent() * -1);
+ meterThing->setStateValue(sungrowMeterApparentPowerPhaseCStateTypeId, sungrowConnection->phaseCVoltage() * sungrowConnection->phaseCCurrent() * -1);
+ meterThing->setStateValue(sungrowMeterFrequencyStateTypeId, sungrowConnection->gridFrequency());
+ }
+
+ // Update the battery if available
+ Thing *batteryThing = getBatteryThing(thing);
+ if (batteryThing) {
+ batteryThing->setStateValue(sungrowBatteryVoltageStateTypeId, sungrowConnection->batteryVoltage());
+ batteryThing->setStateValue(sungrowBatteryTemperatureStateTypeId, sungrowConnection->batteryTemperature());
+ batteryThing->setStateValue(sungrowBatteryBatteryLevelStateTypeId, sungrowConnection->batteryLevel());
+ batteryThing->setStateValue(sungrowBatteryBatteryCriticalStateTypeId, sungrowConnection->batteryLevel() < 5);
+
+ batteryThing->setStateValue(sungrowBatteryCurrentPowerStateTypeId, sungrowConnection->batteryPower());
+ quint16 runningState = sungrowConnection->runningState();
+ if (runningState & (0x1 << 1)) { //Bit 1: Battery charging bit
+ batteryThing->setStateValue(sungrowBatteryChargingStateStateTypeId, "charging");
+ } else if (runningState & (0x1 << 2)) { //Bit 2: Battery discharging bit
+ batteryThing->setStateValue(sungrowBatteryChargingStateStateTypeId, "discharging");
+ } else {
+ batteryThing->setStateValue(sungrowBatteryChargingStateStateTypeId, "idle");
+ }
+ }
+ });
+
+ m_tcpConnections.insert(thing, sungrowConnection);
+
+ if (monitor->reachable())
+ sungrowConnection->connectDevice();
+
+ info->finish(Thing::ThingErrorNoError);
+ return;
+ }
+
+ if (thing->thingClassId() == sungrowMeterThingClassId) {
+
+ // Get the parent thing and the associated connection
+ Thing *connectionThing = myThings().findById(thing->parentId());
+ if (!connectionThing) {
+ qCWarning(dcSungrow()) << "Failed to set up Sungrow energy meter because the parent thing with ID" << thing->parentId().toString() << "could not be found.";
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ auto sungrowConnection = m_tcpConnections.value(connectionThing);
+ if (!sungrowConnection) {
+ qCWarning(dcSungrow()) << "Failed to set up Sungrow energy meter because the connection for" << connectionThing << "does not exist.";
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ // Note: The states will be handled in the parent inverter thing on updated
+ info->finish(Thing::ThingErrorNoError);
+ return;
+ }
+
+ if (thing->thingClassId() == sungrowBatteryThingClassId) {
+ // Get the parent thing and the associated connection
+ Thing *connectionThing = myThings().findById(thing->parentId());
+ if (!connectionThing) {
+ qCWarning(dcSungrow()) << "Failed to set up Sungrow battery because the parent thing with ID" << thing->parentId().toString() << "could not be found.";
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ auto sungrowConnection = m_tcpConnections.value(connectionThing);
+ if (!sungrowConnection) {
+ qCWarning(dcSungrow()) << "Failed to set up Sungrow battery because the connection for" << connectionThing << "does not exist.";
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ // Note: The states will be handled in the parent inverter thing on updated
+ info->finish(Thing::ThingErrorNoError);
+ return;
+ }
+}
+
+void IntegrationPluginSungrow::postSetupThing(Thing *thing)
+{
+
+ if (thing->thingClassId() == sungrowInverterTcpThingClassId) {
+
+ // Create the update timer if not already set up
+ if (!m_refreshTimer) {
+ qCDebug(dcSungrow()) << "Starting plugin timer...";
+ m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2);
+ connect(m_refreshTimer, &PluginTimer::timeout, this, [this] {
+ foreach(auto thing, myThings().filterByThingClassId(sungrowInverterTcpThingClassId)) {
+ auto monitor = m_monitors.value(thing);
+ if (!monitor->reachable()) {
+ continue;
+ }
+
+ auto connection = m_tcpConnections.value(thing);
+ if (connection->initializing()) {
+ qCDebug(dcSungrow()) << "Skip updating" << connection->modbusTcpMaster() << "since the connection is still initializing.";
+ continue;
+ }
+ if (connection->reachable()) {
+ qCDebug(dcSungrow()) << "Updating connection" << connection->modbusTcpMaster()->hostAddress().toString();
+ connection->update();
+ } else {
+ qCDebug(dcSungrow()) << "Device not reachable. Probably a TCP connection error. Reconnecting TCP socket";
+ connection->reconnectDevice();
+ }
+ }
+ });
+ m_refreshTimer->start();
+ }
+ return;
+ }
+
+ if (thing->thingClassId() == sungrowMeterThingClassId || thing->thingClassId() == sungrowBatteryThingClassId) {
+ Thing *connectionThing = myThings().findById(thing->parentId());
+ if (connectionThing) {
+ thing->setStateValue("connected", connectionThing->stateValue("connected"));
+ }
+ return;
+ }
+}
+
+void IntegrationPluginSungrow::thingRemoved(Thing *thing)
+{
+ if (thing->thingClassId() == sungrowInverterTcpThingClassId && m_tcpConnections.contains(thing)) {
+ auto connection = m_tcpConnections.take(thing);
+ connection->modbusTcpMaster()->disconnectDevice();
+ delete connection;
+ }
+
+ // Unregister related hardware resources
+ if (m_monitors.contains(thing))
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+
+ if (myThings().isEmpty() && m_refreshTimer) {
+ qCDebug(dcSungrow()) << "Stopping refresh timer";
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
+ m_refreshTimer = nullptr;
+ }
+}
+
+Thing *IntegrationPluginSungrow::getMeterThing(Thing *parentThing)
+{
+ Things meterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(sungrowMeterThingClassId);
+ if (meterThings.isEmpty())
+ return nullptr;
+
+ return meterThings.first();
+}
+
+Thing *IntegrationPluginSungrow::getBatteryThing(Thing *parentThing)
+{
+ Things batteryThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(sungrowBatteryThingClassId);
+ if (batteryThings.isEmpty())
+ return nullptr;
+
+ return batteryThings.first();
+}
diff --git a/sungrow/integrationpluginsungrow.h b/sungrow/integrationpluginsungrow.h
new file mode 100644
index 0000000..dc0bede
--- /dev/null
+++ b/sungrow/integrationpluginsungrow.h
@@ -0,0 +1,73 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef INTEGRATIONPLUGINSUNGROW_H
+#define INTEGRATIONPLUGINSUNGROW_H
+
+#include
+#include
+#include
+
+#include "extern-plugininfo.h"
+
+#include "sungrowmodbustcpconnection.h"
+
+class IntegrationPluginSungrow: public IntegrationPlugin
+{
+ Q_OBJECT
+
+ Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsungrow.json")
+ Q_INTERFACES(IntegrationPlugin)
+
+public:
+ explicit IntegrationPluginSungrow();
+
+ void discoverThings(ThingDiscoveryInfo *info) override;
+ void setupThing(ThingSetupInfo *info) override;
+ void postSetupThing(Thing *thing) override;
+ void thingRemoved(Thing *thing) override;
+
+private:
+ const int m_modbusTcpPort = 502;
+ const quint16 m_modbusSlaveAddress = 1;
+ PluginTimer *m_refreshTimer = nullptr;
+
+ QHash m_monitors;
+ QHash m_tcpConnections;
+
+ void setupSungrowTcpConnection(ThingSetupInfo *info);
+
+ Thing *getMeterThing(Thing *parentThing);
+ Thing *getBatteryThing(Thing *parentThing);
+};
+
+#endif // INTEGRATIONPLUGINSUNGROW_H
+
+
diff --git a/sungrow/integrationpluginsungrow.json b/sungrow/integrationpluginsungrow.json
new file mode 100644
index 0000000..8a894d0
--- /dev/null
+++ b/sungrow/integrationpluginsungrow.json
@@ -0,0 +1,289 @@
+{
+ "name": "Sungrow",
+ "displayName": "Sungrow",
+ "id": "250c9b83-1127-4013-bbd0-11e7ea482057",
+ "vendors": [
+ {
+ "name": "sungrow",
+ "displayName": "Sungrow",
+ "id": "cdc58e0d-bfdb-45d9-b961-9c0b036c35aa",
+ "thingClasses": [
+ {
+ "name": "sungrowInverterTcp",
+ "displayName": "Sungrow Inverter",
+ "id": "59cb2da4-da07-11ee-adea-7397f8a9afe9",
+ "createMethods": ["discovery"],
+ "discoveryType": "weak",
+ "interfaces": ["solarinverter", "connectable"],
+ "providedInterfaces": [ "energymeter", "energystorage"],
+ "paramTypes": [
+ {
+ "id": "62137142-da07-11ee-9522-2f74f3b1fc5d",
+ "name":"macAddress",
+ "displayName": "MAC address",
+ "type": "QString",
+ "inputType": "MacAddress",
+ "defaultValue": ""
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "6c68b170-da07-11ee-9891-3335ced0ad72",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "6fb11fe8-da07-11ee-b77f-8393cc11be21",
+ "name": "currentPower",
+ "displayName": "Active power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "7bf092a2-da07-11ee-abe9-138e66e6f2a5",
+ "name": "totalEnergyProduced",
+ "displayName": "Total energy produced",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.0,
+ "cached": true
+ },
+ {
+ "id": "80b35fc2-da07-11ee-a73d-5774b082c92b",
+ "name": "temperature",
+ "displayName": "Temperature",
+ "type": "double",
+ "unit": "DegreeCelsius",
+ "defaultValue": 0.00,
+ "cached": false
+ },
+ {
+ "id": "84bcbc58-da07-11ee-b94d-a39792ee6f59",
+ "name": "frequency",
+ "displayName": "Frequency",
+ "type": "double",
+ "unit": "Hertz",
+ "defaultValue": 0.00,
+ "cached": false
+ }
+ ]
+ },
+ {
+ "name": "sungrowMeter",
+ "displayName": "Sungrow Meter",
+ "id": "a935e49c-da07-11ee-bd11-3fcf27dc6373",
+ "createMethods": ["auto"],
+ "interfaces": [ "energymeter", "connectable"],
+ "stateTypes": [
+ {
+ "id": "d26b59dc-da07-11ee-b494-3781cc3081a7",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "d777530e-da07-11ee-a526-c7d23dd34f57",
+ "name": "currentPower",
+ "displayName": "Current power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0.00,
+ "cached": false
+ },
+ {
+ "id": "e80bb836-da07-11ee-afd1-dbff8484ba11",
+ "name": "voltagePhaseA",
+ "displayName": "Voltage phase A",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "ebefd252-da07-11ee-a8a4-c7ea9df0b6f9",
+ "name": "voltagePhaseB",
+ "displayName": "Voltage phase B",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "efd82b30-da07-11ee-9668-7b523696940d",
+ "name": "voltagePhaseC",
+ "displayName": "Voltage phase C",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "f3d850c0-da07-11ee-883f-4ff8b7e55bdc",
+ "name": "currentPhaseA",
+ "displayName": "Current phase A",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "f82603de-da07-11ee-93bc-6b9f9f333c30",
+ "name": "currentPhaseB",
+ "displayName": "Current phase B",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "fcf77988-da07-11ee-99da-3b2415014506",
+ "name": "currentPhaseC",
+ "displayName": "Current phase C",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "167aa300-faef-11ee-859a-bb6f4e8be7c9",
+ "name": "apparentPowerPhaseA",
+ "displayName": "Apparent power phase A",
+ "type": "double",
+ "unit": "VoltAmpere",
+ "defaultValue": 0
+ },
+ {
+ "id": "2c3ac134-faef-11ee-9c28-9f6bb77683d3",
+ "name": "apparentPowerPhaseB",
+ "displayName": "Apparent power phase B",
+ "type": "double",
+ "unit": "VoltAmpere",
+ "defaultValue": 0
+ },
+ {
+ "id": "3f7ca9e2-faef-11ee-81e7-6f53d07e9197",
+ "name": "apparentPowerPhaseC",
+ "displayName": "Apparent power phase C",
+ "type": "double",
+ "unit": "VoltAmpere",
+ "defaultValue": 0
+ },
+ {
+ "id": "00eb83c2-da08-11ee-b67d-1f74a41e6218",
+ "name": "totalEnergyProduced",
+ "displayName": "Total returned energy",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00,
+ "cached": true
+ },
+ {
+ "id": "03ef972a-da08-11ee-9a1f-d741d1e276be",
+ "name": "totalEnergyConsumed",
+ "displayName": "Total imported energy",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00,
+ "cached": true
+ },
+ {
+ "id": "07b526ea-da08-11ee-ab24-ab0d1ca8555d",
+ "name": "frequency",
+ "displayName": "Frequency",
+ "type": "double",
+ "unit": "Hertz",
+ "defaultValue": 0.00,
+ "cached": false
+ }
+ ]
+ },
+ {
+ "name": "sungrowBattery",
+ "displayName": "Sungrow Battery",
+ "id": "0aea1b90-da08-11ee-9195-afc9f857a324",
+ "createMethods": ["auto"],
+ "interfaces": ["energystorage", "connectable"],
+ "stateTypes": [
+ {
+ "id": "0ff3c834-da08-11ee-bac7-0f1044f86ea3",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "13f48cde-da08-11ee-81c8-3362a92b58c8",
+ "name": "batteryCritical",
+ "displayName": "Battery critical",
+ "type": "bool",
+ "defaultValue": false
+ },
+ {
+ "id": "1738bcf8-da08-11ee-b1d0-a3e1183dabba",
+ "name": "batteryLevel",
+ "displayName": "Battery level",
+ "type": "int",
+ "unit": "Percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "1aa8cf86-da08-11ee-9697-3b0a8023db88",
+ "name": "currentPower",
+ "displayName": "Total real power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0.00,
+ "cached": false
+ },
+ {
+ "id": "1e6bf760-da08-11ee-ba39-c31ecdbb8fc9",
+ "name": "voltage",
+ "displayName": "Voltage",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0.00,
+ "cached": false
+ },
+ {
+ "id": "221ccae2-da08-11ee-9e4d-4b6abbf9e564",
+ "name": "temperature",
+ "displayName": "Temperature",
+ "type": "double",
+ "unit": "DegreeCelsius",
+ "defaultValue": 0.00,
+ "cached": false
+ },
+ {
+ "id": "264ac092-da08-11ee-ad8d-9f0751d6c499",
+ "name": "capacity",
+ "displayName": "Capacity",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "29e8a6d8-da08-11ee-87ba-539307b7d2ee",
+ "name": "chargingState",
+ "displayName": "Charging state",
+ "type": "QString",
+ "possibleValues": ["idle", "charging", "discharging"],
+ "defaultValue": "idle",
+ "cached": false
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/sungrow/meta.json b/sungrow/meta.json
new file mode 100644
index 0000000..17a8a5d
--- /dev/null
+++ b/sungrow/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "Sungrow Inverter",
+ "tagline": "Connect to Sungrow inverters.",
+ "icon": "sungrow.png",
+ "stability": "consumer",
+ "offline": true,
+ "technologies": [
+ "network"
+ ],
+ "categories": [
+ "energy"
+ ]
+}
diff --git a/sungrow/sungrow-registers.json b/sungrow/sungrow-registers.json
new file mode 100644
index 0000000..b69d9c9
--- /dev/null
+++ b/sungrow/sungrow-registers.json
@@ -0,0 +1,667 @@
+{
+ "className": "Sungrow",
+ "protocol": "TCP",
+ "endianness": "LittleEndian",
+ "errorLimitUntilNotReachable": 5,
+ "queuedRequests": true,
+ "queuedRequestsDelay": 400,
+ "checkReachableRegister": "totalPVPower",
+ "enums": [
+ {
+ "name": "SystemState",
+ "values": [
+ {
+ "key": "Stop",
+ "value": 2
+ },
+ {
+ "key": "Standby",
+ "value": 8
+ },
+ {
+ "key": "InitialStandby",
+ "value": 16
+ },
+ {
+ "key": "Startup",
+ "value": 32
+ },
+ {
+ "key": "Running",
+ "value": 64
+ },
+ {
+ "key": "Fault",
+ "value": 256
+ },
+ {
+ "key": "RunningMainMode",
+ "value": 1024
+ },
+ {
+ "key": "RunningForcedMode",
+ "value": 2048
+ },
+ {
+ "key": "RunningOffGridMode",
+ "value": 4096
+ },
+ {
+ "key": "Restarting",
+ "value": 9473
+ },
+ {
+ "key": "RunningExternalEMSMode",
+ "value": 16384
+ }
+ ]
+ },
+ {
+ "name": "BatteryType",
+ "values": [
+ {
+ "key": "LeadAcidNarada",
+ "value": 0
+ },
+ {
+ "key": "LiIonSamsung",
+ "value": 1
+ },
+ {
+ "key": "NoBattery",
+ "value": 2
+ },
+ {
+ "key": "LeadAcidOther",
+ "value": 3
+ },
+ {
+ "key": "LiIonUS2000A",
+ "value": 4
+ },
+ {
+ "key": "LiIonLG",
+ "value": 5
+ },
+ {
+ "key": "LiIonUS2000B",
+ "value": 6
+ },
+ {
+ "key": "LiIonGCL",
+ "value": 7
+ },
+ {
+ "key": "LiIonBSG",
+ "value": 8
+ },
+ {
+ "key": "LiIonSungrow",
+ "value": 9
+ },
+ {
+ "key": "LiIonBYD",
+ "value": 10
+ },
+ {
+ "key": "LiIonTawaki",
+ "value": 11
+ }
+ ]
+ }
+ ],
+ "blocks": [
+ {
+ "id": "version",
+ "readSchedule": "init",
+ "registers": [
+ {
+ "id": "protocolNumber",
+ "address": 4949,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Protocol number",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "protocolVersion",
+ "address": 4951,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Device type code",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "armSoftwareVersion",
+ "address": 4953,
+ "size": 15,
+ "type": "string",
+ "registerType": "inputRegister",
+ "description": "ARM software version",
+ "access": "RO"
+ },
+ {
+ "id": "dspSoftwareVersion",
+ "address": 4968,
+ "size": 15,
+ "type": "string",
+ "registerType": "inputRegister",
+ "description": "ARM software version",
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "identification",
+ "readSchedule": "init",
+ "registers": [
+ {
+ "id": "serialNumber",
+ "address": 4989,
+ "size": 10,
+ "type": "string",
+ "registerType": "inputRegister",
+ "description": "Serial number",
+ "access": "RO"
+ },
+ {
+ "id": "deviceTypeCode",
+ "address": 4999,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Device type code",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "nominalOutputPower",
+ "address": 5000,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Nominal output power",
+ "unit": "kW",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "energyValues1",
+ "readSchedule": "update",
+ "registers": [
+ {
+ "id": "inverterTemperature",
+ "address": 5007,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Inverter temperature",
+ "unit": "°C",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "dummy0",
+ "address": 5008,
+ "size": 8,
+ "type": "raw",
+ "registerType": "inputRegister",
+ "description": "none",
+ "access": "RO"
+ },
+ {
+ "id": "totalPVPower",
+ "address": 5016,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Total PV power",
+ "defaultValue": "0",
+ "unit": "W",
+ "access": "RO"
+ },
+ {
+ "id": "phaseAVoltage",
+ "address": 5018,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Phase A voltage",
+ "unit": "V",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "phaseBVoltage",
+ "address": 5019,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Phase B voltage",
+ "unit": "V",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "phaseCVoltage",
+ "address": 5020,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Phase C voltage",
+ "unit": "V",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "dummy1",
+ "address": 5021,
+ "size": 11,
+ "type": "raw",
+ "registerType": "inputRegister",
+ "description": "none",
+ "access": "RO"
+ },
+ {
+ "id": "reactivePower",
+ "address": 5032,
+ "size": 2,
+ "type": "int32",
+ "registerType": "inputRegister",
+ "description": "Reactive power",
+ "defaultValue": "0",
+ "unit": "var",
+ "access": "RO"
+ },
+ {
+ "id": "powerFactor",
+ "address": 5034,
+ "size": 1,
+ "type": "int32",
+ "registerType": "inputRegister",
+ "description": "Power factor",
+ "defaultValue": "0",
+ "staticScaleFactor": -3,
+ "access": "RO"
+ },
+ {
+ "id": "gridFrequency",
+ "address": 5035,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Grid frequency",
+ "defaultValue": "0",
+ "unit": "Hz",
+ "staticScaleFactor": -2,
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "energyValues2",
+ "readSchedule": "update",
+ "registers": [
+ {
+ "id": "systemState",
+ "address": 12999,
+ "size": 1,
+ "type": "uint16",
+ "enum": "SystemState",
+ "registerType": "inputRegister",
+ "description": "System state",
+ "defaultValue": "SystemStateStop",
+ "access": "RO"
+ },
+ {
+ "id": "runningState",
+ "address": 13000,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Running state",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "dailyPVGeneration",
+ "address": 13001,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Daily PV generation",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "totalPVGeneration",
+ "address": 13002,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Total PV generation",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "dailyPVExport",
+ "address": 13004,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Daily PV export",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "totalPVExport",
+ "address": 13005,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Total PV export´",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "loadPower",
+ "address": 13007,
+ "size": 2,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Load power",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "exportPower",
+ "address": 13009,
+ "size": 2,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Export power",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "dailyBatteryChargePV",
+ "address": 13011,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Daily battery charge from PV",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "totalBatteryChargePV",
+ "address": 13012,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Total battery charge from PV",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "dummy2",
+ "address": 13014,
+ "size": 5,
+ "type": "raw",
+ "registerType": "inputRegister",
+ "description": "none",
+ "access": "RO"
+ },
+ {
+ "id": "batteryVoltage",
+ "address": 13019,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Battery voltage",
+ "unit": "V",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "batteryCurrent",
+ "address": 13020,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Battery current",
+ "unit": "A",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "batteryPower",
+ "address": 13021,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Battery power",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "batteryLevel",
+ "address": 13022,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Battery level",
+ "unit": "%",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "batteryHealthState",
+ "address": 13023,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Battery health state",
+ "unit": "%",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "batteryTemperature",
+ "address": 13024,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Battery temperature",
+ "unit": "°C",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "dummy3",
+ "address": 13025,
+ "size": 4,
+ "type": "raw",
+ "registerType": "inputRegister",
+ "description": "none",
+ "access": "RO"
+ },
+ {
+ "id": "gridState",
+ "address": 13029,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Grid state",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "phaseACurrent",
+ "address": 13030,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Phase A current",
+ "unit": "A",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "phaseBCurrent",
+ "address": 13031,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Phase B current",
+ "unit": "A",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "phaseCCurrent",
+ "address": 13032,
+ "size": 1,
+ "type": "int16",
+ "registerType": "inputRegister",
+ "description": "Phase C current",
+ "unit": "A",
+ "staticScaleFactor": -1,
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "totalActivePower",
+ "address": 13033,
+ "size": 2,
+ "type": "int32",
+ "registerType": "inputRegister",
+ "description": "Total active power",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "dailyImportEnergy",
+ "address": 13035,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "inputRegister",
+ "description": "Daily import energy",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "totalImportEnergy",
+ "address": 13036,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Total import energy",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "dummy4",
+ "address": 13038,
+ "size": 7,
+ "type": "raw",
+ "registerType": "inputRegister",
+ "description": "none",
+ "access": "RO"
+ },
+ {
+ "id": "totalExportEnergy",
+ "address": 13045,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "inputRegister",
+ "description": "Total export energy",
+ "unit": "kWh",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "batteryInformation",
+ "readSchedule": "init",
+ "registers": [
+ {
+ "id": "batteryType",
+ "address": 13054,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "holdingRegister",
+ "description": "Battery type",
+ "enum": "BatteryType",
+ "defaultValue": "BatteryTypeNoBattery",
+ "access": "RO"
+ },
+ {
+ "id": "batteryNominalVoltage",
+ "address": 13055,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "holdingRegister",
+ "description": "Battery nominal voltage",
+ "unit": "V",
+ "defaultValue": "0",
+ "staticScaleFactor": -1,
+ "access": "RO"
+ },
+ {
+ "id": "batteryCapacity",
+ "address": 13056,
+ "size": 1,
+ "type": "uint16",
+ "registerType": "holdingRegister",
+ "description": "Battery capacity",
+ "unit": "Ah",
+ "defaultValue": "10",
+ "access": "RO"
+ }
+ ]
+ }
+ ],
+ "registers": [
+ ]
+}
diff --git a/sungrow/sungrow.png b/sungrow/sungrow.png
new file mode 100644
index 0000000..67409f3
Binary files /dev/null and b/sungrow/sungrow.png differ
diff --git a/sungrow/sungrow.pro b/sungrow/sungrow.pro
new file mode 100644
index 0000000..2cd5204
--- /dev/null
+++ b/sungrow/sungrow.pro
@@ -0,0 +1,14 @@
+include(../plugins.pri)
+
+# Generate modbus connection
+MODBUS_CONNECTIONS += sungrow-registers.json
+#MODBUS_TOOLS_CONFIG += VERBOSE
+include(../modbus.pri)
+
+HEADERS += \
+ integrationpluginsungrow.h \
+ sungrowdiscovery.h
+
+SOURCES += \
+ integrationpluginsungrow.cpp \
+ sungrowdiscovery.cpp
diff --git a/sungrow/sungrowdiscovery.cpp b/sungrow/sungrowdiscovery.cpp
new file mode 100644
index 0000000..1d1888e
--- /dev/null
+++ b/sungrow/sungrowdiscovery.cpp
@@ -0,0 +1,161 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "sungrowdiscovery.h"
+#include "extern-plugininfo.h"
+
+SungrowDiscovery::SungrowDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent) :
+ QObject{parent},
+ m_networkDeviceDiscovery{networkDeviceDiscovery},
+ m_port{port},
+ m_modbusAddress{modbusAddress}
+{
+
+}
+
+void SungrowDiscovery::startDiscovery()
+{
+ qCDebug(dcSungrow()) << "Discovery: Start searching for Sungrow inverters in the network";
+ m_startDateTime = QDateTime::currentDateTime();
+
+ NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &SungrowDiscovery::checkNetworkDevice);
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=] () {
+ qCDebug(dcSungrow()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
+
+ // Give the last connections added right before the network discovery finished a chance to check the device...
+ QTimer::singleShot(3000, this, [this] () {
+ qCDebug(dcSungrow()) << "Discovery: Grace period timer triggered.";
+ finishDiscovery();
+ });
+ });
+}
+
+QList SungrowDiscovery::discoveryResults() const
+{
+ return m_discoveryResults;
+}
+
+void SungrowDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
+{
+ /* Create a Sungrow connection and try to initialize it.
+ * Only if initialized successfully and all information have been fetched correctly from
+ * the device we can assume this is what we are locking for (ip, port, modbus address, correct registers).
+ */
+
+ qCDebug(dcSungrow()) << "Creating Sungrow Modbus TCP connection for" << networkDeviceInfo.address() << "Port:" << m_port << "Slave Address" << m_modbusAddress;
+ SungrowModbusTcpConnection *connection = new SungrowModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this);
+ connection->modbusTcpMaster()->setTimeout(5000);
+ connection->modbusTcpMaster()->setNumberOfRetries(0);
+ m_connections.append(connection);
+
+ connect(connection, &SungrowModbusTcpConnection::reachableChanged, this, [=](bool reachable){
+ qCDebug(dcSungrow()) << "Sungrow Modbus TCP Connection reachable changed:" << reachable;
+ if (!reachable) {
+ cleanupConnection(connection);
+ return;
+ }
+ qCDebug(dcSungrow()) << "Connected, proceeding with initialization";
+
+ connect(connection, &SungrowModbusTcpConnection::initializationFinished, this, [=](bool success){
+ if (!success) {
+ qCDebug(dcSungrow()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";
+ cleanupConnection(connection);
+ return;
+ }
+
+ qCDebug(dcSungrow()) << "Discovery: Initialized successfully" << networkDeviceInfo << connection->serialNumber();
+ qCDebug(dcSungrow()) << " - Protocol number:" << connection->protocolNumber();
+ qCDebug(dcSungrow()) << " - Protocol version:" << connection->protocolVersion();
+ qCDebug(dcSungrow()) << " - ARM software version:" << connection->armSoftwareVersion();
+ qCDebug(dcSungrow()) << " - DSP software version:" << connection->dspSoftwareVersion();
+
+ if (connection->deviceTypeCode() >= 0xd00 && connection->deviceTypeCode() <= 0xeff) {
+ SungrowDiscoveryResult result;
+ result.networkDeviceInfo = networkDeviceInfo;
+ result.serialNumber = connection->serialNumber();
+ result.nominalOutputPower = connection->nominalOutputPower();
+ result.deviceType = connection->deviceTypeCode();
+ m_discoveryResults.append(result);
+ }
+
+ connection->disconnectDevice();
+ });
+
+ qCDebug(dcSungrow()) << "Discovery: The host" << networkDeviceInfo << "is reachable. Starting with initialization.";
+ if (!connection->initialize()) {
+ qCDebug(dcSungrow()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";
+ cleanupConnection(connection);
+ }
+ });
+
+ // In case of an error skip the host
+ connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionStateChanged, this, [=](bool connected){
+ if (connected) {
+ qCDebug(dcSungrow()) << "Discovery: Connected with" << networkDeviceInfo.address().toString() << m_port;
+ }
+ });
+
+ // In case of an error skip the host
+ connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [=](QModbusDevice::Error error){
+ if (error != QModbusDevice::NoError) {
+ qCDebug(dcSungrow()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";
+ cleanupConnection(connection);
+ }
+ });
+
+ // If the reachability check failed skip the host
+ connect(connection, &SungrowModbusTcpConnection::checkReachabilityFailed, this, [=](){
+ qCDebug(dcSungrow()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";
+ cleanupConnection(connection);
+ });
+
+ connection->connectDevice();
+}
+
+void SungrowDiscovery::cleanupConnection(SungrowModbusTcpConnection *connection)
+{
+ qCDebug(dcSungrow()) << "Discovery: Cleanup connection" << connection->modbusTcpMaster();
+ m_connections.removeAll(connection);
+ connection->disconnectDevice();
+ connection->deleteLater();
+}
+
+void SungrowDiscovery::finishDiscovery()
+{
+ qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
+
+ foreach (SungrowModbusTcpConnection *connection, m_connections)
+ cleanupConnection(connection);
+
+ qCDebug(dcSungrow()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() << "Sungrow inverters in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
+ emit discoveryFinished();
+}
diff --git a/sungrow/sungrowdiscovery.h b/sungrow/sungrowdiscovery.h
new file mode 100644
index 0000000..6dc497d
--- /dev/null
+++ b/sungrow/sungrowdiscovery.h
@@ -0,0 +1,76 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef SUNGROWDISCOVERY_H
+#define SUNGROWDISCOVERY_H
+
+#include
+#include
+
+#include
+
+#include "sungrowmodbustcpconnection.h"
+
+class SungrowDiscovery : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SungrowDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 502, quint16 modbusAddress = 1, QObject *parent = nullptr);
+ typedef struct SungrowDiscoveryResult {
+ QString serialNumber;
+ NetworkDeviceInfo networkDeviceInfo;
+ float nominalOutputPower;
+ int deviceType;
+ } SungrowDiscoveryResult;
+
+ void startDiscovery();
+
+ QList discoveryResults() const;
+
+signals:
+ void discoveryFinished();
+
+private:
+ NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
+ quint16 m_port;
+ quint16 m_modbusAddress;
+
+ QDateTime m_startDateTime;
+
+ QList m_connections;
+ QList m_discoveryResults;
+
+ void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
+ void cleanupConnection(SungrowModbusTcpConnection *connection);
+
+ void finishDiscovery();
+};
+
+#endif // SUNGROWDISCOVERY_H
diff --git a/sungrow/translations/250c9b83-1127-4013-bbd0-11e7ea482057-en_US.ts b/sungrow/translations/250c9b83-1127-4013-bbd0-11e7ea482057-en_US.ts
new file mode 100644
index 0000000..3bafa32
--- /dev/null
+++ b/sungrow/translations/250c9b83-1127-4013-bbd0-11e7ea482057-en_US.ts
@@ -0,0 +1,203 @@
+
+
+
+
+ IntegrationPluginSungrow
+
+
+ The network device discovery is not available.
+
+
+
+
+ The MAC address is not known. Please reconfigure this inverter.
+
+
+
+
+ Sungrow
+
+
+ Active power
+ The name of the StateType ({6fb11fe8-da07-11ee-b77f-8393cc11be21}) of ThingClass sungrowInverterTcp
+
+
+
+
+ Battery critical
+ The name of the StateType ({13f48cde-da08-11ee-81c8-3362a92b58c8}) of ThingClass sungrowBattery
+
+
+
+
+ Battery level
+ The name of the StateType ({1738bcf8-da08-11ee-b1d0-a3e1183dabba}) of ThingClass sungrowBattery
+
+
+
+
+ Capacity
+ The name of the StateType ({264ac092-da08-11ee-ad8d-9f0751d6c499}) of ThingClass sungrowBattery
+
+
+
+
+ Charging state
+ The name of the StateType ({29e8a6d8-da08-11ee-87ba-539307b7d2ee}) of ThingClass sungrowBattery
+
+
+
+
+
+
+ Connected
+ The name of the StateType ({0ff3c834-da08-11ee-bac7-0f1044f86ea3}) of ThingClass sungrowBattery
+----------
+The name of the StateType ({d26b59dc-da07-11ee-b494-3781cc3081a7}) of ThingClass sungrowMeter
+----------
+The name of the StateType ({6c68b170-da07-11ee-9891-3335ced0ad72}) of ThingClass sungrowInverterTcp
+
+
+
+
+ Current phase A
+ The name of the StateType ({f3d850c0-da07-11ee-883f-4ff8b7e55bdc}) of ThingClass sungrowMeter
+
+
+
+
+ Current phase B
+ The name of the StateType ({f82603de-da07-11ee-93bc-6b9f9f333c30}) of ThingClass sungrowMeter
+
+
+
+
+ Current phase C
+ The name of the StateType ({fcf77988-da07-11ee-99da-3b2415014506}) of ThingClass sungrowMeter
+
+
+
+
+ Current power
+ The name of the StateType ({d777530e-da07-11ee-a526-c7d23dd34f57}) of ThingClass sungrowMeter
+
+
+
+
+
+ Frequency
+ The name of the StateType ({07b526ea-da08-11ee-ab24-ab0d1ca8555d}) of ThingClass sungrowMeter
+----------
+The name of the StateType ({84bcbc58-da07-11ee-b94d-a39792ee6f59}) of ThingClass sungrowInverterTcp
+
+
+
+
+ MAC address
+ The name of the ParamType (ThingClass: sungrowInverterTcp, Type: thing, ID: {62137142-da07-11ee-9522-2f74f3b1fc5d})
+
+
+
+
+
+ Sungrow
+ The name of the vendor ({cdc58e0d-bfdb-45d9-b961-9c0b036c35aa})
+----------
+The name of the plugin Sungrow ({250c9b83-1127-4013-bbd0-11e7ea482057})
+
+
+
+
+ Sungrow Battery
+ The name of the ThingClass ({0aea1b90-da08-11ee-9195-afc9f857a324})
+
+
+
+
+ Sungrow Inverter
+ The name of the ThingClass ({59cb2da4-da07-11ee-adea-7397f8a9afe9})
+
+
+
+
+ Sungrow Meter
+ The name of the ThingClass ({a935e49c-da07-11ee-bd11-3fcf27dc6373})
+
+
+
+
+
+ Temperature
+ The name of the StateType ({221ccae2-da08-11ee-9e4d-4b6abbf9e564}) of ThingClass sungrowBattery
+----------
+The name of the StateType ({80b35fc2-da07-11ee-a73d-5774b082c92b}) of ThingClass sungrowInverterTcp
+
+
+
+
+ Total energy produced
+ The name of the StateType ({7bf092a2-da07-11ee-abe9-138e66e6f2a5}) of ThingClass sungrowInverterTcp
+
+
+
+
+ Total imported energy
+ The name of the StateType ({03ef972a-da08-11ee-9a1f-d741d1e276be}) of ThingClass sungrowMeter
+
+
+
+
+ Total real power
+ The name of the StateType ({1aa8cf86-da08-11ee-9697-3b0a8023db88}) of ThingClass sungrowBattery
+
+
+
+
+ Total returned energy
+ The name of the StateType ({00eb83c2-da08-11ee-b67d-1f74a41e6218}) of ThingClass sungrowMeter
+
+
+
+
+ Voltage
+ The name of the StateType ({1e6bf760-da08-11ee-ba39-c31ecdbb8fc9}) of ThingClass sungrowBattery
+
+
+
+
+ Voltage phase A
+ The name of the StateType ({e80bb836-da07-11ee-afd1-dbff8484ba11}) of ThingClass sungrowMeter
+
+
+
+
+ Voltage phase B
+ The name of the StateType ({ebefd252-da07-11ee-a8a4-c7ea9df0b6f9}) of ThingClass sungrowMeter
+
+
+
+
+ Voltage phase C
+ The name of the StateType ({efd82b30-da07-11ee-9668-7b523696940d}) of ThingClass sungrowMeter
+
+
+
+
+ charging
+ The name of a possible value of StateType {29e8a6d8-da08-11ee-87ba-539307b7d2ee} of ThingClass sungrowBattery
+
+
+
+
+ discharging
+ The name of a possible value of StateType {29e8a6d8-da08-11ee-87ba-539307b7d2ee} of ThingClass sungrowBattery
+
+
+
+
+ idle
+ The name of a possible value of StateType {29e8a6d8-da08-11ee-87ba-539307b7d2ee} of ThingClass sungrowBattery
+
+
+
+