From 5e29ea6d173c8d677f23df266ad702feae81d387 Mon Sep 17 00:00:00 2001 From: trinnes Date: Mon, 4 Mar 2024 11:12:07 +0100 Subject: [PATCH 1/3] Add Sungrow plug-in --- debian/control | 8 + debian/nymea-plugin-sungrow.install.in | 2 + nymea-plugins-modbus.pro | 1 + sungrow/README.md | 26 + sungrow/integrationpluginsungrow.cpp | 379 +++++++++++ sungrow/integrationpluginsungrow.h | 73 ++ sungrow/integrationpluginsungrow.json | 289 ++++++++ sungrow/meta.json | 13 + sungrow/sungrow-registers.json | 642 ++++++++++++++++++ sungrow/sungrow.png | Bin 0 -> 9006 bytes sungrow/sungrow.pro | 14 + sungrow/sungrowdiscovery.cpp | 161 +++++ sungrow/sungrowdiscovery.h | 76 +++ ...c9b83-1127-4013-bbd0-11e7ea482057-en_US.ts | 203 ++++++ 14 files changed, 1887 insertions(+) create mode 100644 debian/nymea-plugin-sungrow.install.in create mode 100644 sungrow/README.md create mode 100644 sungrow/integrationpluginsungrow.cpp create mode 100644 sungrow/integrationpluginsungrow.h create mode 100644 sungrow/integrationpluginsungrow.json create mode 100644 sungrow/meta.json create mode 100644 sungrow/sungrow-registers.json create mode 100644 sungrow/sungrow.png create mode 100644 sungrow/sungrow.pro create mode 100644 sungrow/sungrowdiscovery.cpp create mode 100644 sungrow/sungrowdiscovery.h create mode 100644 sungrow/translations/250c9b83-1127-4013-bbd0-11e7ea482057-en_US.ts 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..7747067 --- /dev/null +++ b/sungrow/integrationpluginsungrow.cpp @@ -0,0 +1,379 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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(SungrowModbusTcpConnection *connection, m_tcpConnections) { + if (connection->initializing()) { + qCDebug(dcSungrow()) << "Skip updating" << connection->modbusTcpMaster() << "since the connection is still initializing."; + continue; + } + qCDebug(dcSungrow()) << "Updating connection" << connection->modbusTcpMaster()->hostAddress().toString(); + connection->update(); + } + }); + 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..496e919 --- /dev/null +++ b/sungrow/sungrow-registers.json @@ -0,0 +1,642 @@ +{ + "className": "Sungrow", + "protocol": "TCP", + "endianness": "LittleEndian", + "errorLimitUntilNotReachable": 5, + "queuedRequests": true, + "queuedRequestsDelay": 200, + "checkReachableRegister": "inverterTemperature", + "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": "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": "energyValues2", + "readSchedule": "update", + "registers": [ + { + "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": "energyValues3", + "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": "energyValues4", + "readSchedule": "update", + "registers": [ + { + "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": "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" + } + ] + }, + { + "id": "batteryValues", + "readSchedule": "update", + "registers": [ + { + "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" + } + ] + } + ], + "registers": [ + { + "id": "inverterTemperature", + "address": 5007, + "size": 1, + "readSchedule": "update", + "type": "int16", + "registerType": "inputRegister", + "description": "Inverter temperature", + "unit": "°C", + "staticScaleFactor": -1, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "totalExportEnergy", + "address": 13045, + "size": 2, + "readSchedule": "update", + "type": "uint32", + "registerType": "inputRegister", + "description": "Total export energy", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + } + ] +} diff --git a/sungrow/sungrow.png b/sungrow/sungrow.png new file mode 100644 index 0000000000000000000000000000000000000000..67409f3a755b03968c2275c322cec548d46fd891 GIT binary patch literal 9006 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 U%neeo)Bpeg07*qoM6N<$f+)ygasU7T literal 0 HcmV?d00001 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 + + + + From 6bd514f44ea11d1a9509d0b9d7d0fdb00e134410 Mon Sep 17 00:00:00 2001 From: trinnes Date: Thu, 2 May 2024 20:49:16 +0200 Subject: [PATCH 2/3] Sungrow: Reduce amount of modbus requests --- sungrow/sungrow-registers.json | 265 ++++++++++++++++++--------------- 1 file changed, 145 insertions(+), 120 deletions(-) diff --git a/sungrow/sungrow-registers.json b/sungrow/sungrow-registers.json index 496e919..b69d9c9 100644 --- a/sungrow/sungrow-registers.json +++ b/sungrow/sungrow-registers.json @@ -4,8 +4,8 @@ "endianness": "LittleEndian", "errorLimitUntilNotReachable": 5, "queuedRequests": true, - "queuedRequestsDelay": 200, - "checkReachableRegister": "inverterTemperature", + "queuedRequestsDelay": 400, + "checkReachableRegister": "totalPVPower", "enums": [ { "name": "SystemState", @@ -196,6 +196,27 @@ "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, @@ -242,13 +263,16 @@ "staticScaleFactor": -1, "defaultValue": "0", "access": "RO" - } - ] - }, - { - "id": "energyValues2", - "readSchedule": "update", - "registers": [ + }, + { + "id": "dummy1", + "address": 5021, + "size": 11, + "type": "raw", + "registerType": "inputRegister", + "description": "none", + "access": "RO" + }, { "id": "reactivePower", "address": 5032, @@ -286,7 +310,7 @@ ] }, { - "id": "energyValues3", + "id": "energyValues2", "readSchedule": "update", "registers": [ { @@ -403,13 +427,96 @@ "defaultValue": "0", "staticScaleFactor": -1, "access": "RO" - } - ] - }, - { - "id": "energyValues4", - "readSchedule": "update", - "registers": [ + }, + { + "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, @@ -490,6 +597,27 @@ "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" } ] }, @@ -532,111 +660,8 @@ "access": "RO" } ] - }, - { - "id": "batteryValues", - "readSchedule": "update", - "registers": [ - { - "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" - } - ] } ], "registers": [ - { - "id": "inverterTemperature", - "address": 5007, - "size": 1, - "readSchedule": "update", - "type": "int16", - "registerType": "inputRegister", - "description": "Inverter temperature", - "unit": "°C", - "staticScaleFactor": -1, - "defaultValue": "0", - "access": "RO" - }, - { - "id": "totalExportEnergy", - "address": 13045, - "size": 2, - "readSchedule": "update", - "type": "uint32", - "registerType": "inputRegister", - "description": "Total export energy", - "unit": "kWh", - "defaultValue": "0", - "staticScaleFactor": -1, - "access": "RO" - } ] } From 0a06dc565c222202e3704e6fbf26d3eafad007b5 Mon Sep 17 00:00:00 2001 From: trinnes Date: Sun, 12 May 2024 11:35:38 +0200 Subject: [PATCH 3/3] Sungrow: Improve connection stability --- sungrow/integrationpluginsungrow.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/sungrow/integrationpluginsungrow.cpp b/sungrow/integrationpluginsungrow.cpp index 7747067..5fe2cb1 100644 --- a/sungrow/integrationpluginsungrow.cpp +++ b/sungrow/integrationpluginsungrow.cpp @@ -318,13 +318,24 @@ void IntegrationPluginSungrow::postSetupThing(Thing *thing) qCDebug(dcSungrow()) << "Starting plugin timer..."; m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { - foreach(SungrowModbusTcpConnection *connection, m_tcpConnections) { + 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; } - qCDebug(dcSungrow()) << "Updating connection" << connection->modbusTcpMaster()->hostAddress().toString(); - connection->update(); + 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();