From 20b414ae10e50f4f6688787882b67e2ab1a76b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 28 Jul 2022 11:24:33 +0200 Subject: [PATCH] Make use of network device monitor and implement kostal discovery --- kostal/integrationpluginkostal.cpp | 458 +++++++++++++++++----------- kostal/integrationpluginkostal.h | 9 +- kostal/integrationpluginkostal.json | 8 - kostal/kostal-registers.json | 2 + kostal/kostal.pro | 6 +- kostal/kostaldiscovery.cpp | 178 +++++++++++ kostal/kostaldiscovery.h | 84 +++++ libnymea-modbus/modbustcpmaster.cpp | 1 + libnymea-modbus/modbustcpmaster.h | 1 + 9 files changed, 556 insertions(+), 191 deletions(-) create mode 100644 kostal/kostaldiscovery.cpp create mode 100644 kostal/kostaldiscovery.h diff --git a/kostal/integrationpluginkostal.cpp b/kostal/integrationpluginkostal.cpp index cea7a27..20c1e30 100644 --- a/kostal/integrationpluginkostal.cpp +++ b/kostal/integrationpluginkostal.cpp @@ -29,10 +29,11 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginkostal.h" - -#include "network/networkdevicediscovery.h" -#include "hardwaremanager.h" #include "plugininfo.h" +#include "kostaldiscovery.h" + +#include +#include IntegrationPluginKostal::IntegrationPluginKostal() { @@ -47,49 +48,71 @@ void IntegrationPluginKostal::discoverThings(ThingDiscoveryInfo *info) 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 + KostalDiscovery *discovery = new KostalDiscovery(hardwareManager()->networkDeviceDiscovery(), 1502, 71, info); + connect(discovery, &KostalDiscovery::discoveryFinished, info, [=](){ + foreach (const KostalDiscovery::KostalDiscoveryResult &result, discovery->discoveryResults()) { - qCDebug(dcKostal()) << "Found" << networkDeviceInfo; - - QString title; - if (networkDeviceInfo.hostName().isEmpty()) { - title = networkDeviceInfo.address().toString(); - } else { - title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; - } - - QString description; - if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { - description = networkDeviceInfo.macAddress(); - } else { - description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; - } - - ThingDescriptor descriptor(kostalInverterThingClassId, title, description); - ParamList params; - params << Param(kostalInverterThingIpAddressParamTypeId, networkDeviceInfo.address().toString()); - params << Param(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); - descriptor.setParams(params); + ThingDescriptor descriptor(kostalInverterThingClassId, result.manufacturerName + " " + result.productName, "Serial: " + result.serialNumber + " - " + result.networkDeviceInfo.address().toString()); + qCDebug(dcKostal()) << "Discovered:" << descriptor.title() << descriptor.description(); // Check if we already have set up this device - Things existingThings = myThings().filterByParam(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + Things existingThings = myThings().filterByParam(kostalInverterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { - qCDebug(dcKostal()) << "This connection already exists in the system:" << networkDeviceInfo; + qCDebug(dcKostal()) << "This Kostal inverter already exists in the system:" << result.networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); } + ParamList params; + params << Param(kostalInverterThingMacAddressParamTypeId, result.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); }); -} -void IntegrationPluginKostal::startMonitoringAutoThings() -{ + // Start the discovery process + discovery->startDiscovery(); +// NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); +// connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ +// foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + +// qCDebug(dcKostal()) << "Found" << networkDeviceInfo; + +// QString title; +// if (networkDeviceInfo.hostName().isEmpty()) { +// title = networkDeviceInfo.address().toString(); +// } else { +// title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; +// } + +// QString description; +// if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { +// description = networkDeviceInfo.macAddress(); +// } else { +// description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; +// } + +// ThingDescriptor descriptor(kostalInverterThingClassId, title, description); +// ParamList params; +// params << Param(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); +// descriptor.setParams(params); + +// // Check if we already have set up this device +// Things existingThings = myThings().filterByParam(kostalInverterThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); +// if (existingThings.count() == 1) { +// qCDebug(dcKostal()) << "This connection already exists in the system:" << networkDeviceInfo; +// descriptor.setThingId(existingThings.first()->id()); +// } + +// info->addThingDescriptor(descriptor); +// } + +// info->finish(Thing::ThingErrorNoError); +// }); } void IntegrationPluginKostal::setupThing(ThingSetupInfo *info) @@ -97,124 +120,63 @@ void IntegrationPluginKostal::setupThing(ThingSetupInfo *info) Thing *thing = info->thing(); qCDebug(dcKostal()) << "Setup" << thing << thing->params(); + // Inverter (connection) if (thing->thingClassId() == kostalInverterThingClassId) { - QHostAddress hostAddress = QHostAddress(thing->paramValue(kostalInverterThingIpAddressParamTypeId).toString()); - if (hostAddress.isNull()) { - info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); + + // Handle reconfigure + if (m_kostalConnections.contains(thing)) { + qCDebug(dcKostal()) << "Reconfiguring existing thing" << thing->name(); + m_kostalConnections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(kostalInverterThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcKostal()) << "The configured mac address is not valid" << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing.")); return; } - uint port = thing->paramValue(kostalInverterThingPortParamTypeId).toUInt(); - quint16 slaveId = thing->paramValue(kostalInverterThingSlaveIdParamTypeId).toUInt(); + // Create the monitor + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); + m_monitors.insert(thing, monitor); - KostalModbusTcpConnection *kostalConnection = new KostalModbusTcpConnection(hostAddress, port, slaveId, this); - connect(kostalConnection, &KostalModbusTcpConnection::initializationFinished, this, [this, thing, kostalConnection, info]{ - qCDebug(dcKostal()) << "Connection init" << kostalConnection; + QHostAddress address = monitor->networkDeviceInfo().address(); + if (address.isNull()) { + qCWarning(dcKostal()) << "Cannot set up thing. The host address is not known yet. Maybe it will be available in the next run..."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); + return; + } - // FIXME: check if success - - m_kostalConnections.insert(thing, kostalConnection); - info->finish(Thing::ThingErrorNoError); - - // Set connected true - thing->setStateValue(kostalInverterConnectedStateTypeId, true); - foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { - if (childThing->thingClassId() == kostalBatteryThingClassId) { - childThing->setStateValue(kostalBatteryConnectedStateTypeId, true); - } else if (childThing->thingClassId() == kostalMeterThingClassId) { - childThing->setStateValue(kostalMeterConnectedStateTypeId, true); - } - } - - connect(kostalConnection, &KostalModbusTcpConnection::totalAcPowerChanged, this, [thing](float totalAcPower){ - qCDebug(dcKostal()) << thing << "total AC power changed" << totalAcPower << "W"; - thing->setStateValue(kostalInverterCurrentPowerStateTypeId, - totalAcPower); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::totalYieldChanged, this, [thing](float totalYield){ - qCDebug(dcKostal()) << thing << "total yeald changed" << totalYield << "Wh"; - thing->setStateValue(kostalInverterTotalEnergyProducedStateTypeId, totalYield / 1000.0); // kWh - }); - - // Current - connect(kostalConnection, &KostalModbusTcpConnection::currentPhase1Changed, this, [thing](float currentPhase1){ - qCDebug(dcKostal()) << thing << "current phase 1 changed" << currentPhase1 << "A"; - thing->setStateValue(kostalInverterPhaseACurrentStateTypeId, currentPhase1); // A - }); - - connect(kostalConnection, &KostalModbusTcpConnection::currentPhase2Changed, this, [thing](float currentPhase2){ - qCDebug(dcKostal()) << thing << "current phase 2 changed" << currentPhase2 << "A"; - thing->setStateValue(kostalInverterPhaseBCurrentStateTypeId, currentPhase2); // A - }); - - connect(kostalConnection, &KostalModbusTcpConnection::currentPhase3Changed, this, [thing](float currentPhase3){ - qCDebug(dcKostal()) << thing << "current phase 3 changed" << currentPhase3 << "A"; - thing->setStateValue(kostalInverterPhaseCCurrentStateTypeId, currentPhase3); // A - }); - - // Voltage - connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase1Changed, this, [thing](float voltagePhase1){ - qCDebug(dcKostal()) << thing << "voltage phase 1 changed" << voltagePhase1 << "V"; - thing->setStateValue(kostalInverterVoltagePhaseAStateTypeId, voltagePhase1); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase2Changed, this, [thing](float voltagePhase2){ - qCDebug(dcKostal()) << thing << "voltage phase 2 changed" << voltagePhase2 << "V"; - thing->setStateValue(kostalInverterVoltagePhaseBStateTypeId, voltagePhase2); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase3Changed, this, [thing](float voltagePhase3){ - qCDebug(dcKostal()) << thing << "voltage phase 3 changed" << voltagePhase3 << "V"; - thing->setStateValue(kostalInverterVoltagePhaseCStateTypeId, voltagePhase3); - }); - - // Current power - connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase1Changed, this, [thing](float activePowerPhase1){ - qCDebug(dcKostal()) << thing << "active power phase 1 changed" << activePowerPhase1 << "W"; - thing->setStateValue(kostalInverterCurrentPowerPhaseAStateTypeId, activePowerPhase1); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase2Changed, this, [thing](float activePowerPhase2){ - qCDebug(dcKostal()) << thing << "active power phase 2 changed" << activePowerPhase2 << "W"; - thing->setStateValue(kostalInverterCurrentPowerPhaseBStateTypeId, activePowerPhase2); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase3Changed, this, [thing](float activePowerPhase3){ - qCDebug(dcKostal()) << thing << "active power phase 3 changed" << activePowerPhase3 << "W"; - thing->setStateValue(kostalInverterCurrentPowerPhaseCStateTypeId, activePowerPhase3); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::gridFrequencyInverterChanged, this, [thing](float gridFrequencyInverter){ - qCDebug(dcKostal()) << thing << "grid frequency changed" << gridFrequencyInverter << "Hz"; - thing->setStateValue(kostalInverterFrequencyStateTypeId, gridFrequencyInverter); - }); - - - // Update registers - kostalConnection->update(); - }); - - connect(kostalConnection, &KostalModbusTcpConnection::connectionStateChanged, this, [this, thing, kostalConnection](bool status){ - qCDebug(dcKostal()) << "Connected changed to" << status << "for" << thing; - if (status) { - // Connected true will be set after successfull init - kostalConnection->initialize(); - } else { - thing->setStateValue(kostalInverterConnectedStateTypeId, false); - foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { - if (childThing->thingClassId() == kostalBatteryThingClassId) { - childThing->setStateValue(kostalBatteryConnectedStateTypeId, false); - } else if (childThing->thingClassId() == kostalMeterThingClassId) { - childThing->setStateValue(kostalMeterConnectedStateTypeId, false); - } - } + // Clean up in case the setup gets aborted + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcKostal()) << "Unregister monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } }); - kostalConnection->connectDevice(); + // Wait for the monitor to be ready + if (monitor->reachable()) { + // Thing already reachable...let's continue with the setup + setupKostalConnection(info); + } else { + qCDebug(dcKostal()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "..."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcKostal()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup..."; + setupKostalConnection(info); + } + }); + } return; } + // Meter if (thing->thingClassId() == kostalMeterThingClassId) { // Get the parent thing and the associated connection Thing *connectionThing = myThings().findById(thing->parentId()); @@ -357,45 +319,47 @@ void IntegrationPluginKostal::postSetupThing(Thing *thing) // Check if we have to create the meter for the Kostal inverter if (myThings().filterByParentId(thing->id()).filterByThingClassId(kostalMeterThingClassId).isEmpty()) { - qCDebug(dcKostal()) << "--> Read block \"powerMeterValues\" registers from:" << 220 << "size:" << 38; QModbusReply *reply = kostalConnection->readBlockPowerMeterValues(); - if (reply) { - if (!reply->isFinished()) { - connect(reply, &QModbusReply::finished, this, [=](){ - if (reply->error() == QModbusDevice::NoError) { - const QModbusDataUnit unit = reply->result(); - const QVector blockValues = unit.values(); - - bool notZero = false; - for (int i = 0; i < blockValues.size(); i++) { - if (blockValues.at(i) != 0) { - notZero = true; - break; - } - } - - if (notZero) { - qCDebug(dcKostal()) << "There is a meter connected but not set up yet. Creating a meter..."; - // No meter thing created for this inverter, lets create one with the inverter as parent - ThingClass meterThingClass = thingClass(kostalMeterThingClassId); - ThingDescriptor descriptor(kostalMeterThingClassId, meterThingClass.name(), QString(), thing->id()); - // No params required, all we need is the connection - emit autoThingsAppeared(ThingDescriptors() << descriptor); - } else { - qCDebug(dcKostal()) << "There is no meter connected to the inverter" << thing; - } - } - }); - - connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){ - qCWarning(dcKostal()) << "Modbus reply error occurred while updating block \"powerMeterValues\" registers" << error << reply->errorString(); - emit reply->finished(); - }); - } - } else { + if (!reply) { qCWarning(dcKostal()) << "Error occurred while reading block \"powerMeterValues\" registers"; + return; } + + if (reply->isFinished()) { + reply->deleteLater(); // Broadcast reply returns immediatly + return; + } + + connect(reply, &QModbusReply::finished, this, [=](){ + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + const QVector blockValues = unit.values(); + + bool notZero = false; + for (int i = 0; i < blockValues.size(); i++) { + if (blockValues.at(i) != 0) { + notZero = true; + break; + } + } + + if (notZero) { + qCDebug(dcKostal()) << "There is a meter connected but not set up yet. Creating a meter..."; + // No meter thing created for this inverter, lets create one with the inverter as parent + ThingClass meterThingClass = thingClass(kostalMeterThingClassId); + ThingDescriptor descriptor(kostalMeterThingClassId, meterThingClass.name(), QString(), thing->id()); + // No params required, all we need is the connection + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } else { + qCDebug(dcKostal()) << "There is no meter connected to the inverter" << thing; + } + } + }); + + connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){ + qCWarning(dcKostal()) << "Modbus reply error occurred while updating block \"powerMeterValues\" registers" << error << reply->errorString(); + }); } // Check if we have to create the battery for the Kostal inverter @@ -412,6 +376,7 @@ void IntegrationPluginKostal::postSetupThing(Thing *thing) emit autoThingsAppeared(ThingDescriptors() << descriptor); } + // Create the update timer if not already set up if (!m_pluginTimer) { qCDebug(dcKostal()) << "Starting plugin timer..."; m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(4); @@ -433,15 +398,154 @@ void IntegrationPluginKostal::thingRemoved(Thing *thing) delete connection; } + // Unregister related hardware resources + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + if (myThings().isEmpty() && m_pluginTimer) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } -void IntegrationPluginKostal::executeAction(ThingActionInfo *info) +void IntegrationPluginKostal::setupKostalConnection(ThingSetupInfo *info) { - info->finish(Thing::ThingErrorNoError); + Thing *thing = info->thing(); + + QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); + uint port = thing->paramValue(kostalInverterThingPortParamTypeId).toUInt(); + quint16 slaveId = thing->paramValue(kostalInverterThingSlaveIdParamTypeId).toUInt(); + + qCDebug(dcKostal()) << "Setting up kostal on" << address.toString() << port << "unit ID:" << slaveId; + KostalModbusTcpConnection *kostalConnection = new KostalModbusTcpConnection(address, port, slaveId, this); + connect(info, &ThingSetupInfo::aborted, kostalConnection, &KostalModbusTcpConnection::deleteLater); + + // Reconnect on monitor reachable changed + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcKostal()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (!thing->setupComplete()) + return; + + if (reachable && !thing->stateValue("connected").toBool()) { + kostalConnection->setHostAddress(monitor->networkDeviceInfo().address()); + kostalConnection->connectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + kostalConnection->disconnectDevice(); + } + }); + + connect(kostalConnection, &KostalModbusTcpConnection::initializationFinished, info, [=](bool success){ + if (!success) { + qCWarning(dcKostal()) << "Connection init finished with errors" << thing->name() << kostalConnection->hostAddress().toString(); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); + kostalConnection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the wallbox.")); + return; + } + + qCDebug(dcKostal()) << "Connection init finished successfully" << kostalConnection; + m_kostalConnections.insert(thing, kostalConnection); + info->finish(Thing::ThingErrorNoError); + + // Set connected true + thing->setStateValue("connected", true); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", true); + } + + connect(kostalConnection, &KostalModbusTcpConnection::totalAcPowerChanged, thing, [thing](float totalAcPower){ + qCDebug(dcKostal()) << thing << "total AC power changed" << totalAcPower << "W"; + thing->setStateValue(kostalInverterCurrentPowerStateTypeId, - totalAcPower); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::totalYieldChanged, thing, [thing](float totalYield){ + qCDebug(dcKostal()) << thing << "total yeald changed" << totalYield << "Wh"; + thing->setStateValue(kostalInverterTotalEnergyProducedStateTypeId, totalYield / 1000.0); // kWh + }); + + // Current + connect(kostalConnection, &KostalModbusTcpConnection::currentPhase1Changed, thing, [thing](float currentPhase1){ + qCDebug(dcKostal()) << thing << "current phase 1 changed" << currentPhase1 << "A"; + thing->setStateValue(kostalInverterPhaseACurrentStateTypeId, currentPhase1); // A + }); + + connect(kostalConnection, &KostalModbusTcpConnection::currentPhase2Changed, thing, [thing](float currentPhase2){ + qCDebug(dcKostal()) << thing << "current phase 2 changed" << currentPhase2 << "A"; + thing->setStateValue(kostalInverterPhaseBCurrentStateTypeId, currentPhase2); // A + }); + + connect(kostalConnection, &KostalModbusTcpConnection::currentPhase3Changed, thing, [thing](float currentPhase3){ + qCDebug(dcKostal()) << thing << "current phase 3 changed" << currentPhase3 << "A"; + thing->setStateValue(kostalInverterPhaseCCurrentStateTypeId, currentPhase3); // A + }); + + // Voltage + connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase1Changed, thing, [thing](float voltagePhase1){ + qCDebug(dcKostal()) << thing << "voltage phase 1 changed" << voltagePhase1 << "V"; + thing->setStateValue(kostalInverterVoltagePhaseAStateTypeId, voltagePhase1); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase2Changed, thing, [thing](float voltagePhase2){ + qCDebug(dcKostal()) << thing << "voltage phase 2 changed" << voltagePhase2 << "V"; + thing->setStateValue(kostalInverterVoltagePhaseBStateTypeId, voltagePhase2); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::voltagePhase3Changed, thing, [thing](float voltagePhase3){ + qCDebug(dcKostal()) << thing << "voltage phase 3 changed" << voltagePhase3 << "V"; + thing->setStateValue(kostalInverterVoltagePhaseCStateTypeId, voltagePhase3); + }); + + // Current power + connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase1Changed, thing, [thing](float activePowerPhase1){ + qCDebug(dcKostal()) << thing << "active power phase 1 changed" << activePowerPhase1 << "W"; + thing->setStateValue(kostalInverterCurrentPowerPhaseAStateTypeId, activePowerPhase1); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase2Changed, thing, [thing](float activePowerPhase2){ + qCDebug(dcKostal()) << thing << "active power phase 2 changed" << activePowerPhase2 << "W"; + thing->setStateValue(kostalInverterCurrentPowerPhaseBStateTypeId, activePowerPhase2); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::activePowerPhase3Changed, thing, [thing](float activePowerPhase3){ + qCDebug(dcKostal()) << thing << "active power phase 3 changed" << activePowerPhase3 << "W"; + thing->setStateValue(kostalInverterCurrentPowerPhaseCStateTypeId, activePowerPhase3); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::gridFrequencyInverterChanged, thing, [thing](float gridFrequencyInverter){ + qCDebug(dcKostal()) << thing << "grid frequency changed" << gridFrequencyInverter << "Hz"; + thing->setStateValue(kostalInverterFrequencyStateTypeId, gridFrequencyInverter); + }); + + // Update registers + kostalConnection->update(); + }); + + connect(kostalConnection, &KostalModbusTcpConnection::reachableChanged, thing, [this, thing, kostalConnection](bool reachable){ + qCDebug(dcKostal()) << "Reachable changed to" << reachable << "for" << thing; + if (reachable) { + // Connected true will be set after successfull init + kostalConnection->initialize(); + } else { + thing->setStateValue("connected", false); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", false); + } + } + }); + + + connect(kostalConnection, &KostalModbusTcpConnection::initializationFinished, info, [=](bool success){ + if (success) { + thing->setStateValue("connected", true); + } else { + thing->setStateValue("connected", false); + // Try once to reconnect the device + kostalConnection->reconnectDevice(); + } + }); + + kostalConnection->connectDevice(); } - - diff --git a/kostal/integrationpluginkostal.h b/kostal/integrationpluginkostal.h index 866ab1d..a7e5c0d 100644 --- a/kostal/integrationpluginkostal.h +++ b/kostal/integrationpluginkostal.h @@ -33,6 +33,9 @@ #include #include +#include + +#include "extern-plugininfo.h" #include "kostalmodbustcpconnection.h" @@ -44,21 +47,19 @@ class IntegrationPluginKostal: public IntegrationPlugin Q_INTERFACES(IntegrationPlugin) public: - /** Constructor */ explicit IntegrationPluginKostal(); 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_kostalConnections; + QHash m_monitors; - + void setupKostalConnection(ThingSetupInfo *info); }; diff --git a/kostal/integrationpluginkostal.json b/kostal/integrationpluginkostal.json index dfd6487..1910f60 100644 --- a/kostal/integrationpluginkostal.json +++ b/kostal/integrationpluginkostal.json @@ -16,14 +16,6 @@ "interfaces": ["solarinverter", "connectable"], "providedInterfaces": [ "energymeter", "energystorage"], "paramTypes": [ - { - "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", - "name": "ipAddress", - "displayName": "IP address", - "type": "QString", - "inputType": "IPv4Address", - "defaultValue": "127.0.0.1" - }, { "id": "906f6099-d0e1-4297-a2b3-f8ec4482c578", "name":"macAddress", diff --git a/kostal/kostal-registers.json b/kostal/kostal-registers.json index df21a96..48878c4 100644 --- a/kostal/kostal-registers.json +++ b/kostal/kostal-registers.json @@ -2,6 +2,8 @@ "className": "Kostal", "protocol": "TCP", "endianness": "LittleEndian", + "errorLimitUntilNotReachable": 20, + "checkReachableRegister": "inverterState", "enums": [ { "name": "ByteOrder", diff --git a/kostal/kostal.pro b/kostal/kostal.pro index b5d31e6..d401401 100644 --- a/kostal/kostal.pro +++ b/kostal/kostal.pro @@ -6,7 +6,9 @@ MODBUS_CONNECTIONS += kostal-registers.json include(../modbus.pri) HEADERS += \ - integrationpluginkostal.h + integrationpluginkostal.h \ + kostaldiscovery.h SOURCES += \ - integrationpluginkostal.cpp + integrationpluginkostal.cpp \ + kostaldiscovery.cpp diff --git a/kostal/kostaldiscovery.cpp b/kostal/kostaldiscovery.cpp new file mode 100644 index 0000000..6e55346 --- /dev/null +++ b/kostal/kostaldiscovery.cpp @@ -0,0 +1,178 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "kostaldiscovery.h" +#include "extern-plugininfo.h" + +KostalDiscovery::KostalDiscovery(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(dcKostal()) << "Discovery: SunnyWebBox: Grace period timer triggered."; + finishDiscovery(); + }); +} + +void KostalDiscovery::startDiscovery() +{ + qCInfo(dcKostal()) << "Discovery: Start searching for Kostal inverters 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, &KostalDiscovery::checkNetworkDevice); + + // Check what might be left on finished + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcKostal()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + qCDebug(dcKostal()) << "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(); + }); +} + +QList KostalDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void KostalDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + // Create a kostal connection and try to initialize it. + // Only if initialized successfully and all information have been fetched correctly from + // the device we can assume this is what we are locking for (ip, port, modbus address, correct registers). + // We cloud tough also filter the result only for certain software versions, manufactueres or whatever... + + if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) + return; + + KostalModbusTcpConnection *connection = new KostalModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + m_connections.append(connection); + m_verifiedNetworkDeviceInfos.append(networkDeviceInfo); + + connect(connection, &KostalModbusTcpConnection::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, &KostalModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcKostal()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + return; + } + KostalDiscoveryResult result; + result.productName = connection->productName(); + result.manufacturerName = connection->inverterManufacturer(); + result.serialNumber = connection->inverterSerialNumber1(); + result.articleNumber = connection->inverterArticleNumber(); + result.softwareVersionIoController = connection->softwareVersionIoController(); + result.softwareVersionMainController = connection->softwareVersionMainController(); + result.networkDeviceInfo = networkDeviceInfo; + m_discoveryResults.append(result); + + qCDebug(dcKostal()) << "Discovery: --> Found" << result.manufacturerName << result.productName + << "Article:" << result.articleNumber + << "Serial number:" << result.serialNumber + << "Software version main controller:" << result.softwareVersionMainController + << "Software version IO controller:" << result.softwareVersionIoController + << result.networkDeviceInfo; + + + // Done with this connection + cleanupConnection(connection); + }); + + if (!connection->initialize()) { + qCDebug(dcKostal()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + + // Initializing... + }); + + // If we get any error...skip this host... + connect(connection, &KostalModbusTcpConnection::connectionErrorOccurred, this, [=](QModbusDevice::Error error){ + if (error != QModbusDevice::NoError) { + qCDebug(dcKostal()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &KostalModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcKostal()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); +} + +void KostalDiscovery::cleanupConnection(KostalModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void KostalDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (KostalModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcKostal()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() + << "Kostal Inverters in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); +} diff --git a/kostal/kostaldiscovery.h b/kostal/kostaldiscovery.h new file mode 100644 index 0000000..c78c451 --- /dev/null +++ b/kostal/kostaldiscovery.h @@ -0,0 +1,84 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 KOSTALDISCOVERY_H +#define KOSTALDISCOVERY_H + +#include +#include + +#include + +#include "kostalmodbustcpconnection.h" + +class KostalDiscovery : public QObject +{ + Q_OBJECT +public: + explicit KostalDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 1502, quint16 modbusAddress = 71, QObject *parent = nullptr); + typedef struct KostalDiscoveryResult { + QString productName; + QString manufacturerName; + QString serialNumber; + QString articleNumber; + QString softwareVersionMainController; + QString softwareVersionIoController; + NetworkDeviceInfo networkDeviceInfo; + } KostalDiscoveryResult; + + void startDiscovery(); + + QList 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; + + QList m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(KostalModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // KOSTALDISCOVERY_H diff --git a/libnymea-modbus/modbustcpmaster.cpp b/libnymea-modbus/modbustcpmaster.cpp index b689921..88f79db 100644 --- a/libnymea-modbus/modbustcpmaster.cpp +++ b/libnymea-modbus/modbustcpmaster.cpp @@ -454,6 +454,7 @@ QUuid ModbusTCPMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr void ModbusTCPMaster::onModbusErrorOccurred(QModbusDevice::Error error) { qCWarning(dcModbusTcpMaster()) << "An error occurred" << error; + emit connectionErrorOccurred(error); } void ModbusTCPMaster::onModbusStateChanged(QModbusDevice::State state) diff --git a/libnymea-modbus/modbustcpmaster.h b/libnymea-modbus/modbustcpmaster.h index 4629419..ef2bc9b 100644 --- a/libnymea-modbus/modbustcpmaster.h +++ b/libnymea-modbus/modbustcpmaster.h @@ -104,6 +104,7 @@ private slots: signals: void connectionStateChanged(bool status); + void connectionErrorOccurred(QModbusDevice::Error error); void writeRequestExecuted(const QUuid &requestId, bool success); void writeRequestError(const QUuid &requestId, const QString &error);