From 446b3e3dda0a01757423cfeccc2165db2c3c3d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 08:49:58 +0200 Subject: [PATCH] Update mtec plugin with network device discovery and internal connection mechanism --- mtec/integrationpluginmtec.cpp | 159 ++++++++++++++--------------- mtec/integrationpluginmtec.h | 11 +- mtec/integrationpluginmtec.json | 174 ++++++++++++++++---------------- mtec/mtec.cpp | 62 ++++-------- mtec/mtec.h | 30 ++---- 5 files changed, 189 insertions(+), 247 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index 0dffcc0..15c3ac3 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -28,6 +28,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#include "network/networkdevicediscovery.h" #include "integrationpluginmtec.h" #include "plugininfo.h" @@ -38,81 +39,96 @@ IntegrationPluginMTec::IntegrationPluginMTec() void IntegrationPluginMTec::discoverThings(ThingDiscoveryInfo *info) { - qCDebug(dcMTec()) << "Discover M-Tec heat pumps"; - - if (info->thingClassId() == mtecThingClassId) { - QString description = "Heatpump"; - ThingDescriptor descriptor(info->thingClassId(), "M-Tec", description); - info->addThingDescriptor(descriptor); - - // TODO Find out, if a discovery is possible/needed - // Otherwise, just report no error for now - info->finish(Thing::ThingErrorNoError); + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcMTec()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; } + + // Perform a network device discovery and filter for "go-eCharger" hosts + NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { + + qCDebug(dcMTec()) << "Found" << networkDevice; + + QString title; + if (networkDevice.hostName().isEmpty()) { + title = networkDevice.address().toString(); + } else { + title = networkDevice.hostName() + " (" + networkDevice.address().toString() + ")"; + } + + QString description; + if (networkDevice.macAddressManufacturer().isEmpty()) { + description = networkDevice.macAddress(); + } else { + description = networkDevice.macAddress() + " (" + networkDevice.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(mtecThingClassId, title, description); + ParamList params; + params << Param(mtecThingIpAddressParamTypeId, networkDevice.address().toString()); + params << Param(mtecThingMacAddressParamTypeId, networkDevice.macAddress()); + descriptor.setParams(params); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(mtecThingMacAddressParamTypeId, networkDevice.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcMTec()) << "This heat pump already exists in the system!" << networkDevice; + descriptor.setThingId(existingThings.first()->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); } void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) { - qCDebug(dcMTec()) << "Setup" << info->thing(); - Thing *thing = info->thing(); + qCDebug(dcMTec()) << "Setup" << thing; if (thing->thingClassId() == mtecThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(mtecThingIpAddressParamTypeId).toString()); - if (hostAddress.isNull()) { info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); return; } - qCDebug(dcMTec()) << "User entered address: " << hostAddress.toString(); - - /* Check, if address is already in use for another device */ - /* for (QHash::iterator item=m_mtecConnections.begin(); item != m_mtecConnections.end(); item++) { */ - /* if (hostAddress.isEqual(item.value()->getHostAddress())) { */ - /* qCDebug(dcMTec()) << "Address of thing: " << item.value()->getHostAddress().toString(); */ - - /* qCDebug(dcMTec()) << "Address in use already"; */ - /* } else { */ - /* qCDebug(dcMTec()) << "Different address of other thing: " << item.value()->getHostAddress().toString(); */ - - /* } */ - /* } */ - - foreach (MTec *mtecConnection, m_mtecConnections.values()) { - if (mtecConnection->getHostAddress().isEqual(hostAddress)) { - qCWarning(dcMTec()) << "Address" << hostAddress.toString() << "already in use by" << m_mtecConnections.key(mtecConnection); - info->finish(Thing::ThingErrorThingInUse, QT_TR_NOOP("IP address already in use by another thing.")); - return; - } + qCDebug(dcMTec()) << "Using ip address" << hostAddress.toString(); + if (myThings().filterByParam(mtecThingIpAddressParamTypeId, hostAddress.toString()).count() > 0) { + info->finish(Thing::ThingErrorThingInUse, QT_TR_NOOP("IP address already in use by another thing.")); + return; } - qCDebug(dcMTec()) << "Creating M-Tec object"; + // TODO: start timer and give 15 seconds until connected, since the controler is down for ~10 seconds after a disconnect - /* Create new MTec object and store it in hash table */ MTec *mtec = new MTec(hostAddress, this); - m_mtecConnections.insert(thing, mtec); + connect(mtec, &MTec::statusUpdated, this, &IntegrationPluginMTec::onStatusUpdated); + connect(mtec, &MTec::connectedChanged, thing, [=](bool connected){ + qCDebug(dcMTec()) << "Connected changed to" << connected; + thing->setStateValue(mtecConnectedStateTypeId, connected); + }); + + m_mtecConnections.insert(thing, mtec); + if (!mtec->connectDevice()) { + qCWarning(dcMTec()) << "Initial connect returned false. Lets wait 15 seconds until the connection can be established."; + } - info->thing()->setStateValue(mtecConnectedStateTypeId, true); info->finish(Thing::ThingErrorNoError); } } void IntegrationPluginMTec::postSetupThing(Thing *thing) { - qCDebug(dcMTec()) << "PostSetup called for" << thing; - if (thing->thingClassId() == mtecThingClassId) { MTec *mtec = m_mtecConnections.value(thing); - if (mtec) { - connect(mtec, &MTec::statusUpdated, this, &IntegrationPluginMTec::onStatusUpdated); - connect(mtec, &MTec::connectedChanged, this, &IntegrationPluginMTec::onConnectedChanged); - - qCDebug(dcMTec()) << "Thing set up, calling update"; update(thing); - - thing->setStateValue(mtecConnectedStateTypeId, true); + //thing->setStateValue(mtecConnectedStateTypeId, true); } } } @@ -126,63 +142,38 @@ void IntegrationPluginMTec::thingRemoved(Thing *thing) void IntegrationPluginMTec::executeAction(ThingActionInfo *info) { - Thing *thing = info->thing(); - Action action = info->action(); +// Thing *thing = info->thing(); +// Action action = info->action(); + info->finish(Thing::ThingErrorNoError); - if (thing->thingClassId() == mtecThingClassId) { - /* if (action.actionTypeId() == mtecPowerActionTypeId) { */ - - /* } else { */ - /* Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); */ - /* } */ - } else { - Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); - } +// if (thing->thingClassId() == mtecThingClassId) { +// /* if (action.actionTypeId() == mtecPowerActionTypeId) { */ + +// } else { +// Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); +// } } void IntegrationPluginMTec::update(Thing *thing) { if (thing->thingClassId() == mtecThingClassId) { qCDebug(dcMTec()) << "Updating thing" << thing; - MTec *mtec = m_mtecConnections.value(thing); - - if (mtec) { - mtec->onRequestStatus(); - } + if (!mtec) return; + mtec->requestStatus(); } } -void IntegrationPluginMTec::onConnectedChanged(MTecHelpers::ConnectionState state) -{ - MTec *mtec = qobject_cast(sender()); - Thing *thing = m_mtecConnections.key(mtec); - - qCDebug(dcMTec()) << "Received connection change event from heat pump" << thing; - - if (!thing) - return; - - if (state == MTecHelpers::ConnectionState::Online) { - thing->setStateValue(mtecConnectedStateTypeId, true); - } - - thing->setStateValue(mtecStatusStateTypeId, MTecHelpers::connectionStateToString(state)); -} - void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) { MTec *mtec = qobject_cast(sender()); Thing *thing = m_mtecConnections.key(mtec); - - qCDebug(dcMTec()) << "Received status from heat pump" << thing; - if (!thing) return; + qCDebug(dcMTec()) << "Received status from heat pump" << thing; /* Received a structure holding the status info of the * heat pump. Update the thing states with the individual fields. */ - thing->setStateValue(mtecStatusStateTypeId, info.status); thing->setStateValue(mtecActualPowerConsumptionStateTypeId, info.actualPowerConsumption); thing->setStateValue(mtecActualExcessEnergySmartHomeStateTypeId, info.actualExcessEnergySmartHome); thing->setStateValue(mtecActualExcessEnergyElectricityMeterStateTypeId, info.actualExcessEnergyElectricityMeter); @@ -192,8 +183,6 @@ void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) void IntegrationPluginMTec::onRefreshTimer() { - qCDebug(dcMTec()) << "onRefreshTimer called"; - foreach (Thing *thing, myThings().filterByThingClassId(mtecThingClassId)) { update(thing); } diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h index a2ecedb..1dee6ba 100644 --- a/mtec/integrationpluginmtec.h +++ b/mtec/integrationpluginmtec.h @@ -51,19 +51,16 @@ public: void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; - - -private: void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void executeAction(ThingActionInfo *info) override; + +private: + QHash m_mtecConnections; + void update(Thing *thing); - QHash m_mtecConnections; - QHash m_asyncActions; - private slots: - void onConnectedChanged(MTecHelpers::ConnectionState state); void onRefreshTimer(); void onStatusUpdated(const MTecInfo &info); diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json index 9f91d44..7ea0e0a 100644 --- a/mtec/integrationpluginmtec.json +++ b/mtec/integrationpluginmtec.json @@ -8,98 +8,94 @@ "displayName": "M-Tec", "id": "04d3fa7c-e469-4a79-a119-155426e5a846", "thingClasses": [ - { - "name": "mtec", - "displayName": "MTec", - "id": "451e38d8-50d5-4ae9-8d9f-21af9347128d", - "createMethods": ["user"], - "interfaces": ["connectable"], - "paramTypes": [ { - "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", - "name": "ipAddress", - "displayName": "IP address", - "type": "QString" - } - ], - "stateTypes": [ - { - "id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b", - "name": "connected", - "displayName": "Connected", - "displayNameEvent": "Connected changed", - "type": "bool", - "defaultValue": false, - "cached": false - }, - { - "id": "9bf5f8d6-116a-4399-a728-51470a3a5620", - "name": "status", - "displayName": "Status", - "displayNameEvent": "Status changed", - "type": "QString", - "defaultValue": "Off", - "possibleValues": [ - "Off", - "Connecting", - "Connected", - "Error" - ] - }, - { - "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", - "name": "actualPowerConsumption", - "displayName": "Actual power consumption", - "displayNameEvent": "Actual power consumption changed", - "type": "double", - "unit": "Watt", - "defaultValue": 0 - }, - { - "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", - "name": "actualExcessEnergySmartHome", - "displayName": "Actual excess energy from Smart home System", - "displayNameEvent": "Actual excess energy from Smart home System changed", - "displayNameAction": "Change actual excess energy from Smart home System", - "type": "double", - "unit": "Watt", - "defaultValue": 0 - }, - { - "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", - "name": "actualExcessEnergyElectricityMeter", - "displayName": "Actual excess energy from Electricity Meter", - "displayNameEvent": "Actual excess energy from Electricity Meter changed", - "type": "double", - "unit": "Watt", - "defaultValue": 0 - }, - { - "id": "087c0296-705b-483a-b1e9-7ce08202c035", - "name": "externalSetValueScaling", - "displayName": "Control of the heat source by an external control [100%]", - "displayNameEvent": "Control of the heat source by an external control [100%] changed", - "type": "double", - "unit": "Percentage", - "defaultValue": 100 - }, - { - "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", - "name": "requestExternalHeatSource", - "displayName": "Request external heat source", - "displayNameEvent": "Request external heat source changed", - "type": "QString", - "defaultValue": "No request, external heat source must be turned off", - "possibleValues": [ - "No request, external heat source must be turned off", - "External heat source is released and can be switched on", - "External heat source is required and must be turned on" + "name": "mtec", + "displayName": "MTec", + "id": "451e38d8-50d5-4ae9-8d9f-21af9347128d", + "createMethods": ["discovery", "user"], + "interfaces": ["connectable"], + "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", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", + "name": "actualPowerConsumption", + "displayName": "Actual power consumption", + "displayNameEvent": "Actual power consumption changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", + "name": "actualExcessEnergySmartHome", + "displayName": "Actual excess energy from Smart home System", + "displayNameEvent": "Actual excess energy from Smart home System changed", + "displayNameAction": "Change actual excess energy from Smart home System", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", + "name": "actualExcessEnergyElectricityMeter", + "displayName": "Actual excess energy from Electricity Meter", + "displayNameEvent": "Actual excess energy from Electricity Meter changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "087c0296-705b-483a-b1e9-7ce08202c035", + "name": "externalSetValueScaling", + "displayName": "Control of the heat source by an external control [100%]", + "displayNameEvent": "Control of the heat source by an external control [100%] changed", + "type": "double", + "unit": "Percentage", + "defaultValue": 100 + }, + { + "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", + "name": "requestExternalHeatSource", + "displayName": "Request external heat source", + "displayNameEvent": "Request external heat source changed", + "type": "QString", + "defaultValue": "No request, external heat source must be turned off", + "possibleValues": [ + "No request, external heat source must be turned off", + "External heat source is released and can be switched on", + "External heat source is required and must be turned on" + ] + } + ], + "actionTypes": [ ] } - ], - "actionTypes": [ - ] - } ] } ] diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp index 21d25bf..afa5864 100644 --- a/mtec/mtec.cpp +++ b/mtec/mtec.cpp @@ -39,13 +39,11 @@ MTec::MTec(const QHostAddress &address, QObject *parent) : m_hostAddress(address) { m_modbusMaster = new ModbusTCPMaster(address, 502, this); + m_modbusMaster->setTimeout(2000); + m_modbusMaster->setNumberOfRetries(5); - qCDebug(dcMTec()) << "created ModbusTCPMaster"; - - if (m_modbusMaster->connectDevice()) { - emit connectedChanged(MTecHelpers::ConnectionState::Connecting); - } - + qCDebug(dcMTec()) << "Created ModbusTCPMaster for" << address.toString(); + connect(m_modbusMaster, &ModbusTCPMaster::connectionStateChanged, this, &MTec::connectedChanged); connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &MTec::onReceivedHoldingRegister); connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &MTec::onModbusError); connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &MTec::onModbusError); @@ -55,59 +53,35 @@ MTec::~MTec() { } -void MTec::onModbusError() +bool MTec::connectDevice() { - qCWarning(dcMTec()) << "MTec: Received modbus error"; - - /* Only emit connected changed signal, if a soft connection - * had already been established. - * This avoids a series of possibly fake connection state changes, - * while the modbus server in the heatpump is starting up. */ - if (m_softConnection) { - m_softConnection = false; - emit connectedChanged(MTecHelpers::ConnectionState::Offline); - } + return m_modbusMaster->connectDevice(); } -void MTec::onRequestStatus() +void MTec::disconnectDevice() { - if ((m_softConnection) || - ((m_connected) && (m_requestsSent < MTec::ConnectionRetries))) { + m_modbusMaster->disconnectDevice(); +} - if (m_requestsSent < MTec::ConnectionRetries) { - m_firstTimeout = QDateTime::currentDateTime(); - m_firstTimeout = m_firstTimeout.addSecs(MTec::FirstConnectionTimeout); - - /* Save original modbus timeout, will be set again - * after first response is received */ - m_modbusTimeout = m_modbusMaster->timeout(); - - /* The M-Tec heatpump requires a longer timeout to - * start-up the modbus communication */ - m_modbusMaster->setTimeout(MTec::FirstConnectionTimeout); - } else { - /* Set back original modbus timeout */ - m_modbusMaster->setTimeout(m_modbusTimeout); - } - - } else { - qCDebug(dcMTec()) << "Max number of modbus connects reached, giving up"; +void MTec::requestStatus() +{ + if (!m_modbusMaster->connected()) { return; } m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualPowerConsumption, 1); - m_requestsSent ++; } +void MTec::onModbusError() +{ + qCWarning(dcMTec()) << "MTec: Received modbus error"; +} + + void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); - /* Some response was received, so the modbus communication is - * established. */ - m_softConnection = true; - emit connectedChanged(MTecHelpers::ConnectionState::Online); - switch (modbusRegister) { case ActualPowerConsumption: if (value.length() == 1) { diff --git a/mtec/mtec.h b/mtec/mtec.h index 1fbe047..181da2c 100644 --- a/mtec/mtec.h +++ b/mtec/mtec.h @@ -49,23 +49,17 @@ public: /** Destructor */ ~MTec(); - inline QHostAddress getHostAddress() const { return m_hostAddress; }; + inline QHostAddress hostAddress() const { return m_hostAddress; }; + + bool connectDevice(); + void disconnectDevice(); + + void requestStatus(); private: - /** - * The first response of the M-Tec heatpump may be delayed - * by 10 s. The plugin will wait for the defined time in milliseconds - * for a response before sending an additional request or setting - * an error code. - */ - static const int FirstConnectionTimeout = 15000; - /** Modbus Unit ID (undocumented, guessing 1 for now) */ static const quint16 ModbusUnitID = 1; - /** Number of retries to establish a modbus connection with the heat pump */ - static const int ConnectionRetries = 3; - /** The following modbus addresses can be read: */ enum RegisterList { /** @@ -112,21 +106,13 @@ private: /** This structure is filled by the receivedStatus... functions */ MTecInfo m_info ; - int m_requestsSent = 0; - bool m_connected = false; - bool m_softConnection = false; - - QDateTime m_firstTimeout; - - int m_modbusTimeout; signals: - void connectedChanged(MTecHelpers::ConnectionState state); + void connectedChanged(bool connected); void statusUpdated(const MTecInfo &info); -public slots: +private slots: void onModbusError(); - void onRequestStatus(); void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); };