diff --git a/huawei/README.md b/huawei/README.md index dba2cef..82d7646 100644 --- a/huawei/README.md +++ b/huawei/README.md @@ -1,12 +1,39 @@ # Huawei FusionSolar -Connects to a Huawei FusionSolar using Modbus TCP. +Connects to a Huawei FusionSolar using Modbus RTU or TCP. -## Supported things +## Huawei FusionSolar -* Inverter -* Meter (only current power) -* Battery (on Unit 1 and 2) +In order to communicate with the Huawei FusionSolar inverter, a working communikation must be provided. This can be done with a Huawei SmartDongle or with a direct modbus RTU connection. + +Once nymea has connected successfully to the inverter, following devices will be supported: + +* Huawei FusionSolar Inverter (all model supported by the SmartDongle) +* Huawei Meter (connected internally to the Inverter) +* Luna2000 battery units 1 and 2 if connected + +### Huawei SmartDongle + +The [SmartDongle](https://solar.huawei.com/-/media/Solar/attachment/pdf/apac/datasheet/SmartDongle-WLAN-FE.pdf) can be used to communicate +with the Huawei Solar Inverter. In order to allow nymea to read from the device please make sure following requirements are met: + +* SmartDongle software version must be at least `SP130`. +* Connect the Huawei SmartDongle to the network +* Use the official FusonSolar App to enable Modbus TCP access for the Dongle (full access). You can find more informations [here](https://forum.huawei.com/enterprise/en/modbus-tcp-guide/thread/789585-100027?page=1#comments-area). + + +You can also contact the [official Huawei support](mailto:eu_inverter_support@huawei.com) in order to get the update files and instructions, or get it from [here](https://support.huawei.com/enterprise/en/digital-power/sdongle-pid-23826585/software). + +> The SmartDongle provides only access to the registers specified in the Huawei `openAPI`. Full modbus register access requires a modbus RTU connction. + + +### Direct modbus RTU connection (RS485). + +If you want to communicate directly with the inverter using modbus RTU, you need to wire up the connection correctly and set up the Modbus RTU interface in the nymea settings using the right configuations. Then you can discover the Modbus RTU interface while adding the Huawei Modbus RTU inverter in nymea. + +The Modbus RTU offers a full access to all modbus registers available on the inverter. + +Please contact your installer how to enable a modbus RTU connection for your setup. ## More https://solar.huawei.com/eu/ diff --git a/huawei/huawei-fusion-solar-registers.json b/huawei/huawei-fusion-solar-registers.json new file mode 100644 index 0000000..6386d6c --- /dev/null +++ b/huawei/huawei-fusion-solar-registers.json @@ -0,0 +1,280 @@ +{ + "className": "HuaweiFusion", + "protocol": "TCP", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 15, + "checkReachableRegister": "inverterActivePower", + "enums": [ + { + "name": "InverterDeviceStatus", + "values": [ + { + "key": "StandbyInitializing", + "value": 0 + }, + { + "key": "StandbyDetectingInsulationResistance", + "value": 1 + }, + { + "key": "StandbyDetectingIrradiation", + "value": 2 + }, + { + "key": "StandbyDridDetecting", + "value": 3 + }, + { + "key": "Starting", + "value": 256 + }, + { + "key": "OnGrid", + "value": 512 + }, + { + "key": "PowerLimited", + "value": 513 + }, + { + "key": "SelfDerating", + "value": 514 + }, + { + "key": "ShutdownFault", + "value": 768 + }, + { + "key": "ShutdownCommand", + "value": 769 + }, + { + "key": "ShutdownOVGR", + "value": 770 + }, + { + "key": "ShutdownCommunicationDisconnected", + "value": 771 + }, + { + "key": "ShutdownPowerLimit", + "value": 772 + }, + { + "key": "ShutdownManualStartupRequired", + "value": 773 + }, + { + "key": "ShutdownInputUnderpower", + "value": 774 + }, + { + "key": "GridSchedulingPCurve", + "value": 1025 + }, + { + "key": "GridSchedulingQUCurve", + "value": 1026 + }, + { + "key": "GridSchedulingPFUCurve", + "value": 1027 + }, + { + "key": "GridSchedulingDryContact", + "value": 1028 + }, + { + "key": "GridSchedulingQPCurve", + "value": 1029 + }, + { + "key": "SpotCheckReady", + "value": 1280 + }, + { + "key": "SpotChecking", + "value": 1281 + }, + { + "key": "Inspecting", + "value": 1536 + }, + { + "key": "AfciSelfCheck", + "value": 1792 + }, + { + "key": "IVScanning", + "value": 2048 + }, + { + "key": "DCInputDetection", + "value": 2304 + }, + { + "key": "RunningOffGridCharging", + "value": 2560 + }, + { + "key": "StandbyNoIrradiation", + "value": 40960 + } + ] + }, + { + "name": "BatteryDeviceStatus", + "values": [ + { + "key": "Offline", + "value": 0 + }, + { + "key": "Standby", + "value": 1 + }, + { + "key": "Running", + "value": 1 + }, + { + "key": "Fault", + "value": 1 + }, + { + "key": "SleepMode", + "value": 1 + } + ] + } + ], + "blocks": [ + ], + "registers": [ + { + "id": "inverterActivePower", + "address": 32080, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Inverter active power", + "unit": "kW", + "staticScaleFactor": -3, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "inverterDeviceStatus", + "address": 32089, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Inverter device status", + "enum": "InverterDeviceStatus", + "defaultValue": "InverterDeviceStatusStandbyInitializing", + "access": "RO" + }, + { + "id": "inverterEnergyProduced", + "address": 32106, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Inverter energy produced", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "powerMeterActivePower", + "address": 37113, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Power meter active power", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "lunaBattery1Status", + "address": 37000, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Luna 2000 Battery 1 status", + "enum": "BatteryDeviceStatus", + "defaultValue": "BatteryDeviceStatusOffline", + "access": "RO" + }, + { + "id": "lunaBattery1Power", + "address": 37001, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Luna 2000 Battery 1 power", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "lunaBattery1Soc", + "address": 37004, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Luna 2000 Battery 1 state of charge", + "staticScaleFactor": -1, + "unit": "%", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "lunaBattery2Status", + "address": 37741, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Luna 2000 Battery 2 status", + "enum": "BatteryDeviceStatus", + "defaultValue": "BatteryDeviceStatusOffline", + "access": "RO" + }, + { + "id": "lunaBattery2Power", + "address": 37743, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Luna 2000 Battery 2 power", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "lunaBattery2Soc", + "address": 37738, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "readSchedule": "update", + "description": "Luna 2000 Battery 2 state of charge", + "staticScaleFactor": -1, + "unit": "%", + "defaultValue": "0", + "access": "RO" + } + ] +} + diff --git a/huawei/huawei-registers.json b/huawei/huawei-registers.json index 25629a4..3ae47e3 100644 --- a/huawei/huawei-registers.json +++ b/huawei/huawei-registers.json @@ -1,6 +1,6 @@ { "className": "Huawei", - "protocol": "TCP", + "protocol": "RTU", "endianness": "BigEndian", "errorLimitUntilNotReachable": 15, "checkReachableRegister": "inverterActivePower", @@ -149,6 +149,135 @@ } ], "blocks": [ + { + "id": "identifyer", + "readSchedule": "init", + "registers": [ + { + "id": "model", + "address": 30000, + "size": 15, + "type": "string", + "registerType": "holdingRegister", + "description": "Model", + "access": "RO" + }, + { + "id": "serialNumber", + "address": 30015, + "size": 10, + "type": "string", + "registerType": "holdingRegister", + "description": "Serial number", + "access": "RO" + }, + { + "id": "productNumber", + "address": 30025, + "size": 10, + "type": "string", + "registerType": "holdingRegister", + "description": "Product number", + "access": "RO" + } + ] + }, + { + "id": "setup", + "readSchedule": "init", + "registers": [ + { + "id": "modelId", + "address": 30070, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Model ID", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "numberOfPvStrings", + "address": 30071, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Number of PV strings", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "numberOfMppTracks", + "address": 30072, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Number of MPP tracks", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "ratedPower", + "address": 30073, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Rated power (Pn)", + "unit": "kW", + "staticScaleFactor": -3, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "maxActivePower", + "address": 30075, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Maximum active power (P max)", + "unit": "kW", + "staticScaleFactor": -3, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "maxApparentPower", + "address": 30077, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Maximum apparant power (S max)", + "unit": "kVA", + "staticScaleFactor": -3, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "maxReactivePowerToGrid", + "address": 30079, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "description": "Maximum reactive power (Q max) - fed to grid", + "unit": "kVar", + "staticScaleFactor": -3, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "maxReactivePowerFromGrid", + "address": 30081, + "size": 2, + "type": "int32", + "registerType": "holdingRegister", + "description": "Maximum reactive power (Q max) - absorbed from grid", + "unit": "kVar", + "staticScaleFactor": -3, + "defaultValue": "0", + "access": "RO" + } + ] + } ], "registers": [ { diff --git a/huawei/huawei.pro b/huawei/huawei.pro index d5d9324..fbc8b0d 100644 --- a/huawei/huawei.pro +++ b/huawei/huawei.pro @@ -1,14 +1,16 @@ include(../plugins.pri) # Generate modbus connection -MODBUS_CONNECTIONS += huawei-registers.json #MODBUS_TOOLS_CONFIG += VERBOSE +MODBUS_CONNECTIONS += huawei-fusion-solar-registers.json huawei-registers.json include(../modbus.pri) HEADERS += \ huaweifusionsolar.h \ + huaweifusionsolardiscovery.h \ integrationpluginhuawei.h SOURCES += \ huaweifusionsolar.cpp \ + huaweifusionsolardiscovery.cpp \ integrationpluginhuawei.cpp diff --git a/huawei/huaweifusionsolar.cpp b/huawei/huaweifusionsolar.cpp index 9298378..a57c960 100644 --- a/huawei/huaweifusionsolar.cpp +++ b/huawei/huaweifusionsolar.cpp @@ -1,4 +1,4 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io @@ -35,15 +35,57 @@ NYMEA_LOGGING_CATEGORY(dcHuaweiFusionSolar, "HuaweiFusionSolar") HuaweiFusionSolar::HuaweiFusionSolar(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent) : - HuaweiModbusTcpConnection(hostAddress, port, slaveId, parent) + HuaweiFusionModbusTcpConnection(hostAddress, port, slaveId, parent) { - + connect(this, &HuaweiFusionModbusTcpConnection::connectionStateChanged, this, [=](bool connected){ + if (!connected) { + m_registersQueue.clear(); + } + }); } bool HuaweiFusionSolar::initialize() { - // No init registers defined. Nothing to be done and we are finished. - emit initializationFinished(true); + if (!reachable()) { + qCWarning(dcHuaweiFusionSolar()) << "Tried to initialize but the device is not to be reachable."; + return false; + } + + if (m_initReply) { + qCWarning(dcHuaweiFusionSolar()) << "Tried to initialize but the init process is already running."; + return false; + } + + qCDebug(dcHuaweiFusionSolar()) << "Initialize connection by reading \"Inverter active power\" register:" << 32080 << "size:" << 2; + m_initReply = readInverterActivePower(); + if (!m_initReply) { + qCWarning(dcHuaweiFusionSolar()) << "Error occurred while initializing connection and read \"Inverter active power\" register from" << hostAddress().toString() << errorString(); + return false; + } + + if (m_initReply->isFinished()) { + m_initReply->deleteLater(); // Broadcast reply returns immediatly + m_initReply = nullptr; + return false; + } + + connect(m_initReply, &QModbusReply::finished, this, [this](){ + if (m_initReply->error() == QModbusDevice::NoError) { + qCDebug(dcHuaweiFusionSolar()) << "Initialization finished of HuaweiFusionSolar" << hostAddress().toString() << "finished successfully"; + emit initializationFinished(true); + } else { + qCWarning(dcHuaweiFusionSolar()) << "Initialization finished of HuaweiFusionSolar" << hostAddress().toString() << "failed."; + emit initializationFinished(false); + } + + m_initReply->deleteLater(); + m_initReply = nullptr; + }); + + connect(m_initReply, &QModbusReply::errorOccurred, this, [this] (QModbusDevice::Error error){ + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while initializing connection and read \"Inverter active power\" registers from" << hostAddress().toString() << error << m_initReply->errorString(); + }); + return true; } @@ -54,19 +96,19 @@ bool HuaweiFusionSolar::update() return true; // Add the requests - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterInverterActivePower); - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterInverterDeviceStatus); - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterInverterEnergyProduced); - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterLunaBattery1Status); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterInverterActivePower); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterInverterDeviceStatus); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterInverterEnergyProduced); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterLunaBattery1Status); if (m_battery1Available) { - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterLunaBattery1Power); - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterLunaBattery1Soc); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterLunaBattery1Power); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterLunaBattery1Soc); } - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterPowerMeterActivePower); - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterLunaBattery2Status); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterPowerMeterActivePower); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterLunaBattery2Status); if (m_battery2Available) { - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterLunaBattery2Power); - m_registersQueue.enqueue(HuaweiModbusTcpConnection::RegisterLunaBattery2Soc); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterLunaBattery2Power); + m_registersQueue.enqueue(HuaweiFusionModbusTcpConnection::RegisterLunaBattery2Soc); } // Note: since huawei can only process one request at the time, we need to queue the requests @@ -89,316 +131,399 @@ void HuaweiFusionSolar::readNextRegister() m_currentRegisterRequest = m_registersQueue.dequeue(); switch (m_currentRegisterRequest) { - case HuaweiModbusTcpConnection::RegisterInverterActivePower: { + case HuaweiFusionModbusTcpConnection::RegisterInverterActivePower: { + // Update registers from Inverter active power qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Inverter active power\" register:" << 32080 << "size:" << 2; QModbusReply *reply = readInverterActivePower(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Inverter active power\" register" << 32080 << "size:" << 2 << values; - processInverterActivePowerRegisterValues(values); - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter active power\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Inverter active power\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Inverter active power\" register" << 32080 << "size:" << 2 << unit.values(); + processInverterActivePowerRegisterValues(unit.values()); + } + + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter active power\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter active power\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); + break; } - case HuaweiModbusTcpConnection::RegisterInverterDeviceStatus: { + case HuaweiFusionModbusTcpConnection::RegisterInverterDeviceStatus: { // Update registers from Inverter device status qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Inverter device status\" register:" << 32089 << "size:" << 1; QModbusReply *reply = readInverterDeviceStatus(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Inverter device status\" register" << 32089 << "size:" << 1 << values; - processInverterDeviceStatusRegisterValues(values); - qCDebug(dcHuaweiFusionSolar()) << "Inverter status" << inverterDeviceStatus(); - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter device status\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Inverter device status\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Inverter device status\" register" << 32089 << "size:" << 1 << unit.values(); + processInverterDeviceStatusRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter device status\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter device status\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); + break; } - case HuaweiModbusTcpConnection::RegisterInverterEnergyProduced: { + case HuaweiFusionModbusTcpConnection::RegisterInverterEnergyProduced: { // Update registers from Inverter energy produced qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Inverter energy produced\" register:" << 32106 << "size:" << 2; QModbusReply *reply = readInverterEnergyProduced(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Inverter energy produced\" register" << 32106 << "size:" << 2 << values; - processInverterEnergyProducedRegisterValues(values); - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter energy produced\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Inverter energy produced\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } - break; - } - case HuaweiModbusTcpConnection::RegisterLunaBattery1Status: { - // Update registers from Luna 2000 Battery 1 status - qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 1 status\" register:" << 37000 << "size:" << 1; - QModbusReply *reply = readLunaBattery1Status(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 1 status\" register" << 37000 << "size:" << 1 << values; - processLunaBattery1StatusRegisterValues(values); - qCDebug(dcHuaweiFusionSolar()) << "Battery 1 status" << m_lunaBattery1Status; - if (m_lunaBattery1Status == BatteryDeviceStatusOffline) { - m_battery1Available = false; - } else { - m_battery1Available = true; - } - } - finishRequest(); - }); - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 status\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { - qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 1 status\" registers from" << hostAddress().toString() << errorString(); + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly finishRequest(); + return; } - break; - } - case HuaweiModbusTcpConnection::RegisterLunaBattery1Power: { - // Update registers from Luna 2000 Battery 1 power - qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 1 power\" register:" << 37001 << "size:" << 2; - QModbusReply *reply = readLunaBattery1Power(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 1 power\" register" << 37001 << "size:" << 2 << values; - processLunaBattery1PowerRegisterValues(values); - } - finishRequest(); - }); - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 power\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Inverter energy produced\" register" << 32106 << "size:" << 2 << unit.values(); + processInverterEnergyProducedRegisterValues(unit.values()); } - } else { - qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 1 power\" registers from" << hostAddress().toString() << errorString(); finishRequest(); - } - break; - } - case HuaweiModbusTcpConnection::RegisterLunaBattery1Soc: { - // Update registers from Luna 2000 Battery 1 state of charge - qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 1 state of charge\" register:" << 37004 << "size:" << 1; - QModbusReply *reply = readLunaBattery1Soc(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 1 state of charge\" register" << 37004 << "size:" << 1 << values; - processLunaBattery1SocRegisterValues(values); - } - finishRequest(); - }); + }); - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 state of charge\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter energy produced\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Inverter energy produced\" registers from" << hostAddress().toString() << error << reply->errorString(); } - } else { - qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 1 state of charge\" registers from" << hostAddress().toString() << errorString(); - finishRequest(); - } + }); break; } - case HuaweiModbusTcpConnection::RegisterPowerMeterActivePower: { + case HuaweiFusionModbusTcpConnection::RegisterPowerMeterActivePower: { // Update registers from Power meter active power qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Power meter active power\" register:" << 37113 << "size:" << 2; QModbusReply *reply = readPowerMeterActivePower(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Power meter active power\" register" << 37113 << "size:" << 2 << values; - processPowerMeterActivePowerRegisterValues(values); - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Power meter active power\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Power meter active power\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Power meter active power\" register" << 37113 << "size:" << 2 << unit.values(); + processPowerMeterActivePowerRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Power meter active power\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Power meter active power\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); + break; } - case HuaweiModbusTcpConnection::RegisterLunaBattery2Status: { + case HuaweiFusionModbusTcpConnection::RegisterLunaBattery1Status: { + // Update registers from Luna 2000 Battery 1 status + qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 1 status\" register:" << 37000 << "size:" << 1; + QModbusReply *reply = readLunaBattery1Status(); + if (!reply) { + qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 1 status\" registers from" << hostAddress().toString() << errorString(); + finishRequest(); + return; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 1 status\" register" << 37000 << "size:" << 1 << unit.values(); + processLunaBattery1StatusRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 status\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 status\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); + break; + } + case HuaweiFusionModbusTcpConnection::RegisterLunaBattery1Power: { + // Update registers from Luna 2000 Battery 1 power + qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 1 power\" register:" << 37001 << "size:" << 2; + QModbusReply *reply = readLunaBattery1Power(); + if (!reply) { + qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 1 power\" registers from" << hostAddress().toString() << errorString(); + finishRequest(); + return; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 1 power\" register" << 37001 << "size:" << 2 << unit.values(); + processLunaBattery1PowerRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 power\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 power\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); + break; + } + case HuaweiFusionModbusTcpConnection::RegisterLunaBattery1Soc: { + // Update registers from Luna 2000 Battery 1 state of charge + qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 1 state of charge\" register:" << 37004 << "size:" << 1; + QModbusReply *reply = readLunaBattery1Soc(); + if (!reply) { + qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 1 state of charge\" registers from" << hostAddress().toString() << errorString(); + finishRequest(); + return; + } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 1 state of charge\" register" << 37004 << "size:" << 1 << unit.values(); + processLunaBattery1SocRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 state of charge\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 1 state of charge\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); + break; + } + + case HuaweiFusionModbusTcpConnection::RegisterLunaBattery2Status: { // Update registers from Luna 2000 Battery 2 status qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 2 status\" register:" << 37741 << "size:" << 1; QModbusReply *reply = readLunaBattery2Status(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 2 status\" register" << 37741 << "size:" << 1 << values; - processLunaBattery2StatusRegisterValues(values); - qCDebug(dcHuaweiFusionSolar()) << "Battery 2 status" << m_lunaBattery2Status; - if (m_lunaBattery2Status == BatteryDeviceStatusOffline) { - m_battery2Available = false; - } else { - m_battery2Available = true; - } - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 status\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 2 status\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 2 status\" register" << 37741 << "size:" << 1 << unit.values(); + processLunaBattery2StatusRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 status\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 status\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); break; } - case HuaweiModbusTcpConnection::RegisterLunaBattery2Power: { + case HuaweiFusionModbusTcpConnection::RegisterLunaBattery2Power: { // Update registers from Luna 2000 Battery 2 power qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 2 power\" register:" << 37743 << "size:" << 2; QModbusReply *reply = readLunaBattery2Power(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 2 power\" register" << 37743 << "size:" << 2 << values; - processLunaBattery2PowerRegisterValues(values); - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 power\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 2 power\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 2 power\" register" << 37743 << "size:" << 2 << unit.values(); + processLunaBattery2PowerRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 power\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 power\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); break; } - case HuaweiModbusTcpConnection::RegisterLunaBattery2Soc: { + case HuaweiFusionModbusTcpConnection::RegisterLunaBattery2Soc: { // Update registers from Luna 2000 Battery 2 state of charge qCDebug(dcHuaweiFusionSolar()) << "--> Read \"Luna 2000 Battery 2 state of charge\" register:" << 37738 << "size:" << 1; QModbusReply *reply = readLunaBattery2Soc(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); - connect(reply, &QModbusReply::finished, this, [this, reply](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector values = unit.values(); - qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 2 state of charge\" register" << 37738 << "size:" << 1 << values; - processLunaBattery2SocRegisterValues(values); - } - finishRequest(); - }); - - connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ - qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 state of charge\" registers from" << hostAddress().toString() << error << reply->errorString(); - }); - } else { - reply->deleteLater(); // Broadcast reply returns immediatly - finishRequest(); - } - } else { + if (!reply) { qCWarning(dcHuaweiFusionSolar()) << "Error occurred while reading \"Luna 2000 Battery 2 state of charge\" registers from" << hostAddress().toString() << errorString(); finishRequest(); + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + finishRequest(); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, this, [this, reply](){ + handleModbusError(reply->error()); + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + qCDebug(dcHuaweiFusionSolar()) << "<-- Response from \"Luna 2000 Battery 2 state of charge\" register" << 37738 << "size:" << 1 << unit.values(); + processLunaBattery2SocRegisterValues(unit.values()); + } + finishRequest(); + }); + + connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){ + if (reply->error() == QModbusDevice::ProtocolError) { + QModbusResponse response = reply->rawResult(); + if (response.isException()) { + qCDebug(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 state of charge\" registers from" << hostAddress().toString() << exceptionToString(response.exceptionCode()); + } + } else { + qCWarning(dcHuaweiFusionSolar()) << "Modbus reply error occurred while updating \"Luna 2000 Battery 2 state of charge\" registers from" << hostAddress().toString() << error << reply->errorString(); + } + }); break; } } @@ -409,3 +534,45 @@ void HuaweiFusionSolar::finishRequest() m_currentRegisterRequest = -1; QTimer::singleShot(1000, this, &HuaweiFusionSolar::readNextRegister); } + +QString HuaweiFusionSolar::exceptionToString(QModbusPdu::ExceptionCode exception) +{ + QString exceptionString; + switch (exception) { + case QModbusPdu::IllegalFunction: + exceptionString = "Illegal function"; + break; + case QModbusPdu::IllegalDataAddress: + exceptionString = "Illegal data address"; + break; + case QModbusPdu::IllegalDataValue: + exceptionString = "Illegal data value"; + break; + case QModbusPdu::ServerDeviceFailure: + exceptionString = "Server device failure"; + break; + case QModbusPdu::Acknowledge: + exceptionString = "Acknowledge"; + break; + case QModbusPdu::ServerDeviceBusy: + exceptionString = "Server device busy"; + break; + case QModbusPdu::NegativeAcknowledge: + exceptionString = "Negative acknowledge"; + break; + case QModbusPdu::MemoryParityError: + exceptionString = "Memory parity error"; + break; + case QModbusPdu::GatewayPathUnavailable: + exceptionString = "Gateway path unavailable"; + break; + case QModbusPdu::GatewayTargetDeviceFailedToRespond: + exceptionString = "Gateway target device failed to respond"; + break; + case QModbusPdu::ExtendedException: + exceptionString = "Extended exception"; + break; + } + + return exceptionString; +} diff --git a/huawei/huaweifusionsolar.h b/huawei/huaweifusionsolar.h index 2f9de41..7c56ecf 100644 --- a/huawei/huaweifusionsolar.h +++ b/huawei/huaweifusionsolar.h @@ -34,20 +34,21 @@ #include #include -#include "huaweimodbustcpconnection.h" +#include "huaweifusionmodbustcpconnection.h" -class HuaweiFusionSolar : public HuaweiModbusTcpConnection +class HuaweiFusionSolar : public HuaweiFusionModbusTcpConnection { Q_OBJECT public: explicit HuaweiFusionSolar(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr); + ~HuaweiFusionSolar() = default; - virtual bool initialize() override; + bool initialize() override; virtual bool update() override; - private: - QQueue m_registersQueue; + QQueue m_registersQueue; + QModbusReply *m_initReply = nullptr; int m_currentRegisterRequest = -1; void finishRequest(); @@ -55,6 +56,8 @@ private: bool m_battery1Available = true; bool m_battery2Available = true; + QString exceptionToString(QModbusPdu::ExceptionCode exception); + private slots: void readNextRegister(); diff --git a/huawei/huaweifusionsolardiscovery.cpp b/huawei/huaweifusionsolardiscovery.cpp new file mode 100644 index 0000000..10103d8 --- /dev/null +++ b/huawei/huaweifusionsolardiscovery.cpp @@ -0,0 +1,166 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 "huaweifusionsolardiscovery.h" +#include "extern-plugininfo.h" + +HuaweiFusionSolarDiscovery::HuaweiFusionSolarDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent) : + QObject(parent), + m_networkDeviceDiscovery(networkDeviceDiscovery), + m_port(port), + m_modbusAddress(modbusAddress) +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + qCDebug(dcHuawei()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); +} + + +void HuaweiFusionSolarDiscovery::startDiscovery() +{ + qCInfo(dcHuawei()) << "Discovery: Start searching for Huawei FusionSolar SmartDongle in the network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + // Check any already discovered infos.. + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + checkNetworkDevice(networkDeviceInfo); + } + + // Imedialty check any new device gets discovered + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &HuaweiFusionSolarDiscovery::checkNetworkDevice); + + // Check what might be left on finished + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcHuawei()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + qCDebug(dcHuawei()) << "Discovery: Network discovery finished. Start finishing discovery..."; + // Send a report request to nework device info not sent already... + foreach (const NetworkDeviceInfo &networkDeviceInfo, m_networkDeviceInfos) { + if (!m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) { + checkNetworkDevice(networkDeviceInfo); + } + } + + m_gracePeriodTimer.start(); + }); +} + +NetworkDeviceInfos HuaweiFusionSolarDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void HuaweiFusionSolarDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) + return; + + // The dongle must have a huawei registered mac address + if (!networkDeviceInfo.macAddressManufacturer().toLower().contains("huawei")) + return; + + HuaweiFusionSolar *connection = new HuaweiFusionSolar(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + m_connections.append(connection); + m_verifiedNetworkDeviceInfos.append(networkDeviceInfo); + + connect(connection, &HuaweiFusionSolar::reachableChanged, this, [=](bool reachable){ + if (!reachable) { + // Disconnected ... done with this connection + cleanupConnection(connection); + return; + } + + // Modbus TCP connected...ok, let's try to initialize it! + connect(connection, &HuaweiFusionSolar::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcHuawei()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + return; + } + + m_discoveryResults.append(networkDeviceInfo); + + qCDebug(dcHuawei()) << "Discovery: --> Found" << networkDeviceInfo; + + // Done with this connection + cleanupConnection(connection); + }); + + if (!connection->initialize()) { + qCDebug(dcHuawei()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + + // Initializing... + }); + + // If we get any error...skip this host... + connect(connection, &HuaweiFusionSolar::connectionErrorOccurred, this, [=](QModbusDevice::Error error){ + if (error != QModbusDevice::NoError) { + qCDebug(dcHuawei()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &HuaweiFusionSolar::checkReachabilityFailed, this, [=](){ + qCDebug(dcHuawei()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); + +} + +void HuaweiFusionSolarDiscovery::cleanupConnection(HuaweiFusionSolar *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void HuaweiFusionSolarDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (HuaweiFusionSolar *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcHuawei()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() + << "inverters in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); +} diff --git a/huawei/huaweifusionsolardiscovery.h b/huawei/huaweifusionsolardiscovery.h new file mode 100644 index 0000000..9cecdf2 --- /dev/null +++ b/huawei/huaweifusionsolardiscovery.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 HUAWEIFUSIONSOLARDISCOVERY_H +#define HUAWEIFUSIONSOLARDISCOVERY_H + +#include + +#include + +#include "huaweifusionsolar.h" + +class HuaweiFusionSolarDiscovery : public QObject +{ + Q_OBJECT +public: + explicit HuaweiFusionSolarDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 502, quint16 modbusAddress = 1, QObject *parent = nullptr); + + void startDiscovery(); + + NetworkDeviceInfos discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + quint16 m_port; + quint16 m_modbusAddress; + + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + NetworkDeviceInfos m_networkDeviceInfos; + NetworkDeviceInfos m_verifiedNetworkDeviceInfos; + + QList m_connections; + + NetworkDeviceInfos m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(HuaweiFusionSolar *connection); + + void finishDiscovery(); + +}; + +#endif // HUAWEIFUSIONSOLARDISCOVERY_H diff --git a/huawei/integrationpluginhuawei.cpp b/huawei/integrationpluginhuawei.cpp index 1c83ee2..928fdf0 100644 --- a/huawei/integrationpluginhuawei.cpp +++ b/huawei/integrationpluginhuawei.cpp @@ -28,10 +28,11 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#include "huaweifusionsolardiscovery.h" #include "integrationpluginhuawei.h" #include "plugininfo.h" -#include +#include #include IntegrationPluginHuawei::IntegrationPluginHuawei() @@ -41,59 +42,69 @@ IntegrationPluginHuawei::IntegrationPluginHuawei() void IntegrationPluginHuawei::discoverThings(ThingDiscoveryInfo *info) { - if (!hardwareManager()->networkDeviceDiscovery()->available()) { - qCWarning(dcHuawei()) << "The network discovery is not available on this platform."; - info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); - return; - } + if (info->thingClassId() == huaweiFusionSolarInverterThingClassId) { + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcHuawei()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } - NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + // Create a discovery with the info as parent for auto deleting the object once the discovery info is done + HuaweiFusionSolarDiscovery *discovery = new HuaweiFusionSolarDiscovery(hardwareManager()->networkDeviceDiscovery(), 502, 1, info); + connect(discovery, &HuaweiFusionSolarDiscovery::discoveryFinished, info, [=](){ + foreach (const NetworkDeviceInfo &networkDeviceInfo, discovery->discoveryResults()) { - qCDebug(dcHuawei()) << "Found" << networkDeviceInfo; + ThingDescriptor descriptor(huaweiFusionSolarInverterThingClassId, QT_TR_NOOP("Huawei Solar Inverter"), networkDeviceInfo.macAddress() + " - " + networkDeviceInfo.address().toString()); + qCDebug(dcHuawei()) << "Discovered:" << descriptor.title() << descriptor.description(); - // Filter for mac manufacturer - if (!networkDeviceInfo.macAddressManufacturer().contains("Huawei")) + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(huaweiFusionSolarInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcHuawei()) << "This inverter already exists in the system:" << networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(huaweiFusionSolarInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + // Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults... + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + + // Start the discovery process + discovery->startDiscovery(); + + } else if (info->thingClassId() == huaweiRtuInverterThingClassId) { + qCDebug(dcHuawei()) << "Discovering modbus RTU resources..."; + if (hardwareManager()->modbusRtuResource()->modbusRtuMasters().isEmpty()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No Modbus RTU interface available. Please set up a Modbus RTU interface first.")); + return; + } + + uint slaveAddress = info->params().paramValue(huaweiRtuInverterThingSlaveAddressParamTypeId).toUInt(); + if (slaveAddress > 254 || slaveAddress == 0) { + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The Modbus slave address must be a value between 1 and 254.")); + return; + } + + foreach (ModbusRtuMaster *modbusMaster, hardwareManager()->modbusRtuResource()->modbusRtuMasters()) { + qCDebug(dcHuawei()) << "Found RTU master resource" << modbusMaster << "connected" << modbusMaster->connected(); + if (!modbusMaster->connected()) continue; - QString title; - if (networkDeviceInfo.hostName().isEmpty()) { - title = "Huawei FusionSolar"; - } else { - title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; - } - - QString description; - if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { - description = networkDeviceInfo.macAddress(); - } else { - description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; - } - - ThingDescriptor descriptor(huaweiInverterThingClassId, title, description); + ThingDescriptor descriptor(info->thingClassId(), "Huawei Inverter", QString::number(slaveAddress) + " " + modbusMaster->serialPort()); ParamList params; - params << Param(huaweiInverterThingIpAddressParamTypeId, networkDeviceInfo.address().toString()); - params << Param(huaweiInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + params << Param(huaweiRtuInverterThingSlaveAddressParamTypeId, slaveAddress); + params << Param(huaweiRtuInverterThingModbusMasterUuidParamTypeId, modbusMaster->modbusUuid()); descriptor.setParams(params); - - // Check if we already have set up this device - Things existingThings = myThings().filterByParam(huaweiInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); - if (existingThings.count() == 1) { - qCDebug(dcHuawei()) << "This connection already exists in the system:" << networkDeviceInfo; - descriptor.setThingId(existingThings.first()->id()); - } - info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); - }); -} - -void IntegrationPluginHuawei::startMonitoringAutoThings() -{ - + } } void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) @@ -101,50 +112,111 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) Thing *thing = info->thing(); qCDebug(dcHuawei()) << "Setup" << thing << thing->params(); - if (thing->thingClassId() == huaweiInverterThingClassId) { - QHostAddress hostAddress = QHostAddress(thing->paramValue(huaweiInverterThingIpAddressParamTypeId).toString()); - if (hostAddress.isNull()) { - info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); + if (thing->thingClassId() == huaweiFusionSolarInverterThingClassId) { + + // Handle reconfigure + if (m_connections.contains(thing)) { + m_connections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + // Make sure we have a valid mac address, otherwise no monitor and not auto searching is possible + MacAddress macAddress = MacAddress(thing->paramValue(huaweiFusionSolarInverterThingMacAddressParamTypeId).toString()); + if (macAddress.isNull()) { + qCWarning(dcHuawei()) << "Failed to set up Fusion Solar because the MAC address is not valid:" << thing->paramValue(huaweiFusionSolarInverterThingMacAddressParamTypeId).toString() << macAddress.toString(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not vaild. Please reconfigure the device to fix this.")); return; } - uint port = thing->paramValue(huaweiInverterThingPortParamTypeId).toUInt(); - quint16 slaveId = thing->paramValue(huaweiInverterThingSlaveIdParamTypeId).toUInt(); + // Create a monitor so we always get the correct IP in the network and see if the device is reachable without polling on our own + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); + m_monitors.insert(thing, monitor); - HuaweiFusionSolar *connection = new HuaweiFusionSolar(hostAddress, port, slaveId, this); + // Continue with setup only if we know that the network device is reachable + if (monitor->reachable()) { + setupFusionSolar(info); + } else { + // otherwise wait until we reach the networkdevice before setting up the device + qCDebug(dcHuawei()) << "Network device" << thing->name() << "is not reachable yet. Continue with the setup once reachable."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcHuawei()) << "Network device" << thing->name() << "is now reachable. Continue with the setup..."; + setupFusionSolar(info); + } + }); + } - connect(connection, &HuaweiFusionSolar::connectionStateChanged, this, [this, thing, connection](bool status){ - qCDebug(dcHuawei()) << "Connected changed to" << status << "for" << thing; - if (status) { + return; + } + + if (thing->thingClassId() == huaweiRtuInverterThingClassId) { + + uint address = thing->paramValue(huaweiRtuInverterThingSlaveAddressParamTypeId).toUInt(); + if (address > 254 || address == 0) { + qCWarning(dcHuawei()) << "Setup failed, slave address is not valid" << address; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus address not valid. It must be a value between 1 and 254.")); + return; + } + + QUuid uuid = thing->paramValue(huaweiRtuInverterThingModbusMasterUuidParamTypeId).toUuid(); + if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(uuid)) { + qCWarning(dcHuawei()) << "Setup failed, hardware manager not available"; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus RTU resource is not available.")); + return; + } + + if (m_rtuConnections.contains(thing)) { + qCDebug(dcHuawei()) << "Already have a Huawei connection for this thing. Cleaning up old connection and initializing new one..."; + delete m_rtuConnections.take(thing); + } + + ModbusRtuMaster *rtuMaster = hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid); + HuaweiModbusRtuConnection *connection = new HuaweiModbusRtuConnection(rtuMaster, address, this); + + connect(connection, &HuaweiModbusRtuConnection::reachableChanged, this, [this, thing, connection](bool reachable){ + qCDebug(dcHuawei()) << thing->name() << "reachable changed" << reachable; + if (reachable) { // Connected true will be set after successfull init connection->initialize(); - thing->setStateValue(huaweiInverterConnectedStateTypeId, true); } else { - thing->setStateValue(huaweiInverterConnectedStateTypeId, false); - } - - foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { - childThing->setStateValue("connected", status); + thing->setStateValue("connected", false); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", false); + } } }); - connect(connection, &HuaweiFusionSolar::inverterActivePowerChanged, this, [thing](float inverterActivePower){ + connect(connection, &HuaweiModbusRtuConnection::initializationFinished, this, [this, thing, connection](bool success){ + if (success) { + thing->setStateValue("connected", true); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", true); + } + + connection->update(); + } + }); + + connect(connection, &HuaweiModbusRtuConnection::inverterActivePowerChanged, this, [thing](float inverterActivePower){ qCDebug(dcHuawei()) << "Inverter power changed" << inverterActivePower * -1000.0 << "W"; - thing->setStateValue(huaweiInverterCurrentPowerStateTypeId, inverterActivePower * -1000.0); + thing->setStateValue(huaweiRtuInverterCurrentPowerStateTypeId, inverterActivePower * -1000.0); }); - connect(connection, &HuaweiFusionSolar::inverterDeviceStatusChanged, this, [thing](HuaweiFusionSolar::InverterDeviceStatus inverterDeviceStatus){ + connect(connection, &HuaweiModbusRtuConnection::inverterDeviceStatusChanged, this, [thing](HuaweiModbusRtuConnection::InverterDeviceStatus inverterDeviceStatus){ qCDebug(dcHuawei()) << "Inverter device status changed" << inverterDeviceStatus; Q_UNUSED(thing) }); - connect(connection, &HuaweiFusionSolar::inverterEnergyProducedChanged, this, [thing](float inverterEnergyProduced){ + connect(connection, &HuaweiModbusRtuConnection::inverterEnergyProducedChanged, this, [thing](float inverterEnergyProduced){ qCDebug(dcHuawei()) << "Inverter total energy produced changed" << inverterEnergyProduced << "kWh"; - thing->setStateValue(huaweiInverterTotalEnergyProducedStateTypeId, inverterEnergyProduced); + thing->setStateValue(huaweiRtuInverterTotalEnergyProducedStateTypeId, inverterEnergyProduced); }); // Meter - connect(connection, &HuaweiFusionSolar::powerMeterActivePowerChanged, this, [this, thing](qint32 powerMeterActivePower){ + connect(connection, &HuaweiModbusRtuConnection::powerMeterActivePowerChanged, this, [this, thing](qint32 powerMeterActivePower){ Things meterThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiMeterThingClassId); if (!meterThings.isEmpty()) { qCDebug(dcHuawei()) << "Meter power changed" << powerMeterActivePower << "W"; @@ -154,9 +226,9 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) }); // Battery 1 - connect(connection, &HuaweiFusionSolar::lunaBattery1StatusChanged, this, [this, thing](HuaweiFusionSolar::BatteryDeviceStatus lunaBattery1Status){ + connect(connection, &HuaweiModbusRtuConnection::lunaBattery1StatusChanged, this, [this, thing](HuaweiModbusRtuConnection::BatteryDeviceStatus lunaBattery1Status){ qCDebug(dcHuawei()) << "Battery 1 status changed" << lunaBattery1Status; - if (lunaBattery1Status != HuaweiFusionSolar::BatteryDeviceStatusOffline) { + if (lunaBattery1Status != HuaweiModbusRtuConnection::BatteryDeviceStatusOffline) { // Check if w have to create the energy storage Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId); bool alreadySetUp = false; @@ -177,7 +249,7 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) } }); - connect(connection, &HuaweiFusionSolar::lunaBattery1PowerChanged, this, [this, thing](qint32 lunaBattery1Power){ + connect(connection, &HuaweiModbusRtuConnection::lunaBattery1PowerChanged, this, [this, thing](qint32 lunaBattery1Power){ qCDebug(dcHuawei()) << "Battery 1 power changed" << lunaBattery1Power << "W"; Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 1); if (!batteryThings.isEmpty()) { @@ -192,7 +264,7 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) } }); - connect(connection, &HuaweiFusionSolar::lunaBattery1SocChanged, this, [this, thing](float lunaBattery1Soc){ + connect(connection, &HuaweiModbusRtuConnection::lunaBattery1SocChanged, this, [this, thing](float lunaBattery1Soc){ qCDebug(dcHuawei()) << "Battery 1 SOC changed" << lunaBattery1Soc << "%"; Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 1); if (!batteryThings.isEmpty()) { @@ -202,9 +274,9 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) }); // Battery 2 - connect(connection, &HuaweiFusionSolar::lunaBattery2StatusChanged, this, [this, thing](HuaweiFusionSolar::BatteryDeviceStatus lunaBattery1Status){ + connect(connection, &HuaweiModbusRtuConnection::lunaBattery2StatusChanged, this, [this, thing](HuaweiModbusRtuConnection::BatteryDeviceStatus lunaBattery1Status){ qCDebug(dcHuawei()) << "Battery 2 status changed" << lunaBattery1Status; - if (lunaBattery1Status != HuaweiFusionSolar::BatteryDeviceStatusOffline) { + if (lunaBattery1Status != HuaweiModbusRtuConnection::BatteryDeviceStatusOffline) { // Check if w have to create the energy storage Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId); bool alreadySetUp = false; @@ -225,7 +297,7 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) } }); - connect(connection, &HuaweiFusionSolar::lunaBattery2PowerChanged, this, [this, thing](qint32 lunaBattery2Power){ + connect(connection, &HuaweiModbusRtuConnection::lunaBattery2PowerChanged, this, [this, thing](qint32 lunaBattery2Power){ qCDebug(dcHuawei()) << "Battery 2 power changed" << lunaBattery2Power << "W"; Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 2); if (!batteryThings.isEmpty()) { @@ -241,7 +313,7 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) } }); - connect(connection, &HuaweiFusionSolar::lunaBattery2SocChanged, this, [this, thing](float lunaBattery2Soc){ + connect(connection, &HuaweiModbusRtuConnection::lunaBattery2SocChanged, this, [this, thing](float lunaBattery2Soc){ qCDebug(dcHuawei()) << "Battery 2 SOC changed" << lunaBattery2Soc << "%"; Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 2); if (!batteryThings.isEmpty()) { @@ -250,11 +322,12 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) } }); - m_connections.insert(thing, connection); - connection->connectDevice(); + m_rtuConnections.insert(thing, connection); + connection->initialize(); // FIXME: make async and check if this is really a huawei info->finish(Thing::ThingErrorNoError); + return; } if (thing->thingClassId() == huaweiMeterThingClassId) { @@ -264,7 +337,7 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) if (parentThing) { thing->setStateValue("connected", parentThing->stateValue("connected").toBool()); } - + return; } if (thing->thingClassId() == huaweiBatteryThingClassId) { @@ -274,12 +347,13 @@ void IntegrationPluginHuawei::setupThing(ThingSetupInfo *info) if (parentThing) { thing->setStateValue("connected", parentThing->stateValue("connected").toBool()); } + return; } } void IntegrationPluginHuawei::postSetupThing(Thing *thing) { - if (thing->thingClassId() == huaweiInverterThingClassId) { + if (thing->thingClassId() == huaweiFusionSolarInverterThingClassId || thing->thingClassId() == huaweiRtuInverterThingClassId) { if (!m_pluginTimer) { qCDebug(dcHuawei()) << "Starting plugin timer..."; m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(5); @@ -289,6 +363,10 @@ void IntegrationPluginHuawei::postSetupThing(Thing *thing) connection->update(); } } + + foreach(HuaweiModbusRtuConnection *connection, m_rtuConnections) { + connection->update(); + } }); m_pluginTimer->start(); @@ -304,8 +382,16 @@ void IntegrationPluginHuawei::postSetupThing(Thing *thing) void IntegrationPluginHuawei::thingRemoved(Thing *thing) { - if (thing->thingClassId() == huaweiInverterThingClassId && m_connections.contains(thing)) { + if (thing->thingClassId() == huaweiFusionSolarInverterThingClassId && m_connections.contains(thing)) { m_connections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + if (thing->thingClassId() == huaweiRtuInverterThingClassId && m_rtuConnections.contains(thing)) { + m_rtuConnections.take(thing)->deleteLater(); } if (myThings().isEmpty() && m_pluginTimer) { @@ -314,9 +400,171 @@ void IntegrationPluginHuawei::thingRemoved(Thing *thing) } } -void IntegrationPluginHuawei::executeAction(ThingActionInfo *info) +void IntegrationPluginHuawei::setupFusionSolar(ThingSetupInfo *info) { + Thing *thing = info->thing(); + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + uint port = thing->paramValue(huaweiFusionSolarInverterThingPortParamTypeId).toUInt(); + quint16 slaveId = thing->paramValue(huaweiFusionSolarInverterThingSlaveIdParamTypeId).toUInt(); + + HuaweiFusionSolar *connection = new HuaweiFusionSolar(monitor->networkDeviceInfo().address(), port, slaveId, this); + + qCDebug(dcHuawei()) << "Finish setup huawei fusion solar dongle" << monitor->networkDeviceInfo().address().toString() << port << slaveId; + + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + if (!thing->setupComplete()) + return; + + qCDebug(dcHuawei()) << "Network device monitor for" << thing->name() << (reachable ? "is now reachable" : "is not reachable any more" ); + if (reachable) { + // Update address and refresh + thing->setStateValue("hostAddress", monitor->networkDeviceInfo().address().toString()); + connection->setHostAddress(monitor->networkDeviceInfo().address()); + connection->update(); + } + }); + + connect(connection, &HuaweiFusionSolar::reachableChanged, this, [this, thing, connection](bool reachable){ + qCDebug(dcHuawei()) << "Reachable changed to" << reachable << "for" << thing; + if (reachable) { + // Connected true will be set after successfull init + connection->initialize(); + thing->setStateValue("connected", true); + } else { + thing->setStateValue("connected", false); + } + + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", reachable); + } + }); + + connect(connection, &HuaweiFusionSolar::inverterActivePowerChanged, this, [thing](float inverterActivePower){ + qCDebug(dcHuawei()) << "Inverter power changed" << inverterActivePower * -1000.0 << "W"; + thing->setStateValue(huaweiFusionSolarInverterCurrentPowerStateTypeId, inverterActivePower * -1000.0); + }); + + connect(connection, &HuaweiFusionSolar::inverterDeviceStatusChanged, this, [thing](HuaweiFusionSolar::InverterDeviceStatus inverterDeviceStatus){ + qCDebug(dcHuawei()) << "Inverter device status changed" << inverterDeviceStatus; + Q_UNUSED(thing) + }); + + connect(connection, &HuaweiFusionSolar::inverterEnergyProducedChanged, this, [thing](float inverterEnergyProduced){ + qCDebug(dcHuawei()) << "Inverter total energy produced changed" << inverterEnergyProduced << "kWh"; + thing->setStateValue(huaweiFusionSolarInverterTotalEnergyProducedStateTypeId, inverterEnergyProduced); + }); + + // Meter + connect(connection, &HuaweiFusionSolar::powerMeterActivePowerChanged, this, [this, thing](qint32 powerMeterActivePower){ + Things meterThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiMeterThingClassId); + if (!meterThings.isEmpty()) { + qCDebug(dcHuawei()) << "Meter power changed" << powerMeterActivePower << "W"; + // Note: > 0 -> return, < 0 consume + meterThings.first()->setStateValue(huaweiMeterCurrentPowerStateTypeId, -powerMeterActivePower); + } + }); + + // Battery 1 + connect(connection, &HuaweiFusionSolar::lunaBattery1StatusChanged, this, [this, thing](HuaweiFusionSolar::BatteryDeviceStatus lunaBattery1Status){ + qCDebug(dcHuawei()) << "Battery 1 status changed" << lunaBattery1Status; + if (lunaBattery1Status != HuaweiFusionSolar::BatteryDeviceStatusOffline) { + // Check if w have to create the energy storage + Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId); + bool alreadySetUp = false; + foreach (Thing *batteryThing, batteryThings) { + if (batteryThing->paramValue(huaweiBatteryThingUnitParamTypeId).toUInt() == 1) { + alreadySetUp = true; + } + } + + if (!alreadySetUp) { + qCDebug(dcHuawei()) << "Set up huawei energy storage 1 for" << thing; + ThingDescriptor descriptor(huaweiBatteryThingClassId, "Luna 2000 Battery", QString(), thing->id()); + ParamList params; + params.append(Param(huaweiBatteryThingUnitParamTypeId, 1)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } + } + }); + + connect(connection, &HuaweiFusionSolar::lunaBattery1PowerChanged, this, [this, thing](qint32 lunaBattery1Power){ + qCDebug(dcHuawei()) << "Battery 1 power changed" << lunaBattery1Power << "W"; + Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 1); + if (!batteryThings.isEmpty()) { + batteryThings.first()->setStateValue(huaweiBatteryCurrentPowerStateTypeId, lunaBattery1Power); + if (lunaBattery1Power < 0) { + batteryThings.first()->setStateValue(huaweiBatteryChargingStateStateTypeId, "discharging"); + } else if (lunaBattery1Power > 0) { + batteryThings.first()->setStateValue(huaweiBatteryChargingStateStateTypeId, "charging"); + } else { + batteryThings.first()->setStateValue(huaweiBatteryChargingStateStateTypeId, "idle"); + } + } + }); + + connect(connection, &HuaweiFusionSolar::lunaBattery1SocChanged, this, [this, thing](float lunaBattery1Soc){ + qCDebug(dcHuawei()) << "Battery 1 SOC changed" << lunaBattery1Soc << "%"; + Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 1); + if (!batteryThings.isEmpty()) { + batteryThings.first()->setStateValue(huaweiBatteryBatteryLevelStateTypeId, lunaBattery1Soc); + batteryThings.first()->setStateValue(huaweiBatteryBatteryCriticalStateTypeId, lunaBattery1Soc < 10); + } + }); + + // Battery 2 + connect(connection, &HuaweiFusionSolar::lunaBattery2StatusChanged, this, [this, thing](HuaweiFusionSolar::BatteryDeviceStatus lunaBattery1Status){ + qCDebug(dcHuawei()) << "Battery 2 status changed" << lunaBattery1Status; + if (lunaBattery1Status != HuaweiFusionSolar::BatteryDeviceStatusOffline) { + // Check if w have to create the energy storage + Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId); + bool alreadySetUp = false; + foreach (Thing *batteryThing, batteryThings) { + if (batteryThing->paramValue(huaweiBatteryThingUnitParamTypeId).toUInt() == 2) { + alreadySetUp = true; + } + } + + if (!alreadySetUp) { + qCDebug(dcHuawei()) << "Set up huawei energy storage 2 for" << thing; + ThingDescriptor descriptor(huaweiBatteryThingClassId, "Luna 2000 Battery", QString(), thing->id()); + ParamList params; + params.append(Param(huaweiBatteryThingUnitParamTypeId, 2)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } + } + }); + + connect(connection, &HuaweiFusionSolar::lunaBattery2PowerChanged, this, [this, thing](qint32 lunaBattery2Power){ + qCDebug(dcHuawei()) << "Battery 2 power changed" << lunaBattery2Power << "W"; + Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 2); + if (!batteryThings.isEmpty()) { + batteryThings.first()->setStateValue(huaweiBatteryCurrentPowerStateTypeId, lunaBattery2Power); + + if (lunaBattery2Power < 0) { + batteryThings.first()->setStateValue(huaweiBatteryChargingStateStateTypeId, "discharging"); + } else if (lunaBattery2Power > 0) { + batteryThings.first()->setStateValue(huaweiBatteryChargingStateStateTypeId, "charging"); + } else { + batteryThings.first()->setStateValue(huaweiBatteryChargingStateStateTypeId, "idle"); + } + } + }); + + connect(connection, &HuaweiFusionSolar::lunaBattery2SocChanged, this, [this, thing](float lunaBattery2Soc){ + qCDebug(dcHuawei()) << "Battery 2 SOC changed" << lunaBattery2Soc << "%"; + Things batteryThings = myThings().filterByParentId(thing->id()).filterByThingClassId(huaweiBatteryThingClassId).filterByParam(huaweiBatteryThingUnitParamTypeId, 2); + if (!batteryThings.isEmpty()) { + batteryThings.first()->setStateValue(huaweiBatteryBatteryLevelStateTypeId, lunaBattery2Soc); + batteryThings.first()->setStateValue(huaweiBatteryBatteryCriticalStateTypeId, lunaBattery2Soc < 10); + } + }); + + m_connections.insert(thing, connection); + connection->connectDevice(); + + // FIXME: make async and check if this is really a huawei info->finish(Thing::ThingErrorNoError); } - diff --git a/huawei/integrationpluginhuawei.h b/huawei/integrationpluginhuawei.h index 8e5afe6..fd45cbe 100644 --- a/huawei/integrationpluginhuawei.h +++ b/huawei/integrationpluginhuawei.h @@ -33,8 +33,11 @@ #include #include +#include +#include "extern-plugininfo.h" #include "huaweifusionsolar.h" +#include "huaweimodbusrtuconnection.h" class IntegrationPluginHuawei: public IntegrationPlugin { @@ -47,16 +50,18 @@ public: explicit IntegrationPluginHuawei(); void discoverThings(ThingDiscoveryInfo *info) override; - void startMonitoringAutoThings() override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; - void executeAction(ThingActionInfo *info) override; private: PluginTimer *m_pluginTimer = nullptr; + QHash m_monitors; QHash m_connections; + QHash m_rtuConnections; + + void setupFusionSolar(ThingSetupInfo *info); }; #endif // INTEGRATIONPLUGINHUAWEI_H diff --git a/huawei/integrationpluginhuawei.json b/huawei/integrationpluginhuawei.json index 0aaeedd..f54d5bc 100644 --- a/huawei/integrationpluginhuawei.json +++ b/huawei/integrationpluginhuawei.json @@ -9,21 +9,13 @@ "id": "f654c99d-a286-4abb-b33e-1a71843d8da0", "thingClasses": [ { - "name": "huaweiInverter", + "name": "huaweiFusionSolarInverter", "displayName": "Huawei FusionSolar Inverter", "id": "87e75ee0-d544-457b-add3-bd4e58160fcd", "createMethods": ["discovery", "user"], "interfaces": ["solarinverter", "connectable"], "providedInterfaces": [ "solarinverter", "energymeter", "energystorage"], "paramTypes": [ - { - "id": "d93371db-0954-4dcd-a1a5-6881b78cb0ea", - "name": "ipAddress", - "displayName": "IP address", - "type": "QString", - "inputType": "IPv4Address", - "defaultValue": "127.0.0.1" - }, { "id": "93517bff-1928-4c4a-8207-5fe596c86eba", "name":"macAddress", @@ -79,6 +71,71 @@ ], "actionTypes": [ ] }, + { + "name": "huaweiRtuInverter", + "displayName": "Huawei Inverter", + "id": "77558007-5076-4ca6-bd46-169f215c3e29", + "createMethods": ["discovery"], + "interfaces": ["solarinverter", "connectable"], + "providedInterfaces": [ "solarinverter", "energymeter", "energystorage"], + "discoveryParamTypes": [ + { + "id": "93a4d3a8-c7d0-470b-b6e3-d8fc43b8e8d0", + "name": "slaveAddress", + "displayName": "Slave address", + "type": "int", + "defaultValue": 1 + } + ], + "paramTypes": [ + { + "id": "5c7b28b1-3691-452e-8f49-d80ae7bcbe2c", + "name": "modbusMasterUuid", + "displayName": "Modbus RTU master", + "type": "QUuid", + "defaultValue": "", + "readOnly": true + }, + { + "id": "de06f027-7940-4c45-9c96-30930ac2796d", + "name": "slaveAddress", + "displayName": "Modbus slave address", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "191ffa22-de6f-4325-8698-56b817f78df5", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "6064d90e-1b6b-40fd-9da0-6ebc713efb7d", + "name": "currentPower", + "displayName": "Active power", + "displayNameEvent": "Active power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "49b92919-301c-4ff7-ae63-0c1a2184e3f4", + "name": "totalEnergyProduced", + "displayName": "AC energy", + "displayNameEvent": "AC energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + } + ], + "actionTypes": [ ] + }, { "name": "huaweiMeter", "displayName": "Huawei Meter",