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..f215b60 --- /dev/null +++ b/debian/nymea-plugin-idm.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginidm.so diff --git a/idm/README.md b/idm/README.md new file mode 100644 index 0000000..bbc94ec --- /dev/null +++ b/idm/README.md @@ -0,0 +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/ diff --git a/idm/idm.cpp b/idm/idm.cpp new file mode 100644 index 0000000..6890cae --- /dev/null +++ b/idm/idm.cpp @@ -0,0 +1,198 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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" +#include "extern-plugininfo.h" +#include "../modbus/modbushelpers.h" + +#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()) << "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); + connect(m_modbusMaster, &ModbusTCPMaster::writeRequestExecuted, this, &Idm::writeRequestExecuted); + } +} + +Idm::~Idm() +{ + qCDebug(dcIdm()) << "iDM: Deleting iDM connection" << m_hostAddress.toString(); +} + +bool Idm::connectDevice() +{ + qCDebug(dcIdm()) << "iDM: Connecting device"; + return m_modbusMaster->connectDevice(); +} + +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); + /* qCDebug(dcIdm()) << "receivedHoldingRegister " << modbusRegister << "(length: " << value.length() << ")"; */ + + switch (modbusRegister) { + case Idm::OutsideTemperature: + /* qCDebug(dcIdm()) << "received outside temperature"; */ + if (value.length() == 2) { + m_idmInfo.outsideTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::OutsideTemperature - modbusRegister]); + } + 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_idmInfo.error = true; + } else { + m_idmInfo.error = false; + } + } + 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_idmInfo.waterTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::HeatStorageTemperature - modbusRegister]); + } + 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_idmInfo.targetWaterTemperature = (double)value[RegisterList::TargetHotWaterTemperature - modbusRegister]; + } + 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_idmInfo.mode = heatPumpOperationModeToString((Idm::IdmHeatPumpMode)value[RegisterList::HeatPumpOperatingMode-modbusRegister]); + } + 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_idmInfo.roomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureHKA - modbusRegister]); + } + 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_idmInfo.targetRoomTemperature = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::RoomTemperatureTargetHeatingEcoHKA - modbusRegister]); + } + 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_idmInfo.powerConsumptionHeatPump = ModbusHelpers::convertRegisterToFloat(&value[RegisterList::CurrentPowerConsumptionHeatPump - modbusRegister]); + } + + /* Everything read without an error + * -> set connected to true */ + m_idmInfo.connected = true; + emit statusUpdated(m_idmInfo); + break; + } +} + +void Idm::onModbusError() +{ + qCDebug(dcIdm()) << "iDM: Received modbus error" << m_modbusMaster->errorString(); + m_idmInfo.connected = false; + emit statusUpdated(m_idmInfo); +} + +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 new file mode 100644 index 0000000..7a70efc --- /dev/null +++ b/idm/idm.h @@ -0,0 +1,183 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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" + +#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 +public: + explicit Idm(const QHostAddress &address, QObject *parent = nullptr); + ~Idm(); + + bool connectDevice(); + QHostAddress getIdmAddress() const; + QUuid setTargetTemperature(double targetTemperature); + void getStatus(); + +private: + /** Modbus Unit ID of Idm device */ + static const quint16 modbusUnitID = 1; + + enum IscModus { + KeineAbwarme = 0, + Heizung = 1, + Warmwasser = 4, + 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 { + 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 + 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 + ExternalRequestTemperatureCooling = 1695, // Externe Anforderungstemperatur Kühlen + HeatingRequirement = 1710, // Anforderung Heizen + CoolingRequirement = 1711, // Anforderung Kühlen + HotWaterChargingRequirement = 1712, // Anforderung Warmwasserladung + 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 + TargetRoomTemperatureZ1R1 = 2004, // Zonenmodul 1 Raumsolltemperatur Raum 1 + CurrentPhotovoltaicsSurplus = 74, // Aktueller PV-Überschuss + CurrentPhotovoltaicsProduction = 78, // Aktueller PV Produktion + CurrentPowerConsumptionHeatPump = 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; + + /** 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_idmInfo; + + /** Converts a heat pump operation mode code to a string (according to manual p. 14) */ + QString heatPumpOperationModeToString(IdmHeatPumpMode mode); + +signals: + void statusUpdated(const IdmInfo &info); + void targetRoomTemperatureChanged(); + void writeRequestExecuted(const QUuid &requestId, bool success); + +private slots: + void onModbusError(); + void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); +}; + +#endif // IDM_H diff --git a/idm/idm.png b/idm/idm.png new file mode 100644 index 0000000..1306b1f Binary files /dev/null and b/idm/idm.png differ diff --git a/idm/idm.pro b/idm/idm.pro new file mode 100644 index 0000000..6bc6551 --- /dev/null +++ b/idm/idm.pro @@ -0,0 +1,19 @@ +include(../plugins.pri) + +QT += \ + network \ + serialbus \ + +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 new file mode 100644 index 0000000..6e8d7f1 --- /dev/null +++ b/idm/idminfo.h @@ -0,0 +1,79 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 +#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 { + /** Set to true, if register values can be read, + * false in case of communication problems */ + bool connected; + + bool power; + + /** RegisterList::OutsideTemperature */ + double roomTemperature; + + /** RegisterList::ExternalOutsideTemperature */ + double outsideTemperature; + + /** RegisterList::HeatStorageTemperature */ + double waterTemperature; + + /** RegisterList::TargetRoomTemperatureZ1R1 (zone 1, room 1) */ + double targetRoomTemperature; + + /** RegisterList::TargetHotWaterTemperature */ + double targetWaterTemperature; + + /** RegisterList::HumiditySensor */ + double humidity; + + /** RegisterList::CurrentPowerConsumptionHeatPump */ + double powerConsumptionHeatPump; + + /** RegisterList::OperationModeSystem */ + QString mode; + + /** True if there is an error code set + * (RegisterList::CurrentFaultNumber != 0) */ + bool error; +}; + +Q_DECLARE_METATYPE(IdmInfo); + +#endif + diff --git a/idm/integrationpluginidm.cpp b/idm/integrationpluginidm.cpp new file mode 100644 index 0000000..3fac78d --- /dev/null +++ b/idm/integrationpluginidm.cpp @@ -0,0 +1,214 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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" + +IntegrationPluginIdm::IntegrationPluginIdm() +{ + +} + +void IntegrationPluginIdm::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcIdm()) << "setupThing called" << thing->name(); + + if (thing->thingClassId() == navigator2ThingClassId) { + QHostAddress hostAddress = QHostAddress(thing->paramValue(navigator2ThingIpAddressParamTypeId).toString()); + if (hostAddress.isNull()) { + qCWarning(dcIdm()) << "Setup failed, IP address not valid"; + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("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, QT_TR_NOOP("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); + if (idm->connectDevice()) { + qCWarning(dcIdm()) << "Could not connect to thing"; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + 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); + + + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +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); + } + + if (thing->thingClassId() == navigator2ThingClassId) { + Idm *idm = m_idmConnections.value(thing); + if (!idm) { + qCWarning(dcIdm()) << "Could not find any iDM connection for" << thing->name(); + return; + } + + thing->setStateValue(navigator2ConnectedStateTypeId, true); + update(thing); + } +} + +void IntegrationPluginIdm::thingRemoved(Thing *thing) +{ + qCDebug(dcIdm()) << "thingRemoved called" << thing->name(); + + if (thing->thingClassId() == navigator2ThingClassId) { + if (m_idmConnections.contains(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) +{ + Thing *thing = info->thing(); + 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(); + 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()); + } + } else { + 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" << thing->name(); + + Idm *idm = m_idmConnections.value(thing); + if (!idm) { + return; + } + idm->getStatus(); + } +} + +void IntegrationPluginIdm::onStatusUpdated(const IdmInfo &info) +{ + qCDebug(dcIdm()) << "Received status from heat pump"; + + Idm *idm = qobject_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.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::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"; + + foreach (Thing *thing, myThings().filterByThingClassId(navigator2ThingClassId)) { + update(thing); + } +} + diff --git a/idm/integrationpluginidm.h b/idm/integrationpluginidm.h new file mode 100644 index 0000000..808edb8 --- /dev/null +++ b/idm/integrationpluginidm.h @@ -0,0 +1,71 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + PluginTimer *m_refreshTimer = nullptr; + QHash m_idmConnections; + QHash m_asyncActions; + + void update(Thing *thing); + void onRefreshTimer(); + +private slots: + void onStatusUpdated(const IdmInfo &info); + void onWriteRequestExecuted(const QUuid &requestId, bool success); + +}; + +#endif // INTEGRATIONPLUGINIDM_H + diff --git a/idm/integrationpluginidm.json b/idm/integrationpluginidm.json new file mode 100644 index 0000000..e66b23e --- /dev/null +++ b/idm/integrationpluginidm.json @@ -0,0 +1,135 @@ +{ + "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": ["user"], + "interfaces": ["thermostat", "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", + "type": "bool", + "defaultValue": false + }, + { + "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", + "displayNameAction": "Set target room temperature", + "displayNameEvent": "Target room temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "minValue": "18", + "maxValue": "28", + "defaultValue": 22.00, + "writable": true + }, + { + "id": "746244d6-dd37-4af8-b2ae-a7d8463e51e2", + "name": "targetWaterTemperature", + "displayName": "Target water temperature", + "displayNameEvent": "Target water temperature changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00 + }, + { + + "id": "b98fb325-100d-4eae-bf8d-97e8f7e1eb00", + "name": "currentPowerConsumptionHeatPump", + "displayName": "Current power consumption", + "displayNameEvent": "Current power consumption heat pump changed", + "displayNameAction": "Change current power consumption het pump", + "type": "double", + "unit": "KiloWatt", + "defaultValue": 0.00 + }, + { + "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 + } + ] + } + ] + } + ] +} + + + + 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/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/modbushelpers.cpp b/modbus/modbushelpers.cpp new file mode 100644 index 0000000..043b671 --- /dev/null +++ b/modbus/modbushelpers.cpp @@ -0,0 +1,61 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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" + +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; + + 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; +} + +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 new file mode 100644 index 0000000..bae33f3 --- /dev/null +++ b/modbus/modbushelpers.h @@ -0,0 +1,44 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 +#include + +class ModbusHelpers { +public: + static float convertRegisterToFloat(const quint16 *reg); + static QVector convertFloatToRegister(float value); +}; + +#endif + 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/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 48bce17..9d37566 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. @@ -29,6 +29,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "modbustcpmaster.h" + #include NYMEA_LOGGING_CATEGORY(dcModbusTCP, "ModbusTCP") @@ -62,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) @@ -86,6 +87,16 @@ void ModbusTCPMaster::setTimeout(int timeout) m_modbusTcpClient->setTimeout(timeout); } +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(); @@ -130,20 +141,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); @@ -217,13 +228,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(); } }); @@ -231,7 +242,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); @@ -261,20 +272,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); @@ -304,21 +315,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); @@ -339,7 +351,7 @@ QUuid ModbusTCPMaster::writeCoil(uint slaveAddress, uint registerAddress, bool v } QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const QVector &values) - { +{ if (!m_modbusTcpClient) { return ""; } @@ -354,14 +366,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 ae3d9b2..4852a74 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,6 +37,8 @@ #include #include +Q_DECLARE_LOGGING_CATEGORY(dcModbusTcp) + class ModbusTCPMaster : public QObject { Q_OBJECT @@ -49,6 +51,9 @@ public: void setNumberOfRetries(int number); 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); QUuid readInputRegister(uint slaveAddress, uint registerAddress, uint size = 1); @@ -81,6 +86,9 @@ 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); void receivedDiscreteInput(uint slaveAddress, uint modbusRegister, const QVector &values); void receivedHoldingRegister(uint slaveAddress, uint modbusRegister, const QVector &values); 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 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 \