diff --git a/debian/control b/debian/control index ab10b9a..05f8045 100644 --- a/debian/control +++ b/debian/control @@ -246,6 +246,15 @@ Description: nymea integration plugin for UniPi devices This package contains the nymea integration plugin for UniPi devices. +Package: nymea-plugin-pcelectric +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for PCE wallboxes + This package contains the nymea integration plugin for wallboxes made by PC Electric. + + Package: nymea-plugin-phoenixconnect Architecture: any Section: libs diff --git a/debian/nymea-plugin-pcelectric.install.in b/debian/nymea-plugin-pcelectric.install.in new file mode 100644 index 0000000..2645b3f --- /dev/null +++ b/debian/nymea-plugin-pcelectric.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginpcelectric.so +pcelectric/translations/*qm usr/share/nymea/translations/ diff --git a/libnymea-modbus/libnymea-modbus.pro b/libnymea-modbus/libnymea-modbus.pro index 61d8b3b..a1b787c 100644 --- a/libnymea-modbus/libnymea-modbus.pro +++ b/libnymea-modbus/libnymea-modbus.pro @@ -17,11 +17,13 @@ gcc { HEADERS += \ modbusdatautils.h \ - modbustcpmaster.h + modbustcpmaster.h \ + queuedmodbusreply.h SOURCES += \ modbusdatautils.cpp \ - modbustcpmaster.cpp + modbustcpmaster.cpp \ + queuedmodbusreply.cpp # define install target diff --git a/libnymea-modbus/modbusdatautils.cpp b/libnymea-modbus/modbusdatautils.cpp index ddfbb93..236f1b1 100644 --- a/libnymea-modbus/modbusdatautils.cpp +++ b/libnymea-modbus/modbusdatautils.cpp @@ -322,3 +322,27 @@ QString ModbusDataUtils::exceptionCodeToString(QModbusPdu::ExceptionCode excepti return exceptionString; } + +QString ModbusDataUtils::registerTypeToString(QModbusDataUnit::RegisterType registerType) +{ + QString registerTypeString; + switch (registerType) { + case QModbusDataUnit::DiscreteInputs: + registerTypeString = "DiscreteInputs"; + break; + case QModbusDataUnit::Coils: + registerTypeString = "Coils"; + break; + case QModbusDataUnit::InputRegisters: + registerTypeString = "InputRegisters"; + break; + case QModbusDataUnit::HoldingRegisters: + registerTypeString = "HoldingRegisters"; + break; + default: + registerTypeString = "Invalid"; + break; + } + + return registerTypeString; +} diff --git a/libnymea-modbus/modbusdatautils.h b/libnymea-modbus/modbusdatautils.h index 7bd4e00..616539c 100644 --- a/libnymea-modbus/modbusdatautils.h +++ b/libnymea-modbus/modbusdatautils.h @@ -34,6 +34,7 @@ #include #include #include +#include class ModbusDataUtils { @@ -106,6 +107,7 @@ public: static QVector convertFromFloat64(double value, ByteOrder byteOrder = ByteOrderLittleEndian); static QString exceptionCodeToString(QModbusPdu::ExceptionCode exception); + static QString registerTypeToString(QModbusDataUnit::RegisterType registerType); }; #endif // MODBUSDATAUTILS_H diff --git a/libnymea-modbus/queuedmodbusreply.cpp b/libnymea-modbus/queuedmodbusreply.cpp new file mode 100644 index 0000000..bb3b44e --- /dev/null +++ b/libnymea-modbus/queuedmodbusreply.cpp @@ -0,0 +1,107 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 "queuedmodbusreply.h" + +QueuedModbusReply::QueuedModbusReply(QObject *parent) + : QObject{parent} +{ + +} + +QueuedModbusReply::QueuedModbusReply(RequestType requestType, QModbusDataUnit dataUnit, QObject *parent) + : QObject{parent}, + m_requestType{requestType}, + m_dataUnit{dataUnit} +{ + +} + +QueuedModbusReply::~QueuedModbusReply() +{ + if (m_reply) + m_reply->deleteLater(); +} + +QueuedModbusReply::RequestType QueuedModbusReply::requestType() const +{ + return m_requestType; +} + +void QueuedModbusReply::setRequestType(RequestType requestType) +{ + Q_ASSERT_X(m_reply == nullptr, "QueuedModbusReply", "Changing the request type after the modbus reply has already been sent has no effect."); + m_requestType = requestType; +} + +QModbusDataUnit QueuedModbusReply::dataUnit() const +{ + return m_dataUnit; +} + +void QueuedModbusReply::setQMOdbusDataUnit(const QModbusDataUnit &dataUnit) +{ + Q_ASSERT_X(m_reply == nullptr, "QueuedModbusReply", "Changing the data unit after the modbus reply has already been sent has no effect."); + m_dataUnit = dataUnit; +} + +QModbusReply *QueuedModbusReply::reply() const +{ + return m_reply; +} + +void QueuedModbusReply::setReply(QModbusReply *reply) +{ + m_reply = reply; + if (!m_reply) + return; + + // Note: do not change the parent of the QModbusReply, + // otherwise the modbus device looses track about it and stops working + + connect(m_reply, &QModbusReply::finished, this, &QueuedModbusReply::finished); + connect(m_reply, &QModbusReply::errorOccurred, this, &QueuedModbusReply::errorOccurred); +} + +QModbusDevice::Error QueuedModbusReply::error() const +{ + if (!m_reply) + return QModbusDevice::UnknownError; + + return m_reply->error(); +} + +QString QueuedModbusReply::errorString() const +{ + if (!m_reply) + return QString(); + + return m_reply->errorString(); +} diff --git a/libnymea-modbus/queuedmodbusreply.h b/libnymea-modbus/queuedmodbusreply.h new file mode 100644 index 0000000..aff508a --- /dev/null +++ b/libnymea-modbus/queuedmodbusreply.h @@ -0,0 +1,77 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 QUEUEDMODBUSREPLY_H +#define QUEUEDMODBUSREPLY_H + +#include +#include +#include + +class QueuedModbusReply : public QObject +{ + Q_OBJECT +public: + enum RequestType { + RequestTypeRead, + RequestTypeWrite + }; + Q_ENUM(RequestType) + + explicit QueuedModbusReply(QObject *parent = nullptr); + explicit QueuedModbusReply(RequestType requestType, QModbusDataUnit dataUnit, QObject *parent = nullptr); + + ~QueuedModbusReply(); + + RequestType requestType() const; + void setRequestType(RequestType requestType); + + QModbusDataUnit dataUnit() const; + void setQMOdbusDataUnit(const QModbusDataUnit &dataUnit); + + // Available once the request will be sent to the modbus slave + QModbusReply *reply() const; + void setReply(QModbusReply *reply); + + QModbusDevice::Error error() const; + QString errorString() const; + +signals: + void finished(); + void errorOccurred(QModbusDevice::Error error); + +private: + RequestType m_requestType = RequestTypeRead; + QModbusDataUnit m_dataUnit; + QModbusReply *m_reply = nullptr; + +}; + +#endif // QUEUEDMODBUSREPLY_H diff --git a/libnymea-modbus/tools/connectiontool/modbusrtu.py b/libnymea-modbus/tools/connectiontool/modbusrtu.py index bf5f06c..0ba5fb2 100644 --- a/libnymea-modbus/tools/connectiontool/modbusrtu.py +++ b/libnymea-modbus/tools/connectiontool/modbusrtu.py @@ -99,12 +99,7 @@ def writePropertyUpdateMethodImplementationsRtu(fileDescriptor, className, regis writeLine(fileDescriptor, ' handleModbusError(reply->error());') writeLine(fileDescriptor, ' if (reply->error() == ModbusRtuReply::NoError) {') writeLine(fileDescriptor, ' QVector values = reply->result();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (values.size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(values);' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(values);' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' }') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -384,12 +379,7 @@ def writeInitMethodImplementationRtu(fileDescriptor, className, registerDefiniti writeLine(fileDescriptor, ' }') writeLine(fileDescriptor) writeLine(fileDescriptor, ' QVector values = reply->result();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" init register" << %s << "size:" << %s << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (values.size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(values);' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(values);' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' verifyInitFinished();') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -541,12 +531,7 @@ def writeUpdateMethodRtu(fileDescriptor, className, registerDefinitions, blockDe writeLine(fileDescriptor, ' }') writeLine(fileDescriptor) writeLine(fileDescriptor, ' QVector values = reply->result();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (values.size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(values);' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(values);' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' verifyUpdateFinished();') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) diff --git a/libnymea-modbus/tools/connectiontool/modbustcp.py b/libnymea-modbus/tools/connectiontool/modbustcp.py index 4ea9afe..1245ec5 100644 --- a/libnymea-modbus/tools/connectiontool/modbustcp.py +++ b/libnymea-modbus/tools/connectiontool/modbustcp.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 - 2023 nymea GmbH +# Copyright (C) 2021 - 2024 nymea GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,6 +16,74 @@ from .toolcommon import * +############################################################## + +def writePropertyGetSetDataUnitDeclarationsTcp(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + if 'unit' in registerDefinition and registerDefinition['unit'] != '': + writeLine(fileDescriptor, ' /* %s [%s] - Address: %s, Size: %s */' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) + else: + writeLine(fileDescriptor, ' /* %s - Address: %s, Size: %s */' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + + # Check if we require a read method + if 'R' in registerDefinition['access']: + writeLine(fileDescriptor, ' QModbusDataUnit %sDataUnit() const;' % (propertyName)) + + # Check if we require a write method + if 'W' in registerDefinition['access']: + writeLine(fileDescriptor, ' QModbusDataUnit set%sDataUnit(%s %s);' % (propertyName[0].upper() + propertyName[1:], propertyTyp, propertyName)) + + writeLine(fileDescriptor) + + +def writePropertyGetSetDataUnitImplementationsTcp(fileDescriptor, className, registerDefinitions): + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) + + # Check if we require a read method + if 'R' in registerDefinition['access']: + + writeLine(fileDescriptor, 'QModbusDataUnit %s::%sDataUnit() const' % (className, propertyName)) + writeLine(fileDescriptor, '{') + + # Build request depending on the register type + if registerDefinition['registerType'] == 'inputRegister': + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::InputRegisters, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) + elif registerDefinition['registerType'] == 'discreteInputs': + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::DiscreteInputs, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) + elif registerDefinition['registerType'] == 'coils': + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) + else: + #Default to holdingRegister + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) + + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + # Check if we require a write method + if 'W' in registerDefinition['access']: + writeLine(fileDescriptor, 'QModbusDataUnit %s::set%sDataUnit(%s %s)' % (className, propertyName[0].upper() + propertyName[1:], propertyTyp, propertyName)) + writeLine(fileDescriptor, '{') + + writeLine(fileDescriptor, ' QVector values = %s;' % getConversionToValueMethod(registerDefinition)) + if registerDefinition['registerType'] == 'holdingRegister': + writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, values.count());' % (registerDefinition['address'])) + elif registerDefinition['registerType'] == 'coils': + writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, values.count());' % (registerDefinition['address'])) + else: + logger.warning('Error: invalid register type for writing.') + exit(1) + + writeLine(fileDescriptor, ' request.setValues(values);') + + writeLine(fileDescriptor, ' return request;') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + ############################################################## def writePropertyGetSetMethodDeclarationsTcp(fileDescriptor, registerDefinitions): @@ -59,22 +127,138 @@ def writePropertyGetSetMethodImplementationsTcp(fileDescriptor, className, regis if 'W' in registerDefinition['access']: writeLine(fileDescriptor, 'QModbusReply *%s::set%s(%s %s)' % (className, propertyName[0].upper() + propertyName[1:], propertyTyp, propertyName)) writeLine(fileDescriptor, '{') - - writeLine(fileDescriptor, ' QVector values = %s;' % getConversionToValueMethod(registerDefinition)) - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Write \\"%s\\" register:" << %s << "size:" << %s << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - if registerDefinition['registerType'] == 'holdingRegister': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, values.count());' % (registerDefinition['address'])) - elif registerDefinition['registerType'] == 'coils': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, values.count());' % (registerDefinition['address'])) - else: - logger.warning('Error: invalid register type for writing.') - exit(1) - - writeLine(fileDescriptor, ' request.setValues(values);') - writeLine(fileDescriptor, ' return m_modbusTcpMaster->sendWriteRequest(request, m_slaveId);') + writeLine(fileDescriptor, ' return m_modbusTcpMaster->sendWriteRequest(set%sDataUnit(%s), m_slaveId);' % (propertyName[0].upper() + propertyName[1:], propertyName)) writeLine(fileDescriptor, '}') writeLine(fileDescriptor) + +############################################################## + +def writeInternalPropertyReadMethodDeclarationsTcp(fileDescriptor, registerDefinitions): + for registerDefinition in registerDefinitions: + if 'R' in registerDefinition['access']: + propertyName = registerDefinition['id'] + writeLine(fileDescriptor, ' QModbusReply *read%s();' % (propertyName[0].upper() + propertyName[1:])) + +############################################################## + +def writeInternalPropertyReadMethodImplementationsTcp(fileDescriptor, className, registerDefinitions): + for registerDefinition in registerDefinitions: + if 'R' in registerDefinition['access']: + propertyName = registerDefinition['id'] + writeLine(fileDescriptor, 'QModbusReply *%s::read%s()' % (className, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' return m_modbusTcpMaster->sendReadRequest(%sDataUnit(), m_slaveId);' % propertyName) + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +############################################################## + +def writeInternalBlockReadDataUnitDeclarationsTcp(fileDescriptor, blockDefinitions): + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] + blockStartAddress = 0 + registerCount = 0 + blockSize = 0 + + for i, blockRegister in enumerate(blockRegisters): + if i == 0: + blockStartAddress = blockRegister['address'] + + registerCount += 1 + blockSize += blockRegister['size'] + + writeLine(fileDescriptor, ' /* Data unit describing read block request from start addess %s with size of %s registers containing following %s properties:' % (blockStartAddress, blockSize, registerCount)) + for i, registerDefinition in enumerate(blockRegisters): + if 'unit' in registerDefinition and registerDefinition['unit'] != '': + writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) + else: + writeLine(fileDescriptor, ' - %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + writeLine(fileDescriptor, ' */' ) + writeLine(fileDescriptor, ' QModbusDataUnit readBlock%sDataUnit() const;' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor) + + +############################################################## + +def writeInternalBlockReadDataUnitImplementationsTcp(fileDescriptor, className, blockDefinitions): + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] + blockStartAddress = 0 + registerCount = 0 + blockSize = 0 + registerType = "" + + for i, blockRegister in enumerate(blockRegisters): + if i == 0: + blockStartAddress = blockRegister['address'] + registerType = blockRegister['registerType'] + + registerCount += 1 + blockSize += blockRegister['size'] + + + writeLine(fileDescriptor, 'QModbusDataUnit %s::readBlock%sDataUnit() const' % (className, blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, '{') + + # Build request depending on the register type + if registerType == 'inputRegister': + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::InputRegisters, %s, %s);' % (blockStartAddress, blockSize)) + elif registerType == 'discreteInputs': + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::DiscreteInputs, %s, %s);' % (blockStartAddress, blockSize)) + elif registerType == 'coils': + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, %s);' % (blockStartAddress, blockSize)) + else: + #Default to holdingRegister + writeLine(fileDescriptor, ' return QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, %s);' % (blockStartAddress, blockSize)) + + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +############################################################## + +def writeInternalBlockReadMethodDeclarationsTcp(fileDescriptor, blockDefinitions): + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] + blockStartAddress = 0 + registerCount = 0 + blockSize = 0 + + for i, blockRegister in enumerate(blockRegisters): + if i == 0: + blockStartAddress = blockRegister['address'] + registerType = blockRegister['registerType'] + + registerCount += 1 + blockSize += blockRegister['size'] + + writeLine(fileDescriptor, ' /* Read block from start addess %s with size of %s registers containing following %s properties:' % (blockStartAddress, blockSize, registerCount)) + for i, registerDefinition in enumerate(blockRegisters): + if 'unit' in registerDefinition and registerDefinition['unit'] != '': + writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) + else: + writeLine(fileDescriptor, ' - %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + writeLine(fileDescriptor, ' */' ) + writeLine(fileDescriptor, ' QModbusReply *readBlock%s();' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor) + +############################################################## + +def writeInternalBlockReadMethodImplementationsTcp(fileDescriptor, className, blockDefinitions): + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + writeLine(fileDescriptor, 'QModbusReply *%s::readBlock%s()' % (className, blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' return m_modbusTcpMaster->sendReadRequest(readBlock%sDataUnit(), m_slaveId);' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + ############################################################## def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, registerDefinitions, queuedRequests, queuedRequestsDelay): @@ -131,12 +315,7 @@ def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, regis writeLine(fileDescriptor, ' const QModbusDataUnit unit = m_currentInitReply->result();') writeLine(fileDescriptor, ' m_currentInitReply = nullptr;') writeLine(fileDescriptor) - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (unit.values().size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" init registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor) writeLine(fileDescriptor, ' if (!verifyInitFinished())') writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedInitRequest);' % (queuedRequestsDelay, className)) @@ -170,11 +349,7 @@ def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, regis writeLine(fileDescriptor, ' if (m_currentUpdateReply->error() == QModbusDevice::NoError) {') writeLine(fileDescriptor, ' const QModbusDataUnit unit = m_currentUpdateReply->result();') writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (unit.values().size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' }') writeLine(fileDescriptor) writeLine(fileDescriptor, ' m_currentUpdateReply->deleteLater();') @@ -209,12 +384,7 @@ def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, regis writeLine(fileDescriptor, ' handleModbusError(reply->error());') writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::NoError) {') writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (unit.values().size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' }') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -292,19 +462,7 @@ def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDef writeLine(fileDescriptor, ' m_currentInitReply = nullptr;') writeLine(fileDescriptor) writeLine(fileDescriptor, ' const QVector blockValues = unit.values();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from reading init block \\"%s\\" register" << %s << "size:" << %s << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' if (blockValues.size() == %s) {' % (blockSize)) - - # Start parsing the registers using offsets - offset = 0 - for i, blockRegister in enumerate(blockRegisters): - propertyName = blockRegister['id'] - writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) - offset += blockRegister['size'] - - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" block registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' processBlock%sRegisterValues(blockValues);' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor) writeLine(fileDescriptor, ' if (!verifyInitFinished())') writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedInitRequest);' % (queuedRequestsDelay, className)) @@ -335,23 +493,11 @@ def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDef writeLine(fileDescriptor, ' if (m_currentUpdateReply->error() == QModbusDevice::NoError) {') writeLine(fileDescriptor, ' const QModbusDataUnit unit = m_currentUpdateReply->result();') writeLine(fileDescriptor, ' const QVector blockValues = unit.values();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from reading block \\"%s\\" register" << %s << "size:" << %s << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' if (blockValues.size() == %s) {' % (blockSize)) - - # Start parsing the registers using offsets - offset = 0 - for i, blockRegister in enumerate(blockRegisters): - propertyName = blockRegister['id'] - writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) - offset += blockRegister['size'] - - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" block registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' m_currentUpdateReply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' m_currentUpdateReply = nullptr;') - writeLine(fileDescriptor, ' if (!verifyUpdateFinished())') - writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor, ' processBlock%sRegisterValues(blockValues);' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, ' m_currentUpdateReply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' m_currentUpdateReply = nullptr;') + writeLine(fileDescriptor, ' if (!verifyUpdateFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedRequest);' % (queuedRequestsDelay, className)) writeLine(fileDescriptor) writeLine(fileDescriptor, ' }') writeLine(fileDescriptor, ' });') @@ -382,19 +528,7 @@ def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDef writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::NoError) {') writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') writeLine(fileDescriptor, ' const QVector blockValues = unit.values();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from reading block \\"%s\\" register" << %s << "size:" << %s << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' if (blockValues.size() == %s) {' % (blockSize)) - - # Start parsing the registers using offsets - offset = 0 - for i, blockRegister in enumerate(blockRegisters): - propertyName = blockRegister['id'] - writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) - offset += blockRegister['size'] - - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" block registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' processBlock%sRegisterValues(blockValues);' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor, ' }') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -412,105 +546,6 @@ def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDef ############################################################## -def writeInternalPropertyReadMethodDeclarationsTcp(fileDescriptor, registerDefinitions): - for registerDefinition in registerDefinitions: - propertyName = registerDefinition['id'] - writeLine(fileDescriptor, ' QModbusReply *read%s();' % (propertyName[0].upper() + propertyName[1:])) - -############################################################## - -def writeInternalPropertyReadMethodImplementationsTcp(fileDescriptor, className, registerDefinitions): - for registerDefinition in registerDefinitions: - propertyName = registerDefinition['id'] - writeLine(fileDescriptor, 'QModbusReply *%s::read%s()' % (className, propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, '{') - - # Build request depending on the register type - if registerDefinition['registerType'] == 'inputRegister': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::InputRegisters, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) - elif registerDefinition['registerType'] == 'discreteInputs': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::DiscreteInputs, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) - elif registerDefinition['registerType'] == 'coils': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) - else: - #Default to holdingRegister - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, %s);' % (registerDefinition['address'], registerDefinition['size'])) - - writeLine(fileDescriptor, ' return m_modbusTcpMaster->sendReadRequest(request, m_slaveId);') - writeLine(fileDescriptor, '}') - writeLine(fileDescriptor) - - -############################################################## - -def writeInternalBlockReadMethodDeclarationsTcp(fileDescriptor, blockDefinitions): - for blockDefinition in blockDefinitions: - blockName = blockDefinition['id'] - blockRegisters = blockDefinition['registers'] - blockStartAddress = 0 - registerCount = 0 - blockSize = 0 - registerType = "" - - for i, blockRegister in enumerate(blockRegisters): - if i == 0: - blockStartAddress = blockRegister['address'] - registerType = blockRegister['registerType'] - - registerCount += 1 - blockSize += blockRegister['size'] - - writeLine(fileDescriptor, ' /* Read block from start addess %s with size of %s registers containing following %s properties:' % (blockStartAddress, blockSize, registerCount)) - for i, registerDefinition in enumerate(blockRegisters): - if 'unit' in registerDefinition and registerDefinition['unit'] != '': - writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) - else: - writeLine(fileDescriptor, ' - %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' */' ) - writeLine(fileDescriptor, ' QModbusReply *readBlock%s();' % (blockName[0].upper() + blockName[1:])) - writeLine(fileDescriptor) - -############################################################## - -def writeInternalBlockReadMethodImplementationsTcp(fileDescriptor, className, blockDefinitions): - for blockDefinition in blockDefinitions: - blockName = blockDefinition['id'] - blockRegisters = blockDefinition['registers'] - blockStartAddress = 0 - registerCount = 0 - blockSize = 0 - registerType = "" - - for i, blockRegister in enumerate(blockRegisters): - if i == 0: - blockStartAddress = blockRegister['address'] - registerType = blockRegister['registerType'] - - registerCount += 1 - blockSize += blockRegister['size'] - - - writeLine(fileDescriptor, 'QModbusReply *%s::readBlock%s()' % (className, blockName[0].upper() + blockName[1:])) - writeLine(fileDescriptor, '{') - - # Build request depending on the register type - if registerType == 'inputRegister': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::InputRegisters, %s, %s);' % (blockStartAddress, blockSize)) - elif registerType == 'discreteInputs': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::DiscreteInputs, %s, %s);' % (blockStartAddress, blockSize)) - elif registerType == 'coils': - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, %s, %s);' % (blockStartAddress, blockSize)) - else: - #Default to holdingRegister - writeLine(fileDescriptor, ' QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, %s, %s);' % (blockStartAddress, blockSize)) - - writeLine(fileDescriptor, ' return m_modbusTcpMaster->sendReadRequest(request, m_slaveId);') - - writeLine(fileDescriptor, '}') - writeLine(fileDescriptor) - -############################################################## - def writeTestReachabilityImplementationsTcp(fileDescriptor, className, registerDefinitions, checkReachableRegister): propertyName = checkReachableRegister['id'] @@ -655,12 +690,7 @@ def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefiniti writeLine(fileDescriptor, ' }') writeLine(fileDescriptor) writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from init \\"%s\\" register" << %s << "size:" << %s << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (unit.values().size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' verifyInitFinished();') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -719,20 +749,7 @@ def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefiniti writeLine(fileDescriptor) writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') writeLine(fileDescriptor, ' const QVector blockValues = unit.values();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from reading init block \\"%s\\" register" << %s << "size:" << %s << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' if (blockValues.size() == %s) {' % (blockSize)) - - # Start parsing the registers using offsets - offset = 0 - for i, blockRegister in enumerate(blockRegisters): - propertyName = blockRegister['id'] - propertyTyp = getCppDataType(blockRegister) - writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) - offset += blockRegister['size'] - - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" block registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' processBlock%sRegisterValues(blockValues);' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor, ' verifyInitFinished();') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -764,7 +781,7 @@ def writeUpdateMethodTcp(fileDescriptor, className, registerDefinitions, blockDe # First check if there are any update registers updateRequired = False for registerDefinition in registerDefinitions: - if registerDefinition['readSchedule'] == 'update': + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'update': updateRequired = True break @@ -845,12 +862,7 @@ def writeUpdateMethodTcp(fileDescriptor, className, registerDefinitions, blockDe writeLine(fileDescriptor, ' }') writeLine(fileDescriptor) writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' if (unit.values().size() == %s) {' % (registerDefinition['size'])) - writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << unit.values();' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' process%sRegisterValues(unit.values());' % (propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, ' verifyUpdateFinished();') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) @@ -908,21 +920,7 @@ def writeUpdateMethodTcp(fileDescriptor, className, registerDefinitions, blockDe writeLine(fileDescriptor) writeLine(fileDescriptor, ' const QModbusDataUnit unit = reply->result();') writeLine(fileDescriptor, ' const QVector blockValues = unit.values();') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from reading block \\"%s\\" register" << %s << "size:" << %s << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' if (blockValues.size() == %s) {' % (blockSize)) - - # Start parsing the registers using offsets - offset = 0 - for i, blockRegister in enumerate(blockRegisters): - propertyName = blockRegister['id'] - propertyTyp = getCppDataType(blockRegister) - writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) - offset += blockRegister['size'] - - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" block registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << blockValues;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' }') - + writeLine(fileDescriptor, ' processBlock%sRegisterValues(blockValues);' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor, ' verifyUpdateFinished();') writeLine(fileDescriptor, ' });') writeLine(fileDescriptor) diff --git a/libnymea-modbus/tools/connectiontool/toolcommon.py b/libnymea-modbus/tools/connectiontool/toolcommon.py index b6a4c16..e6c4f7a 100644 --- a/libnymea-modbus/tools/connectiontool/toolcommon.py +++ b/libnymea-modbus/tools/connectiontool/toolcommon.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 - 2023 nymea GmbH +# Copyright (C) 2021 - 2024 nymea GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -488,6 +488,7 @@ def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions): else: writeLine(fileDescriptor, ' %s m_%s;' % (propertyTyp, propertyName)) +############################################################## def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions): for registerDefinition in registerDefinitions: @@ -512,16 +513,78 @@ def writePropertyProcessMethodImplementations(fileDescriptor, className, registe writeLine(fileDescriptor, 'void %s::process%sRegisterValues(const QVector &values)' % (className, propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, '{') - writeLine(fileDescriptor, ' %s received%s = %s;' % (propertyTyp, propertyName[0].upper() + propertyName[1:], getValueConversionMethod(registerDefinition))) - writeLine(fileDescriptor, ' emit %sReadFinished(received%s);' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from \\"%s\\" register" << %s << "size:" << %s << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + writeLine(fileDescriptor, ' if (values.size() == %s) {' % (registerDefinition['size'])) + writeLine(fileDescriptor, ' %s received%s = %s;' % (propertyTyp, propertyName[0].upper() + propertyName[1:], getValueConversionMethod(registerDefinition))) + writeLine(fileDescriptor, ' emit %sReadFinished(received%s);' % (propertyName, propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (m_%s != received%s) {' % (propertyName, propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' m_%s = received%s;' % (propertyName, propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' emit %sChanged(m_%s);' % (propertyName, propertyName)) + writeLine(fileDescriptor, ' if (m_%s != received%s) {' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' m_%s = received%s;' % (propertyName, propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' emit %sChanged(m_%s);' % (propertyName, propertyName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << values;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) writeLine(fileDescriptor, ' }') writeLine(fileDescriptor, '}') writeLine(fileDescriptor) +############################################################## + +def writeBlockPropertiesProcessMethodDeclaration(fileDescriptor, blockDefinitions): + + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] + blockStartAddress = 0 + blockSize = 0 + registerCount = 0 + + writeLine(fileDescriptor, ' /* Process block data from start addess %s with size of %s registers containing following %s properties:' % (blockStartAddress, blockSize, registerCount)) + for i, registerDefinition in enumerate(blockRegisters): + if 'unit' in registerDefinition and registerDefinition['unit'] != '': + writeLine(fileDescriptor, ' - %s [%s] - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['unit'], registerDefinition['address'], registerDefinition['size'])) + else: + writeLine(fileDescriptor, ' - %s - Address: %s, Size: %s' % (registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + + writeLine(fileDescriptor, ' */' ) + writeLine(fileDescriptor, ' void processBlock%sRegisterValues(const QVector &blockValues);' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor) + + +def writeBlockPropertiesProcessMethodImplementations(fileDescriptor, className, blockDefinitions): + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] + blockStartAddress = 0 + blockSize = 0 + registerCount = 0 + + for i, blockRegister in enumerate(blockRegisters): + if i == 0: + blockStartAddress = blockRegister['address'] + + registerCount += 1 + blockSize += blockRegister['size'] + + writeLine(fileDescriptor, 'void %s::processBlock%sRegisterValues(const QVector &blockValues)' % (className, blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "<-- Response from reading block \\"%s\\" register" << %s << "size:" << %s << blockValues;' % (className, blockName, blockStartAddress, blockSize)) + writeLine(fileDescriptor, ' if (blockValues.size() == %s) {' % (blockSize)) + + # Start parsing the registers using offsets + offset = 0 + for i, blockRegister in enumerate(blockRegisters): + propertyName = blockRegister['id'] + writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) + offset += blockRegister['size'] + + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Reading from \\"%s\\" block registers" << %s << "size:" << %s << "returned different size than requested. Ignoring incomplete data" << blockValues;' % (className, blockName, blockStartAddress, blockSize)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + +############################################################## def writeSendNextQueuedInitRequestMethodImplementation(fileDescriptor, className): writeLine(fileDescriptor, 'void %s::sendNextQueuedInitRequest()' % (className)) diff --git a/libnymea-modbus/tools/generate-connection.py b/libnymea-modbus/tools/generate-connection.py index eba18ae..af9604f 100644 --- a/libnymea-modbus/tools/generate-connection.py +++ b/libnymea-modbus/tools/generate-connection.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2021 - 2023 nymea GmbH +# Copyright (C) 2021 - 2024 nymea GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -102,6 +102,17 @@ def writeTcpHeaderFile(): # Write block get/set method declarations writeBlocksUpdateMethodDeclarations(headerFile, registerJson['blocks']) + # Write registers get/set data unit declarations + writePropertyGetSetDataUnitDeclarationsTcp(headerFile, registerJson['registers']) + if 'blocks' in registerJson: + for blockDefinition in registerJson['blocks']: + writePropertyGetSetDataUnitDeclarationsTcp(headerFile, blockDefinition['registers']) + + if 'blocks' in registerJson: + writeInternalBlockReadDataUnitDeclarationsTcp(headerFile, registerJson['blocks']) + + + # Update methods writePropertyUpdateMethodDeclarations(headerFile, registerJson['registers']) writeLine(headerFile) if 'blocks' in registerJson: @@ -110,6 +121,11 @@ def writeTcpHeaderFile(): writeLine(headerFile) + if 'blocks' in registerJson: + writePropertyUpdateMethodDeclarations(headerFile, registerJson['blocks']) + + writeLine(headerFile) + writeInternalPropertyReadMethodDeclarationsTcp(headerFile, registerJson['registers']) if 'blocks' in registerJson: for blockDefinition in registerJson['blocks']: @@ -161,6 +177,8 @@ def writeTcpHeaderFile(): for blockDefinition in registerJson['blocks']: writePropertyProcessMethodDeclaration(headerFile, blockDefinition['registers']) + writeBlockPropertiesProcessMethodDeclaration(headerFile, registerJson['blocks']) + writeLine(headerFile, ' void handleModbusError(QModbusDevice::Error error);') writeLine(headerFile, ' void testReachability();') writeLine(headerFile) @@ -334,6 +352,18 @@ def writeTcpSourceFile(): for blockDefinition in registerJson['blocks']: writePropertyGetSetMethodImplementationsTcp(sourceFile, className, blockDefinition['registers']) + + # Property get/set data unit methods + writePropertyGetSetDataUnitImplementationsTcp(sourceFile, className, registerJson['registers']) + if 'blocks' in registerJson: + for blockDefinition in registerJson['blocks']: + writePropertyGetSetDataUnitImplementationsTcp(sourceFile, className, blockDefinition['registers']) + + # Get block data unit method + if 'blocks' in registerJson: + writeInternalBlockReadDataUnitImplementationsTcp(sourceFile, className, registerJson['blocks']) + + # Write init and update method implementation blocks = [] if 'blocks' in registerJson: @@ -383,6 +413,8 @@ def writeTcpSourceFile(): for blockDefinition in registerJson['blocks']: writePropertyProcessMethodImplementations(sourceFile, className, blockDefinition['registers']) + writeBlockPropertiesProcessMethodImplementations(sourceFile, className, registerJson['blocks']) + writeLine(sourceFile, 'void %s::handleModbusError(QModbusDevice::Error error)' % (className)) writeLine(sourceFile, '{') writeLine(sourceFile, ' if (error == QModbusDevice::NoError) {') diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index bb9149a..bc1a5c8 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -17,6 +17,7 @@ PLUGIN_DIRS = \ modbuscommander \ mtec \ mypv \ + pcelectric \ phoenixconnect \ schrack \ senseair \ diff --git a/pcelectric/EV11.3-registers.json b/pcelectric/EV11.3-registers.json new file mode 100644 index 0000000..518103f --- /dev/null +++ b/pcelectric/EV11.3-registers.json @@ -0,0 +1,334 @@ +{ + "className": "EV11", + "protocol": "TCP", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 10, + "checkReachableRegister": "chargingState", + "enums": [ + { + "name": "ChargingState", + "values": [ + { + "key": "Initializing", + "value": 0 + }, + { + "key": "A1", + "value": 1 + }, + { + "key": "A2", + "value": 2 + }, + { + "key": "B1", + "value": 3 + }, + { + "key": "B2", + "value": 4 + }, + { + "key": "C1", + "value": 5 + }, + { + "key": "C2", + "value": 6 + }, + { + "key": "Error", + "value": 7 + } + ] + }, + { + "name": "ChargingRelayState", + "values": [ + { + "key": "NoCharging", + "value": 0 + }, + { + "key": "SinglePhase", + "value": 1 + }, + { + "key": "TheePhase", + "value": 2 + } + ] + }, + { + "name": "Error", + "values": [ + { + "key": "NoError", + "value": 0 + }, + { + "key": "Overheating", + "value": 1 + }, + { + "key": "DCFaultCurrent", + "value": 2 + }, + { + "key": "ChargingWithVentilation", + "value": 3 + }, + { + "key": "CPErrorEF", + "value": 4 + }, + { + "key": "CPErrorBypass", + "value": 5 + }, + { + "key": "CPErrorDiodFault", + "value": 6 + }, + { + "key": "DCFaultCurrentCalibrating", + "value": 7 + }, + { + "key": "DCFaultCurrentCommunication", + "value": 8 + }, + { + "key": "DCFaultCurrentError", + "value": 9 + } + ] + } + ], + "blocks": [ + { + "id": "status", + "readSchedule": "update", + "registers": [ + { + "id": "chargingState", + "address": 100, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "enum": "ChargingState", + "description": "Current charging state", + "defaultValue": "ChargingStateInitializing", + "access": "R" + }, + { + "id": "chargingRelayState", + "address": 101, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "enum": "ChargingRelayState", + "description": "Charging relay state", + "defaultValue": "ChargingRelayStateNoCharging", + "access": "R" + }, + { + "id": "maxChargingCurrentDip", + "address": 102, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Maximum charging current (DIP)", + "unit": "mA", + "defaultValue": "6000", + "access": "R" + }, + { + "id": "phaseAutoSwitch", + "address": 103, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Automatic phase switching", + "defaultValue": "0", + "access": "R" + }, + { + "id": "activeChargingCurrent", + "address": 104, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Active charging current", + "unit": "mA", + "defaultValue": "0", + "access": "R" + }, + { + "id": "sessionDuration", + "address": 105, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Session durration", + "unit": "10 seconds", + "defaultValue": "0", + "access": "R" + }, + { + "id": "powerMeter0", + "address": 106, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Current session energy", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "R" + }, + { + "id": "powerMeter1", + "address": 107, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Last session energy", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "R" + }, + { + "id": "powerMeter3", + "address": 108, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Penultimate session energy", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "R" + }, + { + "id": "temperature", + "address": 109, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Onboard temperature", + "unit": "°C", + "staticScaleFactor": -1, + "defaultValue": "0", + "access": "R" + }, + { + "id": "error", + "address": 110, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "enum": "Error", + "description": "Error", + "defaultValue": "ErrorNoError", + "access": "R" + } + ] + }, + { + "id": "initInfos", + "readSchedule": "init", + "registers": [ + { + "id": "firmwareRevision", + "address": 135, + "size": 2, + "type": "string", + "registerType": "holdingRegister", + "description": "Firmware revision (ASCII)", + "access": "R" + }, + { + "id": "hardwareRevision", + "address": 137, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Hardware revision", + "defaultValue": "0", + "access": "R" + }, + { + "id": "serialNumber", + "address": 138, + "size": 3, + "type": "raw", + "registerType": "holdingRegister", + "description": "Serial number", + "access": "R" + }, + { + "id": "macAddress", + "address": 141, + "size": 3, + "type": "raw", + "registerType": "holdingRegister", + "description": "MAC address", + "access": "R" + } + ] + } + ], + "registers": [ + { + "id": "chargingCurrent", + "address": 200, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Write charging current", + "unit": "mA", + "access": "WO" + }, + { + "id": "chargingCurrentOffline", + "address": 201, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Write charging current", + "unit": "mA", + "access": "WO" + }, + { + "id": "maxChargingTime", + "address": 202, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Max charging time", + "unit": "Minutes", + "access": "WO" + }, + { + "id": "heartbeat", + "address": 203, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Heartbeat (write < 60s to keep alive)", + "access": "WO" + }, + { + "id": "ledBrightness", + "address": 204, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "LED brightness", + "unit": "%", + "access": "WO" + } + ] +} diff --git a/pcelectric/README.md b/pcelectric/README.md new file mode 100644 index 0000000..753d65c --- /dev/null +++ b/pcelectric/README.md @@ -0,0 +1,3 @@ +# PC Electric + + diff --git a/pcelectric/integrationpluginpcelectric.cpp b/pcelectric/integrationpluginpcelectric.cpp new file mode 100644 index 0000000..421379f --- /dev/null +++ b/pcelectric/integrationpluginpcelectric.cpp @@ -0,0 +1,398 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 "integrationpluginpcelectric.h" +#include "pcelectricdiscovery.h" +#include "plugininfo.h" + +#include +#include + +IntegrationPluginPcElectric::IntegrationPluginPcElectric() +{ + +} + +void IntegrationPluginPcElectric::init() +{ + +} + +void IntegrationPluginPcElectric::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcPcElectric()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } + + // Create a discovery with the info as parent for auto deleting the object once the discovery info is done + PcElectricDiscovery *discovery = new PcElectricDiscovery(hardwareManager()->networkDeviceDiscovery(), 502, 1, info); + connect(discovery, &PcElectricDiscovery::discoveryFinished, info, [=](){ + foreach (const PcElectricDiscovery::Result &result, discovery->results()) { + + ThingDescriptor descriptor(ev11ThingClassId, "PCE EV11.3 (" + result.serialNumber + ")", "Version: " + result.firmwareRevision + " - " + result.networkDeviceInfo.address().toString()); + qCDebug(dcPcElectric()) << "Discovered:" << descriptor.title() << descriptor.description(); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(ev11ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcPcElectric()) << "This PCE wallbox already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(ev11ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + // Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults... + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + + // Start the discovery process + discovery->startDiscovery(); +} + +void IntegrationPluginPcElectric::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcPcElectric()) << "Setup thing" << thing << thing->params(); + + if (m_connections.contains(thing)) { + qCDebug(dcPcElectric()) << "Reconfiguring existing thing" << thing->name(); + m_connections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(ev11ThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcPcElectric()) << "The configured mac address is not valid" << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing.")); + return; + } + + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); + m_monitors.insert(thing, monitor); + + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcPcElectric()) << "Unregistering monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + // Only make sure the connection is working in the initial setup, otherwise we let the monitor do the work + if (info->isInitialSetup()) { + // Continue with setup only if we know that the network device is reachable + if (monitor->reachable()) { + setupConnection(info); + } else { + // otherwise wait until we reach the networkdevice before setting up the device + qCDebug(dcPcElectric()) << "Network device" << thing->name() << "is not reachable yet. Continue with the setup once reachable."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcPcElectric()) << "Network device" << thing->name() << "is now reachable. Continue with the setup..."; + setupConnection(info); + } + }); + } + } else { + setupConnection(info); + } + + return; +} + +void IntegrationPluginPcElectric::postSetupThing(Thing *thing) +{ + qCDebug(dcPcElectric()) << "Post setup thing" << thing->name(); + if (!m_refreshTimer) { + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(1); + connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { + foreach (PceWallbox *connection, m_connections) { + if (connection->reachable()) { + connection->update(); + } + } + }); + + qCDebug(dcPcElectric()) << "Starting refresh timer..."; + m_refreshTimer->start(); + } +} + +void IntegrationPluginPcElectric::thingRemoved(Thing *thing) +{ + qCDebug(dcPcElectric()) << "Thing removed" << thing->name(); + + if (m_connections.contains(thing)) { + PceWallbox *connection = m_connections.take(thing); + connection->disconnectDevice(); + connection->deleteLater(); + } + + // Unregister related hardware resources + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + + if (myThings().isEmpty() && m_refreshTimer) { + qCDebug(dcPcElectric()) << "Stopping reconnect timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +void IntegrationPluginPcElectric::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + PceWallbox *connection = m_connections.value(thing); + if (!connection->reachable()) { + qCWarning(dcPcElectric()) << "Could not execute action because the connection is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (info->action().actionTypeId() == ev11PowerActionTypeId) { + bool power = info->action().paramValue(ev11PowerActionPowerParamTypeId).toBool(); + quint16 chargingCurrent = 0; + if (power) { + chargingCurrent = thing->stateValue(ev11MaxChargingCurrentStateTypeId).toUInt() * 1000; + if (thing->stateValue(ev11DesiredPhaseCountStateTypeId).toUInt() == 3) { + // If 3 phase charging is enabled, we set the first bit + chargingCurrent |= static_cast(1) << 15; + } + } + + qCDebug(dcPcElectric()) << "Writing charging current register" << chargingCurrent << "mA"; + QueuedModbusReply *reply = connection->setChargingCurrent(chargingCurrent); + connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, power, chargingCurrent](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set power state to" << power << "(" << chargingCurrent << "mA)" << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + qCDebug(dcPcElectric()) << "Successfully set power state to" << power << "(" << chargingCurrent << "mA)"; + thing->setStateValue(ev11PowerStateTypeId, power); + info->finish(Thing::ThingErrorNoError); + }); + return; + } else if (info->action().actionTypeId() == ev11MaxChargingCurrentActionTypeId) { + uint desiredChargingCurrent = info->action().paramValue(ev11MaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + qCDebug(dcPcElectric()) << "Set max charging current to" << desiredChargingCurrent << "A"; + if (thing->stateValue(ev11PowerStateTypeId).toBool()) { + // The charging is enabled, let's write the value to the wallbox + quint16 finalChargingCurrent = static_cast(desiredChargingCurrent * 1000); + if (thing->stateValue(ev11DesiredPhaseCountStateTypeId).toUInt() == 3) { + // If 3 phase charging is enabled, we set the first bit + finalChargingCurrent |= static_cast(1) << 15; + } + + qCDebug(dcPcElectric()) << "Writing charging current register" << finalChargingCurrent << "mA"; + QueuedModbusReply *reply = connection->setChargingCurrent(finalChargingCurrent); + connect(reply, &QueuedModbusReply::finished, info, [reply, info, thing, desiredChargingCurrent](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set charging current to" << desiredChargingCurrent << "mA" << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + qCDebug(dcPcElectric()) << "Successfully set charging current to" << desiredChargingCurrent << "mA"; + thing->setStateValue(ev11MaxChargingCurrentStateTypeId, desiredChargingCurrent); + info->finish(Thing::ThingErrorNoError); + }); + } else { + // Save the value in the state, but do not send the value to the wallbox since the power state is reflected using the charging current... + qCDebug(dcPcElectric()) << "Setting charging current to" << desiredChargingCurrent << "without synching to wallbox since the power state is false"; + thing->setStateValue(ev11MaxChargingCurrentStateTypeId, desiredChargingCurrent); + info->finish(Thing::ThingErrorNoError); + } + return; + } else if (info->action().actionTypeId() == ev11DesiredPhaseCountActionTypeId) { + uint desiredPhaseCount = info->action().paramValue(ev11DesiredPhaseCountActionDesiredPhaseCountParamTypeId).toUInt(); + qCDebug(dcPcElectric()) << "Desried phase count changed" << desiredPhaseCount; + thing->setStateValue(ev11DesiredPhaseCountStateTypeId, desiredPhaseCount); + info->finish(Thing::ThingErrorNoError); + + // Update the max charging current according to the new desired phase count + if (thing->stateValue(ev11PowerStateTypeId).toBool()) { + uint chargingCurrent = thing->stateValue(ev11MaxChargingCurrentStateTypeId).toUInt(); + quint16 finalChargingCurrent = static_cast(chargingCurrent * 1000); + if (thing->stateValue(ev11DesiredPhaseCountStateTypeId).toUInt() == 3) { + // If 3 phase charging is enabled, we set the first bit + finalChargingCurrent |= static_cast(1) << 15; + } + + qCDebug(dcPcElectric()) << "Writing charging current register" << finalChargingCurrent << "mA"; + QueuedModbusReply *reply = connection->setChargingCurrent(finalChargingCurrent); + connect(reply, &QueuedModbusReply::finished, info, [reply, finalChargingCurrent](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set charging current to" << finalChargingCurrent << "mA" << reply->errorString(); + return; + } + + qCDebug(dcPcElectric()) << "Successfully set charging current to" << finalChargingCurrent << "mA"; + }); + } + return; + } + + + Q_ASSERT_X(false, "IntegrationPluginPcElectric::executeAction", QString("Unhandled action: %1").arg(info->action().actionTypeId().toString()).toLocal8Bit()); +} + +void IntegrationPluginPcElectric::setupConnection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + + qCDebug(dcPcElectric()) << "Setting up PCE wallbox finished successfully" << monitor->networkDeviceInfo().address().toString(); + + PceWallbox *connection = new PceWallbox(monitor->networkDeviceInfo().address(), 502, 1, this); + connect(info, &ThingSetupInfo::aborted, connection, &PceWallbox::deleteLater); + + // Monitor reachability + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + if (!thing->setupComplete()) + return; + + qCDebug(dcPcElectric()) << "Network device monitor for" << thing->name() << (reachable ? "is now reachable" : "is not reachable any more" ); + if (reachable && !thing->stateValue("connected").toBool()) { + connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address()); + connection->connectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + connection->disconnectDevice(); + } + }); + + // Connection reachability + connect(connection, &PceWallbox::reachableChanged, thing, [thing](bool reachable){ + qCInfo(dcPcElectric()) << "Reachable changed to" << reachable << "for" << thing; + thing->setStateValue("connected", reachable); + }); + + connect(connection, &PceWallbox::updateFinished, thing, [thing, connection](){ + qCDebug(dcPcElectric()) << "Update finished for" << thing; + qCDebug(dcPcElectric()) << connection; + if (!connection->phaseAutoSwitch()) { + // Note: if auto phase switching is disabled, the wallbox forces 3 phase charging + thing->setStatePossibleValues(ev11DesiredPhaseCountStateTypeId, { 3 }); // Disable phase switching (default 3) + thing->setStateValue(ev11DesiredPhaseCountStateTypeId, 3); + thing->setStateValue(ev11PhaseCountStateTypeId, 3); + } else { + thing->setStatePossibleValues(ev11DesiredPhaseCountStateTypeId, { 1, 3 }); // Enable phase switching + } + + if (connection->chargingRelayState() != EV11ModbusTcpConnection::ChargingRelayStateNoCharging) { + if (connection->chargingRelayState() == EV11ModbusTcpConnection::ChargingRelayStateSinglePhase) { + thing->setStateValue(ev11PhaseCountStateTypeId, 1); + } else if (connection->chargingRelayState() == EV11ModbusTcpConnection::ChargingRelayStateTheePhase) { + thing->setStateValue(ev11PhaseCountStateTypeId, 3); + } + } + + thing->setStateMaxValue(ev11MaxChargingCurrentStateTypeId, connection->maxChargingCurrentDip() / 1000); + thing->setStateValue(ev11PluggedInStateTypeId, connection->chargingState() >= PceWallbox::ChargingStateB1 && + connection->chargingState() < PceWallbox::ChargingStateError); + + thing->setStateValue(ev11ChargingStateTypeId, connection->chargingState() == PceWallbox::ChargingStateC2); + if (connection->chargingRelayState() != EV11ModbusTcpConnection::ChargingRelayStateNoCharging) { + thing->setStateValue(ev11PhaseCountStateTypeId, connection->chargingRelayState() == EV11ModbusTcpConnection::ChargingRelayStateSinglePhase ? 1 : 3); + } + + thing->setStateValue(ev11CurrentVersionStateTypeId, connection->firmwareRevision()); + thing->setStateValue(ev11SessionEnergyStateTypeId, connection->powerMeter0()); + thing->setStateValue(ev11TemperatureStateTypeId, connection->temperature()); + + switch (connection->error()) { + case EV11ModbusTcpConnection::ErrorNoError: + thing->setStateValue(ev11ErrorStateTypeId, "Kein Fehler aktiv"); + break; + case EV11ModbusTcpConnection::ErrorOverheating: + thing->setStateValue(ev11ErrorStateTypeId, "Übertemperatur. Ladevorgang wird automatisch fortgesetzt."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrent: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor ausgelöst."); + break; + case EV11ModbusTcpConnection::ErrorChargingWithVentilation: + thing->setStateValue(ev11ErrorStateTypeId, "Ladeanforderung mit Belüftung."); + break; + case EV11ModbusTcpConnection::ErrorCPErrorEF: + thing->setStateValue(ev11ErrorStateTypeId, "CP Signal, Fehlercode E oder F."); + break; + case EV11ModbusTcpConnection::ErrorCPErrorBypass: + thing->setStateValue(ev11ErrorStateTypeId, "CP Signal, bypass."); + break; + case EV11ModbusTcpConnection::ErrorCPErrorDiodFault: + thing->setStateValue(ev11ErrorStateTypeId, "CP Signal, Diode defekt."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrentCalibrating: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor, Kalibrirung."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrentCommunication: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor, Kommunikationsfehler."); + break; + case EV11ModbusTcpConnection::ErrorDCFaultCurrentError: + thing->setStateValue(ev11ErrorStateTypeId, "DC Fehlerstromsensor, Fehler."); + break; + } + }); + + connect(thing, &Thing::settingChanged, connection, [thing, connection](const ParamTypeId ¶mTypeId, const QVariant &value){ + if (paramTypeId == ev11SettingsLedBrightnessParamTypeId) { + quint16 percentage = value.toUInt(); + qCDebug(dcPcElectric()) << "Set LED brightness" << percentage << "%"; + QueuedModbusReply *reply = connection->setLedBrightness(percentage); + connect(reply, &QueuedModbusReply::finished, thing, [reply, percentage](){ + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Could not set led brightness to" << percentage << "%" << reply->errorString(); + return; + } + + qCDebug(dcPcElectric()) << "Successfully set led brightness to" << percentage << "%"; + }); + } + }); + + m_connections.insert(thing, connection); + info->finish(Thing::ThingErrorNoError); + + // Connect reight the way if the monitor indicates reachable, otherwise the connect will handle the connect later + if (monitor->reachable()) + connection->connectDevice(); +} diff --git a/pcelectric/integrationpluginpcelectric.h b/pcelectric/integrationpluginpcelectric.h new file mode 100644 index 0000000..9fb1519 --- /dev/null +++ b/pcelectric/integrationpluginpcelectric.h @@ -0,0 +1,69 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 INTEGRATIONPLUGINPCELECTRIC_H +#define INTEGRATIONPLUGINPCELECTRIC_H + +#include + +#include +#include +#include + +#include "pcewallbox.h" +#include "extern-plugininfo.h" + +class IntegrationPluginPcElectric : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginpcelectric.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginPcElectric(); + void init() override; + + 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_refreshTimer = nullptr; + QHash m_connections; + QHash m_monitors; + + void setupConnection(ThingSetupInfo *info); + +}; + +#endif // INTEGRATIONPLUGINPCELECTRIC_H diff --git a/pcelectric/integrationpluginpcelectric.json b/pcelectric/integrationpluginpcelectric.json new file mode 100644 index 0000000..eed7855 --- /dev/null +++ b/pcelectric/integrationpluginpcelectric.json @@ -0,0 +1,148 @@ +{ + "name": "PcElectric", + "displayName": "PC Electric", + "id": "aa7ff833-a8e0-45cc-a1ef-65f05871f272", + "paramTypes":[ ], + "vendors": [ + { + "name": "PcElectric", + "displayName": "PC Electric GmbH", + "id": "b365937b-f1d6-46bf-9ff1-e787373b8aa6", + "thingClasses": [ + { + "name": "ev11", + "displayName": "PCE EV11.3", + "id": "88d96940-a940-4a07-8176-5e6aba7ca832", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "connectable"], + "paramTypes": [ + { + "id": "0a3f8d12-9d33-4ae2-b763-9568f32e8da1", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "settingsTypes": [ + { + "id": "3a1329a2-84cc-47b9-a6c2-e96fdfd0c454", + "name": "ledBrightness", + "displayName": "LED brightness", + "type": "uint", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 50 + } + ], + "stateTypes": [ + { + "id": "ca8d680c-c2f8-456a-a246-9c6cd64e25a7", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "c12a7a27-fa56-450c-a1ec-717c868554f2", + "name": "power", + "displayName": "Charging enabled", + "displayNameEvent": "Charging enabled or disabled", + "displayNameAction": "Enable or disable charging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "b5bbf23c-06db-463b-bb5c-3aea38e18818", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameEvent": "Maximum charging current changed", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "defaultValue": 6, + "minValue": 6, + "maxValue": 16, + "writable": true + }, + { + "id": "50164bbd-9802-4cf6-82de-626b74293a1b", + "name": "pluggedIn", + "displayName": "Plugged in", + "displayNameEvent": "Plugged or unplugged", + "type": "bool", + "defaultValue": false + }, + { + "id": "b7972cd7-471a-46bd-ab99-f49997f12309", + "name": "charging", + "displayName": "Charging", + "displayNameEvent": "Charging started or stopped", + "type": "bool", + "defaultValue": false + }, + { + "id": "bca88c23-e940-40c1-afca-eb511fd17aab", + "name": "phaseCount", + "displayName": "Active phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 3 + }, + { + "id": "d91f7d96-2599-400a-91da-d164477098b7", + "name": "desiredPhaseCount", + "displayName": "Desired phase count", + "displayNameAction": "Set desired phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "possibleValues": [1, 3], + "defaultValue": 3, + "writable": true + }, + { + "id": "3da3ee80-e9e7-4237-85a6-b4adcb2f483b", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "bb092562-377e-458e-bb8a-735af9036652", + "name": "temperature", + "displayName": "Onboard temperature", + "displayNameEvent": "Onboard temperature changed", + "unit": "DegreeCelsius", + "type": "double", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "2ea1a53f-b2b0-452d-8060-cdb114db05a7", + "name": "error", + "displayName": "Error", + "type": "QString", + "defaultValue": "Kein Fehler", + "suggestLogging": true + }, + { + "id": "142b4276-e2e9-4149-adc4-89d9d3e31117", + "name": "currentVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} diff --git a/pcelectric/meta.json b/pcelectric/meta.json new file mode 100644 index 0000000..6e433a8 --- /dev/null +++ b/pcelectric/meta.json @@ -0,0 +1,13 @@ +{ + "title": "PC Electric", + "tagline": "Integrate the PCE EV11.3 wallbox with nymea.", + "icon": "pce.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "modbus" + ], + "categories": [ + "energy" + ] +} diff --git a/pcelectric/pce.png b/pcelectric/pce.png new file mode 100644 index 0000000..db773ed Binary files /dev/null and b/pcelectric/pce.png differ diff --git a/pcelectric/pcelectric.pro b/pcelectric/pcelectric.pro new file mode 100644 index 0000000..61740a5 --- /dev/null +++ b/pcelectric/pcelectric.pro @@ -0,0 +1,17 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += EV11.3-registers.json +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + integrationpluginpcelectric.h \ + pcelectricdiscovery.h \ + pcewallbox.h + +SOURCES += \ + integrationpluginpcelectric.cpp \ + pcelectricdiscovery.cpp \ + pcewallbox.cpp + diff --git a/pcelectric/pcelectricdiscovery.cpp b/pcelectric/pcelectricdiscovery.cpp new file mode 100644 index 0000000..427834a --- /dev/null +++ b/pcelectric/pcelectricdiscovery.cpp @@ -0,0 +1,169 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 "pcelectricdiscovery.h" +#include "extern-plugininfo.h" + +PcElectricDiscovery::PcElectricDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent) + : QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery}, + m_port{port}, + m_modbusAddress{modbusAddress} +{ + +} + +QList PcElectricDiscovery::results() const +{ + return m_results; +} + + +void PcElectricDiscovery::startDiscovery() +{ + qCInfo(dcPcElectric()) << "Discovery: Start searching for PCE wallboxes in the network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &PcElectricDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + // Finish with some delay so the last added network device information objects still can be checked. + QTimer::singleShot(3000, this, [this](){ + qCDebug(dcPcElectric()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + }); +} + +void PcElectricDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + EV11ModbusTcpConnection *connection = new EV11ModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + m_connections.append(connection); + + connect(connection, &EV11ModbusTcpConnection::reachableChanged, this, [=](bool reachable){ + if (!reachable) { + // Disconnected ... done with this connection + cleanupConnection(connection); + return; + } + + // Modbus TCP connected...ok, let's try to initialize it! + connect(connection, &EV11ModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcPcElectric()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + return; + } + + // Parse the mac address from the registers and compair with the network device info mac address. + // If they match, we most likly found a PCE wallbox + + QByteArray macRawData; + QDataStream stream(&macRawData, QIODevice::WriteOnly); + for (int i = 0; i < connection->macAddress().count(); i++) + stream << connection->macAddress().at(i); + + MacAddress registerMacAddress(macRawData); + qCDebug(dcPcElectric()) << "Fetched mac address" << macRawData.toHex() << registerMacAddress; + + // According to PCE the HW revision must be 0 + if (registerMacAddress == MacAddress(networkDeviceInfo.macAddress()) && connection->hardwareRevision() == 0) { + + // Parse the serial number + QByteArray serialRawData; + QDataStream stream(&serialRawData, QIODevice::ReadWrite); + stream << static_cast(0); + for (int i = 0; i < connection->serialNumber().count(); i++) + stream << connection->serialNumber().at(i); + + quint64 serialNumber = serialRawData.toHex().toULongLong(nullptr, 16); + qCDebug(dcPcElectric()) << "Serial number" << serialRawData.toHex() << serialNumber; + + Result result; + result.serialNumber = QString::number(serialNumber); + result.firmwareRevision = connection->firmwareRevision(); + result.networkDeviceInfo = networkDeviceInfo; + m_results.append(result); + + qCInfo(dcPcElectric()) << "Discovery: --> Found" + << "Serial number:" << result.serialNumber + << "Firmware revision:" << result.firmwareRevision + << result.networkDeviceInfo; + } + + // Done with this connection + cleanupConnection(connection); + }); + + // Initializing... + if (!connection->initialize()) { + qCDebug(dcPcElectric()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If we get any error...skip this host... + connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [=](QModbusDevice::Error error){ + if (error != QModbusDevice::NoError) { + qCDebug(dcPcElectric()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &EV11ModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcPcElectric()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); +} + +void PcElectricDiscovery::cleanupConnection(EV11ModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void PcElectricDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (EV11ModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcPcElectric()) << "Discovery: Finished the discovery process. Found" << m_results.count() + << "PCE wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit discoveryFinished(); +} diff --git a/pcelectric/pcelectricdiscovery.h b/pcelectric/pcelectricdiscovery.h new file mode 100644 index 0000000..bdee2f1 --- /dev/null +++ b/pcelectric/pcelectricdiscovery.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 PCELECTRICDISCOVERY_H +#define PCELECTRICDISCOVERY_H + +#include + +#include + +#include "ev11modbustcpconnection.h" + +class PcElectricDiscovery : public QObject +{ + Q_OBJECT +public: + explicit PcElectricDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent = nullptr); + + typedef struct Result { + QString serialNumber; + QString firmwareRevision; + NetworkDeviceInfo networkDeviceInfo; + } Result; + + QList results() const; + +public slots: + void startDiscovery(); + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + quint16 m_port; + quint16 m_modbusAddress; + QDateTime m_startDateTime; + + QList m_connections; + + QList m_results; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(EV11ModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // PCELECTRICDISCOVERY_H diff --git a/pcelectric/pcewallbox.cpp b/pcelectric/pcewallbox.cpp new file mode 100644 index 0000000..dc0b914 --- /dev/null +++ b/pcelectric/pcewallbox.cpp @@ -0,0 +1,267 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 "pcewallbox.h" +#include "extern-plugininfo.h" + +#include + +PceWallbox::PceWallbox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent) + : EV11ModbusTcpConnection{hostAddress, port, slaveId, parent} +{ + // Timer for resetting the heartbeat register (watchdog) + m_timer.setInterval(30000); + m_timer.setSingleShot(false); + connect(&m_timer, &QTimer::timeout, this, &PceWallbox::sendHeartbeat); + + connect(this, &EV11ModbusTcpConnection::reachableChanged, this, [this](bool reachable){ + if (!reachable) { + m_timer.stop(); + + qDeleteAll(m_queue); + m_queue.clear(); + + if (m_currentReply) { + m_currentReply = nullptr; + } + + } else { + initialize(); + } + }); + + connect(this, &EV11ModbusTcpConnection::initializationFinished, this, [this](bool success){ + if (success) { + qCDebug(dcPcElectric()) << "Connection initialized successfully" << m_modbusTcpMaster->hostAddress().toString(); + m_timer.start(); + + sendHeartbeat(); + update(); + + } else { + qCWarning(dcPcElectric()) << "Connection initialization failed for" << m_modbusTcpMaster->hostAddress().toString(); + } + }); +} + +bool PceWallbox::update() +{ + if (m_aboutToDelete) + return false; + + if (!reachable()) + return false; + + // Make sure we only have one update call in the queue + foreach (QueuedModbusReply *r, m_queue) { + if (r->dataUnit().startAddress() == readBlockInitInfosDataUnit().startAddress()) { + return true; + } + } + + QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, readBlockStatusDataUnit(), this); + connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); + connect(reply, &QueuedModbusReply::finished, this, [this, reply](){ + + if (m_currentReply == reply) + m_currentReply = nullptr; + + if (reply->error() != QModbusDevice::NoError) { + emit updateFinished(); + sendNextRequest(); + return; + } + + const QModbusDataUnit unit = reply->reply()->result(); + const QVector blockValues = unit.values(); + processBlockStatusRegisterValues(blockValues); + + emit updateFinished(); + sendNextRequest(); + }); + + enqueueRequest(reply); + return true; +} + +QueuedModbusReply *PceWallbox::setChargingCurrent(quint16 chargingCurrent) +{ + if (m_aboutToDelete) + return nullptr; + + QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setChargingCurrentDataUnit(chargingCurrent), this); + + connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); + connect(reply, &QueuedModbusReply::finished, this, [this, reply](){ + if (m_currentReply == reply) + m_currentReply = nullptr; + + sendNextRequest(); + return; + }); + + enqueueRequest(reply, true); + return reply; +} + +QueuedModbusReply *PceWallbox::setLedBrightness(quint16 percentage) +{ + if (m_aboutToDelete) + return nullptr; + + QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setLedBrightnessDataUnit(percentage), this); + + connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); + connect(reply, &QueuedModbusReply::finished, this, [this, reply](){ + if (m_currentReply == reply) + m_currentReply = nullptr; + + sendNextRequest(); + return; + }); + + enqueueRequest(reply, true); + return reply; +} + +void PceWallbox::gracefullDeleteLater() +{ + // Clean up the queue + m_aboutToDelete = true; + cleanupQueue(); + + m_timer.stop(); + + if (!m_currentReply) { + qCDebug(dcPcElectric()) << "Deleting object without pending request..."; + // No pending request, we can close the connection and delete the object + disconnect(this, nullptr, nullptr, nullptr); + disconnectDevice(); + deleteLater(); + } else { + qCDebug(dcPcElectric()) << "Pending request, deleting object once the request is finished..."; + } +} + +void PceWallbox::sendHeartbeat() +{ + if (m_aboutToDelete) + return; + + QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setHeartbeatDataUnit(m_heartbeat++), this); + + connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater); + + connect(reply, &QueuedModbusReply::finished, this, [this, reply](){ + if (m_currentReply == reply) + m_currentReply = nullptr; + + if (reply->error() != QModbusDevice::NoError) { + qCWarning(dcPcElectric()) << "Failed to send heartbeat to" << m_modbusTcpMaster->hostAddress().toString() << reply->errorString(); + } else { + qCDebug(dcPcElectric()) << "Successfully sent heartbeat to" << m_modbusTcpMaster->hostAddress().toString(); + } + + sendNextRequest(); + return; + }); + + enqueueRequest(reply, true); +} + +void PceWallbox::sendNextRequest() +{ + if (m_queue.isEmpty()) + return; + + if (m_currentReply) + return; + + if (m_aboutToDelete) { + disconnect(this, nullptr, nullptr, nullptr); + disconnectDevice(); + deleteLater(); + return; + } + + m_currentReply = m_queue.dequeue(); + switch(m_currentReply->requestType()) { + case QueuedModbusReply::RequestTypeRead: + qCDebug(dcPcElectric()) << "--> Reading" << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType()) + << "register:" << m_currentReply->dataUnit().startAddress() + << "length" << m_currentReply->dataUnit().valueCount(); + m_currentReply->setReply(m_modbusTcpMaster->sendReadRequest(m_currentReply->dataUnit(), m_slaveId)); + break; + case QueuedModbusReply::RequestTypeWrite: + qCDebug(dcPcElectric()) << "--> Writing" << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType()) + << "register:" << m_currentReply->dataUnit().startAddress() + << "length:" << m_currentReply->dataUnit().valueCount() + << "values:" << m_currentReply->dataUnit().values(); + m_currentReply->setReply(m_modbusTcpMaster->sendWriteRequest(m_currentReply->dataUnit(), m_slaveId)); + break; + } + + if (!m_currentReply->reply()) { + qCWarning(dcPcElectric()) << "Error occurred while sending" << m_currentReply->requestType() + << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType()) + << "register:" << m_currentReply->dataUnit().startAddress() + << "length:" << m_currentReply->dataUnit().valueCount() + << "to" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString(); + m_currentReply->deleteLater(); + m_currentReply = nullptr; + sendNextRequest(); + return; + } + + if (m_currentReply->reply()->isFinished()) { + qCWarning(dcPcElectric()) << "Reply immediatly finished"; + m_currentReply->deleteLater(); + m_currentReply = nullptr; + sendNextRequest(); + return; + } +} + +void PceWallbox::enqueueRequest(QueuedModbusReply *reply, bool prepend) +{ + if (prepend) { + m_queue.prepend(reply); + } else { + m_queue.enqueue(reply); + } + + sendNextRequest(); +} + +void PceWallbox::cleanupQueue() +{ + qDeleteAll(m_queue); + m_queue.clear(); +} diff --git a/pcelectric/pcewallbox.h b/pcelectric/pcewallbox.h new file mode 100644 index 0000000..9954e59 --- /dev/null +++ b/pcelectric/pcewallbox.h @@ -0,0 +1,77 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2024, 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 PCEWALLBOX_H +#define PCEWALLBOX_H + +#include +#include +#include + +#include + +#include "ev11modbustcpconnection.h" + +class PceWallbox : public EV11ModbusTcpConnection +{ + Q_OBJECT +public: + explicit PceWallbox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr); + + bool update() override; + + QueuedModbusReply *setChargingCurrent(quint16 chargingCurrent); // mA + + QueuedModbusReply *setLedBrightness(quint16 percentage); + + // Note: the modbus implementation of the wallbox gets stuck if a Modbus request has been sent + // and we disconnect the socket before the response has arrived. Only a reboot of the wallbox + // fixes the broken communication afterwards. This method waits for the current request before closing the + // socket and deletes it self. + // IMPORTNAT: do not use the object after this call, this is a temporary workaround + void gracefullDeleteLater(); + +private slots: + void sendHeartbeat(); + +private: + QTimer m_timer; + quint16 m_heartbeat = 1; + QueuedModbusReply *m_currentReply = nullptr; + QQueue m_queue; + bool m_aboutToDelete = false; + + void sendNextRequest(); + void enqueueRequest(QueuedModbusReply *reply, bool prepend = false); + + void cleanupQueue(); +}; + +#endif // PCEWALLBOX_H diff --git a/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-de_DE.ts b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-de_DE.ts new file mode 100644 index 0000000..82ee749 --- /dev/null +++ b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-de_DE.ts @@ -0,0 +1,117 @@ + + + + + IntegrationPluginPcElectric + + + The network device discovery is not available. + Die Netzwerk Suche ist nicht verfügbar. + + + + PcElectric + + + Active phases + The name of the StateType ({bca88c23-e940-40c1-afca-eb511fd17aab}) of ThingClass ev11 + Aktive Phasen + + + + Charging + The name of the StateType ({b7972cd7-471a-46bd-ab99-f49997f12309}) of ThingClass ev11 + Lädt + + + + + Charging enabled + The name of the ParamType (ThingClass: ev11, ActionType: power, ID: {c12a7a27-fa56-450c-a1ec-717c868554f2}) +---------- +The name of the StateType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + Laden eingeschalten + + + + Connected + The name of the StateType ({ca8d680c-c2f8-456a-a246-9c6cd64e25a7}) of ThingClass ev11 + Verbunden + + + + Enable or disable charging + The name of the ActionType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + Laden starten/stoppen + + + + Firmware version + The name of the StateType ({142b4276-e2e9-4149-adc4-89d9d3e31117}) of ThingClass ev11 + Firmware Version + + + + Hardware version + The name of the StateType ({b6e65baf-6dcd-4db1-a3dc-962a4c33d157}) of ThingClass ev11 + Hardware Version + + + + LED brightness + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {3a1329a2-84cc-47b9-a6c2-e96fdfd0c454}) + LED Helligkeit + + + + MAC address + The name of the ParamType (ThingClass: ev11, Type: thing, ID: {0a3f8d12-9d33-4ae2-b763-9568f32e8da1}) + MAC Adresse + + + + + Maximum charging current + The name of the ParamType (ThingClass: ev11, ActionType: maxChargingCurrent, ID: {b5bbf23c-06db-463b-bb5c-3aea38e18818}) +---------- +The name of the StateType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + Maximaler Ladestrom + + + + Maximum offline charging current + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {93654273-c4d3-4389-a81e-c0f065d9cd92}) + Maximaler Ladestrom offline + + + + PC Electric + The name of the plugin PcElectric ({aa7ff833-a8e0-45cc-a1ef-65f05871f272}) + PC Electric + + + + PC Electric GmbH + The name of the vendor ({b365937b-f1d6-46bf-9ff1-e787373b8aa6}) + PC Electric GmbH + + + + PCE EV11.X + The name of the ThingClass ({88d96940-a940-4a07-8176-5e6aba7ca832}) + PCE EV11.X + + + + Plugged in + The name of the StateType ({50164bbd-9802-4cf6-82de-626b74293a1b}) of ThingClass ev11 + Angesteckt + + + + Set maximum charging current + The name of the ActionType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + Setze maximalen Ladestrom + + + diff --git a/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-en_US.ts b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-en_US.ts new file mode 100644 index 0000000..a03f87a --- /dev/null +++ b/pcelectric/translations/aa7ff833-a8e0-45cc-a1ef-65f05871f272-en_US.ts @@ -0,0 +1,117 @@ + + + + + IntegrationPluginPcElectric + + + The network device discovery is not available. + + + + + PcElectric + + + Active phases + The name of the StateType ({bca88c23-e940-40c1-afca-eb511fd17aab}) of ThingClass ev11 + + + + + Charging + The name of the StateType ({b7972cd7-471a-46bd-ab99-f49997f12309}) of ThingClass ev11 + + + + + + Charging enabled + The name of the ParamType (ThingClass: ev11, ActionType: power, ID: {c12a7a27-fa56-450c-a1ec-717c868554f2}) +---------- +The name of the StateType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + + + + + Connected + The name of the StateType ({ca8d680c-c2f8-456a-a246-9c6cd64e25a7}) of ThingClass ev11 + + + + + Enable or disable charging + The name of the ActionType ({c12a7a27-fa56-450c-a1ec-717c868554f2}) of ThingClass ev11 + + + + + Firmware version + The name of the StateType ({142b4276-e2e9-4149-adc4-89d9d3e31117}) of ThingClass ev11 + + + + + Hardware version + The name of the StateType ({b6e65baf-6dcd-4db1-a3dc-962a4c33d157}) of ThingClass ev11 + + + + + LED brightness + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {3a1329a2-84cc-47b9-a6c2-e96fdfd0c454}) + + + + + MAC address + The name of the ParamType (ThingClass: ev11, Type: thing, ID: {0a3f8d12-9d33-4ae2-b763-9568f32e8da1}) + + + + + + Maximum charging current + The name of the ParamType (ThingClass: ev11, ActionType: maxChargingCurrent, ID: {b5bbf23c-06db-463b-bb5c-3aea38e18818}) +---------- +The name of the StateType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + + + + + Maximum offline charging current + The name of the ParamType (ThingClass: ev11, Type: settings, ID: {93654273-c4d3-4389-a81e-c0f065d9cd92}) + + + + + PC Electric + The name of the plugin PcElectric ({aa7ff833-a8e0-45cc-a1ef-65f05871f272}) + + + + + PC Electric GmbH + The name of the vendor ({b365937b-f1d6-46bf-9ff1-e787373b8aa6}) + + + + + PCE EV11.X + The name of the ThingClass ({88d96940-a940-4a07-8176-5e6aba7ca832}) + + + + + Plugged in + The name of the StateType ({50164bbd-9802-4cf6-82de-626b74293a1b}) of ThingClass ev11 + + + + + Set maximum charging current + The name of the ActionType ({b5bbf23c-06db-463b-bb5c-3aea38e18818}) of ThingClass ev11 + + + +