diff --git a/debian/control b/debian/control index eadbad7..59c08b1 100644 --- a/debian/control +++ b/debian/control @@ -94,6 +94,22 @@ Description: nymea.io plugin for my-pv heating rods This package will install the nymea.io plugin for my-pv +Package: nymea-plugin-mtec +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-modbus-translations +Description: nymea.io plugin for M-TEC 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 M-TEC heat pumps + + Package: nymea-plugin-sunspec Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-mtec.install.in b/debian/nymea-plugin-mtec.install.in new file mode 100644 index 0000000..c5ec7ef --- /dev/null +++ b/debian/nymea-plugin-mtec.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginmtec.so diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 9d37566..c6bced0 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -34,59 +34,131 @@ NYMEA_LOGGING_CATEGORY(dcModbusTCP, "ModbusTCP") ModbusTCPMaster::ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent) : - QObject(parent) + QObject(parent), + m_hostAddress(hostAddress), + m_port(port) { m_modbusTcpClient = new QModbusTcpClient(this); - m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port); - m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, hostAddress.toString()); - m_modbusTcpClient->setTimeout(1000); - m_modbusTcpClient->setNumberOfRetries(3); + m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port); + m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString()); + m_modbusTcpClient->setTimeout(m_timeout); + m_modbusTcpClient->setNumberOfRetries(m_numberOfRetries); connect(m_modbusTcpClient, &QModbusTcpClient::stateChanged, this, &ModbusTCPMaster::onModbusStateChanged); connect(m_modbusTcpClient, &QModbusRtuSerialMaster::errorOccurred, this, &ModbusTCPMaster::onModbusErrorOccurred); m_reconnectTimer = new QTimer(this); m_reconnectTimer->setSingleShot(true); - connect(m_reconnectTimer, &QTimer::timeout, this, &ModbusTCPMaster::onReconnectTimer); + m_reconnectTimer->setInterval(4000); + connect(m_reconnectTimer, &QTimer::timeout, this, &ModbusTCPMaster::connectDevice); } ModbusTCPMaster::~ModbusTCPMaster() { - if (!m_modbusTcpClient) { - m_modbusTcpClient->disconnectDevice(); - m_modbusTcpClient->deleteLater(); - } - if (!m_reconnectTimer) { + if (m_reconnectTimer) { m_reconnectTimer->stop(); - m_reconnectTimer->deleteLater(); } + + if (m_modbusTcpClient) { + disconnectDevice(); + } +} + +QHostAddress ModbusTCPMaster::hostAddress() const +{ + return m_hostAddress; +} + +uint ModbusTCPMaster::port() const +{ + return m_port; +} + +void ModbusTCPMaster::setPort(uint port) +{ + m_port = port; +} + +void ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress) +{ + m_hostAddress = hostAddress; } bool ModbusTCPMaster::connectDevice() { // TCP connection to target device - qCDebug(dcModbusTCP()) << "Setting up TCP connecion"; - if (!m_modbusTcpClient) return false; - return m_modbusTcpClient->connectDevice(); + // Only connect if we are in the unconnected state + if (m_modbusTcpClient->state() == QModbusDevice::UnconnectedState) { + qCDebug(dcModbusTCP()) << "Connecting modbus TCP client to" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); + m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port); + m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString()); + m_modbusTcpClient->setTimeout(m_timeout); + m_modbusTcpClient->setNumberOfRetries(m_numberOfRetries); + return m_modbusTcpClient->connectDevice(); + } else if (m_modbusTcpClient->state() != QModbusDevice::ConnectedState) { + // Restart the timer in case of connecting not finished yet or closing + m_reconnectTimer->start(); + } else { + qCWarning(dcModbusTCP()) << "Connect modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port) << "called, but the socket is currently in the" << m_modbusTcpClient->state(); + } + + return false; } -bool ModbusTCPMaster::connected() +void ModbusTCPMaster::disconnectDevice() { - return (m_modbusTcpClient->state() == QModbusDevice::State::ConnectedState); + if (!m_modbusTcpClient) + return; + + // Stop the reconnect timer since disconnect was explicitly called + m_reconnectTimer->stop(); + m_modbusTcpClient->disconnectDevice(); +} + +bool ModbusTCPMaster::reconnectDevice() +{ + qCWarning(dcModbusTCP()) << "Reconnecting modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); + if (!m_modbusTcpClient) + return false; + + disconnectDevice(); + return connectDevice(); +} + +bool ModbusTCPMaster::connected() const +{ + return m_connected; +} + +int ModbusTCPMaster::numberOfRetries() const +{ + return m_modbusTcpClient->numberOfRetries(); } void ModbusTCPMaster::setNumberOfRetries(int number) { + m_numberOfRetries = number; m_modbusTcpClient->setNumberOfRetries(number); } +int ModbusTCPMaster::timeout() const +{ + return m_modbusTcpClient->timeout(); +} + void ModbusTCPMaster::setTimeout(int timeout) { + m_timeout = timeout; m_modbusTcpClient->setTimeout(timeout); } +int ModbusTCPMaster::timeout() +{ + return m_modbusTcpClient->timeout(); +} + QString ModbusTCPMaster::errorString() const { return m_modbusTcpClient->errorString(); @@ -97,74 +169,44 @@ QModbusDevice::Error ModbusTCPMaster::error() const return m_modbusTcpClient->error(); } -uint ModbusTCPMaster::port() -{ - return m_modbusTcpClient->connectionParameter(QModbusDevice::NetworkPortParameter).toUInt(); -} - -bool ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress) -{ - m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, hostAddress.toString()); - return connectDevice(); -} - -bool ModbusTCPMaster::setPort(uint port) -{ - m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port); - return connectDevice(); -} - -void ModbusTCPMaster::onReconnectTimer() -{ - if(!m_modbusTcpClient->connectDevice()) { - m_reconnectTimer->start(10000); - } -} - -QHostAddress ModbusTCPMaster::hostAddress() -{ - return QHostAddress(m_modbusTcpClient->connectionParameter(QModbusDevice::NetworkAddressParameter).toString()); -} - QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint size) { if (!m_modbusTcpClient) { - return ""; + return QUuid(); } - QUuid requestId = QUuid::createUuid(); + QUuid requestId = QUuid::createUuid(); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, size); if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, slaveAddress)) { if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); connect(reply, &QModbusReply::finished, this, [reply, requestId, this] { - if (reply->error() == QModbusDevice::NoError) { emit readRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values()); - } else { emit readRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); } }); - connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ + connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; emit readRequestError(requestId, reply->errorString()); - reply->finished(); // To make sure it will be deleted + emit reply->finished(); // To make sure it will be deleted }); + QTimer::singleShot(200, reply, &QModbusReply::deleteLater); } else { delete reply; // broadcast replies return immediately - return ""; + return QUuid(); } } else { qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); - return ""; + return QUuid(); } return requestId; } @@ -172,51 +214,70 @@ QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint si QUuid ModbusTCPMaster::writeHoldingRegisters(uint slaveAddress, uint registerAddress, const QVector &values) { if (!m_modbusTcpClient) { - return ""; + return QUuid(); } + QUuid requestId = QUuid::createUuid(); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, values.length()); request.setValues(values); - if (QModbusReply *reply = m_modbusTcpClient->sendWriteRequest(request, slaveAddress)) { if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); 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(); } reply->deleteLater(); }); - connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ + connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus replay error:" << error; emit writeRequestError(requestId, reply->errorString()); - reply->finished(); // To make sure it will be deleted + emit reply->finished(); // To make sure it will be deleted }); + QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); } else { delete reply; // broadcast replies return immediately - return ""; + return QUuid(); } } else { qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); - return ""; + return QUuid(); } return requestId; } +QModbusReply *ModbusTCPMaster::sendRawRequest(const QModbusRequest &request, int serverAddress) +{ + return m_modbusTcpClient->sendRawRequest(request, serverAddress); +} + +QModbusReply *ModbusTCPMaster::sendReadRequest(const QModbusDataUnit &read, int serverAddress) +{ + return m_modbusTcpClient->sendReadRequest(read, serverAddress); +} + +QModbusReply *ModbusTCPMaster::sendReadWriteRequest(const QModbusDataUnit &read, const QModbusDataUnit &write, int serverAddress) +{ + return m_modbusTcpClient->sendReadWriteRequest(read, write, serverAddress); +} + +QModbusReply *ModbusTCPMaster::sendWriteRequest(const QModbusDataUnit &write, int serverAddress) +{ + return m_modbusTcpClient->sendWriteRequest(write, serverAddress); +} + QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress, uint size) { if (!m_modbusTcpClient) { - return ""; + return QUuid(); } QUuid requestId = QUuid::createUuid(); @@ -226,33 +287,32 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); connect(reply, &QModbusReply::finished, this, [reply, requestId, this] { - if (reply->error() == QModbusDevice::NoError) { emit readRequestExecuted(requestId, true); const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedDiscreteInput(reply->serverAddress(), modbusAddress, unit.values()); - } else { emit readRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); } }); - connect(reply, &QModbusReply::errorOccurred, this, [requestId, this] (QModbusDevice::Error error){ + connect(reply, &QModbusReply::errorOccurred, this, [requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus replay error:" << error; QModbusReply *reply = qobject_cast(sender()); emit readRequestError(requestId, reply->errorString()); - reply->finished(); // To make sure it will be deleted + emit reply->finished(); // To make sure it will be deleted }); + QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); } else { delete reply; // broadcast replies return immediately - return ""; + return QUuid(); } } else { qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); - return ""; + return QUuid(); } return requestId; } @@ -260,10 +320,10 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress, uint size) { if (!m_modbusTcpClient) { - return ""; + return QUuid(); } - QUuid requestId = QUuid::createUuid(); + QUuid requestId = QUuid::createUuid(); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::InputRegisters, registerAddress, size); if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, slaveAddress)) { @@ -276,26 +336,27 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress const QModbusDataUnit unit = reply->result(); uint modbusAddress = unit.startAddress(); emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values()); - } else { emit readRequestExecuted(requestId, false); qCWarning(dcModbusTCP()) << "Read response error:" << reply->error(); } }); - connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ + connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; emit readRequestError(requestId, reply->errorString()); - reply->finished(); // To make sure it will be deleted + emit reply->finished(); // To make sure it will be deleted }); + QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); + } else { delete reply; // broadcast replies return immediately - return ""; + return QUuid(); } } else { qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); - return ""; + return QUuid(); } return requestId; } @@ -303,10 +364,10 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddress, uint size) { if (!m_modbusTcpClient) { - return ""; + return QUuid(); } - QUuid requestId = QUuid::createUuid(); + QUuid requestId = QUuid::createUuid(); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, size); if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, slaveAddress)) { @@ -327,20 +388,22 @@ QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddre } reply->deleteLater(); }); + connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; emit readRequestError(requestId, reply->errorString()); - reply->finished(); // To make sure it will be deleted + emit reply->finished(); // To make sure it will be deleted }); + QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); } else { delete reply; // broadcast replies return immediately - return ""; + return QUuid(); } } else { qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); - return ""; + return QUuid(); } return requestId; } @@ -353,10 +416,10 @@ QUuid ModbusTCPMaster::writeCoil(uint slaveAddress, uint registerAddress, bool v QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const QVector &values) { if (!m_modbusTcpClient) { - return ""; + return QUuid(); } - QUuid requestId = QUuid::createUuid(); + QUuid requestId = QUuid::createUuid(); QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, values.length()); request.setValues(values); @@ -377,20 +440,21 @@ QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const } reply->deleteLater(); }); - connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ + connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){ qCWarning(dcModbusTCP()) << "Modbus reply error:" << error; emit writeRequestError(requestId, reply->errorString()); - reply->finished(); // To make sure it will be deleted + emit reply->finished(); // To make sure it will be deleted }); + QTimer::singleShot(2000, reply, &QModbusReply::deleteLater); } else { delete reply; // broadcast replies return immediately - return ""; + return QUuid(); } } else { qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString(); - return ""; + return QUuid(); } return requestId; } @@ -400,19 +464,25 @@ QUuid ModbusTCPMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr return writeHoldingRegisters(slaveAddress, registerAddress, QVector() << value); } - void ModbusTCPMaster::onModbusErrorOccurred(QModbusDevice::Error error) { qCWarning(dcModbusTCP()) << "An error occured" << error; } - void ModbusTCPMaster::onModbusStateChanged(QModbusDevice::State state) { + qCDebug(dcModbusTCP()) << "Connection state changed for" << m_hostAddress << state; bool connected = (state == QModbusDevice::ConnectedState); - if (!connected) { - //try to reconnect in 10 seconds - m_reconnectTimer->start(10000); + if (m_connected != connected) { + m_connected = connected; + emit connectionStateChanged(m_connected); + } + + // If the socket is connected, stop the reconnect timer... + // If the socket is unconnected (not connecting and not closing), start the reconnect timer + if (m_connected) { + m_reconnectTimer->stop(); + } else if (state == QModbusDevice::UnconnectedState) { + m_reconnectTimer->start(); } - emit connectionStateChanged(connected); } diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index 4852a74..1ecdb51 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -37,8 +37,6 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(dcModbusTcp) - class ModbusTCPMaster : public QObject { Q_OBJECT @@ -46,10 +44,22 @@ public: explicit ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent = nullptr); ~ModbusTCPMaster(); - bool connectDevice(); - bool connected(); + // If you change the hostaddress, make sure to reconnectDevice afterwards + QHostAddress hostAddress() const; + void setHostAddress(const QHostAddress &hostAddress); + + // If you change the port, make sure to reconnectDevice afterwards + uint port() const; + void setPort(uint port); + + bool connected() const; + + int numberOfRetries() const; void setNumberOfRetries(int number); + + int timeout() const; void setTimeout(int timeout); + int timeout(); QString errorString() const; QModbusDevice::Error error() const; @@ -65,18 +75,28 @@ public: QUuid writeHoldingRegister(uint slaveAddress, uint registerAddress, quint16 value); QUuid writeHoldingRegisters(uint slaveAddress, uint registerAddress, const QVector &values); - QHostAddress hostAddress(); - uint port(); - bool setHostAddress(const QHostAddress &hostAddress); - bool setPort(uint port); + // Generic requests + QModbusReply *sendRawRequest(const QModbusRequest &request, int serverAddress); + QModbusReply *sendReadRequest(const QModbusDataUnit &read, int serverAddress); + QModbusReply *sendReadWriteRequest(const QModbusDataUnit &read, const QModbusDataUnit &write, int serverAddress); + QModbusReply *sendWriteRequest(const QModbusDataUnit &write, int serverAddress); + +public slots: + bool connectDevice(); + void disconnectDevice(); + bool reconnectDevice(); private: QTimer *m_reconnectTimer = nullptr; - QModbusTcpClient *m_modbusTcpClient; + QModbusTcpClient *m_modbusTcpClient = nullptr; + + QHostAddress m_hostAddress; + uint m_port; + int m_timeout = 1000; + int m_numberOfRetries = 3; + bool m_connected = false; private slots: - void onReconnectTimer(); - void onModbusErrorOccurred(QModbusDevice::Error error); void onModbusStateChanged(QModbusDevice::State state); @@ -85,9 +105,9 @@ 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 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); diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp new file mode 100644 index 0000000..858acd8 --- /dev/null +++ b/mtec/integrationpluginmtec.cpp @@ -0,0 +1,345 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "network/networkdevicediscovery.h" +#include "integrationpluginmtec.h" +#include "plugininfo.h" + +IntegrationPluginMTec::IntegrationPluginMTec() +{ + +} + +void IntegrationPluginMTec::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcMTec()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } + + // Perform a network device discovery and filter for "go-eCharger" hosts + NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + + qCDebug(dcMTec()) << "Found" << networkDeviceInfo; + + QString title; + if (networkDeviceInfo.hostName().isEmpty()) { + title = networkDeviceInfo.address().toString(); + } else { + title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; + } + + QString description; + if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { + description = networkDeviceInfo.macAddress(); + } else { + description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(mtecThingClassId, title, description); + ParamList params; + params << Param(mtecThingIpAddressParamTypeId, networkDeviceInfo.address().toString()); + params << Param(mtecThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + descriptor.setParams(params); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(mtecThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcMTec()) << "This heat pump already exists in the system!" << networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcMTec()) << "Setup" << thing; + + if (thing->thingClassId() == mtecThingClassId) { + QHostAddress hostAddress = QHostAddress(thing->paramValue(mtecThingIpAddressParamTypeId).toString()); + if (hostAddress.isNull()) { + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); + return; + } + + qCDebug(dcMTec()) << "Using ip address" << hostAddress.toString(); + + MTec *mtec = new MTec(hostAddress, this); + connect(mtec, &MTec::connectedChanged, thing, [=](bool connected){ + qCDebug(dcMTec()) << thing << "Connected changed to" << connected; + thing->setStateValue(mtecConnectedStateTypeId, connected); + }); + + connect(mtec, &MTec::roomTemperatureChanged, thing, [=](double roomTemperature){ + qCDebug(dcMTec()) << thing << "Room temperature" << roomTemperature << "°C"; + thing->setStateValue(mtecTemperatureStateTypeId, roomTemperature); + }); + + connect(mtec, &MTec::targetRoomTemperatureChanged, thing, [=](double targetRoomTemperature){ + qCDebug(dcMTec()) << thing << "Target room temperature" << targetRoomTemperature << "°C"; + thing->setStateValue(mtecTargetTemperatureStateTypeId, targetRoomTemperature); + }); + + connect(mtec, &MTec::waterTankTopTemperatureChanged, thing, [=](double waterTankTopTemperature){ + qCDebug(dcMTec()) << thing << "Water tank top temperature" << waterTankTopTemperature << "°C"; + thing->setStateValue(mtecWaterTankTopTemperatureStateTypeId, waterTankTopTemperature); + }); + + connect(mtec, &MTec::bufferTankTemperatureChanged, thing, [=](double bufferTankTemperature){ + qCDebug(dcMTec()) << thing << "Buffer tank temperature" << bufferTankTemperature << "°C"; + thing->setStateValue(mtecBufferTankTemperatureStateTypeId, bufferTankTemperature); + }); + + connect(mtec, &MTec::totalAccumulatedHeatingEnergyChanged, thing, [=](double totalAccumulatedHeatingEnergy){ + qCDebug(dcMTec()) << thing << "Total accumulated heating energy" << totalAccumulatedHeatingEnergy << "kWh"; + thing->setStateValue(mtecTotalAccumulatedHeatingEnergyStateTypeId, totalAccumulatedHeatingEnergy); + }); + + connect(mtec, &MTec::totalAccumulatedElectricalEnergyChanged, thing, [=](double totalAccumulatedElectricalEnergy){ + qCDebug(dcMTec()) << thing << "Total accumulated electrical energy" << totalAccumulatedElectricalEnergy << "kWh"; + thing->setStateValue(mtecTotalAccumulatedElectricalEnergyStateTypeId, totalAccumulatedElectricalEnergy); + }); + + connect(mtec, &MTec::heatPumpStateChanged, thing, [=](MTec::HeatpumpState heatPumpState){ + qCDebug(dcMTec()) << thing << "Heat pump state" << heatPumpState; + switch (heatPumpState) { + case MTec::HeatpumpStateStandby: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Standby"); + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + case MTec::HeatpumpStatePreRun: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Pre run"); + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + case MTec::HeatpumpStateAutomaticHeat: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Automatic heat"); + thing->setStateValue(mtecHeatingOnStateTypeId, true); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + case MTec::HeatpumpStateDefrost: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Defrost"); + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + case MTec::HeatpumpStateAutomaticCool: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Automatic cool"); + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecCoolingOnStateTypeId, true); + break; + case MTec::HeatpumpStatePostRun: + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Post run"); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + case MTec::HeatpumpStateSaftyShutdown: + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Safty shutdown"); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + case MTec::HeatpumpStateError: + thing->setStateValue(mtecHeatingOnStateTypeId, false); + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Error"); + thing->setStateValue(mtecCoolingOnStateTypeId, false); + break; + } + }); + + connect(mtec, &MTec::heatMeterPowerConsumptionChanged, thing, [=](double heatMeterPowerConsumption){ + qCDebug(dcMTec()) << thing << "Heat meter power consumption" << heatMeterPowerConsumption << "W"; + thing->setStateValue(mtecHeatMeterPowerConsumptionStateTypeId, heatMeterPowerConsumption); + }); + + connect(mtec, &MTec::energyMeterPowerConsumptionChanged, thing, [=](double energyMeterPowerConsumption){ + qCDebug(dcMTec()) << thing << "Energy meter power consumption" << energyMeterPowerConsumption << "W"; + thing->setStateValue(mtecEnergyMeterPowerConsumptionStateTypeId, energyMeterPowerConsumption); + }); + + connect(mtec, &MTec::actualExcessEnergySmartHomeChanged, thing, [=](double actualExcessEnergySmartHome){ + qCDebug(dcMTec()) << thing << "Smart home energy" << actualExcessEnergySmartHome << "W"; + thing->setStateValue(mtecSmartHomeEnergyStateTypeId, actualExcessEnergySmartHome); + }); + + connect(mtec, &MTec::actualExcessEnergySmartHomeElectricityMeterChanged, thing, [=](double actualExcessEnergySmartHomeElectricityMeter){ + qCDebug(dcMTec()) << thing << "Smart home energy electrical meter" << actualExcessEnergySmartHomeElectricityMeter << "W"; + thing->setStateValue(mtecSmartHomeEnergyElectricityMeterStateTypeId, actualExcessEnergySmartHomeElectricityMeter); + }); + + connect(mtec, &MTec::actualOutdoorTemperatureChanged, thing, [=](double actualOutdoorTemperature){ + qCDebug(dcMTec()) << thing << "Outdoor temperature" << actualOutdoorTemperature << "°C"; + thing->setStateValue(mtecOutdoorTemperatureStateTypeId, actualOutdoorTemperature); + }); + + m_mtecConnections.insert(thing, mtec); + + // TODO: start timer and give 15 seconds until connected, since the controler is down for ~10 seconds after a disconnect + + if (!mtec->connectDevice()) { + qCWarning(dcMTec()) << "Initial connect returned false. Lets wait 15 seconds until the connection can be established."; + } + + info->finish(Thing::ThingErrorNoError); + } +} + +void IntegrationPluginMTec::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == mtecThingClassId) { + MTec *mtec = m_mtecConnections.value(thing); + if (mtec) { + update(thing); + } + + if (!m_pluginTimer) { + qCDebug(dcMTec()) << "Starting plugin timer..."; + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { + foreach (Thing *thing, myThings().filterByThingClassId(mtecThingClassId)) { + update(thing); + } + }); + } + } +} + +void IntegrationPluginMTec::thingRemoved(Thing *thing) +{ + if (m_mtecConnections.contains(thing)) { + MTec *mtec = m_mtecConnections.take(thing); + if (mtec) { + mtec->disconnectDevice(); + mtec->deleteLater(); + } + } + + if (myThings().isEmpty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginMTec::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + MTec *mtec = m_mtecConnections.value(thing); + if (!mtec) { + qCWarning(dcMTec()) << "Could not execute action because the MTec connection could not be found for" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + // Make sure we are connected + if (!mtec->connected()) { + qCWarning(dcMTec()) << "Could not execute action because the MTec connection is not connected" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (action.actionTypeId() == mtecTargetTemperatureActionTypeId) { + double targetTemperature = action.paramValue(mtecTargetTemperatureActionTargetTemperatureParamTypeId).toDouble(); + qCDebug(dcMTec()) << "Setting target temperature" << targetTemperature << "°C"; + QModbusReply *reply = mtec->setTargetRoomTemperature(targetTemperature); + if (!reply) { + qCWarning(dcMTec()) << "Failed to send modbus request" << thing; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, this, [=]() { + reply->deleteLater(); + if (reply->error() == QModbusDevice::NoError) { + qCDebug(dcMTec()) << "Setting target temperature" << targetTemperature << "°C" << "finished successfully"; + thing->setStateValue(mtecTargetTemperatureStateTypeId, targetTemperature); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + + connect(reply, &QModbusReply::errorOccurred, this, [=](QModbusDevice::Error error) { + qCWarning(dcMTec()) << thing << "Action execution finished due to modbus replay error:" << error; + reply->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure); + }); + } else if (action.actionTypeId() == mtecSmartHomeEnergyActionTypeId) { + quint16 energy = action.paramValue(mtecSmartHomeEnergyActionSmartHomeEnergyParamTypeId).toUInt(); + qCDebug(dcMTec()) << "Setting smart home energy to" << energy << "W"; + QModbusReply *reply = mtec->setSmartHomeEnergy(energy); + if (!reply) { + qCWarning(dcMTec()) << "Failed to send modbus request" << thing; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, this, [=]() { + reply->deleteLater(); + if (reply->error() == QModbusDevice::NoError) { + qCDebug(dcMTec()) << "Setting smart home energy" << energy << "W" << "finished successfully"; + thing->setStateValue(mtecSmartHomeEnergyStateTypeId, energy); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + + connect(reply, &QModbusReply::errorOccurred, this, [=](QModbusDevice::Error error) { + qCWarning(dcMTec()) << thing << "Action execution finished due to modbus replay error:" << error; + reply->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure); + }); + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); + } +} + +void IntegrationPluginMTec::update(Thing *thing) +{ + if (thing->thingClassId() == mtecThingClassId) { + qCDebug(dcMTec()) << "Updating thing" << thing; + MTec *mtec = m_mtecConnections.value(thing); + if (!mtec) return; + mtec->updateValues(); + } +} + + + diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h new file mode 100644 index 0000000..c1302a4 --- /dev/null +++ b/mtec/integrationpluginmtec.h @@ -0,0 +1,68 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 INTEGRATIONPLUGINMTEC_H +#define INTEGRATIONPLUGINMTEC_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" + +#include "mtec.h" + +#include + +class IntegrationPluginMTec: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginmtec.json") + Q_INTERFACES(IntegrationPlugin) + +public: + /** Constructor */ + explicit IntegrationPluginMTec(); + + 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: + PluginTimer *m_pluginTimer = nullptr; + QHash m_mtecConnections; + +private slots: + void update(Thing *thing); + +}; + +#endif // INTEGRATIONPLUGINMTEC_H + + diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json new file mode 100644 index 0000000..0885ba8 --- /dev/null +++ b/mtec/integrationpluginmtec.json @@ -0,0 +1,206 @@ +{ + "name": "MTec", + "displayName": "M-Tec", + "id": "07cd316b-1e2c-40cf-8358-88d7407506ae", + "vendors": [ + { + "name": "MTec", + "displayName": "M-Tec", + "id": "04d3fa7c-e469-4a79-a119-155426e5a846", + "thingClasses": [ + { + "name": "mtec", + "displayName": "MTec", + "id": "451e38d8-50d5-4ae9-8d9f-21af9347128d", + "createMethods": ["discovery", "user"], + "interfaces": ["thermostat", "connectable"], + "paramTypes": [ + { + "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", + "name": "ipAddress", + "displayName": "IP address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "127.0.0.1" + }, + { + "id": "906f6099-d0e1-4297-a2b3-f8ec4482c578", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "1e2037c8-09dc-4396-974c-efa9c486aa65", + "name": "heatPumpState", + "displayName": "Heat pump state", + "displayNameEvent": "Heat pump state changed", + "type": "QString", + "possibleValues": [ + "Standby", + "Pre run", + "Automatic heat", + "Defrost", + "Automatic cool", + "Post run", + "Safty shutdown", + "Error" + ], + "defaultValue": "Standby", + "suggestLogging": true + }, + { + "id": "9b538cb9-f7a3-471e-8d3b-09f6370a571c", + "name": "targetTemperature", + "displayName": "Target room temperature (heat circuit 0)", + "displayNameEvent": "Target room temperature changed (heat circuit 0=", + "displayNameAction": "Set target room temperature (heat circuit 0)", + "unit": "DegreeCelsius", + "type": "double", + "writable": true, + "minValue": 10, + "maxValue": 30, + "defaultValue": 20, + "suggestLogging": true + }, + { + "id": "b22ac9bb-3842-497c-bd93-f8bea6670e32", + "name": "temperature", + "displayName": "Room temperature heat circuit 0", + "displayNameEvent": "Room temperature heat circuit 0 changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 20, + "suggestLogging": true + }, + { + "id": "07465fbb-6949-4bd1-90d5-acf2d80c161d", + "name": "heatingOn", + "displayName": "Heating on", + "displayNameEvent": "Heating turned on/off", + "type": "bool", + "defaultValue": false, + "suggestLogging": true + }, + { + "id": "8b407c1d-b84f-48d4-9961-b29bc58fff0e", + "name": "coolingOn", + "displayName": "Cooling on", + "displayNameEvent": "Cooling turned on/off", + "type": "bool", + "defaultValue": false, + "suggestLogging": true + }, + { + "id": "d0c8f168-49b5-47ca-9988-c9922be38dd5", + "name": "outdoorTemperature", + "displayName": "Outdoor temperature", + "displayNameEvent": "Outdoor temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "545f94d6-f4fd-48fe-bf3b-f193e5cb76e7", + "name": "waterTankTopTemperature", + "displayName": "Water tank top temperature", + "displayNameEvent": "Water tank top temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "a98e37f8-dcdc-4c4c-aecf-07f376321849", + "name": "bufferTankTemperature", + "displayName": "Buffer tank temperature", + "displayNameEvent": "Buffer tank temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "7d087af8-cdbe-463e-a9bb-7a7a79471963", + "name": "totalAccumulatedHeatingEnergy", + "displayName": "Total accumulated heating energy", + "displayNameEvent": "Total accumulated heating energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", + "name": "totalAccumulatedElectricalEnergy", + "displayName": "Total accumulated electrical energy", + "displayNameEvent": "Total accumulated electrical energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "581abddc-90d6-4dea-a43c-63b117b335fe", + "name": "heatMeterPowerConsumption", + "displayName": "Heat meter power consumption", + "displayNameEvent": "Heat meter power consumption changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "fd52a97e-f94d-4529-b479-b74e61f75a89", + "name": "energyMeterPowerConsumption", + "displayName": "Energy meter power consumption", + "displayNameEvent": "Energy meter power consumption changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "b646ea10-ea7e-4eba-bfda-8e3cd38370a7", + "name": "smartHomeEnergy", + "displayName": "Smart home energy", + "displayNameEvent": "Smart home energy changed", + "displayNameAction": "Set smart home energy", + "type": "uint", + "unit": "Watt", + "minValue": 0, + "maxValue": 20000, + "defaultValue": 0, + "writable": true, + "suggestLogging": true + }, + { + "id": "a7734474-30db-435c-985a-105fb3ea5a86", + "name": "smartHomeEnergyElectricityMeter", + "displayName": "Smart home energy consumed", + "displayNameEvent": "Smart home energy consumed changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "suggestLogging": true + } + ], + "actionTypes": [ ] + } + ] + } + ] +} diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp new file mode 100644 index 0000000..ac953c5 --- /dev/null +++ b/mtec/mtec.cpp @@ -0,0 +1,202 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "mtec.h" +#include "extern-plugininfo.h" + +MTec::MTec(const QHostAddress &address, QObject *parent) : + QObject(parent), + m_hostAddress(address) +{ + m_modbusMaster = new ModbusTCPMaster(address, 502, this); + m_modbusMaster->setTimeout(2000); + m_modbusMaster->setNumberOfRetries(5); + + qCDebug(dcMTec()) << "Created ModbusTCPMaster for" << address.toString(); + connect(m_modbusMaster, &ModbusTCPMaster::connectionStateChanged, this, &MTec::connectedChanged); + connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &MTec::onReceivedHoldingRegister); + connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &MTec::onModbusError); + connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &MTec::onModbusError); +} + +MTec::~MTec() +{ + m_modbusMaster->disconnectDevice(); +} + +QHostAddress MTec::hostAddress() const +{ + return m_hostAddress; +} + +bool MTec::connected() const +{ + return m_modbusMaster->connected(); +} + +QModbusReply *MTec::setTargetRoomTemperature(double targetRoomTemperature) +{ + QVector values; + values << static_cast(qRound(targetRoomTemperature * 10)); + + QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, RegisterTargetRoomTemperature, values.length()); + request.setValues(values); + QModbusReply *reply = m_modbusMaster->sendWriteRequest(request, MTec::ModbusUnitID); + return reply; +} + +QModbusReply *MTec::setSmartHomeEnergy(quint16 smartHomeEnergy) +{ + QVector values; + values << smartHomeEnergy; + QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, RegisterActualExcessEnergySmartHome, values.length()); + request.setValues(values); + QModbusReply *reply = m_modbusMaster->sendWriteRequest(request, MTec::ModbusUnitID); + return reply; +} + +bool MTec::connectDevice() +{ + return m_modbusMaster->connectDevice(); +} + +void MTec::disconnectDevice() +{ + m_modbusMaster->disconnectDevice(); +} + +void MTec::updateValues() +{ + if (!m_modbusMaster->connected()) { + return; + } + + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterRoomTemperature, 1); +} + +void MTec::onModbusError() +{ + qCWarning(dcMTec()) << "Modbus error occured" << m_modbusMaster->errorString(); +} + +void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) +{ + Q_UNUSED(slaveAddress); + + switch (modbusRegister) { + case RegisterRoomTemperature: + if (value.length() == 1) { + m_roomTemperature = value[0] / 10.0; + emit roomTemperatureChanged(m_roomTemperature); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTargetRoomTemperature, 1); + break; + case RegisterTargetRoomTemperature: + if (value.length() == 1) { + m_targetRoomTemperature = value[0] / 10.0; + emit targetRoomTemperatureChanged(m_targetRoomTemperature); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHotWaterTankTemperature, 1); + break; + case RegisterHotWaterTankTemperature: + if (value.length() == 1) { + m_waterTankTopTemperature = value[0] / 10.0; + emit waterTankTopTemperatureChanged(m_waterTankTopTemperature); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterBufferTankTemperature, 1); + break; + case RegisterBufferTankTemperature: + if (value.length() == 1) { + m_bufferTankTemperature = value[0] / 10.0; + emit bufferTankTemperatureChanged(m_bufferTankTemperature); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTotalAccumulatedHeatingEnergy, 1); + break; + case RegisterTotalAccumulatedHeatingEnergy: + if (value.length() == 1) { + m_totalAccumulatedElectricalEnergy = value[0]; + emit totalAccumulatedElectricalEnergyChanged(m_totalAccumulatedElectricalEnergy); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTotalAccumulatedElectricalEnergy, 1); + break; + case RegisterTotalAccumulatedElectricalEnergy: + if (value.length() == 1) { + m_totalAccumulatedElectricalEnergy = value[0]; + emit totalAccumulatedElectricalEnergyChanged(m_totalAccumulatedElectricalEnergy); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatpumpState, 1); + break; + case RegisterHeatpumpState: + if (value.length() == 1) { + m_heatPumpState = static_cast(value[0]); + emit heatPumpStateChanged(m_heatPumpState); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatMeterPowerConsumption, 1); + break; + case RegisterHeatMeterPowerConsumption: + if (value.length() == 1) { + m_heatMeterPowerConsumption = value[0]; + emit heatMeterPowerConsumptionChanged(m_heatMeterPowerConsumption); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterEnergyMeterPowerConsumption, 1); + break; + case RegisterEnergyMeterPowerConsumption: + if (value.length() == 1) { + m_energyMeterPowerConsumption = value[0]; + emit energyMeterPowerConsumptionChanged(m_energyMeterPowerConsumption); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualExcessEnergySmartHome, 1); + break; + case RegisterActualExcessEnergySmartHome: + if (value.length() == 1) { + m_actualExcessEnergySmartHome = value[0]; + emit actualExcessEnergySmartHomeChanged(m_actualExcessEnergySmartHome); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualExcessEnergySmartHomeElectricityMeter, 1); + break; + case RegisterActualExcessEnergySmartHomeElectricityMeter: + if (value.length() == 1) { + m_actualExcessEnergySmartHomeElectricityMeter = value[0]; + emit actualExcessEnergySmartHomeChanged(m_actualExcessEnergySmartHome); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualOutdoorTemperature, 1); + break; + case RegisterActualOutdoorTemperature: + if (value.length() == 1) { + m_actualOutdoorTemperature = value[0] / 10.0; + emit actualOutdoorTemperatureChanged(m_actualOutdoorTemperature); + } + + // TODO: set initialized + + break; + } +} + diff --git a/mtec/mtec.h b/mtec/mtec.h new file mode 100644 index 0000000..50f1740 --- /dev/null +++ b/mtec/mtec.h @@ -0,0 +1,164 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 MTEC_H +#define MTEC_H + +#include +#include + +#include "../modbus/modbustcpmaster.h" + +class MTec : public QObject +{ + Q_OBJECT +public: + enum HeatpumpState { + HeatpumpStateStandby = 0, + HeatpumpStatePreRun = 1, + HeatpumpStateAutomaticHeat = 2, + HeatpumpStateDefrost = 3, + HeatpumpStateAutomaticCool = 4, + HeatpumpStatePostRun = 5, + HeatpumpStateSaftyShutdown = 7, + HeatpumpStateError = 8 + }; + Q_ENUM(HeatpumpState) + + explicit MTec(const QHostAddress &address, QObject *parent = nullptr); + ~MTec(); + + QHostAddress hostAddress() const; + bool connected() const; + + QModbusReply *setTargetRoomTemperature(double targetRoomTemperature); + QModbusReply *setSmartHomeEnergy(quint16 smartHomeEnergy); + +public slots: + bool connectDevice(); + void disconnectDevice(); + + void updateValues(); + +private: + /** Modbus Unit ID (undocumented, guessing 1 for now) */ + static const quint16 ModbusUnitID = 1; + + /** The following modbus addresses can be read: */ + enum Register { + /* R APPL.CtrlAppl.sParam.heatCircuit[0].tempRoom.values.actValue + * Actual room temperature [1/10°C]. */ + RegisterRoomTemperature = 1, + + /* RW APPL.CtrlAppl.sParam.heatCircuit[0].param.normalSetTemp + * Room set temperature for heating circuit [1/10°C]. */ + RegisterTargetRoomTemperature = 4, + + /* R APPL.CtrlAppl.sParam.hotWaterTank[0].topTemp.values.actValue + * Hot water tank top temperature [1/10°C]. */ + RegisterHotWaterTankTemperature = 401, + + /* R APPL.CtrlAppl.sParam.bufferTank[0].topTemp.values.actValue + * Buffer Actual top temperature [1/10°C]. */ + RegisterBufferTankTemperature = 601, + + /* R APPL.CtrlAppl.sStatisticalData.heatpump[0].consumption.energy + * Total accumulated heating energy [kWh] */ + RegisterTotalAccumulatedHeatingEnergy = 701, + + /* R APPL.CtrlAppl.sStatisticalData.heatpump[0].consumption.electricalenergy + * Total accumulated electrical energy [kWh] */ + RegisterTotalAccumulatedElectricalEnergy = 702, + + /* R APPL.CtrlAppl.sParam.heatpump[0].values.heatpumpState */ + RegisterHeatpumpState = 703, + + /* R APPL.CtrlAppl.sParam.heatpump[0].HeatMeter.values.power + * Actual power consumtion [W] */ + RegisterHeatMeterPowerConsumption = 706, + + /* R APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power + * Actual power consumtion [W] */ + RegisterEnergyMeterPowerConsumption = 707, + + /* RW APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0] + * Acutal excess energy given from Smart home System [W] */ + RegisterActualExcessEnergySmartHome = 1000, + + /* R APPL.CtrlAppl.sParam.photovoltaics.ElectricEnergyMeter.values.power + * Acutal excess energy given from Electricity Meter [W] */ + RegisterActualExcessEnergySmartHomeElectricityMeter = 1002, + + /* R APPL.CtrlAppl.sParam.outdoorTemp.values.actValue + * Actual exterior temperature [°C] */ + RegisterActualOutdoorTemperature = 1502, + + }; + + QHostAddress m_hostAddress; + ModbusTCPMaster *m_modbusMaster = nullptr; + + double m_roomTemperature = 0; + double m_targetRoomTemperature = 0; + double m_waterTankTopTemperature = 0; + double m_bufferTankTemperature = 0; + double m_totalAccumulatedHeatingEnergy = 0; + double m_totalAccumulatedElectricalEnergy = 0; + HeatpumpState m_heatPumpState = HeatpumpStateStandby; + double m_heatMeterPowerConsumption = 0; + double m_energyMeterPowerConsumption = 0; + double m_actualExcessEnergySmartHome = 0; + double m_actualExcessEnergySmartHomeElectricityMeter = 0; + double m_actualOutdoorTemperature = 0; + +signals: + void connectedChanged(bool connected); + + void roomTemperatureChanged(double roomTemperature); + void targetRoomTemperatureChanged(double targetRoomTemperature); + void waterTankTopTemperatureChanged(double waterTankTopTemperature); + void bufferTankTemperatureChanged(double bufferTankTemperature); + void totalAccumulatedHeatingEnergyChanged(double totalAccumulatedHeatingEnergy); + void totalAccumulatedElectricalEnergyChanged(double totalAccumulatedElectricalEnergy); + void heatPumpStateChanged(HeatpumpState heatPumpState); + void heatMeterPowerConsumptionChanged(double heatMeterPowerConsumption); + void energyMeterPowerConsumptionChanged(double energyMeterPowerConsumption); + void actualExcessEnergySmartHomeChanged(double actualExcessEnergySmartHome); + void actualExcessEnergySmartHomeElectricityMeterChanged(double actualExcessEnergySmartHomeElectricityMeter); + void actualOutdoorTemperatureChanged(double actualOutdoorTemperature); + +private slots: + void onModbusError(); + void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); + +}; + +#endif // MTEC_H + diff --git a/mtec/mtec.pro b/mtec/mtec.pro new file mode 100644 index 0000000..c68ed3b --- /dev/null +++ b/mtec/mtec.pro @@ -0,0 +1,16 @@ +include(../plugins.pri) + +QT += \ + network \ + serialbus \ + +SOURCES += \ + mtec.cpp \ + integrationpluginmtec.cpp \ + ../modbus/modbustcpmaster.cpp + +HEADERS += \ + mtec.h \ + integrationpluginmtec.h \ + ../modbus/modbustcpmaster.h \ + diff --git a/mtec/translations/07cd316b-1e2c-40cf-8358-88d7407506ae-en_US.ts b/mtec/translations/07cd316b-1e2c-40cf-8358-88d7407506ae-en_US.ts new file mode 100644 index 0000000..d4adf0f --- /dev/null +++ b/mtec/translations/07cd316b-1e2c-40cf-8358-88d7407506ae-en_US.ts @@ -0,0 +1,146 @@ + + + + + IntegrationPluginMTec + + + No IP address given + + + + + IP address already in use by another thing. + + + + + MTec + + + + Actual excess energy from Electricity Meter + The name of the ParamType (ThingClass: mtec, EventType: actualExcessEnergyElectricityMeter, ID: {fd94d39c-0db6-497f-a0a5-6c5452cbcaaf}) +---------- +The name of the StateType ({fd94d39c-0db6-497f-a0a5-6c5452cbcaaf}) of ThingClass mtec + + + + + Actual excess energy from Electricity Meter changed + The name of the EventType ({fd94d39c-0db6-497f-a0a5-6c5452cbcaaf}) of ThingClass mtec + + + + + + Actual excess energy from Smart home System + The name of the ParamType (ThingClass: mtec, EventType: actualExcessEnergySmartHome, ID: {663718fa-807e-4d85-bd78-61a65f8c0b5e}) +---------- +The name of the StateType ({663718fa-807e-4d85-bd78-61a65f8c0b5e}) of ThingClass mtec + + + + + Actual excess energy from Smart home System changed + The name of the EventType ({663718fa-807e-4d85-bd78-61a65f8c0b5e}) of ThingClass mtec + + + + + + Actual power consumption + The name of the ParamType (ThingClass: mtec, EventType: actualPowerConsumption, ID: {c67c79cf-7369-409f-b170-16c4ece9d25a}) +---------- +The name of the StateType ({c67c79cf-7369-409f-b170-16c4ece9d25a}) of ThingClass mtec + + + + + Actual power consumption changed + The name of the EventType ({c67c79cf-7369-409f-b170-16c4ece9d25a}) of ThingClass mtec + + + + + + Connected + The name of the ParamType (ThingClass: mtec, EventType: connected, ID: {8d64954a-855d-44ea-8bc9-88a71ab47b6b}) +---------- +The name of the StateType ({8d64954a-855d-44ea-8bc9-88a71ab47b6b}) of ThingClass mtec + + + + + Connected changed + The name of the EventType ({8d64954a-855d-44ea-8bc9-88a71ab47b6b}) of ThingClass mtec + + + + + + Control of the heat source by an external control [100%] + The name of the ParamType (ThingClass: mtec, EventType: externalSetValueScaling, ID: {087c0296-705b-483a-b1e9-7ce08202c035}) +---------- +The name of the StateType ({087c0296-705b-483a-b1e9-7ce08202c035}) of ThingClass mtec + + + + + Control of the heat source by an external control [100%] changed + The name of the EventType ({087c0296-705b-483a-b1e9-7ce08202c035}) of ThingClass mtec + + + + + IP address + The name of the ParamType (ThingClass: mtec, Type: thing, ID: {f1c43b1e-cffe-4d30-bda0-c23ed648dd71}) + + + + + + M-Tec + The name of the vendor ({04d3fa7c-e469-4a79-a119-155426e5a846}) +---------- +The name of the plugin MTec ({07cd316b-1e2c-40cf-8358-88d7407506ae}) + + + + + MTec + The name of the ThingClass ({451e38d8-50d5-4ae9-8d9f-21af9347128d}) + + + + + + Request external heat source + The name of the ParamType (ThingClass: mtec, EventType: requestExternalHeatSource, ID: {90b17788-ce63-47e3-b97d-1b025a41ce35}) +---------- +The name of the StateType ({90b17788-ce63-47e3-b97d-1b025a41ce35}) of ThingClass mtec + + + + + Request external heat source changed + The name of the EventType ({90b17788-ce63-47e3-b97d-1b025a41ce35}) of ThingClass mtec + + + + + + Status + The name of the ParamType (ThingClass: mtec, EventType: status, ID: {9bf5f8d6-116a-4399-a728-51470a3a5620}) +---------- +The name of the StateType ({9bf5f8d6-116a-4399-a728-51470a3a5620}) of ThingClass mtec + + + + + Status changed + The name of the EventType ({9bf5f8d6-116a-4399-a728-51470a3a5620}) of ThingClass mtec + + + + diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index 2515df5..ebee60d 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -4,6 +4,7 @@ PLUGIN_DIRS = \ drexelundweiss \ energymeters \ modbuscommander \ + mtec \ mypv \ sunspec \ unipi \