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 + + + +