diff --git a/debian/control b/debian/control index 1dcd7c8..5c30848 100644 --- a/debian/control +++ b/debian/control @@ -210,6 +210,14 @@ Description: nymea integration plugin for SMA solar inverters and meters This package contains the nymea integration plugin for SMA solar inverters and meters. +Package: nymea-plugin-solax +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for Solax modbus devices + This package contains the nymea integration plugin for solax compatible solar inverters, meters and batteries. + + Package: nymea-plugin-stiebeleltron Architecture: any Section: libs @@ -250,6 +258,27 @@ Description: nymea integration plugin for PhoenixConnect wallboxes This package contains the nymea integration plugin for wallboxes made by PhonenixConnect and rebranded as Wallbe, Compleo and Scapo. + +Package: nymea-plugin-vestel +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for Vestel wallboxes + This package contains the nymea integration plugin for wallboxes made + by Vestel. + + +Package: nymea-plugin-wattsonic +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for Wattsonic hybrid inverters + This package contains the nymea integration plugin for hybrid inverters made + by Wattsonic. + + Package: nymea-plugin-webasto Architecture: any Section: libs diff --git a/debian/nymea-plugin-solax.install.in b/debian/nymea-plugin-solax.install.in new file mode 100644 index 0000000..32f58b4 --- /dev/null +++ b/debian/nymea-plugin-solax.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsolax.so +solax/translations/*qm usr/share/nymea/translations/ diff --git a/debian/nymea-plugin-vestel.install.in b/debian/nymea-plugin-vestel.install.in new file mode 100644 index 0000000..bd2f486 --- /dev/null +++ b/debian/nymea-plugin-vestel.install.in @@ -0,0 +1,5 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginvestel.so +vestel/translations/*qm usr/share/nymea/translations/ + + + diff --git a/debian/nymea-plugin-wattsonic.install.in b/debian/nymea-plugin-wattsonic.install.in new file mode 100644 index 0000000..c860a58 --- /dev/null +++ b/debian/nymea-plugin-wattsonic.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginwattsonic.so +wattsonic/translations/*qm usr/share/nymea/translations/ diff --git a/debs_1.8.2+202308081314~dc23d1b~buster.tar b/debs_1.8.2+202308081314~dc23d1b~buster.tar deleted file mode 100644 index b0e9127..0000000 Binary files a/debs_1.8.2+202308081314~dc23d1b~buster.tar and /dev/null differ diff --git a/libnymea-modbus/modbusdatautils.cpp b/libnymea-modbus/modbusdatautils.cpp index 1729e58..ddfbb93 100644 --- a/libnymea-modbus/modbusdatautils.cpp +++ b/libnymea-modbus/modbusdatautils.cpp @@ -146,6 +146,16 @@ QString ModbusDataUtils::convertToString(const QVector ®isters, Byte return QString::fromUtf8(bytes).trimmed(); } +QByteArray ModbusDataUtils::convertToByteArray(const QVector ®isters) +{ + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + for (int i = 0; i < registers.count(); i++) { + stream << registers.at(i); + } + return bytes; +} + float ModbusDataUtils::convertToFloat32(const QVector ®isters, ByteOrder byteOrder) { Q_ASSERT_X(registers.count() == 2, "ModbusDataUtils", "invalid raw data size for converting value to float32"); diff --git a/libnymea-modbus/modbusdatautils.h b/libnymea-modbus/modbusdatautils.h index 1a587e3..7bd4e00 100644 --- a/libnymea-modbus/modbusdatautils.h +++ b/libnymea-modbus/modbusdatautils.h @@ -90,6 +90,7 @@ public: static quint64 convertToUInt64(const QVector ®isters, ByteOrder byteOrder = ByteOrderLittleEndian); static qint64 convertToInt64(const QVector ®isters, ByteOrder byteOrder = ByteOrderLittleEndian); static QString convertToString(const QVector ®isters, ByteOrder characterByteOrder = ByteOrderLittleEndian); + static QByteArray convertToByteArray(const QVector ®isters); static float convertToFloat32(const QVector ®isters, ByteOrder byteOrder = ByteOrderLittleEndian); static double convertToFloat64(const QVector ®isters, ByteOrder byteOrder = ByteOrderLittleEndian); diff --git a/libnymea-modbus/tools/README.md b/libnymea-modbus/tools/README.md index 6752d20..ee808e0 100644 --- a/libnymea-modbus/tools/README.md +++ b/libnymea-modbus/tools/README.md @@ -23,6 +23,8 @@ The basic structure of the modbus register JSON looks like following example: "stringEndianness": "BigEndian", "errorLimitUntilNotReachable": 10, "checkReachableRegister": "registerPropertyName", + "queuedRequests": false, + "queuedRequestsDelay": 0, "enums": [ { "name": "NameOfEnum", @@ -159,6 +161,19 @@ Many modbus devices provide inforation using `Enums`, indicating a special state If a register represets an enum, you simply add the property `"enum": "NameOfEnum"` in the register map and the property will be defined using the resulting enum type. All convertion between enum and resulting modbus register value will be done automatically. +## Queued requests + +Some modbus devices can process only one request at the time, and sometimes even require a delay between requests. For this purpose the boolean property `queuedRequests` and integer property `queuedRequestsDelay` (milliseconds) property hase been introdiced. By default, requests are not queued and the delay 0 ms. + +``` +{ + ... + "queuedRequests": false, + "queuedRequestsDelay": 0, + ... +} +``` + ## Read schedules ### init diff --git a/libnymea-modbus/tools/connectiontool/modbustcp.py b/libnymea-modbus/tools/connectiontool/modbustcp.py index a5ff0fb..4ea9afe 100644 --- a/libnymea-modbus/tools/connectiontool/modbustcp.py +++ b/libnymea-modbus/tools/connectiontool/modbustcp.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 - 2022 nymea GmbH +# Copyright (C) 2021 - 2023 nymea GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -77,68 +77,172 @@ def writePropertyGetSetMethodImplementationsTcp(fileDescriptor, className, regis ############################################################## -def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, registerDefinitions): +def writePropertyUpdateMethodImplementationsTcp(fileDescriptor, className, registerDefinitions, queuedRequests, queuedRequestsDelay): for registerDefinition in registerDefinitions: - if not 'readSchedule' in registerDefinition or registerDefinition['readSchedule'] == 'init': - continue + + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue + propertyName = registerDefinition['id'] - propertyTyp = getCppDataType(registerDefinition) writeLine(fileDescriptor, 'void %s::update%s()' % (className, propertyName[0].upper() + propertyName[1:])) writeLine(fileDescriptor, '{') writeLine(fileDescriptor, ' // Update registers from %s' % registerDefinition['description']) writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read \\"%s\\" register:" << %s << "size:" << %s;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' QModbusReply *reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' if (!reply) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' return;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (reply->isFinished()) {') - writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' return;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') - 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, ' }') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){') - writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') - writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' });') + + if queuedRequests: + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + writeLine(fileDescriptor, ' if (m_currentInitReply)') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_currentInitReply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (!m_currentInitReply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" init register from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentInitReply->isFinished()) {') + writeLine(fileDescriptor, ' m_currentInitReply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' m_currentInitReply = nullptr;') + writeLine(fileDescriptor, ' if (!verifyInitFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedInitRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(m_currentInitReply, &QModbusReply::finished, m_currentInitReply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(m_currentInitReply, &QModbusReply::finished, this, [this](){') + writeLine(fileDescriptor, ' handleModbusError(m_currentInitReply->error());') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentInitReply->error() != QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' QModbusResponse response = m_currentInitReply->rawResult();') + writeLine(fileDescriptor, ' if (m_currentInitReply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating init \\"%s\\" registers" << m_currentInitReply->error() << m_currentInitReply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating init \\"%s\\" registers" << m_currentInitReply->error() << m_currentInitReply->errorString();' % (className, blockName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' m_currentInitReply = nullptr;') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + 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) + writeLine(fileDescriptor, ' if (!verifyInitFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedInitRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + else: + writeLine(fileDescriptor, ' if (m_currentUpdateReply)') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_currentUpdateReply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (!m_currentUpdateReply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' if (!verifyUpdateFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentUpdateReply->isFinished()) {') + 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, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(m_currentUpdateReply, &QModbusReply::finished, this, [this](){') + writeLine(fileDescriptor, ' handleModbusError(m_currentUpdateReply->error());') + 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, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_currentUpdateReply->deleteLater();') + 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) + writeLine(fileDescriptor, ' connect(m_currentUpdateReply, &QModbusReply::errorOccurred, this, [this] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = m_currentUpdateReply->rawResult();') + writeLine(fileDescriptor, ' if (m_currentUpdateReply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << m_currentUpdateReply->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + else: + writeLine(fileDescriptor, ' QModbusReply *reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (!reply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (reply->isFinished()) {') + writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') + 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, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor, '}') writeLine(fileDescriptor) ############################################################## -def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDefinitions): +def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDefinitions, queuedRequests, queuedRequestsDelay): 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'] @@ -147,48 +251,162 @@ def writeBlockUpdateMethodImplementationsTcp(fileDescriptor, className, blockDef writeLine(fileDescriptor, '{') writeLine(fileDescriptor, ' // Update register block \"%s\"' % blockName) writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read block \\"%s\\" registers from:" << %s << "size:" << %s;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' QModbusReply *reply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) - writeLine(fileDescriptor, ' if (!reply) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) - writeLine(fileDescriptor, ' return;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (reply->isFinished()) {') - writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' return;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') - writeLine(fileDescriptor, ' handleModbusError(reply->error());') - 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'] - propertyTyp = getCppDataType(blockRegister) - writeLine(fileDescriptor, ' process%sRegisterValues(blockValues.mid(%s, %s));' % (propertyName[0].upper() + propertyName[1:], offset, blockRegister['size'])) - offset += blockRegister['size'] + if queuedRequests: + + if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'init': + + writeLine(fileDescriptor, ' m_currentInitReply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, ' if (!m_currentInitReply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading init block \\"%s\\" registers";' % (className, blockName)) + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentInitReply->isFinished()) {') + writeLine(fileDescriptor, ' m_currentInitReply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' m_currentInitReply = nullptr;') + writeLine(fileDescriptor, ' if (!verifyInitFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedInitRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(m_currentInitReply, &QModbusReply::finished, m_currentInitReply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(m_currentInitReply, &QModbusReply::finished, this, [this](){') + writeLine(fileDescriptor, ' handleModbusError(m_currentInitReply->error());') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentInitReply->error() != QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' QModbusResponse response = m_currentInitReply->rawResult();') + writeLine(fileDescriptor, ' if (m_currentInitReply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating init block \\"%s\\" registers" << m_currentInitReply->error() << m_currentInitReply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating init block \\"%s\\" registers" << m_currentInitReply->error() << m_currentInitReply->errorString();' % (className, blockName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' m_currentInitReply = nullptr;') + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' const QModbusDataUnit unit = m_currentInitReply->result();') + 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) + writeLine(fileDescriptor, ' if (!verifyInitFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedInitRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + else: + writeLine(fileDescriptor, ' m_currentUpdateReply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, ' if (!m_currentUpdateReply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) + writeLine(fileDescriptor, ' if (!verifyUpdateFinished())') + writeLine(fileDescriptor, ' QTimer::singleShot(%s, this, &%s::sendNextQueuedRequest);' % (queuedRequestsDelay, className)) + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentUpdateReply->isFinished()) {') + 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, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(m_currentUpdateReply, &QModbusReply::finished, this, [this](){') + writeLine(fileDescriptor, ' handleModbusError(m_currentUpdateReply->error());') + 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) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(m_currentUpdateReply, &QModbusReply::errorOccurred, this, [this] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = m_currentUpdateReply->rawResult();') + writeLine(fileDescriptor, ' if (m_currentUpdateReply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << m_currentUpdateReply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << m_currentUpdateReply->errorString();' % (className, blockName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + else: + writeLine(fileDescriptor, ' QModbusReply *reply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, ' if (!reply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (reply->isFinished()) {') + writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') + writeLine(fileDescriptor, ' handleModbusError(reply->error());') + 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, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString();' % (className, blockName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') - 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, ' });') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){') - writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') - writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString();' % (className, blockName)) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' });') writeLine(fileDescriptor, '}') writeLine(fileDescriptor) @@ -199,6 +417,7 @@ def writeInternalPropertyReadMethodDeclarationsTcp(fileDescriptor, registerDefin propertyName = registerDefinition['id'] writeLine(fileDescriptor, ' QModbusReply *read%s();' % (propertyName[0].upper() + propertyName[1:])) +############################################################## def writeInternalPropertyReadMethodImplementationsTcp(fileDescriptor, className, registerDefinitions): for registerDefinition in registerDefinitions: @@ -251,6 +470,7 @@ def writeInternalBlockReadMethodDeclarationsTcp(fileDescriptor, blockDefinitions writeLine(fileDescriptor, ' QModbusReply *readBlock%s();' % (blockName[0].upper() + blockName[1:])) writeLine(fileDescriptor) +############################################################## def writeInternalBlockReadMethodImplementationsTcp(fileDescriptor, className, blockDefinitions): for blockDefinition in blockDefinitions: @@ -294,7 +514,6 @@ def writeInternalBlockReadMethodImplementationsTcp(fileDescriptor, className, bl def writeTestReachabilityImplementationsTcp(fileDescriptor, className, registerDefinitions, checkReachableRegister): propertyName = checkReachableRegister['id'] - propertyTyp = getCppDataType(checkReachableRegister) writeLine(fileDescriptor, 'void %s::testReachability()' % (className)) writeLine(fileDescriptor, '{') @@ -342,7 +561,7 @@ def writeTestReachabilityImplementationsTcp(fileDescriptor, className, registerD ############################################################## -def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefinitions, blockDefinitions): +def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefinitions, blockDefinitions, queuedRequests): writeLine(fileDescriptor, 'bool %s::initialize()' % (className)) writeLine(fileDescriptor, '{') writeLine(fileDescriptor, ' if (!m_reachable) {') @@ -363,154 +582,186 @@ def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefiniti break if initRequired: - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (m_initObject) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Tried to initialize but the init process is already running.";' % className) - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' // Parent object for the init process') - writeLine(fileDescriptor, ' m_initObject = new QObject(this);') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' QModbusReply *reply = nullptr;') - # Read individual registers - for registerDefinition in registerDefinitions: - propertyName = registerDefinition['id'] - propertyTyp = getCppDataType(registerDefinition) + if queuedRequests: + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (!m_modbusTcpMaster->connected()) {') + writeLine(fileDescriptor, ' m_initRequestQueue.clear();') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_initializing = true;') + writeLine(fileDescriptor) - if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' // Read %s' % registerDefinition['description']) - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read init \\"%s\\" register:" << %s << "size:" << %s;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' if (!reply) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' finishInitialization(false);') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (reply->isFinished()) {') - writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' m_pendingInitReplies.append(reply);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, m_initObject, [this, reply](){') - writeLine(fileDescriptor, ' handleModbusError(reply->error());') - writeLine(fileDescriptor, ' m_pendingInitReplies.removeAll(reply);') - writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') - writeLine(fileDescriptor, ' finishInitialization(false);') - writeLine(fileDescriptor, ' return;') - 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, ' verifyInitFinished();') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, m_initObject, [this, reply] (QModbusDevice::Error error){') - writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') - writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' });') + # Read individual registers + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + writeLine(fileDescriptor, ' enqueueInitRequest(&%s::update%s);' % (className, propertyName[0].upper() + propertyName[1:])) - # Read init blocks - for blockDefinition in blockDefinitions: - blockName = blockDefinition['id'] - blockRegisters = blockDefinition['registers'] + # Read init blocks + writeLine(fileDescriptor) + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'init': + writeLine(fileDescriptor, ' enqueueInitRequest(&%s::update%sBlock);' % (className, blockName[0].upper() + blockName[1:])) - if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'init': - blockStartAddress = 0 - registerCount = 0 - blockSize = 0 - registerType = "" + writeLine(fileDescriptor, ' sendNextQueuedInitRequest();'); - for i, blockRegister in enumerate(blockRegisters): - if i == 0: - blockStartAddress = blockRegister['address'] - registerType = blockRegister['registerType'] + else: + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_initObject) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Tried to initialize but the init process is already running.";' % className) + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_initializing = true;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' // Parent object for the init process') + writeLine(fileDescriptor, ' m_initObject = new QObject(this);') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' QModbusReply *reply = nullptr;') - registerCount += 1 - blockSize += blockRegister['size'] + # Read individual registers + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' // Read %s' % blockName) - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read init block \\"%s\\" registers from:" << %s << "size:" << %s;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' reply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) - writeLine(fileDescriptor, ' if (!reply) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) - writeLine(fileDescriptor, ' finishInitialization(false);') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (reply->isFinished()) {') - writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' m_pendingInitReplies.append(reply);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, m_initObject, [this, reply](){') - writeLine(fileDescriptor, ' m_pendingInitReplies.removeAll(reply);') - writeLine(fileDescriptor, ' handleModbusError(reply->error());') - writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') - writeLine(fileDescriptor, ' finishInitialization(false);') - writeLine(fileDescriptor, ' return;') - writeLine(fileDescriptor, ' }') - 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)) + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' // Read %s' % registerDefinition['description']) + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read init \\"%s\\" register:" << %s << "size:" << %s;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + writeLine(fileDescriptor, ' reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (!reply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (reply->isFinished()) {') + writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_pendingInitReplies.append(reply);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, m_initObject, [this, reply](){') + writeLine(fileDescriptor, ' handleModbusError(reply->error());') + writeLine(fileDescriptor, ' m_pendingInitReplies.removeAll(reply);') + writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return;') + 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, ' verifyInitFinished();') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, m_initObject, [this, reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') - # 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'] + # Read init blocks + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] - 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, ' verifyInitFinished();') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, m_initObject, [reply] (QModbusDevice::Error error){') - writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') - writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString();' % (className, blockName)) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) + if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'init': + 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) + writeLine(fileDescriptor, ' // Read %s' % blockName) + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read init block \\"%s\\" registers from:" << %s << "size:" << %s;' % (className, blockName, blockStartAddress, blockSize)) + writeLine(fileDescriptor, ' reply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, ' if (!reply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (reply->isFinished()) {') + writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_pendingInitReplies.append(reply);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, m_initObject, [this, reply](){') + writeLine(fileDescriptor, ' m_pendingInitReplies.removeAll(reply);') + writeLine(fileDescriptor, ' handleModbusError(reply->error());') + writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' finishInitialization(false);') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + 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, ' verifyInitFinished();') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, m_initObject, [reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString();' % (className, blockName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) else: writeLine(fileDescriptor, ' // No init registers defined. Nothing to be done and we are finished.') + writeLine(fileDescriptor, ' m_initializing = false;') writeLine(fileDescriptor, ' emit initializationFinished(true);') writeLine(fileDescriptor, ' return true;') writeLine(fileDescriptor, '}') writeLine(fileDescriptor) +############################################################## -def writeUpdateMethodTcp(fileDescriptor, className, registerDefinitions, blockDefinitions): +def writeUpdateMethodTcp(fileDescriptor, className, registerDefinitions, blockDefinitions, queuedRequests): writeLine(fileDescriptor, 'bool %s::update()' % (className)) writeLine(fileDescriptor, '{') - # First check if there are any init registers + # First check if there are any update registers updateRequired = False for registerDefinition in registerDefinitions: if registerDefinition['readSchedule'] == 'update': @@ -523,137 +774,167 @@ def writeUpdateMethodTcp(fileDescriptor, className, registerDefinitions, blockDe break if updateRequired: - writeLine(fileDescriptor, ' if (!m_modbusTcpMaster->connected())') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (!m_pendingUpdateReplies.isEmpty()) {') - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "Tried to update but there are still some update replies pending. Waiting for them to be finished...";' % className) - writeLine(fileDescriptor, ' return true;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' QModbusReply *reply = nullptr;') + if queuedRequests: + writeLine(fileDescriptor, ' if (!m_modbusTcpMaster->connected()) {') + writeLine(fileDescriptor, ' m_updateRequestQueue.clear();') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) - # Read individual registers - for registerDefinition in registerDefinitions: - propertyName = registerDefinition['id'] - propertyTyp = getCppDataType(registerDefinition) + writeLine(fileDescriptor, ' if (!m_updateRequestQueue.isEmpty()) {') + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "Tried to update but there are still some update requests pending. Waiting for them to be finished..." << m_updateRequestQueue.count();' % className) + writeLine(fileDescriptor, ' return true;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) - if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'update': - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' // Read %s' % registerDefinition['description']) - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read \\"%s\\" register:" << %s << "size:" << %s;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) - writeLine(fileDescriptor, ' reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) - writeLine(fileDescriptor, ' if (!reply) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (reply->isFinished()) {') - writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' m_pendingUpdateReplies.append(reply);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') - writeLine(fileDescriptor, ' m_pendingUpdateReplies.removeAll(reply);') - writeLine(fileDescriptor, ' handleModbusError(reply->error());') - writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') - writeLine(fileDescriptor, ' verifyUpdateFinished();') - writeLine(fileDescriptor, ' return;') - 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, ' verifyUpdateFinished();') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){') - writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') - writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' });') + # Read individual registers + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'update': + writeLine(fileDescriptor, ' enqueueRequest(&%s::update%s);' % (className, propertyName[0].upper() + propertyName[1:])) - # Read init blocks - for blockDefinition in blockDefinitions: - blockName = blockDefinition['id'] - blockRegisters = blockDefinition['registers'] + # Read init blocks + writeLine(fileDescriptor) + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'update': + writeLine(fileDescriptor, ' enqueueRequest(&%s::update%sBlock);' % (className, blockName[0].upper() + blockName[1:])) - if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'update': - blockStartAddress = 0 - registerCount = 0 - blockSize = 0 - registerType = "" + writeLine(fileDescriptor, ' sendNextQueuedRequest();'); + else: + writeLine(fileDescriptor, ' if (!m_modbusTcpMaster->connected())') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor) - for i, blockRegister in enumerate(blockRegisters): - if i == 0: - blockStartAddress = blockRegister['address'] - registerType = blockRegister['registerType'] + writeLine(fileDescriptor, ' if (!m_pendingUpdateReplies.isEmpty()) {') + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "Tried to update but there are still some update replies pending. Waiting for them to be finished...";' % className) + writeLine(fileDescriptor, ' return true;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' QModbusReply *reply = nullptr;') - registerCount += 1 - blockSize += blockRegister['size'] + # Read individual registers + for registerDefinition in registerDefinitions: + propertyName = registerDefinition['id'] + propertyTyp = getCppDataType(registerDefinition) - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' // Read %s' % blockName) - writeLine(fileDescriptor, ' reply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) - writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read block \\"%s\\" registers from:" << %s << "size:" << %s;' % (className, blockName, blockStartAddress, blockSize)) - writeLine(fileDescriptor, ' if (!reply) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' if (reply->isFinished()) {') - writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') - writeLine(fileDescriptor, ' return false;') - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' m_pendingUpdateReplies.append(reply);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') - writeLine(fileDescriptor, ' m_pendingUpdateReplies.removeAll(reply);') - writeLine(fileDescriptor, ' handleModbusError(reply->error());') - writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') - writeLine(fileDescriptor, ' verifyUpdateFinished();') - writeLine(fileDescriptor, ' return;') - writeLine(fileDescriptor, ' }') - 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)) + if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'update': + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' // Read %s' % registerDefinition['description']) + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read \\"%s\\" register:" << %s << "size:" << %s;' % (className, registerDefinition['description'], registerDefinition['address'], registerDefinition['size'])) + writeLine(fileDescriptor, ' reply = read%s();' % (propertyName[0].upper() + propertyName[1:])) + writeLine(fileDescriptor, ' if (!reply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << m_modbusTcpMaster->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (reply->isFinished()) {') + writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_pendingUpdateReplies.append(reply);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') + writeLine(fileDescriptor, ' m_pendingUpdateReplies.removeAll(reply);') + writeLine(fileDescriptor, ' handleModbusError(reply->error());') + writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' verifyUpdateFinished();') + writeLine(fileDescriptor, ' return;') + 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, ' verifyUpdateFinished();') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [this, reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while reading \\"%s\\" registers from" << m_modbusTcpMaster->hostAddress().toString() << error << reply->errorString();' % (className, registerDefinition['description'])) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') - # 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'] + # Read init blocks + for blockDefinition in blockDefinitions: + blockName = blockDefinition['id'] + blockRegisters = blockDefinition['registers'] - 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, ' }') + if 'readSchedule' in blockDefinition and blockDefinition['readSchedule'] == 'update': + blockStartAddress = 0 + registerCount = 0 + blockSize = 0 + registerType = "" - writeLine(fileDescriptor, ' verifyUpdateFinished();') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) - writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){') - writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') - writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) - writeLine(fileDescriptor, ' } else {') - writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString();' % (className, blockName)) - writeLine(fileDescriptor, ' }') - writeLine(fileDescriptor, ' });') - writeLine(fileDescriptor) + for i, blockRegister in enumerate(blockRegisters): + if i == 0: + blockStartAddress = blockRegister['address'] + registerType = blockRegister['registerType'] + + registerCount += 1 + blockSize += blockRegister['size'] + + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' // Read %s' % blockName) + writeLine(fileDescriptor, ' reply = readBlock%s();' % (blockName[0].upper() + blockName[1:])) + writeLine(fileDescriptor, ' qCDebug(dc%s()) << "--> Read block \\"%s\\" registers from:" << %s << "size:" << %s;' % (className, blockName, blockStartAddress, blockSize)) + writeLine(fileDescriptor, ' if (!reply) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Error occurred while reading block \\"%s\\" registers";' % (className, blockName)) + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (reply->isFinished()) {') + writeLine(fileDescriptor, ' reply->deleteLater(); // Broadcast reply returns immediatly') + writeLine(fileDescriptor, ' return false;') + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_pendingUpdateReplies.append(reply);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);') + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::finished, this, [this, reply](){') + writeLine(fileDescriptor, ' m_pendingUpdateReplies.removeAll(reply);') + writeLine(fileDescriptor, ' handleModbusError(reply->error());') + writeLine(fileDescriptor, ' if (reply->error() != QModbusDevice::NoError) {') + writeLine(fileDescriptor, ' verifyUpdateFinished();') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor, ' }') + 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, ' verifyUpdateFinished();') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' connect(reply, &QModbusReply::errorOccurred, this, [reply] (QModbusDevice::Error error){') + writeLine(fileDescriptor, ' QModbusResponse response = reply->rawResult();') + writeLine(fileDescriptor, ' if (reply->error() == QModbusDevice::ProtocolError && response.isException()) {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString() << ModbusDataUtils::exceptionCodeToString(response.exceptionCode());' % (className, blockName)) + writeLine(fileDescriptor, ' } else {') + writeLine(fileDescriptor, ' qCWarning(dc%s()) << "Modbus reply error occurred while updating block \\"%s\\" registers" << error << reply->errorString();' % (className, blockName)) + writeLine(fileDescriptor, ' }') + writeLine(fileDescriptor, ' });') + writeLine(fileDescriptor) else: writeLine(fileDescriptor, ' // No update registers defined. Nothing to be done and we are finished.') diff --git a/libnymea-modbus/tools/connectiontool/toolcommon.py b/libnymea-modbus/tools/connectiontool/toolcommon.py index 9de1eda..b6a4c16 100644 --- a/libnymea-modbus/tools/connectiontool/toolcommon.py +++ b/libnymea-modbus/tools/connectiontool/toolcommon.py @@ -219,6 +219,12 @@ def getCppDataType(registerDefinition, rawType = False): if registerDefinition['type'] == 'string': return 'QString' + if registerDefinition['type'] == 'bytearray': + return 'QByteArray' + + if registerDefinition['type'] == 'raw': + return 'QVector' + def getConversionToValueMethod(registerDefinition): # Handle enums @@ -335,6 +341,10 @@ def getValueConversionMethod(registerDefinition): return ('ModbusDataUtils::convertToFloat64(values, m_endianness)') elif registerDefinition['type'] == 'string': return ('ModbusDataUtils::convertToString(values, m_stringEndianness)') + elif registerDefinition['type'] == 'bytearray': + return ('ModbusDataUtils::convertToByteArray(values)') + elif registerDefinition['type'] == 'raw': + return ('values') def writeBlockGetMethodDeclarations(fileDescriptor, registerDefinitions): @@ -352,11 +362,11 @@ def writeBlockGetMethodDeclarations(fileDescriptor, registerDefinitions): def writePropertyUpdateMethodDeclarations(fileDescriptor, registerDefinitions): for registerDefinition in registerDefinitions: - if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init': - continue + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue propertyName = registerDefinition['id'] - propertyTyp = getCppDataType(registerDefinition) writeLine(fileDescriptor, ' void update%s();' % (propertyName[0].upper() + propertyName[1:])) @@ -427,12 +437,23 @@ def writeBlocksUpdateMethodDeclarations(fileDescriptor, blockDefinitions): def writeRegistersDebugLine(fileDescriptor, debugObjectParamName, registerDefinitions): for registerDefinition in registerDefinitions: - if not 'R' in registerDefinition['access']: - continue + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue propertyName = registerDefinition['id'] - propertyTyp = getCppDataType(registerDefinition) - line = ('" - %s - %s: " << %s->%s()' % (registerDefinition['address'], registerDefinition['description'], debugObjectParamName, propertyName)) + registerType = registerDefinition['registerType'] + typeString = '' + if registerType == 'holdingRegister': + typeString = 'holding ' + elif registerType == 'inputRegister': + typeString = 'input ' + elif registerType == 'coils': + typeString = 'coils ' + elif registerType == 'discreteInputs': + typeString = 'discrete' + + line = ('" - %s %s | %s: " << %s->%s()' % (typeString, registerDefinition['address'], registerDefinition['description'], debugObjectParamName, propertyName)) if 'unit' in registerDefinition and registerDefinition['unit'] != '': line += (' << " [%s]"' % registerDefinition['unit']) writeLine(fileDescriptor, ' debug.nospace().noquote() << %s << "\\n";' % (line)) @@ -440,8 +461,9 @@ def writeRegistersDebugLine(fileDescriptor, debugObjectParamName, registerDefini def writePropertyChangedSignals(fileDescriptor, registerDefinitions): for registerDefinition in registerDefinitions: - if not 'R' in registerDefinition['access']: - continue + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue propertyName = registerDefinition['id'] propertyTyp = getCppDataType(registerDefinition) @@ -455,8 +477,9 @@ def writePropertyChangedSignals(fileDescriptor, registerDefinitions): def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions): for registerDefinition in registerDefinitions: - if not 'R' in registerDefinition['access']: - continue + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue propertyName = registerDefinition['id'] propertyTyp = getCppDataType(registerDefinition) @@ -467,10 +490,10 @@ def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions): def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions): - propertyVariables = [] for registerDefinition in registerDefinitions: - if not 'R' in registerDefinition['access']: - continue + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue propertyName = registerDefinition['id'] writeLine(fileDescriptor, ' void process%sRegisterValues(const QVector &values);' % (propertyName[0].upper() + propertyName[1:])) @@ -479,10 +502,10 @@ def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions): def writePropertyProcessMethodImplementations(fileDescriptor, className, registerDefinitions): - propertyVariables = [] for registerDefinition in registerDefinitions: - if not 'R' in registerDefinition['access']: - continue + if 'access' in registerDefinition: + if not 'R' in registerDefinition['access']: + continue propertyTyp = getCppDataType(registerDefinition) propertyName = registerDefinition['id'] @@ -498,3 +521,55 @@ def writePropertyProcessMethodImplementations(fileDescriptor, className, registe writeLine(fileDescriptor, ' }') writeLine(fileDescriptor, '}') writeLine(fileDescriptor) + + +def writeSendNextQueuedInitRequestMethodImplementation(fileDescriptor, className): + writeLine(fileDescriptor, 'void %s::sendNextQueuedInitRequest()' % (className)) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' if (m_initRequestQueue.isEmpty())') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentInitReply)') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' %s::Function function = m_initRequestQueue.dequeue();' % (className)) + writeLine(fileDescriptor, ' (this->*function)();') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writeEnqueueInitRequestMethodImplementation(fileDescriptor, className): + writeLine(fileDescriptor, 'void %s::enqueueInitRequest(%s::Function function)' % (className, className)) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' if (m_initRequestQueue.contains(function))') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_initRequestQueue.enqueue(function);') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writeSendNextQueuedRequestMethodImplementation(fileDescriptor, className): + writeLine(fileDescriptor, 'void %s::sendNextQueuedRequest()' % (className)) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' if (m_updateRequestQueue.isEmpty())') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' if (m_currentUpdateReply)') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' %s::Function function = m_updateRequestQueue.dequeue();' % (className)) + writeLine(fileDescriptor, ' (this->*function)();') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) + + +def writeEnqueueRequestMethodImplementation(fileDescriptor, className): + writeLine(fileDescriptor, 'void %s::enqueueRequest(%s::Function function)' % (className, className)) + writeLine(fileDescriptor, '{') + writeLine(fileDescriptor, ' if (m_updateRequestQueue.contains(function))') + writeLine(fileDescriptor, ' return;') + writeLine(fileDescriptor) + writeLine(fileDescriptor, ' m_updateRequestQueue.enqueue(function);') + writeLine(fileDescriptor, '}') + writeLine(fileDescriptor) \ No newline at end of file diff --git a/libnymea-modbus/tools/generate-connection.py b/libnymea-modbus/tools/generate-connection.py index 1b9541f..d3208eb 100644 --- a/libnymea-modbus/tools/generate-connection.py +++ b/libnymea-modbus/tools/generate-connection.py @@ -62,6 +62,10 @@ def writeTcpHeaderFile(): for enumDefinition in registerJson['enums']: writeEnumDefinition(headerFile, enumDefinition) + if queuedRequests: + writeLine(headerFile, ' typedef void(%s::*Function)(void);' % className) + writeLine(headerFile) + # Constructor writeLine(headerFile, ' explicit %s(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr);' % className) writeLine(headerFile, ' explicit %s(ModbusTcpMaster *modbusTcpMaster, quint16 slaveId, QObject *parent = nullptr);' % className) @@ -71,7 +75,14 @@ def writeTcpHeaderFile(): writeLine(headerFile, ' quint16 slaveId() const;') writeLine(headerFile) writeLine(headerFile, ' bool reachable() const;') + writeLine(headerFile, ' bool initializing() const;') writeLine(headerFile) + + # Write init and update method declarations + writeLine(headerFile, ' virtual bool initialize();') + writeLine(headerFile, ' virtual bool update();') + writeLine(headerFile) + writeLine(headerFile, ' ModbusDataUtils::ByteOrder endianness() const;') writeLine(headerFile, ' void setEndianness(ModbusDataUtils::ByteOrder endianness);') writeLine(headerFile) @@ -109,11 +120,6 @@ def writeTcpHeaderFile(): writeLine(headerFile) - # Write init and update method declarations - writeLine(headerFile, ' virtual bool initialize();') - writeLine(headerFile, ' virtual bool update();') - writeLine(headerFile) - writeLine(headerFile, 'public slots:') writeLine(headerFile, ' bool connectDevice();') writeLine(headerFile, ' void disconnectDevice();') @@ -166,7 +172,16 @@ def writeTcpHeaderFile(): writeLine(headerFile, ' ModbusDataUtils::ByteOrder m_stringEndianness = ModbusDataUtils::ByteOrder%s;' % stringEndianness) writeLine(headerFile, ' quint16 m_slaveId = 1;') writeLine(headerFile) + + if queuedRequests: + writeLine(headerFile, ' QModbusReply *m_currentInitReply = nullptr;') + writeLine(headerFile, ' QQueue<%s::Function> m_initRequestQueue;' % className) + writeLine(headerFile, ' QModbusReply *m_currentUpdateReply = nullptr;') + writeLine(headerFile, ' QQueue<%s::Function> m_updateRequestQueue;' % className) + writeLine(headerFile) + writeLine(headerFile, ' bool m_reachable = false;') + writeLine(headerFile, ' bool m_initializing = false;') writeLine(headerFile, ' QModbusReply *m_checkRechableReply = nullptr;') writeLine(headerFile, ' uint m_checkReachableRetries = 0;') writeLine(headerFile, ' uint m_checkReachableRetriesCount = 0;') @@ -178,16 +193,24 @@ def writeTcpHeaderFile(): writeLine(headerFile, ' QVector m_pendingUpdateReplies;') writeLine(headerFile) writeLine(headerFile, ' QObject *m_initObject = nullptr;') - writeLine(headerFile, ' void verifyInitFinished();') + writeLine(headerFile, ' bool verifyInitFinished();') writeLine(headerFile, ' void finishInitialization(bool success);') writeLine(headerFile) + writeLine(headerFile, ' void setupConnection();') writeLine(headerFile) - writeLine(headerFile, ' void verifyUpdateFinished();') + writeLine(headerFile, ' bool verifyUpdateFinished();') writeLine(headerFile) writeLine(headerFile, ' void onReachabilityCheckFailed();') writeLine(headerFile, ' void evaluateReachableState();') + if queuedRequests: + writeLine(headerFile) + writeLine(headerFile, ' void sendNextQueuedInitRequest();') + writeLine(headerFile, ' void enqueueInitRequest(%s::Function function);' % (className)) + writeLine(headerFile, ' void sendNextQueuedRequest();') + writeLine(headerFile, ' void enqueueRequest(%s::Function function);' % (className)) + # End of class writeLine(headerFile) writeLine(headerFile, '};') @@ -205,6 +228,7 @@ def writeTcpSourceFile(): writeLicenseHeader(sourceFile) writeLine(sourceFile) writeLine(sourceFile, '#include "%s"' % headerFileName) + writeLine(sourceFile) writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') @@ -251,6 +275,12 @@ def writeTcpSourceFile(): writeLine(sourceFile, '}') writeLine(sourceFile) + writeLine(sourceFile, 'bool %s::initializing() const' % (className)) + writeLine(sourceFile, '{') + writeLine(sourceFile, ' return m_initializing;') + writeLine(sourceFile, '}') + writeLine(sourceFile) + writeLine(sourceFile, 'uint %s::checkReachableRetries() const' % (className)) writeLine(sourceFile, '{') writeLine(sourceFile, ' return m_checkReachableRetries;') @@ -310,8 +340,8 @@ def writeTcpSourceFile(): if 'blocks' in registerJson: blocks = registerJson['blocks'] - writeInitMethodImplementationTcp(sourceFile, className, registerJson['registers'], blocks) - writeUpdateMethodTcp(sourceFile, className, registerJson['registers'], blocks) + writeInitMethodImplementationTcp(sourceFile, className, registerJson['registers'], blocks, queuedRequests) + writeUpdateMethodTcp(sourceFile, className, registerJson['registers'], blocks, queuedRequests) writeLine(sourceFile, 'bool %s::connectDevice()' % (className)) writeLine(sourceFile, '{') @@ -332,13 +362,13 @@ def writeTcpSourceFile(): writeLine(sourceFile) # Write update methods - writePropertyUpdateMethodImplementationsTcp(sourceFile, className, registerJson['registers']) + writePropertyUpdateMethodImplementationsTcp(sourceFile, className, registerJson['registers'], queuedRequests, queuedRequestsDelay) if 'blocks' in registerJson: for blockDefinition in registerJson['blocks']: - writePropertyUpdateMethodImplementationsTcp(sourceFile, className, blockDefinition['registers']) + writePropertyUpdateMethodImplementationsTcp(sourceFile, className, blockDefinition['registers'], queuedRequests, queuedRequestsDelay) # Write block update method - writeBlockUpdateMethodImplementationsTcp(sourceFile, className, registerJson['blocks']) + writeBlockUpdateMethodImplementationsTcp(sourceFile, className, registerJson['blocks'], queuedRequests, queuedRequestsDelay) # Write internal protected property read method implementations writeInternalPropertyReadMethodImplementationsTcp(sourceFile, className, registerJson['registers']) @@ -377,11 +407,19 @@ def writeTcpSourceFile(): writeTestReachabilityImplementationsTcp(sourceFile, className, registerJson['registers'], checkReachableRegister) - writeLine(sourceFile, 'void %s::verifyInitFinished()' % (className)) + writeLine(sourceFile, 'bool %s::verifyInitFinished()' % (className)) writeLine(sourceFile, '{') - writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {') - writeLine(sourceFile, ' finishInitialization(true);') - writeLine(sourceFile, ' }') + if queuedRequests: + writeLine(sourceFile, ' if (m_initRequestQueue.isEmpty() && !m_currentInitReply) {') + writeLine(sourceFile, ' finishInitialization(true);') + writeLine(sourceFile, ' return true;') + writeLine(sourceFile, ' }') + else: + writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {') + writeLine(sourceFile, ' finishInitialization(true);') + writeLine(sourceFile, ' return true;') + writeLine(sourceFile, ' }') + writeLine(sourceFile, ' return false;') writeLine(sourceFile, '}') writeLine(sourceFile) @@ -393,12 +431,20 @@ def writeTcpSourceFile(): writeLine(sourceFile, ' qCWarning(dc%s()) << "Initialization finished of %s" << m_modbusTcpMaster->hostAddress().toString() << "failed.";' % (className, className)) writeLine(sourceFile, ' }') writeLine(sourceFile) - writeLine(sourceFile, ' // Cleanup init') - writeLine(sourceFile, ' delete m_initObject;') - writeLine(sourceFile, ' m_initObject = nullptr;') - writeLine(sourceFile, ' m_pendingInitReplies.clear();') + writeLine(sourceFile, ' m_initializing = false;') + + if queuedRequests: + writeLine(sourceFile, ' m_initRequestQueue.clear();') + else: + writeLine(sourceFile, ' // Cleanup init') + writeLine(sourceFile, ' delete m_initObject;') + writeLine(sourceFile, ' m_initObject = nullptr;') + writeLine(sourceFile, ' m_pendingInitReplies.clear();') + writeLine(sourceFile) - writeLine(sourceFile, ' emit initializationFinished(success);') + writeLine(sourceFile, ' QTimer::singleShot(0, this, [this, success](){') + writeLine(sourceFile, ' emit initializationFinished(success);') + writeLine(sourceFile, ' });') writeLine(sourceFile, '}') writeLine(sourceFile) @@ -410,6 +456,10 @@ def writeTcpSourceFile(): writeLine(sourceFile, ' // Cleanup before starting to initialize') writeLine(sourceFile, ' m_pendingInitReplies.clear();') writeLine(sourceFile, ' m_pendingUpdateReplies.clear();') + if queuedRequests: + writeLine(sourceFile, ' m_updateRequestQueue.clear();') + writeLine(sourceFile, ' m_initRequestQueue.clear();') + writeLine(sourceFile, ' m_communicationWorking = false;') writeLine(sourceFile, ' m_communicationFailedCounter = 0;') writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;') @@ -419,6 +469,12 @@ def writeTcpSourceFile(): writeLine(sourceFile, ' m_communicationWorking = false;') writeLine(sourceFile, ' m_communicationFailedCounter = 0;') writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;') + writeLine(sourceFile, ' m_initializing = false;') + + if queuedRequests: + writeLine(sourceFile, ' m_updateRequestQueue.clear();') + writeLine(sourceFile, ' m_initRequestQueue.clear();') + writeLine(sourceFile, ' }') writeLine(sourceFile) writeLine(sourceFile, ' evaluateReachableState();') @@ -426,11 +482,18 @@ def writeTcpSourceFile(): writeLine(sourceFile, '}') writeLine(sourceFile) - writeLine(sourceFile, 'void %s::verifyUpdateFinished()' % (className)) + writeLine(sourceFile, 'bool %s::verifyUpdateFinished()' % (className)) writeLine(sourceFile, '{') - writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {') - writeLine(sourceFile, ' emit updateFinished();') + if queuedRequests: + writeLine(sourceFile, ' if (m_updateRequestQueue.isEmpty() && !m_currentUpdateReply) {') + writeLine(sourceFile, ' emit updateFinished();') + writeLine(sourceFile, ' return true;') + else: + writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {') + writeLine(sourceFile, ' emit updateFinished();') + writeLine(sourceFile, ' return true;') writeLine(sourceFile, ' }') + writeLine(sourceFile, ' return false;') writeLine(sourceFile, '}') writeLine(sourceFile) @@ -461,7 +524,11 @@ def writeTcpSourceFile(): writeLine(sourceFile, '}') writeLine(sourceFile) - + if queuedRequests: + writeSendNextQueuedInitRequestMethodImplementation(sourceFile, className) + writeEnqueueInitRequestMethodImplementation(sourceFile, className) + writeSendNextQueuedRequestMethodImplementation(sourceFile, className) + writeEnqueueRequestMethodImplementation(sourceFile, className) # Write the debug print debugObjectParamName = className[0].lower() + className[1:] @@ -622,7 +689,7 @@ def writeRtuHeaderFile(): writeLine(headerFile, ' void verifyInitFinished();') writeLine(headerFile, ' void finishInitialization(bool success);') writeLine(headerFile) - writeLine(headerFile, ' void verifyUpdateFinished();') + writeLine(headerFile, ' bool verifyUpdateFinished();') writeLine(headerFile) writeLine(headerFile, ' void onReachabilityCheckFailed();') writeLine(headerFile, ' void evaluateReachableState();') @@ -645,6 +712,7 @@ def writeRtuSourceFile(): writeLicenseHeader(sourceFile) writeLine(sourceFile, '#include "%s"' % headerFileName) + writeLine(sourceFile) writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') @@ -831,15 +899,26 @@ def writeRtuSourceFile(): writeLine(sourceFile, ' m_initObject = nullptr;') writeLine(sourceFile, ' m_pendingInitReplies.clear();') writeLine(sourceFile) - writeLine(sourceFile, ' emit initializationFinished(success);') + writeLine(sourceFile, ' QTimer::singleShot(0, this, [this, success](){') + writeLine(sourceFile, ' emit initializationFinished(success);') + writeLine(sourceFile, ' });') + + if queuedRequests: + writeLine(sourceFile) + writeLine(sourceFile, ' m_pendingInitReplies.clear();') + writeLine(sourceFile, ' m_pendingUpdateReplies.clear();') + writeLine(sourceFile, ' update();') + writeLine(sourceFile, '}') writeLine(sourceFile) - writeLine(sourceFile, 'void %s::verifyUpdateFinished()' % (className)) + writeLine(sourceFile, 'bool %s::verifyUpdateFinished()' % (className)) writeLine(sourceFile, '{') writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {') writeLine(sourceFile, ' emit updateFinished();') + writeLine(sourceFile, ' return true;') writeLine(sourceFile, ' }') + writeLine(sourceFile, ' return false;') writeLine(sourceFile, '}') writeLine(sourceFile) @@ -977,15 +1056,28 @@ else: logger.debug('Verified successfully checkReachableRegister: %s' % checkReachableRegister['id']) +queuedRequests = False +queuedRequestsDelay = 0 + +if 'queuedRequests' in registerJson: + queuedRequests = registerJson['queuedRequests'] + +if 'queuedRequestsDelay' in registerJson: + queuedRequestsDelay = registerJson['queuedRequestsDelay'] + # Inform about parsed and validated configs if debugging enabled logger.debug('Script path: %s' % scriptPath) logger.debug('Output directory: %s' % outputDirectory) logger.debug('Class name prefix: %s' % classNamePrefix) logger.debug('Endianness: %s' % endianness) logger.debug('String endianness: %s' % stringEndianness) +logger.debug('Queued requests: %s' % queuedRequests) +logger.debug('Queued requests delay: %s ms' % queuedRequestsDelay) + logger.debug('Error limit until not reachable: %s' % errorLimitUntilNotReachable) logger.debug('Check reachable register: %s' % checkReachableRegister['id']) + protocol = 'TCP' if 'protocol' in registerJson: protocol = registerJson['protocol'] @@ -1063,7 +1155,7 @@ projectIncludeFile = open(projectIncludeFilePath, 'w') writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #') writeLine(projectIncludeFile, '#') writeLine(projectIncludeFile, '# This file has been autogenerated.') -writeLine(projectIncludeFile, '# Any changes in this file may be overwritten from qmake.') +writeLine(projectIncludeFile, '# Any changes in this file may be overwritten on a rebuild.') writeLine(projectIncludeFile, '#') writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #') writeLine(projectIncludeFile) diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index 5e1768d..bb9149a 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -21,9 +21,12 @@ PLUGIN_DIRS = \ schrack \ senseair \ sma \ + solax \ stiebeleltron \ sunspec \ unipi \ + vestel \ + wattsonic \ webasto \ gcc { diff --git a/sma/modbus/smamodbussolarinverterdiscovery.cpp b/sma/modbus/smamodbussolarinverterdiscovery.cpp index 22a49ee..c7150a1 100644 --- a/sma/modbus/smamodbussolarinverterdiscovery.cpp +++ b/sma/modbus/smamodbussolarinverterdiscovery.cpp @@ -122,10 +122,10 @@ void SmaModbusSolarInverterDiscovery::checkNetworkDevice(const NetworkDeviceInfo m_discoveryResults.append(result); qCDebug(dcSma()) << "Discovery: --> Found" << result.productName; - qCDebug(dcSma()) << " Device name:" << result.deviceName; - qCDebug(dcSma()) << " Serial number:" << result.serialNumber; - qCDebug(dcSma()) << " Software version:" << result.softwareVersion; - qCDebug(dcSma()) << " " << result.networkDeviceInfo; + qCDebug(dcSma()) << "Discovery: Device name:" << result.deviceName; + qCDebug(dcSma()) << "Discovery: Serial number:" << result.serialNumber; + qCDebug(dcSma()) << "Discovery: Software version:" << result.softwareVersion; + qCDebug(dcSma()) << "Discovery: " << result.networkDeviceInfo; // Done with this connection cleanupConnection(connection); diff --git a/solax/README.md b/solax/README.md new file mode 100644 index 0000000..6d1ef44 --- /dev/null +++ b/solax/README.md @@ -0,0 +1,19 @@ +# Solax + +Connects Solax inverters to nymea. + +Currently supported models: + +* X1 models +* X3 models +* G4 inverters +* Meters +* Batteries + +## Connecting a second inverter + +There is the possibility to connect an other solar inverter to the meter 2 interface of your Solax installation. In order to have both possibilities you can switch between a second meter or a second inverter in the settings of the main inverter within nymea. + +# Requirements + +nymea uses the modbus TCP connection in order to connect to the Solax inverter. Therefore the inverter must be reachable using the local network. The inverter allows only to have **one TCP connection**, please make sure there is no other service or device using modbus TCP with your inverter besides nymea. \ No newline at end of file diff --git a/solax/integrationpluginsolax.cpp b/solax/integrationpluginsolax.cpp new file mode 100644 index 0000000..8da3687 --- /dev/null +++ b/solax/integrationpluginsolax.cpp @@ -0,0 +1,496 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "integrationpluginsolax.h" +#include "plugininfo.h" +#include "solaxdiscovery.h" + +#include +#include + +IntegrationPluginSolax::IntegrationPluginSolax() +{ + +} + +void IntegrationPluginSolax::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcSolax()) << "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 + SolaxDiscovery *discovery = new SolaxDiscovery(hardwareManager()->networkDeviceDiscovery(), 502, 1, info); + connect(discovery, &SolaxDiscovery::discoveryFinished, info, [=](){ + foreach (const SolaxDiscovery::SolaxDiscoveryResult &result, discovery->discoveryResults()) { + + QString title; + if (result.productName.isEmpty()) { + title = "SolaX Inverter"; + } else { + title = "SolaX " + result.productName; + } + + if (!result.serialNumber.isEmpty()) + title.append(" " + result.serialNumber); + + ThingDescriptor descriptor(solaxInverterTcpThingClassId, title, result.networkDeviceInfo.address().toString() + " " + result.networkDeviceInfo.macAddress()); + qCInfo(dcSolax()) << "Discovered:" << descriptor.title() << descriptor.description(); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(solaxInverterTcpThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcSolax()) << "This solax inverter already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(solaxInverterTcpThingMacAddressParamTypeId, 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 IntegrationPluginSolax::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCInfo(dcSolax()) << "Setup" << thing << thing->params(); + + // Inverter (connection) + if (thing->thingClassId() == solaxInverterTcpThingClassId) { + + // Handle reconfigure + if (m_tcpConnections.contains(thing)) { + qCDebug(dcSolax()) << "Reconfiguring existing thing" << thing->name(); + m_tcpConnections.take(thing)->deleteLater(); + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(solaxInverterTcpThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcSolax()) << "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; + } + + // Create the monitor + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); + m_monitors.insert(thing, monitor); + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + // Clean up in case the setup gets aborted + if (m_monitors.contains(thing)) { + qCDebug(dcSolax()) << "Unregister monitor because the setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); + uint port = thing->paramValue(solaxInverterTcpThingPortParamTypeId).toUInt(); + quint16 slaveId = thing->paramValue(solaxInverterTcpThingSlaveIdParamTypeId).toUInt(); + + qCInfo(dcSolax()) << "Setting up solax on" << address.toString() << port << "unit ID:" << slaveId; + SolaxModbusTcpConnection *solaxConnection = new SolaxModbusTcpConnection(address, port, slaveId, this); + connect(info, &ThingSetupInfo::aborted, solaxConnection, &SolaxModbusTcpConnection::deleteLater); + + // Reconnect on monitor reachable changed + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcSolax()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (!thing->setupComplete()) + return; + + if (reachable && !thing->stateValue("connected").toBool()) { + solaxConnection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address()); + solaxConnection->reconnectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + solaxConnection->disconnectDevice(); + } + }); + + connect(solaxConnection, &SolaxModbusTcpConnection::reachableChanged, thing, [this, thing, solaxConnection](bool reachable){ + qCDebug(dcSolax()) << "Reachable changed to" << reachable << "for" << thing; + if (reachable) { + // Connected true will be set after successfull init + solaxConnection->initialize(); + } else { + thing->setStateValue("connected", false); + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", false); + } + + // Reset any energy data due to connection loss + Thing *child = getMeterThing(thing, 1); + if (child) { + child->setStateValue(solaxMeterCurrentPowerStateTypeId, 0); + } + + child = getMeterThing(thing, 2); + if (child) { + child->setStateValue(solaxMeterCurrentPowerStateTypeId, 0); + child->setStateValue(solaxMeterCurrentPowerPhaseAStateTypeId, 0); + child->setStateValue(solaxMeterCurrentPowerPhaseBStateTypeId, 0); + child->setStateValue(solaxMeterCurrentPowerPhaseCStateTypeId, 0); + } + + Things inverterThings = myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId); + if (!inverterThings.isEmpty()) { + child = inverterThings.first(); + child->setSettingValue(solaxInverterChildCurrentPowerStateTypeId, 0); + } + + child = getBatteryThing(thing); + if (child) { + child->setStateValue(solaxBatteryVoltageStateTypeId, solaxConnection->batteryVoltage()); + child->setStateValue(solaxBatteryCurrentPowerStateTypeId, 0); + child->setStateValue(solaxBatteryChargingStateStateTypeId, "idle"); + } + } + }); + + connect(solaxConnection, &SolaxModbusTcpConnection::initializationFinished, thing, [=](bool success){ + thing->setStateValue("connected", success); + + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + childThing->setStateValue("connected", success); + } + + if (!success) { + // Try once to reconnect the device + solaxConnection->reconnectDevice(); + } else { + qCInfo(dcSolax()) << "Connection initialized successfully for" << thing; + solaxConnection->update(); + } + }); + + connect(solaxConnection, &SolaxModbusTcpConnection::updateFinished, thing, [=](){ + qCDebug(dcSolax()) << "Updated" << solaxConnection; + + ThingClass meterThingClass = thingClass(solaxMeterThingClassId); + + // Check if we have to create the meter for this solax inverter, or remove it due to communication errors + if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).isEmpty()) { + if (solaxConnection->meter1ComState() == 1 && myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 1).isEmpty()) { + qCDebug(dcSolax()) << "There is no meter set up for this inverter. Creating a meter 1 for" << thing << solaxConnection->modbusTcpMaster(); + ThingDescriptor descriptor(solaxMeterThingClassId, meterThingClass.displayName(), QString(), thing->id()); + ParamList params; + params.append(Param(solaxMeterThingIdParamTypeId, 1)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } + } + + // Note: it is possible to connect an additional inverter to the solax inverter, which will be measured by the meter 2. + // if so, we create a child inverter instead of the second meter thing. + if (solaxConnection->meter2ComState() == 1) { + if (thing->setting(solaxInverterTcpSettingsMeter2InverterParamTypeId).toBool()) { + if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId).isEmpty()) { + // Add the meter 2 as child inverter + emit autoThingsAppeared(ThingDescriptors() << ThingDescriptor(solaxInverterChildThingClassId, "SolaX inverter", QString(), thing->id())); + } + } else { + if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 2).isEmpty()) { + ThingDescriptor descriptor(solaxMeterThingClassId, meterThingClass.displayName() + " 2", QString(), thing->id()); + ParamList params; + params.append(Param(solaxMeterThingIdParamTypeId, 2)); + descriptor.setParams(params); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } + } + } else { + // Communication error with meter 2, remove any child devices for meter 2 registers + cleanupMeter2(thing); + cleanupChildInverters(thing); + } + + // Check if we have to create the battery for the solax inverter + if (solaxConnection->batteryConnected() != 0 && myThings().filterByParentId(thing->id()).filterByThingClassId(solaxBatteryThingClassId).isEmpty()) { + qCDebug(dcSolax()) << "There is a battery connected but not set up yet. Creating a battery..."; + ThingClass batteryThingClass = thingClass(solaxBatteryThingClassId); + ThingDescriptor descriptor(solaxBatteryThingClassId, batteryThingClass.displayName(), QString(), thing->id()); + emit autoThingsAppeared(ThingDescriptors() << descriptor); + } + + // Update inverter states + thing->setStateValue(solaxInverterTcpCurrentPowerStateTypeId, -solaxConnection->inverterPower()); + thing->setStateValue(solaxInverterTcpCurrentStateTypeId, -solaxConnection->inverterCurrent()); + thing->setStateValue(solaxInverterTcpCurrentVoltageStateTypeId, solaxConnection->inverterVoltage()); + thing->setStateValue(solaxInverterTcpTemperatureStateTypeId, solaxConnection->temperature()); + thing->setStateValue(solaxInverterTcpFrequencyStateTypeId, solaxConnection->inverterFrequency()); + thing->setStateValue(solaxInverterTcpTotalEnergyProducedStateTypeId, solaxConnection->totalEnergyProduced()); + + // Update the meter 1 if available + Thing *meterThing = getMeterThing(thing, 1); + if (meterThing) { + meterThing->setStateValue(solaxMeterTotalEnergyConsumedStateTypeId, solaxConnection->meterTotalEnergyConsumend()); + meterThing->setStateValue(solaxMeterTotalEnergyProducedStateTypeId, solaxConnection->meterTotalEnergyProduced()); + + // Power + meterThing->setStateValue(solaxMeterCurrentPowerStateTypeId, -solaxConnection->meterPower()); + } + + // Update inverter 2 states if available + if (thing->setting(solaxInverterTcpSettingsMeter2InverterParamTypeId).toBool()) { + Things childInverters = myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId); + if (!childInverters.isEmpty()) { + Thing *childInverter = childInverters.first(); + childInverter->setStateValue(solaxInverterChildCurrentPowerStateTypeId, -solaxConnection->meter2Power()); + childInverter->setStateValue(solaxInverterChildTotalEnergyProducedStateTypeId, solaxConnection->meter2EnergyProduced()); + } + } else { + // Update the meter 2 if available + meterThing = getMeterThing(thing, 2); + if (meterThing) { + meterThing->setStateValue(solaxMeterTotalEnergyConsumedStateTypeId, solaxConnection->meter2EnergyConsumed()); + meterThing->setStateValue(solaxMeterTotalEnergyProducedStateTypeId, solaxConnection->meter2EnergyProduced()); + + // Power + meterThing->setStateValue(solaxMeterCurrentPowerStateTypeId, solaxConnection->meter2Power()); + meterThing->setStateValue(solaxMeterCurrentPowerPhaseAStateTypeId, solaxConnection->meter2PowerR()); + meterThing->setStateValue(solaxMeterCurrentPowerPhaseBStateTypeId, solaxConnection->meter2PowerS()); + meterThing->setStateValue(solaxMeterCurrentPowerPhaseCStateTypeId, solaxConnection->meter2PowerT()); + } + } + + // Update the battery if available + Thing *batteryThing = getBatteryThing(thing); + if (batteryThing) { + batteryThing->setStateValue(solaxBatteryVoltageStateTypeId, solaxConnection->batteryVoltage()); + batteryThing->setStateValue(solaxBatteryTemperatureStateTypeId, solaxConnection->batteryTemperature()); + batteryThing->setStateValue(solaxBatteryBatteryLevelStateTypeId, solaxConnection->batteryCapacity()); + batteryThing->setStateValue(solaxBatteryBatteryCriticalStateTypeId, solaxConnection->batteryCapacity() < 5); + + double batteryPower = solaxConnection->batteryPower(); + batteryThing->setStateValue(solaxBatteryCurrentPowerStateTypeId, solaxConnection->batteryPower()); + if (batteryPower == 0) { + batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "idle"); + } else if (batteryPower < 0) { + batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "discharging"); + } else if (batteryPower > 0) { + batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "charging"); + } + } + }); + + connect(thing, &Thing::settingChanged, solaxConnection, [this, thing](const ParamTypeId ¶mTypeId, const QVariant &value){ + if (paramTypeId == solaxInverterTcpSettingsMeter2InverterParamTypeId) { + // Note: we just need to cleanup here, if there is any device connected, it will be created in the update method. + if (value.toBool()) { + // The meter will be used as inverter. Clean up any meters for meter 2 registers... + cleanupMeter2(thing); + } else { + // The meter will be used as meter. Clean up any child inverters for meter 2 registers... + cleanupChildInverters(thing); + } + } + }); + + m_tcpConnections.insert(thing, solaxConnection); + + if (monitor->reachable()) + solaxConnection->connectDevice(); + + info->finish(Thing::ThingErrorNoError); + return; + } + + // Meter + if (thing->thingClassId() == solaxMeterThingClassId) { + + // Get the parent thing and the associated connection + Thing *connectionThing = myThings().findById(thing->parentId()); + if (!connectionThing) { + qCWarning(dcSolax()) << "Failed to set up solax energy meter because the parent thing with ID" << thing->parentId().toString() << "could not be found."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + SolaxModbusTcpConnection *solaxConnection = m_tcpConnections.value(connectionThing); + if (!solaxConnection) { + qCWarning(dcSolax()) << "Failed to set up solax energy meter because the connection for" << connectionThing << "does not exist."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + // Note: The states will be handled in the parent inverter thing on updated + + info->finish(Thing::ThingErrorNoError); + return; + } + + // Child inverter + if (thing->thingClassId() == solaxInverterChildThingClassId) { + + // Get the parent thing and the associated connection + Thing *connectionThing = myThings().findById(thing->parentId()); + if (!connectionThing) { + qCWarning(dcSolax()) << "Failed to set up solax child inverter because the parent thing with ID" << thing->parentId().toString() << "could not be found."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + SolaxModbusTcpConnection *solaxConnection = m_tcpConnections.value(connectionThing); + if (!solaxConnection) { + qCWarning(dcSolax()) << "Failed to set up solax child inverter because the connection for" << connectionThing << "does not exist."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + // Note: The states will be handled in the parent inverter thing on updated + + info->finish(Thing::ThingErrorNoError); + return; + } + + // Battery + if (thing->thingClassId() == solaxBatteryThingClassId) { + // Get the parent thing and the associated connection + Thing *connectionThing = myThings().findById(thing->parentId()); + if (!connectionThing) { + qCWarning(dcSolax()) << "Failed to set up solax battery because the parent thing with ID" << thing->parentId().toString() << "could not be found."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + SolaxModbusTcpConnection *solaxConnection = m_tcpConnections.value(connectionThing); + if (!solaxConnection) { + qCWarning(dcSolax()) << "Failed to set up solax battery because the connection for" << connectionThing << "does not exist."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + // Note: The states will be handled in the parent inverter thing on updated + + info->finish(Thing::ThingErrorNoError); + return; + } +} + +void IntegrationPluginSolax::postSetupThing(Thing *thing) +{ + + if (thing->thingClassId() == solaxInverterTcpThingClassId) { + + // Create the update timer if not already set up + if (!m_refreshTimer) { + qCDebug(dcSolax()) << "Starting plugin timer..."; + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_refreshTimer, &PluginTimer::timeout, this, [this] { + foreach(SolaxModbusTcpConnection *connection, m_tcpConnections) { + if (connection->initializing()) + continue; + + //qCDebug(dcSolax()) << "Update connection" << connection->modbusTcpMaster()->hostAddress().toString(); + connection->update(); + } + }); + + m_refreshTimer->start(); + } + return; + } + + if (thing->thingClassId() == solaxMeterThingClassId || thing->thingClassId() == solaxBatteryThingClassId || thing->thingClassId() == solaxInverterChildThingClassId) { + Thing *connectionThing = myThings().findById(thing->parentId()); + if (connectionThing) { + thing->setStateValue("connected", connectionThing->stateValue("connected")); + } + + return; + } +} + +void IntegrationPluginSolax::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == solaxInverterTcpThingClassId && m_tcpConnections.contains(thing)) { + SolaxModbusTcpConnection *connection = m_tcpConnections.take(thing); + connection->modbusTcpMaster()->disconnectDevice(); + delete connection; + } + + // Unregister related hardware resources + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + + if (myThings().isEmpty() && m_refreshTimer) { + qCDebug(dcSolax()) << "Stopping refresh timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +Thing *IntegrationPluginSolax::getMeterThing(Thing *parentThing, uint meterId) +{ + Things meterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, meterId); + if (meterThings.isEmpty()) + return nullptr; + + return meterThings.first(); +} + +Thing *IntegrationPluginSolax::getBatteryThing(Thing *parentThing) +{ + Things batteryThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxBatteryThingClassId); + if (batteryThings.isEmpty()) + return nullptr; + + return batteryThings.first(); +} + +void IntegrationPluginSolax::cleanupMeter2(Thing *parentThing) +{ + Things meters = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 2); + if (!meters.isEmpty()) { + emit autoThingDisappeared(meters.first()->id()); + } +} + +void IntegrationPluginSolax::cleanupChildInverters(Thing *parentThing) +{ + Things inverterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxInverterChildThingClassId); + if (!inverterThings.isEmpty()) { + foreach (Thing *inverterThing, inverterThings) { + emit autoThingDisappeared(inverterThing->id()); + } + } +} + diff --git a/solax/integrationpluginsolax.h b/solax/integrationpluginsolax.h new file mode 100644 index 0000000..f30b3e6 --- /dev/null +++ b/solax/integrationpluginsolax.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 INTEGRATIONPLUGINSOLAX_H +#define INTEGRATIONPLUGINSOLAX_H + +#include +#include +#include + +#include "extern-plugininfo.h" + +#include "solaxmodbustcpconnection.h" +#include "solaxmodbusrtuconnection.h" + +class IntegrationPluginSolax: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsolax.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginSolax(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +private: + PluginTimer *m_refreshTimer = nullptr; + + QHash m_monitors; + QHash m_tcpConnections; + QHash m_rtuConnections; + + void setupSolaxTcpConnection(ThingSetupInfo *info); + + Thing *getMeterThing(Thing *parentThing, uint meterId); + Thing *getBatteryThing(Thing *parentThing); + + void cleanupMeter2(Thing *parentThing); + void cleanupChildInverters(Thing *parentThing); +}; + +#endif // INTEGRATIONPLUGINSOLAX_H + + diff --git a/solax/integrationpluginsolax.json b/solax/integrationpluginsolax.json new file mode 100644 index 0000000..0850a22 --- /dev/null +++ b/solax/integrationpluginsolax.json @@ -0,0 +1,384 @@ +{ + "name": "Solax", + "displayName": "SolaX Power", + "id": "c316666c-7070-42e2-8d37-1145715dc986", + "vendors": [ + { + "name": "solax", + "displayName": "SolaX Power", + "id": "a672201c-6b11-4e79-bef9-60a23e08ff8f", + "thingClasses": [ + { + "name": "solaxInverterTcp", + "displayName": "Solax Inverter", + "id": "fa1a559a-12a6-416f-ab77-a431a38bc3c2", + "createMethods": ["discovery"], + "discoveryType": "weak", + "interfaces": ["solarinverter", "connectable"], + "providedInterfaces": [ "energymeter", "energystorage"], + "paramTypes": [ + { + "id": "acdee28d-4c73-4ed9-ad1b-d5d1440164c0", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + }, + { + "id": "c5324c59-39e6-439c-a9e0-bbe8055c9db0", + "name":"port", + "displayName": "Port", + "type": "int", + "defaultValue": 502 + }, + { + "id": "154f8f71-1d84-4653-94a0-31337af55359", + "name":"slaveId", + "displayName": "Slave ID", + "type": "int", + "defaultValue": 1 + } + ], + "settingsTypes": [ + { + "id": "d065c829-6431-4a87-a30e-91d2dd864598", + "name": "meter2Inverter", + "displayName": "Inverter on Meter 2", + "type": "bool", + "defaultValue": false + } + ], + "stateTypes": [ + { + "id": "948d0f5c-4547-4894-be13-8b7ea2af50df", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "7cc0df36-7ec8-499d-ba6b-8b62520a0d61", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "85b505d0-363c-4608-8b26-1e9d4427d7ce", + "name": "currentVoltage", + "displayName": "Voltage", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + } + , + { + "id": "38e333be-86e8-42d8-a753-4e8102d5c2be", + "name": "current", + "displayName": "Current", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "cbf8cd14-1661-4063-be78-a7151dfc24d4", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0, + "cached": true + }, + { + "id": "e0bafe29-2eba-450b-9a0b-df65d0cbac7f", + "name": "temperature", + "displayName": "Temperature", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "9badd000-74b7-4293-8892-864a185d5073", + "name": "frequency", + "displayName": "Frequency", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00, + "cached": false + } + ], + "actionTypes": [ ] + }, + { + "name": "solaxInverterChild", + "displayName": "Solax Inverter", + "id": "84774ef9-5c4b-4f3f-95e7-846ba8380e22", + "createMethods": ["auto"], + "interfaces": ["solarinverter", "connectable"], + "paramTypes": [ ], + "stateTypes": [ + { + "id": "6b3e98c0-a562-4579-8e53-c4d7ac532057", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "f2644a0f-b16d-442b-add3-4458180c635c", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "da1788cb-eb3e-43c6-8815-a60f983c7fe8", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0, + "cached": true + } + ], + "actionTypes": [ ] + }, + { + "name": "solaxMeter", + "displayName": "SolaX Meter", + "id": "293d7cef-7bfb-4830-8958-b4b77ccb9786", + "createMethods": ["auto"], + "interfaces": [ "energymeter", "connectable"], + "paramTypes": [ + { + "id": "2c50e082-9fba-4859-a8f4-18957518b359", + "name": "id", + "displayName": "Meter ID", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "a9db94a3-64b4-4472-b5f9-89aded4f907c", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "077234cc-87b1-40f2-a06b-532219e35948", + "name": "currentPower", + "displayName": "Current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "d6a76445-e552-44bc-9d49-a64ac9f3263e", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "74d4fa43-10d8-4c85-a2a0-1c318bf4b44d", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "1be70078-7144-4325-b1fc-f73b23a33848", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "displayNameEvent": "Current power phase C changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "1da7318a-9b2f-4abd-a30b-df0da04e8d9b", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "89cebad3-8985-4f5c-bd69-cd041a436d48", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "d80a0934-5a83-4bac-aeac-2360144b3f93", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "d64f0d70-34a9-4426-a3c9-3689bf806f45", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "4007afc5-83d9-4427-bb3d-fe0197c33172", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "37a57511-dad5-490c-aa82-88f8e7ebbe1f", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "59397bac-a4d9-4e50-99a3-f329e3806b25", + "name": "totalEnergyProduced", + "displayName": "Total returned energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00, + "cached": true + }, + { + "id": "44f30880-cba9-4ce7-995d-8cbad4ff31a9", + "name": "totalEnergyConsumed", + "displayName": "Total imported energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00, + "cached": true + }, + { + "id": "09932aaa-5754-4fd9-a634-965902352de5", + "name": "frequency", + "displayName": "Frequency", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00, + "cached": false + } + ], + "actionTypes": [ ] + }, + { + "name": "solaxBattery", + "displayName": "SolaX Battery", + "id": "f9a03f59-7e2f-4794-98de-bd026d0052ce", + "createMethods": ["auto"], + "interfaces": [ "energystorage", "connectable"], + "paramTypes": [ + ], + "stateTypes": [ + { + "id": "456f091a-e12f-4b1a-82b3-0a2467f79ee3", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "5344d1dc-a109-4b44-8d50-24f69a6f6993", + "name": "batteryCritical", + "displayName": "Battery critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "2d601edb-31e8-4c00-8567-b9f81121a33c", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "edc3c2fd-382d-41ac-b894-50881fb92bea", + "name": "currentPower", + "displayName": "Total real power", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "e09c87be-ed6b-49f8-9693-ff15ff512db6", + "name": "voltage", + "displayName": "Voltage", + "type": "double", + "unit": "Volt", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "c21af13f-3ace-4f86-9d77-579b2a5e202c", + "name": "temperature", + "displayName": "Temperature", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "98099dbd-3f66-43b3-8192-f2e3fdcd5d62", + "name": "capacity", + "displayName": "Capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "829173e8-7535-4aba-b403-d498ff68250e", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + } + ], + "actionTypes": [ ] + } + ] + } + ] +} diff --git a/solax/meta.json b/solax/meta.json new file mode 100644 index 0000000..0ac7e51 --- /dev/null +++ b/solax/meta.json @@ -0,0 +1,13 @@ +{ + "title": "SolaX Power Inverter", + "tagline": "Connect to SolaX Power X1 and X3 G4 inverters.", + "icon": "solax.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/solax/solax-registers.json b/solax/solax-registers.json new file mode 100644 index 0000000..a53a158 --- /dev/null +++ b/solax/solax-registers.json @@ -0,0 +1,944 @@ +{ + "className": "Solax", + "protocol": "BOTH", + "endianness": "LittleEndian", + "errorLimitUntilNotReachable": 5, + "queuedRequests": true, + "queuedRequestsDelay": 200, + "checkReachableRegister": "inverterPower", + "enums": [ + { + "name": "RunMode", + "values": [ + { + "key": "WaitMode", + "value": 0 + }, + { + "key": "CheckMode", + "value": 1 + }, + { + "key": "NormalMode", + "value": 2 + }, + { + "key": "FaultMode", + "value": 3 + }, + { + "key": "PermanentFaultMode", + "value": 4 + }, + { + "key": "UpdateMode", + "value": 5 + }, + { + "key": "EpsCheckMode", + "value": 6 + }, + { + "key": "EpsMode", + "value": 7 + }, + { + "key": "SelfTest", + "value": 8 + }, + { + "key": "IdleMode", + "value": 9 + } + ] + } + ], + "blocks": [ + { + "id": "identification", + "readSchedule": "init", + "registers": [ + { + "id": "serialNumber", + "address": 0, + "size": 7, + "type": "string", + "registerType": "holdingRegister", + "description": "Serial number", + "access": "RO" + }, + { + "id": "factoryName", + "address": 7, + "size": 7, + "type": "string", + "registerType": "holdingRegister", + "description": "Factory name", + "access": "RO" + }, + { + "id": "moduleName", + "address": 14, + "size": 7, + "type": "string", + "registerType": "holdingRegister", + "description": "Module name", + "access": "RO" + } + ] + }, + { + "id": "versions", + "readSchedule": "init", + "registers": [ + { + "id": "firmwareVersion", + "address": 125, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Firmware version", + "access": "RO" + }, + { + "id": "hardwareVerrsion", + "address": 126, + "size": 1, + "type": "uint16", + "registerType": "holdingRegister", + "description": "Hardware version", + "access": "RO" + } + ] + }, + { + "id": "inverterEnergyValues", + "readSchedule": "update", + "registers": [ + { + "id": "inverterVoltage", + "address": 0, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Inverter voltage (X1)", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "inverterCurrent", + "address": 1, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Inverter current (X1)", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "inverterPower", + "address": 2, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Inverter power (X1)", + "unit": "W", + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "hybridVoltageCurrentValues", + "readSchedule": "update", + "registers": [ + { + "id": "pvVoltage1", + "address": 3, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "PV voltage 1 (Hybrid)", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "pvVoltage2", + "address": 4, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "PV voltage 2 (Hybrid)", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "pvCurrent1", + "address": 5, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "PV current 1 (Hybrid)", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "pvCurrent2", + "address": 6, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "PV current 2 (Hybrid)", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + } + ] + }, + { + "id": "inverterInformation", + "readSchedule": "update", + "registers": [ + { + "id": "inverterFrequency", + "address": 7, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Inverter frequency (X1)", + "unit": "Hz", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "temperature", + "address": 8, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Radiator temperature", + "unit": "°C", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "runMode", + "address": 9, + "size": 1, + "type": "uint16", + "enum": "RunMode", + "registerType": "inputRegister", + "description": "Run mode", + "defaultValue": "RunModeIdleMode", + "access": "RO" + }, + { + "id": "powerDc1", + "address": 10, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Power DC 1 (Hybrid)", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "powerDc2", + "address": 11, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Power DC 2 (Hybrid)", + "unit": "W", + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "batteryValues", + "readSchedule": "update", + "registers": [ + { + "id": "batteryVoltage", + "address": 20, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery voltage (Charge 1)", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryCurrent", + "address": 21, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery current (Charge 1)", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryPower", + "address": 22, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery power (Charge 1)", + "unit": "W", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryConnected", + "address": 23, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Battery connected (0 disconnected, 1 connected)", + "defaultValue": "false", + "access": "RO" + }, + { + "id": "batteryTemperature", + "address": 24, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery temperature", + "unit": "°C", + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "batteryEnergyValues", + "readSchedule": "update", + "registers": [ + { + "id": "batteryCapacity", + "address": 28, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Battery capacity", + "unit": "%", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "batteryEnergyOut", + "address": 29, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Battery output energy", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "bmsWarning", + "address": 31, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "BMS warning", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "batteryEnergyOutToday", + "address": 32, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Battery output energy today", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryEnergyIn", + "address": 33, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Battery input energy", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryEnergyInToday", + "address": 35, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Battery input energy today", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + } + ] + }, + { + "id": "meterValues", + "readSchedule": "update", + "registers": [ + { + "id": "meterPower", + "address": 70, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter power (+ returned, - aquired)", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterTotalEnergyProduced", + "address": 72, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter total energy returned", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meterTotalEnergyConsumend", + "address": 74, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter total energy consumed", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "gridAndEpsPhaseValues", + "readSchedule": "update", + "registers": [ + { + "id": "gridVoltageR", + "address": 106, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Grid voltage R L1", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "gridCurrentR", + "address": 107, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Grid current R L1", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "gridPowerR", + "address": 108, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Grid power R L1", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "gridFrequencyR", + "address": 109, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Grid frequency R L1", + "unit": "Hz", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "gridVoltageS", + "address": 110, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Grid voltage S L2", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "gridCurrentS", + "address": 111, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Grid current S L2", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "gridPowerS", + "address": 112, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Grid power S L2", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "gridFrequencyS", + "address": 113, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Grid frequency S L2", + "unit": "Hz", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "gridVoltageT", + "address": 114, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Grid voltage T L3", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "gridCurrentT", + "address": 115, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Grid current T L3", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "gridPowerT", + "address": 116, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Grid power T L3", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "gridFrequencyT", + "address": 117, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Grid frequency T L3", + "unit": "Hz", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "epsVoltageR", + "address": 118, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS voltage R L1", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "epsCurrentR", + "address": 119, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS current R L1", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "epsPowerActiveR", + "address": 120, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS power R L1", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "epsPowerSR", + "address": 121, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS power S R L1", + "unit": "VA", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "epsVoltageS", + "address": 122, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS voltage S L2", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "epsCurrentS", + "address": 123, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS current S L2", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "epsPowerS", + "address": 124, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS power S L2", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "epsPowerSS", + "address": 125, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS power S S L1", + "unit": "VA", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "epsVoltageT", + "address": 126, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS voltage T L3", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "epsCurrentT", + "address": 127, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS current T L3", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "epsPowerT", + "address": 128, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS power T L3", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "epsPowerST", + "address": 129, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "EPS power S T L1", + "unit": "VA", + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "meter2Values", + "readSchedule": "update", + "registers": [ + { + "id": "meter2Power", + "address": 168, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter 2 power", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meter2EnergyProduced", + "address": 170, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Meter 2 energy produced", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "meter2EnergyConsumed", + "address": 172, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Meter 2 energy consumed", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "meter2EnergyProducedToday", + "address": 174, + "size": 2, + "type": "uint32", + "registerType": "inputRegister", + "description": "Meter 2 energy produced today", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "meter2EnergyConsumedToday", + "address": 176, + "size": 2, + "type": "uint16", + "registerType": "inputRegister", + "description": "Meter 2 energy consumed today", + "unit": "kWh", + "defaultValue": "0", + "staticScaleFactor": -2, + "access": "RO" + }, + { + "id": "meter2PowerR", + "address": 178, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter 2 power R L1", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meter2PowerS", + "address": 180, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter 2 power S L2", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meter2PowerT", + "address": 182, + "size": 2, + "type": "int32", + "registerType": "inputRegister", + "description": "Meter 2 power T L3", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meter1ComState", + "address": 184, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Meter 1 communication state (0 error, 1 normal)", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "meter2ComState", + "address": 185, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Meter 2 communication state (0 error, 1 normal)", + "defaultValue": "0", + "access": "RO" + } + ] + }, + { + "id": "batteryValues2", + "readSchedule": "update", + "registers": [ + { + "id": "batteryVoltage2", + "address": 194, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery voltage 2 (Charge 1)", + "unit": "V", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryCurrent2", + "address": 195, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery current 2 (Charge 1)", + "unit": "A", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryPower2", + "address": 196, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery power 2 (Charge 1)", + "unit": "W", + "defaultValue": "0", + "staticScaleFactor": -1, + "access": "RO" + }, + { + "id": "batteryConnected2", + "address": 197, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Battery connected 2 (0 disconnected, 1 connected)", + "defaultValue": "false", + "access": "RO" + }, + { + "id": "batteryTemperature2", + "address": 198, + "size": 1, + "type": "int16", + "registerType": "inputRegister", + "description": "Battery temperature 2", + "unit": "°C", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "batteryCapacity2", + "address": 199, + "size": 1, + "type": "uint16", + "registerType": "inputRegister", + "description": "Battery capacity 2", + "unit": "%", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "RO" + } + ] + } + ], + "registers": [ + { + "id": "totalEnergyProduced", + "address": 82, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Inverter total energy AC port", + "unit": "kWh", + "staticScaleFactor": -2, + "defaultValue": "0", + "access": "RO" + } + ] +} diff --git a/solax/solax.png b/solax/solax.png new file mode 100644 index 0000000..75c647f Binary files /dev/null and b/solax/solax.png differ diff --git a/solax/solax.pro b/solax/solax.pro new file mode 100644 index 0000000..39928cd --- /dev/null +++ b/solax/solax.pro @@ -0,0 +1,14 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += solax-registers.json +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + integrationpluginsolax.h \ + solaxdiscovery.h + +SOURCES += \ + integrationpluginsolax.cpp \ + solaxdiscovery.cpp diff --git a/solax/solaxdiscovery.cpp b/solax/solaxdiscovery.cpp new file mode 100644 index 0000000..1464015 --- /dev/null +++ b/solax/solaxdiscovery.cpp @@ -0,0 +1,163 @@ + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "solaxdiscovery.h" +#include "extern-plugininfo.h" + +SolaxDiscovery::SolaxDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress, QObject *parent) : + QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery}, + m_port{port}, + m_modbusAddress{modbusAddress} +{ + +} + +void SolaxDiscovery::startDiscovery() +{ + qCInfo(dcSolax()) << "Discovery: Start searching for Solax inverters in the network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &SolaxDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcSolax()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + + // Give the last connections added right before the network discovery finished a chance to check the device... + QTimer::singleShot(3000, this, [this](){ + qCDebug(dcSolax()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + }); +} + +QList SolaxDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void SolaxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + // Create a Solax connection and try to initialize it. + // Only if initialized successfully and all information have been fetched correctly from + // the device we can assume this is what we are locking for (ip, port, modbus address, correct registers). + // We cloud tough also filter the result only for certain software versions, manufactueres or whatever... + + SolaxModbusTcpConnection *connection = new SolaxModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this); + connection->modbusTcpMaster()->setTimeout(500); + connection->modbusTcpMaster()->setNumberOfRetries(0); + m_connections.append(connection); + + connect(connection, &SolaxModbusTcpConnection::reachableChanged, this, [=](bool reachable){ + if (!reachable) { + // Disconnected ... done with this connection + cleanupConnection(connection); + return; + } + + // Modbus TCP connected and reachable call successed, let's try to initialize it! + connect(connection, &SolaxModbusTcpConnection::initializationFinished, this, [=](bool success){ + if (!success) { + qCDebug(dcSolax()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + return; + } + + qCInfo(dcSolax()) << "Discovery: Initialized successfully" << networkDeviceInfo << connection->factoryName() << connection->serialNumber(); + + // Let's make sure the information are correct + if (connection->factoryName().toLower().contains("solax")) { + SolaxDiscoveryResult result; + result.productName = connection->moduleName(); + result.manufacturerName = connection->factoryName(); + result.serialNumber = connection->serialNumber(); + result.networkDeviceInfo = networkDeviceInfo; + m_discoveryResults.append(result); + + qCInfo(dcSolax()) << "Discovery: --> Found" << result.manufacturerName << result.productName + << "Serial number:" << result.serialNumber + << result.networkDeviceInfo; + } + + connection->disconnectDevice(); + }); + + qCDebug(dcSolax()) << "Discovery: The host" << networkDeviceInfo << "is reachable, trying to initialize..."; + if (!connection->initialize()) { + qCDebug(dcSolax()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If we get any error...skip this host... + connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionStateChanged, this, [=](bool connected){ + if (connected) { + qCDebug(dcSolax()) << "Discovery: Connected with" << networkDeviceInfo.address().toString() << m_port; + } + }); + + // If we get any error...skip this host... + connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [=](QModbusDevice::Error error){ + if (error != QModbusDevice::NoError) { + qCDebug(dcSolax()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + } + }); + + // If check reachability failed...skip this host... + connect(connection, &SolaxModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(dcSolax()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";; + cleanupConnection(connection); + }); + + // Try to connect, maybe it works, maybe not... + connection->connectDevice(); +} + +void SolaxDiscovery::cleanupConnection(SolaxModbusTcpConnection *connection) +{ + qCDebug(dcSolax()) << "Discovery: Cleanup connection" << connection->modbusTcpMaster(); + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void SolaxDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (SolaxModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(dcSolax()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() << "Solax Inverters in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit discoveryFinished(); +} diff --git a/solax/solaxdiscovery.h b/solax/solaxdiscovery.h new file mode 100644 index 0000000..b4561cb --- /dev/null +++ b/solax/solaxdiscovery.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 SOLAXDISCOVERY_H +#define SOLAXDISCOVERY_H + +#include +#include + +#include + +#include "solaxmodbustcpconnection.h" + +class SolaxDiscovery : public QObject +{ + Q_OBJECT +public: + explicit SolaxDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 502, quint16 modbusAddress = 1, QObject *parent = nullptr); + typedef struct SolaxDiscoveryResult { + QString productName; + QString manufacturerName; + QString serialNumber; + NetworkDeviceInfo networkDeviceInfo; + } SolaxDiscoveryResult; + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + quint16 m_port; + quint16 m_modbusAddress; + + QDateTime m_startDateTime; + + QList m_connections; + QList m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(SolaxModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // SOLAXDISCOVERY_H diff --git a/solax/translations/c316666c-7070-42e2-8d37-1145715dc986-en_US.ts b/solax/translations/c316666c-7070-42e2-8d37-1145715dc986-en_US.ts new file mode 100644 index 0000000..a4fb497 --- /dev/null +++ b/solax/translations/c316666c-7070-42e2-8d37-1145715dc986-en_US.ts @@ -0,0 +1,239 @@ + + + + + IntegrationPluginSolax + + + The network device discovery is not available. + + + + + The MAC address is not known. Please reconfigure the thing. + + + + + Solax + + + Active power + The name of the StateType ({7cc0df36-7ec8-499d-ba6b-8b62520a0d61}) of ThingClass solaxInverterTcp + + + + + Battery critical + The name of the StateType ({5344d1dc-a109-4b44-8d50-24f69a6f6993}) of ThingClass solaxBattery + + + + + Battery level + The name of the StateType ({2d601edb-31e8-4c00-8567-b9f81121a33c}) of ThingClass solaxBattery + + + + + Capacity + The name of the StateType ({98099dbd-3f66-43b3-8192-f2e3fdcd5d62}) of ThingClass solaxBattery + + + + + Charging state + The name of the StateType ({829173e8-7535-4aba-b403-d498ff68250e}) of ThingClass solaxBattery + + + + + + + Connected + The name of the StateType ({456f091a-e12f-4b1a-82b3-0a2467f79ee3}) of ThingClass solaxBattery +---------- +The name of the StateType ({a9db94a3-64b4-4472-b5f9-89aded4f907c}) of ThingClass solaxMeter +---------- +The name of the StateType ({948d0f5c-4547-4894-be13-8b7ea2af50df}) of ThingClass solaxInverterTcp + + + + + Current + The name of the StateType ({38e333be-86e8-42d8-a753-4e8102d5c2be}) of ThingClass solaxInverterTcp + + + + + Current phase A + The name of the StateType ({d64f0d70-34a9-4426-a3c9-3689bf806f45}) of ThingClass solaxMeter + + + + + Current phase B + The name of the StateType ({4007afc5-83d9-4427-bb3d-fe0197c33172}) of ThingClass solaxMeter + + + + + Current phase C + The name of the StateType ({37a57511-dad5-490c-aa82-88f8e7ebbe1f}) of ThingClass solaxMeter + + + + + Current power + The name of the StateType ({077234cc-87b1-40f2-a06b-532219e35948}) of ThingClass solaxMeter + + + + + Current power phase A + The name of the StateType ({d6a76445-e552-44bc-9d49-a64ac9f3263e}) of ThingClass solaxMeter + + + + + Current power phase B + The name of the StateType ({74d4fa43-10d8-4c85-a2a0-1c318bf4b44d}) of ThingClass solaxMeter + + + + + Current power phase C + The name of the StateType ({1be70078-7144-4325-b1fc-f73b23a33848}) of ThingClass solaxMeter + + + + + Frequency + The name of the StateType ({09932aaa-5754-4fd9-a634-965902352de5}) of ThingClass solaxMeter + + + + + MAC address + The name of the ParamType (ThingClass: solaxInverterTcp, Type: thing, ID: {acdee28d-4c73-4ed9-ad1b-d5d1440164c0}) + + + + + Port + The name of the ParamType (ThingClass: solaxInverterTcp, Type: thing, ID: {c5324c59-39e6-439c-a9e0-bbe8055c9db0}) + + + + + Slave ID + The name of the ParamType (ThingClass: solaxInverterTcp, Type: thing, ID: {154f8f71-1d84-4653-94a0-31337af55359}) + + + + + SolaX Battery + The name of the ThingClass ({f9a03f59-7e2f-4794-98de-bd026d0052ce}) + + + + + SolaX Meter + The name of the ThingClass ({293d7cef-7bfb-4830-8958-b4b77ccb9786}) + + + + + + SolaX Power + The name of the vendor ({a672201c-6b11-4e79-bef9-60a23e08ff8f}) +---------- +The name of the plugin Solax ({c316666c-7070-42e2-8d37-1145715dc986}) + + + + + Solax Inverter + The name of the ThingClass ({fa1a559a-12a6-416f-ab77-a431a38bc3c2}) + + + + + + Temperature + The name of the StateType ({c21af13f-3ace-4f86-9d77-579b2a5e202c}) of ThingClass solaxBattery +---------- +The name of the StateType ({e0bafe29-2eba-450b-9a0b-df65d0cbac7f}) of ThingClass solaxInverterTcp + + + + + Total energy produced + The name of the StateType ({cbf8cd14-1661-4063-be78-a7151dfc24d4}) of ThingClass solaxInverterTcp + + + + + Total imported energy + The name of the StateType ({44f30880-cba9-4ce7-995d-8cbad4ff31a9}) of ThingClass solaxMeter + + + + + Total real power + The name of the StateType ({edc3c2fd-382d-41ac-b894-50881fb92bea}) of ThingClass solaxBattery + + + + + Total returned energy + The name of the StateType ({59397bac-a4d9-4e50-99a3-f329e3806b25}) of ThingClass solaxMeter + + + + + + Voltage + The name of the StateType ({e09c87be-ed6b-49f8-9693-ff15ff512db6}) of ThingClass solaxBattery +---------- +The name of the StateType ({85b505d0-363c-4608-8b26-1e9d4427d7ce}) of ThingClass solaxInverterTcp + + + + + Voltage phase A + The name of the StateType ({1da7318a-9b2f-4abd-a30b-df0da04e8d9b}) of ThingClass solaxMeter + + + + + Voltage phase B + The name of the StateType ({89cebad3-8985-4f5c-bd69-cd041a436d48}) of ThingClass solaxMeter + + + + + Voltage phase C + The name of the StateType ({d80a0934-5a83-4bac-aeac-2360144b3f93}) of ThingClass solaxMeter + + + + + charging + The name of a possible value of StateType {829173e8-7535-4aba-b403-d498ff68250e} of ThingClass solaxBattery + + + + + discharging + The name of a possible value of StateType {829173e8-7535-4aba-b403-d498ff68250e} of ThingClass solaxBattery + + + + + idle + The name of a possible value of StateType {829173e8-7535-4aba-b403-d498ff68250e} of ThingClass solaxBattery + + + + diff --git a/vestel/README.md b/vestel/README.md new file mode 100644 index 0000000..938b676 --- /dev/null +++ b/vestel/README.md @@ -0,0 +1,9 @@ +# Vestel + +Connects nymea to Vestel wallboxes. Currently supported models are: + +* EVC04 + +# Requirements + +nymea uses the Modbus TCP connection to connect to the wallbox. Please make sure that the wallbox is connected to the same network as the nymea system and that the Local Load Management Option is set to Modbus TCP in the wallboxes web interface. diff --git a/vestel/evc04-registers.json b/vestel/evc04-registers.json new file mode 100644 index 0000000..f29c993 --- /dev/null +++ b/vestel/evc04-registers.json @@ -0,0 +1,362 @@ +{ + "className": "EVC04", + "protocol": "TCP", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 20, + "checkReachableRegister": "chargepointState", + "enums": [ + { + "name": "ChargePointState", + "values": [ + { + "key": "Available", + "value": 0 + }, + { + "key": "Preparing", + "value": 1 + }, + { + "key": "Charging", + "value": 2 + }, + { + "key": "SuspendedEVSE", + "value": 3 + }, + { + "key": "SuspendedEV", + "value": 4 + }, + { + "key": "Finishing", + "value": 5 + }, + { + "key": "Reserved", + "value": 6 + }, + { + "key": "Unavailable", + "value": 7 + }, + { + "key": "Faulted", + "value": 8 + } + ] + }, + { + "name": "ChargingState", + "values": [ + { + "key": "NotCharging", + "value": 0 + }, + { + "key": "Charging", + "value": 1 + } + ] + }, + { + "name": "NumPhases", + "values": [ + { + "key": "1", + "value": 0 + }, + { + "key": "3", + "value": 1 + } + ] + }, + { + "name": "CableState", + "values": [ + { + "key": "NotConnected", + "value": 0 + }, + { + "key": "CableConnectedVehicleNotConnected", + "value": 1 + }, + { + "key": "CableConnectedVehicleConnected", + "value": 2 + }, + { + "key": "CableConnectedVehicleConnectedCableLocked", + "value": 3 + } + ] + } + ], + "registers": [ + { + "id": "serialNumber", + "address": 100, + "size": 25, + "type": "raw", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Serial number", + "access": "RO" + }, + { + "id": "chargepointId", + "address": 130, + "size": 50, + "type": "raw", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Chargepoint ID", + "access": "RO" + }, + { + "id": "brand", + "address": 190, + "size": 10, + "type": "raw", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Brand", + "access": "RO" + }, + { + "id": "model", + "address": 210, + "size": 5, + "type": "raw", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Model", + "access": "RO" + }, + { + "id": "firmwareVersion", + "address": 230, + "size": 50, + "type": "raw", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Firmware version", + "access": "RO" + }, + { + "id": "time", + "address": 294, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Wallbox time", + "access": "RO" + }, + { + "id": "maxChargePointPower", + "address": 400, + "size": 2, + "type": "uint32", + "readSchedule": "init", + "registerType": "inputRegister", + "description": "Max power of Chargepoint", + "unit": "W", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "numPhases", + "address": 404, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Number of Phases", + "enum": "NumPhases", + "defaultValue": "NumPhases1", + "access": "RO" + }, + { + "id": "chargepointState", + "address": 1000, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Chargepoint State", + "enum": "ChargePointState", + "defaultValue": "ChargePointStateAvailable", + "access": "RO" + }, + { + "id": "chargingState", + "address": 1001, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Charging State", + "enum": "ChargingState", + "defaultValue": "ChargingStateNotCharging", + "access": "RO" + }, + { + "id": "cableState", + "address": 1004, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Cable state", + "enum": "CableState", + "defaultValue": "CableStateNotConnected", + "access": "RO" + }, + { + "id": "activePowerTotal", + "address": 1020, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Active Power Total", + "unit": "W", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "meterReading", + "address": 1036, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Meter reading", + "unit": "1/10 kWh", + "defaultValue": "ChargingStateNotCharging", + "access": "RO" + }, + { + "id": "sessionMaxCurrent", + "address": 1100, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Session max current", + "unit": "A", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "evseMinCurrent", + "address": 1102, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "EVSE min current", + "unit": "A", + "defaultValue": 6, + "access": "RO" + }, + { + "id": "evseMaxCurrent", + "address": 1104, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "EVSE max current", + "unit": "A", + "defaultValue": 32, + "access": "RO" + }, + { + "id": "cableMaxCurrent", + "address": 1106, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Cable max current", + "unit": "A", + "defaultValue": 32, + "access": "RO" + }, + { + "id": "sessionEnergy", + "address": 1502, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Session energy", + "unit": "Wh", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "sessionDuration", + "address": 1508, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "inputRegister", + "description": "Session duration", + "unit": "s", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "failsafeCurrent", + "address": 2000, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Failsafe current", + "unit": "A", + "defaultValue": 6, + "access": "RW" + }, + { + "id": "failsafeTimeout", + "address": 2002, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Failsafe timeout", + "unit": "s", + "defaultValue": 20, + "access": "RW" + }, + { + "id": "chargingCurrent", + "address": 5004, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Dynamic charging current", + "unit": "A", + "defaultValue": 6, + "access": "RW" + }, + { + "id": "aliveRegister", + "address": 6000, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Alive register", + "defaultValue": 0, + "access": "RW" + } + ] +} diff --git a/vestel/evc04discovery.cpp b/vestel/evc04discovery.cpp new file mode 100644 index 0000000..40b4e9b --- /dev/null +++ b/vestel/evc04discovery.cpp @@ -0,0 +1,136 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "evc04discovery.h" +#include "extern-plugininfo.h" + +EVC04Discovery::EVC04Discovery(NetworkDeviceDiscovery *networkDeviceDiscovery, const QLoggingCategory &dc, QObject *parent) : + QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery}, + m_dc{dc.categoryName()} +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + qCDebug(m_dc) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); +} + +void EVC04Discovery::startDiscovery() +{ + qCInfo(m_dc()) << "Discovery: Searching for Vestel EVC04 wallboxes in the network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &EVC04Discovery::checkNetworkDevice); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(m_dc()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_gracePeriodTimer.start(); + discoveryReply->deleteLater(); + }); +} + +QList EVC04Discovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void EVC04Discovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + int port = 502; + int slaveId = 0xff; + qCDebug(m_dc()) << "Checking network device:" << networkDeviceInfo << "Port:" << port << "Slave ID:" << slaveId; + + EVC04ModbusTcpConnection *connection = new EVC04ModbusTcpConnection(networkDeviceInfo.address(), port, slaveId, this); + m_connections.append(connection); + + connect(connection, &EVC04ModbusTcpConnection::reachableChanged, this, [=](bool reachable){ + if (!reachable) { + cleanupConnection(connection); + return; + } + + connect(connection, &EVC04ModbusTcpConnection::initializationFinished, this, [=](bool success){ + qCDebug(m_dc()) << "Discovered device on" << networkDeviceInfo.address() << connection->brand() << connection->model() << connection->firmwareVersion(); + qCDebug(m_dc()) << connection; + if (!success) { + qCDebug(m_dc()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + return; + } + Result result; + result.chargepointId = QString(QString::fromUtf16(connection->chargepointId().data(), connection->chargepointId().length()).toUtf8()).trimmed(); + result.brand = QString(QString::fromUtf16(connection->brand().data(), connection->brand().length()).toUtf8()).trimmed(); + result.model = QString(QString::fromUtf16(connection->model().data(), connection->model().length()).toUtf8()).trimmed(); + result.firmwareVersion = QString(QString::fromUtf16(connection->firmwareVersion().data(), connection->firmwareVersion().length()).toUtf8()).trimmed(); + result.networkDeviceInfo = networkDeviceInfo; + m_discoveryResults.append(result); + + qCDebug(m_dc()) << "Discovery: Found wallbox with firmware version:" << result.firmwareVersion << result.networkDeviceInfo; + + cleanupConnection(connection); + }); + + if (!connection->initialize()) { + qCDebug(m_dc()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + } + }); + + connect(connection, &EVC04ModbusTcpConnection::checkReachabilityFailed, this, [=](){ + qCDebug(m_dc()) << "Discovery: Checking reachability failed on" << networkDeviceInfo.address().toString(); + cleanupConnection(connection); + }); + + connection->connectDevice(); +} + +void EVC04Discovery::cleanupConnection(EVC04ModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void EVC04Discovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers...we don't care any more + foreach (EVC04ModbusTcpConnection *connection, m_connections) + cleanupConnection(connection); + + qCInfo(m_dc()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() + << "Vestel EVC04 wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + + emit discoveryFinished(); +} diff --git a/vestel/evc04discovery.h b/vestel/evc04discovery.h new file mode 100644 index 0000000..15a4743 --- /dev/null +++ b/vestel/evc04discovery.h @@ -0,0 +1,79 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 AMTRONECUDISCOVERY_H +#define AMTRONECUDISCOVERY_H + +#include +#include +#include + +#include + +#include "evc04modbustcpconnection.h" + +class EVC04Discovery : public QObject +{ + Q_OBJECT +public: + explicit EVC04Discovery(NetworkDeviceDiscovery *networkDeviceDiscovery, const QLoggingCategory &dc, QObject *parent = nullptr); + struct Result { + QString chargepointId; + QString firmwareVersion; + QString brand; + QString model; + NetworkDeviceInfo networkDeviceInfo; + }; + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QLoggingCategory m_dc; + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + QList m_connections; + + QList m_discoveryResults; + + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void cleanupConnection(EVC04ModbusTcpConnection *connection); + + void finishDiscovery(); +}; + +#endif // AMTRONECUDISCOVERY_H diff --git a/vestel/integrationpluginvestel.cpp b/vestel/integrationpluginvestel.cpp new file mode 100644 index 0000000..7395cfd --- /dev/null +++ b/vestel/integrationpluginvestel.cpp @@ -0,0 +1,405 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 "integrationpluginvestel.h" +#include "plugininfo.h" +#include "evc04discovery.h" + +#include +#include + +IntegrationPluginVestel::IntegrationPluginVestel() +{ + +} + +void IntegrationPluginVestel::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcVestel()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("Unable to discover devices in the network. The system may not be installed correctly.")); + return; + } + + if (info->thingClassId() == evc04ThingClassId) { + EVC04Discovery *discovery = new EVC04Discovery(hardwareManager()->networkDeviceDiscovery(), dcVestel(), info); + connect(discovery, &EVC04Discovery::discoveryFinished, info, [=](){ + foreach (const EVC04Discovery::Result &result, discovery->discoveryResults()) { + + QString name = result.chargepointId; + QString description = result.brand + " " + result.model; + ThingDescriptor descriptor(evc04ThingClassId, name, description); + qCDebug(dcVestel()) << "Discovered:" << descriptor.title() << descriptor.description(); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(evc04ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcVestel()) << "This wallbox already exists in the system:" << result.networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + ParamList params; + params << Param(evc04ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + } +} + +void IntegrationPluginVestel::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcVestel()) << "Setup" << thing << thing->params(); + + if (thing->thingClassId() == evc04ThingClassId) { + + if (m_evc04Connections.contains(thing)) { + qCDebug(dcVestel()) << "Reconfiguring existing thing" << thing->name(); + m_evc04Connections.take(thing)->deleteLater(); + + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + } + + MacAddress macAddress = MacAddress(thing->paramValue(evc04ThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcVestel()) << "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(dcVestel()) << "Unregistering monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + if (monitor->reachable()) { + setupEVC04Connection(info); + } else { + qCDebug(dcVestel()) << "Waiting for the network monitor to get reachable before continuing to set up the connection" << thing->name() << "..."; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + qCDebug(dcVestel()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continuing setup on" << monitor->networkDeviceInfo().address().toString(); + setupEVC04Connection(info); + } + }); + } + + return; + } +} + +void IntegrationPluginVestel::postSetupThing(Thing *thing) +{ + Q_UNUSED(thing) + if (!m_pluginTimer) { + qCDebug(dcVestel()) << "Starting plugin timer..."; + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { + foreach(EVC04ModbusTcpConnection *connection, m_evc04Connections) { + qCDebug(dcVestel()) << "Updating connection" << connection->modbusTcpMaster()->hostAddress().toString(); + connection->update(); + connection->setAliveRegister(1); + } + }); + + m_pluginTimer->start(); + } +} + +void IntegrationPluginVestel::executeAction(ThingActionInfo *info) +{ + if (info->thing()->thingClassId() == evc04ThingClassId) { + EVC04ModbusTcpConnection *evc04Connection = m_evc04Connections.value(info->thing()); + + if (info->action().actionTypeId() == evc04PowerActionTypeId) { + bool power = info->action().paramValue(evc04PowerActionPowerParamTypeId).toBool(); + + // If the car is *not* connected, writing a 0 to the charging current register will cause it to go to 6 A instead of 0 + // Because of this, we we're not connected, we'll do nothing, but once it get's connected, we'll sync the state over (see below in cableStateChanged) + if (!power && evc04Connection->cableState() < EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnected) { + info->thing()->setStateValue(evc04PowerStateTypeId, false); + info->finish(Thing::ThingErrorNoError); + return; + } + + QModbusReply *reply = evc04Connection->setChargingCurrent(power ? info->thing()->stateValue(evc04MaxChargingCurrentStateTypeId).toUInt() : 0); + connect(reply, &QModbusReply::finished, info, [info, reply, power](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(evc04PowerStateTypeId, power); + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcVestel()) << "Error setting power:" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + } + if (info->action().actionTypeId() == evc04MaxChargingCurrentActionTypeId) { + int maxChargingCurrent = info->action().paramValue(evc04MaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); + QModbusReply *reply = evc04Connection->setChargingCurrent(maxChargingCurrent); + connect(reply, &QModbusReply::finished, info, [info, reply, maxChargingCurrent](){ + if (reply->error() == QModbusDevice::NoError) { + info->thing()->setStateValue(evc04MaxChargingCurrentStateTypeId, maxChargingCurrent); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + } + } +} + +void IntegrationPluginVestel::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == evc04ThingClassId && m_evc04Connections.contains(thing)) { + EVC04ModbusTcpConnection *connection = m_evc04Connections.take(thing); + delete connection; + } + + // Unregister related hardware resources + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + + if (myThings().isEmpty() && m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginVestel::setupEVC04Connection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); + + qCDebug(dcVestel()) << "Setting up EVC04 wallbox on" << address.toString(); + EVC04ModbusTcpConnection *evc04Connection = new EVC04ModbusTcpConnection(address, 502, 0xff, this); + connect(info, &ThingSetupInfo::aborted, evc04Connection, &EVC04ModbusTcpConnection::deleteLater); + + // Reconnect on monitor reachable changed + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcVestel()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (!thing->setupComplete()) + return; + + if (reachable && !thing->stateValue("connected").toBool()) { + evc04Connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address()); + evc04Connection->connectDevice(); + } else if (!reachable) { + // Note: We disable autoreconnect explicitly and we will + // connect the device once the monitor says it is reachable again + evc04Connection->disconnectDevice(); + } + }); + + connect(evc04Connection, &EVC04ModbusTcpConnection::reachableChanged, thing, [thing, evc04Connection](bool reachable){ + qCDebug(dcVestel()) << "Reachable changed to" << reachable << "for" << thing; + if (reachable) { + evc04Connection->initialize(); + } else { + thing->setStateValue(evc04ConnectedStateTypeId, false); + thing->setStateValue(evc04CurrentPowerStateTypeId, 0); + } + }); + + connect(evc04Connection, &EVC04ModbusTcpConnection::initializationFinished, thing, [=](bool success){ + if (!thing->setupComplete()) + return; + + if (success) { + thing->setStateValue(evc04ConnectedStateTypeId, true); + } else { + thing->setStateValue(evc04ConnectedStateTypeId, false); + thing->setStateValue(evc04CurrentPowerStateTypeId, 0); + + // Try once to reconnect the device + evc04Connection->reconnectDevice(); + } + }); + + connect(evc04Connection, &EVC04ModbusTcpConnection::initializationFinished, info, [=](bool success){ + if (!success) { + qCWarning(dcVestel()) << "Connection init finished with errors" << thing->name() << evc04Connection->modbusTcpMaster()->hostAddress().toString(); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); + evc04Connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox.")); + return; + } + + qCDebug(dcVestel()) << "Connection init finished successfully" << evc04Connection; + + m_evc04Connections.insert(thing, evc04Connection); + info->finish(Thing::ThingErrorNoError); + + thing->setStateValue(evc04ConnectedStateTypeId, true); + thing->setStateValue(evc04VersionStateTypeId, QString(QString::fromUtf16(evc04Connection->firmwareVersion().data(), evc04Connection->firmwareVersion().length()).toUtf8()).trimmed()); + + evc04Connection->update(); + }); + + connect(evc04Connection, &EVC04ModbusTcpConnection::updateFinished, thing, [this, evc04Connection, thing](){ + qCDebug(dcVestel()) << "EVC04 update finished:" << thing->name() << evc04Connection; + + qCDebug(dcVestel()) << "Serial:" << QString(QString::fromUtf16(evc04Connection->serialNumber().data(), evc04Connection->serialNumber().length()).toUtf8()).trimmed(); + qCDebug(dcVestel()) << "ChargePoint ID:" << QString(QString::fromUtf16(evc04Connection->chargepointId().data(), evc04Connection->chargepointId().length()).toUtf8()).trimmed(); + qCDebug(dcVestel()) << "Brand:" << QString(QString::fromUtf16(evc04Connection->brand().data(), evc04Connection->brand().length()).toUtf8()).trimmed(); + qCDebug(dcVestel()) << "Model:" << QString(QString::fromUtf16(evc04Connection->model().data(), evc04Connection->model().length()).toUtf8()).trimmed(); + + updateEVC04MaxCurrent(thing); + + // I've been observing the wallbox getting stuck on modbus. It is still functional, but modbus keeps on returning the same old values + // until the TCP connection is closed and reopened. Checking the wallbox time register to detect that and auto-reconnect. + if (m_lastWallboxTime[thing] == evc04Connection->time()) { + qCWarning(dcVestel()) << "Wallbox seems stuck and returning outdated values. Reconnecting..."; + evc04Connection->reconnectDevice(); + } + m_lastWallboxTime[thing] = evc04Connection->time(); + }); + + connect(evc04Connection, &EVC04ModbusTcpConnection::chargepointStateChanged, thing, [thing](EVC04ModbusTcpConnection::ChargePointState chargePointState) { + qCDebug(dcVestel()) << "Chargepoint state changed" << chargePointState; + // switch (chargePointState) { + // case EVC04ModbusTcpConnection::ChargePointStateAvailable: + // case EVC04ModbusTcpConnection::ChargePointStatePreparing: + // case EVC04ModbusTcpConnection::ChargePointStateReserved: + // case EVC04ModbusTcpConnection::ChargePointStateUnavailable: + // case EVC04ModbusTcpConnection::ChargePointStateFaulted: + // thing->setStateValue(evc04PluggedInStateTypeId, false); + // break; + // case EVC04ModbusTcpConnection::ChargePointStateCharging: + // case EVC04ModbusTcpConnection::ChargePointStateSuspendedEVSE: + // case EVC04ModbusTcpConnection::ChargePointStateSuspendedEV: + // case EVC04ModbusTcpConnection::ChargePointStateFinishing: + // thing->setStateValue(evc04PluggedInStateTypeId, true); + // break; + // } + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::chargingStateChanged, thing, [thing](EVC04ModbusTcpConnection::ChargingState chargingState) { + qCDebug(dcVestel()) << "Charging state changed:" << chargingState; + thing->setStateValue(evc04ChargingStateTypeId, chargingState == EVC04ModbusTcpConnection::ChargingStateCharging); + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::activePowerTotalChanged, thing, [thing](quint16 activePowerTotal) { + qCDebug(dcVestel()) << "Total active power:" << activePowerTotal; + // The wallbox reports some 5-6W even when there's nothing connected. Let's hide that if we're not charging + if (thing->stateValue(evc04ChargingStateTypeId).toBool() == true) { + thing->setStateValue(evc04CurrentPowerStateTypeId, activePowerTotal); + } else { + thing->setStateValue(evc04CurrentPowerStateTypeId, 0); + } + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::meterReadingChanged, thing, [thing](quint32 meterReading) { + qCDebug(dcVestel()) << "Meter reading changed:" << meterReading; + thing->setStateValue(evc04TotalEnergyConsumedStateTypeId, meterReading / 10.0); + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::sessionMaxCurrentChanged, thing, [](quint16 sessionMaxCurrent) { + // This mostly just reflects what we've been writing to cargingCurrent, so not of much use... + qCDebug(dcVestel()) << "Session max current changed:" << sessionMaxCurrent; + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::cableMaxCurrentChanged, thing, [this, thing](quint16 cableMaxCurrent) { + qCDebug(dcVestel()) << "Cable max current changed:" << cableMaxCurrent; + updateEVC04MaxCurrent(thing); + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::evseMinCurrentChanged, thing, [thing](quint16 evseMinCurrent) { + qCDebug(dcVestel()) << "EVSE min current changed:" << evseMinCurrent; + thing->setStateMinValue(evc04MaxChargingCurrentStateTypeId, evseMinCurrent); + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::evseMaxCurrentChanged, thing, [this, thing](quint16 evseMaxCurrent) { + qCDebug(dcVestel()) << "EVSE max current changed:" << evseMaxCurrent; + updateEVC04MaxCurrent(thing); + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::sessionEnergyChanged, thing, [thing](quint32 sessionEnergy) { + qCDebug(dcVestel()) << "Session energy changed:" << sessionEnergy; + thing->setStateValue(evc04SessionEnergyStateTypeId, sessionEnergy / 1000.0); + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::chargingCurrentChanged, thing, [thing](quint16 chargingCurrent) { + qCDebug(dcVestel()) << "Charging current changed:" << chargingCurrent; + if (chargingCurrent > 0) { + thing->setStateValue(evc04PowerStateTypeId, true); + thing->setStateValue(evc04MaxChargingCurrentStateTypeId, chargingCurrent); + } else { + thing->setStateValue(evc04PowerStateTypeId, false); + } + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::numPhasesChanged, thing, [thing](EVC04ModbusTcpConnection::NumPhases numPhases) { + switch (numPhases) { + case EVC04ModbusTcpConnection::NumPhases1: + thing->setStateValue(evc04PhaseCountStateTypeId, 1); + break; + case EVC04ModbusTcpConnection::NumPhases3: + thing->setStateValue(evc04PhaseCountStateTypeId, 3); + break; + } + }); + connect(evc04Connection, &EVC04ModbusTcpConnection::cableStateChanged, thing, [evc04Connection, thing](EVC04ModbusTcpConnection::CableState cableState) { + switch (cableState) { + case EVC04ModbusTcpConnection::CableStateNotConnected: + case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleNotConnected: + thing->setStateValue(evc04PluggedInStateTypeId, false); + break; + case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnected: + case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnectedCableLocked: + thing->setStateValue(evc04PluggedInStateTypeId, true); + + // The car was plugged in, sync the power state now as the wallbox only allows to set that when the car is connected + if (thing->stateValue(evc04PowerStateTypeId).toBool() == false) { + qCInfo(dcVestel()) << "Car plugged in. Syncing cached power off state to wallbox"; + evc04Connection->setChargingCurrent(0); + } + + break; + } + }); + + evc04Connection->connectDevice(); +} + +void IntegrationPluginVestel::updateEVC04MaxCurrent(Thing *thing) +{ + EVC04ModbusTcpConnection *connection = m_evc04Connections.value(thing); + quint16 wallboxMax = connection->maxChargePointPower() > 0 ? connection->maxChargePointPower() / 230 : 32; + quint16 evseMax = connection->evseMaxCurrent() > 0 ? connection->evseMaxCurrent() : wallboxMax; + quint16 cableMax = connection->cableMaxCurrent() > 0 ? connection->cableMaxCurrent() : wallboxMax; + + + quint8 overallMax = qMin(qMin(wallboxMax, evseMax), cableMax); + qCDebug(dcVestel()) << "Adjusting max current: Wallbox max:" << wallboxMax << "EVSE max:" << evseMax << "cable max:" << cableMax << "Overall:" << overallMax; + thing->setStateMinMaxValues(evc04MaxChargingCurrentStateTypeId, 6, overallMax); +} diff --git a/vestel/integrationpluginvestel.h b/vestel/integrationpluginvestel.h new file mode 100644 index 0000000..60da25f --- /dev/null +++ b/vestel/integrationpluginvestel.h @@ -0,0 +1,72 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 INTEGRATIONPLUGINVESTEL_H +#define INTEGRATIONPLUGINVESTEL_H + +#include +#include +#include + +#include "extern-plugininfo.h" + +#include "evc04modbustcpconnection.h" + +class IntegrationPluginVestel: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginvestel.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginVestel(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + void setupEVC04Connection(ThingSetupInfo *info); + + void updateEVC04MaxCurrent(Thing *thing); + + PluginTimer *m_pluginTimer = nullptr; + QHash m_evc04Connections; + QHash m_monitors; + + QHash m_lastWallboxTime; + +}; + +#endif // INTEGRATIONPLUGINVESTEL_H + diff --git a/vestel/integrationpluginvestel.json b/vestel/integrationpluginvestel.json new file mode 100644 index 0000000..5ac623f --- /dev/null +++ b/vestel/integrationpluginvestel.json @@ -0,0 +1,120 @@ +{ + "name": "vestel", + "displayName": "Vestel", + "id": "d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea", + "vendors": [ + { + "name": "vestel", + "displayName": "Vestel", + "id": "9a8441f9-f71a-4e3d-a2a0-2f912fa6f047", + "thingClasses": [ + { + "name": "evc04", + "displayName": "EVC04", + "id": "396de19c-ef2b-4f32-b551-9d0d153304a4", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "smartmeterconsumer", "connectable"], + "paramTypes": [ + { + "id": "afdc55b7-b509-4d0c-ab79-176f2edf8488", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "ababb315-4c90-4d7b-8336-de0f22630c32", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "cef29b4b-11e5-4342-9fae-0ac90a70082a", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "c1be88ee-776f-4d05-8dba-ca8086181086", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "73a165bc-23b9-487e-9758-505bcc6cf10c", + "name": "phaseCount", + "displayName": "Connected phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "4646932a-a1f4-4b21-919e-a8c393f4e801", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "dc6eea53-0ae1-4786-b1c2-f1d5a14be9b0", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0, + "cached": true + }, + { + "id": "639f28f3-4956-4d7c-9d72-cbbe8eda0d7a", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "7f619704-36d8-4898-9625-484f96949e1c", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "defaultValue": 6, + "writable": true + }, + { + "id": "b37996f6-5721-419e-b6cc-83a21e945a08", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "0b4d26c3-558a-4b75-92c8-a2b07606e762", + "name": "version", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} diff --git a/vestel/meta.json b/vestel/meta.json new file mode 100644 index 0000000..8f53235 --- /dev/null +++ b/vestel/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Bestel", + "tagline": "Connect Vestel wallboxes to nymea.", + "icon": "vestel.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/vestel/translations/d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea-en_US.ts b/vestel/translations/d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea-en_US.ts new file mode 100644 index 0000000..bcf9eb1 --- /dev/null +++ b/vestel/translations/d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea-en_US.ts @@ -0,0 +1,123 @@ + + + + + IntegrationPluginVestel + + + Unable to discover devices in the network. The system may not be installed correctly. + + + + + The MAC address is not known. Please reconfigure the thing. + + + + + The host address is not known yet. Trying later again. + + + + + Error communicating with the wallbox. + + + + + vestel + + + Active power + The name of the StateType ({4646932a-a1f4-4b21-919e-a8c393f4e801}) of ThingClass evc04 + + + + + Charging + The name of the StateType ({c1be88ee-776f-4d05-8dba-ca8086181086}) of ThingClass evc04 + + + + + + Charging enabled + The name of the ParamType (ThingClass: evc04, ActionType: power, ID: {639f28f3-4956-4d7c-9d72-cbbe8eda0d7a}) +---------- +The name of the StateType ({639f28f3-4956-4d7c-9d72-cbbe8eda0d7a}) of ThingClass evc04 + + + + + Connected + The name of the StateType ({ababb315-4c90-4d7b-8336-de0f22630c32}) of ThingClass evc04 + + + + + Connected phases + The name of the StateType ({73a165bc-23b9-487e-9758-505bcc6cf10c}) of ThingClass evc04 + + + + + EVC04 + The name of the ThingClass ({396de19c-ef2b-4f32-b551-9d0d153304a4}) + + + + + MAC address + The name of the ParamType (ThingClass: evc04, Type: thing, ID: {afdc55b7-b509-4d0c-ab79-176f2edf8488}) + + + + + + Maximum charging current + The name of the ParamType (ThingClass: evc04, ActionType: maxChargingCurrent, ID: {7f619704-36d8-4898-9625-484f96949e1c}) +---------- +The name of the StateType ({7f619704-36d8-4898-9625-484f96949e1c}) of ThingClass evc04 + + + + + Plugged in + The name of the StateType ({cef29b4b-11e5-4342-9fae-0ac90a70082a}) of ThingClass evc04 + + + + + Session energy + The name of the StateType ({b37996f6-5721-419e-b6cc-83a21e945a08}) of ThingClass evc04 + + + + + Set charging enabled + The name of the ActionType ({639f28f3-4956-4d7c-9d72-cbbe8eda0d7a}) of ThingClass evc04 + + + + + Set maximum charging current + The name of the ActionType ({7f619704-36d8-4898-9625-484f96949e1c}) of ThingClass evc04 + + + + + Total consumed energy + The name of the StateType ({dc6eea53-0ae1-4786-b1c2-f1d5a14be9b0}) of ThingClass evc04 + + + + + + Vestel + The name of the vendor ({9a8441f9-f71a-4e3d-a2a0-2f912fa6f047}) +---------- +The name of the plugin vestel ({d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea}) + + + + diff --git a/vestel/vestel.png b/vestel/vestel.png new file mode 100644 index 0000000..336afc8 Binary files /dev/null and b/vestel/vestel.png differ diff --git a/vestel/vestel.pro b/vestel/vestel.pro new file mode 100644 index 0000000..0265b6b --- /dev/null +++ b/vestel/vestel.pro @@ -0,0 +1,15 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += evc04-registers.json + +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + evc04discovery.h \ + integrationpluginvestel.h + +SOURCES += \ + evc04discovery.cpp \ + integrationpluginvestel.cpp diff --git a/wattsonic/README.md b/wattsonic/README.md new file mode 100644 index 0000000..b0a1386 --- /dev/null +++ b/wattsonic/README.md @@ -0,0 +1,4 @@ +# Wattsonic + +This plugin allows to connect wattsonic hybrid inverters to nymea + diff --git a/wattsonic/integrationpluginwattsonic.cpp b/wattsonic/integrationpluginwattsonic.cpp new file mode 100644 index 0000000..f9b5c9c --- /dev/null +++ b/wattsonic/integrationpluginwattsonic.cpp @@ -0,0 +1,262 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "integrationpluginwattsonic.h" +#include "plugininfo.h" +#include "wattsonicdiscovery.h" + +#include +#include + +IntegrationPluginWattsonic::IntegrationPluginWattsonic() +{ + +} + +void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info) +{ + + if (info->thingClassId() == inverterThingClassId) { + + WattsonicDiscovery *discovery = new WattsonicDiscovery(hardwareManager()->modbusRtuResource(), info); + connect(discovery, &WattsonicDiscovery::discoveryFinished, info, [=](bool modbusRtuMasterAvailable){ + if (!modbusRtuMasterAvailable) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No suitable Modbus RTU connection available. Please set up a Modbus RTU master with a baudrate of 9600, 8 data bits, 1 stop bit and no parity.")); + return; + } + + foreach (const WattsonicDiscovery::Result &result, discovery->discoveryResults()) { + + QString name = "Wattsonic hybrid inverter"; + ThingDescriptor descriptor(inverterThingClassId, name, result.serialNumber); + qCDebug(dcWattsonic()) << "Discovered:" << descriptor.title() << descriptor.description(); + + ParamList params { + {inverterThingModbusMasterUuidParamTypeId, result.modbusRtuMasterId}, + {inverterThingSlaveAddressParamTypeId, result.slaveId} + }; + descriptor.setParams(params); + + // Check if we already have set up this device + Thing *existingThing = myThings().findByParams(params); + if (existingThing) { + qCDebug(dcWattsonic()) << "This inverter already exists in the system:" << result.serialNumber; + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + } +} + +void IntegrationPluginWattsonic::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcWattsonic()) << "Setup" << thing << thing->params(); + + if (info->thing()->thingClassId() == inverterThingClassId) { + if (m_connections.contains(thing)) { + qCDebug(dcWattsonic()) << "Reconfiguring existing thing" << thing->name(); + m_connections.take(thing)->deleteLater(); + } + + setupWattsonicConnection(info); + return; + } + + if (info->thing()->thingClassId() == meterThingClassId) { + info->finish(Thing::ThingErrorNoError); + return; + } + + if (info->thing()->thingClassId() == batteryThingClassId) { + info->finish(Thing::ThingErrorNoError); + return; + } + +} + +void IntegrationPluginWattsonic::postSetupThing(Thing *thing) +{ + if (thing->thingClassId() == inverterThingClassId) { + Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); + if (meters.isEmpty()) { + qCInfo(dcWattsonic()) << "No energy meter set up yet. Creating thing..."; + ThingDescriptor descriptor(meterThingClassId, "Wattsonic energy meter", QString(), thing->id()); + emit autoThingsAppeared({descriptor}); + } + Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId); + if (batteries.isEmpty()) { + qCInfo(dcWattsonic()) << "No battery set up yet. Creating thing..."; + ThingDescriptor descriptor(batteryThingClassId, "Wattsonic energy storage", QString(), thing->id()); + emit autoThingsAppeared({descriptor}); + } + } + + + if (!m_pluginTimer) { + qCDebug(dcWattsonic()) << "Starting plugin timer..."; + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { + foreach(WattsonicModbusRtuConnection *connection, m_connections) { + qCDebug(dcWattsonic()) << "Updating connection" << connection->modbusRtuMaster()->serialPort() << connection->slaveId(); + connection->update(); + } + }); + + m_pluginTimer->start(); + } +} + +void IntegrationPluginWattsonic::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == inverterThingClassId && m_connections.contains(thing)) { + delete m_connections.take(thing); + } + + if (myThings().isEmpty() && m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + uint slaveId = thing->paramValue(inverterThingSlaveAddressParamTypeId).toUInt(); + if (slaveId > 247 || slaveId == 0) { + qCWarning(dcWattsonic()) << "Setup failed, slave ID is not valid" << slaveId; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus address not valid. It must be a value between 1 and 247.")); + return; + } + + QUuid uuid = thing->paramValue(inverterThingModbusMasterUuidParamTypeId).toUuid(); + if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(uuid)) { + qCWarning(dcWattsonic()) << "Setup failed, hardware manager not available"; + info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus RTU resource is not available.")); + return; + } + + WattsonicModbusRtuConnection *connection = new WattsonicModbusRtuConnection(hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid), slaveId, this); + connect(info, &ThingSetupInfo::aborted, connection, &ModbusRtuMaster::deleteLater); + + m_connections.insert(thing, connection); + connect(info, &ThingSetupInfo::aborted, this, [=](){ + m_connections.take(info->thing())->deleteLater(); + }); + + connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [connection, thing, this](bool reachable){ + qCDebug(dcWattsonic()) << "Reachable state changed" << reachable; + if (reachable) { + connection->initialize(); + } else { + thing->setStateValue("connected", false); + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + child->setStateValue("connected", true); + } + } + }); + + connect(connection, &WattsonicModbusRtuConnection::initializationFinished, info, [=](bool success){ + qCDebug(dcWattsonic()) << "Initialisation finished" << success; + if (info->isInitialSetup() && !success) { + m_connections.take(info->thing())->deleteLater(); + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + info->finish(Thing::ThingErrorNoError); + + if (success) { + qCDebug(dcWattsonic) << "Firmware version:" << connection->firmwareVersion(); +// info->thing()->setStateValue(inverterCurrentVersionStateTypeId, compact20Connection->firmwareVersion()); + } + }); + + connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [=](bool reachable){ + thing->setStateValue(inverterConnectedStateTypeId, reachable); + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + child->setStateValue("connected", reachable); + } + }); + + + connect(connection, &WattsonicModbusRtuConnection::updateFinished, thing, [this, connection, thing](){ + qCDebug(dcWattsonic()) << "Update finished:" << thing->name() << connection; + + Thing *inverter = thing; + + inverter->setStateValue(inverterCurrentPowerStateTypeId, connection->pAC() * -1.0); + inverter->setStateValue(inverterTotalEnergyProducedStateTypeId, connection->totalPVGenerationFromInstallation() * 0.1); + qCInfo(dcWattsonic()) << "Updating inverter:" << inverter->stateValue(inverterCurrentPowerStateTypeId).toDouble() << "W" << inverter->stateValue(inverterTotalEnergyProducedStateTypeId).toDouble() << "kWh"; + + Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId); + if (!meters.isEmpty()) { + Thing *meter = meters.first(); + meter->setStateValue(meterCurrentPowerStateTypeId, connection->totalPowerOnMeter() * -1.0); + meter->setStateValue(meterTotalEnergyConsumedStateTypeId, connection->totalEnergyPurchasedFromGrid() / 10.0); + meter->setStateValue(meterTotalEnergyProducedStateTypeId, connection->totalEnergyInjectedToGrid() / 10.0); + meter->setStateValue(meterCurrentPowerPhaseAStateTypeId, connection->phaseAPower() * -1.0); + meter->setStateValue(meterCurrentPowerPhaseBStateTypeId, connection->phaseBPower() * -1.0); + meter->setStateValue(meterCurrentPowerPhaseCStateTypeId, connection->phaseCPower() * -1.0); + meter->setStateValue(meterVoltagePhaseAStateTypeId, connection->gridPhaseAVoltage() / 10.0); + meter->setStateValue(meterVoltagePhaseBStateTypeId, connection->gridPhaseBVoltage() / 10.0); + meter->setStateValue(meterVoltagePhaseCStateTypeId, connection->gridPhaseCVoltage() / 10.0); + // The phase current registers don't seem to contain proper values. Calculating ourselves instead +// meter->setStateValue(meterCurrentPhaseAStateTypeId, connection->gridPhaseACurrent() / 10.0); +// meter->setStateValue(meterCurrentPhaseBStateTypeId, connection->gridPhaseBCurrent() / 10.0); +// meter->setStateValue(meterCurrentPhaseCStateTypeId, connection->gridPhaseCCurrent() / 10.0); + meter->setStateValue(meterCurrentPhaseAStateTypeId, (connection->phaseAPower() * -1.0) / (connection->gridPhaseAVoltage() / 10.0)); + meter->setStateValue(meterCurrentPhaseBStateTypeId, (connection->phaseBPower() * -1.0) / (connection->gridPhaseBVoltage() / 10.0)); + meter->setStateValue(meterCurrentPhaseCStateTypeId, (connection->phaseCPower() * -1.0) / (connection->gridPhaseCVoltage() / 10.0)); + qCInfo(dcWattsonic()) << "Updating meter:" << meter->stateValue(meterCurrentPowerStateTypeId).toDouble() << "W" << meter->stateValue(meterTotalEnergyProducedStateTypeId).toDouble() << "kWh"; + } + Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId); + if (!batteries.isEmpty() && connection->SOC() > 0) { + Thing *battery = batteries.first(); + QHash map { + {WattsonicModbusRtuConnection::BatteryModeDischarge, "discharging"}, + {WattsonicModbusRtuConnection::BatteryModeCharge, "charging"} + }; + battery->setStateValue(batteryChargingStateStateTypeId, map.value(connection->batteryMode())); + battery->setStateValue(batteryCurrentPowerStateTypeId, connection->batteryPower() * -1.0); + battery->setStateValue(batteryBatteryLevelStateTypeId, connection->SOC() / 100.0); + battery->setStateValue(batteryBatteryCriticalStateTypeId, connection->SOC() < 500); + } + + }); + + +} diff --git a/wattsonic/integrationpluginwattsonic.h b/wattsonic/integrationpluginwattsonic.h new file mode 100644 index 0000000..7ab7271 --- /dev/null +++ b/wattsonic/integrationpluginwattsonic.h @@ -0,0 +1,66 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 INTEGRATIONPLUGINWATTSONIC_H +#define INTEGRATIONPLUGINWATTSONIC_H + +#include +#include +#include + +#include "extern-plugininfo.h" + +#include "wattsonicmodbusrtuconnection.h" + +class IntegrationPluginWattsonic: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginwattsonic.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginWattsonic(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +private: + void setupWattsonicConnection(ThingSetupInfo *info); + + PluginTimer *m_pluginTimer = nullptr; + QHash m_connections; +}; + +#endif // INTEGRATIONPLUGINWATTSONIC_H + + diff --git a/wattsonic/integrationpluginwattsonic.json b/wattsonic/integrationpluginwattsonic.json new file mode 100644 index 0000000..ee3a1d5 --- /dev/null +++ b/wattsonic/integrationpluginwattsonic.json @@ -0,0 +1,246 @@ +{ + "name": "Wattsonic", + "displayName": "Wattsonic", + "id": "d0eaf684-001e-4b1c-9e37-122955958de3", + "vendors": [ + { + "name": "wattsonic", + "displayName": "Wattsonic", + "id": "c335f9bc-3bc9-46bf-8c50-800cd93e827a", + "thingClasses": [ + { + "name": "inverter", + "displayName": "Wattsonic hybrid inverter", + "id": "688bef8d-2ba8-4eb3-b30e-16193eba02fb", + "createMethods": ["discovery", "user"], + "interfaces": ["solarinverter", "connectable"], + "paramTypes": [ + { + "id": "55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2", + "name": "slaveAddress", + "displayName": "Modbus slave address", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "4f1238b5-07e0-4516-b84a-71670141ef81", + "name": "modbusMasterUuid", + "displayName": "Modbus RTU master", + "type": "QUuid", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "c0f77c00-b82a-478e-826b-bc3204d66100", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "2cbb25e6-c1bd-4216-b354-6ad6fa957e29", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "3107958e-07a2-4ebd-83a1-96fb5998cfb9", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + } + ] + }, + { + "id": "e27a9590-0d13-4d7f-b66b-946dad86b8c8", + "name": "meter", + "displayName": "Wattsonic energy meter", + "createMethods": ["auto"], + "interfaces": ["energymeter", "connectable"], + "stateTypes": [ + { + "id": "d3bf44be-00d0-4119-ad16-45648bc1532a", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "d5ce5ba0-bc1d-4af3-9afa-cbfb6c0d01ec", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "cached": false, + "defaultValue": 0 + }, + { + "id": "60d486a6-337d-4977-9624-78d99657aea9", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "3fd2e110-8851-4530-bb53-e8e5b20cd4cb", + "name": "totalEnergyProduced", + "displayName": "Total returned energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "1def2f00-8d1c-4d22-b662-a31a552c8a82", + "name": "currentPowerPhaseA", + "displayName": "Power consumption phase A", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "404549a1-de1e-42e2-8d5d-f581b8674b7d", + "name": "currentPowerPhaseB", + "displayName": "Power consumption phase B", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "6fccc09c-7c89-45b7-908e-9cbe5bda1c2c", + "name": "currentPowerPhaseC", + "displayName": "Power consumption phase C", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "12798e83-3093-411e-ab8e-5955956717da", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "eef67976-2c39-4d79-8b6d-053e369346a8", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "1e237634-d23c-4c20-b710-b3586bbb988f", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "80c12358-c2d9-4c1b-9150-b7dc3da8a7ae", + "name": "currentPhaseA", + "displayName": "Current phase A", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "d0914688-59ae-4bd9-97ee-1668e7948ab0", + "name": "currentPhaseB", + "displayName": "Current phase B", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "ecb8072f-dcb0-4e5e-8cc9-95947d3b7185", + "name": "currentPhaseC", + "displayName": "Current phase C", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + } + ] + }, + { + "id": "04c4e5fd-7b64-444f-8905-75651833224e", + "name": "battery", + "displayName": "Wattsonic energy storage", + "createMethods": ["auto"], + "interfaces": ["energystorage", "connectable"], + "stateTypes": [ + { + "id": "a8673429-043e-4149-8775-c89cab0b63ca", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "76c70da1-484d-4bb2-8070-22e5e141cadd", + "name": "batteryCritical", + "displayName": "Battery critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "113cac91-24cf-45eb-93bf-e81145ba6c4e", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 0 + }, + { + "id": "dfaa3890-e3d2-4262-be3d-ece18b520ed9", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "608da87c-e45f-471d-975d-936807a94c9a", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "cached": false, + "defaultValue": 0 + }, + { + "id": "5c652ecd-1302-4796-92fa-672a3d9442c8", + "name": "capacity", + "displayName": "Capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + } + ] + } + ] +} diff --git a/wattsonic/meta.json b/wattsonic/meta.json new file mode 100644 index 0000000..3fd2ac8 --- /dev/null +++ b/wattsonic/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Wattsonic", + "tagline": "Connect Wattsonic devices to nymea.", + "icon": "wattsonic.svg", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/wattsonic/translations/d0eaf684-001e-4b1c-9e37-122955958de3-en_US.ts b/wattsonic/translations/d0eaf684-001e-4b1c-9e37-122955958de3-en_US.ts new file mode 100644 index 0000000..edb17f7 --- /dev/null +++ b/wattsonic/translations/d0eaf684-001e-4b1c-9e37-122955958de3-en_US.ts @@ -0,0 +1,65 @@ + + + + + IntegrationPluginWattsonic + + + The Modbus address not valid. It must be a value between 1 and 247. + + + + + The Modbus RTU resource is not available. + + + + + Wattsonic + + + Connected + The name of the StateType ({c0f77c00-b82a-478e-826b-bc3204d66100}) of ThingClass inverter + + + + + Current power consumption + The name of the StateType ({3107958e-07a2-4ebd-83a1-96fb5998cfb9}) of ThingClass inverter + + + + + Modbus RTU master + The name of the ParamType (ThingClass: inverter, Type: thing, ID: {4f1238b5-07e0-4516-b84a-71670141ef81}) + + + + + Modbus slave address + The name of the ParamType (ThingClass: inverter, Type: thing, ID: {55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2}) + + + + + Total energy produced + The name of the StateType ({2cbb25e6-c1bd-4216-b354-6ad6fa957e29}) of ThingClass inverter + + + + + + Wattsonic + The name of the vendor ({c335f9bc-3bc9-46bf-8c50-800cd93e827a}) +---------- +The name of the plugin Wattsonic ({d0eaf684-001e-4b1c-9e37-122955958de3}) + + + + + Wattsonic hybrid inverter + The name of the ThingClass ({688bef8d-2ba8-4eb3-b30e-16193eba02fb}) + + + + diff --git a/wattsonic/wattsonic-registers.json b/wattsonic/wattsonic-registers.json new file mode 100644 index 0000000..383cfba --- /dev/null +++ b/wattsonic/wattsonic-registers.json @@ -0,0 +1,362 @@ +{ + "className": "Wattsonic", + "protocol": "RTU", + "endianness": "BigEndian", + "errorLimitUntilNotReachable": 20, + "checkReachableRegister": "serialNumber", + "enums": [ + { + "name": "InverterStatus", + "values": [ + { + "key": "Wait", + "value": 0 + }, + { + "key": "Check", + "value": 1 + }, + { + "key": "OnGrid", + "value": 2 + }, + { + "key": "Fault", + "value": 3 + }, + { + "key": "Flash", + "value": 4 + }, + { + "key": "OffGrid", + "value": 5 + } + ] + }, + { + "name": "BatteryMode", + "values": [ + { + "key": "Discharge", + "value": 0 + }, + { + "key": "Charge", + "value": 1 + } + ] + } + ], + "blocks": [ + ], + "registers": [ + { + "id": "serialNumber", + "address": 10000, + "size": 8, + "type": "string", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "Serial number", + "access": "RO" + }, + { + "id": "firmwareVersion", + "address": 10011, + "size": 2, + "type": "uint32", + "readSchedule": "init", + "registerType": "holdingRegister", + "description": "Firmware version", + "access": "RO" + }, + { + "id": "inverterStatus", + "address": 10105, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Inverter status", + "enum": "InverterStatus", + "defaultValue": "InverterStatusWait", + "access": "RO" + }, + { + "id": "phaseAPower", + "address": 10994, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Phase A power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "phaseBPower", + "address": 10996, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Phase B power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "phaseCPower", + "address": 10998, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Phase C power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalPowerOnMeter", + "address": 11000, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total power on meter", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalGridInjectionEnergy", + "address": 11002, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total grid injection energy on meter", + "defaultValue": 0, + "unit": "1/100 kWh", + "access": "RO" + }, + { + "id": "totalPurchasingEnergyFromGrid", + "address": 11004, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total purchasing energy from grid on meter", + "defaultValue": 0, + "unit": "1/100 kWh", + "access": "RO" + }, + { + "id": "gridPhaseAVoltage", + "address": 11009, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase A Voltage", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, + { + "id": "gridPhaseACurrent", + "address": 11010, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase A Current", + "defaultValue": 0, + "unit": "1/10 A", + "access": "RO" + }, + { + "id": "gridPhaseBVoltage", + "address": 11011, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase B Voltage", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, + { + "id": "gridPhaseBCurrent", + "address": 11012, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase B Current", + "defaultValue": 0, + "unit": "1/10 A", + "access": "RO" + }, + { + "id": "gridPhaseCVoltage", + "address": 11013, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase C Voltage", + "defaultValue": 0, + "unit": "1/10 V", + "access": "RO" + }, + { + "id": "gridPhaseCCurrent", + "address": 11014, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Grid Phase C Current", + "defaultValue": 0, + "unit": "1/10 A", + "access": "RO" + }, + { + "id": "pAC", + "address": 11016, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "P_AC", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalPVGenerationFromInstallation", + "address": 11020, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total PV Generation from installation", + "defaultValue": 0, + "unit": "1/10 kWh", + "access": "RO" + }, + { + "id": "pvInputTotalPower", + "address": 11028, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "PV Total Input Power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalBackupP", + "address": 30230, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total_Backup_P/AC Active Power", + "unit": "1/1000 kW", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "batteryMode", + "address": 30256, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery mode", + "enum": "BatteryMode", + "defaultValue": "BatteryModeDischarge", + "access": "RO" + }, + { + "id": "batteryPower", + "address": 30258, + "size": 2, + "type": "int32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery power", + "defaultValue": 0, + "unit": "1/1000 kW", + "access": "RO" + }, + { + "id": "totalEnergyInjectedToGrid", + "address": 31102, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total energy injected to grid", + "defaultValue": 0, + "unit": "1/10 kWh", + "access": "RO" + }, + { + "id": "totalEnergyPurchasedFromGrid", + "address": 31104, + "size": 2, + "type": "uint32", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Total energy purchased from grid", + "defaultValue": 0, + "unit": "1/10 kWh", + "access": "RO" + }, + { + "id": "batteryStrings", + "address": 32001, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "Battery strings", + "defaultValue": 0, + "access": "RO" + }, + { + "id": "SOC", + "address": 33000, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "SOC", + "defaultValue": 0, + "unit": "% * 100", + "access": "RO" + }, + { + "id": "SOH", + "address": 33001, + "size": 1, + "type": "uint16", + "readSchedule": "update", + "registerType": "holdingRegister", + "description": "SOH", + "defaultValue": 0, + "unit": "% * 100", + "access": "RO" + } + ] +} diff --git a/wattsonic/wattsonic.pro b/wattsonic/wattsonic.pro new file mode 100644 index 0000000..9875f8e --- /dev/null +++ b/wattsonic/wattsonic.pro @@ -0,0 +1,15 @@ +include(../plugins.pri) + +# Generate modbus connection +MODBUS_CONNECTIONS += wattsonic-registers.json + +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +HEADERS += \ + wattsonicdiscovery.h \ + integrationpluginwattsonic.h + +SOURCES += \ + wattsonicdiscovery.cpp \ + integrationpluginwattsonic.cpp diff --git a/wattsonic/wattsonicdiscovery.cpp b/wattsonic/wattsonicdiscovery.cpp new file mode 100644 index 0000000..5e599d1 --- /dev/null +++ b/wattsonic/wattsonicdiscovery.cpp @@ -0,0 +1,100 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "wattsonicdiscovery.h" +#include "extern-plugininfo.h" + +#include + +QList slaveIdCandidates = {247}; + +WattsonicDiscovery::WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent): + QObject{parent}, + m_modbusRtuResource(modbusRtuResource) +{ + +} + +void WattsonicDiscovery::startDiscovery() +{ + qCInfo(dcWattsonic()) << "Discovery: Searching for Wattsonic device on modbus RTU..."; + + QList candidateMasters; + foreach (ModbusRtuMaster *master, m_modbusRtuResource->modbusRtuMasters()) { + if (master->baudrate() == 9600 && master->dataBits() == 8 && master->stopBits() == 1 && master->parity() == QSerialPort::NoParity) { + candidateMasters.append(master); + } + } + + if (candidateMasters.isEmpty()) { + qCWarning(dcWattsonic()) << "No usable modbus RTU master found."; + emit discoveryFinished(false); + return; + } + + foreach (ModbusRtuMaster *master, candidateMasters) { + if (master->connected()) { + tryConnect(master, 0); + } else { + qCWarning(dcWattsonic()) << "Modbus RTU master" << master->modbusUuid().toString() << "is not connected."; + } + } +} + +QList WattsonicDiscovery::discoveryResults() const +{ + return m_discoveryResults; +} + +void WattsonicDiscovery::tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex) +{ + quint8 slaveId = slaveIdCandidates.at(slaveIdIndex); + qCDebug(dcWattsonic()) << "Scanning modbus RTU master" << master->modbusUuid() << "Slave ID:" << slaveId; + + ModbusRtuReply *reply = master->readHoldingRegister(slaveId, 10000, 8); + connect(reply, &ModbusRtuReply::finished, this, [=](){ + + if (reply->error() == ModbusRtuReply::NoError) { + + QString serialNumber = ModbusDataUtils::convertToString(reply->result(), ModbusDataUtils::ByteOrderBigEndian); + qCDebug(dcWattsonic()) << "Test reply finished!" << reply->error() << reply->result() << serialNumber; + + Result result {master->modbusUuid(), serialNumber, slaveId}; + m_discoveryResults.append(result); + + } + + if (slaveIdIndex < slaveIdCandidates.count() - 1) { + tryConnect(master, slaveIdIndex+1); + } else { + emit discoveryFinished(true); + } + }); +} diff --git a/wattsonic/wattsonicdiscovery.h b/wattsonic/wattsonicdiscovery.h new file mode 100644 index 0000000..1001b52 --- /dev/null +++ b/wattsonic/wattsonicdiscovery.h @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 WATTSONICDISCOVERY_H +#define WATTSONICDISCOVERY_H + +#include +#include + +class WattsonicDiscovery : public QObject +{ + Q_OBJECT +public: + explicit WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent = nullptr); + + struct Result { + QUuid modbusRtuMasterId; + QString serialNumber; + quint16 slaveId; + }; + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(bool modbusRtuMasterAvailable); + +private slots: + void tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex); + +private: + ModbusRtuHardwareResource *m_modbusRtuResource = nullptr; + + QList m_discoveryResults; +}; + +#endif // WATTSONICDISCOVERY_H