Update mtec plugin with network device discovery and internal connection mechanism

This commit is contained in:
Simon Stürz 2021-06-10 08:49:58 +02:00 committed by Michael Zanetti
parent 82dc46cd3b
commit 446b3e3dda
5 changed files with 189 additions and 247 deletions

View File

@ -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<Thing *, MTec *>::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<MTec *>(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<MTec *>(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);
}

View File

@ -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<Thing *, MTec *> m_mtecConnections;
void update(Thing *thing);
QHash<Thing *, MTec *> m_mtecConnections;
QHash<QUuid, ThingActionInfo *> m_asyncActions;
private slots:
void onConnectedChanged(MTecHelpers::ConnectionState state);
void onRefreshTimer();
void onStatusUpdated(const MTecInfo &info);

View File

@ -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": [
]
}
]
}
]

View File

@ -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<quint16> &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) {

View File

@ -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<quint16> &value);
};