From f8bc0784370c1976e39e7462ea7185bd95a4cc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 27 May 2021 10:32:04 +0200 Subject: [PATCH 01/14] Extend modbus tcp class --- modbus/modbustcpmaster.cpp | 215 +++++++++++++++++++++++-------------- modbus/modbustcpmaster.h | 32 ++++-- 2 files changed, 157 insertions(+), 90 deletions(-) diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 9d37566..e7bef77 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -34,13 +34,15 @@ 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); @@ -52,38 +54,87 @@ ModbusTCPMaster::ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QOb ModbusTCPMaster::~ModbusTCPMaster() { - if (!m_modbusTcpClient) { - m_modbusTcpClient->disconnectDevice(); - m_modbusTcpClient->deleteLater(); - } - if (!m_reconnectTimer) { + if (m_reconnectTimer) { m_reconnectTimer->stop(); - m_reconnectTimer->deleteLater(); + delete m_reconnectTimer; + m_reconnectTimer = nullptr; } + + if (m_modbusTcpClient) { + m_modbusTcpClient->disconnectDevice(); + delete m_modbusTcpClient; + m_modbusTcpClient = nullptr; + } +} + +QHostAddress ModbusTCPMaster::hostAddress() const +{ + return m_hostAddress; +} + +uint ModbusTCPMaster::port() const +{ + return m_port; +} + +bool ModbusTCPMaster::setPort(uint port) +{ + m_port = port; + return connectDevice(); +} + +bool ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress) +{ + m_hostAddress = hostAddress; + return connectDevice(); } bool ModbusTCPMaster::connectDevice() { // TCP connection to target device - qCDebug(dcModbusTCP()) << "Setting up TCP connecion"; - + qCDebug(dcModbusTCP()) << "Setting up TCP connecion" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); if (!m_modbusTcpClient) return false; + 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(); } -bool ModbusTCPMaster::connected() +void ModbusTCPMaster::disconnectDevice() +{ + if (!m_modbusTcpClient) + return; + + m_modbusTcpClient->disconnectDevice(); +} + +bool ModbusTCPMaster::connected() const { return (m_modbusTcpClient->state() == QModbusDevice::State::ConnectedState); } +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); } @@ -97,74 +148,51 @@ 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()) { + 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 +200,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 +273,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 +306,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 +322,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 +350,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 +374,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 +402,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 +426,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 +450,20 @@ 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); } + emit connectionStateChanged(connected); } diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index 4852a74..92a7372 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,9 +44,21 @@ public: explicit ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent = nullptr); ~ModbusTCPMaster(); + QHostAddress hostAddress() const; + bool setHostAddress(const QHostAddress &hostAddress); + + uint port() const; + bool setPort(uint port); + bool connectDevice(); - bool connected(); + void disconnectDevice(); + + bool connected() const; + + int numberOfRetries() const; void setNumberOfRetries(int number); + + int timeout() const; void setTimeout(int timeout); QString errorString() const; @@ -65,14 +75,20 @@ 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); 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; private slots: void onReconnectTimer(); From a62032ae45310aafe6733954bc5fb052cbcd24c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 27 May 2021 11:27:41 +0200 Subject: [PATCH 02/14] Improve reconnect behaviour --- modbus/modbustcpmaster.cpp | 72 +++++++++++++++++++++++--------------- modbus/modbustcpmaster.h | 17 +++++---- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index e7bef77..880f4c6 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -49,21 +49,18 @@ ModbusTCPMaster::ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QOb 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_reconnectTimer) { m_reconnectTimer->stop(); - delete m_reconnectTimer; - m_reconnectTimer = nullptr; } if (m_modbusTcpClient) { - m_modbusTcpClient->disconnectDevice(); - delete m_modbusTcpClient; - m_modbusTcpClient = nullptr; + disconnectDevice(); } } @@ -77,30 +74,37 @@ uint ModbusTCPMaster::port() const return m_port; } -bool ModbusTCPMaster::setPort(uint port) +void ModbusTCPMaster::setPort(uint port) { m_port = port; - return connectDevice(); } -bool ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress) +void ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress) { m_hostAddress = hostAddress; - return connectDevice(); } bool ModbusTCPMaster::connectDevice() { // TCP connection to target device - qCDebug(dcModbusTCP()) << "Setting up TCP connecion" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port); if (!m_modbusTcpClient) return false; - 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); + // 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 m_modbusTcpClient->connectDevice(); + return false; } void ModbusTCPMaster::disconnectDevice() @@ -108,12 +112,24 @@ void ModbusTCPMaster::disconnectDevice() 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_modbusTcpClient->state() == QModbusDevice::State::ConnectedState); + return m_connected; } int ModbusTCPMaster::numberOfRetries() const @@ -148,13 +164,6 @@ QModbusDevice::Error ModbusTCPMaster::error() const return m_modbusTcpClient->error(); } -void ModbusTCPMaster::onReconnectTimer() -{ - if (!m_modbusTcpClient->connectDevice()) { - m_reconnectTimer->start(10000); - } -} - QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint size) { if (!m_modbusTcpClient) { @@ -458,12 +467,17 @@ void ModbusTCPMaster::onModbusErrorOccurred(QModbusDevice::Error 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); } - emit connectionStateChanged(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(); + } } diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index 92a7372..7857922 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -44,14 +44,13 @@ public: explicit ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent = nullptr); ~ModbusTCPMaster(); + // If you change the hostaddress, make sure to reconnectDevice afterwards QHostAddress hostAddress() const; - bool setHostAddress(const QHostAddress &hostAddress); + void setHostAddress(const QHostAddress &hostAddress); + // If you change the port, make sure to reconnectDevice afterwards uint port() const; - bool setPort(uint port); - - bool connectDevice(); - void disconnectDevice(); + void setPort(uint port); bool connected() const; @@ -81,6 +80,11 @@ public: 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 = nullptr; @@ -89,10 +93,9 @@ private: 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); From 621c98369e6f6d6ab29ea19f656174f90c3bf44e Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Wed, 21 Apr 2021 11:03:47 +0200 Subject: [PATCH 03/14] Added initial version of M-Tec heatpump plugin Currently only read functionality. Not yet tested with actual hardware. --- debian/control | 16 ++ debian/nymea-plugin-mtec.install.in | 1 + modbus/modbustcpmaster.h | 2 + mtec/integrationpluginmtec.cpp | 198 ++++++++++++++++++ mtec/integrationpluginmtec.h | 73 +++++++ mtec/integrationpluginmtec.json | 106 ++++++++++ mtec/mtec.cpp | 143 +++++++++++++ mtec/mtec.h | 130 ++++++++++++ mtec/mtec.pro | 19 ++ mtec/mtechelpers.cpp | 65 ++++++ mtec/mtechelpers.h | 46 ++++ mtec/mtecinfo.h | 59 ++++++ ...d316b-1e2c-40cf-8358-88d7407506ae-en_US.ts | 146 +++++++++++++ nymea-plugins-modbus.pro | 1 + 14 files changed, 1005 insertions(+) create mode 100644 debian/nymea-plugin-mtec.install.in create mode 100644 mtec/integrationpluginmtec.cpp create mode 100644 mtec/integrationpluginmtec.h create mode 100644 mtec/integrationpluginmtec.json create mode 100644 mtec/mtec.cpp create mode 100644 mtec/mtec.h create mode 100644 mtec/mtec.pro create mode 100644 mtec/mtechelpers.cpp create mode 100644 mtec/mtechelpers.h create mode 100644 mtec/mtecinfo.h create mode 100644 mtec/translations/07cd316b-1e2c-40cf-8358-88d7407506ae-en_US.ts 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.h b/modbus/modbustcpmaster.h index 7857922..12f051c 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -59,6 +59,7 @@ public: int timeout() const; void setTimeout(int timeout); + int timeout(); QString errorString() const; QModbusDevice::Error error() const; @@ -104,6 +105,7 @@ signals: void writeRequestExecuted(const QUuid &requestId, bool success); void writeRequestError(const QUuid &requestId, const QString &error); + void readRequestError(const QUuid &requestId, const QString &error); void readRequestExecuted(const QUuid &requestId, bool success); void readRequestError(const QUuid &requestId, const QString &error); diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp new file mode 100644 index 0000000..1c94e2e --- /dev/null +++ b/mtec/integrationpluginmtec.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 "integrationpluginmtec.h" +#include "plugininfo.h" + +IntegrationPluginMTec::IntegrationPluginMTec() +{ + +} + +void IntegrationPluginMTec::discoverThings(ThingDiscoveryInfo *info) +{ + qCDebug(dcMTec()) << "Discover M-Tec heat pumps"; + + if (info->thingClassId() == mtecThingClassId) { + QString description = "Heatpump"; + ThingDescriptor descriptor(info->thingClassId(), "M-Tec", description); + info->addThingDescriptor(descriptor); + + // TODO Find out, if a discovery is possible/needed + // Otherwise, just report no error for now + info->finish(Thing::ThingErrorNoError); + } +} + +void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) +{ + qCDebug(dcMTec()) << "Setup" << info->thing(); + + Thing *thing = info->thing(); + + if (thing->thingClassId() == mtecThingClassId) { + QHostAddress hostAddress = QHostAddress(thing->paramValue(mtecThingIpAddressParamTypeId).toString()); + + if (hostAddress.isNull()) { + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); + return; + } + + qCDebug(dcMTec()) << "User entered address: " << hostAddress.toString(); + + /* Check, if address is already in use for another device */ + /* for (QHash::iterator item=m_mtecConnections.begin(); item != m_mtecConnections.end(); item++) { */ + /* if (hostAddress.isEqual(item.value()->getHostAddress())) { */ + /* qCDebug(dcMTec()) << "Address of thing: " << item.value()->getHostAddress().toString(); */ + + /* qCDebug(dcMTec()) << "Address in use already"; */ + /* } else { */ + /* qCDebug(dcMTec()) << "Different address of other thing: " << item.value()->getHostAddress().toString(); */ + + /* } */ + /* } */ + + foreach (MTec *mtecConnection, m_mtecConnections.values()) { + if (mtecConnection->getHostAddress().isEqual(hostAddress)) { + qCWarning(dcMTec()) << "Address" << hostAddress.toString() << "already in use by" << m_mtecConnections.key(mtecConnection); + info->finish(Thing::ThingErrorThingInUse, QT_TR_NOOP("IP address already in use by another thing.")); + return; + } + } + + qCDebug(dcMTec()) << "Creating M-Tec object"; + + /* Create new MTec object and store it in hash table */ + MTec *mtec = new MTec(hostAddress, this); + m_mtecConnections.insert(thing, mtec); + + info->thing()->setStateValue(mtecConnectedStateTypeId, true); + info->finish(Thing::ThingErrorNoError); + } +} + +void IntegrationPluginMTec::postSetupThing(Thing *thing) +{ + qCDebug(dcMTec()) << "PostSetup called for" << thing; + + if (thing->thingClassId() == mtecThingClassId) { + MTec *mtec = m_mtecConnections.value(thing); + + if (mtec) { + connect(mtec, &MTec::statusUpdated, this, &IntegrationPluginMTec::onStatusUpdated); + connect(mtec, &MTec::connectedChanged, this, &IntegrationPluginMTec::onConnectedChanged); + + qCDebug(dcMTec()) << "Thing set up, calling update"; + update(thing); + + thing->setStateValue(mtecConnectedStateTypeId, true); + } + } +} + +void IntegrationPluginMTec::thingRemoved(Thing *thing) +{ + if (m_mtecConnections.contains(thing)) { + m_mtecConnections.take(thing)->deleteLater(); + } +} + +void IntegrationPluginMTec::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == mtecThingClassId) { + /* if (action.actionTypeId() == mtecPowerActionTypeId) { */ + + /* } else { */ + /* Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); */ + /* } */ + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginMTec::update(Thing *thing) +{ + if (thing->thingClassId() == mtecThingClassId) { + qCDebug(dcMTec()) << "Updating thing" << thing; + + MTec *mtec = m_mtecConnections.value(thing); + + if (mtec) { + mtec->onRequestStatus(); + } + } +} + +void IntegrationPluginMTec::onConnectedChanged(bool connected) +{ + MTec *mtec = qobject_cast(sender()); + Thing *thing = m_mtecConnections.key(mtec); + + qCDebug(dcMTec()) << "Received connection change event from heat pump" << thing; + + if (!thing) + return; + + thing->setStateValue(mtecConnectedStateTypeId, connected); +} + +void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) +{ + MTec *mtec = qobject_cast(sender()); + Thing *thing = m_mtecConnections.key(mtec); + + qCDebug(dcMTec()) << "Received status from heat pump" << thing; + + if (!thing) + return; + + /* Received a structure holding the status info of the + * heat pump. Update the thing states with the individual fields. */ + thing->setStateValue(mtecStatusStateTypeId, info.status); + thing->setStateValue(mtecActualPowerConsumptionStateTypeId, info.actualPowerConsumption); + thing->setStateValue(mtecActualExcessEnergySmartHomeStateTypeId, info.actualExcessEnergySmartHome); + thing->setStateValue(mtecActualExcessEnergyElectricityMeterStateTypeId, info.actualExcessEnergyElectricityMeter); + thing->setStateValue(mtecExternalSetValueScalingStateTypeId, info.externalSetValueScaling); + thing->setStateValue(mtecRequestExternalHeatSourceStateTypeId, info.requestExternalHeatSource); +} + +void IntegrationPluginMTec::onRefreshTimer() +{ + qCDebug(dcMTec()) << "onRefreshTimer called"; + + foreach (Thing *thing, myThings().filterByThingClassId(mtecThingClassId)) { + update(thing); + } +} + + diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h new file mode 100644 index 0000000..f7fe9ea --- /dev/null +++ b/mtec/integrationpluginmtec.h @@ -0,0 +1,73 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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; + + +private: + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + void update(Thing *thing); + + QHash m_mtecConnections; + QHash m_asyncActions; + +private slots: + void onConnectedChanged(bool connected); + void onRefreshTimer(); + void onStatusUpdated(const MTecInfo &info); + +}; + +#endif // INTEGRATIONPLUGINMTEC_H + + diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json new file mode 100644 index 0000000..9f91d44 --- /dev/null +++ b/mtec/integrationpluginmtec.json @@ -0,0 +1,106 @@ +{ + "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": ["user"], + "interfaces": ["connectable"], + "paramTypes": [ + { + "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", + "name": "ipAddress", + "displayName": "IP address", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "9bf5f8d6-116a-4399-a728-51470a3a5620", + "name": "status", + "displayName": "Status", + "displayNameEvent": "Status changed", + "type": "QString", + "defaultValue": "Off", + "possibleValues": [ + "Off", + "Connecting", + "Connected", + "Error" + ] + }, + { + "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", + "name": "actualPowerConsumption", + "displayName": "Actual power consumption", + "displayNameEvent": "Actual power consumption changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", + "name": "actualExcessEnergySmartHome", + "displayName": "Actual excess energy from Smart home System", + "displayNameEvent": "Actual excess energy from Smart home System changed", + "displayNameAction": "Change actual excess energy from Smart home System", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", + "name": "actualExcessEnergyElectricityMeter", + "displayName": "Actual excess energy from Electricity Meter", + "displayNameEvent": "Actual excess energy from Electricity Meter changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "087c0296-705b-483a-b1e9-7ce08202c035", + "name": "externalSetValueScaling", + "displayName": "Control of the heat source by an external control [100%]", + "displayNameEvent": "Control of the heat source by an external control [100%] changed", + "type": "double", + "unit": "Percentage", + "defaultValue": 100 + }, + { + "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", + "name": "requestExternalHeatSource", + "displayName": "Request external heat source", + "displayNameEvent": "Request external heat source changed", + "type": "QString", + "defaultValue": "No request, external heat source must be turned off", + "possibleValues": [ + "No request, external heat source must be turned off", + "External heat source is released and can be switched on", + "External heat source is required and must be turned on" + ] + } + ], + "actionTypes": [ + ] + } + ] + } + ] +} diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp new file mode 100644 index 0000000..7a027fd --- /dev/null +++ b/mtec/mtec.cpp @@ -0,0 +1,143 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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" +#include "mtechelpers.h" + +#include + +MTec::MTec(const QHostAddress &address, QObject *parent) : + QObject(parent), + m_hostAddress(address) +{ + m_modbusMaster = new ModbusTCPMaster(address, 502, this); + + qCDebug(dcMTec()) << "created ModbusTCPMaster"; + m_connected = m_modbusMaster->connectDevice(); + + 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() +{ +} + +void MTec::onModbusError() +{ + qCWarning(dcMTec()) << "MTec: Received modbus error"; + + emit connectedChanged(false); +} + +void MTec::onRequestStatus() +{ + if ((m_connected) && (!m_firstTimeout.isValid())) { + /* No timestamp for timeout of first telegram defined, + * -> first request has not yet been sent + * -> do it now */ + + /* Note: m_firstRequest is set to false in + * the onReceivedHoldingRegister function */ + if (m_firstRequest == true) { + m_firstTimeout = QDateTime::currentDateTime(); + m_firstTimeout = m_firstTimeout.addSecs(MTec::FirstConnectionTimeout); + + /* Save original modbus timeout, will be set again + * after first response is received */ + m_modbusTimeout = m_modbusMaster->timeout(); + + /* The M-Tec heatpump requires a longer timeout to + * start-up the modbus communication */ + m_modbusMaster->setTimeout(MTec::FirstConnectionTimeout); + } else { + /* Set back original modbus timeout */ + m_modbusMaster->setTimeout(m_modbusTimeout); + } + } else { + QDateTime now = QDateTime::currentDateTime(); + + if (m_firstTimeout.msecsTo(now) < MTec::FirstConnectionTimeout) { + /* Timeout of first request not yet reached + * -> return without sending another request */ + return; + } + } + + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualPowerConsumption, 1); +} + +void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) +{ + Q_UNUSED(slaveAddress); + + /* Some response was received, so the modbus communication is + * established. */ + m_firstRequest = false; + + switch (modbusRegister) { + case ActualPowerConsumption: + if (value.length() == 1) { + m_info.actualPowerConsumption = MTecHelpers::convertRegisterToFloat(value[0]); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualExcessEnergySmartHome, 1); + break; + case ActualExcessEnergySmartHome: + if (value.length() == 1) { + m_info.actualExcessEnergySmartHome = MTecHelpers::convertRegisterToFloat(value[0]); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualExcessEnergyElectricityMeter, 1); + break; + case ActualExcessEnergyElectricityMeter: + if (value.length() == 1) { + m_info.actualExcessEnergyElectricityMeter = MTecHelpers::convertRegisterToFloat(value[0]); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ExternalSetValueScaling, 1); + break; + case ExternalSetValueScaling: + if (value.length() == 1) { + m_info.externalSetValueScaling = MTecHelpers::convertRegisterToFloat(value[0]); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::RequestExternalHeatSource, 1); + break; + case RequestExternalHeatSource: + if (value.length() == 1) { + m_info.requestExternalHeatSource = MTecHelpers::externalHeatSourceRequestToString(value[0]); + + /* Everything read without errors + * -> update status in plugin */ + emit statusUpdated(m_info); + } + break; + } +} + diff --git a/mtec/mtec.h b/mtec/mtec.h new file mode 100644 index 0000000..d725b8a --- /dev/null +++ b/mtec/mtec.h @@ -0,0 +1,130 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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" + +#include "mtecinfo.h" + +class MTec : public QObject +{ + Q_OBJECT +public: + /** Constructor */ + explicit MTec(const QHostAddress &address, QObject *parent = nullptr); + + /** Destructor */ + ~MTec(); + + inline QHostAddress getHostAddress() const { return m_hostAddress; }; + +private: + /** + * The first response of the M-Tec heatpump may be delayed + * by 10 s. The plugin will wait for the defined time in milliseconds + * for a response before sending an additional request or setting + * an error code. + */ + static const int FirstConnectionTimeout = 15000; + + /** Modbus Unit ID (undocumented, guessing 1 for now) */ + static const quint16 ModbusUnitID = 1; + + /** The following modbus addresses can be read: */ + enum RegisterList { + /** + * APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power + * Actual power consumtion [W] + */ + ActualPowerConsumption = 707, + + /** + * APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0] + * Acutal excess energy given from Smart home System [W] + */ + ActualExcessEnergySmartHome = 1000, + + /** + * APPL.CtrlAppl.sParam.photovoltaics.ElectricEnergyMeter.values.power + * Acutal excess energy given from Electricity Meter [W] + */ + ActualExcessEnergyElectricityMeter = 1002, + + /** + * APPL.CtrlAppl.sParam.extHeatSource[0].param.externalSetvalueScaled + * Control of the heat source by an external control [100%] + */ + ExternalSetValueScaling = 1600, + + /** + * APPL.CtrlAppl.sParam.extHeatSource[0].values.requestExtHeatsource + * 0…no request, external heat source must be turned off. + * 1…external heat source is released and can be switched on. + * 2…external heat source is required and must be turned on. + */ + RequestExternalHeatSource = 1601 + }; + + /* Note: This class only requires one IP address and one + * TCP Modbus connection. Multiple devices are managed + * within the IntegrationPluginMTec class. */ + QHostAddress m_hostAddress; + + /** Pointer to ModbusTCPMaster object, responseible for low-level communicaiton */ + ModbusTCPMaster *m_modbusMaster = nullptr; + + /** This structure is filled by the receivedStatus... functions */ + MTecInfo m_info ; + + bool m_firstRequest = true; + bool m_connected = false; + + QDateTime m_firstTimeout; + + int m_modbusTimeout; + +signals: + void connectedChanged(bool connected); + void statusUpdated(const MTecInfo &info); + +public slots: + void onModbusError(); + void onRequestStatus(); + 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..eaaf6a0 --- /dev/null +++ b/mtec/mtec.pro @@ -0,0 +1,19 @@ +include(../plugins.pri) + +QT += \ + network \ + serialbus \ + +SOURCES += \ + mtec.cpp \ + mtechelpers.cpp \ + integrationpluginmtec.cpp \ + ../modbus/modbustcpmaster.cpp + +HEADERS += \ + mtec.h \ + mtechelpers.h \ + mtecinfo.h \ + integrationpluginmtec.h \ + ../modbus/modbustcpmaster.h \ + diff --git a/mtec/mtechelpers.cpp b/mtec/mtechelpers.cpp new file mode 100644 index 0000000..27cb7a0 --- /dev/null +++ b/mtec/mtechelpers.cpp @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "mtechelpers.h" + +float MTecHelpers::convertRegisterToFloat(const quint16 reg) +{ + float value = 0.0; + + value = reg/100.0; + + return value; +} + +void MTecHelpers::convertFloatToRegister(quint16 ®, float value) +{ + reg = qRound(value * 100.0); +} + +QString MTecHelpers::externalHeatSourceRequestToString(quint16 value) +{ + QString str{}; + + switch (value) { + case 0: + str = "No request, external heat source must be turned off"; + break; + case 1: + str = "External heat source is released and can be switched on"; + break; + case 2: + str = "External heat source is required and must be turned on"; + break; + } + + return str; +} + diff --git a/mtec/mtechelpers.h b/mtec/mtechelpers.h new file mode 100644 index 0000000..452ba51 --- /dev/null +++ b/mtec/mtechelpers.h @@ -0,0 +1,46 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 MTECHELPERS_H +#define MTECHELPERS_H + +#include +#include + +class MTecHelpers { +public: + static float convertRegisterToFloat(quint16 reg); + static void convertFloatToRegister(quint16 ®, float value); + + static QString externalHeatSourceRequestToString(quint16 value); +}; + +#endif + diff --git a/mtec/mtecinfo.h b/mtec/mtecinfo.h new file mode 100644 index 0000000..fe87854 --- /dev/null +++ b/mtec/mtecinfo.h @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 MTECINFO_H +#define MTECINFO_H + +#include +#include + +/** This struct holds the status information that is read from the MTec device + * and passed to the nymea framework within this plugin. + */ +struct MTecInfo { + /** Contains more detailed info on the status + * (Off, Connecting, Connected, Error) */ + QString status; + + double actualPowerConsumption; + + double actualExcessEnergySmartHome; + + double actualExcessEnergyElectricityMeter; + + double externalSetValueScaling; + + QString requestExternalHeatSource; +}; + +Q_DECLARE_METATYPE(MTecInfo); + +#endif + 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 \ From a71b86f82e7ae63ed550d7878c2ab67bde46643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 12 May 2021 10:31:07 +0200 Subject: [PATCH 04/14] Fix modbus tcp master code errors from rebase --- modbus/modbustcpmaster.cpp | 5 +++++ modbus/modbustcpmaster.h | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modbus/modbustcpmaster.cpp b/modbus/modbustcpmaster.cpp index 880f4c6..c6bced0 100644 --- a/modbus/modbustcpmaster.cpp +++ b/modbus/modbustcpmaster.cpp @@ -154,6 +154,11 @@ void ModbusTCPMaster::setTimeout(int timeout) m_modbusTcpClient->setTimeout(timeout); } +int ModbusTCPMaster::timeout() +{ + return m_modbusTcpClient->timeout(); +} + QString ModbusTCPMaster::errorString() const { return m_modbusTcpClient->errorString(); diff --git a/modbus/modbustcpmaster.h b/modbus/modbustcpmaster.h index 12f051c..1ecdb51 100644 --- a/modbus/modbustcpmaster.h +++ b/modbus/modbustcpmaster.h @@ -108,7 +108,6 @@ signals: 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); From 82dc46cd3b51b52f553363ab87308b959e806155 Mon Sep 17 00:00:00 2001 From: Hermann Detz Date: Thu, 20 May 2021 09:49:17 +0200 Subject: [PATCH 05/14] Improved error handling logic to connect --- mtec/integrationpluginmtec.cpp | 8 +++++-- mtec/integrationpluginmtec.h | 3 ++- mtec/mtec.cpp | 38 +++++++++++++++++++--------------- mtec/mtec.h | 9 ++++++-- mtec/mtechelpers.cpp | 38 +++++++++++++++++++++++++++------- mtec/mtechelpers.h | 12 ++++++++++- 6 files changed, 77 insertions(+), 31 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index 1c94e2e..0dffcc0 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -153,7 +153,7 @@ void IntegrationPluginMTec::update(Thing *thing) } } -void IntegrationPluginMTec::onConnectedChanged(bool connected) +void IntegrationPluginMTec::onConnectedChanged(MTecHelpers::ConnectionState state) { MTec *mtec = qobject_cast(sender()); Thing *thing = m_mtecConnections.key(mtec); @@ -163,7 +163,11 @@ void IntegrationPluginMTec::onConnectedChanged(bool connected) if (!thing) return; - thing->setStateValue(mtecConnectedStateTypeId, connected); + if (state == MTecHelpers::ConnectionState::Online) { + thing->setStateValue(mtecConnectedStateTypeId, true); + } + + thing->setStateValue(mtecStatusStateTypeId, MTecHelpers::connectionStateToString(state)); } void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h index f7fe9ea..a2ecedb 100644 --- a/mtec/integrationpluginmtec.h +++ b/mtec/integrationpluginmtec.h @@ -34,6 +34,7 @@ #include "plugintimer.h" #include "mtec.h" +#include "mtechelpers.h" #include @@ -62,7 +63,7 @@ private: QHash m_asyncActions; private slots: - void onConnectedChanged(bool connected); + void onConnectedChanged(MTecHelpers::ConnectionState state); void onRefreshTimer(); void onStatusUpdated(const MTecInfo &info); diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp index 7a027fd..21d25bf 100644 --- a/mtec/mtec.cpp +++ b/mtec/mtec.cpp @@ -41,7 +41,10 @@ MTec::MTec(const QHostAddress &address, QObject *parent) : m_modbusMaster = new ModbusTCPMaster(address, 502, this); qCDebug(dcMTec()) << "created ModbusTCPMaster"; - m_connected = m_modbusMaster->connectDevice(); + + if (m_modbusMaster->connectDevice()) { + emit connectedChanged(MTecHelpers::ConnectionState::Connecting); + } connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &MTec::onReceivedHoldingRegister); connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &MTec::onModbusError); @@ -56,19 +59,22 @@ void MTec::onModbusError() { qCWarning(dcMTec()) << "MTec: Received modbus error"; - emit connectedChanged(false); + /* Only emit connected changed signal, if a soft connection + * had already been established. + * This avoids a series of possibly fake connection state changes, + * while the modbus server in the heatpump is starting up. */ + if (m_softConnection) { + m_softConnection = false; + emit connectedChanged(MTecHelpers::ConnectionState::Offline); + } } void MTec::onRequestStatus() { - if ((m_connected) && (!m_firstTimeout.isValid())) { - /* No timestamp for timeout of first telegram defined, - * -> first request has not yet been sent - * -> do it now */ + if ((m_softConnection) || + ((m_connected) && (m_requestsSent < MTec::ConnectionRetries))) { - /* Note: m_firstRequest is set to false in - * the onReceivedHoldingRegister function */ - if (m_firstRequest == true) { + if (m_requestsSent < MTec::ConnectionRetries) { m_firstTimeout = QDateTime::currentDateTime(); m_firstTimeout = m_firstTimeout.addSecs(MTec::FirstConnectionTimeout); @@ -83,17 +89,14 @@ void MTec::onRequestStatus() /* Set back original modbus timeout */ m_modbusMaster->setTimeout(m_modbusTimeout); } + } else { - QDateTime now = QDateTime::currentDateTime(); - - if (m_firstTimeout.msecsTo(now) < MTec::FirstConnectionTimeout) { - /* Timeout of first request not yet reached - * -> return without sending another request */ - return; - } + qCDebug(dcMTec()) << "Max number of modbus connects reached, giving up"; + return; } m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualPowerConsumption, 1); + m_requestsSent ++; } void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) @@ -102,7 +105,8 @@ void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const /* Some response was received, so the modbus communication is * established. */ - m_firstRequest = false; + m_softConnection = true; + emit connectedChanged(MTecHelpers::ConnectionState::Online); switch (modbusRegister) { case ActualPowerConsumption: diff --git a/mtec/mtec.h b/mtec/mtec.h index d725b8a..1fbe047 100644 --- a/mtec/mtec.h +++ b/mtec/mtec.h @@ -37,6 +37,7 @@ #include "../modbus/modbustcpmaster.h" #include "mtecinfo.h" +#include "mtechelpers.h" class MTec : public QObject { @@ -62,6 +63,9 @@ private: /** Modbus Unit ID (undocumented, guessing 1 for now) */ static const quint16 ModbusUnitID = 1; + /** Number of retries to establish a modbus connection with the heat pump */ + static const int ConnectionRetries = 3; + /** The following modbus addresses can be read: */ enum RegisterList { /** @@ -108,15 +112,16 @@ private: /** This structure is filled by the receivedStatus... functions */ MTecInfo m_info ; - bool m_firstRequest = true; + int m_requestsSent = 0; bool m_connected = false; + bool m_softConnection = false; QDateTime m_firstTimeout; int m_modbusTimeout; signals: - void connectedChanged(bool connected); + void connectedChanged(MTecHelpers::ConnectionState state); void statusUpdated(const MTecInfo &info); public slots: diff --git a/mtec/mtechelpers.cpp b/mtec/mtechelpers.cpp index 27cb7a0..b57547b 100644 --- a/mtec/mtechelpers.cpp +++ b/mtec/mtechelpers.cpp @@ -44,19 +44,41 @@ void MTecHelpers::convertFloatToRegister(quint16 ®, float value) reg = qRound(value * 100.0); } -QString MTecHelpers::externalHeatSourceRequestToString(quint16 value) +QString MTecHelpers::connectionStateToString(ConnectionState state) { QString str{}; - switch (value) { - case 0: - str = "No request, external heat source must be turned off"; + switch (state) { + case Offline: + str = QT_TR_NOOP("Off"); break; - case 1: - str = "External heat source is released and can be switched on"; + case Connecting: + str = QT_TR_NOOP("Connecting"); break; - case 2: - str = "External heat source is required and must be turned on"; + case Online: + str = QT_TR_NOOP("Connected"); + break; + case Error: + str = QT_TR_NOOP("Error"); + break; + } + + return str; +} + +QString MTecHelpers::externalHeatSourceRequestToString(quint16 value) +{ + QString str{}; + + switch (value) { + case 0: + str = QT_TR_NOOP("No request, external heat source must be turned off"); + break; + case 1: + str = QT_TR_NOOP("External heat source is released and can be switched on"); + break; + case 2: + str = QT_TR_NOOP("External heat source is required and must be turned on"); break; } diff --git a/mtec/mtechelpers.h b/mtec/mtechelpers.h index 452ba51..c95ae28 100644 --- a/mtec/mtechelpers.h +++ b/mtec/mtechelpers.h @@ -32,13 +32,23 @@ #define MTECHELPERS_H #include +#include #include -class MTecHelpers { +class MTecHelpers : public QObject { public: + enum ConnectionState { + Offline, + Connecting, + Online, + Error + }; + Q_ENUM(ConnectionState); + static float convertRegisterToFloat(quint16 reg); static void convertFloatToRegister(quint16 ®, float value); + static QString connectionStateToString(ConnectionState state); static QString externalHeatSourceRequestToString(quint16 value); }; From 446b3e3dda0a01757423cfeccc2165db2c3c3d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 08:49:58 +0200 Subject: [PATCH 06/14] Update mtec plugin with network device discovery and internal connection mechanism --- mtec/integrationpluginmtec.cpp | 159 ++++++++++++++--------------- mtec/integrationpluginmtec.h | 11 +- mtec/integrationpluginmtec.json | 174 ++++++++++++++++---------------- mtec/mtec.cpp | 62 ++++-------- mtec/mtec.h | 30 ++---- 5 files changed, 189 insertions(+), 247 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index 0dffcc0..15c3ac3 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -28,6 +28,7 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#include "network/networkdevicediscovery.h" #include "integrationpluginmtec.h" #include "plugininfo.h" @@ -38,81 +39,96 @@ IntegrationPluginMTec::IntegrationPluginMTec() void IntegrationPluginMTec::discoverThings(ThingDiscoveryInfo *info) { - qCDebug(dcMTec()) << "Discover M-Tec heat pumps"; - - if (info->thingClassId() == mtecThingClassId) { - QString description = "Heatpump"; - ThingDescriptor descriptor(info->thingClassId(), "M-Tec", description); - info->addThingDescriptor(descriptor); - - // TODO Find out, if a discovery is possible/needed - // Otherwise, just report no error for now - info->finish(Thing::ThingErrorNoError); + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcMTec()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; } + + // Perform a network device discovery and filter for "go-eCharger" hosts + NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { + + qCDebug(dcMTec()) << "Found" << networkDevice; + + QString title; + if (networkDevice.hostName().isEmpty()) { + title = networkDevice.address().toString(); + } else { + title = networkDevice.hostName() + " (" + networkDevice.address().toString() + ")"; + } + + QString description; + if (networkDevice.macAddressManufacturer().isEmpty()) { + description = networkDevice.macAddress(); + } else { + description = networkDevice.macAddress() + " (" + networkDevice.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(mtecThingClassId, title, description); + ParamList params; + params << Param(mtecThingIpAddressParamTypeId, networkDevice.address().toString()); + params << Param(mtecThingMacAddressParamTypeId, networkDevice.macAddress()); + descriptor.setParams(params); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(mtecThingMacAddressParamTypeId, networkDevice.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcMTec()) << "This heat pump already exists in the system!" << networkDevice; + descriptor.setThingId(existingThings.first()->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); } void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) { - qCDebug(dcMTec()) << "Setup" << info->thing(); - Thing *thing = info->thing(); + qCDebug(dcMTec()) << "Setup" << thing; if (thing->thingClassId() == mtecThingClassId) { QHostAddress hostAddress = QHostAddress(thing->paramValue(mtecThingIpAddressParamTypeId).toString()); - if (hostAddress.isNull()) { info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given")); return; } - qCDebug(dcMTec()) << "User entered address: " << hostAddress.toString(); - - /* Check, if address is already in use for another device */ - /* for (QHash::iterator item=m_mtecConnections.begin(); item != m_mtecConnections.end(); item++) { */ - /* if (hostAddress.isEqual(item.value()->getHostAddress())) { */ - /* qCDebug(dcMTec()) << "Address of thing: " << item.value()->getHostAddress().toString(); */ - - /* qCDebug(dcMTec()) << "Address in use already"; */ - /* } else { */ - /* qCDebug(dcMTec()) << "Different address of other thing: " << item.value()->getHostAddress().toString(); */ - - /* } */ - /* } */ - - foreach (MTec *mtecConnection, m_mtecConnections.values()) { - if (mtecConnection->getHostAddress().isEqual(hostAddress)) { - qCWarning(dcMTec()) << "Address" << hostAddress.toString() << "already in use by" << m_mtecConnections.key(mtecConnection); - info->finish(Thing::ThingErrorThingInUse, QT_TR_NOOP("IP address already in use by another thing.")); - return; - } + qCDebug(dcMTec()) << "Using ip address" << hostAddress.toString(); + if (myThings().filterByParam(mtecThingIpAddressParamTypeId, hostAddress.toString()).count() > 0) { + info->finish(Thing::ThingErrorThingInUse, QT_TR_NOOP("IP address already in use by another thing.")); + return; } - qCDebug(dcMTec()) << "Creating M-Tec object"; + // TODO: start timer and give 15 seconds until connected, since the controler is down for ~10 seconds after a disconnect - /* Create new MTec object and store it in hash table */ MTec *mtec = new MTec(hostAddress, this); - m_mtecConnections.insert(thing, mtec); + connect(mtec, &MTec::statusUpdated, this, &IntegrationPluginMTec::onStatusUpdated); + connect(mtec, &MTec::connectedChanged, thing, [=](bool connected){ + qCDebug(dcMTec()) << "Connected changed to" << connected; + thing->setStateValue(mtecConnectedStateTypeId, connected); + }); + + m_mtecConnections.insert(thing, mtec); + if (!mtec->connectDevice()) { + qCWarning(dcMTec()) << "Initial connect returned false. Lets wait 15 seconds until the connection can be established."; + } - info->thing()->setStateValue(mtecConnectedStateTypeId, true); info->finish(Thing::ThingErrorNoError); } } void IntegrationPluginMTec::postSetupThing(Thing *thing) { - qCDebug(dcMTec()) << "PostSetup called for" << thing; - if (thing->thingClassId() == mtecThingClassId) { MTec *mtec = m_mtecConnections.value(thing); - if (mtec) { - connect(mtec, &MTec::statusUpdated, this, &IntegrationPluginMTec::onStatusUpdated); - connect(mtec, &MTec::connectedChanged, this, &IntegrationPluginMTec::onConnectedChanged); - - qCDebug(dcMTec()) << "Thing set up, calling update"; update(thing); - - thing->setStateValue(mtecConnectedStateTypeId, true); + //thing->setStateValue(mtecConnectedStateTypeId, true); } } } @@ -126,63 +142,38 @@ void IntegrationPluginMTec::thingRemoved(Thing *thing) void IntegrationPluginMTec::executeAction(ThingActionInfo *info) { - Thing *thing = info->thing(); - Action action = info->action(); +// Thing *thing = info->thing(); +// Action action = info->action(); + info->finish(Thing::ThingErrorNoError); - if (thing->thingClassId() == mtecThingClassId) { - /* if (action.actionTypeId() == mtecPowerActionTypeId) { */ - - /* } else { */ - /* Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); */ - /* } */ - } else { - Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); - } +// if (thing->thingClassId() == mtecThingClassId) { +// /* if (action.actionTypeId() == mtecPowerActionTypeId) { */ + +// } else { +// Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); +// } } void IntegrationPluginMTec::update(Thing *thing) { if (thing->thingClassId() == mtecThingClassId) { qCDebug(dcMTec()) << "Updating thing" << thing; - MTec *mtec = m_mtecConnections.value(thing); - - if (mtec) { - mtec->onRequestStatus(); - } + if (!mtec) return; + mtec->requestStatus(); } } -void IntegrationPluginMTec::onConnectedChanged(MTecHelpers::ConnectionState state) -{ - MTec *mtec = qobject_cast(sender()); - Thing *thing = m_mtecConnections.key(mtec); - - qCDebug(dcMTec()) << "Received connection change event from heat pump" << thing; - - if (!thing) - return; - - if (state == MTecHelpers::ConnectionState::Online) { - thing->setStateValue(mtecConnectedStateTypeId, true); - } - - thing->setStateValue(mtecStatusStateTypeId, MTecHelpers::connectionStateToString(state)); -} - void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) { MTec *mtec = qobject_cast(sender()); Thing *thing = m_mtecConnections.key(mtec); - - qCDebug(dcMTec()) << "Received status from heat pump" << thing; - if (!thing) return; + qCDebug(dcMTec()) << "Received status from heat pump" << thing; /* Received a structure holding the status info of the * heat pump. Update the thing states with the individual fields. */ - thing->setStateValue(mtecStatusStateTypeId, info.status); thing->setStateValue(mtecActualPowerConsumptionStateTypeId, info.actualPowerConsumption); thing->setStateValue(mtecActualExcessEnergySmartHomeStateTypeId, info.actualExcessEnergySmartHome); thing->setStateValue(mtecActualExcessEnergyElectricityMeterStateTypeId, info.actualExcessEnergyElectricityMeter); @@ -192,8 +183,6 @@ void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) void IntegrationPluginMTec::onRefreshTimer() { - qCDebug(dcMTec()) << "onRefreshTimer called"; - foreach (Thing *thing, myThings().filterByThingClassId(mtecThingClassId)) { update(thing); } diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h index a2ecedb..1dee6ba 100644 --- a/mtec/integrationpluginmtec.h +++ b/mtec/integrationpluginmtec.h @@ -51,19 +51,16 @@ public: void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; - - -private: void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void executeAction(ThingActionInfo *info) override; + +private: + QHash m_mtecConnections; + void update(Thing *thing); - QHash m_mtecConnections; - QHash m_asyncActions; - private slots: - void onConnectedChanged(MTecHelpers::ConnectionState state); void onRefreshTimer(); void onStatusUpdated(const MTecInfo &info); diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json index 9f91d44..7ea0e0a 100644 --- a/mtec/integrationpluginmtec.json +++ b/mtec/integrationpluginmtec.json @@ -8,98 +8,94 @@ "displayName": "M-Tec", "id": "04d3fa7c-e469-4a79-a119-155426e5a846", "thingClasses": [ - { - "name": "mtec", - "displayName": "MTec", - "id": "451e38d8-50d5-4ae9-8d9f-21af9347128d", - "createMethods": ["user"], - "interfaces": ["connectable"], - "paramTypes": [ { - "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", - "name": "ipAddress", - "displayName": "IP address", - "type": "QString" - } - ], - "stateTypes": [ - { - "id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b", - "name": "connected", - "displayName": "Connected", - "displayNameEvent": "Connected changed", - "type": "bool", - "defaultValue": false, - "cached": false - }, - { - "id": "9bf5f8d6-116a-4399-a728-51470a3a5620", - "name": "status", - "displayName": "Status", - "displayNameEvent": "Status changed", - "type": "QString", - "defaultValue": "Off", - "possibleValues": [ - "Off", - "Connecting", - "Connected", - "Error" - ] - }, - { - "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", - "name": "actualPowerConsumption", - "displayName": "Actual power consumption", - "displayNameEvent": "Actual power consumption changed", - "type": "double", - "unit": "Watt", - "defaultValue": 0 - }, - { - "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", - "name": "actualExcessEnergySmartHome", - "displayName": "Actual excess energy from Smart home System", - "displayNameEvent": "Actual excess energy from Smart home System changed", - "displayNameAction": "Change actual excess energy from Smart home System", - "type": "double", - "unit": "Watt", - "defaultValue": 0 - }, - { - "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", - "name": "actualExcessEnergyElectricityMeter", - "displayName": "Actual excess energy from Electricity Meter", - "displayNameEvent": "Actual excess energy from Electricity Meter changed", - "type": "double", - "unit": "Watt", - "defaultValue": 0 - }, - { - "id": "087c0296-705b-483a-b1e9-7ce08202c035", - "name": "externalSetValueScaling", - "displayName": "Control of the heat source by an external control [100%]", - "displayNameEvent": "Control of the heat source by an external control [100%] changed", - "type": "double", - "unit": "Percentage", - "defaultValue": 100 - }, - { - "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", - "name": "requestExternalHeatSource", - "displayName": "Request external heat source", - "displayNameEvent": "Request external heat source changed", - "type": "QString", - "defaultValue": "No request, external heat source must be turned off", - "possibleValues": [ - "No request, external heat source must be turned off", - "External heat source is released and can be switched on", - "External heat source is required and must be turned on" + "name": "mtec", + "displayName": "MTec", + "id": "451e38d8-50d5-4ae9-8d9f-21af9347128d", + "createMethods": ["discovery", "user"], + "interfaces": ["connectable"], + "paramTypes": [ + { + "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", + "name": "ipAddress", + "displayName": "IP address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "127.0.0.1" + }, + { + "id": "906f6099-d0e1-4297-a2b3-f8ec4482c578", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", + "name": "actualPowerConsumption", + "displayName": "Actual power consumption", + "displayNameEvent": "Actual power consumption changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", + "name": "actualExcessEnergySmartHome", + "displayName": "Actual excess energy from Smart home System", + "displayNameEvent": "Actual excess energy from Smart home System changed", + "displayNameAction": "Change actual excess energy from Smart home System", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", + "name": "actualExcessEnergyElectricityMeter", + "displayName": "Actual excess energy from Electricity Meter", + "displayNameEvent": "Actual excess energy from Electricity Meter changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "087c0296-705b-483a-b1e9-7ce08202c035", + "name": "externalSetValueScaling", + "displayName": "Control of the heat source by an external control [100%]", + "displayNameEvent": "Control of the heat source by an external control [100%] changed", + "type": "double", + "unit": "Percentage", + "defaultValue": 100 + }, + { + "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", + "name": "requestExternalHeatSource", + "displayName": "Request external heat source", + "displayNameEvent": "Request external heat source changed", + "type": "QString", + "defaultValue": "No request, external heat source must be turned off", + "possibleValues": [ + "No request, external heat source must be turned off", + "External heat source is released and can be switched on", + "External heat source is required and must be turned on" + ] + } + ], + "actionTypes": [ ] } - ], - "actionTypes": [ - ] - } ] } ] diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp index 21d25bf..afa5864 100644 --- a/mtec/mtec.cpp +++ b/mtec/mtec.cpp @@ -39,13 +39,11 @@ MTec::MTec(const QHostAddress &address, QObject *parent) : m_hostAddress(address) { m_modbusMaster = new ModbusTCPMaster(address, 502, this); + m_modbusMaster->setTimeout(2000); + m_modbusMaster->setNumberOfRetries(5); - qCDebug(dcMTec()) << "created ModbusTCPMaster"; - - if (m_modbusMaster->connectDevice()) { - emit connectedChanged(MTecHelpers::ConnectionState::Connecting); - } - + qCDebug(dcMTec()) << "Created ModbusTCPMaster for" << address.toString(); + connect(m_modbusMaster, &ModbusTCPMaster::connectionStateChanged, this, &MTec::connectedChanged); connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &MTec::onReceivedHoldingRegister); connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &MTec::onModbusError); connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &MTec::onModbusError); @@ -55,59 +53,35 @@ MTec::~MTec() { } -void MTec::onModbusError() +bool MTec::connectDevice() { - qCWarning(dcMTec()) << "MTec: Received modbus error"; - - /* Only emit connected changed signal, if a soft connection - * had already been established. - * This avoids a series of possibly fake connection state changes, - * while the modbus server in the heatpump is starting up. */ - if (m_softConnection) { - m_softConnection = false; - emit connectedChanged(MTecHelpers::ConnectionState::Offline); - } + return m_modbusMaster->connectDevice(); } -void MTec::onRequestStatus() +void MTec::disconnectDevice() { - if ((m_softConnection) || - ((m_connected) && (m_requestsSent < MTec::ConnectionRetries))) { + m_modbusMaster->disconnectDevice(); +} - if (m_requestsSent < MTec::ConnectionRetries) { - m_firstTimeout = QDateTime::currentDateTime(); - m_firstTimeout = m_firstTimeout.addSecs(MTec::FirstConnectionTimeout); - - /* Save original modbus timeout, will be set again - * after first response is received */ - m_modbusTimeout = m_modbusMaster->timeout(); - - /* The M-Tec heatpump requires a longer timeout to - * start-up the modbus communication */ - m_modbusMaster->setTimeout(MTec::FirstConnectionTimeout); - } else { - /* Set back original modbus timeout */ - m_modbusMaster->setTimeout(m_modbusTimeout); - } - - } else { - qCDebug(dcMTec()) << "Max number of modbus connects reached, giving up"; +void MTec::requestStatus() +{ + if (!m_modbusMaster->connected()) { return; } m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualPowerConsumption, 1); - m_requestsSent ++; } +void MTec::onModbusError() +{ + qCWarning(dcMTec()) << "MTec: Received modbus error"; +} + + void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); - /* Some response was received, so the modbus communication is - * established. */ - m_softConnection = true; - emit connectedChanged(MTecHelpers::ConnectionState::Online); - switch (modbusRegister) { case ActualPowerConsumption: if (value.length() == 1) { diff --git a/mtec/mtec.h b/mtec/mtec.h index 1fbe047..181da2c 100644 --- a/mtec/mtec.h +++ b/mtec/mtec.h @@ -49,23 +49,17 @@ public: /** Destructor */ ~MTec(); - inline QHostAddress getHostAddress() const { return m_hostAddress; }; + inline QHostAddress hostAddress() const { return m_hostAddress; }; + + bool connectDevice(); + void disconnectDevice(); + + void requestStatus(); private: - /** - * The first response of the M-Tec heatpump may be delayed - * by 10 s. The plugin will wait for the defined time in milliseconds - * for a response before sending an additional request or setting - * an error code. - */ - static const int FirstConnectionTimeout = 15000; - /** Modbus Unit ID (undocumented, guessing 1 for now) */ static const quint16 ModbusUnitID = 1; - /** Number of retries to establish a modbus connection with the heat pump */ - static const int ConnectionRetries = 3; - /** The following modbus addresses can be read: */ enum RegisterList { /** @@ -112,21 +106,13 @@ private: /** This structure is filled by the receivedStatus... functions */ MTecInfo m_info ; - int m_requestsSent = 0; - bool m_connected = false; - bool m_softConnection = false; - - QDateTime m_firstTimeout; - - int m_modbusTimeout; signals: - void connectedChanged(MTecHelpers::ConnectionState state); + void connectedChanged(bool connected); void statusUpdated(const MTecInfo &info); -public slots: +private slots: void onModbusError(); - void onRequestStatus(); void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value); }; From 539133cc262b88e3e525976ed7f1f3649b38d15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 08:51:19 +0200 Subject: [PATCH 07/14] Log all state by default for debugging --- mtec/integrationpluginmtec.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json index 7ea0e0a..3ba445c 100644 --- a/mtec/integrationpluginmtec.json +++ b/mtec/integrationpluginmtec.json @@ -49,7 +49,8 @@ "displayNameEvent": "Actual power consumption changed", "type": "double", "unit": "Watt", - "defaultValue": 0 + "defaultValue": 0, + "suggestLogging": true }, { "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", @@ -59,7 +60,8 @@ "displayNameAction": "Change actual excess energy from Smart home System", "type": "double", "unit": "Watt", - "defaultValue": 0 + "defaultValue": 0, + "suggestLogging": true }, { "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", @@ -68,7 +70,8 @@ "displayNameEvent": "Actual excess energy from Electricity Meter changed", "type": "double", "unit": "Watt", - "defaultValue": 0 + "defaultValue": 0, + "suggestLogging": true }, { "id": "087c0296-705b-483a-b1e9-7ce08202c035", @@ -77,7 +80,8 @@ "displayNameEvent": "Control of the heat source by an external control [100%] changed", "type": "double", "unit": "Percentage", - "defaultValue": 100 + "defaultValue": 100, + "suggestLogging": true }, { "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", @@ -90,7 +94,8 @@ "No request, external heat source must be turned off", "External heat source is released and can be switched on", "External heat source is required and must be turned on" - ] + ], + "suggestLogging": true } ], "actionTypes": [ From d0d55427c35995c5692e91940f0f43d60f134894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 09:49:09 +0200 Subject: [PATCH 08/14] Setup plugin timer --- mtec/integrationpluginmtec.cpp | 29 ++++++++++++++++++++--------- mtec/integrationpluginmtec.h | 5 ++--- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index 15c3ac3..4da5de1 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -128,7 +128,16 @@ void IntegrationPluginMTec::postSetupThing(Thing *thing) MTec *mtec = m_mtecConnections.value(thing); if (mtec) { update(thing); - //thing->setStateValue(mtecConnectedStateTypeId, true); + } + + 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); + } + }); } } } @@ -136,7 +145,16 @@ void IntegrationPluginMTec::postSetupThing(Thing *thing) void IntegrationPluginMTec::thingRemoved(Thing *thing) { if (m_mtecConnections.contains(thing)) { - m_mtecConnections.take(thing)->deleteLater(); + MTec *mtec = m_mtecConnections.take(thing); + if (mtec) { + mtec->disconnectDevice(); + mtec->deleteLater(); + } + } + + if (myThings().isEmpty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; } } @@ -181,11 +199,4 @@ void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) thing->setStateValue(mtecRequestExternalHeatSourceStateTypeId, info.requestExternalHeatSource); } -void IntegrationPluginMTec::onRefreshTimer() -{ - foreach (Thing *thing, myThings().filterByThingClassId(mtecThingClassId)) { - update(thing); - } -} - diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h index 1dee6ba..0e7d484 100644 --- a/mtec/integrationpluginmtec.h +++ b/mtec/integrationpluginmtec.h @@ -56,12 +56,11 @@ public: void executeAction(ThingActionInfo *info) override; private: + PluginTimer *m_pluginTimer = nullptr; QHash m_mtecConnections; - void update(Thing *thing); - private slots: - void onRefreshTimer(); + void update(Thing *thing); void onStatusUpdated(const MTecInfo &info); }; From f6b81746a6132364db70a7574d781076e56d36b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 09:55:51 +0200 Subject: [PATCH 09/14] Remove ip check since not needed any more due to discovery functionality --- mtec/integrationpluginmtec.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index 4da5de1..a15ba43 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -99,10 +99,6 @@ void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) } qCDebug(dcMTec()) << "Using ip address" << hostAddress.toString(); - if (myThings().filterByParam(mtecThingIpAddressParamTypeId, hostAddress.toString()).count() > 0) { - info->finish(Thing::ThingErrorThingInUse, QT_TR_NOOP("IP address already in use by another thing.")); - return; - } // TODO: start timer and give 15 seconds until connected, since the controler is down for ~10 seconds after a disconnect From 1a1276366a18cd89895e9fbbfb6199329caedc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 11:25:17 +0200 Subject: [PATCH 10/14] Implement wanted states and update using signals --- mtec/integrationpluginmtec.cpp | 98 ++++++++++++++++++-------- mtec/integrationpluginmtec.h | 2 - mtec/integrationpluginmtec.json | 117 ++++++++++++++++++++------------ mtec/mtec.cpp | 72 +++++++++++++------- mtec/mtec.h | 102 ++++++++++++++++------------ mtec/mtec.pro | 3 - mtec/mtechelpers.cpp | 87 ------------------------ mtec/mtechelpers.h | 56 --------------- mtec/mtecinfo.h | 59 ---------------- 9 files changed, 249 insertions(+), 347 deletions(-) delete mode 100644 mtec/mtechelpers.cpp delete mode 100644 mtec/mtechelpers.h delete mode 100644 mtec/mtecinfo.h diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index a15ba43..eabef90 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -100,16 +100,81 @@ void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) qCDebug(dcMTec()) << "Using ip address" << hostAddress.toString(); - // TODO: start timer and give 15 seconds until connected, since the controler is down for ~10 seconds after a disconnect - MTec *mtec = new MTec(hostAddress, this); - connect(mtec, &MTec::statusUpdated, this, &IntegrationPluginMTec::onStatusUpdated); connect(mtec, &MTec::connectedChanged, thing, [=](bool connected){ - qCDebug(dcMTec()) << "Connected changed to" << connected; + qCDebug(dcMTec()) << thing << "Connected changed to" << connected; thing->setStateValue(mtecConnectedStateTypeId, connected); }); + connect(mtec, &MTec::waterTankTemperatureChanged, thing, [=](double waterTankTemperature){ + qCDebug(dcMTec()) << thing << "Water tank temperature" << waterTankTemperature << "°C"; + thing->setStateValue(mtecWaterTankTemperatureStateTypeId, waterTankTemperature); + }); + + connect(mtec, &MTec::bufferTankTemperatureChanged, thing, [=](double bufferTankTemperature){ + qCDebug(dcMTec()) << thing << "Buffer tank temperature" << bufferTankTemperature << "°C"; + thing->setStateValue(mtecBufferTankTemperatureStateTypeId, bufferTankTemperature); + }); + + connect(mtec, &MTec::totalAccumulatedElectricalEnergyChanged, thing, [=](double totalAccumulatedElectricalEnergy){ + qCDebug(dcMTec()) << thing << "Total accumulated 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"); + break; + case MTec::HeatpumpStatePreRun: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Pre run"); + break; + case MTec::HeatpumpStateAutomaticHeat: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Automatic heat"); + break; + case MTec::HeatpumpStateDefrost: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Defrost"); + break; + case MTec::HeatpumpStateAutomaticCool: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Automatic cool"); + break; + case MTec::HeatpumpStatePostRun: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Post run"); + break; + case MTec::HeatpumpStateSaftyShutdown: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Safty shutdown"); + break; + case MTec::HeatpumpStateError: + thing->setStateValue(mtecHeatPumpStateStateTypeId, "Error"); + 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::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."; } @@ -159,13 +224,6 @@ void IntegrationPluginMTec::executeAction(ThingActionInfo *info) // Thing *thing = info->thing(); // Action action = info->action(); info->finish(Thing::ThingErrorNoError); - -// if (thing->thingClassId() == mtecThingClassId) { -// /* if (action.actionTypeId() == mtecPowerActionTypeId) { */ - -// } else { -// Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); -// } } void IntegrationPluginMTec::update(Thing *thing) @@ -174,25 +232,9 @@ void IntegrationPluginMTec::update(Thing *thing) qCDebug(dcMTec()) << "Updating thing" << thing; MTec *mtec = m_mtecConnections.value(thing); if (!mtec) return; - mtec->requestStatus(); + mtec->updateValues(); } } -void IntegrationPluginMTec::onStatusUpdated(const MTecInfo &info) -{ - MTec *mtec = qobject_cast(sender()); - Thing *thing = m_mtecConnections.key(mtec); - if (!thing) - return; - - qCDebug(dcMTec()) << "Received status from heat pump" << thing; - /* Received a structure holding the status info of the - * heat pump. Update the thing states with the individual fields. */ - thing->setStateValue(mtecActualPowerConsumptionStateTypeId, info.actualPowerConsumption); - thing->setStateValue(mtecActualExcessEnergySmartHomeStateTypeId, info.actualExcessEnergySmartHome); - thing->setStateValue(mtecActualExcessEnergyElectricityMeterStateTypeId, info.actualExcessEnergyElectricityMeter); - thing->setStateValue(mtecExternalSetValueScalingStateTypeId, info.externalSetValueScaling); - thing->setStateValue(mtecRequestExternalHeatSourceStateTypeId, info.requestExternalHeatSource); -} diff --git a/mtec/integrationpluginmtec.h b/mtec/integrationpluginmtec.h index 0e7d484..c1302a4 100644 --- a/mtec/integrationpluginmtec.h +++ b/mtec/integrationpluginmtec.h @@ -34,7 +34,6 @@ #include "plugintimer.h" #include "mtec.h" -#include "mtechelpers.h" #include @@ -61,7 +60,6 @@ private: private slots: void update(Thing *thing); - void onStatusUpdated(const MTecInfo &info); }; diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json index 3ba445c..1ab7f82 100644 --- a/mtec/integrationpluginmtec.json +++ b/mtec/integrationpluginmtec.json @@ -41,60 +41,91 @@ "type": "bool", "defaultValue": false, "cached": false + }, + { + "id": "545f94d6-f4fd-48fe-bf3b-f193e5cb76e7", + "name": "waterTankTemperature", + "displayName": "Water tank temperature", + "displayNameEvent": "Water tank temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0 + }, + { + "id": "a98e37f8-dcdc-4c4c-aecf-07f376321849", + "name": "bufferTankTemperature", + "displayName": "Buffer tank temperature", + "displayNameEvent": "Buffer tank temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0 + }, + { + "id": "d0c8f168-49b5-47ca-9988-c9922be38dd5", + "name": "outdoorTemperature", + "displayName": "Outdoor temperature", + "displayNameEvent": "Outdoor temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0 }, { "id": "c67c79cf-7369-409f-b170-16c4ece9d25a", - "name": "actualPowerConsumption", - "displayName": "Actual power consumption", - "displayNameEvent": "Actual power consumption changed", + "name": "totalAccumulatedElectricalEnergy", + "displayName": "Total accumulated electrical energy", + "displayNameEvent": "Total accumulated electrical energy changed", "type": "double", - "unit": "Watt", + "unit": "KiloWattHour", "defaultValue": 0, "suggestLogging": true }, { - "id": "663718fa-807e-4d85-bd78-61a65f8c0b5e", - "name": "actualExcessEnergySmartHome", - "displayName": "Actual excess energy from Smart home System", - "displayNameEvent": "Actual excess energy from Smart home System changed", - "displayNameAction": "Change actual excess energy from Smart home System", - "type": "double", - "unit": "Watt", - "defaultValue": 0, - "suggestLogging": true - }, - { - "id": "fd94d39c-0db6-497f-a0a5-6c5452cbcaaf", - "name": "actualExcessEnergyElectricityMeter", - "displayName": "Actual excess energy from Electricity Meter", - "displayNameEvent": "Actual excess energy from Electricity Meter changed", - "type": "double", - "unit": "Watt", - "defaultValue": 0, - "suggestLogging": true - }, - { - "id": "087c0296-705b-483a-b1e9-7ce08202c035", - "name": "externalSetValueScaling", - "displayName": "Control of the heat source by an external control [100%]", - "displayNameEvent": "Control of the heat source by an external control [100%] changed", - "type": "double", - "unit": "Percentage", - "defaultValue": 100, - "suggestLogging": true - }, - { - "id": "90b17788-ce63-47e3-b97d-1b025a41ce35", - "name": "requestExternalHeatSource", - "displayName": "Request external heat source", - "displayNameEvent": "Request external heat source changed", + "id": "1e2037c8-09dc-4396-974c-efa9c486aa65", + "name": "heatPumpState", + "displayName": "Heat pump state", + "displayNameEvent": "Heat pump state changed", "type": "QString", - "defaultValue": "No request, external heat source must be turned off", "possibleValues": [ - "No request, external heat source must be turned off", - "External heat source is released and can be switched on", - "External heat source is required and must be turned on" + "Standby", + "Pre run", + "Automatic heat", + "Defrost", + "Automatic cool", + "Post run", + "Safty shutdown", + "Error" ], + "defaultValue": "Standby", + "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", + "type": "double", + "unit": "Watt", + "defaultValue": 0, "suggestLogging": true } ], diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp index afa5864..c8fd5bb 100644 --- a/mtec/mtec.cpp +++ b/mtec/mtec.cpp @@ -30,9 +30,6 @@ #include "mtec.h" #include "extern-plugininfo.h" -#include "mtechelpers.h" - -#include MTec::MTec(const QHostAddress &address, QObject *parent) : QObject(parent), @@ -51,6 +48,7 @@ MTec::MTec(const QHostAddress &address, QObject *parent) : MTec::~MTec() { + m_modbusMaster->disconnectDevice(); } bool MTec::connectDevice() @@ -63,58 +61,82 @@ void MTec::disconnectDevice() m_modbusMaster->disconnectDevice(); } -void MTec::requestStatus() +void MTec::updateValues() { if (!m_modbusMaster->connected()) { return; } - m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualPowerConsumption, 1); + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHotWaterTankTemperature, 1); } void MTec::onModbusError() { - qCWarning(dcMTec()) << "MTec: Received modbus error"; + qCWarning(dcMTec()) << "Modbus error occured" << m_modbusMaster->errorString(); } - void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value) { Q_UNUSED(slaveAddress); switch (modbusRegister) { - case ActualPowerConsumption: + case RegisterHotWaterTankTemperature: if (value.length() == 1) { - m_info.actualPowerConsumption = MTecHelpers::convertRegisterToFloat(value[0]); + m_waterTankTemperature = value[0] / 10.0; + emit waterTankTemperatureChanged(m_waterTankTemperature); } - m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualExcessEnergySmartHome, 1); + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterBufferTankTemperature, 1); break; - case ActualExcessEnergySmartHome: + case RegisterBufferTankTemperature: if (value.length() == 1) { - m_info.actualExcessEnergySmartHome = MTecHelpers::convertRegisterToFloat(value[0]); + m_bufferTankTemperature = value[0] / 10.0; + emit bufferTankTemperatureChanged(m_bufferTankTemperature); } - m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ActualExcessEnergyElectricityMeter, 1); + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTotalAccumulatedElectricalEnergy, 1); break; - case ActualExcessEnergyElectricityMeter: + case RegisterTotalAccumulatedElectricalEnergy: if (value.length() == 1) { - m_info.actualExcessEnergyElectricityMeter = MTecHelpers::convertRegisterToFloat(value[0]); + m_totalAccumulatedElectricalEnergy = value[0] / 100.0; + emit totalAccumulatedElectricalEnergyChanged(m_totalAccumulatedElectricalEnergy); } - m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::ExternalSetValueScaling, 1); + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatpumpState, 1); break; - case ExternalSetValueScaling: + case RegisterHeatpumpState: if (value.length() == 1) { - m_info.externalSetValueScaling = MTecHelpers::convertRegisterToFloat(value[0]); + m_heatPumpState = static_cast(value[0]); + emit heatPumpStateChanged(m_heatPumpState); } - m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, MTec::RequestExternalHeatSource, 1); + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatMeterPowerConsumption, 1); break; - case RequestExternalHeatSource: + case RegisterHeatMeterPowerConsumption: if (value.length() == 1) { - m_info.requestExternalHeatSource = MTecHelpers::externalHeatSourceRequestToString(value[0]); + m_heatMeterPowerConsumption = value[0] / 100.0; + emit heatMeterPowerConsumptionChanged(m_heatMeterPowerConsumption); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterEnergyMeterPowerConsumption, 1); + break; + case RegisterEnergyMeterPowerConsumption: + if (value.length() == 1) { + m_energyMeterPowerConsumption = value[0] / 100.0; + emit energyMeterPowerConsumptionChanged(m_energyMeterPowerConsumption); + } + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualExcessEnergySmartHome, 1); + break; + case RegisterActualExcessEnergySmartHome: + if (value.length() == 1) { + m_actualExcessEnergySmartHome = value[0] / 100.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 - /* Everything read without errors - * -> update status in plugin */ - emit statusUpdated(m_info); - } break; } } diff --git a/mtec/mtec.h b/mtec/mtec.h index 181da2c..7b20057 100644 --- a/mtec/mtec.h +++ b/mtec/mtec.h @@ -36,17 +36,23 @@ #include "../modbus/modbustcpmaster.h" -#include "mtecinfo.h" -#include "mtechelpers.h" - class MTec : public QObject { Q_OBJECT public: - /** Constructor */ - explicit MTec(const QHostAddress &address, QObject *parent = nullptr); + enum HeatpumpState { + HeatpumpStateStandby = 0, + HeatpumpStatePreRun = 1, + HeatpumpStateAutomaticHeat = 2, + HeatpumpStateDefrost = 3, + HeatpumpStateAutomaticCool = 4, + HeatpumpStatePostRun = 5, + HeatpumpStateSaftyShutdown = 7, + HeatpumpStateError = 8 + }; + Q_ENUM(HeatpumpState) - /** Destructor */ + explicit MTec(const QHostAddress &address, QObject *parent = nullptr); ~MTec(); inline QHostAddress hostAddress() const { return m_hostAddress; }; @@ -54,62 +60,70 @@ public: bool connectDevice(); void disconnectDevice(); - void requestStatus(); + void updateValues(); private: /** Modbus Unit ID (undocumented, guessing 1 for now) */ static const quint16 ModbusUnitID = 1; /** The following modbus addresses can be read: */ - enum RegisterList { - /** - * APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power - * Actual power consumtion [W] - */ - ActualPowerConsumption = 707, + enum Register { + /* APPL.CtrlAppl.sParam.hotWaterTank[0].topTemp.values.actValue + * Hot water tank top temperature [1/10°C]. */ + RegisterHotWaterTankTemperature = 401, - /** - * APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0] - * Acutal excess energy given from Smart home System [W] - */ - ActualExcessEnergySmartHome = 1000, + /* APPL.CtrlAppl.sParam.bufferTank[0].topTemp.values.actValue + * Buffer Actual top temperature [1/10°C]. */ + RegisterBufferTankTemperature = 601, - /** - * APPL.CtrlAppl.sParam.photovoltaics.ElectricEnergyMeter.values.power - * Acutal excess energy given from Electricity Meter [W] - */ - ActualExcessEnergyElectricityMeter = 1002, + /* APPL.CtrlAppl.sStatisticalData.heatpump[0].consumption.electricalenergy + * Total accumulated electrical energy [kWh] */ + RegisterTotalAccumulatedElectricalEnergy = 702, - /** - * APPL.CtrlAppl.sParam.extHeatSource[0].param.externalSetvalueScaled - * Control of the heat source by an external control [100%] - */ - ExternalSetValueScaling = 1600, + /* APPL.CtrlAppl.sParam.heatpump[0].values.heatpumpState */ + RegisterHeatpumpState = 703, + + /* APPL.CtrlAppl.sParam.heatpump[0].HeatMeter.values.power + * Actual power consumtion [W] */ + RegisterHeatMeterPowerConsumption = 706, + + /* APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power + * Actual power consumtion [W] */ + RegisterEnergyMeterPowerConsumption = 707, + + /* APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0] + * Acutal excess energy given from Smart home System [W] */ + RegisterActualExcessEnergySmartHome = 1000, + + /* APPL.CtrlAppl.sParam.outdoorTemp.values.actValue + * Actual exterior temperature [°C] */ + RegisterActualOutdoorTemperature = 1502, - /** - * APPL.CtrlAppl.sParam.extHeatSource[0].values.requestExtHeatsource - * 0…no request, external heat source must be turned off. - * 1…external heat source is released and can be switched on. - * 2…external heat source is required and must be turned on. - */ - RequestExternalHeatSource = 1601 }; - /* Note: This class only requires one IP address and one - * TCP Modbus connection. Multiple devices are managed - * within the IntegrationPluginMTec class. */ QHostAddress m_hostAddress; - - /** Pointer to ModbusTCPMaster object, responseible for low-level communicaiton */ ModbusTCPMaster *m_modbusMaster = nullptr; - /** This structure is filled by the receivedStatus... functions */ - MTecInfo m_info ; - + double m_waterTankTemperature = 0; + double m_bufferTankTemperature = 0; + double m_totalAccumulatedElectricalEnergy = 0; + HeatpumpState m_heatPumpState = HeatpumpStateStandby; + double m_heatMeterPowerConsumption = 0; + double m_energyMeterPowerConsumption = 0; + double m_actualExcessEnergySmartHome = 0; + double m_actualOutdoorTemperature = 0; signals: void connectedChanged(bool connected); - void statusUpdated(const MTecInfo &info); + + void waterTankTemperatureChanged(double waterTankTemperature); + void bufferTankTemperatureChanged(double bufferTankTemperature); + void totalAccumulatedElectricalEnergyChanged(double totalAccumulatedElectricalEnergy); + void heatPumpStateChanged(HeatpumpState heatPumpState); + void heatMeterPowerConsumptionChanged(double heatMeterPowerConsumption); + void energyMeterPowerConsumptionChanged(double energyMeterPowerConsumption); + void actualExcessEnergySmartHomeChanged(double actualExcessEnergySmartHome); + void actualOutdoorTemperatureChanged(double actualOutdoorTemperature); private slots: void onModbusError(); diff --git a/mtec/mtec.pro b/mtec/mtec.pro index eaaf6a0..c68ed3b 100644 --- a/mtec/mtec.pro +++ b/mtec/mtec.pro @@ -6,14 +6,11 @@ QT += \ SOURCES += \ mtec.cpp \ - mtechelpers.cpp \ integrationpluginmtec.cpp \ ../modbus/modbustcpmaster.cpp HEADERS += \ mtec.h \ - mtechelpers.h \ - mtecinfo.h \ integrationpluginmtec.h \ ../modbus/modbustcpmaster.h \ diff --git a/mtec/mtechelpers.cpp b/mtec/mtechelpers.cpp deleted file mode 100644 index b57547b..0000000 --- a/mtec/mtechelpers.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* 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 "mtechelpers.h" - -float MTecHelpers::convertRegisterToFloat(const quint16 reg) -{ - float value = 0.0; - - value = reg/100.0; - - return value; -} - -void MTecHelpers::convertFloatToRegister(quint16 ®, float value) -{ - reg = qRound(value * 100.0); -} - -QString MTecHelpers::connectionStateToString(ConnectionState state) -{ - QString str{}; - - switch (state) { - case Offline: - str = QT_TR_NOOP("Off"); - break; - case Connecting: - str = QT_TR_NOOP("Connecting"); - break; - case Online: - str = QT_TR_NOOP("Connected"); - break; - case Error: - str = QT_TR_NOOP("Error"); - break; - } - - return str; -} - -QString MTecHelpers::externalHeatSourceRequestToString(quint16 value) -{ - QString str{}; - - switch (value) { - case 0: - str = QT_TR_NOOP("No request, external heat source must be turned off"); - break; - case 1: - str = QT_TR_NOOP("External heat source is released and can be switched on"); - break; - case 2: - str = QT_TR_NOOP("External heat source is required and must be turned on"); - break; - } - - return str; -} - diff --git a/mtec/mtechelpers.h b/mtec/mtechelpers.h deleted file mode 100644 index c95ae28..0000000 --- a/mtec/mtechelpers.h +++ /dev/null @@ -1,56 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* 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 MTECHELPERS_H -#define MTECHELPERS_H - -#include -#include -#include - -class MTecHelpers : public QObject { -public: - enum ConnectionState { - Offline, - Connecting, - Online, - Error - }; - Q_ENUM(ConnectionState); - - static float convertRegisterToFloat(quint16 reg); - static void convertFloatToRegister(quint16 ®, float value); - - static QString connectionStateToString(ConnectionState state); - static QString externalHeatSourceRequestToString(quint16 value); -}; - -#endif - diff --git a/mtec/mtecinfo.h b/mtec/mtecinfo.h deleted file mode 100644 index fe87854..0000000 --- a/mtec/mtecinfo.h +++ /dev/null @@ -1,59 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* 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 MTECINFO_H -#define MTECINFO_H - -#include -#include - -/** This struct holds the status information that is read from the MTec device - * and passed to the nymea framework within this plugin. - */ -struct MTecInfo { - /** Contains more detailed info on the status - * (Off, Connecting, Connected, Error) */ - QString status; - - double actualPowerConsumption; - - double actualExcessEnergySmartHome; - - double actualExcessEnergyElectricityMeter; - - double externalSetValueScaling; - - QString requestExternalHeatSource; -}; - -Q_DECLARE_METATYPE(MTecInfo); - -#endif - From b79a055f48964ca617874aea017053df7268ccc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 13:59:22 +0200 Subject: [PATCH 11/14] Add thermostat interface, extend values and implement execute action --- mtec/integrationpluginmtec.cpp | 117 ++++++++++++++++++++++++-- mtec/integrationpluginmtec.json | 141 +++++++++++++++++++++++--------- mtec/mtec.cpp | 73 +++++++++++++++-- mtec/mtec.h | 51 +++++++++--- 4 files changed, 317 insertions(+), 65 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index eabef90..e1ebbd0 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -106,9 +106,19 @@ void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) thing->setStateValue(mtecConnectedStateTypeId, connected); }); - connect(mtec, &MTec::waterTankTemperatureChanged, thing, [=](double waterTankTemperature){ - qCDebug(dcMTec()) << thing << "Water tank temperature" << waterTankTemperature << "°C"; - thing->setStateValue(mtecWaterTankTemperatureStateTypeId, waterTankTemperature); + 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){ @@ -116,8 +126,13 @@ void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) 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 energy" << totalAccumulatedElectricalEnergy << "kWh"; + qCDebug(dcMTec()) << thing << "Total accumulated electrical energy" << totalAccumulatedElectricalEnergy << "kWh"; thing->setStateValue(mtecTotalAccumulatedElectricalEnergyStateTypeId, totalAccumulatedElectricalEnergy); }); @@ -126,27 +141,43 @@ void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) 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; } }); @@ -166,6 +197,11 @@ void IntegrationPluginMTec::setupThing(ThingSetupInfo *info) 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); @@ -221,9 +257,76 @@ void IntegrationPluginMTec::thingRemoved(Thing *thing) void IntegrationPluginMTec::executeAction(ThingActionInfo *info) { -// Thing *thing = info->thing(); -// Action action = info->action(); - info->finish(Thing::ThingErrorNoError); + 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"; + 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"; + 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) diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json index 1ab7f82..dfa068e 100644 --- a/mtec/integrationpluginmtec.json +++ b/mtec/integrationpluginmtec.json @@ -13,7 +13,7 @@ "displayName": "MTec", "id": "451e38d8-50d5-4ae9-8d9f-21af9347128d", "createMethods": ["discovery", "user"], - "interfaces": ["connectable"], + "interfaces": ["thermostat", "connectable"], "paramTypes": [ { "id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71", @@ -41,43 +41,6 @@ "type": "bool", "defaultValue": false, "cached": false - }, - { - "id": "545f94d6-f4fd-48fe-bf3b-f193e5cb76e7", - "name": "waterTankTemperature", - "displayName": "Water tank temperature", - "displayNameEvent": "Water tank temperature changed", - "unit": "DegreeCelsius", - "type": "double", - "defaultValue": 0 - }, - { - "id": "a98e37f8-dcdc-4c4c-aecf-07f376321849", - "name": "bufferTankTemperature", - "displayName": "Buffer tank temperature", - "displayNameEvent": "Buffer tank temperature changed", - "unit": "DegreeCelsius", - "type": "double", - "defaultValue": 0 - }, - { - "id": "d0c8f168-49b5-47ca-9988-c9922be38dd5", - "name": "outdoorTemperature", - "displayName": "Outdoor temperature", - "displayNameEvent": "Outdoor temperature changed", - "unit": "DegreeCelsius", - "type": "double", - "defaultValue": 0 - }, - { - "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": "1e2037c8-09dc-4396-974c-efa9c486aa65", @@ -98,6 +61,91 @@ "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 + }, + { + "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 + }, + { + "id": "07465fbb-6949-4bd1-90d5-acf2d80c161d", + "name": "heatingOn", + "displayName": "Heating on", + "displayNameEvent": "Heating turned on/off", + "type": "bool", + "defaultValue": false + }, + { + "id": "8b407c1d-b84f-48d4-9961-b29bc58fff0e", + "name": "coolingOn", + "displayName": "Cooling on", + "displayNameEvent": "Cooling turned on/off", + "type": "bool", + "defaultValue": false + }, + { + "id": "d0c8f168-49b5-47ca-9988-c9922be38dd5", + "name": "outdoorTemperature", + "displayName": "Outdoor temperature", + "displayNameEvent": "Outdoor temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0 + }, + { + "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 + }, + { + "id": "a98e37f8-dcdc-4c4c-aecf-07f376321849", + "name": "bufferTankTemperature", + "displayName": "Buffer tank temperature", + "displayNameEvent": "Buffer tank temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0 + }, + { + "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", @@ -123,14 +171,27 @@ "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": [ - ] + "actionTypes": [ ] } ] } diff --git a/mtec/mtec.cpp b/mtec/mtec.cpp index c8fd5bb..ac953c5 100644 --- a/mtec/mtec.cpp +++ b/mtec/mtec.cpp @@ -51,6 +51,37 @@ 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(); @@ -67,7 +98,7 @@ void MTec::updateValues() return; } - m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHotWaterTankTemperature, 1); + m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterRoomTemperature, 1); } void MTec::onModbusError() @@ -80,10 +111,24 @@ void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const 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_waterTankTemperature = value[0] / 10.0; - emit waterTankTemperatureChanged(m_waterTankTemperature); + m_waterTankTopTemperature = value[0] / 10.0; + emit waterTankTopTemperatureChanged(m_waterTankTopTemperature); } m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterBufferTankTemperature, 1); break; @@ -92,11 +137,18 @@ void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const 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] / 100.0; + m_totalAccumulatedElectricalEnergy = value[0]; emit totalAccumulatedElectricalEnergyChanged(m_totalAccumulatedElectricalEnergy); } m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatpumpState, 1); @@ -110,21 +162,28 @@ void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const break; case RegisterHeatMeterPowerConsumption: if (value.length() == 1) { - m_heatMeterPowerConsumption = value[0] / 100.0; + 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] / 100.0; + 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] / 100.0; + 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); diff --git a/mtec/mtec.h b/mtec/mtec.h index 7b20057..50f1740 100644 --- a/mtec/mtec.h +++ b/mtec/mtec.h @@ -55,8 +55,13 @@ public: explicit MTec(const QHostAddress &address, QObject *parent = nullptr); ~MTec(); - inline QHostAddress hostAddress() const { return m_hostAddress; }; + QHostAddress hostAddress() const; + bool connected() const; + QModbusReply *setTargetRoomTemperature(double targetRoomTemperature); + QModbusReply *setSmartHomeEnergy(quint16 smartHomeEnergy); + +public slots: bool connectDevice(); void disconnectDevice(); @@ -68,34 +73,50 @@ private: /** The following modbus addresses can be read: */ enum Register { - /* APPL.CtrlAppl.sParam.hotWaterTank[0].topTemp.values.actValue + /* 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, - /* APPL.CtrlAppl.sParam.bufferTank[0].topTemp.values.actValue + /* R APPL.CtrlAppl.sParam.bufferTank[0].topTemp.values.actValue * Buffer Actual top temperature [1/10°C]. */ RegisterBufferTankTemperature = 601, - /* APPL.CtrlAppl.sStatisticalData.heatpump[0].consumption.electricalenergy + /* 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, - /* APPL.CtrlAppl.sParam.heatpump[0].values.heatpumpState */ + /* R APPL.CtrlAppl.sParam.heatpump[0].values.heatpumpState */ RegisterHeatpumpState = 703, - /* APPL.CtrlAppl.sParam.heatpump[0].HeatMeter.values.power + /* R APPL.CtrlAppl.sParam.heatpump[0].HeatMeter.values.power * Actual power consumtion [W] */ RegisterHeatMeterPowerConsumption = 706, - /* APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power + /* R APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power * Actual power consumtion [W] */ RegisterEnergyMeterPowerConsumption = 707, - /* APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0] + /* RW APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0] * Acutal excess energy given from Smart home System [W] */ RegisterActualExcessEnergySmartHome = 1000, - /* APPL.CtrlAppl.sParam.outdoorTemp.values.actValue + /* 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, @@ -104,25 +125,33 @@ private: QHostAddress m_hostAddress; ModbusTCPMaster *m_modbusMaster = nullptr; - double m_waterTankTemperature = 0; + 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 waterTankTemperatureChanged(double waterTankTemperature); + 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: From 9f8beae620d00c806240bd850dca19fc1b33473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 14:07:11 +0200 Subject: [PATCH 12/14] Fixing action execution value update --- mtec/integrationpluginmtec.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index e1ebbd0..c67b63d 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -288,6 +288,7 @@ void IntegrationPluginMTec::executeAction(ThingActionInfo *info) 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); @@ -313,6 +314,7 @@ void IntegrationPluginMTec::executeAction(ThingActionInfo *info) 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); From 9808059f3c4729b43101241777034eec871b1b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 14:12:24 +0200 Subject: [PATCH 13/14] Make sure all states get logged --- mtec/integrationpluginmtec.json | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mtec/integrationpluginmtec.json b/mtec/integrationpluginmtec.json index dfa068e..0885ba8 100644 --- a/mtec/integrationpluginmtec.json +++ b/mtec/integrationpluginmtec.json @@ -72,7 +72,8 @@ "writable": true, "minValue": 10, "maxValue": 30, - "defaultValue": 20 + "defaultValue": 20, + "suggestLogging": true }, { "id": "b22ac9bb-3842-497c-bd93-f8bea6670e32", @@ -81,7 +82,8 @@ "displayNameEvent": "Room temperature heat circuit 0 changed", "unit": "DegreeCelsius", "type": "double", - "defaultValue": 20 + "defaultValue": 20, + "suggestLogging": true }, { "id": "07465fbb-6949-4bd1-90d5-acf2d80c161d", @@ -89,7 +91,8 @@ "displayName": "Heating on", "displayNameEvent": "Heating turned on/off", "type": "bool", - "defaultValue": false + "defaultValue": false, + "suggestLogging": true }, { "id": "8b407c1d-b84f-48d4-9961-b29bc58fff0e", @@ -97,7 +100,8 @@ "displayName": "Cooling on", "displayNameEvent": "Cooling turned on/off", "type": "bool", - "defaultValue": false + "defaultValue": false, + "suggestLogging": true }, { "id": "d0c8f168-49b5-47ca-9988-c9922be38dd5", @@ -106,7 +110,8 @@ "displayNameEvent": "Outdoor temperature changed", "unit": "DegreeCelsius", "type": "double", - "defaultValue": 0 + "defaultValue": 0, + "suggestLogging": true }, { "id": "545f94d6-f4fd-48fe-bf3b-f193e5cb76e7", @@ -115,7 +120,8 @@ "displayNameEvent": "Water tank top temperature changed", "unit": "DegreeCelsius", "type": "double", - "defaultValue": 0 + "defaultValue": 0, + "suggestLogging": true }, { "id": "a98e37f8-dcdc-4c4c-aecf-07f376321849", @@ -124,7 +130,8 @@ "displayNameEvent": "Buffer tank temperature changed", "unit": "DegreeCelsius", "type": "double", - "defaultValue": 0 + "defaultValue": 0, + "suggestLogging": true }, { "id": "7d087af8-cdbe-463e-a9bb-7a7a79471963", From 180898a5a58e75d5b648312cc86adaf1f8240057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 11 Jun 2021 11:11:30 +0200 Subject: [PATCH 14/14] Update discovery to renamed network device --- mtec/integrationpluginmtec.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mtec/integrationpluginmtec.cpp b/mtec/integrationpluginmtec.cpp index c67b63d..858acd8 100644 --- a/mtec/integrationpluginmtec.cpp +++ b/mtec/integrationpluginmtec.cpp @@ -48,34 +48,34 @@ void IntegrationPluginMTec::discoverThings(ThingDiscoveryInfo *info) // Perform a network device discovery and filter for "go-eCharger" hosts NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { - qCDebug(dcMTec()) << "Found" << networkDevice; + qCDebug(dcMTec()) << "Found" << networkDeviceInfo; QString title; - if (networkDevice.hostName().isEmpty()) { - title = networkDevice.address().toString(); + if (networkDeviceInfo.hostName().isEmpty()) { + title = networkDeviceInfo.address().toString(); } else { - title = networkDevice.hostName() + " (" + networkDevice.address().toString() + ")"; + title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; } QString description; - if (networkDevice.macAddressManufacturer().isEmpty()) { - description = networkDevice.macAddress(); + if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { + description = networkDeviceInfo.macAddress(); } else { - description = networkDevice.macAddress() + " (" + networkDevice.macAddressManufacturer() + ")"; + description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; } ThingDescriptor descriptor(mtecThingClassId, title, description); ParamList params; - params << Param(mtecThingIpAddressParamTypeId, networkDevice.address().toString()); - params << Param(mtecThingMacAddressParamTypeId, networkDevice.macAddress()); + 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, networkDevice.macAddress()); + Things existingThings = myThings().filterByParam(mtecThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { - qCDebug(dcMTec()) << "This heat pump already exists in the system!" << networkDevice; + qCDebug(dcMTec()) << "This heat pump already exists in the system!" << networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); }