diff --git a/debian/control b/debian/control index 1dcd7c8..71da841 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 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/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/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..d4ccdd1 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,139 +582,165 @@ 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) - 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, ' // 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.') @@ -505,12 +750,13 @@ def writeInitMethodImplementationTcp(fileDescriptor, className, registerDefiniti 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 +769,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..8ec6581 100644 --- a/libnymea-modbus/tools/connectiontool/toolcommon.py +++ b/libnymea-modbus/tools/connectiontool/toolcommon.py @@ -352,11 +352,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 +427,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 +451,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 +467,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 +480,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 +492,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 +511,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..2ddc354 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) @@ -72,6 +76,12 @@ def writeTcpHeaderFile(): writeLine(headerFile) writeLine(headerFile, ' bool reachable() 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 +119,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,6 +171,14 @@ 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, ' QModbusReply *m_checkRechableReply = nullptr;') writeLine(headerFile, ' uint m_checkReachableRetries = 0;') @@ -178,16 +191,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 +226,7 @@ def writeTcpSourceFile(): writeLicenseHeader(sourceFile) writeLine(sourceFile) writeLine(sourceFile, '#include "%s"' % headerFileName) + writeLine(sourceFile) writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') @@ -310,8 +332,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 +354,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 +399,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 +423,19 @@ 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();') + + 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 +447,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 +460,11 @@ def writeTcpSourceFile(): writeLine(sourceFile, ' m_communicationWorking = false;') writeLine(sourceFile, ' m_communicationFailedCounter = 0;') writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;') + + if queuedRequests: + writeLine(sourceFile, ' m_updateRequestQueue.clear();') + writeLine(sourceFile, ' m_initRequestQueue.clear();') + writeLine(sourceFile, ' }') writeLine(sourceFile) writeLine(sourceFile, ' evaluateReachableState();') @@ -426,11 +472,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 +514,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 +679,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 +702,7 @@ def writeRtuSourceFile(): writeLicenseHeader(sourceFile) writeLine(sourceFile, '#include "%s"' % headerFileName) + writeLine(sourceFile) writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') writeLine(sourceFile, '#include ') @@ -831,15 +889,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 +1046,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 +1145,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..771cf06 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -21,6 +21,7 @@ PLUGIN_DIRS = \ schrack \ senseair \ sma \ + solax \ stiebeleltron \ sunspec \ unipi \ 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..942b247 --- /dev/null +++ b/solax/integrationpluginsolax.cpp @@ -0,0 +1,472 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 { + // Start update cycle + 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"); + } + } + + // Run the next update cycle + solaxConnection->update(); + }); + + 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() == 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)); + +} + +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..2ffe416 --- /dev/null +++ b/solax/integrationpluginsolax.h @@ -0,0 +1,74 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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: + 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 + + + +