Make use of network device monitor and implement kostal discovery

This commit is contained in:
Simon Stürz 2022-07-28 11:24:33 +02:00
parent 13a5fc9038
commit 20b414ae10
9 changed files with 556 additions and 191 deletions

View File

@ -29,10 +29,11 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginkostal.h"
#include "network/networkdevicediscovery.h"
#include "hardwaremanager.h"
#include "plugininfo.h"
#include "kostaldiscovery.h"
#include <network/networkdevicediscovery.h>
#include <hardwaremanager.h>
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<quint16> 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<quint16> 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();
}

View File

@ -33,6 +33,9 @@
#include <plugintimer.h>
#include <integrations/integrationplugin.h>
#include <network/networkdevicemonitor.h>
#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<Thing *, KostalModbusTcpConnection *> m_kostalConnections;
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
void setupKostalConnection(ThingSetupInfo *info);
};

View File

@ -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",

View File

@ -2,6 +2,8 @@
"className": "Kostal",
"protocol": "TCP",
"endianness": "LittleEndian",
"errorLimitUntilNotReachable": 20,
"checkReachableRegister": "inverterState",
"enums": [
{
"name": "ByteOrder",

View File

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

178
kostal/kostaldiscovery.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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::KostalDiscoveryResult> 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();
}

84
kostal/kostaldiscovery.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <QObject>
#include <QTimer>
#include <network/networkdevicediscovery.h>
#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<KostalDiscoveryResult> 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<KostalModbusTcpConnection *> m_connections;
QList<KostalDiscoveryResult> m_discoveryResults;
void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
void cleanupConnection(KostalModbusTcpConnection *connection);
void finishDiscovery();
};
#endif // KOSTALDISCOVERY_H

View File

@ -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)

View File

@ -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);