From 352413563f1d8f431e8c1ef8e44ee6be8780690b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 8 Feb 2021 16:22:32 +0100 Subject: [PATCH 01/30] Use new modbus RTU hardware resource for the modbus commander plugin Use new modbus RTU hardware resource for the modbus commander plugin Fix action abort for RTU actions and fix connected state fro child RTU things Fix modbus rtu client and make it work with the basic resource component Remove modbusrtu master from project file Remove custom modbus rtu master include --- modbus/modbusrtumaster.cpp | 1 - .../integrationpluginmodbuscommander.cpp | 356 +++++++++--------- .../integrationpluginmodbuscommander.h | 7 +- .../integrationpluginmodbuscommander.json | 67 +--- modbuscommander/modbuscommander.pro | 6 +- 5 files changed, 196 insertions(+), 241 deletions(-) diff --git a/modbus/modbusrtumaster.cpp b/modbus/modbusrtumaster.cpp index 57a45af..d702c3b 100644 --- a/modbus/modbusrtumaster.cpp +++ b/modbus/modbusrtumaster.cpp @@ -54,7 +54,6 @@ ModbusRTUMaster::ModbusRTUMaster(QString serialPort, uint baudrate, QSerialPort: connect(m_reconnectTimer, &QTimer::timeout, this, &ModbusRTUMaster::onReconnectTimer); } - ModbusRTUMaster::~ModbusRTUMaster() { if (!m_modbusRtuSerialMaster) { diff --git a/modbuscommander/integrationpluginmodbuscommander.cpp b/modbuscommander/integrationpluginmodbuscommander.cpp index 0057354..cae1683 100644 --- a/modbuscommander/integrationpluginmodbuscommander.cpp +++ b/modbuscommander/integrationpluginmodbuscommander.cpp @@ -31,6 +31,10 @@ #include "integrationpluginmodbuscommander.h" #include "plugininfo.h" +#include "hardwaremanager.h" +#include "hardware/modbus/modbusrtumaster.h" +#include "hardware/modbus/modbusrtuhardwareresource.h" + #include IntegrationPluginModbusCommander::IntegrationPluginModbusCommander() @@ -39,8 +43,6 @@ IntegrationPluginModbusCommander::IntegrationPluginModbusCommander() void IntegrationPluginModbusCommander::init() { - connect(this, &IntegrationPluginModbusCommander::configValueChanged, this, &IntegrationPluginModbusCommander::onPluginConfigurationChanged); - m_slaveAddressParamTypeId.insert(coilThingClassId, coilThingSlaveAddressParamTypeId); m_slaveAddressParamTypeId.insert(inputRegisterThingClassId, inputRegisterThingSlaveAddressParamTypeId); m_slaveAddressParamTypeId.insert(discreteInputThingClassId, discreteInputThingSlaveAddressParamTypeId); @@ -62,46 +64,49 @@ void IntegrationPluginModbusCommander::init() m_valueStateTypeId.insert(inputRegisterThingClassId, inputRegisterValueStateTypeId); m_valueStateTypeId.insert(discreteInputThingClassId, discreteInputValueStateTypeId); m_valueStateTypeId.insert(holdingRegisterThingClassId, holdingRegisterValueStateTypeId); + + // Plugin configuration + connect(this, &IntegrationPluginModbusCommander::configValueChanged, this, &IntegrationPluginModbusCommander::onPluginConfigurationChanged); + + // Modbus RTU hardware resource + connect(hardwareManager()->modbusRtuResource(), &ModbusRtuHardwareResource::modbusRtuMasterRemoved, this, [=](const QUuid &modbusUuid){ + qCDebug(dcModbusCommander()) << "Modbus RTU master has been removed" << modbusUuid.toString(); + + // Check if there is any device using this resource + foreach (Thing *thing, m_modbusRtuMasters.keys()) { + if (m_modbusRtuMasters.value(thing)->modbusUuid() == modbusUuid) { + qCWarning(dcModbusCommander()) << "Hardware resource removed for" << thing << ". The thing will not be functional any more until a new resource has been configured for it."; + m_modbusRtuMasters.remove(thing); + thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false); + + // Set all child things disconnected + foreach (Thing *childThing, myThings()) { + if (childThing->parentId() == thing->id()) { + thing->setStateValue(m_connectedStateTypeId[childThing->thingClassId()], false); + } + } + } + } + }); } void IntegrationPluginModbusCommander::discoverThings(ThingDiscoveryInfo *info) { ThingClassId thingClassId = info->thingClassId(); if (thingClassId == modbusRTUClientThingClassId) { - Q_FOREACH(QSerialPortInfo port, QSerialPortInfo::availablePorts()) { - qCDebug(dcModbusCommander()) << "Found serial port:" << port.systemLocation() << "manufacturer" << port.manufacturer() << "description" << port.description() << "serial number" << port.serialNumber(); - if (port.isBusy()) { - qCDebug(dcModbusCommander()) << "Serial port ist busy, skipping."; - continue; + foreach (ModbusRtuMaster *modbusMaster, hardwareManager()->modbusRtuResource()->modbusRtuMasters()) { + qCDebug(dcModbusCommander()) << "Found RTU master resource" << modbusMaster; + if (modbusMaster->connected()) { + ParamList parameters; + ThingDescriptor thingDescriptor(thingClassId, "Modbus RTU master", modbusMaster->serialPort()); + parameters.append(Param(modbusRTUClientThingModbusMasterUuidParamTypeId, modbusMaster->modbusUuid())); + thingDescriptor.setParams(parameters); + info->addThingDescriptor(thingDescriptor); + } else { + qCWarning(dcModbusCommander()) << "Found configured resource" << modbusMaster << "but it is not connected. Skipping."; } - QString manufacturer = port.manufacturer(); - if (manufacturer.isEmpty()) { - manufacturer = "unknown"; - } - QString description = port.description()+" Manufacturer: "+port.manufacturer(); - ThingDescriptor thingDescriptor(thingClassId, "Modbus RTU interface", description); - ParamList parameters; - QString serialPort = port.systemLocation(); - QString serialnumber = port.serialNumber(); - if (serialnumber.isEmpty()) { - serialnumber = port.manufacturer()+QString::number(port.productIdentifier(), 16); - - } - qCDebug(dcModbusCommander()) << " - Serial number" << serialnumber; - Q_FOREACH (Thing *exisingThing, myThings().filterByParam(modbusRTUClientThingClassId)) { - thingDescriptor.setThingId(exisingThing->id()); - // Rediscovery is broken because of a missing unique device id - // This is a workaround and doesnt work if multiple uart converters are attached. - // ThingDiscoveryInfo may be extended to distinquish between discovery and rediscovery - break; - } - parameters.append(Param(modbusRTUClientThingSerialPortParamTypeId, serialPort)); - parameters.append(Param(modbusRTUClientThingSerialnumberParamTypeId, serialnumber)); - thingDescriptor.setParams(parameters); - info->addThingDescriptor(thingDescriptor); } - //FIXME missing info if it is a rediscovery info->finish(Thing::ThingErrorNoError); return; } else if (thingClassId == discreteInputThingClassId) { @@ -112,7 +117,7 @@ void IntegrationPluginModbusCommander::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptor(descriptor); } if (clientThing->thingClassId() == modbusRTUClientThingClassId) { - ThingDescriptor descriptor(thingClassId, "Discrete input", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingSerialPortParamTypeId).toString()); + ThingDescriptor descriptor(thingClassId, "Discrete input", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString()); descriptor.setParentId(clientThing->id()); info->addThingDescriptor(descriptor); } @@ -128,7 +133,7 @@ void IntegrationPluginModbusCommander::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptor(descriptor); } if (clientThing->thingClassId() == modbusRTUClientThingClassId) { - ThingDescriptor descriptor(thingClassId, "Coil", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingSerialPortParamTypeId).toString()); + ThingDescriptor descriptor(thingClassId, "Coil", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString()); descriptor.setParentId(clientThing->id()); info->addThingDescriptor(descriptor); } @@ -143,7 +148,7 @@ void IntegrationPluginModbusCommander::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptor(descriptor); } if (clientThing->thingClassId() == modbusRTUClientThingClassId) { - ThingDescriptor descriptor(thingClassId, "Holding register", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingSerialPortParamTypeId).toString()); + ThingDescriptor descriptor(thingClassId, "Holding register", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString()); descriptor.setParentId(clientThing->id()); info->addThingDescriptor(descriptor); } @@ -159,7 +164,7 @@ void IntegrationPluginModbusCommander::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptor(descriptor); } if (clientThing->thingClassId() == modbusRTUClientThingClassId) { - ThingDescriptor descriptor(thingClassId, "Input register", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingSerialPortParamTypeId).toString()); + ThingDescriptor descriptor(thingClassId, "Input register", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString()); descriptor.setParentId(clientThing->id()); info->addThingDescriptor(descriptor); } @@ -215,74 +220,51 @@ void IntegrationPluginModbusCommander::setupThing(ThingSetupInfo *info) } }); connect(thing, &Thing::settingChanged, thing, [thing, modbusTCPMaster] (const ParamTypeId ¶mTypeId, const QVariant &value) { - if (paramTypeId == modbusTCPClientSettingsNumberOfRetriesParamTypeId) { - qCDebug(dcModbusCommander()) << "Set number of retries" << thing->name() << value.toUInt(); - modbusTCPMaster->setNumberOfRetries(value.toUInt()); - } else if (paramTypeId == modbusTCPClientSettingsTimeoutParamTypeId) { - qCDebug(dcModbusCommander()) << "Set timeout " << thing->name() << value.toUInt(); - modbusTCPMaster->setTimeout(value.toUInt()); - } - }); + if (paramTypeId == modbusTCPClientSettingsNumberOfRetriesParamTypeId) { + qCDebug(dcModbusCommander()) << "Set number of retries" << thing->name() << value.toUInt(); + modbusTCPMaster->setNumberOfRetries(value.toUInt()); + } else if (paramTypeId == modbusTCPClientSettingsTimeoutParamTypeId) { + qCDebug(dcModbusCommander()) << "Set timeout " << thing->name() << value.toUInt(); + modbusTCPMaster->setTimeout(value.toUInt()); + } + }); modbusTCPMaster->connectDevice(); } else if (thing->thingClassId() == modbusRTUClientThingClassId) { + QUuid modbusUuid = thing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toUuid(); - QString serialPort = thing->paramValue(modbusRTUClientThingSerialPortParamTypeId).toString(); - uint baudrate = thing->paramValue(modbusRTUClientThingBaudRateParamTypeId).toUInt(); - uint stopBits = thing->paramValue(modbusRTUClientThingStopBitsParamTypeId).toUInt(); - uint dataBits = thing->paramValue(modbusRTUClientThingDataBitsParamTypeId).toUInt(); - uint numberOfRetries = thing->setting(modbusRTUClientSettingsNumberOfRetriesParamTypeId).toUInt(); - uint timeout = thing->setting(modbusRTUClientSettingsTimeoutParamTypeId).toUInt(); - QSerialPort::Parity parity = QSerialPort::Parity::NoParity; - QString parityString = thing->paramValue(modbusRTUClientThingParityParamTypeId).toString(); - if (parityString.contains("No")) { - parity = QSerialPort::Parity::NoParity; - } else if (parityString.contains("Even")) { - parity = QSerialPort::Parity::EvenParity; - } else if (parityString.contains("Odd")) { - parity = QSerialPort::Parity::OddParity; - } - qCDebug(dcModbusCommander()) << "Setting up RTU client" << thing->name(); - qCDebug(dcModbusCommander()) << " baud:" << baudrate; - qCDebug(dcModbusCommander()) << " stop bits:" << stopBits; - qCDebug(dcModbusCommander()) << " data bits:" << dataBits; - qCDebug(dcModbusCommander()) << " parity:" << parityString; - qCDebug(dcModbusCommander()) << " number of retries:" << numberOfRetries; - qCDebug(dcModbusCommander()) << " timeout:" << timeout; - - - if (m_modbusRTUMasters.contains(thing)) { - // In case of a rediscovery - m_modbusRTUMasters.take(thing)->deleteLater(); + if (!hardwareManager()->modbusRtuResource()->available()) { + qCWarning(dcModbusCommander()) << "Cannot set up thing" << thing << ". The modbus RTU hardware resource is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The modbus RTU hardware resource is not available")); + return; } - ModbusRTUMaster *modbusRTUMaster = new ModbusRTUMaster(serialPort, baudrate, parity, dataBits, stopBits, this); - modbusRTUMaster->setTimeout(timeout); - modbusRTUMaster->setNumberOfRetries(numberOfRetries); - connect(modbusRTUMaster, &ModbusRTUMaster::connectionStateChanged, this, &IntegrationPluginModbusCommander::onConnectionStateChanged); - connect(modbusRTUMaster, &ModbusRTUMaster::requestExecuted, this, &IntegrationPluginModbusCommander::onRequestExecuted); - connect(modbusRTUMaster, &ModbusRTUMaster::requestError, this, &IntegrationPluginModbusCommander::onRequestError); - connect(modbusRTUMaster, &ModbusRTUMaster::receivedCoil, this, &IntegrationPluginModbusCommander::onReceivedCoil); - connect(modbusRTUMaster, &ModbusRTUMaster::receivedDiscreteInput, this, &IntegrationPluginModbusCommander::onReceivedDiscreteInput); - connect(modbusRTUMaster, &ModbusRTUMaster::receivedHoldingRegister, this, &IntegrationPluginModbusCommander::onReceivedHoldingRegister); - connect(modbusRTUMaster, &ModbusRTUMaster::receivedInputRegister, this, &IntegrationPluginModbusCommander::onReceivedInputRegister); - connect(modbusRTUMaster, &ModbusRTUMaster::connectionStateChanged, info, [info, modbusRTUMaster, this] (bool connected) { - if (connected) { - info->finish(Thing::ThingErrorNoError); - m_modbusRTUMasters.insert(info->thing(), modbusRTUMaster); + if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(modbusUuid)) { + qCWarning(dcModbusCommander()) << "Cannot set up thing" << thing << ". The modbus RTU hardware resource" << modbusUuid.toString() << "does not exist any more. Reconfiguration required."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Configured modbus RTU master could not be found. Please reconfigure the client and assign a new valid modbus RTU master.")); + return; + } + + ModbusRtuMaster *modbusMaster = hardwareManager()->modbusRtuResource()->getModbusRtuMaster(modbusUuid); + qCDebug(dcModbusCommander()) << "Setting up" << thing << "using" << modbusMaster; + m_modbusRtuMasters.insert(thing, modbusMaster); + + connect(modbusMaster, &ModbusRtuMaster::connectedChanged, thing, [=](bool connected){ + qCDebug(dcModbusCommander()) << "Modbus RTU client" << modbusMaster << "connected changed" << connected; + thing->setStateValue(modbusRTUClientConnectedStateTypeId, connected); + + // Note: only set the connected state for the child things if disconnected. + // The child things will be evaluated upon read requests if the slave is connected or not. + if (!connected) { + foreach (Thing *childThing, myThings()) { + if (childThing->parentId() == thing->id()) { + thing->setStateValue(m_connectedStateTypeId[childThing->thingClassId()], connected); + } + } } }); - connect(thing, &Thing::settingChanged, thing, [thing, modbusRTUMaster] (const ParamTypeId ¶mTypeId, const QVariant &value) { - if (paramTypeId == modbusRTUClientSettingsNumberOfRetriesParamTypeId) { - qCDebug(dcModbusCommander()) << "Set number of retries" << thing->name() << value.toUInt(); - modbusRTUMaster->setNumberOfRetries(value.toUInt()); - } else if (paramTypeId == modbusRTUClientSettingsTimeoutParamTypeId) { - qCDebug(dcModbusCommander()) << "Set timeout " << thing->name() << value.toUInt(); - modbusRTUMaster->setTimeout(value.toUInt()); - } - }); - modbusRTUMaster->connectDevice(); + info->finish(Thing::ThingErrorNoError); } else if ((thing->thingClassId() == coilThingClassId) || (thing->thingClassId() == discreteInputThingClassId) || (thing->thingClassId() == holdingRegisterThingClassId) @@ -362,9 +344,6 @@ void IntegrationPluginModbusCommander::thingRemoved(Thing *thing) if (thing->thingClassId() == modbusTCPClientThingClassId) { ModbusTCPMaster *modbus = m_modbusTCPMasters.take(thing); modbus->deleteLater(); - } else if (thing->thingClassId() == modbusRTUClientThingClassId) { - ModbusRTUMaster *modbus = m_modbusRTUMasters.take(thing); - modbus->deleteLater(); } if (myThings().empty()) { @@ -392,12 +371,7 @@ void IntegrationPluginModbusCommander::onPluginConfigurationChanged(const ParamT void IntegrationPluginModbusCommander::onConnectionStateChanged(bool status) { auto modbus = sender(); - - if (m_modbusRTUMasters.values().contains(static_cast(modbus))) { - Thing *thing = m_modbusRTUMasters.key(static_cast(modbus)); - qCDebug(dcModbusCommander()) << "Connections state changed" << thing->name() << status; - thing->setStateValue(modbusRTUClientConnectedStateTypeId, status); - } else if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { + if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { Thing *thing = m_modbusTCPMasters.key(static_cast(modbus)); qCDebug(dcModbusCommander()) << "Connections state changed" << thing->name() << status; thing->setStateValue(modbusTCPClientConnectedStateTypeId, status); @@ -439,20 +413,7 @@ void IntegrationPluginModbusCommander::onRequestError(QUuid requestId, const QSt void IntegrationPluginModbusCommander::onReceivedCoil(quint32 slaveAddress, quint32 modbusRegister, const QVector &values) { auto modbus = sender(); - - if (m_modbusRTUMasters.values().contains(static_cast(modbus))) { - Thing *parent = m_modbusRTUMasters.key(static_cast(modbus)); - foreach (Thing *thing, myThings().filterByParentId(parent->id())) { - if (thing->thingClassId() == coilThingClassId) { - if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress) - && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) { - thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]); - thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); - return; - } - } - } - } else if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { + if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { Thing *parent = m_modbusTCPMasters.key(static_cast(modbus)); foreach (Thing *thing, myThings().filterByParentId(parent->id())) { if (thing->thingClassId() == coilThingClassId) { @@ -471,19 +432,7 @@ void IntegrationPluginModbusCommander::onReceivedDiscreteInput(quint32 slaveAddr { auto modbus = sender(); - if (m_modbusRTUMasters.values().contains(static_cast(modbus))) { - Thing *parent = m_modbusRTUMasters.key(static_cast(modbus)); - foreach (Thing *thing, myThings().filterByParentId(parent->id())) { - if (thing->thingClassId() == discreteInputThingClassId) { - if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress) - && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) { - thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]); - thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); - return; - } - } - } - } else if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { + if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { Thing *parent = m_modbusTCPMasters.key(static_cast(modbus)); foreach (Thing *thing, myThings().filterByParentId(parent->id())) { if (thing->thingClassId() == discreteInputThingClassId) { @@ -502,19 +451,7 @@ void IntegrationPluginModbusCommander::onReceivedHoldingRegister(uint slaveAddre { auto modbus = sender(); - if (m_modbusRTUMasters.values().contains(static_cast(modbus))) { - Thing *parent = m_modbusRTUMasters.key(static_cast(modbus)); - foreach (Thing *thing, myThings().filterByParentId(parent->id())) { - if (thing->thingClassId() == holdingRegisterThingClassId) { - if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress) - && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) { - thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]); - thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); - return; - } - } - } - } else if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { + if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { Thing *parent = m_modbusTCPMasters.key(static_cast(modbus)); foreach (Thing *thing, myThings().filterByParentId(parent->id())) { if (thing->thingClassId() == holdingRegisterThingClassId) { @@ -533,19 +470,7 @@ void IntegrationPluginModbusCommander::onReceivedInputRegister(uint slaveAddress { auto modbus = sender(); - if (m_modbusRTUMasters.values().contains(static_cast(modbus))) { - Thing *parent = m_modbusRTUMasters.key(static_cast(modbus)); - foreach (Thing *thing, myThings().filterByParentId(parent->id())) { - if (thing->thingClassId() == inputRegisterThingClassId) { - if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress) - && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) { - thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]); - thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); - return; - } - } - } - } else if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { + if (m_modbusTCPMasters.values().contains(static_cast(modbus))) { Thing *parent = m_modbusTCPMasters.key(static_cast(modbus)); foreach (Thing *thing, myThings().filterByParentId(parent->id())) { if (thing->thingClassId() == inputRegisterThingClassId) { @@ -592,23 +517,76 @@ void IntegrationPluginModbusCommander::readRegister(Thing *thing) } } else if (parent->thingClassId() == modbusRTUClientThingClassId) { - ModbusRTUMaster *modbus = m_modbusRTUMasters.value(parent); - if (!modbus) + ModbusRtuMaster *modbusMaster = m_modbusRtuMasters.value(parent); + if (!modbusMaster) return; - if (!modbus->connected()) + if (!modbusMaster->connected()) return; // Send requests only if the modbus interface is connected if (thing->thingClassId() == coilThingClassId) { - requestId = modbus->readCoil(slaveAddress, registerAddress); + ModbusRtuReply *reply = modbusMaster->readCoil(slaveAddress, registerAddress); + connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcModbusCommander()) << "Failed to read coil from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress; + thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false); + return; + } + + if (!reply->result().isEmpty()) { + thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0)); + } + thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); + }); } else if (thing->thingClassId() == discreteInputThingClassId) { - requestId = modbus->readDiscreteInput(slaveAddress, registerAddress); + ModbusRtuReply *reply = modbusMaster->readDiscreteInput(slaveAddress, registerAddress); + connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcModbusCommander()) << "Failed to read discrete input from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress; + thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false); + return; + } + + if (!reply->result().isEmpty()) { + thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0)); + } + thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); + }); } else if (thing->thingClassId() == holdingRegisterThingClassId) { - requestId = modbus->readHoldingRegister(slaveAddress, registerAddress); + ModbusRtuReply *reply = modbusMaster->readHoldingRegister(slaveAddress, registerAddress); + connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcModbusCommander()) << "Failed to read holding register from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress; + thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false); + return; + } + + if (!reply->result().isEmpty()) { + thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0)); + } + thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); + }); } else if (thing->thingClassId() == inputRegisterThingClassId) { - requestId = modbus->readInputRegister(slaveAddress, registerAddress); + ModbusRtuReply *reply = modbusMaster->readInputRegister(slaveAddress, registerAddress); + connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcModbusCommander()) << "Failed to read input register from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress; + thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false); + return; + } + + if (!reply->result().isEmpty()) { + thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0)); + } + thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true); + }); } + + // Note: we don't want proceed with the method here, since we are not + // working with the requestId any more on RTU + return; } + if (!requestId.isNull()) { m_readRequests.insert(requestId, thing); QTimer::singleShot(5000, this, [requestId, this] {m_readRequests.remove(requestId);}); @@ -623,8 +601,10 @@ void IntegrationPluginModbusCommander::writeRegister(Thing *thing, ThingActionIn Thing *parent = myThings().findById(thing->parentId()); if (!parent) { qCWarning(dcModbusCommander()) << "Could not find parent device" << thing->name(); + info->finish(Thing::ThingErrorHardwareNotAvailable); return; } + uint registerAddress = thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())).toUInt();; uint slaveAddress = thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())).toUInt(); @@ -633,8 +613,11 @@ void IntegrationPluginModbusCommander::writeRegister(Thing *thing, ThingActionIn if (parent->thingClassId() == modbusTCPClientThingClassId) { ModbusTCPMaster *modbus = m_modbusTCPMasters.value(parent); - if (!modbus) + if (!modbus) { + qCWarning(dcModbusCommander()) << "Could not find modbus TCP master for" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); return; + } if (thing->thingClassId() == coilThingClassId) { requestId = modbus->writeCoil(slaveAddress, registerAddress, action.param(coilValueActionValueParamTypeId).value().toBool()); @@ -643,15 +626,48 @@ void IntegrationPluginModbusCommander::writeRegister(Thing *thing, ThingActionIn } } else if (parent->thingClassId() == modbusRTUClientThingClassId) { - ModbusRTUMaster *modbus = m_modbusRTUMasters.value(parent); - if (!modbus) + ModbusRtuMaster *modbusMaster = m_modbusRtuMasters.value(parent); + if (!modbusMaster) { + qCWarning(dcModbusCommander()) << "Could not find modbus RTU master for" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); return; + } if (thing->thingClassId() == coilThingClassId) { - requestId = modbus->writeCoil(slaveAddress, registerAddress, action.param(coilValueActionValueParamTypeId).value().toBool()); + QVector values; + values.append(static_cast(action.param(coilValueActionValueParamTypeId).value().toBool())); + + ModbusRtuReply *reply = modbusMaster->writeCoils(slaveAddress, registerAddress, values); + connect(info, &ThingActionInfo::aborted, reply, &ModbusRtuReply::deleteLater); + connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcModbusCommander()) << "Failed to write coils from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress << values << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + info->finish(Thing::ThingErrorNoError); + }); } else if (thing->thingClassId() == holdingRegisterThingClassId) { - requestId = modbus->writeHoldingRegister(slaveAddress, registerAddress, action.param(holdingRegisterValueActionValueParamTypeId).value().toUInt()); + QVector values; + values.append(static_cast(action.param(holdingRegisterValueActionValueParamTypeId).value().toUInt())); + + ModbusRtuReply *reply = modbusMaster->writeHoldingRegisters(slaveAddress, registerAddress, values); + connect(info, &ThingActionInfo::aborted, reply, &ModbusRtuReply::deleteLater); + connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){ + if (reply->error() != ModbusRtuReply::NoError) { + qCWarning(dcModbusCommander()) << "Failed to write holding registers from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress << values << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + info->finish(Thing::ThingErrorNoError); + }); } + + // Note: we don't want proceed with the method here, since we are not + // working with the requestId any more on RTU + return; } if (requestId.toString().isNull()){ diff --git a/modbuscommander/integrationpluginmodbuscommander.h b/modbuscommander/integrationpluginmodbuscommander.h index 4c7b5c0..54f6906 100644 --- a/modbuscommander/integrationpluginmodbuscommander.h +++ b/modbuscommander/integrationpluginmodbuscommander.h @@ -31,11 +31,11 @@ #ifndef INTEGRATIONPLUGINMODBUSCOMMANDER_H #define INTEGRATIONPLUGINMODBUSCOMMANDER_H -#include "integrations/integrationplugin.h" #include "plugintimer.h" +#include "integrations/integrationplugin.h" +#include "hardware/modbus/modbusrtumaster.h" #include "../modbus/modbustcpmaster.h" -#include "../modbus/modbusrtumaster.h" #include #include @@ -60,8 +60,9 @@ public: private: PluginTimer *m_refreshTimer = nullptr; - QHash m_modbusRTUMasters; + //QHash m_modbusRTUMasters; QHash m_modbusTCPMasters; + QHash m_modbusRtuMasters; QHash m_asyncActions; QHash m_readRequests; diff --git a/modbuscommander/integrationpluginmodbuscommander.json b/modbuscommander/integrationpluginmodbuscommander.json index 15bfd37..14f3162 100644 --- a/modbuscommander/integrationpluginmodbuscommander.json +++ b/modbuscommander/integrationpluginmodbuscommander.json @@ -72,74 +72,15 @@ "id": "776df314-6186-4eb5-b824-f0d916f6d9c3", "name": "modbusRTUClient", "displayName": "Modbus RTU client", - "createMethods": ["discovery", "user"], + "createMethods": ["discovery"], "interfaces": ["connectable"], - "settingsTypes": [ - { - "id": "b0af32f0-b8cc-4642-af5a-576732522b2c", - "name": "timeout", - "displayName": "Timeout", - "type": "uint", - "minValue": 10, - "defaultValue": 100 - }, - { - "id": "c4f16d6c-c1f2-4862-b0bd-6fae7193eaa8", - "name": "numberOfRetries", - "displayName": "Number of retries", - "type": "uint", - "defaultValue": 3 - } - ], "paramTypes": [ { "id": "ed49f7d8-ab18-4c37-9b80-1004b75dcb91", - "name": "serialPort", - "displayName": "Serial port", - "type": "QString", - "inputType": "TextLine", - "defaultValue": "ttyAMA0" - }, - { - "id": "9908b01f-a76b-4b21-8242-b507c9252254", - "name": "serialnumber", - "displayName": "Serial number", - "type": "QString", + "name": "modbusMasterUuid", + "displayName": "Modbus RTU master", + "type": "QUuid", "defaultValue": "" - }, - { - "id": "45dfc828-f238-4263-89a3-9b35cf5dea39", - "name": "baudRate", - "displayName": "Baud rate", - "type": "uint", - "defaultValue": 9600 - }, - { - "id": "a27c664b-9f43-4573-a2cc-f65a8fa1a069", - "name": "dataBits", - "displayName": "Data bits", - "type": "uint", - "defaultValue": 8 - }, - { - "id": "4ea8bcdf-d4c5-45a4-a54f-f10ac3f08a78", - "name": "stopBits", - "displayName": "Stop bits", - "type": "uint", - "defaultValue": 1 - }, - { - "id": "72de1b08-2a27-49c5-90e0-8788c3ea1da3", - "name": "parity", - "displayName": "Parity", - "type": "QString", - "inputType": "TextLine", - "allowedValues": [ - "No Parity", - "Even Parity", - "Odd Parity" - ], - "defaultValue": "No Parity" } ], "stateTypes": [ diff --git a/modbuscommander/modbuscommander.pro b/modbuscommander/modbuscommander.pro index 9c7a28e..ab4dfc1 100644 --- a/modbuscommander/modbuscommander.pro +++ b/modbuscommander/modbuscommander.pro @@ -7,11 +7,9 @@ QT += \ SOURCES += \ integrationpluginmodbuscommander.cpp \ - ../modbus/modbustcpmaster.cpp \ - ../modbus/modbusrtumaster.cpp \ + ../modbus/modbustcpmaster.cpp HEADERS += \ integrationpluginmodbuscommander.h \ - ../modbus/modbustcpmaster.h \ - ../modbus/modbusrtumaster.h \ + ../modbus/modbustcpmaster.h From 36803368b3923906ad9f960789d135a264bff56b Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Tue, 11 Aug 2020 13:01:34 +0200 Subject: [PATCH 02/30] added idm plugin --- debian/control | 16 +++ debian/nymea-plugin-idm.install.in | 1 + idm/README.md | 15 +++ idm/idm.cpp | 43 +++++++ idm/idm.h | 105 +++++++++++++++++ idm/idm.pro | 15 +++ idm/integrationpluginidm.cpp | 80 +++++++++++++ idm/integrationpluginidm.h | 97 ++++++++++++++++ idm/integrationpluginidm.json | 180 +++++++++++++++++++++++++++++ idm/meta.json | 13 +++ nymea-plugins-modbus.pro | 1 + 11 files changed, 566 insertions(+) create mode 100644 debian/nymea-plugin-idm.install.in create mode 100644 idm/README.md create mode 100644 idm/idm.cpp create mode 100644 idm/idm.h create mode 100644 idm/idm.pro create mode 100644 idm/integrationpluginidm.cpp create mode 100644 idm/integrationpluginidm.h create mode 100644 idm/integrationpluginidm.json create mode 100644 idm/meta.json diff --git a/debian/control b/debian/control index 35e0a9e..0e628d8 100644 --- a/debian/control +++ b/debian/control @@ -29,6 +29,22 @@ Description: nymea.io plugin for Drexel & Weiss heat pumps This package will install the nymea.io plugin for Drexel & Weiss heat pumps +Package: nymea-plugin-idm +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-modbus-translations, +Description: nymea.io plugin for iDM heat pumps + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for iDM heat pumps + + Package: nymea-plugin-modbuscommander Architecture: any Section: libs diff --git a/debian/nymea-plugin-idm.install.in b/debian/nymea-plugin-idm.install.in new file mode 100644 index 0000000..5ef2f03 --- /dev/null +++ b/debian/nymea-plugin-idm.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginidm.so diff --git a/idm/README.md b/idm/README.md new file mode 100644 index 0000000..74a43f4 --- /dev/null +++ b/idm/README.md @@ -0,0 +1,15 @@ +# iDM + +## Supported Things + +## More + +https://www.idm-energie.at/en/ + +** Modbus TCP communication not working ** + * Is "Modbus TCP" selected in the "Building Management System" menu? + * Is the Modbus TCP device and the heat pump in the same network? + * Is there an IP address conflict? + * Has the heat pump set the IP address manually? IP address should be set manually, because with "DHCP" the IP address can change (e.g. after a power failure). + * Was the connection made via a switch, possibly blocking this communication? If so, integrate the Modbus TCP device directly (without a switch). + diff --git a/idm/idm.cpp b/idm/idm.cpp new file mode 100644 index 0000000..2fcbc81 --- /dev/null +++ b/idm/idm.cpp @@ -0,0 +1,43 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 "idm.h" + +Idm::Idm(const QHostAddress &address, QObject *parent) : + QObject(parent), + m_hostAddress(address) +{ + m_modbusMaster = new ModbusTCPMaster(address, 502, this); +} + +void Idm::getOutsideTemperature() +{ + m_modbusMaster-> +} diff --git a/idm/idm.h b/idm/idm.h new file mode 100644 index 0000000..4c6fbd2 --- /dev/null +++ b/idm/idm.h @@ -0,0 +1,105 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 IDM_H +#define IDM_H + +#include + +#include "../modbus/modbustcpmaster.h" + +class Idm : public QObject +{ + Q_OBJECT +public: + explicit Idm(const QHostAddress &address, QObject *parent = nullptr); + void getOutsideTemperature(); + void getCurrentFaultNumber(); + + +private: + + enum IscModus { + KeineAbwarme = 0, + Heizung = 1, + Warmwasser = 4, + Warmequelle = 8, + }; + + enum RegisterList { + OutsideTemperature = 1000, + MeanOutsideTemperature = 1002, + CurrentFaultNumber = 1004, + OperationModeSystem = 1005, + SmartGridStatus = 1006, + HeatStorageTemperature = 1008, + ColdStorageTemperature = 1010, + DrinkingWaterHeaterTempBelow = 1012, + DrinkingWaterHeaterTempAbove = 1014, + HotWaterTapTemperature = 1030, + TargetHotWaterTemperature = 1032, + HeatPumpOperatingMode = 1090, + SummationFaultHeatPump = 1099, + Humiditysensor = 1392, + ExternalOutsideTemperature = 1690, + ExternalHumidity = 1692, + ExternalRequestTemperatureHeating = 1694, //Externe Anforderungstemperatur Heizen + ExternalRequestTemperatureCooling = 1695, // Externe Anforderungstemperatur Kühlen + HeatingRequirement = 1710 + CoolingRequirement = 1711, + HotWaterChargingRequirement = 1712, // Anforderung Warmwasserladung + //Wärmemenge Heizen + //Wärmemenge Kühlen, + //Wärmemenge Warmwasser, + //Wärmemenge Abtauung, + //Wärmemenge Passive Kühlung, + //Wärmemenge Solar, + //Wärmemenge Elektroheizeinsatz, + Momentanleistung + SolarKollektortemperatur + SolarKollektorrücklauftemperatur + SolarLadetemperatur + MomentanleistungSolar, + SolarOperatingMode = + ISCModus = 1874, + AcknowledgeFaultMessages = 1999, // Störmeldungen quittieren + Aktueller PV-Überschuss + Aktueller PV Produktion + Aktuelle Leistungsaufnahme Wärmepumpe + }; + + QHostAddress m_hostAddress; + ModbusTCPMaster *m_modbusMaster = nullptr; + +signals: + +}; + +#endif // IDM_H diff --git a/idm/idm.pro b/idm/idm.pro new file mode 100644 index 0000000..bc0cf39 --- /dev/null +++ b/idm/idm.pro @@ -0,0 +1,15 @@ +include(../plugins.pri) + +QT += \ + network \ + serialbus \ + +SOURCES += \ + idm.cpp \ + integrationpluginidm.cpp \ + ../modbus/modbustcpmaster.cpp \ + +HEADERS += \ + idm.h \ + integrationpluginidm.h \ + ../modbus/modbustcpmaster.h \ diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp new file mode 100644 index 0000000..af78481 --- /dev/null +++ b/idm/integrationpluginidm.cpp @@ -0,0 +1,80 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 "integrationpluginidm.h" +#include "plugininfo.h" + +void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) +{ + if (info->thingClassId() == navigator2ThingClassId) { + //TODO add discovery method + } +} + +void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == navigator2ThingClassId) { + QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); + Idm *idm = new Idm(hostAddress, this); + m_idmConnections.insert(thing, idm); + } + +} + +void IntegrationPluginIdm::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == navigator2ThingClassId) { + Idm *idm = m_idmConnections.value(thing); + + } +} + +void IntegrationPluginIdm::thingRemoved(Thing *thing) +{ + if (m_idmConnections.contains(thing)) + m_idmConnections.take(thing)->deleteLater(); +} + +void IntegrationPluginIdm::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == navigator2ThingClassId) { + if (action.actionTypeId() == navigator2PowerActionTypeId) { + } 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()); + } +} diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h new file mode 100644 index 0000000..58d2910 --- /dev/null +++ b/idm/integrationpluginidm.h @@ -0,0 +1,97 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 INTEGRATIONPLUGINIDM_H +#define INTEGRATIONPLUGINIDM_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" + +#include "idm.h" + +#include + +class IntegrationPluginIdm: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginidm.json") + Q_INTERFACES(IntegrationPlugin) + + +public: + explicit IntegrationPluginIdm(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + + enum IdmSysMode { + IdmSysModeStandby = 0, + IdmSysModeAutomatic, + IdmSysModeAway, + IdmSysModeOnlyWarmwater, + IdmSysModeOnlyRoomHeating + }; + + enum IdmSmartGridMode { + EVUSperreKeinPVErtrag, + EVUBezugKeinPVErtrag, + KeinEVUBezugPVErtrag, + EVUSperrePVErtrag + }; + + enum IdmStatus { + Heating = 2, + Standby = 3, + Boosted = 4, + HeatFinished = 5, + Setup = 9, + ErrorOvertempFuseBlown = 201, + ErrorOvertempMeasured = 202, + ErrorOvertempElectronics = 203, + ErrorHardwareFault = 204, + ErrorTempSensor = 205 + }; + + PluginTimer *m_refreshTimer = nullptr; + QHash m_idmConnections; + QHash m_asyncActions; + +private slots: + void onRefreshTimer(); +}; + +#endif // INTEGRATIONPLUGINIDM_H + diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json new file mode 100644 index 0000000..0bb2721 --- /dev/null +++ b/idm/integrationpluginidm.json @@ -0,0 +1,180 @@ +{ + "name": "Idm", + "displayName": "iDM", + "id": "3968d86d-d51a-4ad1-a185-91faa017e38f", + "vendors": [ + { + "name": "Idm", + "displayName": "iDM", + "id": "6f54e4b0-1057-4004-87a9-97fdf4581625", + "thingClasses": [ + { + "name": "navigator2", + "displayName": "Navigator 2.0", + "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", + "createMethods": ["Discovery"], + "interfaces": ["heating", "temperaturesensor", "connectable"], + "paramTypes": [ + { + "id": "05714e5c-d66a-4095-bbff-a0eb96fb035b", + "name":"ipAddress", + "displayName": "IP address", + "type": "QString" + } + ], + "stateTypes":[ + { + "id": "cfd71e64-b666-45ef-8db0-8213acd82c5f", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "33c27167-8e24-4cc5-943c-d17cd03e0f68", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Change power", + "type": "bool", + "defaultValue": 0, + "writable": true + }, + { + "id": "f0f596bf-7e45-43ea-b3d4-767b82dd422a", + "name": "temperature", + "displayName": "Room temperature", + "displayNameEvent": "Room temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0 + }, + { + "id": "fcf8e97f-a672-407f-94ae-30df15b310f4", + "name": "waterTemperature", + "displayName": "Water temperature", + "displayNameEvent": "Water temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0 + }, + { + "id": "9f3462c2-7c42-4eeb-afc4-092e1e41a25d", + "name": "outsideAirTemperature", + "displayName": "Outside air temperature", + "displayNameEvent": "Outside air temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0 + }, + { + "id": "efae7493-68c3-4cb9-853c-81011bdf09ca", + "name": "targetTemperature", + "displayName": "Target room temperature", + "displayNameEvent": "Target room temperature changed", + "displayNameAction": "Change room target temperature", + "type": "double", + "unit": "DegreeCelsius", + "minValue": 14.00, + "maxValue": 26.00, + "defaultValue": 22.00, + "writable": true + }, + { + "id": "746244d6-dd37-4af8-b2ae-a7d8463e51e2", + "name": "targetWaterTemperature", + "displayName": "Target water temperature", + "displayNameEvent": "Target water temperature changed", + "displayNameAction": "Change water target temperature", + "type": "double", + "unit": "DegreeCelsius", + "minValue": 20.00, + "maxValue": 55.00, + "defaultValue": 46.00, + "writable": true + }, + { + "id": "e539366b-44da-4119-b11b-497bcdb1f522", + "name": "mode", + "displayName": "Mode", + "displayNameEvent": "Mode changed", + "type": "QString", + "defaultValue": "Off", + "possibleValues": [ + "Off", + "Heating", + "Cooling", + "Hot water", + "Defrost" + ] + }, + { + "id": "49fd83ee-ddf3-4477-9ee4-e01c53283b43", + "name": "error", + "displayName": "Error", + "displayNameEvent": "Error changed", + "type": "bool", + "defaultValue": false + } + ], + "actionTypes": [ + { + "id": "29b65c13-46e9-49b1-970a-68252bdfeadc", + "name": "externTemperature", + "displayName": "Extern temperature", + "paramTypes" : [ + { + "id": "d60fcb0c-19b5-4cac-9b95-a1b414518385", + "name": "temperature", + "displayName": "Temperature", + "type": "double", + "defaultValue": 0, + "unit": "DegreeCelsius" + } + ] + }, + { + "id": "046f2e72-899a-4d82-91d3-fd268c784a1c", + "name": "externHumidity", + "displayName": "Extern humidity", + "paramTypes" : [ + { + "id": "034b9a8c-1a5a-45da-8422-393273d0a159", + "name": "humidity", + "displayName": "humidity", + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100, + "unit": "Percentage" + } + ] + }, + { + "id": "87633d1f-3826-4bf0-9a2c-46a927446eb5", + "name": "pvEnergy", + "displayName": "Set avilable PV Energy", + "paramTypes" : [ + { + "id": "84b251ab-33b5-45e5-9a5c-468e3affe821", + "name": "energy", + "displayName": "Energy", + "type": "double", + "defaultValue": 0.00, + "minValue": 0.00, + "unit": "KiloWatt" + } + ] + } + ] + } + ] + } + ] +} + + + + diff --git a/idm/meta.json b/idm/meta.json new file mode 100644 index 0000000..71b2812 --- /dev/null +++ b/idm/meta.json @@ -0,0 +1,13 @@ +{ + "title": "iDM", + "tagline": "Control iDM network enabled heat pumps.", + "icon": "idm.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "heating" + ] +} diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index 694d2c8..207d8bc 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -6,6 +6,7 @@ PLUGIN_DIRS = \ mypv \ sunspec \ unipi \ + idm \ wallbe \ webasto \ From d59a1407a805fa38948a18b23a2299b6c2576322 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Tue, 22 Sep 2020 15:22:57 +0200 Subject: [PATCH 03/30] added idm logo --- idm/idm.png | Bin 0 -> 72524 bytes idm/integrationpluginidm.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 idm/idm.png diff --git a/idm/idm.png b/idm/idm.png new file mode 100644 index 0000000000000000000000000000000000000000..1306b1fd538ec7f7c4e816a9f0dcc15c3445a174 GIT binary patch literal 72524 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 literal 0 HcmV?d00001 diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 0bb2721..7c6593b 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -155,7 +155,7 @@ { "id": "87633d1f-3826-4bf0-9a2c-46a927446eb5", "name": "pvEnergy", - "displayName": "Set avilable PV Energy", + "displayName": "Set available PV Energy", "paramTypes" : [ { "id": "84b251ab-33b5-45e5-9a5c-468e3affe821", From 534322b534ba7bbef39473ec0ae7d6964d65e48a Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Mon, 28 Sep 2020 20:01:22 +0200 Subject: [PATCH 04/30] Makeshift version to debug nymea app (issue #443) Furthermore: Modbus registers defined according to heat pump manual (RegisterList in idm.h) The plugin presently just delivers a hard-coded value of 24.5 for the outside temperature for testing. --- idm/idm.cpp | 39 +++++++++++++- idm/idm.h | 96 +++++++++++++++++++++-------------- idm/idm.pro | 1 + idm/idminfo.h | 49 ++++++++++++++++++ idm/integrationpluginidm.cpp | 89 +++++++++++++++++++++++++++++++- idm/integrationpluginidm.h | 7 ++- idm/integrationpluginidm.json | 2 +- 7 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 idm/idminfo.h diff --git a/idm/idm.cpp b/idm/idm.cpp index 2fcbc81..2acb9ee 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -34,10 +34,45 @@ Idm::Idm(const QHostAddress &address, QObject *parent) : QObject(parent), m_hostAddress(address) { + /* qCDebug(dcIdm()) << "Creating Idm with addr: " << address; */ + m_modbusMaster = new ModbusTCPMaster(address, 502, this); + + connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); } -void Idm::getOutsideTemperature() +Idm::~Idm() { - m_modbusMaster-> + if (m_modbusMaster) { + delete m_modbusMaster; + } } + +double Idm::getOutsideTemperature() +{ + //m_modbusMaster-> + return 0.0; +} + +void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) +{ + /* qCDebug(dcIdm()) << "Idm::onReceivedHoldingRegister"; */ + + Q_UNUSED(slaveAddress); + Q_UNUSED(modbusRegister); + Q_UNUSED(value); + + IdmInfo *info = new IdmInfo; + info->m_outsideTemperature = 24.3; + + emit statusUpdated(info); +} + +void Idm::onRequestStatus() +{ + /* Reading a total of 16 bytes, starting from address 1000. + * This covers the following parameters: + * */ + m_modbusMaster->readHoldingRegister(0xff, RegisterList::OutsideTemperature, 16); +} + diff --git a/idm/idm.h b/idm/idm.h index 4c6fbd2..0792fff 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -35,13 +35,16 @@ #include "../modbus/modbustcpmaster.h" +#include "idminfo.h" + class Idm : public QObject { Q_OBJECT public: explicit Idm(const QHostAddress &address, QObject *parent = nullptr); - void getOutsideTemperature(); - void getCurrentFaultNumber(); + ~Idm(); + double getOutsideTemperature(); + //void getCurrentFaultNumber(); private: @@ -54,51 +57,66 @@ private: }; enum RegisterList { - OutsideTemperature = 1000, - MeanOutsideTemperature = 1002, - CurrentFaultNumber = 1004, - OperationModeSystem = 1005, - SmartGridStatus = 1006, - HeatStorageTemperature = 1008, - ColdStorageTemperature = 1010, - DrinkingWaterHeaterTempBelow = 1012, - DrinkingWaterHeaterTempAbove = 1014, - HotWaterTapTemperature = 1030, - TargetHotWaterTemperature = 1032, - HeatPumpOperatingMode = 1090, - SummationFaultHeatPump = 1099, - Humiditysensor = 1392, - ExternalOutsideTemperature = 1690, - ExternalHumidity = 1692, - ExternalRequestTemperatureHeating = 1694, //Externe Anforderungstemperatur Heizen + /* The following modbus addresses are according to the manual + * Modbus TCP Navigatorregelung 2.0 pages 13-31. + * Comments at the end of each line give their original name + * in the German manual. */ + OutsideTemperature = 1000, // Außentemperatur (B31) + MeanOutsideTemperature = 1002, // Gemittelte Außentemperature + CurrentFaultNumber = 1004, // Aktuelle Störungsnummer + OperationModeSystem = 1005, // Betriebsart System + SmartGridStatus = 1006, // Smart Grid Status + HeatStorageTemperature = 1008, // Wärmespeichertemperatur (B38) + ColdStorageTemperature = 1010, // Kältespeichertemperatur (B40) + DrinkingWaterHeaterTempBottom = 1012, // Trinkwassererwärmertmp. unten (B41) + DrinkingWaterHeaterTempTop = 1014, // Trinkwassererwärmertmp. oben (B48) + HotWaterTapTemperature = 1030, // Warmwasserzapftemperatur (B42) + TargetHotWaterTemperature = 1032, // Warmwasser-Solltemperatur + HeatPumpOperatingMode = 1090, // Betriebsart Wärmepumpe + SummationFaultHeatPump = 1099, // Summenstörung Wärepumpe + Humiditysensor = 1392, // Feuchtesensor + ExternalOutsideTemperature = 1690, // Externe Außentemperatur + ExternalHumidity = 1692, // Externe Feuchte + ExternalRequestTemperatureHeating = 1694, // Externe Anforderungstemperatur Heizen ExternalRequestTemperatureCooling = 1695, // Externe Anforderungstemperatur Kühlen - HeatingRequirement = 1710 - CoolingRequirement = 1711, + HeatingRequirement = 1710, // Anforderung Heizen + CoolingRequirement = 1711, // Anforderung Kühlen HotWaterChargingRequirement = 1712, // Anforderung Warmwasserladung - //Wärmemenge Heizen - //Wärmemenge Kühlen, - //Wärmemenge Warmwasser, - //Wärmemenge Abtauung, - //Wärmemenge Passive Kühlung, - //Wärmemenge Solar, - //Wärmemenge Elektroheizeinsatz, - Momentanleistung - SolarKollektortemperatur - SolarKollektorrücklauftemperatur - SolarLadetemperatur - MomentanleistungSolar, - SolarOperatingMode = - ISCModus = 1874, - AcknowledgeFaultMessages = 1999, // Störmeldungen quittieren - Aktueller PV-Überschuss - Aktueller PV Produktion - Aktuelle Leistungsaufnahme Wärmepumpe + HeatQuantityHeating = 1750, // Wärmemenge Heizen + HeatQuantityCooling = 1752, // Wärmemenge Kühlen + HeatQuantityHotWater = 1754, // Wärmemenge Warmwasser + HeatQuantityDefrosting = 1756, // Wärmemenge Abtauung + HeatQuantityPassiveCooling = 1758, // Wärmemenge Passive Kühlung, + HeatQuantityPhotovolatics = 1760, // Wärmemenge Solar + HeatQuantityHeatingElemetn = 1762, // Wärmemenge Elektroheizeinsatz, + CurrentPower = 1790, // Momentanleistung + CurrentPowerSolar = 1792, // MomentanleistungSolar + SolarCollectorTemperature = 1850, // SolarKollektortemperatur (B73) + SolarCollectorReturnTemperature = 1852, // SolarKollektorruecklauftemperatur (B75) + SolarChargeTemperature = 1854, // SolarLadetemperatur (B74) + SolarOperatingMode = 1856, // Betriebsart Solar + ISCMode = 1875, // ISCModus + AcknowledgeFaultMessages = 1999, // Störmeldungen quittieren + CurrentPhotovoltaicsSurplus = 74, // Aktueller PV-Überschuss + CurrentPhotovoltaicsProduction = 78, // Aktueller PV Produktion + CurrentPowerConsumption = 4122, // Aktuelle Leistungsaufnahme Wärmepumpe }; + /* Note: This class only requires one IP address and one + * TCP Modbus connection. Multiple devices are managed + * within the IntegrationPluginIdm class. */ QHostAddress m_hostAddress; ModbusTCPMaster *m_modbusMaster = nullptr; signals: + void statusUpdated(IdmInfo *info); + +private slots: + void onRequestStatus(); + +// only public for debugging! +public slots: + void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); }; diff --git a/idm/idm.pro b/idm/idm.pro index bc0cf39..63867ed 100644 --- a/idm/idm.pro +++ b/idm/idm.pro @@ -11,5 +11,6 @@ SOURCES += \ HEADERS += \ idm.h \ + idminfo.h \ integrationpluginidm.h \ ../modbus/modbustcpmaster.h \ diff --git a/idm/idminfo.h b/idm/idminfo.h new file mode 100644 index 0000000..115c49b --- /dev/null +++ b/idm/idminfo.h @@ -0,0 +1,49 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 IDMINFO_H +#define IDMINFO_H + +#include + +struct IdmInfo { + bool m_connected; + bool m_power; + double m_roomTemperature; + double m_outsideTemperature; + double m_waterTemperature; + double m_targetRoomTemperature; + double m_targetWaterTemperature; + QString m_mode; + bool m_error; +}; + +#endif + diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index af78481..cd3e113 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -31,10 +31,24 @@ #include "integrationpluginidm.h" #include "plugininfo.h" +IntegrationPluginIdm::IntegrationPluginIdm() +{ + +} + void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == navigator2ThingClassId) { - //TODO add discovery method + // TODO Is a discovery method actually needed? + // The plugin has a parameter for the IP address + + QString description = "Navigator 2"; + ThingDescriptor descriptor(info->thingClassId(), "", description); + info->addThingDescriptor(descriptor); + + // Just report no error for now, until the above question + // is clarified + info->finish(Thing::ThingErrorNoError); } } @@ -46,22 +60,38 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); Idm *idm = new Idm(hostAddress, this); m_idmConnections.insert(thing, idm); + m_idmInfos.insert(thing, info); + + info->finish(Thing::ThingErrorNoError); } } void IntegrationPluginIdm::postSetupThing(Thing *thing) { + if (!m_refreshTimer) { + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginIdm::onRefreshTimer); + } + if (thing->thingClassId() == navigator2ThingClassId) { Idm *idm = m_idmConnections.value(thing); + connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); + + qCDebug(dcIdm()) << "Thing set up, calling update"; + update(thing); + + thing->setStateValue(navigator2ConnectedStateTypeId, true); } } void IntegrationPluginIdm::thingRemoved(Thing *thing) { - if (m_idmConnections.contains(thing)) + if (m_idmConnections.contains(thing)) { m_idmConnections.take(thing)->deleteLater(); + m_idmInfos.take(thing)->deleteLater(); + } } void IntegrationPluginIdm::executeAction(ThingActionInfo *info) @@ -78,3 +108,58 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } + +void IntegrationPluginIdm::update(Thing *thing) +{ + if (thing->thingClassId() == navigator2ThingClassId) { + qCDebug(dcIdm()) << "Updating thing"; + + Idm *idm = m_idmConnections.value(thing); + Q_UNUSED(idm); + + QVector val{}; + + idm->onReceivedHoldingRegister(0, 1021, val); + //idm->onRequestStatus(); + + //idm->readHoldingRegister(0xff, RegisterList::OutsideTemperature); + if (m_idmInfos.contains(thing)) { + ThingSetupInfo *info = m_idmInfos.take(thing); + info->finish(Thing::ThingErrorNoError); + } + } +} + +void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) +{ + if (!info) + return; + + qCDebug(dcIdm()) << "Received status from heat pump"; + + Idm *idm = static_cast(sender()); + Thing *thing = m_idmConnections.key(idm); + + if (!thing) + return; + + /* Received a structure holding the status info of the + * heat pump. Update the thing states with the individual fields. */ + thing->setStateValue(navigator2ConnectedStateTypeId, info->m_connected); + thing->setStateValue(navigator2PowerStateTypeId, info->m_power); + thing->setStateValue(navigator2TemperatureStateTypeId, info->m_roomTemperature); + thing->setStateValue(navigator2OutsideAirTemperatureStateTypeId, info->m_outsideTemperature); + thing->setStateValue(navigator2WaterTemperatureStateTypeId, info->m_waterTemperature); + thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->m_targetRoomTemperature); + thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->m_targetWaterTemperature); + thing->setStateValue(navigator2ModeStateTypeId, info->m_mode); + thing->setStateValue(navigator2ErrorStateTypeId, info->m_error); +} + +void IntegrationPluginIdm::onRefreshTimer() +{ + foreach (Thing *thing, myThings().filterByThingClassId(navigator2ThingClassId)) { + update(thing); + } +} + diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 58d2910..3da5ef6 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -38,6 +38,7 @@ #include + class IntegrationPluginIdm: public IntegrationPlugin { Q_OBJECT @@ -54,6 +55,7 @@ public: void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void executeAction(ThingActionInfo *info) override; + void update(Thing *thing); private: @@ -87,10 +89,13 @@ private: PluginTimer *m_refreshTimer = nullptr; QHash m_idmConnections; + QHash m_idmInfos; QHash m_asyncActions; -private slots: void onRefreshTimer(); + +private slots: + void onStatusUpdated(IdmInfo *info); }; #endif // INTEGRATIONPLUGINIDM_H diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 7c6593b..edc139d 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -12,7 +12,7 @@ "name": "navigator2", "displayName": "Navigator 2.0", "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", - "createMethods": ["Discovery"], + "createMethods": ["User","Discovery"], "interfaces": ["heating", "temperaturesensor", "connectable"], "paramTypes": [ { From bd5f3be35d125845773e84c23500a4126e0d0f0d Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 09:20:02 +0200 Subject: [PATCH 05/30] Helper functions to convert modbus reg to float --- modbus/modbushelpers.cpp | 52 ++++++++++++++++++++++++++++++++++++++++ modbus/modbushelpers.h | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 modbus/modbushelpers.cpp create mode 100644 modbus/modbushelpers.h diff --git a/modbus/modbushelpers.cpp b/modbus/modbushelpers.cpp new file mode 100644 index 0000000..96b5394 --- /dev/null +++ b/modbus/modbushelpers.cpp @@ -0,0 +1,52 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 "modbushelpers.h" + +#include + +float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { + float result = 0.0; + + if (reg != nullptr) { + /* low-order byte is sent first, so swap order */ + quint32 tmp = 0.0; + + tmp |= ((quint32)(reg[1]) << 16) & 0xFFFF0000; + tmp |= reg[0]; + + /* copy value over to float variable without any conversion */ + /* needs to be done with char * to avoid pedantic compiler errors */ + memcpy((char *)&result, (char *)&tmp, sizeof(result)); + } + + return result; +} + diff --git a/modbus/modbushelpers.h b/modbus/modbushelpers.h new file mode 100644 index 0000000..c9e7af2 --- /dev/null +++ b/modbus/modbushelpers.h @@ -0,0 +1,42 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, 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 MODBUSHELPERS_H +#define MODBUSHELPERS_H + +#include + +class ModbusHelpers { +public: + static float convertRegisterToFloat(const quint16 *reg); +}; + +#endif + From 9b346848a2b0d183983baca04f2402954bfe9329 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 11:28:31 +0200 Subject: [PATCH 06/30] Readout working --- idm/idm.cpp | 118 +++++++++++++++++++++++++++++------ idm/idm.h | 62 ++++++++++++++---- idm/idm.pro | 3 + idm/idminfo.h | 21 +++++++ idm/integrationpluginidm.cpp | 10 +-- idm/integrationpluginidm.h | 9 +-- 6 files changed, 179 insertions(+), 44 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 2acb9ee..c9d930d 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -29,16 +29,21 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "idm.h" +#include "../modbus/modbushelpers.h" + +#include Idm::Idm(const QHostAddress &address, QObject *parent) : QObject(parent), m_hostAddress(address) { - /* qCDebug(dcIdm()) << "Creating Idm with addr: " << address; */ - m_modbusMaster = new ModbusTCPMaster(address, 502, this); - connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); + if (m_modbusMaster) { + m_modbusMaster->connectDevice(); + + connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); + } } Idm::~Idm() @@ -48,31 +53,104 @@ Idm::~Idm() } } -double Idm::getOutsideTemperature() -{ - //m_modbusMaster-> - return 0.0; -} - void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { - /* qCDebug(dcIdm()) << "Idm::onReceivedHoldingRegister"; */ - Q_UNUSED(slaveAddress); - Q_UNUSED(modbusRegister); - Q_UNUSED(value); - IdmInfo *info = new IdmInfo; - info->m_outsideTemperature = 24.3; + switch (modbusRegister) { + case Idm::OutsideTemperature: + if (value.length() == 2) { + m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); + } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); + break; + case Idm::CurrentFaultNumber: + if (value.length() == 1) { + if (value[0] > 0) { + m_info->m_error = true; + } else { + m_info->m_error = false; + } + } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 2); + break; + case Idm::TargetHotWaterTemperature: + if (value.length() == 2) { + m_info->m_targetWaterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::TargetHotWaterTemperature - modbusRegister]); + } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); + break; + case Idm::HeatPumpOperatingMode: + if (value.length() == 1) { + printf("read heat pump operation mode: %d\n", value[0]); + m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); + } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::ExternalOutsideTemperature, 2); + break; + case Idm::ExternalOutsideTemperature: + if (value.length() == 2) { + m_info->m_outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::ExternalOutsideTemperature - modbusRegister]); + } - emit statusUpdated(info); + emit statusUpdated(m_info); + break; + } } void Idm::onRequestStatus() { - /* Reading a total of 16 bytes, starting from address 1000. - * This covers the following parameters: - * */ - m_modbusMaster->readHoldingRegister(0xff, RegisterList::OutsideTemperature, 16); + m_info = new IdmInfo; + + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::OutsideTemperature, 2); +} + +QString Idm::systemOperationModeToString(IdmSysMode mode) { + QString result{}; + + /* Operation modes according to table of manual p. 13 */ + switch (mode) { + case IdmSysModeStandby: + result = "Standby"; + break; + case IdmSysModeAutomatic: + result = "Automatik"; + break; + case IdmSysModeAway: + result = "Abwesend"; + break; + case IdmSysModeOnlyHotwater: + result = "Nur Warmwasser"; + break; + case IdmSysModeOnlyRoomHeating: + result = "Nur Heizung/Kühlung"; + break; + } + + return result; +} + +QString Idm::heatPumpOperationModeToString(IdmHeatPumpMode mode) { + QString result{}; + + /* Operation modes according to table of manual p. 14 */ + switch (mode) { + case IdmHeatPumpModeOff: + result = "Off"; + break; + case IdmHeatPumpModeHeating: + result = "Heating"; + break; + case IdmHeatPumpModeCooling: + result = "Cooling"; + break; + case IdmHeatPumpModeHotWater: + result = "Hot water"; + break; + case IdmHeatPumpModeDefrost: + result = "Defrost"; + break; + } + + return result; } diff --git a/idm/idm.h b/idm/idm.h index 0792fff..8b83544 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -41,13 +41,23 @@ class Idm : public QObject { Q_OBJECT public: + /** Constructor */ explicit Idm(const QHostAddress &address, QObject *parent = nullptr); - ~Idm(); - double getOutsideTemperature(); - //void getCurrentFaultNumber(); + /** Destructor */ + ~Idm(); private: + /* Note: It would be desirable to read the modbus registers + * of the Idm heat pump in groups to minimize the number + * of read requests. However, a maximum of 6 registers + * can be read simultaneously. With the given set of + * addresses it is not possible to reasonably group the + * registers, therefore they are read individually. + */ + + /** Modbus Unit ID of Idm device */ + static const quint16 ModbusUnitID = 1; enum IscModus { KeineAbwarme = 0, @@ -56,11 +66,29 @@ private: Warmequelle = 8, }; + /** System operation modes according to manual p. 13 */ + enum IdmSysMode { + IdmSysModeStandby = 0, + IdmSysModeAutomatic = 1, + IdmSysModeAway = 2, + IdmSysModeOnlyHotwater = 4, + IdmSysModeOnlyRoomHeating = 5 + }; + + /** Heat pump operation modes according to manual p. 14 */ + enum IdmHeatPumpMode { + IdmHeatPumpModeOff = 0, + IdmHeatPumpModeHeating = 1, + IdmHeatPumpModeCooling = 2, + IdmHeatPumpModeHotWater = 4, + IdmHeatPumpModeDefrost = 8 + }; + + /** The following modbus addresses are according to the manual + * Modbus TCP Navigatorregelung 2.0 pages 13-31. + * Comments at the end of each line give their original name + * in the German manual. */ enum RegisterList { - /* The following modbus addresses are according to the manual - * Modbus TCP Navigatorregelung 2.0 pages 13-31. - * Comments at the end of each line give their original name - * in the German manual. */ OutsideTemperature = 1000, // Außentemperatur (B31) MeanOutsideTemperature = 1002, // Gemittelte Außentemperature CurrentFaultNumber = 1004, // Aktuelle Störungsnummer @@ -75,6 +103,7 @@ private: HeatPumpOperatingMode = 1090, // Betriebsart Wärmepumpe SummationFaultHeatPump = 1099, // Summenstörung Wärepumpe Humiditysensor = 1392, // Feuchtesensor + RoomTemperatureTargetHeatingHKA = 1401, // Raumsolltemperatur Heizen Normal HK A ExternalOutsideTemperature = 1690, // Externe Außentemperatur ExternalHumidity = 1692, // Externe Feuchte ExternalRequestTemperatureHeating = 1694, // Externe Anforderungstemperatur Heizen @@ -97,6 +126,7 @@ private: SolarOperatingMode = 1856, // Betriebsart Solar ISCMode = 1875, // ISCModus AcknowledgeFaultMessages = 1999, // Störmeldungen quittieren + TargetRoomTemperatureZ1R1 = 2004, // Zonenmodul 1 Raumsolltemperatur Raum 1 CurrentPhotovoltaicsSurplus = 74, // Aktueller PV-Überschuss CurrentPhotovoltaicsProduction = 78, // Aktueller PV Produktion CurrentPowerConsumption = 4122, // Aktuelle Leistungsaufnahme Wärmepumpe @@ -106,18 +136,26 @@ private: * TCP Modbus connection. Multiple devices are managed * within the IntegrationPluginIdm class. */ QHostAddress m_hostAddress; + + /** Pointer to ModbusTCPMaster object, responseible for low-level communicaiton */ ModbusTCPMaster *m_modbusMaster = nullptr; + /** This structure is allocated within onRequestStatus and filled + * by the receivedStatusGroupx functions */ + IdmInfo *m_info = nullptr; + + /** Converts a system operation mode code to a string (according to manual p. 13) */ + QString systemOperationModeToString(IdmSysMode mode); + + /** Converts a heat pump operation mode code to a string (according to manual p. 14) */ + QString heatPumpOperationModeToString(IdmHeatPumpMode mode); + signals: void statusUpdated(IdmInfo *info); -private slots: - void onRequestStatus(); - -// only public for debugging! public slots: + void onRequestStatus(); void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); - }; #endif // IDM_H diff --git a/idm/idm.pro b/idm/idm.pro index 63867ed..6bc6551 100644 --- a/idm/idm.pro +++ b/idm/idm.pro @@ -8,9 +8,12 @@ SOURCES += \ idm.cpp \ integrationpluginidm.cpp \ ../modbus/modbustcpmaster.cpp \ + ../modbus/modbushelpers.cpp \ HEADERS += \ idm.h \ idminfo.h \ integrationpluginidm.h \ ../modbus/modbustcpmaster.h \ + ../modbus/modbushelpers.h \ + diff --git a/idm/idminfo.h b/idm/idminfo.h index 115c49b..e43438f 100644 --- a/idm/idminfo.h +++ b/idm/idminfo.h @@ -31,19 +31,40 @@ #ifndef IDMINFO_H #define IDMINFO_H +#include #include +/** This struct holds the status information that is read from the IDM device + * and passed to the nymea framework within this plugin. + */ struct IdmInfo { bool m_connected; bool m_power; + + /** RegisterList::OutsideTemperature */ double m_roomTemperature; + + /** RegisterList::ExternalOutsideTemperature */ double m_outsideTemperature; + + /** RegisterList::HeatStorageTemperature */ double m_waterTemperature; + + /** RegisterList::TargetRoomTemperatureZ1R1 (zone 1, room 1) */ double m_targetRoomTemperature; + + /** RegisterList::TargetHotWaterTemperature */ double m_targetWaterTemperature; + + /** RegisterList::OperationModeSystem */ QString m_mode; + + /** True if there is an error code set + * (RegisterList::CurrentFaultNumber != 0) */ bool m_error; }; +Q_DECLARE_METATYPE(IdmInfo); + #endif diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index cd3e113..840e25e 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -43,7 +43,7 @@ void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) // The plugin has a parameter for the IP address QString description = "Navigator 2"; - ThingDescriptor descriptor(info->thingClassId(), "", description); + ThingDescriptor descriptor(info->thingClassId(), "Idm", description); info->addThingDescriptor(descriptor); // Just report no error for now, until the above question @@ -58,8 +58,12 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) if (thing->thingClassId() == navigator2ThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); + + /* Create new Idm object and store it in hash table */ Idm *idm = new Idm(hostAddress, this); m_idmConnections.insert(thing, idm); + + /* Store thing info in hash table */ m_idmInfos.insert(thing, info); info->finish(Thing::ThingErrorNoError); @@ -119,10 +123,8 @@ void IntegrationPluginIdm::update(Thing *thing) QVector val{}; - idm->onReceivedHoldingRegister(0, 1021, val); - //idm->onRequestStatus(); + idm->onRequestStatus(); - //idm->readHoldingRegister(0xff, RegisterList::OutsideTemperature); if (m_idmInfos.contains(thing)) { ThingSetupInfo *info = m_idmInfos.take(thing); info->finish(Thing::ThingErrorNoError); diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 3da5ef6..81de0e1 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -48,6 +48,7 @@ class IntegrationPluginIdm: public IntegrationPlugin public: + /** Constructor */ explicit IntegrationPluginIdm(); void discoverThings(ThingDiscoveryInfo *info) override; @@ -59,14 +60,6 @@ public: private: - enum IdmSysMode { - IdmSysModeStandby = 0, - IdmSysModeAutomatic, - IdmSysModeAway, - IdmSysModeOnlyWarmwater, - IdmSysModeOnlyRoomHeating - }; - enum IdmSmartGridMode { EVUSperreKeinPVErtrag, EVUBezugKeinPVErtrag, From 470eb643170e2648ea6725a42073ce30804c1c3c Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 12:47:55 +0200 Subject: [PATCH 07/30] Fixed hot water temperature reading --- idm/idm.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index c9d930d..070833d 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -72,6 +72,12 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const m_info->m_error = false; } } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatStorageTemperature, 2); + break; + case Idm::HeatStorageTemperature: + if (value.length() == 2) { + m_info->m_waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); + } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 2); break; case Idm::TargetHotWaterTemperature: @@ -82,7 +88,6 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const break; case Idm::HeatPumpOperatingMode: if (value.length() == 1) { - printf("read heat pump operation mode: %d\n", value[0]); m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::ExternalOutsideTemperature, 2); From ad70d1d254a0f2084fa4f906eefb79d2a500aa3b Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 14:01:30 +0200 Subject: [PATCH 08/30] Fixes for Idm plugin Hopefully reading room temperature and outside temperature correctly now. --- idm/idm.cpp | 8 ++++---- idm/idm.h | 1 + modbus/modbustcpmaster.cpp | 1 + modbus/modbustcpmaster.h | 2 ++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 070833d..a948bfd 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -60,7 +60,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const switch (modbusRegister) { case Idm::OutsideTemperature: if (value.length() == 2) { - m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); + m_info->m_outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); break; @@ -90,11 +90,11 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const if (value.length() == 1) { m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::ExternalOutsideTemperature, 2); + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); break; - case Idm::ExternalOutsideTemperature: + case Idm::RoomTemperatureHKA: if (value.length() == 2) { - m_info->m_outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::ExternalOutsideTemperature - modbusRegister]); + m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::ExternalOutsideTemperature - modbusRegister]); } emit statusUpdated(m_info); diff --git a/idm/idm.h b/idm/idm.h index 8b83544..152acc5 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -102,6 +102,7 @@ private: TargetHotWaterTemperature = 1032, // Warmwasser-Solltemperatur HeatPumpOperatingMode = 1090, // Betriebsart Wärmepumpe SummationFaultHeatPump = 1099, // Summenstörung Wärepumpe + RoomTemperatureHKA = 1364, // Heizkreis A Raumtemperature (B61) Humiditysensor = 1392, // Feuchtesensor RoomTemperatureTargetHeatingHKA = 1401, // Raumsolltemperatur Heizen Normal HK A ExternalOutsideTemperature = 1690, // Externe Außentemperatur diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 48bce17..88eacb7 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -29,6 +29,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "modbustcpmaster.h" + #include NYMEA_LOGGING_CATEGORY(dcModbusTCP, "ModbusTCP") diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index ae3d9b2..86aa28c 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -37,6 +37,8 @@ #include #include +Q_DECLARE_LOGGING_CATEGORY(dcModbus) + class ModbusTCPMaster : public QObject { Q_OBJECT From 375b7a9d5de86ab3cc02c4d0db27b24a32a4a136 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 15:27:47 +0200 Subject: [PATCH 09/30] Added humidity and connected state ModbusTCPMaster now emits readRequestError and writeRequestError, which trigger connected = false. --- idm/idm.cpp | 22 ++++++++++++++++++++++ idm/idm.h | 3 ++- idm/idminfo.h | 6 ++++++ idm/integrationpluginidm.json | 11 ++++++++++- modbus/modbustcpmaster.cpp | 17 +++++++++-------- modbus/modbustcpmaster.h | 1 + 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index a948bfd..3c5f7ee 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -43,6 +43,8 @@ Idm::Idm(const QHostAddress &address, QObject *parent) : m_modbusMaster->connectDevice(); connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); + connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &Idm::onModbusError); + connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &Idm::onModbusError); } } @@ -90,6 +92,12 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const if (value.length() == 1) { m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HumiditySensor, 2); + break; + case Idm::HumiditySensor: + if (value.length() == 2) { + m_info->m_humidity = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HumiditySensor - modbusRegister]); + } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); break; case Idm::RoomTemperatureHKA: @@ -97,11 +105,25 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::ExternalOutsideTemperature - modbusRegister]); } + /* Everything read without an error + * -> set connected to true */ + m_info->m_connected = true; emit statusUpdated(m_info); break; } } +void Idm::onModbusError() +{ + if (m_info == nullptr) { + m_info = new IdmInfo; + } + + m_info->m_connected = false; + + emit statusUpdated(m_info); +} + void Idm::onRequestStatus() { m_info = new IdmInfo; diff --git a/idm/idm.h b/idm/idm.h index 152acc5..4832eb0 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -103,7 +103,7 @@ private: HeatPumpOperatingMode = 1090, // Betriebsart Wärmepumpe SummationFaultHeatPump = 1099, // Summenstörung Wärepumpe RoomTemperatureHKA = 1364, // Heizkreis A Raumtemperature (B61) - Humiditysensor = 1392, // Feuchtesensor + HumiditySensor = 1392, // Feuchtesensor RoomTemperatureTargetHeatingHKA = 1401, // Raumsolltemperatur Heizen Normal HK A ExternalOutsideTemperature = 1690, // Externe Außentemperatur ExternalHumidity = 1692, // Externe Feuchte @@ -155,6 +155,7 @@ signals: void statusUpdated(IdmInfo *info); public slots: + void onModbusError(); void onRequestStatus(); void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); }; diff --git a/idm/idminfo.h b/idm/idminfo.h index e43438f..7b51380 100644 --- a/idm/idminfo.h +++ b/idm/idminfo.h @@ -38,7 +38,10 @@ * and passed to the nymea framework within this plugin. */ struct IdmInfo { + /** Set to true, if register values can be read, + * false in case of communication problems */ bool m_connected; + bool m_power; /** RegisterList::OutsideTemperature */ @@ -56,6 +59,9 @@ struct IdmInfo { /** RegisterList::TargetHotWaterTemperature */ double m_targetWaterTemperature; + /** RegisterList::HumiditySensor */ + double m_humidity; + /** RegisterList::OperationModeSystem */ QString m_mode; diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index edc139d..69a4715 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -13,7 +13,7 @@ "displayName": "Navigator 2.0", "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", "createMethods": ["User","Discovery"], - "interfaces": ["heating", "temperaturesensor", "connectable"], + "interfaces": ["thermostat", "temperaturesensor", "humiditysensor", "connectable"], "paramTypes": [ { "id": "05714e5c-d66a-4095-bbff-a0eb96fb035b", @@ -69,6 +69,15 @@ "unit": "DegreeCelsius", "defaultValue": 0 }, + { + "id": "109b4189-491a-4498-8470-b52b1b434c71", + "name": "humidity", + "displayName": "Humidity", + "displayNameEvent": "Humidity changed", + "type": "double", + "minValue": 0, + "maxValue": 100 + }, { "id": "efae7493-68c3-4cb9-853c-81011bdf09ca", "name": "targetTemperature", diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 88eacb7..3220842 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -305,21 +305,22 @@ QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddre connect(reply, &QModbusReply::finished, this, [reply, requestId, this] { if (reply->error() == QModbusDevice::NoError) { - writeRequestExecuted(requestId, true); + emit writeRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedHoldingRegister(reply->serverAddress(), modbusAddress, unit.values()); } else { - writeRequestExecuted(requestId, false); + emit writeRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + emit readRequestError(requestId, reply->errorString()); } reply->deleteLater(); }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ - qCWarning(dcModbusTCP()) << "Modbus replay error:" << error; - emit writeRequestError(requestId, reply->errorString()); + qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; + emit readRequestError(requestId, reply->errorString()); reply->finished(); // To make sure it will be deleted }); QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); @@ -340,7 +341,7 @@ QUuid ModbusTCPMaster::writeCoil(uint slaveAddress, uint registerAddress, bool v } QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const QVector &values) - { +{ if (!m_modbusTcpClient) { return ""; } @@ -355,14 +356,14 @@ QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const connect(reply, &QModbusReply::finished, this, [reply, requestId, this] () { if (reply->error() == QModbusDevice::NoError) { - writeRequestExecuted(requestId, true); + emit writeRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values()); } else { - writeRequestExecuted(requestId, false); - qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); + emit writeRequestExecuted(requestId, false); + qCWarning(dcModbusTCP()) << "Write response error:" << reply->error(); } reply->deleteLater(); }); diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index 86aa28c..bede700 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -82,6 +82,7 @@ signals: void writeRequestExecuted(const QUuid &requestId, bool success); void writeRequestError(const QUuid &requestId, const QString &error); + void readRequestError(const QUuid &requestId, const QString &error); void receivedCoil(uint slaveAddress, uint modbusRegister, const QVector &values); void receivedDiscreteInput(uint slaveAddress, uint modbusRegister, const QVector &values); From 68dd60ba7790637e1debc972dfbd8e8696ddfb43 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 15:55:10 +0200 Subject: [PATCH 10/30] Fixes to humidty and room temperature readings --- idm/idm.cpp | 2 +- idm/integrationpluginidm.cpp | 1 + idm/integrationpluginidm.json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 3c5f7ee..5489f2b 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -102,7 +102,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const break; case Idm::RoomTemperatureHKA: if (value.length() == 2) { - m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::ExternalOutsideTemperature - modbusRegister]); + m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); } /* Everything read without an error diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 840e25e..951c8cc 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -154,6 +154,7 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) thing->setStateValue(navigator2WaterTemperatureStateTypeId, info->m_waterTemperature); thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->m_targetRoomTemperature); thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->m_targetWaterTemperature); + thing->setStateValue(navigator2HumidityStateTypeId, info->m_humidity); thing->setStateValue(navigator2ModeStateTypeId, info->m_mode); thing->setStateValue(navigator2ErrorStateTypeId, info->m_error); } diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 69a4715..e2d1f4a 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -75,6 +75,7 @@ "displayName": "Humidity", "displayNameEvent": "Humidity changed", "type": "double", + "defaultValue": 0, "minValue": 0, "maxValue": 100 }, From c6ca132713efb48cae241f33adf62e5fa76cfa97 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 16:18:18 +0200 Subject: [PATCH 11/30] Added power consumption of heat pump --- idm/idm.cpp | 12 ++++++++++++ idm/idm.h | 2 +- idm/idminfo.h | 3 +++ idm/integrationpluginidm.cpp | 1 + idm/integrationpluginidm.json | 10 ++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 5489f2b..899db67 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -104,6 +104,18 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const if (value.length() == 2) { m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingHKA, 2); + break; + case Idm::RoomTemperatureTargetHeatingHKA: + if (value.length() == 2) { + m_info->m_targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingHKA - modbusRegister]); + } + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); + break; + case Idm::CurrentPowerConsumptionHeatPump: + if (value.length() == 2) { + m_info->m_powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); + } /* Everything read without an error * -> set connected to true */ diff --git a/idm/idm.h b/idm/idm.h index 4832eb0..6d8b28c 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -130,7 +130,7 @@ private: TargetRoomTemperatureZ1R1 = 2004, // Zonenmodul 1 Raumsolltemperatur Raum 1 CurrentPhotovoltaicsSurplus = 74, // Aktueller PV-Überschuss CurrentPhotovoltaicsProduction = 78, // Aktueller PV Produktion - CurrentPowerConsumption = 4122, // Aktuelle Leistungsaufnahme Wärmepumpe + CurrentPowerConsumptionHeatPump = 4122, // Aktuelle Leistungsaufnahme Wärmepumpe }; /* Note: This class only requires one IP address and one diff --git a/idm/idminfo.h b/idm/idminfo.h index 7b51380..1139f8a 100644 --- a/idm/idminfo.h +++ b/idm/idminfo.h @@ -62,6 +62,9 @@ struct IdmInfo { /** RegisterList::HumiditySensor */ double m_humidity; + /** RegisterList::CurrentPowerConsumptionHeatPump */ + double m_powerConsumptionHeatPump; + /** RegisterList::OperationModeSystem */ QString m_mode; diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 951c8cc..472079c 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -155,6 +155,7 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->m_targetRoomTemperature); thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->m_targetWaterTemperature); thing->setStateValue(navigator2HumidityStateTypeId, info->m_humidity); + thing->setStateValue(navigator2CurrentPowerConsumptionHeatPumpStateTypeId, info->m_powerConsumptionHeatPump); thing->setStateValue(navigator2ModeStateTypeId, info->m_mode); thing->setStateValue(navigator2ErrorStateTypeId, info->m_error); } diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index e2d1f4a..5292ce0 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -105,6 +105,16 @@ "defaultValue": 46.00, "writable": true }, + { + + "id": "b98fb325-100d-4eae-bf8d-97e8f7e1eb00", + "name": "currentPowerConsumptionHeatPump", + "displayName": "Current power consumption heat pump", + "displayNameEvent": "Current power consumption heat pump changed", + "displayNameAction": "Change current power consumption het pump", + "type": "double", + "defaultValue": 0 + }, { "id": "e539366b-44da-4119-b11b-497bcdb1f522", "name": "mode", From 723217120b10f852cf95c103bedadf1f4e5501d0 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 16:42:41 +0200 Subject: [PATCH 12/30] Minor fix regarding target hot water temp --- idm/idm.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 899db67..0c6a7aa 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -80,11 +80,11 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const if (value.length() == 2) { m_info->m_waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 2); + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 1); break; case Idm::TargetHotWaterTemperature: - if (value.length() == 2) { - m_info->m_targetWaterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::TargetHotWaterTemperature - modbusRegister]); + if (value.length() == 1) { + m_info->m_targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); break; From ef183285295c719491912987378bfe9120c9b226 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 16 Oct 2020 17:54:12 +0200 Subject: [PATCH 13/30] Corrected units and state names --- idm/idm.cpp | 1 + idm/integrationpluginidm.json | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 0c6a7aa..cf3156c 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -84,6 +84,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const break; case Idm::TargetHotWaterTemperature: if (value.length() == 1) { + /* The hot water target temperature is stored as UCHAR (manual p. 13) */ m_info->m_targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 5292ce0..984d7e0 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -76,6 +76,7 @@ "displayNameEvent": "Humidity changed", "type": "double", "defaultValue": 0, + "unit": "Percentage", "minValue": 0, "maxValue": 100 }, @@ -113,6 +114,7 @@ "displayNameEvent": "Current power consumption heat pump changed", "displayNameAction": "Change current power consumption het pump", "type": "double", + "unit": "KiloWatt", "defaultValue": 0 }, { @@ -162,8 +164,8 @@ "paramTypes" : [ { "id": "034b9a8c-1a5a-45da-8422-393273d0a159", - "name": "humidity", - "displayName": "humidity", + "name": "extHumidity", + "displayName": "External Humidity", "type": "int", "defaultValue": 0, "minValue": 0, @@ -174,13 +176,13 @@ }, { "id": "87633d1f-3826-4bf0-9a2c-46a927446eb5", - "name": "pvEnergy", - "displayName": "Set available PV Energy", + "name": "pvPower", + "displayName": "Set available PV Power", "paramTypes" : [ { "id": "84b251ab-33b5-45e5-9a5c-468e3affe821", - "name": "energy", - "displayName": "Energy", + "name": "pvPower", + "displayName": "PV Power", "type": "double", "defaultValue": 0.00, "minValue": 0.00, From 2c342bc658311c686b0fac219be19357750d77c5 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Mon, 2 Nov 2020 14:06:42 +0100 Subject: [PATCH 14/30] Added functionality to set target room temp --- idm/idm.cpp | 22 ++++++++++++++++++++++ idm/idm.h | 3 +++ idm/integrationpluginidm.cpp | 14 +++++++++++++- idm/integrationpluginidm.h | 1 + idm/integrationpluginidm.json | 2 +- modbus/modbushelpers.cpp | 11 ++++++++++- modbus/modbushelpers.h | 2 ++ 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index cf3156c..26eb2b0 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -55,10 +55,32 @@ Idm::~Idm() } } +void Idm::setTargetRoomTemperature (double temperature) { + QVector registers{}; + + printf("Setting target room temperature to %g\n", temperature); + + ModbusHelpers::convertFloatToRegister(registers, temperature); + + printf("registers to be sent: %x %x\n", registers[0], registers[1]); + + m_modbusMaster->writeHoldingRegisters(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, registers); + + emit targetRoomTemperatureChanged(); +} + void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); + /* Introducing a delay here for testing. + * Purposely set here, so one delay works for all branches + * of the following switch statement. In fact, the delay + * is used before evaluating what was just read, which + * does not seem to make sense, but it also acts before + * the next read command is sent. */ + QThread::msleep(200); + switch (modbusRegister) { case Idm::OutsideTemperature: if (value.length() == 2) { diff --git a/idm/idm.h b/idm/idm.h index 6d8b28c..d1e0d24 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -47,6 +47,8 @@ public: /** Destructor */ ~Idm(); + void setTargetRoomTemperature (double temperature); + private: /* Note: It would be desirable to read the modbus registers * of the Idm heat pump in groups to minimize the number @@ -153,6 +155,7 @@ private: signals: void statusUpdated(IdmInfo *info); + void targetRoomTemperatureChanged(); public slots: void onModbusError(); diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 472079c..fdcb8be 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -82,6 +82,7 @@ void IntegrationPluginIdm::postSetupThing(Thing *thing) Idm *idm = m_idmConnections.value(thing); connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); + connect(idm, &Idm::targetRoomTemperatureChanged, this, &IntegrationPluginIdm::onTargetRoomTemperatureChanged); qCDebug(dcIdm()) << "Thing set up, calling update"; update(thing); @@ -105,7 +106,13 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) if (thing->thingClassId() == navigator2ThingClassId) { if (action.actionTypeId() == navigator2PowerActionTypeId) { - } else { + + } else if (action.actionTypeId() == navigator2TargetTemperatureStateTypeId) { + Idm *idm = m_idmConnections.value(thing); + + idm->setTargetRoomTemperature(action.param(navigator2TargetTemperatureEventTargetTemperatureParamTypeId).value().value()); + + } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); } } else { @@ -160,6 +167,11 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) thing->setStateValue(navigator2ErrorStateTypeId, info->m_error); } +void IntegrationPluginIdm::onTargetRoomTemperatureChanged() +{ + +} + void IntegrationPluginIdm::onRefreshTimer() { foreach (Thing *thing, myThings().filterByThingClassId(navigator2ThingClassId)) { diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 81de0e1..5d0cc11 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -89,6 +89,7 @@ private: private slots: void onStatusUpdated(IdmInfo *info); + void onTargetRoomTemperatureChanged(); }; #endif // INTEGRATIONPLUGINIDM_H diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 984d7e0..6f8a2e9 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -110,7 +110,7 @@ "id": "b98fb325-100d-4eae-bf8d-97e8f7e1eb00", "name": "currentPowerConsumptionHeatPump", - "displayName": "Current power consumption heat pump", + "displayName": "Curr. power consumption", "displayNameEvent": "Current power consumption heat pump changed", "displayNameAction": "Change current power consumption het pump", "type": "double", diff --git a/modbus/modbushelpers.cpp b/modbus/modbushelpers.cpp index 96b5394..ee26a03 100644 --- a/modbus/modbushelpers.cpp +++ b/modbus/modbushelpers.cpp @@ -37,7 +37,7 @@ float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { if (reg != nullptr) { /* low-order byte is sent first, so swap order */ - quint32 tmp = 0.0; + quint32 tmp = 0; tmp |= ((quint32)(reg[1]) << 16) & 0xFFFF0000; tmp |= reg[0]; @@ -50,3 +50,12 @@ float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { return result; } +void ModbusHelpers::convertFloatToRegister(QVector ®, float value) { + quint32 tmp = 0; + + memcpy((char *)&tmp, (char *)&value, sizeof(value)); + + reg.append((quint16)(tmp)); + reg.append((quint16)((tmp & 0xFFFF0000) >> 16)); +} + diff --git a/modbus/modbushelpers.h b/modbus/modbushelpers.h index c9e7af2..acea6b8 100644 --- a/modbus/modbushelpers.h +++ b/modbus/modbushelpers.h @@ -32,10 +32,12 @@ #define MODBUSHELPERS_H #include +#include class ModbusHelpers { public: static float convertRegisterToFloat(const quint16 *reg); + static void convertFloatToRegister(QVector ®, float value); }; #endif From e202b328b78949334e85b700c73d73f3fb8fcf37 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Wed, 11 Nov 2020 22:10:49 +0100 Subject: [PATCH 15/30] Removed faulty functionality. Write access to target temperatures for air and water, as well as humidity sensor were removed. Code clean-up + comments added. --- idm/idm.cpp | 22 +---------- idm/idm.h | 37 +++++++++++++----- idm/integrationpluginidm.cpp | 14 +------ idm/integrationpluginidm.h | 21 ---------- idm/integrationpluginidm.json | 73 ++--------------------------------- 5 files changed, 33 insertions(+), 134 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 26eb2b0..e749fc9 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -55,20 +55,6 @@ Idm::~Idm() } } -void Idm::setTargetRoomTemperature (double temperature) { - QVector registers{}; - - printf("Setting target room temperature to %g\n", temperature); - - ModbusHelpers::convertFloatToRegister(registers, temperature); - - printf("registers to be sent: %x %x\n", registers[0], registers[1]); - - m_modbusMaster->writeHoldingRegisters(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, registers); - - emit targetRoomTemperatureChanged(); -} - void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); @@ -115,12 +101,6 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const if (value.length() == 1) { m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HumiditySensor, 2); - break; - case Idm::HumiditySensor: - if (value.length() == 2) { - m_info->m_humidity = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HumiditySensor - modbusRegister]); - } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); break; case Idm::RoomTemperatureHKA: @@ -129,7 +109,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingHKA, 2); break; - case Idm::RoomTemperatureTargetHeatingHKA: + case Idm::RoomTemperatureTargetHeatingEcoHKA: if (value.length() == 2) { m_info->m_targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingHKA - modbusRegister]); } diff --git a/idm/idm.h b/idm/idm.h index d1e0d24..af68685 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -37,6 +37,33 @@ #include "idminfo.h" +/* + * Functionality: + * The current version allows read access to selected + * modbus registers: + * + Room temperature (HK A) + * + Water temperature + * + Outside air temperature + * + Target room temperature eco mode (HK A) + * + Target water temperature + * + Current power consumption + * + Operation mode + * + Error + * + * At present there is no write access for target + * room temperature and target water temperature. These + * result in an "invalid data access" error from the + * device. + */ + +/* Note: It would be desirable to read the modbus registers + * of the Idm heat pump in groups to minimize the number + * of read requests. However, a maximum of 6 registers + * can be read simultaneously. With the given set of + * addresses it is not possible to reasonably group the + * registers, therefore they are read individually. + */ + class Idm : public QObject { Q_OBJECT @@ -47,16 +74,7 @@ public: /** Destructor */ ~Idm(); - void setTargetRoomTemperature (double temperature); - private: - /* Note: It would be desirable to read the modbus registers - * of the Idm heat pump in groups to minimize the number - * of read requests. However, a maximum of 6 registers - * can be read simultaneously. With the given set of - * addresses it is not possible to reasonably group the - * registers, therefore they are read individually. - */ /** Modbus Unit ID of Idm device */ static const quint16 ModbusUnitID = 1; @@ -107,6 +125,7 @@ private: RoomTemperatureHKA = 1364, // Heizkreis A Raumtemperature (B61) HumiditySensor = 1392, // Feuchtesensor RoomTemperatureTargetHeatingHKA = 1401, // Raumsolltemperatur Heizen Normal HK A + RoomTemperatureTargetHeatingEcoHKA = 1415, // Raumsolltemperatur Heizen Eco HK A ExternalOutsideTemperature = 1690, // Externe Außentemperatur ExternalHumidity = 1692, // Externe Feuchte ExternalRequestTemperatureHeating = 1694, // Externe Anforderungstemperatur Heizen diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index fdcb8be..4b1fba9 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -82,7 +82,6 @@ void IntegrationPluginIdm::postSetupThing(Thing *thing) Idm *idm = m_idmConnections.value(thing); connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); - connect(idm, &Idm::targetRoomTemperatureChanged, this, &IntegrationPluginIdm::onTargetRoomTemperatureChanged); qCDebug(dcIdm()) << "Thing set up, calling update"; update(thing); @@ -107,12 +106,7 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) if (thing->thingClassId() == navigator2ThingClassId) { if (action.actionTypeId() == navigator2PowerActionTypeId) { - } else if (action.actionTypeId() == navigator2TargetTemperatureStateTypeId) { - Idm *idm = m_idmConnections.value(thing); - - idm->setTargetRoomTemperature(action.param(navigator2TargetTemperatureEventTargetTemperatureParamTypeId).value().value()); - - } else { + } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); } } else { @@ -161,17 +155,11 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) thing->setStateValue(navigator2WaterTemperatureStateTypeId, info->m_waterTemperature); thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->m_targetRoomTemperature); thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->m_targetWaterTemperature); - thing->setStateValue(navigator2HumidityStateTypeId, info->m_humidity); thing->setStateValue(navigator2CurrentPowerConsumptionHeatPumpStateTypeId, info->m_powerConsumptionHeatPump); thing->setStateValue(navigator2ModeStateTypeId, info->m_mode); thing->setStateValue(navigator2ErrorStateTypeId, info->m_error); } -void IntegrationPluginIdm::onTargetRoomTemperatureChanged() -{ - -} - void IntegrationPluginIdm::onRefreshTimer() { foreach (Thing *thing, myThings().filterByThingClassId(navigator2ThingClassId)) { diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 5d0cc11..42b9627 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -60,26 +60,6 @@ public: private: - enum IdmSmartGridMode { - EVUSperreKeinPVErtrag, - EVUBezugKeinPVErtrag, - KeinEVUBezugPVErtrag, - EVUSperrePVErtrag - }; - - enum IdmStatus { - Heating = 2, - Standby = 3, - Boosted = 4, - HeatFinished = 5, - Setup = 9, - ErrorOvertempFuseBlown = 201, - ErrorOvertempMeasured = 202, - ErrorOvertempElectronics = 203, - ErrorHardwareFault = 204, - ErrorTempSensor = 205 - }; - PluginTimer *m_refreshTimer = nullptr; QHash m_idmConnections; QHash m_idmInfos; @@ -89,7 +69,6 @@ private: private slots: void onStatusUpdated(IdmInfo *info); - void onTargetRoomTemperatureChanged(); }; #endif // INTEGRATIONPLUGINIDM_H diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 6f8a2e9..a45ce03 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -13,7 +13,7 @@ "displayName": "Navigator 2.0", "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", "createMethods": ["User","Discovery"], - "interfaces": ["thermostat", "temperaturesensor", "humiditysensor", "connectable"], + "interfaces": ["temperaturesensor", "connectable"], "paramTypes": [ { "id": "05714e5c-d66a-4095-bbff-a0eb96fb035b", @@ -69,42 +69,23 @@ "unit": "DegreeCelsius", "defaultValue": 0 }, - { - "id": "109b4189-491a-4498-8470-b52b1b434c71", - "name": "humidity", - "displayName": "Humidity", - "displayNameEvent": "Humidity changed", - "type": "double", - "defaultValue": 0, - "unit": "Percentage", - "minValue": 0, - "maxValue": 100 - }, { "id": "efae7493-68c3-4cb9-853c-81011bdf09ca", "name": "targetTemperature", "displayName": "Target room temperature", "displayNameEvent": "Target room temperature changed", - "displayNameAction": "Change room target temperature", "type": "double", "unit": "DegreeCelsius", - "minValue": 14.00, - "maxValue": 26.00, - "defaultValue": 22.00, - "writable": true + "defaultValue": 22.00 }, { "id": "746244d6-dd37-4af8-b2ae-a7d8463e51e2", "name": "targetWaterTemperature", "displayName": "Target water temperature", "displayNameEvent": "Target water temperature changed", - "displayNameAction": "Change water target temperature", "type": "double", "unit": "DegreeCelsius", - "minValue": 20.00, - "maxValue": 55.00, - "defaultValue": 46.00, - "writable": true + "defaultValue": 46.00 }, { @@ -142,54 +123,6 @@ } ], "actionTypes": [ - { - "id": "29b65c13-46e9-49b1-970a-68252bdfeadc", - "name": "externTemperature", - "displayName": "Extern temperature", - "paramTypes" : [ - { - "id": "d60fcb0c-19b5-4cac-9b95-a1b414518385", - "name": "temperature", - "displayName": "Temperature", - "type": "double", - "defaultValue": 0, - "unit": "DegreeCelsius" - } - ] - }, - { - "id": "046f2e72-899a-4d82-91d3-fd268c784a1c", - "name": "externHumidity", - "displayName": "Extern humidity", - "paramTypes" : [ - { - "id": "034b9a8c-1a5a-45da-8422-393273d0a159", - "name": "extHumidity", - "displayName": "External Humidity", - "type": "int", - "defaultValue": 0, - "minValue": 0, - "maxValue": 100, - "unit": "Percentage" - } - ] - }, - { - "id": "87633d1f-3826-4bf0-9a2c-46a927446eb5", - "name": "pvPower", - "displayName": "Set available PV Power", - "paramTypes" : [ - { - "id": "84b251ab-33b5-45e5-9a5c-468e3affe821", - "name": "pvPower", - "displayName": "PV Power", - "type": "double", - "defaultValue": 0.00, - "minValue": 0.00, - "unit": "KiloWatt" - } - ] - } ] } ] From af93c6c9a0d7c3b1fd9bd3d1ee086b9091e87c2f Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Tue, 17 Nov 2020 11:17:48 +0100 Subject: [PATCH 16/30] Fix preventing adding the same IP a second time --- idm/idm.h | 2 ++ idm/integrationpluginidm.cpp | 29 +++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/idm/idm.h b/idm/idm.h index af68685..b2f142b 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -74,6 +74,8 @@ public: /** Destructor */ ~Idm(); + QHostAddress getIdmAddress() const {return m_hostAddress;}; + private: /** Modbus Unit ID of Idm device */ diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 4b1fba9..dcf410b 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -39,7 +39,7 @@ IntegrationPluginIdm::IntegrationPluginIdm() void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == navigator2ThingClassId) { - // TODO Is a discovery method actually needed? + // TODO Is a discovery method actually possible? // The plugin has a parameter for the IP address QString description = "Navigator 2"; @@ -59,16 +59,29 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) if (thing->thingClassId() == navigator2ThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); - /* Create new Idm object and store it in hash table */ - Idm *idm = new Idm(hostAddress, this); - m_idmConnections.insert(thing, idm); + if (hostAddress.isNull() == false) { + /* Check, if address is already in use for another device */ + bool found = false; - /* Store thing info in hash table */ - m_idmInfos.insert(thing, info); + for (QHash::iterator item=m_idmConnections.begin(); item != m_idmConnections.end(); item++) { + if (hostAddress.isEqual(item.value()->getIdmAddress())) + found = true; + } - info->finish(Thing::ThingErrorNoError); + if (found == false) { + /* Create new Idm object and store it in hash table */ + Idm *idm = new Idm(hostAddress, this); + m_idmConnections.insert(thing, idm); + + /* Store thing info in hash table */ + m_idmInfos.insert(thing, info); + + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorThingInUse); + } + } } - } void IntegrationPluginIdm::postSetupThing(Thing *thing) From 0dfb894fcb5c5b62878ee886c547f8a067800046 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Wed, 18 Nov 2020 13:23:21 +0100 Subject: [PATCH 17/30] Commit to allow debugging of setupThing --- idm/integrationpluginidm.cpp | 48 ++++++++++++++++++++++++++++++----- idm/integrationpluginidm.json | 2 +- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index dcf410b..c36c38d 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -38,6 +38,8 @@ IntegrationPluginIdm::IntegrationPluginIdm() void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) { + qCDebug(dcIdm()) << "discoverThings called"; + if (info->thingClassId() == navigator2ThingClassId) { // TODO Is a discovery method actually possible? // The plugin has a parameter for the IP address @@ -54,21 +56,40 @@ void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) { + qCDebug(dcIdm()) << "setupThing called"; + Thing *thing = info->thing(); if (thing->thingClassId() == navigator2ThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); if (hostAddress.isNull() == false) { + QString msg = "User entered address: "; + msg += hostAddress.toString(); + + qCDebug(dcIdm()) << msg; + /* Check, if address is already in use for another device */ bool found = false; for (QHash::iterator item=m_idmConnections.begin(); item != m_idmConnections.end(); item++) { - if (hostAddress.isEqual(item.value()->getIdmAddress())) + if (hostAddress.isEqual(item.value()->getIdmAddress())) { + msg = "Addr of thing: "; + msg += item.value()->getIdmAddress().toString(); + qCDebug(dcIdm()) << msg; + + qCDebug(dcIdm()) << "Address in use already"; found = true; + } else { + msg = "Different addr of other thing: "; + msg += item.value()->getIdmAddress().toString(); + qCDebug(dcIdm()) << msg; + } } if (found == false) { + qCDebug(dcIdm()) << "Creating Idm object"; + /* Create new Idm object and store it in hash table */ Idm *idm = new Idm(hostAddress, this); m_idmConnections.insert(thing, idm); @@ -76,30 +97,41 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) /* Store thing info in hash table */ m_idmInfos.insert(thing, info); - info->finish(Thing::ThingErrorNoError); + msg = "Added IDM heatpump with address "; + msg += hostAddress.toString(); + + info->thing()->setStateValue(navigator2ConnectedStateTypeId, true); + info->finish(Thing::ThingErrorNoError, msg.toStdString().c_str()); } else { - info->finish(Thing::ThingErrorThingInUse); + info->finish(Thing::ThingErrorThingInUse, "IP address already in use"); } + } else { + info->finish(Thing::ThingErrorInvalidParameter, "No IP address given"); } } } void IntegrationPluginIdm::postSetupThing(Thing *thing) { + qCDebug(dcIdm()) << "postSetupThing called"; + if (!m_refreshTimer) { m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginIdm::onRefreshTimer); } if (thing->thingClassId() == navigator2ThingClassId) { + qCDebug(dcIdm()) << "Thing id: " << thing->id(); Idm *idm = m_idmConnections.value(thing); - connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); + if (idm != nullptr) { + connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); - qCDebug(dcIdm()) << "Thing set up, calling update"; - update(thing); + qCDebug(dcIdm()) << "Thing set up, calling update"; + update(thing); - thing->setStateValue(navigator2ConnectedStateTypeId, true); + thing->setStateValue(navigator2ConnectedStateTypeId, true); + } } } @@ -175,6 +207,8 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) void IntegrationPluginIdm::onRefreshTimer() { + qCDebug(dcIdm()) << "onRefreshTimer called"; + foreach (Thing *thing, myThings().filterByThingClassId(navigator2ThingClassId)) { update(thing); } diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index a45ce03..9ed02ed 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -12,7 +12,7 @@ "name": "navigator2", "displayName": "Navigator 2.0", "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", - "createMethods": ["User","Discovery"], + "createMethods": ["User"], "interfaces": ["temperaturesensor", "connectable"], "paramTypes": [ { From 11fe0998251aedde49e59ffce02f82fe91c81b7c Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Wed, 18 Nov 2020 15:49:16 +0100 Subject: [PATCH 18/30] Another fix for previous commit Merge should be fine now --- idm/integrationpluginidm.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index c36c38d..4e226f8 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -172,8 +172,14 @@ void IntegrationPluginIdm::update(Thing *thing) idm->onRequestStatus(); if (m_idmInfos.contains(thing)) { +<<<<<<< HEAD ThingSetupInfo *info = m_idmInfos.take(thing); info->finish(Thing::ThingErrorNoError); +======= + /* ThingSetupInfo *info = m_idmInfos.take(thing); */ + /* qCDebug(dcIdm()) << "Finishing setup 4!"; */ + /* info->finish(Thing::ThingErrorNoError); */ +>>>>>>> 3b5ab5c... Another fix for previous commit } } } From e9204b94a30dfe1c8f61577af52edba2f7f54d90 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Fri, 20 Nov 2020 13:39:10 +0100 Subject: [PATCH 19/30] Fixed crash upon deleting of Idm thing Modified debug output (added to idm.cpp, but also partially commented out to prevent unnessary verbosity) --- idm/idm.cpp | 31 ++++++++++++++++++++++--------- idm/integrationpluginidm.cpp | 8 ++++---- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index e749fc9..537c5ed 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -29,6 +29,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "idm.h" +#include "extern-plugininfo.h" #include "../modbus/modbushelpers.h" #include @@ -40,6 +41,7 @@ Idm::Idm(const QHostAddress &address, QObject *parent) : m_modbusMaster = new ModbusTCPMaster(address, 502, this); if (m_modbusMaster) { + qCDebug(dcIdm()) << "created ModbusTCPMaster"; m_modbusMaster->connectDevice(); connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); @@ -58,6 +60,7 @@ Idm::~Idm() void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); + /* qCDebug(dcIdm()) << "receivedHoldingRegister " << modbusRegister << "(length: " << value.length() << ")"; */ /* Introducing a delay here for testing. * Purposely set here, so one delay works for all branches @@ -69,12 +72,14 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const switch (modbusRegister) { case Idm::OutsideTemperature: + /* qCDebug(dcIdm()) << "received outside temperature"; */ if (value.length() == 2) { m_info->m_outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); break; case Idm::CurrentFaultNumber: + /* qCDebug(dcIdm()) << "current fault number"; */ if (value.length() == 1) { if (value[0] > 0) { m_info->m_error = true; @@ -85,12 +90,14 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatStorageTemperature, 2); break; case Idm::HeatStorageTemperature: + /* qCDebug(dcIdm()) << "received storage temperature"; */ if (value.length() == 2) { m_info->m_waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 1); break; case Idm::TargetHotWaterTemperature: + /* qCDebug(dcIdm()) << "received target hot water temperature"; */ if (value.length() == 1) { /* The hot water target temperature is stored as UCHAR (manual p. 13) */ m_info->m_targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; @@ -98,24 +105,28 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); break; case Idm::HeatPumpOperatingMode: + /* qCDebug(dcIdm()) << "received heat pump operating mode"; */ if (value.length() == 1) { m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); break; case Idm::RoomTemperatureHKA: + /* qCDebug(dcIdm()) << "received room temperature hka"; */ if (value.length() == 2) { m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingHKA, 2); + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingEcoHKA, 2); break; case Idm::RoomTemperatureTargetHeatingEcoHKA: + /* qCDebug(dcIdm()) << "received room temprature hka eco"; */ if (value.length() == 2) { - m_info->m_targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingHKA - modbusRegister]); + m_info->m_targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); } m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); break; case Idm::CurrentPowerConsumptionHeatPump: + /* qCDebug(dcIdm()) << "received power consumption heat pump"; */ if (value.length() == 2) { m_info->m_powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); } @@ -130,20 +141,22 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const void Idm::onModbusError() { - if (m_info == nullptr) { - m_info = new IdmInfo; + qCDebug(dcIdm()) << "Received modbus error"; + + if (m_info != nullptr) { + m_info->m_connected = false; + emit statusUpdated(m_info); } - - m_info->m_connected = false; - - emit statusUpdated(m_info); } void Idm::onRequestStatus() { m_info = new IdmInfo; - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::OutsideTemperature, 2); + QUuid reqId{}; + reqId = m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::OutsideTemperature, 2); + + /* qCDebug(dcIdm()) << "Request id: " << reqId; */ } QString Idm::systemOperationModeToString(IdmSysMode mode) { diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 4e226f8..58344f8 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -139,7 +139,6 @@ void IntegrationPluginIdm::thingRemoved(Thing *thing) { if (m_idmConnections.contains(thing)) { m_idmConnections.take(thing)->deleteLater(); - m_idmInfos.take(thing)->deleteLater(); } } @@ -167,9 +166,9 @@ void IntegrationPluginIdm::update(Thing *thing) Idm *idm = m_idmConnections.value(thing); Q_UNUSED(idm); - QVector val{}; - - idm->onRequestStatus(); + if (idm != nullptr) { + idm->onRequestStatus(); + } if (m_idmInfos.contains(thing)) { <<<<<<< HEAD @@ -186,6 +185,7 @@ void IntegrationPluginIdm::update(Thing *thing) void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) { + /* qCDebug(dcIdm()) << "onStatusUpdated"; */ if (!info) return; From d78cf182a62f8dbaa5a3bbe456ffdb86af690a24 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Tue, 2 Feb 2021 15:32:53 +0100 Subject: [PATCH 20/30] Removed thread delay and used Timer instead --- idm/idm.cpp | 76 +++++++++++--------- idm/idm.h | 4 +- idm/idminfo.h | 22 +++--- idm/integrationpluginidm.cpp | 127 +++++++++++++++++----------------- idm/integrationpluginidm.h | 1 - idm/integrationpluginidm.json | 8 +-- 6 files changed, 121 insertions(+), 117 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 537c5ed..0045365 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -32,18 +32,18 @@ #include "extern-plugininfo.h" #include "../modbus/modbushelpers.h" +#include #include Idm::Idm(const QHostAddress &address, QObject *parent) : QObject(parent), m_hostAddress(address) { + qCDebug(dcIdm()) << "iDM: Creating iDM connection" << m_hostAddress.toString(); m_modbusMaster = new ModbusTCPMaster(address, 502, this); if (m_modbusMaster) { - qCDebug(dcIdm()) << "created ModbusTCPMaster"; - m_modbusMaster->connectDevice(); - + qCDebug(dcIdm()) << "iDM: Created ModbusTCPMaster"; connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &Idm::onModbusError); connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &Idm::onModbusError); @@ -52,9 +52,13 @@ Idm::Idm(const QHostAddress &address, QObject *parent) : Idm::~Idm() { - if (m_modbusMaster) { - delete m_modbusMaster; - } + qCDebug(dcIdm()) << "iDM: Deleting iDM connection" << m_hostAddress.toString(); +} + +bool Idm::connectDevice() +{ + qCDebug(dcIdm()) << "iDM: Connecting device"; + return m_modbusMaster->connectDevice(); } void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) @@ -62,78 +66,84 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const Q_UNUSED(slaveAddress); /* qCDebug(dcIdm()) << "receivedHoldingRegister " << modbusRegister << "(length: " << value.length() << ")"; */ - /* Introducing a delay here for testing. - * Purposely set here, so one delay works for all branches - * of the following switch statement. In fact, the delay - * is used before evaluating what was just read, which - * does not seem to make sense, but it also acts before - * the next read command is sent. */ - QThread::msleep(200); - switch (modbusRegister) { case Idm::OutsideTemperature: /* qCDebug(dcIdm()) << "received outside temperature"; */ if (value.length() == 2) { - m_info->m_outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); + m_info->outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); + }); break; case Idm::CurrentFaultNumber: /* qCDebug(dcIdm()) << "current fault number"; */ if (value.length() == 1) { if (value[0] > 0) { - m_info->m_error = true; + m_info->error = true; } else { - m_info->m_error = false; + m_info->error = false; } } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatStorageTemperature, 2); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatStorageTemperature, 2); + }); break; case Idm::HeatStorageTemperature: /* qCDebug(dcIdm()) << "received storage temperature"; */ if (value.length() == 2) { - m_info->m_waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); + m_info->waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 1); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 1); + }); break; case Idm::TargetHotWaterTemperature: /* qCDebug(dcIdm()) << "received target hot water temperature"; */ if (value.length() == 1) { /* The hot water target temperature is stored as UCHAR (manual p. 13) */ - m_info->m_targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; + m_info->targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); + }); break; case Idm::HeatPumpOperatingMode: /* qCDebug(dcIdm()) << "received heat pump operating mode"; */ if (value.length() == 1) { - m_info->m_mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); + m_info->mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); + }); break; case Idm::RoomTemperatureHKA: /* qCDebug(dcIdm()) << "received room temperature hka"; */ if (value.length() == 2) { - m_info->m_roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); + m_info->roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingEcoHKA, 2); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingEcoHKA, 2); + }); break; case Idm::RoomTemperatureTargetHeatingEcoHKA: /* qCDebug(dcIdm()) << "received room temprature hka eco"; */ if (value.length() == 2) { - m_info->m_targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); + m_info->targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); } - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); + QTimer::singleShot(200, this, [this] { + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); + }); break; case Idm::CurrentPowerConsumptionHeatPump: /* qCDebug(dcIdm()) << "received power consumption heat pump"; */ if (value.length() == 2) { - m_info->m_powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); + m_info->powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); } /* Everything read without an error * -> set connected to true */ - m_info->m_connected = true; + m_info->connected = true; emit statusUpdated(m_info); break; } @@ -141,10 +151,10 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const void Idm::onModbusError() { - qCDebug(dcIdm()) << "Received modbus error"; + qCDebug(dcIdm()) << "iDM: Received modbus error"; if (m_info != nullptr) { - m_info->m_connected = false; + m_info->connected = false; emit statusUpdated(m_info); } } diff --git a/idm/idm.h b/idm/idm.h index b2f142b..2bd5ecb 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -68,12 +68,10 @@ class Idm : public QObject { Q_OBJECT public: - /** Constructor */ explicit Idm(const QHostAddress &address, QObject *parent = nullptr); - - /** Destructor */ ~Idm(); + bool connectDevice(); QHostAddress getIdmAddress() const {return m_hostAddress;}; private: diff --git a/idm/idminfo.h b/idm/idminfo.h index 1139f8a..fcd29f2 100644 --- a/idm/idminfo.h +++ b/idm/idminfo.h @@ -40,37 +40,37 @@ struct IdmInfo { /** Set to true, if register values can be read, * false in case of communication problems */ - bool m_connected; + bool connected; - bool m_power; + bool power; /** RegisterList::OutsideTemperature */ - double m_roomTemperature; + double roomTemperature; /** RegisterList::ExternalOutsideTemperature */ - double m_outsideTemperature; + double outsideTemperature; /** RegisterList::HeatStorageTemperature */ - double m_waterTemperature; + double waterTemperature; /** RegisterList::TargetRoomTemperatureZ1R1 (zone 1, room 1) */ - double m_targetRoomTemperature; + double targetRoomTemperature; /** RegisterList::TargetHotWaterTemperature */ - double m_targetWaterTemperature; + double targetWaterTemperature; /** RegisterList::HumiditySensor */ - double m_humidity; + double humidity; /** RegisterList::CurrentPowerConsumptionHeatPump */ - double m_powerConsumptionHeatPump; + double powerConsumptionHeatPump; /** RegisterList::OperationModeSystem */ - QString m_mode; + QString mode; /** True if there is an error code set * (RegisterList::CurrentFaultNumber != 0) */ - bool m_error; + bool error; }; Q_DECLARE_METATYPE(IdmInfo); diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 58344f8..0fa74dc 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -51,69 +51,63 @@ void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) // Just report no error for now, until the above question // is clarified info->finish(Thing::ThingErrorNoError); + } else { + Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); } } void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) { - qCDebug(dcIdm()) << "setupThing called"; - Thing *thing = info->thing(); + qCDebug(dcIdm()) << "setupThing called" << thing->name(); if (thing->thingClassId() == navigator2ThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); - if (hostAddress.isNull() == false) { - QString msg = "User entered address: "; - msg += hostAddress.toString(); - - qCDebug(dcIdm()) << msg; - - /* Check, if address is already in use for another device */ - bool found = false; - - for (QHash::iterator item=m_idmConnections.begin(); item != m_idmConnections.end(); item++) { - if (hostAddress.isEqual(item.value()->getIdmAddress())) { - msg = "Addr of thing: "; - msg += item.value()->getIdmAddress().toString(); - qCDebug(dcIdm()) << msg; - - qCDebug(dcIdm()) << "Address in use already"; - found = true; - } else { - msg = "Different addr of other thing: "; - msg += item.value()->getIdmAddress().toString(); - qCDebug(dcIdm()) << msg; - } - } - - if (found == false) { - qCDebug(dcIdm()) << "Creating Idm object"; - - /* Create new Idm object and store it in hash table */ - Idm *idm = new Idm(hostAddress, this); - m_idmConnections.insert(thing, idm); - - /* Store thing info in hash table */ - m_idmInfos.insert(thing, info); - - msg = "Added IDM heatpump with address "; - msg += hostAddress.toString(); - - info->thing()->setStateValue(navigator2ConnectedStateTypeId, true); - info->finish(Thing::ThingErrorNoError, msg.toStdString().c_str()); - } else { - info->finish(Thing::ThingErrorThingInUse, "IP address already in use"); - } - } else { + if (hostAddress.isNull()) { + qCWarning(dcIdm()) << "Setup failed, IP address not valid"; info->finish(Thing::ThingErrorInvalidParameter, "No IP address given"); + return; } + + if (m_idmConnections.contains(thing)) { + qCDebug(dcIdm()) << "Cleaning up after reconfiguration"; + m_idmConnections.take(thing)->deleteLater(); + } + + qCDebug(dcIdm()) << "User entered address: " << hostAddress.toString(); + + /* Check, if address is already in use for another device */ + Q_FOREACH (Idm *idm, m_idmConnections) { + if (hostAddress.isEqual(idm->getIdmAddress())) { + + qCWarning(dcIdm()) << "Address already in use"; + info->finish(Thing::ThingErrorSetupFailed, "IP address already in use"); + return; + } + } + + qCDebug(dcIdm()) << "Creating Idm object"; + /* Create new Idm object and store it in hash table */ + Idm *idm = new Idm(hostAddress, this); + m_idmConnections.insert(thing, idm); + connect(idm, &Idm::statusUpdated, info, [info] (IdmInfo *idmInfo) { + if (idmInfo->connected) { + info->finish(Thing::ThingErrorNoError); + } + }); + connect(idm, &Idm::destroyed, this, [thing, this] {m_idmConnections.remove(thing);}); + connect(info, &ThingSetupInfo::aborted, idm, &Idm::deleteLater); + connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); + + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } void IntegrationPluginIdm::postSetupThing(Thing *thing) { - qCDebug(dcIdm()) << "postSetupThing called"; + qCDebug(dcIdm()) << "postSetupThing called" << thing->name(); if (!m_refreshTimer) { m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); @@ -125,7 +119,6 @@ void IntegrationPluginIdm::postSetupThing(Thing *thing) Idm *idm = m_idmConnections.value(thing); if (idm != nullptr) { - connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); qCDebug(dcIdm()) << "Thing set up, calling update"; update(thing); @@ -137,8 +130,12 @@ void IntegrationPluginIdm::postSetupThing(Thing *thing) void IntegrationPluginIdm::thingRemoved(Thing *thing) { - if (m_idmConnections.contains(thing)) { - m_idmConnections.take(thing)->deleteLater(); + qCDebug(dcIdm()) << "thingRemoved called" << thing->name(); + + if (thing->thingClassId() == navigator2ThingClassId) { + if (m_idmConnections.contains(thing)) { + m_idmConnections.take(thing)->deleteLater(); + } } } @@ -148,10 +145,10 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) Action action = info->action(); if (thing->thingClassId() == navigator2ThingClassId) { - if (action.actionTypeId() == navigator2PowerActionTypeId) { - - } else { - Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); + if (action.actionTypeId() == navigator2PowerActionTypeId) { + + } 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()); @@ -164,11 +161,11 @@ void IntegrationPluginIdm::update(Thing *thing) qCDebug(dcIdm()) << "Updating thing"; Idm *idm = m_idmConnections.value(thing); - Q_UNUSED(idm); if (idm != nullptr) { idm->onRequestStatus(); } +<<<<<<< HEAD if (m_idmInfos.contains(thing)) { <<<<<<< HEAD @@ -180,6 +177,8 @@ void IntegrationPluginIdm::update(Thing *thing) /* info->finish(Thing::ThingErrorNoError); */ >>>>>>> 3b5ab5c... Another fix for previous commit } +======= +>>>>>>> 27a88b6... Removed thread delay and used Timer instead } } @@ -199,16 +198,16 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) /* Received a structure holding the status info of the * heat pump. Update the thing states with the individual fields. */ - thing->setStateValue(navigator2ConnectedStateTypeId, info->m_connected); - thing->setStateValue(navigator2PowerStateTypeId, info->m_power); - thing->setStateValue(navigator2TemperatureStateTypeId, info->m_roomTemperature); - thing->setStateValue(navigator2OutsideAirTemperatureStateTypeId, info->m_outsideTemperature); - thing->setStateValue(navigator2WaterTemperatureStateTypeId, info->m_waterTemperature); - thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->m_targetRoomTemperature); - thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->m_targetWaterTemperature); - thing->setStateValue(navigator2CurrentPowerConsumptionHeatPumpStateTypeId, info->m_powerConsumptionHeatPump); - thing->setStateValue(navigator2ModeStateTypeId, info->m_mode); - thing->setStateValue(navigator2ErrorStateTypeId, info->m_error); + thing->setStateValue(navigator2ConnectedStateTypeId, info->connected); + thing->setStateValue(navigator2PowerStateTypeId, info->power); + thing->setStateValue(navigator2TemperatureStateTypeId, info->roomTemperature); + thing->setStateValue(navigator2OutsideAirTemperatureStateTypeId, info->outsideTemperature); + thing->setStateValue(navigator2WaterTemperatureStateTypeId, info->waterTemperature); + thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->targetRoomTemperature); + thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->targetWaterTemperature); + thing->setStateValue(navigator2CurrentPowerConsumptionHeatPumpStateTypeId, info->powerConsumptionHeatPump); + thing->setStateValue(navigator2ModeStateTypeId, info->mode); + thing->setStateValue(navigator2ErrorStateTypeId, info->error); } void IntegrationPluginIdm::onRefreshTimer() diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 42b9627..6ce157e 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -62,7 +62,6 @@ private: PluginTimer *m_refreshTimer = nullptr; QHash m_idmConnections; - QHash m_idmInfos; QHash m_asyncActions; void onRefreshTimer(); diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 9ed02ed..0b68065 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -85,18 +85,18 @@ "displayNameEvent": "Target water temperature changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 46.00 + "defaultValue": 0.00 }, { "id": "b98fb325-100d-4eae-bf8d-97e8f7e1eb00", "name": "currentPowerConsumptionHeatPump", - "displayName": "Curr. power consumption", + "displayName": "Current power consumption", "displayNameEvent": "Current power consumption heat pump changed", "displayNameAction": "Change current power consumption het pump", "type": "double", "unit": "KiloWatt", - "defaultValue": 0 + "defaultValue": 0.00 }, { "id": "e539366b-44da-4119-b11b-497bcdb1f522", @@ -121,8 +121,6 @@ "type": "bool", "defaultValue": false } - ], - "actionTypes": [ ] } ] From b36f91e34eb33e7f9501c91c12346c296a979d4c Mon Sep 17 00:00:00 2001 From: Boernsman Date: Tue, 2 Feb 2021 15:43:42 +0100 Subject: [PATCH 21/30] Removed discovery, fixed setup bug --- idm/README.md | 19 +++++++++++-------- idm/integrationpluginidm.cpp | 26 +++++--------------------- idm/integrationpluginidm.h | 1 - idm/integrationpluginidm.json | 2 +- 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/idm/README.md b/idm/README.md index 74a43f4..19b4cb2 100644 --- a/idm/README.md +++ b/idm/README.md @@ -1,15 +1,18 @@ # iDM +Connect nymea to iDM heat pumps. + ## Supported Things +* Navigator 2.0 based heat pump models + +## Requirements + +* The package 'nymea-plugin-idm' must be installed +* Navigator 2.0 settings + * "Modbus TCP" must be selected in the "Building Management System" menu? +* Both devices must be in the same local area network. + ## More https://www.idm-energie.at/en/ - -** Modbus TCP communication not working ** - * Is "Modbus TCP" selected in the "Building Management System" menu? - * Is the Modbus TCP device and the heat pump in the same network? - * Is there an IP address conflict? - * Has the heat pump set the IP address manually? IP address should be set manually, because with "DHCP" the IP address can change (e.g. after a power failure). - * Was the connection made via a switch, possibly blocking this communication? If so, integrate the Modbus TCP device directly (without a switch). - diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 0fa74dc..b0efe1f 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -36,26 +36,6 @@ IntegrationPluginIdm::IntegrationPluginIdm() } -void IntegrationPluginIdm::discoverThings(ThingDiscoveryInfo *info) -{ - qCDebug(dcIdm()) << "discoverThings called"; - - if (info->thingClassId() == navigator2ThingClassId) { - // TODO Is a discovery method actually possible? - // The plugin has a parameter for the IP address - - QString description = "Navigator 2"; - ThingDescriptor descriptor(info->thingClassId(), "Idm", description); - info->addThingDescriptor(descriptor); - - // Just report no error for now, until the above question - // is clarified - info->finish(Thing::ThingErrorNoError); - } else { - Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); - } -} - void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); @@ -80,7 +60,6 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) /* Check, if address is already in use for another device */ Q_FOREACH (Idm *idm, m_idmConnections) { if (hostAddress.isEqual(idm->getIdmAddress())) { - qCWarning(dcIdm()) << "Address already in use"; info->finish(Thing::ThingErrorSetupFailed, "IP address already in use"); return; @@ -90,6 +69,11 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) qCDebug(dcIdm()) << "Creating Idm object"; /* Create new Idm object and store it in hash table */ Idm *idm = new Idm(hostAddress, this); + if (idm->connectDevice()) { + qCWarning(dcIdm()) << "Could not connect to thing"; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } m_idmConnections.insert(thing, idm); connect(idm, &Idm::statusUpdated, info, [info] (IdmInfo *idmInfo) { if (idmInfo->connected) { diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 6ce157e..e44f29d 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -51,7 +51,6 @@ public: /** Constructor */ explicit IntegrationPluginIdm(); - void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 0b68065..79617ff 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -12,7 +12,7 @@ "name": "navigator2", "displayName": "Navigator 2.0", "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", - "createMethods": ["User"], + "createMethods": ["user"], "interfaces": ["temperaturesensor", "connectable"], "paramTypes": [ { From c64012505ddc16f1a06dee4d3a7fb4f6c072d173 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Fri, 5 Feb 2021 13:50:28 +0100 Subject: [PATCH 22/30] fixed rebase --- idm/integrationpluginidm.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index b0efe1f..66483d6 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -149,20 +149,6 @@ void IntegrationPluginIdm::update(Thing *thing) if (idm != nullptr) { idm->onRequestStatus(); } -<<<<<<< HEAD - - if (m_idmInfos.contains(thing)) { -<<<<<<< HEAD - ThingSetupInfo *info = m_idmInfos.take(thing); - info->finish(Thing::ThingErrorNoError); -======= - /* ThingSetupInfo *info = m_idmInfos.take(thing); */ - /* qCDebug(dcIdm()) << "Finishing setup 4!"; */ - /* info->finish(Thing::ThingErrorNoError); */ ->>>>>>> 3b5ab5c... Another fix for previous commit - } -======= ->>>>>>> 27a88b6... Removed thread delay and used Timer instead } } From 31e1169d4dc657f1397048c6b4f3513e8f02ffef Mon Sep 17 00:00:00 2001 From: Boernsman Date: Fri, 5 Feb 2021 13:55:41 +0100 Subject: [PATCH 23/30] fixed debian install file --- debian/nymea-plugin-idm.install.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/nymea-plugin-idm.install.in b/debian/nymea-plugin-idm.install.in index 5ef2f03..f215b60 100644 --- a/debian/nymea-plugin-idm.install.in +++ b/debian/nymea-plugin-idm.install.in @@ -1 +1 @@ -usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginidm.so +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginidm.so From ccdb0c371fa74f0d774c0aa4db4bf836956d761b Mon Sep 17 00:00:00 2001 From: Boernsman Date: Sun, 2 May 2021 11:45:44 +0200 Subject: [PATCH 24/30] fixed wrong modbus tcp signals --- modbus/modbustcpmaster.cpp | 18 +++++++++--------- modbus/modbustcpmaster.h | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 3220842..22b035e 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -131,20 +131,20 @@ QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint si connect(reply, &QModbusReply::finished, this, [reply, requestId, this] { if (reply->error() == QModbusDevice::NoError) { - writeRequestExecuted(requestId, true); + emit readRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values()); } else { - writeRequestExecuted(requestId, false); + emit readRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); } }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; - emit writeRequestError(requestId, reply->errorString()); + emit readRequestError(requestId, reply->errorString()); reply->finished(); // To make sure it will be deleted }); QTimer::singleShot(200, reply, &QModbusReply::deleteLater); @@ -218,13 +218,13 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress connect(reply, &QModbusReply::finished, this, [reply, requestId, this] { if (reply->error() == QModbusDevice::NoError) { - writeRequestExecuted(requestId, true); + emit readRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedDiscreteInput(reply->serverAddress(), modbusAddress, unit.values()); } else { - writeRequestExecuted(requestId, false); + emit readRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); } }); @@ -232,7 +232,7 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress qCWarning(dcModbusTCP()) << "Modbus replay error:" << error; QModbusReply *reply = qobject_cast(sender()); - emit writeRequestError(requestId, reply->errorString()); + emit readRequestError(requestId, reply->errorString()); reply->finished(); // To make sure it will be deleted }); QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); @@ -262,20 +262,20 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress connect(reply, &QModbusReply::finished, this, [reply, requestId, this] { reply->deleteLater(); if (reply->error() == QModbusDevice::NoError) { - writeRequestExecuted(requestId, true); + emit readRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values()); } else { - writeRequestExecuted(requestId, false); + emit readRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); } }); connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; - emit writeRequestError(requestId, reply->errorString()); + emit readRequestError(requestId, reply->errorString()); reply->finished(); // To make sure it will be deleted }); QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index bede700..977be48 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -82,6 +82,8 @@ signals: void writeRequestExecuted(const QUuid &requestId, bool success); void writeRequestError(const QUuid &requestId, const QString &error); + + void readRequestExecuted(const QUuid &requestId, bool success); void readRequestError(const QUuid &requestId, const QString &error); void receivedCoil(uint slaveAddress, uint modbusRegister, const QVector &values); From f004899127460dcd3fae1fa3be4e88a790231fcd Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 10 May 2021 02:16:59 +0200 Subject: [PATCH 25/30] changed interface, fixed remarks from reviewer --- idm/idm.cpp | 51 ++++++++++++++++------------------- idm/idm.h | 11 ++++---- idm/idminfo.h | 2 +- idm/integrationpluginidm.cpp | 50 +++++++++++++++++++--------------- idm/integrationpluginidm.h | 7 ++--- idm/integrationpluginidm.json | 12 +++++---- modbus/modbushelpers.cpp | 5 +--- modbus/modbushelpers.h | 6 ++--- 8 files changed, 71 insertions(+), 73 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index 0045365..c21bd24 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -33,7 +33,6 @@ #include "../modbus/modbushelpers.h" #include -#include Idm::Idm(const QHostAddress &address, QObject *parent) : QObject(parent), @@ -61,6 +60,11 @@ bool Idm::connectDevice() return m_modbusMaster->connectDevice(); } +QHostAddress Idm::getIdmAddress() const +{ + return m_hostAddress; +} + void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); @@ -70,7 +74,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::OutsideTemperature: /* qCDebug(dcIdm()) << "received outside temperature"; */ if (value.length() == 2) { - m_info->outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); + m_info.outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); } QTimer::singleShot(200, this, [this] { m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); @@ -80,9 +84,9 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const /* qCDebug(dcIdm()) << "current fault number"; */ if (value.length() == 1) { if (value[0] > 0) { - m_info->error = true; + m_info.error = true; } else { - m_info->error = false; + m_info.error = false; } } QTimer::singleShot(200, this, [this] { @@ -92,7 +96,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::HeatStorageTemperature: /* qCDebug(dcIdm()) << "received storage temperature"; */ if (value.length() == 2) { - m_info->waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); + m_info.waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); } QTimer::singleShot(200, this, [this] { m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 1); @@ -102,7 +106,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const /* qCDebug(dcIdm()) << "received target hot water temperature"; */ if (value.length() == 1) { /* The hot water target temperature is stored as UCHAR (manual p. 13) */ - m_info->targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; + m_info.targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; } QTimer::singleShot(200, this, [this] { m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); @@ -111,7 +115,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::HeatPumpOperatingMode: /* qCDebug(dcIdm()) << "received heat pump operating mode"; */ if (value.length() == 1) { - m_info->mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); + m_info.mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } QTimer::singleShot(200, this, [this] { m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); @@ -120,7 +124,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::RoomTemperatureHKA: /* qCDebug(dcIdm()) << "received room temperature hka"; */ if (value.length() == 2) { - m_info->roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); + m_info.roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); } QTimer::singleShot(200, this, [this] { m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingEcoHKA, 2); @@ -129,7 +133,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::RoomTemperatureTargetHeatingEcoHKA: /* qCDebug(dcIdm()) << "received room temprature hka eco"; */ if (value.length() == 2) { - m_info->targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); + m_info.targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); } QTimer::singleShot(200, this, [this] { m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); @@ -138,12 +142,12 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::CurrentPowerConsumptionHeatPump: /* qCDebug(dcIdm()) << "received power consumption heat pump"; */ if (value.length() == 2) { - m_info->powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); + m_info.powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); } /* Everything read without an error * -> set connected to true */ - m_info->connected = true; + m_info.connected = true; emit statusUpdated(m_info); break; } @@ -152,24 +156,17 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const void Idm::onModbusError() { qCDebug(dcIdm()) << "iDM: Received modbus error"; - - if (m_info != nullptr) { - m_info->connected = false; - emit statusUpdated(m_info); - } + m_info.connected = false; + emit statusUpdated(m_info); } void Idm::onRequestStatus() { - m_info = new IdmInfo; - - QUuid reqId{}; - reqId = m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::OutsideTemperature, 2); - - /* qCDebug(dcIdm()) << "Request id: " << reqId; */ + m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::OutsideTemperature, 2); } -QString Idm::systemOperationModeToString(IdmSysMode mode) { +QString Idm::systemOperationModeToString(IdmSysMode mode) +{ QString result{}; /* Operation modes according to table of manual p. 13 */ @@ -190,13 +187,12 @@ QString Idm::systemOperationModeToString(IdmSysMode mode) { result = "Nur Heizung/Kühlung"; break; } - return result; } -QString Idm::heatPumpOperationModeToString(IdmHeatPumpMode mode) { +QString Idm::heatPumpOperationModeToString(IdmHeatPumpMode mode) +{ QString result{}; - /* Operation modes according to table of manual p. 14 */ switch (mode) { case IdmHeatPumpModeOff: @@ -215,7 +211,6 @@ QString Idm::heatPumpOperationModeToString(IdmHeatPumpMode mode) { result = "Defrost"; break; } - return result; } diff --git a/idm/idm.h b/idm/idm.h index 2bd5ecb..b9699d4 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -72,12 +72,13 @@ public: ~Idm(); bool connectDevice(); - QHostAddress getIdmAddress() const {return m_hostAddress;}; + QHostAddress getIdmAddress() const; + bool setTargetTemperature; private: /** Modbus Unit ID of Idm device */ - static const quint16 ModbusUnitID = 1; + static const quint16 ModbusUnitID = 1; enum IscModus { KeineAbwarme = 0, @@ -164,7 +165,7 @@ private: /** This structure is allocated within onRequestStatus and filled * by the receivedStatusGroupx functions */ - IdmInfo *m_info = nullptr; + IdmInfo m_info; /** Converts a system operation mode code to a string (according to manual p. 13) */ QString systemOperationModeToString(IdmSysMode mode); @@ -173,7 +174,7 @@ private: QString heatPumpOperationModeToString(IdmHeatPumpMode mode); signals: - void statusUpdated(IdmInfo *info); + void statusUpdated(const IdmInfo &info); void targetRoomTemperatureChanged(); public slots: diff --git a/idm/idminfo.h b/idm/idminfo.h index fcd29f2..6e8d7f1 100644 --- a/idm/idminfo.h +++ b/idm/idminfo.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 66483d6..5e02ebb 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -44,9 +44,10 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) if (thing->thingClassId() == navigator2ThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); - if (hostAddress.isNull()) { + + if (!hostAddress.isNull()) { qCWarning(dcIdm()) << "Setup failed, IP address not valid"; - info->finish(Thing::ThingErrorInvalidParameter, "No IP address given"); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); return; } @@ -75,8 +76,8 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) return; } m_idmConnections.insert(thing, idm); - connect(idm, &Idm::statusUpdated, info, [info] (IdmInfo *idmInfo) { - if (idmInfo->connected) { + connect(idm, &Idm::statusUpdated, info, [info] (const IdmInfo &idmInfo) { + if (idmInfo.connected) { info->finish(Thing::ThingErrorNoError); } }); @@ -94,6 +95,7 @@ void IntegrationPluginIdm::postSetupThing(Thing *thing) qCDebug(dcIdm()) << "postSetupThing called" << thing->name(); if (!m_refreshTimer) { + qCDebug(dcIdm()) << "Starting refresh timer"; m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginIdm::onRefreshTimer); } @@ -121,6 +123,12 @@ void IntegrationPluginIdm::thingRemoved(Thing *thing) m_idmConnections.take(thing)->deleteLater(); } } + + if (myThings().isEmpty()) { + qCDebug(dcIdm()) << "Stopping refresh timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } } void IntegrationPluginIdm::executeAction(ThingActionInfo *info) @@ -129,7 +137,9 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) Action action = info->action(); if (thing->thingClassId() == navigator2ThingClassId) { - if (action.actionTypeId() == navigator2PowerActionTypeId) { + if (action.actionTypeId() == navigator2TargetTemperatureActionTypeId) { + double targetTemperature = thing->stateValue(navigator2TargetTemperatureStateTypeId).toDouble(); + Q_UNUSED(targetTemperature); } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); @@ -152,15 +162,11 @@ void IntegrationPluginIdm::update(Thing *thing) } } -void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) +void IntegrationPluginIdm::onStatusUpdated(const IdmInfo &info) { - /* qCDebug(dcIdm()) << "onStatusUpdated"; */ - if (!info) - return; - qCDebug(dcIdm()) << "Received status from heat pump"; - Idm *idm = static_cast(sender()); + Idm *idm = qobject_cast(sender()); Thing *thing = m_idmConnections.key(idm); if (!thing) @@ -168,16 +174,16 @@ void IntegrationPluginIdm::onStatusUpdated(IdmInfo *info) /* Received a structure holding the status info of the * heat pump. Update the thing states with the individual fields. */ - thing->setStateValue(navigator2ConnectedStateTypeId, info->connected); - thing->setStateValue(navigator2PowerStateTypeId, info->power); - thing->setStateValue(navigator2TemperatureStateTypeId, info->roomTemperature); - thing->setStateValue(navigator2OutsideAirTemperatureStateTypeId, info->outsideTemperature); - thing->setStateValue(navigator2WaterTemperatureStateTypeId, info->waterTemperature); - thing->setStateValue(navigator2TargetTemperatureStateTypeId, info->targetRoomTemperature); - thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info->targetWaterTemperature); - thing->setStateValue(navigator2CurrentPowerConsumptionHeatPumpStateTypeId, info->powerConsumptionHeatPump); - thing->setStateValue(navigator2ModeStateTypeId, info->mode); - thing->setStateValue(navigator2ErrorStateTypeId, info->error); + thing->setStateValue(navigator2ConnectedStateTypeId, info.connected); + thing->setStateValue(navigator2PowerStateTypeId, info.power); + thing->setStateValue(navigator2TemperatureStateTypeId, info.roomTemperature); + thing->setStateValue(navigator2OutsideAirTemperatureStateTypeId, info.outsideTemperature); + thing->setStateValue(navigator2WaterTemperatureStateTypeId, info.waterTemperature); + thing->setStateValue(navigator2TargetTemperatureStateTypeId, info.targetRoomTemperature); + thing->setStateValue(navigator2TargetWaterTemperatureStateTypeId, info.targetWaterTemperature); + thing->setStateValue(navigator2CurrentPowerConsumptionHeatPumpStateTypeId, info.powerConsumptionHeatPump); + thing->setStateValue(navigator2ModeStateTypeId, info.mode); + thing->setStateValue(navigator2ErrorStateTypeId, info.error); } void IntegrationPluginIdm::onRefreshTimer() diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index e44f29d..351be4c 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -46,27 +46,24 @@ class IntegrationPluginIdm: public IntegrationPlugin Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginidm.json") Q_INTERFACES(IntegrationPlugin) - public: - /** Constructor */ explicit IntegrationPluginIdm(); void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void executeAction(ThingActionInfo *info) override; - void update(Thing *thing); private: - PluginTimer *m_refreshTimer = nullptr; QHash m_idmConnections; QHash m_asyncActions; + void update(Thing *thing); void onRefreshTimer(); private slots: - void onStatusUpdated(IdmInfo *info); + void onStatusUpdated(const IdmInfo &info); }; #endif // INTEGRATIONPLUGINIDM_H diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json index 79617ff..e66b23e 100644 --- a/idm/integrationpluginidm.json +++ b/idm/integrationpluginidm.json @@ -13,7 +13,7 @@ "displayName": "Navigator 2.0", "id": "1c95ac91-4eca-4cbf-b0f4-d60d35d069ed", "createMethods": ["user"], - "interfaces": ["temperaturesensor", "connectable"], + "interfaces": ["thermostat", "connectable"], "paramTypes": [ { "id": "05714e5c-d66a-4095-bbff-a0eb96fb035b", @@ -37,10 +37,8 @@ "name": "power", "displayName": "Power", "displayNameEvent": "Power changed", - "displayNameAction": "Change power", "type": "bool", - "defaultValue": 0, - "writable": true + "defaultValue": false }, { "id": "f0f596bf-7e45-43ea-b3d4-767b82dd422a", @@ -73,10 +71,14 @@ "id": "efae7493-68c3-4cb9-853c-81011bdf09ca", "name": "targetTemperature", "displayName": "Target room temperature", + "displayNameAction": "Set target room temperature", "displayNameEvent": "Target room temperature changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 22.00 + "minValue": "18", + "maxValue": "28", + "defaultValue": 22.00, + "writable": true }, { "id": "746244d6-dd37-4af8-b2ae-a7d8463e51e2", diff --git a/modbus/modbushelpers.cpp b/modbus/modbushelpers.cpp index ee26a03..8e69cf2 100644 --- a/modbus/modbushelpers.cpp +++ b/modbus/modbushelpers.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -30,8 +30,6 @@ #include "modbushelpers.h" -#include - float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { float result = 0.0; @@ -46,7 +44,6 @@ float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { /* needs to be done with char * to avoid pedantic compiler errors */ memcpy((char *)&result, (char *)&tmp, sizeof(result)); } - return result; } diff --git a/modbus/modbushelpers.h b/modbus/modbushelpers.h index acea6b8..540daed 100644 --- a/modbus/modbushelpers.h +++ b/modbus/modbushelpers.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -36,8 +36,8 @@ class ModbusHelpers { public: - static float convertRegisterToFloat(const quint16 *reg); - static void convertFloatToRegister(QVector ®, float value); + static float convertRegisterToFloat(const quint16 *reg); + static void convertFloatToRegister(QVector ®, float value); }; #endif From 4c1ec41a15111caefeee671124e38af102e5ad14 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 10 May 2021 11:19:40 +0200 Subject: [PATCH 26/30] added set target temperature action --- idm/idm.cpp | 58 ++++++++++++++++++++---------------- idm/idm.h | 14 ++++----- idm/integrationpluginidm.cpp | 52 +++++++++++++++++++++----------- idm/integrationpluginidm.h | 3 +- modbus/modbushelpers.cpp | 7 +++-- modbus/modbushelpers.h | 2 +- modbus/modbustcpmaster.cpp | 9 ++++-- modbus/modbustcpmaster.h | 6 ++-- 8 files changed, 94 insertions(+), 57 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index c21bd24..799a3de 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -46,6 +46,7 @@ Idm::Idm(const QHostAddress &address, QObject *parent) : connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &Idm::onReceivedHoldingRegister); connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &Idm::onModbusError); connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &Idm::onModbusError); + connect(m_modbusMaster, &ModbusTCPMaster::writeRequestExecuted, this, &Idm::writeRequestExecuted); } } @@ -65,6 +66,18 @@ QHostAddress Idm::getIdmAddress() const return m_hostAddress; } +void Idm::getStatus() +{ + //this request starts an update cycle + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::OutsideTemperature, 2); +} + +QUuid Idm::setTargetTemperature(double targetTemperature) +{ + QVector value = ModbusHelpers::convertFloatToRegister(targetTemperature); + return m_modbusMaster->writeHoldingRegisters(Idm::modbusUnitID, Idm::RegisterList::RoomTemperatureTargetHeatingEcoHKA, value); +} + void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); @@ -74,81 +87,81 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const case Idm::OutsideTemperature: /* qCDebug(dcIdm()) << "received outside temperature"; */ if (value.length() == 2) { - m_info.outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); + m_idmInfo.outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentFaultNumber, 1); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::CurrentFaultNumber, 1); }); break; case Idm::CurrentFaultNumber: /* qCDebug(dcIdm()) << "current fault number"; */ if (value.length() == 1) { if (value[0] > 0) { - m_info.error = true; + m_idmInfo.error = true; } else { - m_info.error = false; + m_idmInfo.error = false; } } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatStorageTemperature, 2); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::HeatStorageTemperature, 2); }); break; case Idm::HeatStorageTemperature: /* qCDebug(dcIdm()) << "received storage temperature"; */ if (value.length() == 2) { - m_info.waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); + m_idmInfo.waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::TargetHotWaterTemperature, 1); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::TargetHotWaterTemperature, 1); }); break; case Idm::TargetHotWaterTemperature: /* qCDebug(dcIdm()) << "received target hot water temperature"; */ if (value.length() == 1) { /* The hot water target temperature is stored as UCHAR (manual p. 13) */ - m_info.targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; + m_idmInfo.targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::HeatPumpOperatingMode, 1); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::HeatPumpOperatingMode, 1); }); break; case Idm::HeatPumpOperatingMode: /* qCDebug(dcIdm()) << "received heat pump operating mode"; */ if (value.length() == 1) { - m_info.mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); + m_idmInfo.mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureHKA, 2); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::RoomTemperatureHKA, 2); }); break; case Idm::RoomTemperatureHKA: /* qCDebug(dcIdm()) << "received room temperature hka"; */ if (value.length() == 2) { - m_info.roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); + m_idmInfo.roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::RoomTemperatureTargetHeatingEcoHKA, 2); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::RoomTemperatureTargetHeatingEcoHKA, 2); }); break; case Idm::RoomTemperatureTargetHeatingEcoHKA: /* qCDebug(dcIdm()) << "received room temprature hka eco"; */ if (value.length() == 2) { - m_info.targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); + m_idmInfo.targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); } QTimer::singleShot(200, this, [this] { - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); + m_modbusMaster->readHoldingRegister(Idm::modbusUnitID, Idm::CurrentPowerConsumptionHeatPump, 2); }); break; case Idm::CurrentPowerConsumptionHeatPump: /* qCDebug(dcIdm()) << "received power consumption heat pump"; */ if (value.length() == 2) { - m_info.powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); + m_idmInfo.powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); } /* Everything read without an error * -> set connected to true */ - m_info.connected = true; - emit statusUpdated(m_info); + m_idmInfo.connected = true; + emit statusUpdated(m_idmInfo); break; } } @@ -156,13 +169,8 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const void Idm::onModbusError() { qCDebug(dcIdm()) << "iDM: Received modbus error"; - m_info.connected = false; - emit statusUpdated(m_info); -} - -void Idm::onRequestStatus() -{ - m_modbusMaster->readHoldingRegister(Idm::ModbusUnitID, Idm::OutsideTemperature, 2); + m_idmInfo.connected = false; + emit statusUpdated(m_idmInfo); } QString Idm::systemOperationModeToString(IdmSysMode mode) diff --git a/idm/idm.h b/idm/idm.h index b9699d4..6e669ce 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -73,12 +73,12 @@ public: bool connectDevice(); QHostAddress getIdmAddress() const; - bool setTargetTemperature; + QUuid setTargetTemperature(double targetTemperature); + void getStatus(); private: - /** Modbus Unit ID of Idm device */ - static const quint16 ModbusUnitID = 1; + static const quint16 modbusUnitID = 1; enum IscModus { KeineAbwarme = 0, @@ -160,12 +160,12 @@ private: * within the IntegrationPluginIdm class. */ QHostAddress m_hostAddress; - /** Pointer to ModbusTCPMaster object, responseible for low-level communicaiton */ + /** Pointer to ModbusTCPMaster object, responsible for low-level communicaiton */ ModbusTCPMaster *m_modbusMaster = nullptr; /** This structure is allocated within onRequestStatus and filled * by the receivedStatusGroupx functions */ - IdmInfo m_info; + IdmInfo m_idmInfo; /** Converts a system operation mode code to a string (according to manual p. 13) */ QString systemOperationModeToString(IdmSysMode mode); @@ -176,10 +176,10 @@ private: signals: void statusUpdated(const IdmInfo &info); void targetRoomTemperatureChanged(); + void writeRequestExecuted(const QUuid &requestId, bool success); -public slots: +private slots: void onModbusError(); - void onRequestStatus(); void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); }; diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 5e02ebb..db4a875 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -62,7 +62,7 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) Q_FOREACH (Idm *idm, m_idmConnections) { if (hostAddress.isEqual(idm->getIdmAddress())) { qCWarning(dcIdm()) << "Address already in use"; - info->finish(Thing::ThingErrorSetupFailed, "IP address already in use"); + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("IP address already in use")); return; } } @@ -75,15 +75,18 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) info->finish(Thing::ThingErrorHardwareNotAvailable); return; } - m_idmConnections.insert(thing, idm); - connect(idm, &Idm::statusUpdated, info, [info] (const IdmInfo &idmInfo) { + + connect(idm, &Idm::statusUpdated, info, [info, thing, idm, this] (const IdmInfo &idmInfo) { if (idmInfo.connected) { + m_idmConnections.insert(thing, idm); + connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); + connect(idm, &Idm::writeRequestExecuted, this, &IntegrationPluginIdm::onWriteRequestExecuted); info->finish(Thing::ThingErrorNoError); } }); connect(idm, &Idm::destroyed, this, [thing, this] {m_idmConnections.remove(thing);}); connect(info, &ThingSetupInfo::aborted, idm, &Idm::deleteLater); - connect(idm, &Idm::statusUpdated, this, &IntegrationPluginIdm::onStatusUpdated); + } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); @@ -101,16 +104,14 @@ void IntegrationPluginIdm::postSetupThing(Thing *thing) } if (thing->thingClassId() == navigator2ThingClassId) { - qCDebug(dcIdm()) << "Thing id: " << thing->id(); Idm *idm = m_idmConnections.value(thing); - - if (idm != nullptr) { - - qCDebug(dcIdm()) << "Thing set up, calling update"; - update(thing); - - thing->setStateValue(navigator2ConnectedStateTypeId, true); + if (!idm) { + qCWarning(dcIdm()) << "Could not find any iDM connection for" << thing->name(); + return; } + + thing->setStateValue(navigator2ConnectedStateTypeId, true); + update(thing); } } @@ -137,9 +138,14 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) Action action = info->action(); if (thing->thingClassId() == navigator2ThingClassId) { + Idm *idm = m_idmConnections.value(thing); + if (!idm) { + return info->finish(Thing::ThingErrorHardwareFailure); + } if (action.actionTypeId() == navigator2TargetTemperatureActionTypeId) { double targetTemperature = thing->stateValue(navigator2TargetTemperatureStateTypeId).toDouble(); - Q_UNUSED(targetTemperature); + QUuid requestId = idm->setTargetTemperature(targetTemperature); + m_asyncActions.insert(requestId, info); } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); @@ -152,13 +158,13 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) void IntegrationPluginIdm::update(Thing *thing) { if (thing->thingClassId() == navigator2ThingClassId) { - qCDebug(dcIdm()) << "Updating thing"; + qCDebug(dcIdm()) << "Updating thing" << thing->name(); Idm *idm = m_idmConnections.value(thing); - - if (idm != nullptr) { - idm->onRequestStatus(); + if (!idm) { + return; } + idm->getStatus(); } } @@ -186,6 +192,18 @@ void IntegrationPluginIdm::onStatusUpdated(const IdmInfo &info) thing->setStateValue(navigator2ErrorStateTypeId, info.error); } +void IntegrationPluginIdm::onWriteRequestExecuted(const QUuid &requestId, bool success) +{ + if (m_asyncActions.contains(requestId)) { + ThingActionInfo *info = m_asyncActions.value(requestId); + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable); + } + } +} + void IntegrationPluginIdm::onRefreshTimer() { qCDebug(dcIdm()) << "onRefreshTimer called"; diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h index 351be4c..808edb8 100644 --- a/idm/integrationpluginidm.h +++ b/idm/integrationpluginidm.h @@ -38,7 +38,6 @@ #include - class IntegrationPluginIdm: public IntegrationPlugin { Q_OBJECT @@ -64,6 +63,8 @@ private: private slots: void onStatusUpdated(const IdmInfo &info); + void onWriteRequestExecuted(const QUuid &requestId, bool success); + }; #endif // INTEGRATIONPLUGINIDM_H diff --git a/modbus/modbushelpers.cpp b/modbus/modbushelpers.cpp index 8e69cf2..043b671 100644 --- a/modbus/modbushelpers.cpp +++ b/modbus/modbushelpers.cpp @@ -31,6 +31,7 @@ #include "modbushelpers.h" float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { + float result = 0.0; if (reg != nullptr) { @@ -47,12 +48,14 @@ float ModbusHelpers::convertRegisterToFloat(const quint16 *reg) { return result; } -void ModbusHelpers::convertFloatToRegister(QVector ®, float value) { +QVector ModbusHelpers::convertFloatToRegister(float value) +{ quint32 tmp = 0; - memcpy((char *)&tmp, (char *)&value, sizeof(value)); + QVector reg; reg.append((quint16)(tmp)); reg.append((quint16)((tmp & 0xFFFF0000) >> 16)); + return reg; } diff --git a/modbus/modbushelpers.h b/modbus/modbushelpers.h index 540daed..bae33f3 100644 --- a/modbus/modbushelpers.h +++ b/modbus/modbushelpers.h @@ -37,7 +37,7 @@ class ModbusHelpers { public: static float convertRegisterToFloat(const quint16 *reg); - static void convertFloatToRegister(QVector ®, float value); + static QVector convertFloatToRegister(float value); }; #endif diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 22b035e..1022204 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -63,7 +63,7 @@ ModbusTCPMaster::~ModbusTCPMaster() } bool ModbusTCPMaster::connectDevice() { - // TCP connction to target device + // TCP connection to target device qCDebug(dcModbusTCP()) << "Setting up TCP connecion"; if (!m_modbusTcpClient) @@ -87,6 +87,11 @@ void ModbusTCPMaster::setTimeout(int timeout) m_modbusTcpClient->setTimeout(timeout); } +QString ModbusTCPMaster::errorString() const +{ + return m_modbusTcpClient->errorString(); +} + uint ModbusTCPMaster::port() { return m_modbusTcpClient->connectionParameter(QModbusDevice::NetworkPortParameter).toUInt(); diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index 977be48..ac604fe 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -37,7 +37,7 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(dcModbus) +Q_DECLARE_LOGGING_CATEGORY(dcModbusTcp) class ModbusTCPMaster : public QObject { @@ -51,6 +51,8 @@ public: void setNumberOfRetries(int number); void setTimeout(int timeout); + QString errorString() const; + QUuid readCoil(uint slaveAddress, uint registerAddress, uint size = 1); QUuid readDiscreteInput(uint slaveAddress, uint registerAddress, uint size = 1); QUuid readInputRegister(uint slaveAddress, uint registerAddress, uint size = 1); From 7fed767ff10c45f80226a5ad430d1c1bdc02a82b Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 10 May 2021 11:23:38 +0200 Subject: [PATCH 27/30] added modbusTcp error method --- idm/idm.cpp | 2 +- ...8d86d-d51a-4ad1-a185-91faa017e38f-en_US.ts | 200 ++++++++++++++++++ modbus/modbustcpmaster.cpp | 5 + modbus/modbustcpmaster.h | 1 + 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 idm/translations/3968d86d-d51a-4ad1-a185-91faa017e38f-en_US.ts diff --git a/idm/idm.cpp b/idm/idm.cpp index 799a3de..f2377d5 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -168,7 +168,7 @@ void Idm::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const void Idm::onModbusError() { - qCDebug(dcIdm()) << "iDM: Received modbus error"; + qCDebug(dcIdm()) << "iDM: Received modbus error" << m_modbusMaster->errorString(); m_idmInfo.connected = false; emit statusUpdated(m_idmInfo); } diff --git a/idm/translations/3968d86d-d51a-4ad1-a185-91faa017e38f-en_US.ts b/idm/translations/3968d86d-d51a-4ad1-a185-91faa017e38f-en_US.ts new file mode 100644 index 0000000..3511917 --- /dev/null +++ b/idm/translations/3968d86d-d51a-4ad1-a185-91faa017e38f-en_US.ts @@ -0,0 +1,200 @@ + + + + + Idm + + + + Connected + The name of the ParamType (ThingClass: navigator2, EventType: connected, ID: {cfd71e64-b666-45ef-8db0-8213acd82c5f}) +---------- +The name of the StateType ({cfd71e64-b666-45ef-8db0-8213acd82c5f}) of ThingClass navigator2 + + + + + Connected changed + The name of the EventType ({cfd71e64-b666-45ef-8db0-8213acd82c5f}) of ThingClass navigator2 + + + + + + Current power consumption + The name of the ParamType (ThingClass: navigator2, EventType: currentPowerConsumptionHeatPump, ID: {b98fb325-100d-4eae-bf8d-97e8f7e1eb00}) +---------- +The name of the StateType ({b98fb325-100d-4eae-bf8d-97e8f7e1eb00}) of ThingClass navigator2 + + + + + Current power consumption heat pump changed + The name of the EventType ({b98fb325-100d-4eae-bf8d-97e8f7e1eb00}) of ThingClass navigator2 + + + + + + Error + The name of the ParamType (ThingClass: navigator2, EventType: error, ID: {49fd83ee-ddf3-4477-9ee4-e01c53283b43}) +---------- +The name of the StateType ({49fd83ee-ddf3-4477-9ee4-e01c53283b43}) of ThingClass navigator2 + + + + + Error changed + The name of the EventType ({49fd83ee-ddf3-4477-9ee4-e01c53283b43}) of ThingClass navigator2 + + + + + IP address + The name of the ParamType (ThingClass: navigator2, Type: thing, ID: {05714e5c-d66a-4095-bbff-a0eb96fb035b}) + + + + + + Mode + The name of the ParamType (ThingClass: navigator2, EventType: mode, ID: {e539366b-44da-4119-b11b-497bcdb1f522}) +---------- +The name of the StateType ({e539366b-44da-4119-b11b-497bcdb1f522}) of ThingClass navigator2 + + + + + Mode changed + The name of the EventType ({e539366b-44da-4119-b11b-497bcdb1f522}) of ThingClass navigator2 + + + + + Navigator 2.0 + The name of the ThingClass ({1c95ac91-4eca-4cbf-b0f4-d60d35d069ed}) + + + + + + Outside air temperature + The name of the ParamType (ThingClass: navigator2, EventType: outsideAirTemperature, ID: {9f3462c2-7c42-4eeb-afc4-092e1e41a25d}) +---------- +The name of the StateType ({9f3462c2-7c42-4eeb-afc4-092e1e41a25d}) of ThingClass navigator2 + + + + + Outside air temperature changed + The name of the EventType ({9f3462c2-7c42-4eeb-afc4-092e1e41a25d}) of ThingClass navigator2 + + + + + + Power + The name of the ParamType (ThingClass: navigator2, EventType: power, ID: {33c27167-8e24-4cc5-943c-d17cd03e0f68}) +---------- +The name of the StateType ({33c27167-8e24-4cc5-943c-d17cd03e0f68}) of ThingClass navigator2 + + + + + Power changed + The name of the EventType ({33c27167-8e24-4cc5-943c-d17cd03e0f68}) of ThingClass navigator2 + + + + + + Room temperature + The name of the ParamType (ThingClass: navigator2, EventType: temperature, ID: {f0f596bf-7e45-43ea-b3d4-767b82dd422a}) +---------- +The name of the StateType ({f0f596bf-7e45-43ea-b3d4-767b82dd422a}) of ThingClass navigator2 + + + + + Room temperature changed + The name of the EventType ({f0f596bf-7e45-43ea-b3d4-767b82dd422a}) of ThingClass navigator2 + + + + + Set target room temperature + The name of the ActionType ({efae7493-68c3-4cb9-853c-81011bdf09ca}) of ThingClass navigator2 + + + + + + + Target room temperature + The name of the ParamType (ThingClass: navigator2, ActionType: targetTemperature, ID: {efae7493-68c3-4cb9-853c-81011bdf09ca}) +---------- +The name of the ParamType (ThingClass: navigator2, EventType: targetTemperature, ID: {efae7493-68c3-4cb9-853c-81011bdf09ca}) +---------- +The name of the StateType ({efae7493-68c3-4cb9-853c-81011bdf09ca}) of ThingClass navigator2 + + + + + Target room temperature changed + The name of the EventType ({efae7493-68c3-4cb9-853c-81011bdf09ca}) of ThingClass navigator2 + + + + + + Target water temperature + The name of the ParamType (ThingClass: navigator2, EventType: targetWaterTemperature, ID: {746244d6-dd37-4af8-b2ae-a7d8463e51e2}) +---------- +The name of the StateType ({746244d6-dd37-4af8-b2ae-a7d8463e51e2}) of ThingClass navigator2 + + + + + Target water temperature changed + The name of the EventType ({746244d6-dd37-4af8-b2ae-a7d8463e51e2}) of ThingClass navigator2 + + + + + + Water temperature + The name of the ParamType (ThingClass: navigator2, EventType: waterTemperature, ID: {fcf8e97f-a672-407f-94ae-30df15b310f4}) +---------- +The name of the StateType ({fcf8e97f-a672-407f-94ae-30df15b310f4}) of ThingClass navigator2 + + + + + Water temperature changed + The name of the EventType ({fcf8e97f-a672-407f-94ae-30df15b310f4}) of ThingClass navigator2 + + + + + + iDM + The name of the vendor ({6f54e4b0-1057-4004-87a9-97fdf4581625}) +---------- +The name of the plugin Idm ({3968d86d-d51a-4ad1-a185-91faa017e38f}) + + + + + IntegrationPluginIdm + + + No IP address given + + + + + IP address already in use + + + + diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 1022204..9d37566 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -92,6 +92,11 @@ QString ModbusTCPMaster::errorString() const return m_modbusTcpClient->errorString(); } +QModbusDevice::Error ModbusTCPMaster::error() const +{ + return m_modbusTcpClient->error(); +} + uint ModbusTCPMaster::port() { return m_modbusTcpClient->connectionParameter(QModbusDevice::NetworkPortParameter).toUInt(); diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index ac604fe..4852a74 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -52,6 +52,7 @@ public: void setTimeout(int timeout); QString errorString() const; + QModbusDevice::Error error() const; QUuid readCoil(uint slaveAddress, uint registerAddress, uint size = 1); QUuid readDiscreteInput(uint slaveAddress, uint registerAddress, uint size = 1); From f336dd0c8ea3966975c41035505715f8a4725b33 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 10 May 2021 11:31:34 +0200 Subject: [PATCH 28/30] cleaned up a bit --- idm/idm.cpp | 26 -------------------------- idm/idm.h | 3 --- idm/integrationpluginidm.cpp | 1 + 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/idm/idm.cpp b/idm/idm.cpp index f2377d5..6890cae 100644 --- a/idm/idm.cpp +++ b/idm/idm.cpp @@ -173,31 +173,6 @@ void Idm::onModbusError() emit statusUpdated(m_idmInfo); } -QString Idm::systemOperationModeToString(IdmSysMode mode) -{ - QString result{}; - - /* Operation modes according to table of manual p. 13 */ - switch (mode) { - case IdmSysModeStandby: - result = "Standby"; - break; - case IdmSysModeAutomatic: - result = "Automatik"; - break; - case IdmSysModeAway: - result = "Abwesend"; - break; - case IdmSysModeOnlyHotwater: - result = "Nur Warmwasser"; - break; - case IdmSysModeOnlyRoomHeating: - result = "Nur Heizung/Kühlung"; - break; - } - return result; -} - QString Idm::heatPumpOperationModeToString(IdmHeatPumpMode mode) { QString result{}; @@ -221,4 +196,3 @@ QString Idm::heatPumpOperationModeToString(IdmHeatPumpMode mode) } return result; } - diff --git a/idm/idm.h b/idm/idm.h index 6e669ce..7a70efc 100644 --- a/idm/idm.h +++ b/idm/idm.h @@ -167,9 +167,6 @@ private: * by the receivedStatusGroupx functions */ IdmInfo m_idmInfo; - /** Converts a system operation mode code to a string (according to manual p. 13) */ - QString systemOperationModeToString(IdmSysMode mode); - /** Converts a heat pump operation mode code to a string (according to manual p. 14) */ QString heatPumpOperationModeToString(IdmHeatPumpMode mode); diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index db4a875..860cded 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -146,6 +146,7 @@ void IntegrationPluginIdm::executeAction(ThingActionInfo *info) double targetTemperature = thing->stateValue(navigator2TargetTemperatureStateTypeId).toDouble(); QUuid requestId = idm->setTargetTemperature(targetTemperature); m_asyncActions.insert(requestId, info); + connect(info, &ThingActionInfo::aborted, [requestId, this] {m_asyncActions.remove(requestId);}); } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); From 14ab5398d485578ade1c9f98849d51791f935ead Mon Sep 17 00:00:00 2001 From: Boernsman Date: Mon, 10 May 2021 11:33:18 +0200 Subject: [PATCH 29/30] typo in readme --- idm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idm/README.md b/idm/README.md index 19b4cb2..bbc94ec 100644 --- a/idm/README.md +++ b/idm/README.md @@ -10,7 +10,7 @@ Connect nymea to iDM heat pumps. * The package 'nymea-plugin-idm' must be installed * Navigator 2.0 settings - * "Modbus TCP" must be selected in the "Building Management System" menu? + * "Modbus TCP" must be selected in the "Building Management System" menu? * Both devices must be in the same local area network. ## More From 47b661c2c92e07004bb9f21a01694d8ee14cb508 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 19 May 2021 06:45:25 +0200 Subject: [PATCH 30/30] fixed ip address validation --- idm/integrationpluginidm.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp index 860cded..3fac78d 100644 --- a/idm/integrationpluginidm.cpp +++ b/idm/integrationpluginidm.cpp @@ -43,9 +43,7 @@ void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) if (thing->thingClassId() == navigator2ThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); - - - if (!hostAddress.isNull()) { + if (hostAddress.isNull()) { qCWarning(dcIdm()) << "Setup failed, IP address not valid"; info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); return;