diff --git a/wattsonic/README.md b/wattsonic/README.md index b0a1386..3417d07 100644 --- a/wattsonic/README.md +++ b/wattsonic/README.md @@ -1,4 +1,44 @@ # Wattsonic -This plugin allows to connect wattsonic hybrid inverters to nymea +This plugin allows to connect wattsonic hybrid inverters to nymea. If there are any meters and energy storages connected to the system, they should be detected automatically. +## Supported inverters + +Currently all `Gen2` and `Gen3` single and three phase inverters should be supported. + +The full list of supported models can be found here: + +Single Phase Hybrid + +* 3.0K-30A-1P +* 3.6K-30A-1P +* 4.2K-30A-1P +* 4.6K-30A-1P +* 5.0K-30A-1P +* 6.0K-30A-1P +* 7.0K-30A-1P +* 8.0K-30A-1P +* 3.0K-30A-1P-S +* 3.6K-30A-1P-S + +Three Phase Hybrid + +* 4.0K-25A-3P +* 5.0K-25A-3P +* 6.0K-25A-3P +* 8.0K-25A-3P +* 10K-25A-3P +* 12K-25A-3P +* 10K-40A-3P +* 12K-40A-3P +* 15K-40A-3P +* 20K-40A-3P + + +Three Phase Hybrid (25-50K) + +* 25K-100A-3P +* 30K-100A-3P +* 36K-100A-3P +* 40K-100A-3P +* 50K-100A-3P diff --git a/wattsonic/integrationpluginwattsonic.cpp b/wattsonic/integrationpluginwattsonic.cpp index 98920da..258ad38 100644 --- a/wattsonic/integrationpluginwattsonic.cpp +++ b/wattsonic/integrationpluginwattsonic.cpp @@ -54,8 +54,8 @@ void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info) foreach (const WattsonicDiscovery::Result &result, discovery->discoveryResults()) { - QString name = "Wattsonic inverter (" + result.serialNumber + ")"; - ThingDescriptor descriptor(inverterThingClassId, name, QString("Modbus ID: %1").arg(result.slaveId)); + QString name = "Wattsonic " + result.inverterInfo.type + + " " + result.inverterInfo.model; + ThingDescriptor descriptor(inverterThingClassId, name, QString("Serial number: %1").arg(result.serialNumber)); qCDebug(dcWattsonic()) << "Discovered:" << descriptor.title() << descriptor.description(); ParamList params { @@ -157,16 +157,16 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) return; } - WattsonicModbusRtuConnection *connection = new WattsonicModbusRtuConnection(hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid), slaveId, this); + WattsonicInverter *connection = new WattsonicInverter(hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid), slaveId, this); m_connections.insert(thing, connection); // Only for setup - connect(info, &ThingSetupInfo::aborted, connection, &WattsonicModbusRtuConnection::deleteLater); + connect(info, &ThingSetupInfo::aborted, connection, &WattsonicInverter::deleteLater); connect(info, &ThingSetupInfo::aborted, this, [=](){ m_connections.take(info->thing())->deleteLater(); }); - connect(connection, &WattsonicModbusRtuConnection::initializationFinished, info, [=](bool success){ + connect(connection, &WattsonicInverter::customInitializationFinished, info, [=](bool success){ qCDebug(dcWattsonic()) << "Initialization finished" << success; if (info->isInitialSetup() && !success) { m_connections.take(info->thing())->deleteLater(); @@ -179,7 +179,7 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) }); // Runtime connections - connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [connection, thing, this](bool reachable){ + connect(connection, &WattsonicInverter::reachableChanged, thing, [connection, thing, this](bool reachable){ qCDebug(dcWattsonic()) << "Reachable state changed" << reachable; if (reachable) { connection->initialize(); @@ -188,11 +188,11 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) setConnectedStates(thing, reachable); }); - connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [this, thing](bool reachable){ + connect(connection, &WattsonicInverter::reachableChanged, thing, [this, thing](bool reachable){ setConnectedStates(thing, reachable); }); - connect(connection, &WattsonicModbusRtuConnection::updateFinished, thing, [this, connection, thing](){ + connect(connection, &WattsonicInverter::updateFinished, thing, [this, connection, thing](){ qCDebug(dcWattsonic()) << "Update finished:" << thing->name() << connection; // We received an update, make sure we are connected @@ -207,7 +207,13 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) // Check if a meter is connected or not. We detect a meter by reading the totalEnergyPurchasedFromGrid totalEnergyInjectedToGrid registers, // As of now, there is no other way to detect the presence of the meter. Voltage is always there, even if there is no meter connected. - bool meterDetected = connection->totalEnergyPurchasedFromGrid() != 0 || connection->totalEnergyInjectedToGrid() != 0; + + // Some meters do not have the total counters, for whatever reason... + bool meterDetected = connection->totalEnergyInjectedToGrid() != 0 || + connection->totalEnergyPurchasedFromGrid() != 0 || + connection->phaseAPower() != 0 || + connection->phaseBPower() != 0 || + connection->phaseCPower() != 0; if (meterDetected) { if (meters.isEmpty()) { @@ -232,21 +238,21 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) if (!meters.isEmpty()) { Thing *meter = meters.first(); meter->setStateValue(meterCurrentPowerStateTypeId, connection->totalPowerOnMeter() * -1.0); - meter->setStateValue(meterTotalEnergyConsumedStateTypeId, connection->totalEnergyPurchasedFromGrid() / 10.0); - meter->setStateValue(meterTotalEnergyProducedStateTypeId, connection->totalEnergyInjectedToGrid() / 10.0); + meter->setStateValue(meterTotalEnergyConsumedStateTypeId, connection->totalEnergyPurchasedFromGrid()); + meter->setStateValue(meterTotalEnergyProducedStateTypeId, connection->totalEnergyInjectedToGrid()); meter->setStateValue(meterCurrentPowerPhaseAStateTypeId, connection->phaseAPower() * -1.0); meter->setStateValue(meterCurrentPowerPhaseBStateTypeId, connection->phaseBPower() * -1.0); meter->setStateValue(meterCurrentPowerPhaseCStateTypeId, connection->phaseCPower() * -1.0); - meter->setStateValue(meterVoltagePhaseAStateTypeId, connection->gridPhaseAVoltage() / 10.0); - meter->setStateValue(meterVoltagePhaseBStateTypeId, connection->gridPhaseBVoltage() / 10.0); - meter->setStateValue(meterVoltagePhaseCStateTypeId, connection->gridPhaseCVoltage() / 10.0); + meter->setStateValue(meterVoltagePhaseAStateTypeId, connection->gridPhaseAVoltage()); + meter->setStateValue(meterVoltagePhaseBStateTypeId, connection->gridPhaseBVoltage()); + meter->setStateValue(meterVoltagePhaseCStateTypeId, connection->gridPhaseCVoltage()); // The phase current registers don't seem to contain proper values. Calculating ourselves instead - // meter->setStateValue(meterCurrentPhaseAStateTypeId, connection->gridPhaseACurrent() / 10.0); - // meter->setStateValue(meterCurrentPhaseBStateTypeId, connection->gridPhaseBCurrent() / 10.0); - // meter->setStateValue(meterCurrentPhaseCStateTypeId, connection->gridPhaseCCurrent() / 10.0); - meter->setStateValue(meterCurrentPhaseAStateTypeId, (connection->phaseAPower() * -1.0) / (connection->gridPhaseAVoltage() / 10.0)); - meter->setStateValue(meterCurrentPhaseBStateTypeId, (connection->phaseBPower() * -1.0) / (connection->gridPhaseBVoltage() / 10.0)); - meter->setStateValue(meterCurrentPhaseCStateTypeId, (connection->phaseCPower() * -1.0) / (connection->gridPhaseCVoltage() / 10.0)); + // meter->setStateValue(meterCurrentPhaseAStateTypeId, connection->gridPhaseACurrent()); + // meter->setStateValue(meterCurrentPhaseBStateTypeId, connection->gridPhaseBCurrent()); + // meter->setStateValue(meterCurrentPhaseCStateTypeId, connection->gridPhaseCCurrent()); + meter->setStateValue(meterCurrentPhaseAStateTypeId, (connection->phaseAPower() * -1.0) / connection->gridPhaseAVoltage()); + meter->setStateValue(meterCurrentPhaseBStateTypeId, (connection->phaseBPower() * -1.0) / connection->gridPhaseBVoltage()); + meter->setStateValue(meterCurrentPhaseCStateTypeId, (connection->phaseCPower() * -1.0) / connection->gridPhaseCVoltage()); qCInfo(dcWattsonic()) << "Updating meter:" << meter->stateValue(meterCurrentPowerStateTypeId).toDouble() << "W" << meter->stateValue(meterTotalEnergyProducedStateTypeId).toDouble() << "kWh"; } @@ -273,14 +279,15 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) if (!batteries.isEmpty() && connection->SOC() > 0) { Thing *battery = batteries.first(); - QHash map { - {WattsonicModbusRtuConnection::BatteryModeDischarge, "discharging"}, - {WattsonicModbusRtuConnection::BatteryModeCharge, "charging"} + QHash map { + {WattsonicInverter::BatteryModeDischarge, "discharging"}, + {WattsonicInverter::BatteryModeCharge, "charging"} }; + battery->setStateValue(batteryChargingStateStateTypeId, map.value(connection->batteryMode())); battery->setStateValue(batteryCurrentPowerStateTypeId, connection->batteryPower() * -1.0); - battery->setStateValue(batteryBatteryLevelStateTypeId, connection->SOC() / 100.0); - battery->setStateValue(batteryBatteryCriticalStateTypeId, connection->SOC() < 500); + battery->setStateValue(batteryBatteryLevelStateTypeId, connection->SOC()); + battery->setStateValue(batteryBatteryCriticalStateTypeId, connection->SOC() < 5); } }); } diff --git a/wattsonic/integrationpluginwattsonic.h b/wattsonic/integrationpluginwattsonic.h index a682003..eb2a72e 100644 --- a/wattsonic/integrationpluginwattsonic.h +++ b/wattsonic/integrationpluginwattsonic.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2024, nymea GmbH +* Copyright 2013 - 2025, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -36,8 +36,7 @@ #include #include "extern-plugininfo.h" - -#include "wattsonicmodbusrtuconnection.h" +#include "wattsonicinverter.h" class IntegrationPluginWattsonic: public IntegrationPlugin { @@ -58,7 +57,7 @@ private: void setupWattsonicConnection(ThingSetupInfo *info); PluginTimer *m_pluginTimer = nullptr; - QHash m_connections; + QHash m_connections; void setConnectedStates(Thing * thing, bool connected); }; diff --git a/wattsonic/wattsonic-registers.json b/wattsonic/wattsonic-registers.json index da8c26f..9df71f1 100644 --- a/wattsonic/wattsonic-registers.json +++ b/wattsonic/wattsonic-registers.json @@ -48,8 +48,7 @@ ] } ], - "blocks": [ - ], + "blocks": [ ], "registers": [ { "id": "serialNumber", @@ -61,6 +60,16 @@ "description": "Serial number", "access": "RO" }, + { + "id": "equipmentInfo", + "address": 10008, + "size": 1, + "type": "uint16", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "EquipmentInfo", + "access": "RO" + }, { "id": "firmwareVersion", "address": 10011, @@ -140,7 +149,8 @@ "registerType": "holdingRegister", "description": "Total grid injection energy on meter", "defaultValue": 0, - "unit": "1/100 kWh", + "staticScaleFactor": -2, + "unit": "kWh", "access": "RO" }, { @@ -152,7 +162,8 @@ "registerType": "holdingRegister", "description": "Total purchasing energy from grid on meter", "defaultValue": 0, - "unit": "1/100 kWh", + "staticScaleFactor": -2, + "unit": "kWh", "access": "RO" }, { @@ -164,7 +175,8 @@ "registerType": "holdingRegister", "description": "Grid Phase A Voltage", "defaultValue": 0, - "unit": "1/10 V", + "staticScaleFactor": -1, + "unit": "V", "access": "RO" }, { @@ -176,7 +188,8 @@ "registerType": "holdingRegister", "description": "Grid Phase A Current", "defaultValue": 0, - "unit": "1/10 A", + "staticScaleFactor": -1, + "unit": "A", "access": "RO" }, { @@ -188,7 +201,8 @@ "registerType": "holdingRegister", "description": "Grid Phase B Voltage", "defaultValue": 0, - "unit": "1/10 V", + "staticScaleFactor": -1, + "unit": "V", "access": "RO" }, { @@ -200,7 +214,8 @@ "registerType": "holdingRegister", "description": "Grid Phase B Current", "defaultValue": 0, - "unit": "1/10 A", + "staticScaleFactor": -1, + "unit": "A", "access": "RO" }, { @@ -212,7 +227,8 @@ "registerType": "holdingRegister", "description": "Grid Phase C Voltage", "defaultValue": 0, - "unit": "1/10 V", + "staticScaleFactor": -1, + "unit": "V", "access": "RO" }, { @@ -224,7 +240,8 @@ "registerType": "holdingRegister", "description": "Grid Phase C Current", "defaultValue": 0, - "unit": "1/10 A", + "staticScaleFactor": -1, + "unit": "A", "access": "RO" }, { @@ -248,7 +265,8 @@ "registerType": "holdingRegister", "description": "Total PV Generation from installation", "defaultValue": 0, - "unit": "1/10 kWh", + "staticScaleFactor": -1, + "unit": "kWh", "access": "RO" }, { @@ -263,111 +281,180 @@ "unit": "W", "access": "RO" }, + + { - "id": "totalBackupP", - "address": 30230, - "size": 2, - "type": "int32", - "readSchedule": "update", - "registerType": "holdingRegister", - "description": "Total_Backup_P/AC Active Power", - "unit": "W", - "defaultValue": 0, - "access": "RO" - }, - { - "id": "batteryVoltageDc", + "id": "batteryVoltageDcGen3", "address": 30254, "size": 1, "type": "uint16", "readSchedule": "update", "registerType": "holdingRegister", - "description": "Battery voltage DC", + "description": "Battery voltage DC (gen3)", "defaultValue": 0, - "unit": "1/10 V", + "staticScaleFactor": -1, + "unit": "V", "access": "RO" }, { - "id": "batteryMode", + "id": "batteryModeGen3", "address": 30256, "size": 1, "type": "uint16", "readSchedule": "update", "registerType": "holdingRegister", - "description": "Battery mode", + "description": "Battery mode (gen3)", "enum": "BatteryMode", "defaultValue": "BatteryModeDischarge", "access": "RO" }, { - "id": "batteryPower", + "id": "batteryPowerGen3", "address": 30258, "size": 2, "type": "int32", "readSchedule": "update", "registerType": "holdingRegister", - "description": "Battery power", + "description": "Battery power (gen3)", "defaultValue": 0, "unit": "W", "access": "RO" }, { - "id": "totalEnergyInjectedToGrid", + "id": "totalEnergyInjectedToGridGen3", "address": 31102, "size": 2, "type": "uint32", "readSchedule": "update", "registerType": "holdingRegister", - "description": "Total energy injected to grid", + "description": "Total energy injected to grid (gen3)", "defaultValue": 0, - "unit": "1/10 kWh", + "staticScaleFactor": -1, + "unit": "kWh", "access": "RO" }, { - "id": "totalEnergyPurchasedFromGrid", + "id": "totalEnergyPurchasedFromGridGen3", "address": 31104, "size": 2, "type": "uint32", "readSchedule": "update", "registerType": "holdingRegister", - "description": "Total energy purchased from grid", + "description": "Total energy purchased from grid (gen3)", "defaultValue": 0, - "unit": "1/10 kWh", + "staticScaleFactor": -1, + "unit": "kWh", "access": "RO" }, { - "id": "batteryStrings", - "address": 32001, - "size": 1, - "type": "uint16", - "readSchedule": "update", - "registerType": "holdingRegister", - "description": "Battery strings", - "defaultValue": 0, - "access": "RO" - }, - { - "id": "SOC", + "id": "SOCGen3", "address": 33000, "size": 1, "type": "uint16", "readSchedule": "update", "registerType": "holdingRegister", - "description": "SOC", + "description": "SOC (gen3)", "defaultValue": 0, - "unit": "% * 100", + "staticScaleFactor": -2, + "unit": "%", "access": "RO" }, { - "id": "SOH", + "id": "SOHGen3", "address": 33001, "size": 1, "type": "uint16", "readSchedule": "update", "registerType": "holdingRegister", - "description": "SOH", + "description": "SOH (gen3)", "defaultValue": 0, - "unit": "% * 100", + "staticScaleFactor": -2, + "unit": "%", + "access": "RO" + }, + + + { + "id": "batteryVoltageDcGen2", + "address": 40254, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery voltage DC (gen2)", + "defaultValue": 0, + "staticScaleFactor": -1, + "unit": "V", + "access": "RO" + }, + { + "id": "batteryModeGen2", + "address": 40256, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Battery mode (gen2)", + "enum": "BatteryMode", + "defaultValue": "BatteryModeDischarge", + "access": "RO" + }, + { + "id": "batteryPowerGen2", + "address": 40258, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "description": "Battery power (gen2)", + "defaultValue": 0, + "unit": "W", + "access": "RO" + }, + { + "id": "totalEnergyInjectedToGridGen2", + "address": 41102, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Total energy injected to grid (gen2)", + "defaultValue": 0, + "staticScaleFactor": -1, + "unit": "kWh", + "access": "RO" + }, + { + "id": "totalEnergyPurchasedFromGridGen2", + "address": 41104, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Total energy purchased from grid (gen2)", + "defaultValue": 0, + "staticScaleFactor": -1, + "unit": "kWh", + "access": "RO" + }, + { + "id": "SOCGen2", + "address": 43000, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "SOC (gen2)", + "defaultValue": 0, + "staticScaleFactor": -2, + "unit": "%", + "access": "RO" + }, + { + "id": "SOHGen2", + "address": 43001, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "SOH (gen2)", + "defaultValue": 0, + "staticScaleFactor": -2, + "unit": "%", "access": "RO" } ] diff --git a/wattsonic/wattsonic.pro b/wattsonic/wattsonic.pro index 9875f8e..67e7ef9 100644 --- a/wattsonic/wattsonic.pro +++ b/wattsonic/wattsonic.pro @@ -8,8 +8,10 @@ include(../modbus.pri) HEADERS += \ wattsonicdiscovery.h \ - integrationpluginwattsonic.h + integrationpluginwattsonic.h \ + wattsonicinverter.h SOURCES += \ wattsonicdiscovery.cpp \ - integrationpluginwattsonic.cpp + integrationpluginwattsonic.cpp \ + wattsonicinverter.cpp diff --git a/wattsonic/wattsonicdiscovery.cpp b/wattsonic/wattsonicdiscovery.cpp index c71f0c0..e80eb9a 100644 --- a/wattsonic/wattsonicdiscovery.cpp +++ b/wattsonic/wattsonicdiscovery.cpp @@ -43,21 +43,22 @@ WattsonicDiscovery::WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResou void WattsonicDiscovery::startDiscovery(quint16 slaveId) { qCInfo(dcWattsonic()) << "Discovery: Searching for Wattsonic device on modbus RTU..."; + m_candidateMasters.clear(); + m_verifiedMasters.clear(); - QList candidateMasters; foreach (ModbusRtuMaster *master, m_modbusRtuResource->modbusRtuMasters()) { if (master->baudrate() == 9600 && master->dataBits() == 8 && master->stopBits() == 1 && master->parity() == QSerialPort::NoParity) { - candidateMasters.append(master); + m_candidateMasters.append(master); } } - if (candidateMasters.isEmpty()) { + if (m_candidateMasters.isEmpty()) { qCWarning(dcWattsonic()) << "Discovery: No usable modbus RTU master found."; emit discoveryFinished(false); return; } - foreach (ModbusRtuMaster *master, candidateMasters) { + foreach (ModbusRtuMaster *master, m_candidateMasters) { if (master->connected()) { tryConnect(master, slaveId); } else { @@ -74,18 +75,35 @@ QList WattsonicDiscovery::discoveryResults() const void WattsonicDiscovery::tryConnect(ModbusRtuMaster *master, quint16 slaveId) { qCDebug(dcWattsonic()) << "Discovery: Scanning modbus RTU master" << master->modbusUuid() << "Slave ID:" << slaveId; + m_verifiedMasters.append(master); - ModbusRtuReply *reply = master->readHoldingRegister(slaveId, 10000, 8); - connect(reply, &ModbusRtuReply::finished, this, [this, master, reply, slaveId](){ + WattsonicInverter *connection = new WattsonicInverter(master, slaveId, this); + connect(connection, &WattsonicInverter::reachableChanged, this, [connection](bool reachable){ + if (reachable) { + qCDebug(dcWattsonic()) << "Discovery: The connection is now reachable. Starting the initialization"; + connection->initialize(); + } else { + connection->deleteLater(); + } + }); - if (reply->error() == ModbusRtuReply::NoError) { - QString serialNumber = ModbusDataUtils::convertToString(reply->result(), ModbusDataUtils::ByteOrderBigEndian); - qCDebug(dcWattsonic()) << "Discovery: Test reply finished!" << reply->error() << reply->result() << serialNumber; - Result result {master->modbusUuid(), serialNumber, slaveId}; - m_discoveryResults.append(result); + connect(connection, &WattsonicInverter::customInitializationFinished, this, [this, connection, master, slaveId](bool success){ + + if (!success) { + qCDebug(dcWattsonic()) << "Initialization failed on" << master << "skip "; + return; } - emit discoveryFinished(true); + qCDebug(dcWattsonic()) << "Discovery: Init finished successfully:" << connection->serialNumber() + << connection->inverterInfo().type << connection->inverterInfo().model << connection->generation(); + + Result result {master->modbusUuid(), connection->serialNumber(), slaveId, connection->generation(), connection->inverterInfo()}; + m_discoveryResults.append(result); + + if (m_verifiedMasters.count() == m_candidateMasters.count()) { + qCDebug(dcWattsonic()) << "Discovery: Verified all masters. Finish discovery with" << m_discoveryResults.count() << "discovered inverters."; + emit discoveryFinished(true); + } }); } diff --git a/wattsonic/wattsonicdiscovery.h b/wattsonic/wattsonicdiscovery.h index 4bd9927..77f04c6 100644 --- a/wattsonic/wattsonicdiscovery.h +++ b/wattsonic/wattsonicdiscovery.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2023, nymea GmbH +* Copyright 2013 - 2025, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -32,8 +32,11 @@ #define WATTSONICDISCOVERY_H #include + #include +#include "wattsonicinverter.h" + class WattsonicDiscovery : public QObject { Q_OBJECT @@ -44,12 +47,15 @@ public: QUuid modbusRtuMasterId; QString serialNumber; quint16 slaveId; + WattsonicInverter::Generation generation; + WattsonicInverter::Info inverterInfo; }; void startDiscovery(quint16 slaveId); QList discoveryResults() const; + signals: void discoveryFinished(bool modbusRtuMasterAvailable); @@ -58,7 +64,8 @@ private slots: private: ModbusRtuHardwareResource *m_modbusRtuResource = nullptr; - + QList m_candidateMasters; + QList m_verifiedMasters; QList m_discoveryResults; }; diff --git a/wattsonic/wattsonicinverter.cpp b/wattsonic/wattsonicinverter.cpp new file mode 100644 index 0000000..47fb94c --- /dev/null +++ b/wattsonic/wattsonicinverter.cpp @@ -0,0 +1,1241 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "wattsonicinverter.h" + +#include +Q_DECLARE_LOGGING_CATEGORY(dcWattsonicModbusRtuConnection) + +WattsonicInverter::WattsonicInverter(ModbusRtuMaster *modbusRtuMaster, quint16 slaveId, QObject *parent) + : WattsonicModbusRtuConnection{modbusRtuMaster, slaveId, parent} +{ + connect(this, &WattsonicInverter::initializationFinished, this, [this](bool success){ + + // Check if we can read a gen2 register, otherwise it is a gen3 register + if (!success) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Initialization finished with errors."; + emit customInitializationFinished(success); + return; + } + + // Init was successfully + + // Verify equipment info + WattsonicInverter::Info inverterInfo = getInverterInfo(equipmentInfo()); + if (inverterInfo.model.isEmpty() || inverterInfo.type.isEmpty()) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Invalid equipment info" << equipmentInfo(); + emit customInitializationFinished(false); + return; + } + + qCDebug(dcWattsonicModbusRtuConnection()) << "Internal init finished successfully" << serialNumber() << inverterInfo.type << inverterInfo.model; + m_inverterInfo = inverterInfo; + + // Verify which generation this is by testing one generation dependent register. + ModbusRtuReply *reply = readBatteryVoltageDcGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Unable to test the generation request on the modbus master. Something might be wrong with the connection."; + emit customInitializationFinished(false); + return; + } + + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Reply finished with error after reading gen 2 register. Assuming this is a gen 3 device."; + m_generation = Generation3; + emit customInitializationFinished(true); + return; + } else { + qCDebug(dcWattsonicModbusRtuConnection()) << "Successfully read gen2 register. Assuming this is a gen 2 device."; + m_generation = Generation2; + emit customInitializationFinished(true); + } + }); + }); +} + +WattsonicInverter::Info WattsonicInverter::inverterInfo() const +{ + return m_inverterInfo; +} + +WattsonicInverter::Generation WattsonicInverter::generation() const +{ + return m_generation; +} + +float WattsonicInverter::batteryVoltageDc() const +{ + switch (m_generation) { + case Generation2: + return batteryVoltageDcGen2(); + default: + return batteryVoltageDcGen3(); + } +} + +WattsonicModbusRtuConnection::BatteryMode WattsonicInverter::batteryMode() const +{ + switch (m_generation) { + case Generation2: + return batteryModeGen2(); + default: + return batteryModeGen3(); + } +} + +qint32 WattsonicInverter::batteryPower() const +{ + switch (m_generation) { + case Generation2: + return batteryPowerGen2(); + default: + return batteryPowerGen3(); + } +} + +float WattsonicInverter::totalEnergyInjectedToGrid() const +{ + switch (m_generation) { + case Generation2: + return totalEnergyInjectedToGridGen2(); + default: + return totalEnergyInjectedToGridGen3(); + } +} + +float WattsonicInverter::totalEnergyPurchasedFromGrid() const +{ + switch (m_generation) { + case Generation2: + return totalEnergyPurchasedFromGridGen2(); + default: + return totalEnergyPurchasedFromGridGen3(); + } +} + +float WattsonicInverter::SOC() const +{ + switch (m_generation) { + case Generation2: + return SOCGen2(); + default: + return SOCGen3(); + } +} + +float WattsonicInverter::SOH() const +{ + switch (m_generation) { + case Generation2: + return SOHGen2(); + default: + return SOHGen3(); + } +} + +bool WattsonicInverter::update() +{ + if (m_generation == GenerationUnknwon) { + qCDebug(dcWattsonicModbusRtuConnection()) << "Tried to update but we don't know yet if this is a gen2 or gen3 inverter. Waiting for the information."; + return false; + } + + if (!m_modbusRtuMaster->connected()) { + qCDebug(dcWattsonicModbusRtuConnection()) << "Tried to update the registers but the hardware resource seems not to be connected."; + return false; + } + + if (!m_pendingUpdateReplies.isEmpty()) { + qCDebug(dcWattsonicModbusRtuConnection()) << "Tried to update the registers but there are still some update replies pending. Waiting for them to be finished..."; + return true; + } + + // Hardware resource available but communication not working. + // Try to read the check reachability register to re-evaluatoe the communication... + if (m_modbusRtuMaster->connected() && !m_communicationWorking) { + testReachability(); + return false; + } + + ModbusRtuReply *reply = nullptr; + + // Read Inverter status + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Inverter status\" register:" << 10105 << "size:" << 1; + reply = readInverterStatus(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Inverter status\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processInverterStatusRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Inverter status\" registers" << error << reply->errorString(); + }); + + // Read Phase A power + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Phase A power\" register:" << 10994 << "size:" << 2; + reply = readPhaseAPower(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Phase A power\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processPhaseAPowerRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Phase A power\" registers" << error << reply->errorString(); + }); + + // Read Phase B power + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Phase B power\" register:" << 10996 << "size:" << 2; + reply = readPhaseBPower(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Phase B power\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processPhaseBPowerRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Phase B power\" registers" << error << reply->errorString(); + }); + + // Read Phase C power + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Phase C power\" register:" << 10998 << "size:" << 2; + reply = readPhaseCPower(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Phase C power\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processPhaseCPowerRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Phase C power\" registers" << error << reply->errorString(); + }); + + // Read Total power on meter + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total power on meter\" register:" << 11000 << "size:" << 2; + reply = readTotalPowerOnMeter(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total power on meter\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalPowerOnMeterRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total power on meter\" registers" << error << reply->errorString(); + }); + + // Read Total grid injection energy on meter + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total grid injection energy on meter\" register:" << 11002 << "size:" << 2; + reply = readTotalGridInjectionEnergy(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total grid injection energy on meter\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalGridInjectionEnergyRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total grid injection energy on meter\" registers" << error << reply->errorString(); + }); + + // Read Total purchasing energy from grid on meter + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total purchasing energy from grid on meter\" register:" << 11004 << "size:" << 2; + reply = readTotalPurchasingEnergyFromGrid(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total purchasing energy from grid on meter\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalPurchasingEnergyFromGridRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total purchasing energy from grid on meter\" registers" << error << reply->errorString(); + }); + + // Read Grid Phase A Voltage + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Grid Phase A Voltage\" register:" << 11009 << "size:" << 1; + reply = readGridPhaseAVoltage(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Grid Phase A Voltage\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processGridPhaseAVoltageRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Grid Phase A Voltage\" registers" << error << reply->errorString(); + }); + + // Read Grid Phase A Current + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Grid Phase A Current\" register:" << 11010 << "size:" << 1; + reply = readGridPhaseACurrent(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Grid Phase A Current\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processGridPhaseACurrentRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Grid Phase A Current\" registers" << error << reply->errorString(); + }); + + // Read Grid Phase B Voltage + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Grid Phase B Voltage\" register:" << 11011 << "size:" << 1; + reply = readGridPhaseBVoltage(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Grid Phase B Voltage\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processGridPhaseBVoltageRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Grid Phase B Voltage\" registers" << error << reply->errorString(); + }); + + // Read Grid Phase B Current + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Grid Phase B Current\" register:" << 11012 << "size:" << 1; + reply = readGridPhaseBCurrent(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Grid Phase B Current\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processGridPhaseBCurrentRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Grid Phase B Current\" registers" << error << reply->errorString(); + }); + + // Read Grid Phase C Voltage + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Grid Phase C Voltage\" register:" << 11013 << "size:" << 1; + reply = readGridPhaseCVoltage(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Grid Phase C Voltage\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processGridPhaseCVoltageRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Grid Phase C Voltage\" registers" << error << reply->errorString(); + }); + + // Read Grid Phase C Current + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Grid Phase C Current\" register:" << 11014 << "size:" << 1; + reply = readGridPhaseCCurrent(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Grid Phase C Current\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processGridPhaseCCurrentRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Grid Phase C Current\" registers" << error << reply->errorString(); + }); + + // Read P_AC + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"P_AC\" register:" << 11016 << "size:" << 2; + reply = readPAC(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"P_AC\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processPACRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"P_AC\" registers" << error << reply->errorString(); + }); + + // Read Total PV Generation from installation + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total PV Generation from installation\" register:" << 11020 << "size:" << 2; + reply = readTotalPVGenerationFromInstallation(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total PV Generation from installation\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalPVGenerationFromInstallationRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total PV Generation from installation\" registers" << error << reply->errorString(); + }); + + // Read PV Total Input Power + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"PV Total Input Power\" register:" << 11028 << "size:" << 2; + reply = readPvInputTotalPower(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"PV Total Input Power\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processPvInputTotalPowerRegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"PV Total Input Power\" registers" << error << reply->errorString(); + }); + + + if (m_generation == Generation2) { + // Read Battery voltage DC (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Battery voltage DC (gen2)\" register:" << 40254 << "size:" << 1; + reply = readBatteryVoltageDcGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Battery voltage DC (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processBatteryVoltageDcGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Battery voltage DC (gen2)\" registers" << error << reply->errorString(); + }); + + // Read Battery mode (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Battery mode (gen2)\" register:" << 30256 << "size:" << 1; + reply = readBatteryModeGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Battery mode (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processBatteryModeGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Battery mode (gen2)\" registers" << error << reply->errorString(); + }); + + // Read Battery power (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Battery power (gen2)\" register:" << 30258 << "size:" << 2; + reply = readBatteryPowerGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Battery power (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processBatteryPowerGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Battery power (gen2)\" registers" << error << reply->errorString(); + }); + + // Read Total energy injected to grid (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total energy injected to grid (gen2)\" register:" << 31102 << "size:" << 2; + reply = readTotalEnergyInjectedToGridGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total energy injected to grid (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalEnergyInjectedToGridGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total energy injected to grid (gen2)\" registers" << error << reply->errorString(); + }); + + // Read Total energy purchased from grid (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total energy purchased from grid (gen2)\" register:" << 31104 << "size:" << 2; + reply = readTotalEnergyPurchasedFromGridGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total energy purchased from grid (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalEnergyPurchasedFromGridGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total energy purchased from grid (gen2)\" registers" << error << reply->errorString(); + }); + + // Read SOC (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"SOC (gen2)\" register:" << 33000 << "size:" << 1; + reply = readSOCGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"SOC (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processSOCGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"SOC (gen2)\" registers" << error << reply->errorString(); + }); + + // Read SOH (gen2) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"SOH (gen2)\" register:" << 33001 << "size:" << 1; + reply = readSOHGen2(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"SOH (gen2)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processSOHGen2RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"SOH (gen2)\" registers" << error << reply->errorString(); + }); + + } else { + // Read Battery voltage DC (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Battery voltage DC (gen3)\" register:" << 30254 << "size:" << 1; + reply = readBatteryVoltageDcGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Battery voltage DC (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processBatteryVoltageDcGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Battery voltage DC (gen3)\" registers" << error << reply->errorString(); + }); + + // Read Battery mode (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Battery mode (gen3)\" register:" << 30256 << "size:" << 1; + reply = readBatteryModeGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Battery mode (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processBatteryModeGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Battery mode (gen3)\" registers" << error << reply->errorString(); + }); + + // Read Battery power (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Battery power (gen3)\" register:" << 30258 << "size:" << 2; + reply = readBatteryPowerGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Battery power (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processBatteryPowerGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Battery power (gen3)\" registers" << error << reply->errorString(); + }); + + // Read Total energy injected to grid (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total energy injected to grid (gen3)\" register:" << 31102 << "size:" << 2; + reply = readTotalEnergyInjectedToGridGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total energy injected to grid (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalEnergyInjectedToGridGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total energy injected to grid (gen3)\" registers" << error << reply->errorString(); + }); + + // Read Total energy purchased from grid (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"Total energy purchased from grid (gen3)\" register:" << 31104 << "size:" << 2; + reply = readTotalEnergyPurchasedFromGridGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"Total energy purchased from grid (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processTotalEnergyPurchasedFromGridGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"Total energy purchased from grid (gen3)\" registers" << error << reply->errorString(); + }); + + // Read SOC (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"SOC (gen3)\" register:" << 33000 << "size:" << 1; + reply = readSOCGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"SOC (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processSOCGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"SOC (gen3)\" registers" << error << reply->errorString(); + }); + + // Read SOH (gen3) + qCDebug(dcWattsonicModbusRtuConnection()) << "--> Read \"SOH (gen3)\" register:" << 33001 << "size:" << 1; + reply = readSOHGen3(); + if (!reply) { + qCWarning(dcWattsonicModbusRtuConnection()) << "Error occurred while reading \"SOH (gen3)\" registers"; + return false; + } + + if (reply->isFinished()) { + return false; // Broadcast reply returns immediatly + } + + m_pendingUpdateReplies.append(reply); + connect(reply, &ModbusRtuReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + m_pendingUpdateReplies.removeAll(reply); + + if (reply->error() != ModbusRtuReply::NoError) { + verifyUpdateFinished(); + return; + } + + QVector values = reply->result(); + processSOHGen3RegisterValues(values); + verifyUpdateFinished(); + }); + + connect(reply, &ModbusRtuReply::errorOccurred, this, [reply] (ModbusRtuReply::Error error){ + qCWarning(dcWattsonicModbusRtuConnection()) << "ModbusRtu reply error occurred while updating \"SOH (gen3)\" registers" << error << reply->errorString(); + }); + } + + return true; +} + +WattsonicInverter::Info WattsonicInverter::getInverterInfo(quint16 equipmentInfo) +{ + WattsonicInverter::Info inverterType; + quint8 highByte = (equipmentInfo >> 8) & 0xFF; + quint8 lowByte = equipmentInfo & 0xFF; + + switch (highByte) { + case 30: + inverterType.type = "Three Phase Hybrid"; + + switch (lowByte) { + case 0: + inverterType.model = "4.0K-25A-3P"; + break; + case 1: + inverterType.model = "5.0K-25A-3P"; + break; + case 2: + inverterType.model = "6.0K-25A-3P"; + break; + case 3: + inverterType.model = "8.0K-25A-3P"; + break; + case 4: + inverterType.model = "10K-25A-3P"; + break; + case 5: + inverterType.model = "12K-25A-3P"; + break; + case 6: + inverterType.model = "10K-40A-3P"; + break; + case 7: + inverterType.model = "12K-40A-3P"; + break; + case 8: + inverterType.model = "15K-40A-3P"; + break; + case 9: + inverterType.model = "20K-40A-3P"; + break; + default: + break; + } + + break; + case 31: + inverterType.type = "Single Phase Hybrid"; + switch (lowByte) { + case 0: + inverterType.model = "3.0K-30A-1P"; + break; + case 1: + inverterType.model = "3.6K-30A-1P"; + break; + case 2: + inverterType.model = "4.2K-30A-1P"; + break; + case 3: + inverterType.model = "4.6K-30A-1P"; + break; + case 4: + inverterType.model = "5.0K-30A-1P"; + break; + case 5: + inverterType.model = "6.0K-30A-1P"; + break; + case 6: + inverterType.model = "7.0K-30A-1P"; + break; + case 7: + inverterType.model = "8.0K-30A-1P"; + break; + case 8: + inverterType.model = "3.0K-30A-1P-S"; + break; + case 9: + inverterType.model = "3.6K-30A-1P-S"; + break; + default: + break; + } + + break; + case 32: + inverterType.type = "Three Phase Hybrid (25-50K)"; + + switch (lowByte) { + case 0: + inverterType.model = "25K-100A-3P"; + break; + case 1: + inverterType.model = "30K-100A-3P"; + break; + case 2: + inverterType.model = "36K-100A-3P"; + break; + case 3: + inverterType.model = "40K-100A-3P"; + break; + case 4: + inverterType.model = "50K-100A-3P"; + break; + default: + break; + } + + break; + default: + break; + } + + return inverterType; +} + diff --git a/wattsonic/wattsonicinverter.h b/wattsonic/wattsonicinverter.h new file mode 100644 index 0000000..1d146e1 --- /dev/null +++ b/wattsonic/wattsonicinverter.h @@ -0,0 +1,82 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef WATTSONICINVERTER_H +#define WATTSONICINVERTER_H + +#include + +#include "wattsonicmodbusrtuconnection.h" + +class WattsonicInverter : public WattsonicModbusRtuConnection +{ + Q_OBJECT +public: + struct Info { + QString type; + QString model; + }; + + enum Generation { + GenerationUnknwon, + Generation2, + Generation3 + }; + Q_ENUM(Generation) + + explicit WattsonicInverter(ModbusRtuMaster *modbusRtuMaster, quint16 slaveId, QObject *parent = nullptr); + + Generation generation() const; + WattsonicInverter::Info inverterInfo() const; + + // Generation specific registers + float batteryVoltageDc() const; + BatteryMode batteryMode() const; + qint32 batteryPower() const; + float totalEnergyInjectedToGrid() const; + float totalEnergyPurchasedFromGrid() const; + float SOC() const; + float SOH() const; + + bool update() override; + + static WattsonicInverter::Info getInverterInfo(quint16 equipmentInfo); + +signals: + void generationChanged(); + void customInitializationFinished(bool success); + +private: + WattsonicInverter::Info m_inverterInfo; + Generation m_generation = GenerationUnknwon; + +}; + +#endif // WATTSONICINVERTER_H