Merge PR #21: New Plugin: M-Tec

This commit is contained in:
Jenkins nymea 2021-06-30 12:44:10 +02:00
commit b540f72760
12 changed files with 1362 additions and 107 deletions

16
debian/control vendored
View File

@ -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},

1
debian/nymea-plugin-mtec.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginmtec.so

View File

@ -34,59 +34,131 @@
NYMEA_LOGGING_CATEGORY(dcModbusTCP, "ModbusTCP")
ModbusTCPMaster::ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent) :
QObject(parent)
QObject(parent),
m_hostAddress(hostAddress),
m_port(port)
{
m_modbusTcpClient = new QModbusTcpClient(this);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, hostAddress.toString());
m_modbusTcpClient->setTimeout(1000);
m_modbusTcpClient->setNumberOfRetries(3);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString());
m_modbusTcpClient->setTimeout(m_timeout);
m_modbusTcpClient->setNumberOfRetries(m_numberOfRetries);
connect(m_modbusTcpClient, &QModbusTcpClient::stateChanged, this, &ModbusTCPMaster::onModbusStateChanged);
connect(m_modbusTcpClient, &QModbusRtuSerialMaster::errorOccurred, this, &ModbusTCPMaster::onModbusErrorOccurred);
m_reconnectTimer = new QTimer(this);
m_reconnectTimer->setSingleShot(true);
connect(m_reconnectTimer, &QTimer::timeout, this, &ModbusTCPMaster::onReconnectTimer);
m_reconnectTimer->setInterval(4000);
connect(m_reconnectTimer, &QTimer::timeout, this, &ModbusTCPMaster::connectDevice);
}
ModbusTCPMaster::~ModbusTCPMaster()
{
if (!m_modbusTcpClient) {
m_modbusTcpClient->disconnectDevice();
m_modbusTcpClient->deleteLater();
}
if (!m_reconnectTimer) {
if (m_reconnectTimer) {
m_reconnectTimer->stop();
m_reconnectTimer->deleteLater();
}
if (m_modbusTcpClient) {
disconnectDevice();
}
}
QHostAddress ModbusTCPMaster::hostAddress() const
{
return m_hostAddress;
}
uint ModbusTCPMaster::port() const
{
return m_port;
}
void ModbusTCPMaster::setPort(uint port)
{
m_port = port;
}
void ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress)
{
m_hostAddress = hostAddress;
}
bool ModbusTCPMaster::connectDevice() {
// TCP connection to target device
qCDebug(dcModbusTCP()) << "Setting up TCP connecion";
if (!m_modbusTcpClient)
return false;
return m_modbusTcpClient->connectDevice();
// Only connect if we are in the unconnected state
if (m_modbusTcpClient->state() == QModbusDevice::UnconnectedState) {
qCDebug(dcModbusTCP()) << "Connecting modbus TCP client to" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, m_port);
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, m_hostAddress.toString());
m_modbusTcpClient->setTimeout(m_timeout);
m_modbusTcpClient->setNumberOfRetries(m_numberOfRetries);
return m_modbusTcpClient->connectDevice();
} else if (m_modbusTcpClient->state() != QModbusDevice::ConnectedState) {
// Restart the timer in case of connecting not finished yet or closing
m_reconnectTimer->start();
} else {
qCWarning(dcModbusTCP()) << "Connect modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port) << "called, but the socket is currently in the" << m_modbusTcpClient->state();
}
return false;
}
bool ModbusTCPMaster::connected()
void ModbusTCPMaster::disconnectDevice()
{
return (m_modbusTcpClient->state() == QModbusDevice::State::ConnectedState);
if (!m_modbusTcpClient)
return;
// Stop the reconnect timer since disconnect was explicitly called
m_reconnectTimer->stop();
m_modbusTcpClient->disconnectDevice();
}
bool ModbusTCPMaster::reconnectDevice()
{
qCWarning(dcModbusTCP()) << "Reconnecting modbus TCP device" << QString("%1:%2").arg(m_hostAddress.toString()).arg(m_port);
if (!m_modbusTcpClient)
return false;
disconnectDevice();
return connectDevice();
}
bool ModbusTCPMaster::connected() const
{
return m_connected;
}
int ModbusTCPMaster::numberOfRetries() const
{
return m_modbusTcpClient->numberOfRetries();
}
void ModbusTCPMaster::setNumberOfRetries(int number)
{
m_numberOfRetries = number;
m_modbusTcpClient->setNumberOfRetries(number);
}
int ModbusTCPMaster::timeout() const
{
return m_modbusTcpClient->timeout();
}
void ModbusTCPMaster::setTimeout(int timeout)
{
m_timeout = timeout;
m_modbusTcpClient->setTimeout(timeout);
}
int ModbusTCPMaster::timeout()
{
return m_modbusTcpClient->timeout();
}
QString ModbusTCPMaster::errorString() const
{
return m_modbusTcpClient->errorString();
@ -97,74 +169,44 @@ QModbusDevice::Error ModbusTCPMaster::error() const
return m_modbusTcpClient->error();
}
uint ModbusTCPMaster::port()
{
return m_modbusTcpClient->connectionParameter(QModbusDevice::NetworkPortParameter).toUInt();
}
bool ModbusTCPMaster::setHostAddress(const QHostAddress &hostAddress)
{
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, hostAddress.toString());
return connectDevice();
}
bool ModbusTCPMaster::setPort(uint port)
{
m_modbusTcpClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port);
return connectDevice();
}
void ModbusTCPMaster::onReconnectTimer()
{
if(!m_modbusTcpClient->connectDevice()) {
m_reconnectTimer->start(10000);
}
}
QHostAddress ModbusTCPMaster::hostAddress()
{
return QHostAddress(m_modbusTcpClient->connectionParameter(QModbusDevice::NetworkAddressParameter).toString());
}
QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return "";
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, size);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, slaveAddress)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, requestId, this] {
if (reply->error() == QModbusDevice::NoError) {
emit readRequestExecuted(requestId, true);
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
emit receivedCoil(reply->serverAddress(), modbusAddress, unit.values());
} else {
emit readRequestExecuted(requestId, false);
qCWarning(dcModbusTCP()) << "Read response error:" << reply->error();
}
});
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTCP()) << "Modbus reply error:" << error;
emit readRequestError(requestId, reply->errorString());
reply->finished(); // To make sure it will be deleted
emit reply->finished(); // To make sure it will be deleted
});
QTimer::singleShot(200, reply, &QModbusReply::deleteLater);
} else {
delete reply; // broadcast replies return immediately
return "";
return QUuid();
}
} else {
qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString();
return "";
return QUuid();
}
return requestId;
}
@ -172,51 +214,70 @@ QUuid ModbusTCPMaster::readCoil(uint slaveAddress, uint registerAddress, uint si
QUuid ModbusTCPMaster::writeHoldingRegisters(uint slaveAddress, uint registerAddress, const QVector<quint16> &values)
{
if (!m_modbusTcpClient) {
return "";
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, values.length());
request.setValues(values);
if (QModbusReply *reply = m_modbusTcpClient->sendWriteRequest(request, slaveAddress)) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, requestId, this] {
if (reply->error() == QModbusDevice::NoError) {
writeRequestExecuted(requestId, true);
emit writeRequestExecuted(requestId, true);
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
emit receivedHoldingRegister(reply->serverAddress(), modbusAddress, unit.values());
} else {
writeRequestExecuted(requestId, false);
emit writeRequestExecuted(requestId, false);
qCWarning(dcModbusTCP()) << "Read response error:" << reply->error();
}
reply->deleteLater();
});
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTCP()) << "Modbus replay error:" << error;
emit writeRequestError(requestId, reply->errorString());
reply->finished(); // To make sure it will be deleted
emit reply->finished(); // To make sure it will be deleted
});
QTimer::singleShot(2000, reply, &QModbusReply::deleteLater);
} else {
delete reply; // broadcast replies return immediately
return "";
return QUuid();
}
} else {
qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString();
return "";
return QUuid();
}
return requestId;
}
QModbusReply *ModbusTCPMaster::sendRawRequest(const QModbusRequest &request, int serverAddress)
{
return m_modbusTcpClient->sendRawRequest(request, serverAddress);
}
QModbusReply *ModbusTCPMaster::sendReadRequest(const QModbusDataUnit &read, int serverAddress)
{
return m_modbusTcpClient->sendReadRequest(read, serverAddress);
}
QModbusReply *ModbusTCPMaster::sendReadWriteRequest(const QModbusDataUnit &read, const QModbusDataUnit &write, int serverAddress)
{
return m_modbusTcpClient->sendReadWriteRequest(read, write, serverAddress);
}
QModbusReply *ModbusTCPMaster::sendWriteRequest(const QModbusDataUnit &write, int serverAddress)
{
return m_modbusTcpClient->sendWriteRequest(write, serverAddress);
}
QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return "";
return QUuid();
}
QUuid requestId = QUuid::createUuid();
@ -226,33 +287,32 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
connect(reply, &QModbusReply::finished, this, [reply, requestId, this] {
if (reply->error() == QModbusDevice::NoError) {
emit readRequestExecuted(requestId, true);
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
emit receivedDiscreteInput(reply->serverAddress(), modbusAddress, unit.values());
} else {
emit readRequestExecuted(requestId, false);
qCWarning(dcModbusTCP()) << "Read response error:" << reply->error();
}
});
connect(reply, &QModbusReply::errorOccurred, this, [requestId, this] (QModbusDevice::Error error){
connect(reply, &QModbusReply::errorOccurred, this, [requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTCP()) << "Modbus replay error:" << error;
QModbusReply *reply = qobject_cast<QModbusReply *>(sender());
emit readRequestError(requestId, reply->errorString());
reply->finished(); // To make sure it will be deleted
emit reply->finished(); // To make sure it will be deleted
});
QTimer::singleShot(2000, reply, &QModbusReply::deleteLater);
} else {
delete reply; // broadcast replies return immediately
return "";
return QUuid();
}
} else {
qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString();
return "";
return QUuid();
}
return requestId;
}
@ -260,10 +320,10 @@ QUuid ModbusTCPMaster::readDiscreteInput(uint slaveAddress, uint registerAddress
QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return "";
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::InputRegisters, registerAddress, size);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, slaveAddress)) {
@ -276,26 +336,27 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress
const QModbusDataUnit unit = reply->result();
uint modbusAddress = unit.startAddress();
emit receivedInputRegister(reply->serverAddress(), modbusAddress, unit.values());
} else {
emit readRequestExecuted(requestId, false);
qCWarning(dcModbusTCP()) << "Read response error:" << reply->error();
}
});
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTCP()) << "Modbus reply error:" << error;
emit readRequestError(requestId, reply->errorString());
reply->finished(); // To make sure it will be deleted
emit reply->finished(); // To make sure it will be deleted
});
QTimer::singleShot(2000, reply, &QModbusReply::deleteLater);
} else {
delete reply; // broadcast replies return immediately
return "";
return QUuid();
}
} else {
qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString();
return "";
return QUuid();
}
return requestId;
}
@ -303,10 +364,10 @@ QUuid ModbusTCPMaster::readInputRegister(uint slaveAddress, uint registerAddress
QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddress, uint size)
{
if (!m_modbusTcpClient) {
return "";
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, registerAddress, size);
if (QModbusReply *reply = m_modbusTcpClient->sendReadRequest(request, slaveAddress)) {
@ -327,20 +388,22 @@ QUuid ModbusTCPMaster::readHoldingRegister(uint slaveAddress, uint registerAddre
}
reply->deleteLater();
});
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTCP()) << "Modbus reply error:" << error;
emit readRequestError(requestId, reply->errorString());
reply->finished(); // To make sure it will be deleted
emit reply->finished(); // To make sure it will be deleted
});
QTimer::singleShot(2000, reply, &QModbusReply::deleteLater);
} else {
delete reply; // broadcast replies return immediately
return "";
return QUuid();
}
} else {
qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString();
return "";
return QUuid();
}
return requestId;
}
@ -353,10 +416,10 @@ QUuid ModbusTCPMaster::writeCoil(uint slaveAddress, uint registerAddress, bool v
QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const QVector<quint16> &values)
{
if (!m_modbusTcpClient) {
return "";
return QUuid();
}
QUuid requestId = QUuid::createUuid();
QUuid requestId = QUuid::createUuid();
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, registerAddress, values.length());
request.setValues(values);
@ -377,20 +440,21 @@ QUuid ModbusTCPMaster::writeCoils(uint slaveAddress, uint registerAddress, const
}
reply->deleteLater();
});
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
connect(reply, &QModbusReply::errorOccurred, this, [reply, requestId, this] (QModbusDevice::Error error){
qCWarning(dcModbusTCP()) << "Modbus reply error:" << error;
emit writeRequestError(requestId, reply->errorString());
reply->finished(); // To make sure it will be deleted
emit reply->finished(); // To make sure it will be deleted
});
QTimer::singleShot(2000, reply, &QModbusReply::deleteLater);
} else {
delete reply; // broadcast replies return immediately
return "";
return QUuid();
}
} else {
qCWarning(dcModbusTCP()) << "Read error: " << m_modbusTcpClient->errorString();
return "";
return QUuid();
}
return requestId;
}
@ -400,19 +464,25 @@ QUuid ModbusTCPMaster::writeHoldingRegister(uint slaveAddress, uint registerAddr
return writeHoldingRegisters(slaveAddress, registerAddress, QVector<quint16>() << value);
}
void ModbusTCPMaster::onModbusErrorOccurred(QModbusDevice::Error error)
{
qCWarning(dcModbusTCP()) << "An error occured" << error;
}
void ModbusTCPMaster::onModbusStateChanged(QModbusDevice::State state)
{
qCDebug(dcModbusTCP()) << "Connection state changed for" << m_hostAddress << state;
bool connected = (state == QModbusDevice::ConnectedState);
if (!connected) {
//try to reconnect in 10 seconds
m_reconnectTimer->start(10000);
if (m_connected != connected) {
m_connected = connected;
emit connectionStateChanged(m_connected);
}
// If the socket is connected, stop the reconnect timer...
// If the socket is unconnected (not connecting and not closing), start the reconnect timer
if (m_connected) {
m_reconnectTimer->stop();
} else if (state == QModbusDevice::UnconnectedState) {
m_reconnectTimer->start();
}
emit connectionStateChanged(connected);
}

View File

@ -37,8 +37,6 @@
#include <QTimer>
#include <QUuid>
Q_DECLARE_LOGGING_CATEGORY(dcModbusTcp)
class ModbusTCPMaster : public QObject
{
Q_OBJECT
@ -46,10 +44,22 @@ public:
explicit ModbusTCPMaster(const QHostAddress &hostAddress, uint port, QObject *parent = nullptr);
~ModbusTCPMaster();
bool connectDevice();
bool connected();
// If you change the hostaddress, make sure to reconnectDevice afterwards
QHostAddress hostAddress() const;
void setHostAddress(const QHostAddress &hostAddress);
// If you change the port, make sure to reconnectDevice afterwards
uint port() const;
void setPort(uint port);
bool connected() const;
int numberOfRetries() const;
void setNumberOfRetries(int number);
int timeout() const;
void setTimeout(int timeout);
int timeout();
QString errorString() const;
QModbusDevice::Error error() const;
@ -65,18 +75,28 @@ public:
QUuid writeHoldingRegister(uint slaveAddress, uint registerAddress, quint16 value);
QUuid writeHoldingRegisters(uint slaveAddress, uint registerAddress, const QVector<quint16> &values);
QHostAddress hostAddress();
uint port();
bool setHostAddress(const QHostAddress &hostAddress);
bool setPort(uint port);
// Generic requests
QModbusReply *sendRawRequest(const QModbusRequest &request, int serverAddress);
QModbusReply *sendReadRequest(const QModbusDataUnit &read, int serverAddress);
QModbusReply *sendReadWriteRequest(const QModbusDataUnit &read, const QModbusDataUnit &write, int serverAddress);
QModbusReply *sendWriteRequest(const QModbusDataUnit &write, int serverAddress);
public slots:
bool connectDevice();
void disconnectDevice();
bool reconnectDevice();
private:
QTimer *m_reconnectTimer = nullptr;
QModbusTcpClient *m_modbusTcpClient;
QModbusTcpClient *m_modbusTcpClient = nullptr;
QHostAddress m_hostAddress;
uint m_port;
int m_timeout = 1000;
int m_numberOfRetries = 3;
bool m_connected = false;
private slots:
void onReconnectTimer();
void onModbusErrorOccurred(QModbusDevice::Error error);
void onModbusStateChanged(QModbusDevice::State state);
@ -85,9 +105,9 @@ signals:
void writeRequestExecuted(const QUuid &requestId, bool success);
void writeRequestError(const QUuid &requestId, const QString &error);
void readRequestError(const QUuid &requestId, const QString &error);
void readRequestExecuted(const QUuid &requestId, bool success);
void readRequestError(const QUuid &requestId, const QString &error);
void receivedCoil(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values);
void receivedDiscreteInput(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values);

View File

@ -0,0 +1,345 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "network/networkdevicediscovery.h"
#include "integrationpluginmtec.h"
#include "plugininfo.h"
IntegrationPluginMTec::IntegrationPluginMTec()
{
}
void IntegrationPluginMTec::discoverThings(ThingDiscoveryInfo *info)
{
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcMTec()) << "The network discovery is not available on this platform.";
info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available."));
return;
}
// Perform a network device discovery and filter for "go-eCharger" hosts
NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
qCDebug(dcMTec()) << "Found" << networkDeviceInfo;
QString title;
if (networkDeviceInfo.hostName().isEmpty()) {
title = networkDeviceInfo.address().toString();
} else {
title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")";
}
QString description;
if (networkDeviceInfo.macAddressManufacturer().isEmpty()) {
description = networkDeviceInfo.macAddress();
} else {
description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")";
}
ThingDescriptor descriptor(mtecThingClassId, title, description);
ParamList params;
params << Param(mtecThingIpAddressParamTypeId, networkDeviceInfo.address().toString());
params << Param(mtecThingMacAddressParamTypeId, networkDeviceInfo.macAddress());
descriptor.setParams(params);
// Check if we already have set up this device
Things existingThings = myThings().filterByParam(mtecThingMacAddressParamTypeId, networkDeviceInfo.macAddress());
if (existingThings.count() == 1) {
qCDebug(dcMTec()) << "This heat pump already exists in the system!" << networkDeviceInfo;
descriptor.setThingId(existingThings.first()->id());
}
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
}
void IntegrationPluginMTec::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCDebug(dcMTec()) << "Setup" << thing;
if (thing->thingClassId() == mtecThingClassId) {
QHostAddress hostAddress = QHostAddress(thing->paramValue(mtecThingIpAddressParamTypeId).toString());
if (hostAddress.isNull()) {
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("No IP address given"));
return;
}
qCDebug(dcMTec()) << "Using ip address" << hostAddress.toString();
MTec *mtec = new MTec(hostAddress, this);
connect(mtec, &MTec::connectedChanged, thing, [=](bool connected){
qCDebug(dcMTec()) << thing << "Connected changed to" << connected;
thing->setStateValue(mtecConnectedStateTypeId, connected);
});
connect(mtec, &MTec::roomTemperatureChanged, thing, [=](double roomTemperature){
qCDebug(dcMTec()) << thing << "Room temperature" << roomTemperature << "°C";
thing->setStateValue(mtecTemperatureStateTypeId, roomTemperature);
});
connect(mtec, &MTec::targetRoomTemperatureChanged, thing, [=](double targetRoomTemperature){
qCDebug(dcMTec()) << thing << "Target room temperature" << targetRoomTemperature << "°C";
thing->setStateValue(mtecTargetTemperatureStateTypeId, targetRoomTemperature);
});
connect(mtec, &MTec::waterTankTopTemperatureChanged, thing, [=](double waterTankTopTemperature){
qCDebug(dcMTec()) << thing << "Water tank top temperature" << waterTankTopTemperature << "°C";
thing->setStateValue(mtecWaterTankTopTemperatureStateTypeId, waterTankTopTemperature);
});
connect(mtec, &MTec::bufferTankTemperatureChanged, thing, [=](double bufferTankTemperature){
qCDebug(dcMTec()) << thing << "Buffer tank temperature" << bufferTankTemperature << "°C";
thing->setStateValue(mtecBufferTankTemperatureStateTypeId, bufferTankTemperature);
});
connect(mtec, &MTec::totalAccumulatedHeatingEnergyChanged, thing, [=](double totalAccumulatedHeatingEnergy){
qCDebug(dcMTec()) << thing << "Total accumulated heating energy" << totalAccumulatedHeatingEnergy << "kWh";
thing->setStateValue(mtecTotalAccumulatedHeatingEnergyStateTypeId, totalAccumulatedHeatingEnergy);
});
connect(mtec, &MTec::totalAccumulatedElectricalEnergyChanged, thing, [=](double totalAccumulatedElectricalEnergy){
qCDebug(dcMTec()) << thing << "Total accumulated electrical energy" << totalAccumulatedElectricalEnergy << "kWh";
thing->setStateValue(mtecTotalAccumulatedElectricalEnergyStateTypeId, totalAccumulatedElectricalEnergy);
});
connect(mtec, &MTec::heatPumpStateChanged, thing, [=](MTec::HeatpumpState heatPumpState){
qCDebug(dcMTec()) << thing << "Heat pump state" << heatPumpState;
switch (heatPumpState) {
case MTec::HeatpumpStateStandby:
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Standby");
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
case MTec::HeatpumpStatePreRun:
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Pre run");
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
case MTec::HeatpumpStateAutomaticHeat:
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Automatic heat");
thing->setStateValue(mtecHeatingOnStateTypeId, true);
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
case MTec::HeatpumpStateDefrost:
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Defrost");
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
case MTec::HeatpumpStateAutomaticCool:
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Automatic cool");
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecCoolingOnStateTypeId, true);
break;
case MTec::HeatpumpStatePostRun:
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Post run");
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
case MTec::HeatpumpStateSaftyShutdown:
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Safty shutdown");
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
case MTec::HeatpumpStateError:
thing->setStateValue(mtecHeatingOnStateTypeId, false);
thing->setStateValue(mtecHeatPumpStateStateTypeId, "Error");
thing->setStateValue(mtecCoolingOnStateTypeId, false);
break;
}
});
connect(mtec, &MTec::heatMeterPowerConsumptionChanged, thing, [=](double heatMeterPowerConsumption){
qCDebug(dcMTec()) << thing << "Heat meter power consumption" << heatMeterPowerConsumption << "W";
thing->setStateValue(mtecHeatMeterPowerConsumptionStateTypeId, heatMeterPowerConsumption);
});
connect(mtec, &MTec::energyMeterPowerConsumptionChanged, thing, [=](double energyMeterPowerConsumption){
qCDebug(dcMTec()) << thing << "Energy meter power consumption" << energyMeterPowerConsumption << "W";
thing->setStateValue(mtecEnergyMeterPowerConsumptionStateTypeId, energyMeterPowerConsumption);
});
connect(mtec, &MTec::actualExcessEnergySmartHomeChanged, thing, [=](double actualExcessEnergySmartHome){
qCDebug(dcMTec()) << thing << "Smart home energy" << actualExcessEnergySmartHome << "W";
thing->setStateValue(mtecSmartHomeEnergyStateTypeId, actualExcessEnergySmartHome);
});
connect(mtec, &MTec::actualExcessEnergySmartHomeElectricityMeterChanged, thing, [=](double actualExcessEnergySmartHomeElectricityMeter){
qCDebug(dcMTec()) << thing << "Smart home energy electrical meter" << actualExcessEnergySmartHomeElectricityMeter << "W";
thing->setStateValue(mtecSmartHomeEnergyElectricityMeterStateTypeId, actualExcessEnergySmartHomeElectricityMeter);
});
connect(mtec, &MTec::actualOutdoorTemperatureChanged, thing, [=](double actualOutdoorTemperature){
qCDebug(dcMTec()) << thing << "Outdoor temperature" << actualOutdoorTemperature << "°C";
thing->setStateValue(mtecOutdoorTemperatureStateTypeId, actualOutdoorTemperature);
});
m_mtecConnections.insert(thing, mtec);
// TODO: start timer and give 15 seconds until connected, since the controler is down for ~10 seconds after a disconnect
if (!mtec->connectDevice()) {
qCWarning(dcMTec()) << "Initial connect returned false. Lets wait 15 seconds until the connection can be established.";
}
info->finish(Thing::ThingErrorNoError);
}
}
void IntegrationPluginMTec::postSetupThing(Thing *thing)
{
if (thing->thingClassId() == mtecThingClassId) {
MTec *mtec = m_mtecConnections.value(thing);
if (mtec) {
update(thing);
}
if (!m_pluginTimer) {
qCDebug(dcMTec()) << "Starting plugin timer...";
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this] {
foreach (Thing *thing, myThings().filterByThingClassId(mtecThingClassId)) {
update(thing);
}
});
}
}
}
void IntegrationPluginMTec::thingRemoved(Thing *thing)
{
if (m_mtecConnections.contains(thing)) {
MTec *mtec = m_mtecConnections.take(thing);
if (mtec) {
mtec->disconnectDevice();
mtec->deleteLater();
}
}
if (myThings().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_pluginTimer = nullptr;
}
}
void IntegrationPluginMTec::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
MTec *mtec = m_mtecConnections.value(thing);
if (!mtec) {
qCWarning(dcMTec()) << "Could not execute action because the MTec connection could not be found for" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
// Make sure we are connected
if (!mtec->connected()) {
qCWarning(dcMTec()) << "Could not execute action because the MTec connection is not connected" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
if (action.actionTypeId() == mtecTargetTemperatureActionTypeId) {
double targetTemperature = action.paramValue(mtecTargetTemperatureActionTargetTemperatureParamTypeId).toDouble();
qCDebug(dcMTec()) << "Setting target temperature" << targetTemperature << "°C";
QModbusReply *reply = mtec->setTargetRoomTemperature(targetTemperature);
if (!reply) {
qCWarning(dcMTec()) << "Failed to send modbus request" << thing;
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
connect(reply, &QModbusReply::finished, this, [=]() {
reply->deleteLater();
if (reply->error() == QModbusDevice::NoError) {
qCDebug(dcMTec()) << "Setting target temperature" << targetTemperature << "°C" << "finished successfully";
thing->setStateValue(mtecTargetTemperatureStateTypeId, targetTemperature);
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareFailure);
}
});
connect(reply, &QModbusReply::errorOccurred, this, [=](QModbusDevice::Error error) {
qCWarning(dcMTec()) << thing << "Action execution finished due to modbus replay error:" << error;
reply->deleteLater();
info->finish(Thing::ThingErrorHardwareFailure);
});
} else if (action.actionTypeId() == mtecSmartHomeEnergyActionTypeId) {
quint16 energy = action.paramValue(mtecSmartHomeEnergyActionSmartHomeEnergyParamTypeId).toUInt();
qCDebug(dcMTec()) << "Setting smart home energy to" << energy << "W";
QModbusReply *reply = mtec->setSmartHomeEnergy(energy);
if (!reply) {
qCWarning(dcMTec()) << "Failed to send modbus request" << thing;
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
connect(reply, &QModbusReply::finished, this, [=]() {
reply->deleteLater();
if (reply->error() == QModbusDevice::NoError) {
qCDebug(dcMTec()) << "Setting smart home energy" << energy << "W" << "finished successfully";
thing->setStateValue(mtecSmartHomeEnergyStateTypeId, energy);
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareFailure);
}
});
connect(reply, &QModbusReply::errorOccurred, this, [=](QModbusDevice::Error error) {
qCWarning(dcMTec()) << thing << "Action execution finished due to modbus replay error:" << error;
reply->deleteLater();
info->finish(Thing::ThingErrorHardwareFailure);
});
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8());
}
}
void IntegrationPluginMTec::update(Thing *thing)
{
if (thing->thingClassId() == mtecThingClassId) {
qCDebug(dcMTec()) << "Updating thing" << thing;
MTec *mtec = m_mtecConnections.value(thing);
if (!mtec) return;
mtec->updateValues();
}
}

View File

@ -0,0 +1,68 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* 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 <QUuid>
class IntegrationPluginMTec: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginmtec.json")
Q_INTERFACES(IntegrationPlugin)
public:
/** Constructor */
explicit IntegrationPluginMTec();
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void thingRemoved(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
private:
PluginTimer *m_pluginTimer = nullptr;
QHash<Thing *, MTec *> m_mtecConnections;
private slots:
void update(Thing *thing);
};
#endif // INTEGRATIONPLUGINMTEC_H

View File

@ -0,0 +1,206 @@
{
"name": "MTec",
"displayName": "M-Tec",
"id": "07cd316b-1e2c-40cf-8358-88d7407506ae",
"vendors": [
{
"name": "MTec",
"displayName": "M-Tec",
"id": "04d3fa7c-e469-4a79-a119-155426e5a846",
"thingClasses": [
{
"name": "mtec",
"displayName": "MTec",
"id": "451e38d8-50d5-4ae9-8d9f-21af9347128d",
"createMethods": ["discovery", "user"],
"interfaces": ["thermostat", "connectable"],
"paramTypes": [
{
"id": "f1c43b1e-cffe-4d30-bda0-c23ed648dd71",
"name": "ipAddress",
"displayName": "IP address",
"type": "QString",
"inputType": "IPv4Address",
"defaultValue": "127.0.0.1"
},
{
"id": "906f6099-d0e1-4297-a2b3-f8ec4482c578",
"name":"macAddress",
"displayName": "MAC address",
"type": "QString",
"inputType": "MacAddress",
"defaultValue": ""
}
],
"stateTypes": [
{
"id": "8d64954a-855d-44ea-8bc9-88a71ab47b6b",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "1e2037c8-09dc-4396-974c-efa9c486aa65",
"name": "heatPumpState",
"displayName": "Heat pump state",
"displayNameEvent": "Heat pump state changed",
"type": "QString",
"possibleValues": [
"Standby",
"Pre run",
"Automatic heat",
"Defrost",
"Automatic cool",
"Post run",
"Safty shutdown",
"Error"
],
"defaultValue": "Standby",
"suggestLogging": true
},
{
"id": "9b538cb9-f7a3-471e-8d3b-09f6370a571c",
"name": "targetTemperature",
"displayName": "Target room temperature (heat circuit 0)",
"displayNameEvent": "Target room temperature changed (heat circuit 0=",
"displayNameAction": "Set target room temperature (heat circuit 0)",
"unit": "DegreeCelsius",
"type": "double",
"writable": true,
"minValue": 10,
"maxValue": 30,
"defaultValue": 20,
"suggestLogging": true
},
{
"id": "b22ac9bb-3842-497c-bd93-f8bea6670e32",
"name": "temperature",
"displayName": "Room temperature heat circuit 0",
"displayNameEvent": "Room temperature heat circuit 0 changed",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 20,
"suggestLogging": true
},
{
"id": "07465fbb-6949-4bd1-90d5-acf2d80c161d",
"name": "heatingOn",
"displayName": "Heating on",
"displayNameEvent": "Heating turned on/off",
"type": "bool",
"defaultValue": false,
"suggestLogging": true
},
{
"id": "8b407c1d-b84f-48d4-9961-b29bc58fff0e",
"name": "coolingOn",
"displayName": "Cooling on",
"displayNameEvent": "Cooling turned on/off",
"type": "bool",
"defaultValue": false,
"suggestLogging": true
},
{
"id": "d0c8f168-49b5-47ca-9988-c9922be38dd5",
"name": "outdoorTemperature",
"displayName": "Outdoor temperature",
"displayNameEvent": "Outdoor temperature changed",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "545f94d6-f4fd-48fe-bf3b-f193e5cb76e7",
"name": "waterTankTopTemperature",
"displayName": "Water tank top temperature",
"displayNameEvent": "Water tank top temperature changed",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "a98e37f8-dcdc-4c4c-aecf-07f376321849",
"name": "bufferTankTemperature",
"displayName": "Buffer tank temperature",
"displayNameEvent": "Buffer tank temperature changed",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "7d087af8-cdbe-463e-a9bb-7a7a79471963",
"name": "totalAccumulatedHeatingEnergy",
"displayName": "Total accumulated heating energy",
"displayNameEvent": "Total accumulated heating energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "c67c79cf-7369-409f-b170-16c4ece9d25a",
"name": "totalAccumulatedElectricalEnergy",
"displayName": "Total accumulated electrical energy",
"displayNameEvent": "Total accumulated electrical energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "581abddc-90d6-4dea-a43c-63b117b335fe",
"name": "heatMeterPowerConsumption",
"displayName": "Heat meter power consumption",
"displayNameEvent": "Heat meter power consumption changed",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "fd52a97e-f94d-4529-b479-b74e61f75a89",
"name": "energyMeterPowerConsumption",
"displayName": "Energy meter power consumption",
"displayNameEvent": "Energy meter power consumption changed",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"suggestLogging": true
},
{
"id": "b646ea10-ea7e-4eba-bfda-8e3cd38370a7",
"name": "smartHomeEnergy",
"displayName": "Smart home energy",
"displayNameEvent": "Smart home energy changed",
"displayNameAction": "Set smart home energy",
"type": "uint",
"unit": "Watt",
"minValue": 0,
"maxValue": 20000,
"defaultValue": 0,
"writable": true,
"suggestLogging": true
},
{
"id": "a7734474-30db-435c-985a-105fb3ea5a86",
"name": "smartHomeEnergyElectricityMeter",
"displayName": "Smart home energy consumed",
"displayNameEvent": "Smart home energy consumed changed",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"suggestLogging": true
}
],
"actionTypes": [ ]
}
]
}
]
}

202
mtec/mtec.cpp Normal file
View File

@ -0,0 +1,202 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "mtec.h"
#include "extern-plugininfo.h"
MTec::MTec(const QHostAddress &address, QObject *parent) :
QObject(parent),
m_hostAddress(address)
{
m_modbusMaster = new ModbusTCPMaster(address, 502, this);
m_modbusMaster->setTimeout(2000);
m_modbusMaster->setNumberOfRetries(5);
qCDebug(dcMTec()) << "Created ModbusTCPMaster for" << address.toString();
connect(m_modbusMaster, &ModbusTCPMaster::connectionStateChanged, this, &MTec::connectedChanged);
connect(m_modbusMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &MTec::onReceivedHoldingRegister);
connect(m_modbusMaster, &ModbusTCPMaster::readRequestError, this, &MTec::onModbusError);
connect(m_modbusMaster, &ModbusTCPMaster::writeRequestError, this, &MTec::onModbusError);
}
MTec::~MTec()
{
m_modbusMaster->disconnectDevice();
}
QHostAddress MTec::hostAddress() const
{
return m_hostAddress;
}
bool MTec::connected() const
{
return m_modbusMaster->connected();
}
QModbusReply *MTec::setTargetRoomTemperature(double targetRoomTemperature)
{
QVector<quint16> values;
values << static_cast<quint16>(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<quint16> values;
values << smartHomeEnergy;
QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, RegisterActualExcessEnergySmartHome, values.length());
request.setValues(values);
QModbusReply *reply = m_modbusMaster->sendWriteRequest(request, MTec::ModbusUnitID);
return reply;
}
bool MTec::connectDevice()
{
return m_modbusMaster->connectDevice();
}
void MTec::disconnectDevice()
{
m_modbusMaster->disconnectDevice();
}
void MTec::updateValues()
{
if (!m_modbusMaster->connected()) {
return;
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterRoomTemperature, 1);
}
void MTec::onModbusError()
{
qCWarning(dcMTec()) << "Modbus error occured" << m_modbusMaster->errorString();
}
void MTec::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector<quint16> &value)
{
Q_UNUSED(slaveAddress);
switch (modbusRegister) {
case RegisterRoomTemperature:
if (value.length() == 1) {
m_roomTemperature = value[0] / 10.0;
emit roomTemperatureChanged(m_roomTemperature);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTargetRoomTemperature, 1);
break;
case RegisterTargetRoomTemperature:
if (value.length() == 1) {
m_targetRoomTemperature = value[0] / 10.0;
emit targetRoomTemperatureChanged(m_targetRoomTemperature);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHotWaterTankTemperature, 1);
break;
case RegisterHotWaterTankTemperature:
if (value.length() == 1) {
m_waterTankTopTemperature = value[0] / 10.0;
emit waterTankTopTemperatureChanged(m_waterTankTopTemperature);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterBufferTankTemperature, 1);
break;
case RegisterBufferTankTemperature:
if (value.length() == 1) {
m_bufferTankTemperature = value[0] / 10.0;
emit bufferTankTemperatureChanged(m_bufferTankTemperature);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTotalAccumulatedHeatingEnergy, 1);
break;
case RegisterTotalAccumulatedHeatingEnergy:
if (value.length() == 1) {
m_totalAccumulatedElectricalEnergy = value[0];
emit totalAccumulatedElectricalEnergyChanged(m_totalAccumulatedElectricalEnergy);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterTotalAccumulatedElectricalEnergy, 1);
break;
case RegisterTotalAccumulatedElectricalEnergy:
if (value.length() == 1) {
m_totalAccumulatedElectricalEnergy = value[0];
emit totalAccumulatedElectricalEnergyChanged(m_totalAccumulatedElectricalEnergy);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatpumpState, 1);
break;
case RegisterHeatpumpState:
if (value.length() == 1) {
m_heatPumpState = static_cast<HeatpumpState>(value[0]);
emit heatPumpStateChanged(m_heatPumpState);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterHeatMeterPowerConsumption, 1);
break;
case RegisterHeatMeterPowerConsumption:
if (value.length() == 1) {
m_heatMeterPowerConsumption = value[0];
emit heatMeterPowerConsumptionChanged(m_heatMeterPowerConsumption);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterEnergyMeterPowerConsumption, 1);
break;
case RegisterEnergyMeterPowerConsumption:
if (value.length() == 1) {
m_energyMeterPowerConsumption = value[0];
emit energyMeterPowerConsumptionChanged(m_energyMeterPowerConsumption);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualExcessEnergySmartHome, 1);
break;
case RegisterActualExcessEnergySmartHome:
if (value.length() == 1) {
m_actualExcessEnergySmartHome = value[0];
emit actualExcessEnergySmartHomeChanged(m_actualExcessEnergySmartHome);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualExcessEnergySmartHomeElectricityMeter, 1);
break;
case RegisterActualExcessEnergySmartHomeElectricityMeter:
if (value.length() == 1) {
m_actualExcessEnergySmartHomeElectricityMeter = value[0];
emit actualExcessEnergySmartHomeChanged(m_actualExcessEnergySmartHome);
}
m_modbusMaster->readHoldingRegister(MTec::ModbusUnitID, RegisterActualOutdoorTemperature, 1);
break;
case RegisterActualOutdoorTemperature:
if (value.length() == 1) {
m_actualOutdoorTemperature = value[0] / 10.0;
emit actualOutdoorTemperatureChanged(m_actualOutdoorTemperature);
}
// TODO: set initialized
break;
}
}

164
mtec/mtec.h Normal file
View File

@ -0,0 +1,164 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* 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 <QDateTime>
#include <QObject>
#include "../modbus/modbustcpmaster.h"
class MTec : public QObject
{
Q_OBJECT
public:
enum HeatpumpState {
HeatpumpStateStandby = 0,
HeatpumpStatePreRun = 1,
HeatpumpStateAutomaticHeat = 2,
HeatpumpStateDefrost = 3,
HeatpumpStateAutomaticCool = 4,
HeatpumpStatePostRun = 5,
HeatpumpStateSaftyShutdown = 7,
HeatpumpStateError = 8
};
Q_ENUM(HeatpumpState)
explicit MTec(const QHostAddress &address, QObject *parent = nullptr);
~MTec();
QHostAddress hostAddress() const;
bool connected() const;
QModbusReply *setTargetRoomTemperature(double targetRoomTemperature);
QModbusReply *setSmartHomeEnergy(quint16 smartHomeEnergy);
public slots:
bool connectDevice();
void disconnectDevice();
void updateValues();
private:
/** Modbus Unit ID (undocumented, guessing 1 for now) */
static const quint16 ModbusUnitID = 1;
/** The following modbus addresses can be read: */
enum Register {
/* R APPL.CtrlAppl.sParam.heatCircuit[0].tempRoom.values.actValue
* Actual room temperature [1/10°C]. */
RegisterRoomTemperature = 1,
/* RW APPL.CtrlAppl.sParam.heatCircuit[0].param.normalSetTemp
* Room set temperature for heating circuit [1/10°C]. */
RegisterTargetRoomTemperature = 4,
/* R APPL.CtrlAppl.sParam.hotWaterTank[0].topTemp.values.actValue
* Hot water tank top temperature [1/10°C]. */
RegisterHotWaterTankTemperature = 401,
/* R APPL.CtrlAppl.sParam.bufferTank[0].topTemp.values.actValue
* Buffer Actual top temperature [1/10°C]. */
RegisterBufferTankTemperature = 601,
/* R APPL.CtrlAppl.sStatisticalData.heatpump[0].consumption.energy
* Total accumulated heating energy [kWh] */
RegisterTotalAccumulatedHeatingEnergy = 701,
/* R APPL.CtrlAppl.sStatisticalData.heatpump[0].consumption.electricalenergy
* Total accumulated electrical energy [kWh] */
RegisterTotalAccumulatedElectricalEnergy = 702,
/* R APPL.CtrlAppl.sParam.heatpump[0].values.heatpumpState */
RegisterHeatpumpState = 703,
/* R APPL.CtrlAppl.sParam.heatpump[0].HeatMeter.values.power
* Actual power consumtion [W] */
RegisterHeatMeterPowerConsumption = 706,
/* R APPL.CtrlAppl.sParam.heatpump[0].ElectricEnergyMeter.values.power
* Actual power consumtion [W] */
RegisterEnergyMeterPowerConsumption = 707,
/* RW APPL.CtrlAppl.sIOModule.Virt[0].param.sensor[0]
* Acutal excess energy given from Smart home System [W] */
RegisterActualExcessEnergySmartHome = 1000,
/* R APPL.CtrlAppl.sParam.photovoltaics.ElectricEnergyMeter.values.power
* Acutal excess energy given from Electricity Meter [W] */
RegisterActualExcessEnergySmartHomeElectricityMeter = 1002,
/* R APPL.CtrlAppl.sParam.outdoorTemp.values.actValue
* Actual exterior temperature [°C] */
RegisterActualOutdoorTemperature = 1502,
};
QHostAddress m_hostAddress;
ModbusTCPMaster *m_modbusMaster = nullptr;
double m_roomTemperature = 0;
double m_targetRoomTemperature = 0;
double m_waterTankTopTemperature = 0;
double m_bufferTankTemperature = 0;
double m_totalAccumulatedHeatingEnergy = 0;
double m_totalAccumulatedElectricalEnergy = 0;
HeatpumpState m_heatPumpState = HeatpumpStateStandby;
double m_heatMeterPowerConsumption = 0;
double m_energyMeterPowerConsumption = 0;
double m_actualExcessEnergySmartHome = 0;
double m_actualExcessEnergySmartHomeElectricityMeter = 0;
double m_actualOutdoorTemperature = 0;
signals:
void connectedChanged(bool connected);
void roomTemperatureChanged(double roomTemperature);
void targetRoomTemperatureChanged(double targetRoomTemperature);
void waterTankTopTemperatureChanged(double waterTankTopTemperature);
void bufferTankTemperatureChanged(double bufferTankTemperature);
void totalAccumulatedHeatingEnergyChanged(double totalAccumulatedHeatingEnergy);
void totalAccumulatedElectricalEnergyChanged(double totalAccumulatedElectricalEnergy);
void heatPumpStateChanged(HeatpumpState heatPumpState);
void heatMeterPowerConsumptionChanged(double heatMeterPowerConsumption);
void energyMeterPowerConsumptionChanged(double energyMeterPowerConsumption);
void actualExcessEnergySmartHomeChanged(double actualExcessEnergySmartHome);
void actualExcessEnergySmartHomeElectricityMeterChanged(double actualExcessEnergySmartHomeElectricityMeter);
void actualOutdoorTemperatureChanged(double actualOutdoorTemperature);
private slots:
void onModbusError();
void onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector<quint16> &value);
};
#endif // MTEC_H

16
mtec/mtec.pro Normal file
View File

@ -0,0 +1,16 @@
include(../plugins.pri)
QT += \
network \
serialbus \
SOURCES += \
mtec.cpp \
integrationpluginmtec.cpp \
../modbus/modbustcpmaster.cpp
HEADERS += \
mtec.h \
integrationpluginmtec.h \
../modbus/modbustcpmaster.h \

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginMTec</name>
<message>
<location filename="../integrationpluginmtec.cpp" line="64"/>
<source>No IP address given</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginmtec.cpp" line="85"/>
<source>IP address already in use by another thing.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MTec</name>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="50"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="53"/>
<source>Actual excess energy from Electricity Meter</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="56"/>
<source>Actual excess energy from Electricity Meter changed</source>
<extracomment>The name of the EventType ({fd94d39c-0db6-497f-a0a5-6c5452cbcaaf}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="59"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="62"/>
<source>Actual excess energy from Smart home System</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="65"/>
<source>Actual excess energy from Smart home System changed</source>
<extracomment>The name of the EventType ({663718fa-807e-4d85-bd78-61a65f8c0b5e}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="68"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="71"/>
<source>Actual power consumption</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="74"/>
<source>Actual power consumption changed</source>
<extracomment>The name of the EventType ({c67c79cf-7369-409f-b170-16c4ece9d25a}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="77"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="80"/>
<source>Connected</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="83"/>
<source>Connected changed</source>
<extracomment>The name of the EventType ({8d64954a-855d-44ea-8bc9-88a71ab47b6b}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="86"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="89"/>
<source>Control of the heat source by an external control [100%]</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="92"/>
<source>Control of the heat source by an external control [100%] changed</source>
<extracomment>The name of the EventType ({087c0296-705b-483a-b1e9-7ce08202c035}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="95"/>
<source>IP address</source>
<extracomment>The name of the ParamType (ThingClass: mtec, Type: thing, ID: {f1c43b1e-cffe-4d30-bda0-c23ed648dd71})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="98"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="101"/>
<source>M-Tec</source>
<extracomment>The name of the vendor ({04d3fa7c-e469-4a79-a119-155426e5a846})
----------
The name of the plugin MTec ({07cd316b-1e2c-40cf-8358-88d7407506ae})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="104"/>
<source>MTec</source>
<extracomment>The name of the ThingClass ({451e38d8-50d5-4ae9-8d9f-21af9347128d})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="107"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="110"/>
<source>Request external heat source</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="113"/>
<source>Request external heat source changed</source>
<extracomment>The name of the EventType ({90b17788-ce63-47e3-b97d-1b025a41ce35}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="116"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="119"/>
<source>Status</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/mtec/plugininfo.h" line="122"/>
<source>Status changed</source>
<extracomment>The name of the EventType ({9bf5f8d6-116a-4399-a728-51470a3a5620}) of ThingClass mtec</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -4,6 +4,7 @@ PLUGIN_DIRS = \
drexelundweiss \
energymeters \
modbuscommander \
mtec \
mypv \
sunspec \
unipi \