From be428a57096327bbfe5b3bddc49d0eb0c8a07c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 4 Dec 2024 16:20:43 +0100 Subject: [PATCH 1/2] Wattsonic: Add modbus slave ID as discovery parameter --- wattsonic/integrationpluginwattsonic.cpp | 3 ++- wattsonic/integrationpluginwattsonic.json | 11 ++++++++- wattsonic/wattsonicdiscovery.cpp | 27 ++++++++--------------- wattsonic/wattsonicdiscovery.h | 4 ++-- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/wattsonic/integrationpluginwattsonic.cpp b/wattsonic/integrationpluginwattsonic.cpp index ba6a39a..ae7f03f 100644 --- a/wattsonic/integrationpluginwattsonic.cpp +++ b/wattsonic/integrationpluginwattsonic.cpp @@ -76,7 +76,8 @@ void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info) info->finish(Thing::ThingErrorNoError); }); - discovery->startDiscovery(); + + discovery->startDiscovery(info->params().paramValue(inverterDiscoverySlaveAddressParamTypeId).toUInt()); } } diff --git a/wattsonic/integrationpluginwattsonic.json b/wattsonic/integrationpluginwattsonic.json index ee3a1d5..bf8f8e3 100644 --- a/wattsonic/integrationpluginwattsonic.json +++ b/wattsonic/integrationpluginwattsonic.json @@ -14,13 +14,22 @@ "id": "688bef8d-2ba8-4eb3-b30e-16193eba02fb", "createMethods": ["discovery", "user"], "interfaces": ["solarinverter", "connectable"], + "discoveryParamTypes": [ + { + "id": "a4e58882-d26a-43ee-813f-11c6ca74ee6d", + "name": "slaveAddress", + "displayName": "Modbus slave address", + "type": "uint", + "defaultValue": 247 + } + ], "paramTypes": [ { "id": "55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2", "name": "slaveAddress", "displayName": "Modbus slave address", "type": "uint", - "defaultValue": 1 + "defaultValue": 247 }, { "id": "4f1238b5-07e0-4516-b84a-71670141ef81", diff --git a/wattsonic/wattsonicdiscovery.cpp b/wattsonic/wattsonicdiscovery.cpp index 5e599d1..c71f0c0 100644 --- a/wattsonic/wattsonicdiscovery.cpp +++ b/wattsonic/wattsonicdiscovery.cpp @@ -33,8 +33,6 @@ #include -QList slaveIdCandidates = {247}; - WattsonicDiscovery::WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent): QObject{parent}, m_modbusRtuResource(modbusRtuResource) @@ -42,7 +40,7 @@ WattsonicDiscovery::WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResou } -void WattsonicDiscovery::startDiscovery() +void WattsonicDiscovery::startDiscovery(quint16 slaveId) { qCInfo(dcWattsonic()) << "Discovery: Searching for Wattsonic device on modbus RTU..."; @@ -54,16 +52,16 @@ void WattsonicDiscovery::startDiscovery() } if (candidateMasters.isEmpty()) { - qCWarning(dcWattsonic()) << "No usable modbus RTU master found."; + qCWarning(dcWattsonic()) << "Discovery: No usable modbus RTU master found."; emit discoveryFinished(false); return; } foreach (ModbusRtuMaster *master, candidateMasters) { if (master->connected()) { - tryConnect(master, 0); + tryConnect(master, slaveId); } else { - qCWarning(dcWattsonic()) << "Modbus RTU master" << master->modbusUuid().toString() << "is not connected."; + qCWarning(dcWattsonic()) << "Discovery: Modbus RTU master" << master->modbusUuid().toString() << "is not connected."; } } } @@ -73,28 +71,21 @@ QList WattsonicDiscovery::discoveryResults() const return m_discoveryResults; } -void WattsonicDiscovery::tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex) +void WattsonicDiscovery::tryConnect(ModbusRtuMaster *master, quint16 slaveId) { - quint8 slaveId = slaveIdCandidates.at(slaveIdIndex); - qCDebug(dcWattsonic()) << "Scanning modbus RTU master" << master->modbusUuid() << "Slave ID:" << slaveId; + qCDebug(dcWattsonic()) << "Discovery: Scanning modbus RTU master" << master->modbusUuid() << "Slave ID:" << slaveId; ModbusRtuReply *reply = master->readHoldingRegister(slaveId, 10000, 8); - connect(reply, &ModbusRtuReply::finished, this, [=](){ + connect(reply, &ModbusRtuReply::finished, this, [this, master, reply, slaveId](){ if (reply->error() == ModbusRtuReply::NoError) { - QString serialNumber = ModbusDataUtils::convertToString(reply->result(), ModbusDataUtils::ByteOrderBigEndian); - qCDebug(dcWattsonic()) << "Test reply finished!" << reply->error() << reply->result() << serialNumber; + qCDebug(dcWattsonic()) << "Discovery: Test reply finished!" << reply->error() << reply->result() << serialNumber; Result result {master->modbusUuid(), serialNumber, slaveId}; m_discoveryResults.append(result); - } - if (slaveIdIndex < slaveIdCandidates.count() - 1) { - tryConnect(master, slaveIdIndex+1); - } else { - emit discoveryFinished(true); - } + emit discoveryFinished(true); }); } diff --git a/wattsonic/wattsonicdiscovery.h b/wattsonic/wattsonicdiscovery.h index 1001b52..4bd9927 100644 --- a/wattsonic/wattsonicdiscovery.h +++ b/wattsonic/wattsonicdiscovery.h @@ -46,7 +46,7 @@ public: quint16 slaveId; }; - void startDiscovery(); + void startDiscovery(quint16 slaveId); QList discoveryResults() const; @@ -54,7 +54,7 @@ signals: void discoveryFinished(bool modbusRtuMasterAvailable); private slots: - void tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex); + void tryConnect(ModbusRtuMaster *master, quint16 slaveId); private: ModbusRtuHardwareResource *m_modbusRtuResource = nullptr; From b0d17f3fdabe82504fa308affdb5d996e85dc78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 6 Dec 2024 12:28:57 +0100 Subject: [PATCH 2/2] Wattsonic: Add meter and battery detection and cleanup --- wattsonic/integrationpluginwattsonic.cpp | 72 ++++++++++++++++------- wattsonic/integrationpluginwattsonic.json | 7 +++ wattsonic/wattsonic-registers.json | 12 ++++ 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/wattsonic/integrationpluginwattsonic.cpp b/wattsonic/integrationpluginwattsonic.cpp index ae7f03f..98920da 100644 --- a/wattsonic/integrationpluginwattsonic.cpp +++ b/wattsonic/integrationpluginwattsonic.cpp @@ -42,11 +42,11 @@ IntegrationPluginWattsonic::IntegrationPluginWattsonic() void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info) { - if (info->thingClassId() == inverterThingClassId) { WattsonicDiscovery *discovery = new WattsonicDiscovery(hardwareManager()->modbusRtuResource(), info); connect(discovery, &WattsonicDiscovery::discoveryFinished, info, [=](bool modbusRtuMasterAvailable){ + if (!modbusRtuMasterAvailable) { info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No suitable Modbus RTU connection available. Please set up a Modbus RTU master with a baudrate of 9600, 8 data bits, 1 stop bit and no parity.")); return; @@ -54,13 +54,14 @@ void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info) foreach (const WattsonicDiscovery::Result &result, discovery->discoveryResults()) { - QString name = "Wattsonic hybrid inverter"; - ThingDescriptor descriptor(inverterThingClassId, name, result.serialNumber); + QString name = "Wattsonic inverter (" + result.serialNumber + ")"; + ThingDescriptor descriptor(inverterThingClassId, name, QString("Modbus ID: %1").arg(result.slaveId)); qCDebug(dcWattsonic()) << "Discovered:" << descriptor.title() << descriptor.description(); ParamList params { {inverterThingModbusMasterUuidParamTypeId, result.modbusRtuMasterId}, - {inverterThingSlaveAddressParamTypeId, result.slaveId} + {inverterThingSlaveAddressParamTypeId, result.slaveId}, + {inverterThingSerialNumberParamTypeId, result.serialNumber} }; descriptor.setParams(params); @@ -110,21 +111,7 @@ void IntegrationPluginWattsonic::setupThing(ThingSetupInfo *info) void IntegrationPluginWattsonic::postSetupThing(Thing *thing) { - if (thing->thingClassId() == inverterThingClassId) { - Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); - if (meters.isEmpty()) { - qCInfo(dcWattsonic()) << "No energy meter set up yet. Creating thing..."; - ThingDescriptor descriptor(meterThingClassId, "Wattsonic energy meter", QString(), thing->id()); - emit autoThingsAppeared({descriptor}); - } - Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId); - if (batteries.isEmpty()) { - qCInfo(dcWattsonic()) << "No battery set up yet. Creating thing..."; - ThingDescriptor descriptor(batteryThingClassId, "Wattsonic energy storage", QString(), thing->id()); - emit autoThingsAppeared({descriptor}); - } - } - + Q_UNUSED(thing) if (!m_pluginTimer) { qCDebug(dcWattsonic()) << "Starting plugin timer..."; @@ -216,7 +203,32 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) inverter->setStateValue(inverterTotalEnergyProducedStateTypeId, connection->totalPVGenerationFromInstallation() * 0.1); qCInfo(dcWattsonic()) << "Updating inverter:" << inverter->stateValue(inverterCurrentPowerStateTypeId).toDouble() << "W" << inverter->stateValue(inverterTotalEnergyProducedStateTypeId).toDouble() << "kWh"; - Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); + Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); + + // 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; + + if (meterDetected) { + if (meters.isEmpty()) { + qCInfo(dcWattsonic()) << "No energy meter set up yet but measurements detected. Creating meter thing..."; + QString parentSerialNumber = thing->paramValue(inverterThingSerialNumberParamTypeId).toString(); + QString name = "Wattsonic energy meter"; + if (!parentSerialNumber.isEmpty()) + name.append(" (" + parentSerialNumber + ")"); + + ThingDescriptor descriptor(meterThingClassId, name, QString(), thing->id()); + emit autoThingsAppeared({descriptor}); + } + } else { + // No meter detected + if (!meters.isEmpty()) { + // Remove existing meter thing + qCInfo(dcWattsonic()) << "Meter set up yet but energy measurments detected in the registers. Removing meter thing..."; + emit autoThingDisappeared(meters.first()->id()); + } + } + if (!meters.isEmpty()) { Thing *meter = meters.first(); meter->setStateValue(meterCurrentPowerStateTypeId, connection->totalPowerOnMeter() * -1.0); @@ -239,6 +251,26 @@ void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) } Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId); + + // Check if a battery is connected or not. We detect a battery by reading the DC voltage, if there is no voltage, there is no battery + if (connection->batteryVoltageDc() > 0) { + if (batteries.isEmpty()) { + qCInfo(dcWattsonic()) << "No battery set up yet but DC voltage detected in the registers. Creating battery thing..."; + QString parentSerialNumber = thing->paramValue(inverterThingSerialNumberParamTypeId).toString(); + QString name = "Wattsonic energy storage"; + if (!parentSerialNumber.isEmpty()) + name.append(" (" + parentSerialNumber + ")"); + + ThingDescriptor descriptor(batteryThingClassId, name, QString(), thing->id()); + emit autoThingsAppeared({descriptor}); + } + } else { + if (!batteries.isEmpty()) { + qCInfo(dcWattsonic()) << "Battery set up yet but no DC voltage detected in the registers. Removing battery thing..."; + emit autoThingDisappeared(batteries.first()->id()); + } + } + if (!batteries.isEmpty() && connection->SOC() > 0) { Thing *battery = batteries.first(); QHash map { diff --git a/wattsonic/integrationpluginwattsonic.json b/wattsonic/integrationpluginwattsonic.json index bf8f8e3..b7ce3b8 100644 --- a/wattsonic/integrationpluginwattsonic.json +++ b/wattsonic/integrationpluginwattsonic.json @@ -37,6 +37,13 @@ "displayName": "Modbus RTU master", "type": "QUuid", "defaultValue": "" + }, + { + "id": "47a655ca-bc68-42b0-8252-bde20de56974", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" } ], "stateTypes": [ diff --git a/wattsonic/wattsonic-registers.json b/wattsonic/wattsonic-registers.json index 9655845..da8c26f 100644 --- a/wattsonic/wattsonic-registers.json +++ b/wattsonic/wattsonic-registers.json @@ -275,6 +275,18 @@ "defaultValue": 0, "access": "RO" }, + { + "id": "batteryVoltageDc", + "address": 30254, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery voltage DC", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, { "id": "batteryMode", "address": 30256,