Merge PR #134: New Plugin: Vestel

This commit is contained in:
jenkins 2023-12-15 10:21:37 +01:00
commit dda8f5f564
45 changed files with 5851 additions and 380 deletions

29
debian/control vendored
View File

@ -210,6 +210,14 @@ Description: nymea integration plugin for SMA solar inverters and meters
This package contains the nymea integration plugin for SMA solar inverters and meters.
Package: nymea-plugin-solax
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
Description: nymea integration plugin for Solax modbus devices
This package contains the nymea integration plugin for solax compatible solar inverters, meters and batteries.
Package: nymea-plugin-stiebeleltron
Architecture: any
Section: libs
@ -250,6 +258,27 @@ Description: nymea integration plugin for PhoenixConnect wallboxes
This package contains the nymea integration plugin for wallboxes made
by PhonenixConnect and rebranded as Wallbe, Compleo and Scapo.
Package: nymea-plugin-vestel
Architecture: any
Section: libs
Depends: ${shlibs:Depends},
${misc:Depends},
Description: nymea integration plugin for Vestel wallboxes
This package contains the nymea integration plugin for wallboxes made
by Vestel.
Package: nymea-plugin-wattsonic
Architecture: any
Section: libs
Depends: ${shlibs:Depends},
${misc:Depends},
Description: nymea integration plugin for Wattsonic hybrid inverters
This package contains the nymea integration plugin for hybrid inverters made
by Wattsonic.
Package: nymea-plugin-webasto
Architecture: any
Section: libs

2
debian/nymea-plugin-solax.install.in vendored Normal file
View File

@ -0,0 +1,2 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsolax.so
solax/translations/*qm usr/share/nymea/translations/

5
debian/nymea-plugin-vestel.install.in vendored Normal file
View File

@ -0,0 +1,5 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginvestel.so
vestel/translations/*qm usr/share/nymea/translations/

View File

@ -0,0 +1,2 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginwattsonic.so
wattsonic/translations/*qm usr/share/nymea/translations/

View File

@ -146,6 +146,16 @@ QString ModbusDataUtils::convertToString(const QVector<quint16> &registers, Byte
return QString::fromUtf8(bytes).trimmed();
}
QByteArray ModbusDataUtils::convertToByteArray(const QVector<quint16> &registers)
{
QByteArray bytes;
QDataStream stream(&bytes, QIODevice::WriteOnly);
for (int i = 0; i < registers.count(); i++) {
stream << registers.at(i);
}
return bytes;
}
float ModbusDataUtils::convertToFloat32(const QVector<quint16> &registers, ByteOrder byteOrder)
{
Q_ASSERT_X(registers.count() == 2, "ModbusDataUtils", "invalid raw data size for converting value to float32");

View File

@ -90,6 +90,7 @@ public:
static quint64 convertToUInt64(const QVector<quint16> &registers, ByteOrder byteOrder = ByteOrderLittleEndian);
static qint64 convertToInt64(const QVector<quint16> &registers, ByteOrder byteOrder = ByteOrderLittleEndian);
static QString convertToString(const QVector<quint16> &registers, ByteOrder characterByteOrder = ByteOrderLittleEndian);
static QByteArray convertToByteArray(const QVector<quint16> &registers);
static float convertToFloat32(const QVector<quint16> &registers, ByteOrder byteOrder = ByteOrderLittleEndian);
static double convertToFloat64(const QVector<quint16> &registers, ByteOrder byteOrder = ByteOrderLittleEndian);

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -219,6 +219,12 @@ def getCppDataType(registerDefinition, rawType = False):
if registerDefinition['type'] == 'string':
return 'QString'
if registerDefinition['type'] == 'bytearray':
return 'QByteArray'
if registerDefinition['type'] == 'raw':
return 'QVector<quint16>'
def getConversionToValueMethod(registerDefinition):
# Handle enums
@ -335,6 +341,10 @@ def getValueConversionMethod(registerDefinition):
return ('ModbusDataUtils::convertToFloat64(values, m_endianness)')
elif registerDefinition['type'] == 'string':
return ('ModbusDataUtils::convertToString(values, m_stringEndianness)')
elif registerDefinition['type'] == 'bytearray':
return ('ModbusDataUtils::convertToByteArray(values)')
elif registerDefinition['type'] == 'raw':
return ('values')
def writeBlockGetMethodDeclarations(fileDescriptor, registerDefinitions):
@ -352,11 +362,11 @@ def writeBlockGetMethodDeclarations(fileDescriptor, registerDefinitions):
def writePropertyUpdateMethodDeclarations(fileDescriptor, registerDefinitions):
for registerDefinition in registerDefinitions:
if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init':
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
writeLine(fileDescriptor, ' void update%s();' % (propertyName[0].upper() + propertyName[1:]))
@ -427,12 +437,23 @@ def writeBlocksUpdateMethodDeclarations(fileDescriptor, blockDefinitions):
def writeRegistersDebugLine(fileDescriptor, debugObjectParamName, registerDefinitions):
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
line = ('" - %s - %s: " << %s->%s()' % (registerDefinition['address'], registerDefinition['description'], debugObjectParamName, propertyName))
registerType = registerDefinition['registerType']
typeString = ''
if registerType == 'holdingRegister':
typeString = 'holding '
elif registerType == 'inputRegister':
typeString = 'input '
elif registerType == 'coils':
typeString = 'coils '
elif registerType == 'discreteInputs':
typeString = 'discrete'
line = ('" - %s %s | %s: " << %s->%s()' % (typeString, registerDefinition['address'], registerDefinition['description'], debugObjectParamName, propertyName))
if 'unit' in registerDefinition and registerDefinition['unit'] != '':
line += (' << " [%s]"' % registerDefinition['unit'])
writeLine(fileDescriptor, ' debug.nospace().noquote() << %s << "\\n";' % (line))
@ -440,8 +461,9 @@ def writeRegistersDebugLine(fileDescriptor, debugObjectParamName, registerDefini
def writePropertyChangedSignals(fileDescriptor, registerDefinitions):
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
@ -455,8 +477,9 @@ def writePropertyChangedSignals(fileDescriptor, registerDefinitions):
def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions):
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
@ -467,10 +490,10 @@ def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions):
def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions):
propertyVariables = []
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
writeLine(fileDescriptor, ' void process%sRegisterValues(const QVector<quint16> &values);' % (propertyName[0].upper() + propertyName[1:]))
@ -479,10 +502,10 @@ def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions):
def writePropertyProcessMethodImplementations(fileDescriptor, className, registerDefinitions):
propertyVariables = []
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyTyp = getCppDataType(registerDefinition)
propertyName = registerDefinition['id']
@ -498,3 +521,55 @@ def writePropertyProcessMethodImplementations(fileDescriptor, className, registe
writeLine(fileDescriptor, ' }')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeSendNextQueuedInitRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::sendNextQueuedInitRequest()' % (className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_initRequestQueue.isEmpty())')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' if (m_currentInitReply)')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' %s::Function function = m_initRequestQueue.dequeue();' % (className))
writeLine(fileDescriptor, ' (this->*function)();')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeEnqueueInitRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::enqueueInitRequest(%s::Function function)' % (className, className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_initRequestQueue.contains(function))')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' m_initRequestQueue.enqueue(function);')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeSendNextQueuedRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::sendNextQueuedRequest()' % (className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_updateRequestQueue.isEmpty())')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' if (m_currentUpdateReply)')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' %s::Function function = m_updateRequestQueue.dequeue();' % (className))
writeLine(fileDescriptor, ' (this->*function)();')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeEnqueueRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::enqueueRequest(%s::Function function)' % (className, className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_updateRequestQueue.contains(function))')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' m_updateRequestQueue.enqueue(function);')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)

View File

@ -62,6 +62,10 @@ def writeTcpHeaderFile():
for enumDefinition in registerJson['enums']:
writeEnumDefinition(headerFile, enumDefinition)
if queuedRequests:
writeLine(headerFile, ' typedef void(%s::*Function)(void);' % className)
writeLine(headerFile)
# Constructor
writeLine(headerFile, ' explicit %s(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent = nullptr);' % className)
writeLine(headerFile, ' explicit %s(ModbusTcpMaster *modbusTcpMaster, quint16 slaveId, QObject *parent = nullptr);' % className)
@ -71,7 +75,14 @@ def writeTcpHeaderFile():
writeLine(headerFile, ' quint16 slaveId() const;')
writeLine(headerFile)
writeLine(headerFile, ' bool reachable() const;')
writeLine(headerFile, ' bool initializing() const;')
writeLine(headerFile)
# Write init and update method declarations
writeLine(headerFile, ' virtual bool initialize();')
writeLine(headerFile, ' virtual bool update();')
writeLine(headerFile)
writeLine(headerFile, ' ModbusDataUtils::ByteOrder endianness() const;')
writeLine(headerFile, ' void setEndianness(ModbusDataUtils::ByteOrder endianness);')
writeLine(headerFile)
@ -109,11 +120,6 @@ def writeTcpHeaderFile():
writeLine(headerFile)
# Write init and update method declarations
writeLine(headerFile, ' virtual bool initialize();')
writeLine(headerFile, ' virtual bool update();')
writeLine(headerFile)
writeLine(headerFile, 'public slots:')
writeLine(headerFile, ' bool connectDevice();')
writeLine(headerFile, ' void disconnectDevice();')
@ -166,7 +172,16 @@ def writeTcpHeaderFile():
writeLine(headerFile, ' ModbusDataUtils::ByteOrder m_stringEndianness = ModbusDataUtils::ByteOrder%s;' % stringEndianness)
writeLine(headerFile, ' quint16 m_slaveId = 1;')
writeLine(headerFile)
if queuedRequests:
writeLine(headerFile, ' QModbusReply *m_currentInitReply = nullptr;')
writeLine(headerFile, ' QQueue<%s::Function> m_initRequestQueue;' % className)
writeLine(headerFile, ' QModbusReply *m_currentUpdateReply = nullptr;')
writeLine(headerFile, ' QQueue<%s::Function> m_updateRequestQueue;' % className)
writeLine(headerFile)
writeLine(headerFile, ' bool m_reachable = false;')
writeLine(headerFile, ' bool m_initializing = false;')
writeLine(headerFile, ' QModbusReply *m_checkRechableReply = nullptr;')
writeLine(headerFile, ' uint m_checkReachableRetries = 0;')
writeLine(headerFile, ' uint m_checkReachableRetriesCount = 0;')
@ -178,16 +193,24 @@ def writeTcpHeaderFile():
writeLine(headerFile, ' QVector<QModbusReply *> m_pendingUpdateReplies;')
writeLine(headerFile)
writeLine(headerFile, ' QObject *m_initObject = nullptr;')
writeLine(headerFile, ' void verifyInitFinished();')
writeLine(headerFile, ' bool verifyInitFinished();')
writeLine(headerFile, ' void finishInitialization(bool success);')
writeLine(headerFile)
writeLine(headerFile, ' void setupConnection();')
writeLine(headerFile)
writeLine(headerFile, ' void verifyUpdateFinished();')
writeLine(headerFile, ' bool verifyUpdateFinished();')
writeLine(headerFile)
writeLine(headerFile, ' void onReachabilityCheckFailed();')
writeLine(headerFile, ' void evaluateReachableState();')
if queuedRequests:
writeLine(headerFile)
writeLine(headerFile, ' void sendNextQueuedInitRequest();')
writeLine(headerFile, ' void enqueueInitRequest(%s::Function function);' % (className))
writeLine(headerFile, ' void sendNextQueuedRequest();')
writeLine(headerFile, ' void enqueueRequest(%s::Function function);' % (className))
# End of class
writeLine(headerFile)
writeLine(headerFile, '};')
@ -205,6 +228,7 @@ def writeTcpSourceFile():
writeLicenseHeader(sourceFile)
writeLine(sourceFile)
writeLine(sourceFile, '#include "%s"' % headerFileName)
writeLine(sourceFile)
writeLine(sourceFile, '#include <loggingcategories.h>')
writeLine(sourceFile, '#include <math.h>')
writeLine(sourceFile, '#include <QTimer>')
@ -251,6 +275,12 @@ def writeTcpSourceFile():
writeLine(sourceFile, '}')
writeLine(sourceFile)
writeLine(sourceFile, 'bool %s::initializing() const' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' return m_initializing;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
writeLine(sourceFile, 'uint %s::checkReachableRetries() const' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' return m_checkReachableRetries;')
@ -310,8 +340,8 @@ def writeTcpSourceFile():
if 'blocks' in registerJson:
blocks = registerJson['blocks']
writeInitMethodImplementationTcp(sourceFile, className, registerJson['registers'], blocks)
writeUpdateMethodTcp(sourceFile, className, registerJson['registers'], blocks)
writeInitMethodImplementationTcp(sourceFile, className, registerJson['registers'], blocks, queuedRequests)
writeUpdateMethodTcp(sourceFile, className, registerJson['registers'], blocks, queuedRequests)
writeLine(sourceFile, 'bool %s::connectDevice()' % (className))
writeLine(sourceFile, '{')
@ -332,13 +362,13 @@ def writeTcpSourceFile():
writeLine(sourceFile)
# Write update methods
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, registerJson['registers'])
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, registerJson['registers'], queuedRequests, queuedRequestsDelay)
if 'blocks' in registerJson:
for blockDefinition in registerJson['blocks']:
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, blockDefinition['registers'])
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, blockDefinition['registers'], queuedRequests, queuedRequestsDelay)
# Write block update method
writeBlockUpdateMethodImplementationsTcp(sourceFile, className, registerJson['blocks'])
writeBlockUpdateMethodImplementationsTcp(sourceFile, className, registerJson['blocks'], queuedRequests, queuedRequestsDelay)
# Write internal protected property read method implementations
writeInternalPropertyReadMethodImplementationsTcp(sourceFile, className, registerJson['registers'])
@ -377,11 +407,19 @@ def writeTcpSourceFile():
writeTestReachabilityImplementationsTcp(sourceFile, className, registerJson['registers'], checkReachableRegister)
writeLine(sourceFile, 'void %s::verifyInitFinished()' % (className))
writeLine(sourceFile, 'bool %s::verifyInitFinished()' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {')
writeLine(sourceFile, ' finishInitialization(true);')
writeLine(sourceFile, ' }')
if queuedRequests:
writeLine(sourceFile, ' if (m_initRequestQueue.isEmpty() && !m_currentInitReply) {')
writeLine(sourceFile, ' finishInitialization(true);')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
else:
writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {')
writeLine(sourceFile, ' finishInitialization(true);')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
writeLine(sourceFile, ' return false;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -393,12 +431,20 @@ def writeTcpSourceFile():
writeLine(sourceFile, ' qCWarning(dc%s()) << "Initialization finished of %s" << m_modbusTcpMaster->hostAddress().toString() << "failed.";' % (className, className))
writeLine(sourceFile, ' }')
writeLine(sourceFile)
writeLine(sourceFile, ' // Cleanup init')
writeLine(sourceFile, ' delete m_initObject;')
writeLine(sourceFile, ' m_initObject = nullptr;')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile, ' m_initializing = false;')
if queuedRequests:
writeLine(sourceFile, ' m_initRequestQueue.clear();')
else:
writeLine(sourceFile, ' // Cleanup init')
writeLine(sourceFile, ' delete m_initObject;')
writeLine(sourceFile, ' m_initObject = nullptr;')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile)
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' QTimer::singleShot(0, this, [this, success](){')
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' });')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -410,6 +456,10 @@ def writeTcpSourceFile():
writeLine(sourceFile, ' // Cleanup before starting to initialize')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile, ' m_pendingUpdateReplies.clear();')
if queuedRequests:
writeLine(sourceFile, ' m_updateRequestQueue.clear();')
writeLine(sourceFile, ' m_initRequestQueue.clear();')
writeLine(sourceFile, ' m_communicationWorking = false;')
writeLine(sourceFile, ' m_communicationFailedCounter = 0;')
writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;')
@ -419,6 +469,12 @@ def writeTcpSourceFile():
writeLine(sourceFile, ' m_communicationWorking = false;')
writeLine(sourceFile, ' m_communicationFailedCounter = 0;')
writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;')
writeLine(sourceFile, ' m_initializing = false;')
if queuedRequests:
writeLine(sourceFile, ' m_updateRequestQueue.clear();')
writeLine(sourceFile, ' m_initRequestQueue.clear();')
writeLine(sourceFile, ' }')
writeLine(sourceFile)
writeLine(sourceFile, ' evaluateReachableState();')
@ -426,11 +482,18 @@ def writeTcpSourceFile():
writeLine(sourceFile, '}')
writeLine(sourceFile)
writeLine(sourceFile, 'void %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, 'bool %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {')
writeLine(sourceFile, ' emit updateFinished();')
if queuedRequests:
writeLine(sourceFile, ' if (m_updateRequestQueue.isEmpty() && !m_currentUpdateReply) {')
writeLine(sourceFile, ' emit updateFinished();')
writeLine(sourceFile, ' return true;')
else:
writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {')
writeLine(sourceFile, ' emit updateFinished();')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
writeLine(sourceFile, ' return false;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -461,7 +524,11 @@ def writeTcpSourceFile():
writeLine(sourceFile, '}')
writeLine(sourceFile)
if queuedRequests:
writeSendNextQueuedInitRequestMethodImplementation(sourceFile, className)
writeEnqueueInitRequestMethodImplementation(sourceFile, className)
writeSendNextQueuedRequestMethodImplementation(sourceFile, className)
writeEnqueueRequestMethodImplementation(sourceFile, className)
# Write the debug print
debugObjectParamName = className[0].lower() + className[1:]
@ -622,7 +689,7 @@ def writeRtuHeaderFile():
writeLine(headerFile, ' void verifyInitFinished();')
writeLine(headerFile, ' void finishInitialization(bool success);')
writeLine(headerFile)
writeLine(headerFile, ' void verifyUpdateFinished();')
writeLine(headerFile, ' bool verifyUpdateFinished();')
writeLine(headerFile)
writeLine(headerFile, ' void onReachabilityCheckFailed();')
writeLine(headerFile, ' void evaluateReachableState();')
@ -645,6 +712,7 @@ def writeRtuSourceFile():
writeLicenseHeader(sourceFile)
writeLine(sourceFile, '#include "%s"' % headerFileName)
writeLine(sourceFile)
writeLine(sourceFile, '#include <loggingcategories.h>')
writeLine(sourceFile, '#include <math.h>')
writeLine(sourceFile, '#include <QTimer>')
@ -831,15 +899,26 @@ def writeRtuSourceFile():
writeLine(sourceFile, ' m_initObject = nullptr;')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile)
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' QTimer::singleShot(0, this, [this, success](){')
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' });')
if queuedRequests:
writeLine(sourceFile)
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile, ' m_pendingUpdateReplies.clear();')
writeLine(sourceFile, ' update();')
writeLine(sourceFile, '}')
writeLine(sourceFile)
writeLine(sourceFile, 'void %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, 'bool %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {')
writeLine(sourceFile, ' emit updateFinished();')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
writeLine(sourceFile, ' return false;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -977,15 +1056,28 @@ else:
logger.debug('Verified successfully checkReachableRegister: %s' % checkReachableRegister['id'])
queuedRequests = False
queuedRequestsDelay = 0
if 'queuedRequests' in registerJson:
queuedRequests = registerJson['queuedRequests']
if 'queuedRequestsDelay' in registerJson:
queuedRequestsDelay = registerJson['queuedRequestsDelay']
# Inform about parsed and validated configs if debugging enabled
logger.debug('Script path: %s' % scriptPath)
logger.debug('Output directory: %s' % outputDirectory)
logger.debug('Class name prefix: %s' % classNamePrefix)
logger.debug('Endianness: %s' % endianness)
logger.debug('String endianness: %s' % stringEndianness)
logger.debug('Queued requests: %s' % queuedRequests)
logger.debug('Queued requests delay: %s ms' % queuedRequestsDelay)
logger.debug('Error limit until not reachable: %s' % errorLimitUntilNotReachable)
logger.debug('Check reachable register: %s' % checkReachableRegister['id'])
protocol = 'TCP'
if 'protocol' in registerJson:
protocol = registerJson['protocol']
@ -1063,7 +1155,7 @@ projectIncludeFile = open(projectIncludeFilePath, 'w')
writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #')
writeLine(projectIncludeFile, '#')
writeLine(projectIncludeFile, '# This file has been autogenerated.')
writeLine(projectIncludeFile, '# Any changes in this file may be overwritten from qmake.')
writeLine(projectIncludeFile, '# Any changes in this file may be overwritten on a rebuild.')
writeLine(projectIncludeFile, '#')
writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #')
writeLine(projectIncludeFile)

View File

@ -21,9 +21,12 @@ PLUGIN_DIRS = \
schrack \
senseair \
sma \
solax \
stiebeleltron \
sunspec \
unipi \
vestel \
wattsonic \
webasto \
gcc {

View File

@ -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);

19
solax/README.md Normal file
View File

@ -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.

View File

@ -0,0 +1,496 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginsolax.h"
#include "plugininfo.h"
#include "solaxdiscovery.h"
#include <network/networkdevicediscovery.h>
#include <hardwaremanager.h>
IntegrationPluginSolax::IntegrationPluginSolax()
{
}
void IntegrationPluginSolax::discoverThings(ThingDiscoveryInfo *info)
{
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcSolax()) << "The network discovery is not available on this platform.";
info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available."));
return;
}
// Create a discovery with the info as parent for auto deleting the object once the discovery info is done
SolaxDiscovery *discovery = new SolaxDiscovery(hardwareManager()->networkDeviceDiscovery(), 502, 1, info);
connect(discovery, &SolaxDiscovery::discoveryFinished, info, [=](){
foreach (const SolaxDiscovery::SolaxDiscoveryResult &result, discovery->discoveryResults()) {
QString title;
if (result.productName.isEmpty()) {
title = "SolaX Inverter";
} else {
title = "SolaX " + result.productName;
}
if (!result.serialNumber.isEmpty())
title.append(" " + result.serialNumber);
ThingDescriptor descriptor(solaxInverterTcpThingClassId, title, result.networkDeviceInfo.address().toString() + " " + result.networkDeviceInfo.macAddress());
qCInfo(dcSolax()) << "Discovered:" << descriptor.title() << descriptor.description();
// Check if we already have set up this device
Things existingThings = myThings().filterByParam(solaxInverterTcpThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
if (existingThings.count() == 1) {
qCDebug(dcSolax()) << "This solax inverter already exists in the system:" << result.networkDeviceInfo;
descriptor.setThingId(existingThings.first()->id());
}
ParamList params;
params << Param(solaxInverterTcpThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
// Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults...
descriptor.setParams(params);
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
// Start the discovery process
discovery->startDiscovery();
}
void IntegrationPluginSolax::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCInfo(dcSolax()) << "Setup" << thing << thing->params();
// Inverter (connection)
if (thing->thingClassId() == solaxInverterTcpThingClassId) {
// Handle reconfigure
if (m_tcpConnections.contains(thing)) {
qCDebug(dcSolax()) << "Reconfiguring existing thing" << thing->name();
m_tcpConnections.take(thing)->deleteLater();
if (m_monitors.contains(thing)) {
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
}
MacAddress macAddress = MacAddress(thing->paramValue(solaxInverterTcpThingMacAddressParamTypeId).toString());
if (!macAddress.isValid()) {
qCWarning(dcSolax()) << "The configured mac address is not valid" << thing->params();
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing."));
return;
}
// Create the monitor
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
m_monitors.insert(thing, monitor);
connect(info, &ThingSetupInfo::aborted, monitor, [=](){
// Clean up in case the setup gets aborted
if (m_monitors.contains(thing)) {
qCDebug(dcSolax()) << "Unregister monitor because the setup has been aborted.";
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
});
QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address();
uint port = thing->paramValue(solaxInverterTcpThingPortParamTypeId).toUInt();
quint16 slaveId = thing->paramValue(solaxInverterTcpThingSlaveIdParamTypeId).toUInt();
qCInfo(dcSolax()) << "Setting up solax on" << address.toString() << port << "unit ID:" << slaveId;
SolaxModbusTcpConnection *solaxConnection = new SolaxModbusTcpConnection(address, port, slaveId, this);
connect(info, &ThingSetupInfo::aborted, solaxConnection, &SolaxModbusTcpConnection::deleteLater);
// Reconnect on monitor reachable changed
connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
qCDebug(dcSolax()) << "Network device monitor reachable changed for" << thing->name() << reachable;
if (!thing->setupComplete())
return;
if (reachable && !thing->stateValue("connected").toBool()) {
solaxConnection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address());
solaxConnection->reconnectDevice();
} else if (!reachable) {
// Note: We disable autoreconnect explicitly and we will
// connect the device once the monitor says it is reachable again
solaxConnection->disconnectDevice();
}
});
connect(solaxConnection, &SolaxModbusTcpConnection::reachableChanged, thing, [this, thing, solaxConnection](bool reachable){
qCDebug(dcSolax()) << "Reachable changed to" << reachable << "for" << thing;
if (reachable) {
// Connected true will be set after successfull init
solaxConnection->initialize();
} else {
thing->setStateValue("connected", false);
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
childThing->setStateValue("connected", false);
}
// Reset any energy data due to connection loss
Thing *child = getMeterThing(thing, 1);
if (child) {
child->setStateValue(solaxMeterCurrentPowerStateTypeId, 0);
}
child = getMeterThing(thing, 2);
if (child) {
child->setStateValue(solaxMeterCurrentPowerStateTypeId, 0);
child->setStateValue(solaxMeterCurrentPowerPhaseAStateTypeId, 0);
child->setStateValue(solaxMeterCurrentPowerPhaseBStateTypeId, 0);
child->setStateValue(solaxMeterCurrentPowerPhaseCStateTypeId, 0);
}
Things inverterThings = myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId);
if (!inverterThings.isEmpty()) {
child = inverterThings.first();
child->setSettingValue(solaxInverterChildCurrentPowerStateTypeId, 0);
}
child = getBatteryThing(thing);
if (child) {
child->setStateValue(solaxBatteryVoltageStateTypeId, solaxConnection->batteryVoltage());
child->setStateValue(solaxBatteryCurrentPowerStateTypeId, 0);
child->setStateValue(solaxBatteryChargingStateStateTypeId, "idle");
}
}
});
connect(solaxConnection, &SolaxModbusTcpConnection::initializationFinished, thing, [=](bool success){
thing->setStateValue("connected", success);
foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
childThing->setStateValue("connected", success);
}
if (!success) {
// Try once to reconnect the device
solaxConnection->reconnectDevice();
} else {
qCInfo(dcSolax()) << "Connection initialized successfully for" << thing;
solaxConnection->update();
}
});
connect(solaxConnection, &SolaxModbusTcpConnection::updateFinished, thing, [=](){
qCDebug(dcSolax()) << "Updated" << solaxConnection;
ThingClass meterThingClass = thingClass(solaxMeterThingClassId);
// Check if we have to create the meter for this solax inverter, or remove it due to communication errors
if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).isEmpty()) {
if (solaxConnection->meter1ComState() == 1 && myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 1).isEmpty()) {
qCDebug(dcSolax()) << "There is no meter set up for this inverter. Creating a meter 1 for" << thing << solaxConnection->modbusTcpMaster();
ThingDescriptor descriptor(solaxMeterThingClassId, meterThingClass.displayName(), QString(), thing->id());
ParamList params;
params.append(Param(solaxMeterThingIdParamTypeId, 1));
descriptor.setParams(params);
emit autoThingsAppeared(ThingDescriptors() << descriptor);
}
}
// Note: it is possible to connect an additional inverter to the solax inverter, which will be measured by the meter 2.
// if so, we create a child inverter instead of the second meter thing.
if (solaxConnection->meter2ComState() == 1) {
if (thing->setting(solaxInverterTcpSettingsMeter2InverterParamTypeId).toBool()) {
if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId).isEmpty()) {
// Add the meter 2 as child inverter
emit autoThingsAppeared(ThingDescriptors() << ThingDescriptor(solaxInverterChildThingClassId, "SolaX inverter", QString(), thing->id()));
}
} else {
if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 2).isEmpty()) {
ThingDescriptor descriptor(solaxMeterThingClassId, meterThingClass.displayName() + " 2", QString(), thing->id());
ParamList params;
params.append(Param(solaxMeterThingIdParamTypeId, 2));
descriptor.setParams(params);
emit autoThingsAppeared(ThingDescriptors() << descriptor);
}
}
} else {
// Communication error with meter 2, remove any child devices for meter 2 registers
cleanupMeter2(thing);
cleanupChildInverters(thing);
}
// Check if we have to create the battery for the solax inverter
if (solaxConnection->batteryConnected() != 0 && myThings().filterByParentId(thing->id()).filterByThingClassId(solaxBatteryThingClassId).isEmpty()) {
qCDebug(dcSolax()) << "There is a battery connected but not set up yet. Creating a battery...";
ThingClass batteryThingClass = thingClass(solaxBatteryThingClassId);
ThingDescriptor descriptor(solaxBatteryThingClassId, batteryThingClass.displayName(), QString(), thing->id());
emit autoThingsAppeared(ThingDescriptors() << descriptor);
}
// Update inverter states
thing->setStateValue(solaxInverterTcpCurrentPowerStateTypeId, -solaxConnection->inverterPower());
thing->setStateValue(solaxInverterTcpCurrentStateTypeId, -solaxConnection->inverterCurrent());
thing->setStateValue(solaxInverterTcpCurrentVoltageStateTypeId, solaxConnection->inverterVoltage());
thing->setStateValue(solaxInverterTcpTemperatureStateTypeId, solaxConnection->temperature());
thing->setStateValue(solaxInverterTcpFrequencyStateTypeId, solaxConnection->inverterFrequency());
thing->setStateValue(solaxInverterTcpTotalEnergyProducedStateTypeId, solaxConnection->totalEnergyProduced());
// Update the meter 1 if available
Thing *meterThing = getMeterThing(thing, 1);
if (meterThing) {
meterThing->setStateValue(solaxMeterTotalEnergyConsumedStateTypeId, solaxConnection->meterTotalEnergyConsumend());
meterThing->setStateValue(solaxMeterTotalEnergyProducedStateTypeId, solaxConnection->meterTotalEnergyProduced());
// Power
meterThing->setStateValue(solaxMeterCurrentPowerStateTypeId, -solaxConnection->meterPower());
}
// Update inverter 2 states if available
if (thing->setting(solaxInverterTcpSettingsMeter2InverterParamTypeId).toBool()) {
Things childInverters = myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId);
if (!childInverters.isEmpty()) {
Thing *childInverter = childInverters.first();
childInverter->setStateValue(solaxInverterChildCurrentPowerStateTypeId, -solaxConnection->meter2Power());
childInverter->setStateValue(solaxInverterChildTotalEnergyProducedStateTypeId, solaxConnection->meter2EnergyProduced());
}
} else {
// Update the meter 2 if available
meterThing = getMeterThing(thing, 2);
if (meterThing) {
meterThing->setStateValue(solaxMeterTotalEnergyConsumedStateTypeId, solaxConnection->meter2EnergyConsumed());
meterThing->setStateValue(solaxMeterTotalEnergyProducedStateTypeId, solaxConnection->meter2EnergyProduced());
// Power
meterThing->setStateValue(solaxMeterCurrentPowerStateTypeId, solaxConnection->meter2Power());
meterThing->setStateValue(solaxMeterCurrentPowerPhaseAStateTypeId, solaxConnection->meter2PowerR());
meterThing->setStateValue(solaxMeterCurrentPowerPhaseBStateTypeId, solaxConnection->meter2PowerS());
meterThing->setStateValue(solaxMeterCurrentPowerPhaseCStateTypeId, solaxConnection->meter2PowerT());
}
}
// Update the battery if available
Thing *batteryThing = getBatteryThing(thing);
if (batteryThing) {
batteryThing->setStateValue(solaxBatteryVoltageStateTypeId, solaxConnection->batteryVoltage());
batteryThing->setStateValue(solaxBatteryTemperatureStateTypeId, solaxConnection->batteryTemperature());
batteryThing->setStateValue(solaxBatteryBatteryLevelStateTypeId, solaxConnection->batteryCapacity());
batteryThing->setStateValue(solaxBatteryBatteryCriticalStateTypeId, solaxConnection->batteryCapacity() < 5);
double batteryPower = solaxConnection->batteryPower();
batteryThing->setStateValue(solaxBatteryCurrentPowerStateTypeId, solaxConnection->batteryPower());
if (batteryPower == 0) {
batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "idle");
} else if (batteryPower < 0) {
batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "discharging");
} else if (batteryPower > 0) {
batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "charging");
}
}
});
connect(thing, &Thing::settingChanged, solaxConnection, [this, thing](const ParamTypeId &paramTypeId, const QVariant &value){
if (paramTypeId == solaxInverterTcpSettingsMeter2InverterParamTypeId) {
// Note: we just need to cleanup here, if there is any device connected, it will be created in the update method.
if (value.toBool()) {
// The meter will be used as inverter. Clean up any meters for meter 2 registers...
cleanupMeter2(thing);
} else {
// The meter will be used as meter. Clean up any child inverters for meter 2 registers...
cleanupChildInverters(thing);
}
}
});
m_tcpConnections.insert(thing, solaxConnection);
if (monitor->reachable())
solaxConnection->connectDevice();
info->finish(Thing::ThingErrorNoError);
return;
}
// Meter
if (thing->thingClassId() == solaxMeterThingClassId) {
// Get the parent thing and the associated connection
Thing *connectionThing = myThings().findById(thing->parentId());
if (!connectionThing) {
qCWarning(dcSolax()) << "Failed to set up solax energy meter because the parent thing with ID" << thing->parentId().toString() << "could not be found.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
SolaxModbusTcpConnection *solaxConnection = m_tcpConnections.value(connectionThing);
if (!solaxConnection) {
qCWarning(dcSolax()) << "Failed to set up solax energy meter because the connection for" << connectionThing << "does not exist.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
// Note: The states will be handled in the parent inverter thing on updated
info->finish(Thing::ThingErrorNoError);
return;
}
// Child inverter
if (thing->thingClassId() == solaxInverterChildThingClassId) {
// Get the parent thing and the associated connection
Thing *connectionThing = myThings().findById(thing->parentId());
if (!connectionThing) {
qCWarning(dcSolax()) << "Failed to set up solax child inverter because the parent thing with ID" << thing->parentId().toString() << "could not be found.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
SolaxModbusTcpConnection *solaxConnection = m_tcpConnections.value(connectionThing);
if (!solaxConnection) {
qCWarning(dcSolax()) << "Failed to set up solax child inverter because the connection for" << connectionThing << "does not exist.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
// Note: The states will be handled in the parent inverter thing on updated
info->finish(Thing::ThingErrorNoError);
return;
}
// Battery
if (thing->thingClassId() == solaxBatteryThingClassId) {
// Get the parent thing and the associated connection
Thing *connectionThing = myThings().findById(thing->parentId());
if (!connectionThing) {
qCWarning(dcSolax()) << "Failed to set up solax battery because the parent thing with ID" << thing->parentId().toString() << "could not be found.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
SolaxModbusTcpConnection *solaxConnection = m_tcpConnections.value(connectionThing);
if (!solaxConnection) {
qCWarning(dcSolax()) << "Failed to set up solax battery because the connection for" << connectionThing << "does not exist.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
// Note: The states will be handled in the parent inverter thing on updated
info->finish(Thing::ThingErrorNoError);
return;
}
}
void IntegrationPluginSolax::postSetupThing(Thing *thing)
{
if (thing->thingClassId() == solaxInverterTcpThingClassId) {
// Create the update timer if not already set up
if (!m_refreshTimer) {
qCDebug(dcSolax()) << "Starting plugin timer...";
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2);
connect(m_refreshTimer, &PluginTimer::timeout, this, [this] {
foreach(SolaxModbusTcpConnection *connection, m_tcpConnections) {
if (connection->initializing())
continue;
//qCDebug(dcSolax()) << "Update connection" << connection->modbusTcpMaster()->hostAddress().toString();
connection->update();
}
});
m_refreshTimer->start();
}
return;
}
if (thing->thingClassId() == solaxMeterThingClassId || thing->thingClassId() == solaxBatteryThingClassId || thing->thingClassId() == solaxInverterChildThingClassId) {
Thing *connectionThing = myThings().findById(thing->parentId());
if (connectionThing) {
thing->setStateValue("connected", connectionThing->stateValue("connected"));
}
return;
}
}
void IntegrationPluginSolax::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == solaxInverterTcpThingClassId && m_tcpConnections.contains(thing)) {
SolaxModbusTcpConnection *connection = m_tcpConnections.take(thing);
connection->modbusTcpMaster()->disconnectDevice();
delete connection;
}
// Unregister related hardware resources
if (m_monitors.contains(thing))
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
if (myThings().isEmpty() && m_refreshTimer) {
qCDebug(dcSolax()) << "Stopping refresh timer";
hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
m_refreshTimer = nullptr;
}
}
Thing *IntegrationPluginSolax::getMeterThing(Thing *parentThing, uint meterId)
{
Things meterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, meterId);
if (meterThings.isEmpty())
return nullptr;
return meterThings.first();
}
Thing *IntegrationPluginSolax::getBatteryThing(Thing *parentThing)
{
Things batteryThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxBatteryThingClassId);
if (batteryThings.isEmpty())
return nullptr;
return batteryThings.first();
}
void IntegrationPluginSolax::cleanupMeter2(Thing *parentThing)
{
Things meters = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 2);
if (!meters.isEmpty()) {
emit autoThingDisappeared(meters.first()->id());
}
}
void IntegrationPluginSolax::cleanupChildInverters(Thing *parentThing)
{
Things inverterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxInverterChildThingClassId);
if (!inverterThings.isEmpty()) {
foreach (Thing *inverterThing, inverterThings) {
emit autoThingDisappeared(inverterThing->id());
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINSOLAX_H
#define INTEGRATIONPLUGINSOLAX_H
#include <plugintimer.h>
#include <integrations/integrationplugin.h>
#include <network/networkdevicemonitor.h>
#include "extern-plugininfo.h"
#include "solaxmodbustcpconnection.h"
#include "solaxmodbusrtuconnection.h"
class IntegrationPluginSolax: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsolax.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginSolax();
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void thingRemoved(Thing *thing) override;
private:
PluginTimer *m_refreshTimer = nullptr;
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
QHash<Thing *, SolaxModbusTcpConnection *> m_tcpConnections;
QHash<Thing *, SolaxModbusRtuConnection *> 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

View File

@ -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": [ ]
}
]
}
]
}

13
solax/meta.json Normal file
View File

@ -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"
]
}

944
solax/solax-registers.json Normal file
View File

@ -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"
}
]
}

BIN
solax/solax.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

14
solax/solax.pro Normal file
View File

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

163
solax/solaxdiscovery.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "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::SolaxDiscoveryResult> 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();
}

76
solax/solaxdiscovery.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef SOLAXDISCOVERY_H
#define SOLAXDISCOVERY_H
#include <QObject>
#include <QTimer>
#include <network/networkdevicediscovery.h>
#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<SolaxDiscoveryResult> discoveryResults() const;
signals:
void discoveryFinished();
private:
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
quint16 m_port;
quint16 m_modbusAddress;
QDateTime m_startDateTime;
QList<SolaxModbusTcpConnection *> m_connections;
QList<SolaxDiscoveryResult> m_discoveryResults;
void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
void cleanupConnection(SolaxModbusTcpConnection *connection);
void finishDiscovery();
};
#endif // SOLAXDISCOVERY_H

View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginSolax</name>
<message>
<location filename="../integrationpluginsolax.cpp" line="47"/>
<source>The network device discovery is not available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginsolax.cpp" line="100"/>
<source>The MAC address is not known. Please reconfigure the thing.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Solax</name>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="61"/>
<source>Active power</source>
<extracomment>The name of the StateType ({7cc0df36-7ec8-499d-ba6b-8b62520a0d61}) of ThingClass solaxInverterTcp</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="64"/>
<source>Battery critical</source>
<extracomment>The name of the StateType ({5344d1dc-a109-4b44-8d50-24f69a6f6993}) of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="67"/>
<source>Battery level</source>
<extracomment>The name of the StateType ({2d601edb-31e8-4c00-8567-b9f81121a33c}) of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="70"/>
<source>Capacity</source>
<extracomment>The name of the StateType ({98099dbd-3f66-43b3-8192-f2e3fdcd5d62}) of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="73"/>
<source>Charging state</source>
<extracomment>The name of the StateType ({829173e8-7535-4aba-b403-d498ff68250e}) of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="76"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="79"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="82"/>
<source>Connected</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="85"/>
<source>Current</source>
<extracomment>The name of the StateType ({38e333be-86e8-42d8-a753-4e8102d5c2be}) of ThingClass solaxInverterTcp</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="88"/>
<source>Current phase A</source>
<extracomment>The name of the StateType ({d64f0d70-34a9-4426-a3c9-3689bf806f45}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="91"/>
<source>Current phase B</source>
<extracomment>The name of the StateType ({4007afc5-83d9-4427-bb3d-fe0197c33172}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="94"/>
<source>Current phase C</source>
<extracomment>The name of the StateType ({37a57511-dad5-490c-aa82-88f8e7ebbe1f}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="97"/>
<source>Current power</source>
<extracomment>The name of the StateType ({077234cc-87b1-40f2-a06b-532219e35948}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="100"/>
<source>Current power phase A</source>
<extracomment>The name of the StateType ({d6a76445-e552-44bc-9d49-a64ac9f3263e}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="103"/>
<source>Current power phase B</source>
<extracomment>The name of the StateType ({74d4fa43-10d8-4c85-a2a0-1c318bf4b44d}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="106"/>
<source>Current power phase C</source>
<extracomment>The name of the StateType ({1be70078-7144-4325-b1fc-f73b23a33848}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="109"/>
<source>Frequency</source>
<extracomment>The name of the StateType ({09932aaa-5754-4fd9-a634-965902352de5}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="112"/>
<source>MAC address</source>
<extracomment>The name of the ParamType (ThingClass: solaxInverterTcp, Type: thing, ID: {acdee28d-4c73-4ed9-ad1b-d5d1440164c0})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="115"/>
<source>Port</source>
<extracomment>The name of the ParamType (ThingClass: solaxInverterTcp, Type: thing, ID: {c5324c59-39e6-439c-a9e0-bbe8055c9db0})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="118"/>
<source>Slave ID</source>
<extracomment>The name of the ParamType (ThingClass: solaxInverterTcp, Type: thing, ID: {154f8f71-1d84-4653-94a0-31337af55359})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="121"/>
<source>SolaX Battery</source>
<extracomment>The name of the ThingClass ({f9a03f59-7e2f-4794-98de-bd026d0052ce})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="124"/>
<source>SolaX Meter</source>
<extracomment>The name of the ThingClass ({293d7cef-7bfb-4830-8958-b4b77ccb9786})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="127"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="130"/>
<source>SolaX Power</source>
<extracomment>The name of the vendor ({a672201c-6b11-4e79-bef9-60a23e08ff8f})
----------
The name of the plugin Solax ({c316666c-7070-42e2-8d37-1145715dc986})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="133"/>
<source>Solax Inverter</source>
<extracomment>The name of the ThingClass ({fa1a559a-12a6-416f-ab77-a431a38bc3c2})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="136"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="139"/>
<source>Temperature</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="142"/>
<source>Total energy produced</source>
<extracomment>The name of the StateType ({cbf8cd14-1661-4063-be78-a7151dfc24d4}) of ThingClass solaxInverterTcp</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="145"/>
<source>Total imported energy</source>
<extracomment>The name of the StateType ({44f30880-cba9-4ce7-995d-8cbad4ff31a9}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="148"/>
<source>Total real power</source>
<extracomment>The name of the StateType ({edc3c2fd-382d-41ac-b894-50881fb92bea}) of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="151"/>
<source>Total returned energy</source>
<extracomment>The name of the StateType ({59397bac-a4d9-4e50-99a3-f329e3806b25}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="154"/>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="157"/>
<source>Voltage</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="160"/>
<source>Voltage phase A</source>
<extracomment>The name of the StateType ({1da7318a-9b2f-4abd-a30b-df0da04e8d9b}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="163"/>
<source>Voltage phase B</source>
<extracomment>The name of the StateType ({89cebad3-8985-4f5c-bd69-cd041a436d48}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="166"/>
<source>Voltage phase C</source>
<extracomment>The name of the StateType ({d80a0934-5a83-4bac-aeac-2360144b3f93}) of ThingClass solaxMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="169"/>
<source>charging</source>
<extracomment>The name of a possible value of StateType {829173e8-7535-4aba-b403-d498ff68250e} of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="172"/>
<source>discharging</source>
<extracomment>The name of a possible value of StateType {829173e8-7535-4aba-b403-d498ff68250e} of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-modbus-Desktop-Debug/solax/plugininfo.h" line="175"/>
<source>idle</source>
<extracomment>The name of a possible value of StateType {829173e8-7535-4aba-b403-d498ff68250e} of ThingClass solaxBattery</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

9
vestel/README.md Normal file
View File

@ -0,0 +1,9 @@
# Vestel
Connects nymea to Vestel wallboxes. Currently supported models are:
* EVC04
# Requirements
nymea uses the Modbus TCP connection to connect to the wallbox. Please make sure that the wallbox is connected to the same network as the nymea system and that the Local Load Management Option is set to Modbus TCP in the wallboxes web interface.

362
vestel/evc04-registers.json Normal file
View File

@ -0,0 +1,362 @@
{
"className": "EVC04",
"protocol": "TCP",
"endianness": "BigEndian",
"errorLimitUntilNotReachable": 20,
"checkReachableRegister": "chargepointState",
"enums": [
{
"name": "ChargePointState",
"values": [
{
"key": "Available",
"value": 0
},
{
"key": "Preparing",
"value": 1
},
{
"key": "Charging",
"value": 2
},
{
"key": "SuspendedEVSE",
"value": 3
},
{
"key": "SuspendedEV",
"value": 4
},
{
"key": "Finishing",
"value": 5
},
{
"key": "Reserved",
"value": 6
},
{
"key": "Unavailable",
"value": 7
},
{
"key": "Faulted",
"value": 8
}
]
},
{
"name": "ChargingState",
"values": [
{
"key": "NotCharging",
"value": 0
},
{
"key": "Charging",
"value": 1
}
]
},
{
"name": "NumPhases",
"values": [
{
"key": "1",
"value": 0
},
{
"key": "3",
"value": 1
}
]
},
{
"name": "CableState",
"values": [
{
"key": "NotConnected",
"value": 0
},
{
"key": "CableConnectedVehicleNotConnected",
"value": 1
},
{
"key": "CableConnectedVehicleConnected",
"value": 2
},
{
"key": "CableConnectedVehicleConnectedCableLocked",
"value": 3
}
]
}
],
"registers": [
{
"id": "serialNumber",
"address": 100,
"size": 25,
"type": "raw",
"readSchedule": "init",
"registerType": "inputRegister",
"description": "Serial number",
"access": "RO"
},
{
"id": "chargepointId",
"address": 130,
"size": 50,
"type": "raw",
"readSchedule": "init",
"registerType": "inputRegister",
"description": "Chargepoint ID",
"access": "RO"
},
{
"id": "brand",
"address": 190,
"size": 10,
"type": "raw",
"readSchedule": "init",
"registerType": "inputRegister",
"description": "Brand",
"access": "RO"
},
{
"id": "model",
"address": 210,
"size": 5,
"type": "raw",
"readSchedule": "init",
"registerType": "inputRegister",
"description": "Model",
"access": "RO"
},
{
"id": "firmwareVersion",
"address": 230,
"size": 50,
"type": "raw",
"readSchedule": "init",
"registerType": "inputRegister",
"description": "Firmware version",
"access": "RO"
},
{
"id": "time",
"address": 294,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Wallbox time",
"access": "RO"
},
{
"id": "maxChargePointPower",
"address": 400,
"size": 2,
"type": "uint32",
"readSchedule": "init",
"registerType": "inputRegister",
"description": "Max power of Chargepoint",
"unit": "W",
"defaultValue": "0",
"access": "RO"
},
{
"id": "numPhases",
"address": 404,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Number of Phases",
"enum": "NumPhases",
"defaultValue": "NumPhases1",
"access": "RO"
},
{
"id": "chargepointState",
"address": 1000,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Chargepoint State",
"enum": "ChargePointState",
"defaultValue": "ChargePointStateAvailable",
"access": "RO"
},
{
"id": "chargingState",
"address": 1001,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Charging State",
"enum": "ChargingState",
"defaultValue": "ChargingStateNotCharging",
"access": "RO"
},
{
"id": "cableState",
"address": 1004,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Cable state",
"enum": "CableState",
"defaultValue": "CableStateNotConnected",
"access": "RO"
},
{
"id": "activePowerTotal",
"address": 1020,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Active Power Total",
"unit": "W",
"defaultValue": 0,
"access": "RO"
},
{
"id": "meterReading",
"address": 1036,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Meter reading",
"unit": "1/10 kWh",
"defaultValue": "ChargingStateNotCharging",
"access": "RO"
},
{
"id": "sessionMaxCurrent",
"address": 1100,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Session max current",
"unit": "A",
"defaultValue": 0,
"access": "RO"
},
{
"id": "evseMinCurrent",
"address": 1102,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "EVSE min current",
"unit": "A",
"defaultValue": 6,
"access": "RO"
},
{
"id": "evseMaxCurrent",
"address": 1104,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "EVSE max current",
"unit": "A",
"defaultValue": 32,
"access": "RO"
},
{
"id": "cableMaxCurrent",
"address": 1106,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Cable max current",
"unit": "A",
"defaultValue": 32,
"access": "RO"
},
{
"id": "sessionEnergy",
"address": 1502,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Session energy",
"unit": "Wh",
"defaultValue": 0,
"access": "RO"
},
{
"id": "sessionDuration",
"address": 1508,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "inputRegister",
"description": "Session duration",
"unit": "s",
"defaultValue": 0,
"access": "RO"
},
{
"id": "failsafeCurrent",
"address": 2000,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Failsafe current",
"unit": "A",
"defaultValue": 6,
"access": "RW"
},
{
"id": "failsafeTimeout",
"address": 2002,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Failsafe timeout",
"unit": "s",
"defaultValue": 20,
"access": "RW"
},
{
"id": "chargingCurrent",
"address": 5004,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Dynamic charging current",
"unit": "A",
"defaultValue": 6,
"access": "RW"
},
{
"id": "aliveRegister",
"address": 6000,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Alive register",
"defaultValue": 0,
"access": "RW"
}
]
}

136
vestel/evc04discovery.cpp Normal file
View File

@ -0,0 +1,136 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "evc04discovery.h"
#include "extern-plugininfo.h"
EVC04Discovery::EVC04Discovery(NetworkDeviceDiscovery *networkDeviceDiscovery, const QLoggingCategory &dc, QObject *parent) :
QObject{parent},
m_networkDeviceDiscovery{networkDeviceDiscovery},
m_dc{dc.categoryName()}
{
m_gracePeriodTimer.setSingleShot(true);
m_gracePeriodTimer.setInterval(3000);
connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){
qCDebug(m_dc) << "Discovery: Grace period timer triggered.";
finishDiscovery();
});
}
void EVC04Discovery::startDiscovery()
{
qCInfo(m_dc()) << "Discovery: Searching for Vestel EVC04 wallboxes in the network...";
NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &EVC04Discovery::checkNetworkDevice);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
qCDebug(m_dc()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
m_gracePeriodTimer.start();
discoveryReply->deleteLater();
});
}
QList<EVC04Discovery::Result> EVC04Discovery::discoveryResults() const
{
return m_discoveryResults;
}
void EVC04Discovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
{
int port = 502;
int slaveId = 0xff;
qCDebug(m_dc()) << "Checking network device:" << networkDeviceInfo << "Port:" << port << "Slave ID:" << slaveId;
EVC04ModbusTcpConnection *connection = new EVC04ModbusTcpConnection(networkDeviceInfo.address(), port, slaveId, this);
m_connections.append(connection);
connect(connection, &EVC04ModbusTcpConnection::reachableChanged, this, [=](bool reachable){
if (!reachable) {
cleanupConnection(connection);
return;
}
connect(connection, &EVC04ModbusTcpConnection::initializationFinished, this, [=](bool success){
qCDebug(m_dc()) << "Discovered device on" << networkDeviceInfo.address() << connection->brand() << connection->model() << connection->firmwareVersion();
qCDebug(m_dc()) << connection;
if (!success) {
qCDebug(m_dc()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString();
cleanupConnection(connection);
return;
}
Result result;
result.chargepointId = QString(QString::fromUtf16(connection->chargepointId().data(), connection->chargepointId().length()).toUtf8()).trimmed();
result.brand = QString(QString::fromUtf16(connection->brand().data(), connection->brand().length()).toUtf8()).trimmed();
result.model = QString(QString::fromUtf16(connection->model().data(), connection->model().length()).toUtf8()).trimmed();
result.firmwareVersion = QString(QString::fromUtf16(connection->firmwareVersion().data(), connection->firmwareVersion().length()).toUtf8()).trimmed();
result.networkDeviceInfo = networkDeviceInfo;
m_discoveryResults.append(result);
qCDebug(m_dc()) << "Discovery: Found wallbox with firmware version:" << result.firmwareVersion << result.networkDeviceInfo;
cleanupConnection(connection);
});
if (!connection->initialize()) {
qCDebug(m_dc()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString();
cleanupConnection(connection);
}
});
connect(connection, &EVC04ModbusTcpConnection::checkReachabilityFailed, this, [=](){
qCDebug(m_dc()) << "Discovery: Checking reachability failed on" << networkDeviceInfo.address().toString();
cleanupConnection(connection);
});
connection->connectDevice();
}
void EVC04Discovery::cleanupConnection(EVC04ModbusTcpConnection *connection)
{
m_connections.removeAll(connection);
connection->disconnectDevice();
connection->deleteLater();
}
void EVC04Discovery::finishDiscovery()
{
qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
// Cleanup any leftovers...we don't care any more
foreach (EVC04ModbusTcpConnection *connection, m_connections)
cleanupConnection(connection);
qCInfo(m_dc()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count()
<< "Vestel EVC04 wallboxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
m_gracePeriodTimer.stop();
emit discoveryFinished();
}

79
vestel/evc04discovery.h Normal file
View File

@ -0,0 +1,79 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2022, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef AMTRONECUDISCOVERY_H
#define AMTRONECUDISCOVERY_H
#include <QObject>
#include <QTimer>
#include <QLoggingCategory>
#include <network/networkdevicediscovery.h>
#include "evc04modbustcpconnection.h"
class EVC04Discovery : public QObject
{
Q_OBJECT
public:
explicit EVC04Discovery(NetworkDeviceDiscovery *networkDeviceDiscovery, const QLoggingCategory &dc, QObject *parent = nullptr);
struct Result {
QString chargepointId;
QString firmwareVersion;
QString brand;
QString model;
NetworkDeviceInfo networkDeviceInfo;
};
void startDiscovery();
QList<Result> discoveryResults() const;
signals:
void discoveryFinished();
private:
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
QLoggingCategory m_dc;
QTimer m_gracePeriodTimer;
QDateTime m_startDateTime;
QList<EVC04ModbusTcpConnection*> m_connections;
QList<Result> m_discoveryResults;
void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
void cleanupConnection(EVC04ModbusTcpConnection *connection);
void finishDiscovery();
};
#endif // AMTRONECUDISCOVERY_H

View File

@ -0,0 +1,405 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2022, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginvestel.h"
#include "plugininfo.h"
#include "evc04discovery.h"
#include <network/networkdevicediscovery.h>
#include <hardwaremanager.h>
IntegrationPluginVestel::IntegrationPluginVestel()
{
}
void IntegrationPluginVestel::discoverThings(ThingDiscoveryInfo *info)
{
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcVestel()) << "The network discovery is not available on this platform.";
info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("Unable to discover devices in the network. The system may not be installed correctly."));
return;
}
if (info->thingClassId() == evc04ThingClassId) {
EVC04Discovery *discovery = new EVC04Discovery(hardwareManager()->networkDeviceDiscovery(), dcVestel(), info);
connect(discovery, &EVC04Discovery::discoveryFinished, info, [=](){
foreach (const EVC04Discovery::Result &result, discovery->discoveryResults()) {
QString name = result.chargepointId;
QString description = result.brand + " " + result.model;
ThingDescriptor descriptor(evc04ThingClassId, name, description);
qCDebug(dcVestel()) << "Discovered:" << descriptor.title() << descriptor.description();
// Check if we already have set up this device
Things existingThings = myThings().filterByParam(evc04ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
if (existingThings.count() == 1) {
qCDebug(dcVestel()) << "This wallbox already exists in the system:" << result.networkDeviceInfo;
descriptor.setThingId(existingThings.first()->id());
}
ParamList params;
params << Param(evc04ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
descriptor.setParams(params);
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
discovery->startDiscovery();
}
}
void IntegrationPluginVestel::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCDebug(dcVestel()) << "Setup" << thing << thing->params();
if (thing->thingClassId() == evc04ThingClassId) {
if (m_evc04Connections.contains(thing)) {
qCDebug(dcVestel()) << "Reconfiguring existing thing" << thing->name();
m_evc04Connections.take(thing)->deleteLater();
if (m_monitors.contains(thing)) {
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
}
MacAddress macAddress = MacAddress(thing->paramValue(evc04ThingMacAddressParamTypeId).toString());
if (!macAddress.isValid()) {
qCWarning(dcVestel()) << "The configured mac address is not valid" << thing->params();
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing."));
return;
}
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
m_monitors.insert(thing, monitor);
connect(info, &ThingSetupInfo::aborted, monitor, [=](){
if (m_monitors.contains(thing)) {
qCDebug(dcVestel()) << "Unregistering monitor because setup has been aborted.";
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
});
if (monitor->reachable()) {
setupEVC04Connection(info);
} else {
qCDebug(dcVestel()) << "Waiting for the network monitor to get reachable before continuing to set up the connection" << thing->name() << "...";
connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){
if (reachable) {
qCDebug(dcVestel()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continuing setup on" << monitor->networkDeviceInfo().address().toString();
setupEVC04Connection(info);
}
});
}
return;
}
}
void IntegrationPluginVestel::postSetupThing(Thing *thing)
{
Q_UNUSED(thing)
if (!m_pluginTimer) {
qCDebug(dcVestel()) << "Starting plugin timer...";
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this] {
foreach(EVC04ModbusTcpConnection *connection, m_evc04Connections) {
qCDebug(dcVestel()) << "Updating connection" << connection->modbusTcpMaster()->hostAddress().toString();
connection->update();
connection->setAliveRegister(1);
}
});
m_pluginTimer->start();
}
}
void IntegrationPluginVestel::executeAction(ThingActionInfo *info)
{
if (info->thing()->thingClassId() == evc04ThingClassId) {
EVC04ModbusTcpConnection *evc04Connection = m_evc04Connections.value(info->thing());
if (info->action().actionTypeId() == evc04PowerActionTypeId) {
bool power = info->action().paramValue(evc04PowerActionPowerParamTypeId).toBool();
// If the car is *not* connected, writing a 0 to the charging current register will cause it to go to 6 A instead of 0
// Because of this, we we're not connected, we'll do nothing, but once it get's connected, we'll sync the state over (see below in cableStateChanged)
if (!power && evc04Connection->cableState() < EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnected) {
info->thing()->setStateValue(evc04PowerStateTypeId, false);
info->finish(Thing::ThingErrorNoError);
return;
}
QModbusReply *reply = evc04Connection->setChargingCurrent(power ? info->thing()->stateValue(evc04MaxChargingCurrentStateTypeId).toUInt() : 0);
connect(reply, &QModbusReply::finished, info, [info, reply, power](){
if (reply->error() == QModbusDevice::NoError) {
info->thing()->setStateValue(evc04PowerStateTypeId, power);
info->finish(Thing::ThingErrorNoError);
} else {
qCWarning(dcVestel()) << "Error setting power:" << reply->error() << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure);
}
});
}
if (info->action().actionTypeId() == evc04MaxChargingCurrentActionTypeId) {
int maxChargingCurrent = info->action().paramValue(evc04MaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt();
QModbusReply *reply = evc04Connection->setChargingCurrent(maxChargingCurrent);
connect(reply, &QModbusReply::finished, info, [info, reply, maxChargingCurrent](){
if (reply->error() == QModbusDevice::NoError) {
info->thing()->setStateValue(evc04MaxChargingCurrentStateTypeId, maxChargingCurrent);
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareFailure);
}
});
}
}
}
void IntegrationPluginVestel::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == evc04ThingClassId && m_evc04Connections.contains(thing)) {
EVC04ModbusTcpConnection *connection = m_evc04Connections.take(thing);
delete connection;
}
// Unregister related hardware resources
if (m_monitors.contains(thing))
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
if (myThings().isEmpty() && m_pluginTimer) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_pluginTimer = nullptr;
}
}
void IntegrationPluginVestel::setupEVC04Connection(ThingSetupInfo *info)
{
Thing *thing = info->thing();
QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address();
qCDebug(dcVestel()) << "Setting up EVC04 wallbox on" << address.toString();
EVC04ModbusTcpConnection *evc04Connection = new EVC04ModbusTcpConnection(address, 502, 0xff, this);
connect(info, &ThingSetupInfo::aborted, evc04Connection, &EVC04ModbusTcpConnection::deleteLater);
// Reconnect on monitor reachable changed
NetworkDeviceMonitor *monitor = m_monitors.value(thing);
connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
qCDebug(dcVestel()) << "Network device monitor reachable changed for" << thing->name() << reachable;
if (!thing->setupComplete())
return;
if (reachable && !thing->stateValue("connected").toBool()) {
evc04Connection->modbusTcpMaster()->setHostAddress(monitor->networkDeviceInfo().address());
evc04Connection->connectDevice();
} else if (!reachable) {
// Note: We disable autoreconnect explicitly and we will
// connect the device once the monitor says it is reachable again
evc04Connection->disconnectDevice();
}
});
connect(evc04Connection, &EVC04ModbusTcpConnection::reachableChanged, thing, [thing, evc04Connection](bool reachable){
qCDebug(dcVestel()) << "Reachable changed to" << reachable << "for" << thing;
if (reachable) {
evc04Connection->initialize();
} else {
thing->setStateValue(evc04ConnectedStateTypeId, false);
thing->setStateValue(evc04CurrentPowerStateTypeId, 0);
}
});
connect(evc04Connection, &EVC04ModbusTcpConnection::initializationFinished, thing, [=](bool success){
if (!thing->setupComplete())
return;
if (success) {
thing->setStateValue(evc04ConnectedStateTypeId, true);
} else {
thing->setStateValue(evc04ConnectedStateTypeId, false);
thing->setStateValue(evc04CurrentPowerStateTypeId, 0);
// Try once to reconnect the device
evc04Connection->reconnectDevice();
}
});
connect(evc04Connection, &EVC04ModbusTcpConnection::initializationFinished, info, [=](bool success){
if (!success) {
qCWarning(dcVestel()) << "Connection init finished with errors" << thing->name() << evc04Connection->modbusTcpMaster()->hostAddress().toString();
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor);
evc04Connection->deleteLater();
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox."));
return;
}
qCDebug(dcVestel()) << "Connection init finished successfully" << evc04Connection;
m_evc04Connections.insert(thing, evc04Connection);
info->finish(Thing::ThingErrorNoError);
thing->setStateValue(evc04ConnectedStateTypeId, true);
thing->setStateValue(evc04VersionStateTypeId, QString(QString::fromUtf16(evc04Connection->firmwareVersion().data(), evc04Connection->firmwareVersion().length()).toUtf8()).trimmed());
evc04Connection->update();
});
connect(evc04Connection, &EVC04ModbusTcpConnection::updateFinished, thing, [this, evc04Connection, thing](){
qCDebug(dcVestel()) << "EVC04 update finished:" << thing->name() << evc04Connection;
qCDebug(dcVestel()) << "Serial:" << QString(QString::fromUtf16(evc04Connection->serialNumber().data(), evc04Connection->serialNumber().length()).toUtf8()).trimmed();
qCDebug(dcVestel()) << "ChargePoint ID:" << QString(QString::fromUtf16(evc04Connection->chargepointId().data(), evc04Connection->chargepointId().length()).toUtf8()).trimmed();
qCDebug(dcVestel()) << "Brand:" << QString(QString::fromUtf16(evc04Connection->brand().data(), evc04Connection->brand().length()).toUtf8()).trimmed();
qCDebug(dcVestel()) << "Model:" << QString(QString::fromUtf16(evc04Connection->model().data(), evc04Connection->model().length()).toUtf8()).trimmed();
updateEVC04MaxCurrent(thing);
// I've been observing the wallbox getting stuck on modbus. It is still functional, but modbus keeps on returning the same old values
// until the TCP connection is closed and reopened. Checking the wallbox time register to detect that and auto-reconnect.
if (m_lastWallboxTime[thing] == evc04Connection->time()) {
qCWarning(dcVestel()) << "Wallbox seems stuck and returning outdated values. Reconnecting...";
evc04Connection->reconnectDevice();
}
m_lastWallboxTime[thing] = evc04Connection->time();
});
connect(evc04Connection, &EVC04ModbusTcpConnection::chargepointStateChanged, thing, [thing](EVC04ModbusTcpConnection::ChargePointState chargePointState) {
qCDebug(dcVestel()) << "Chargepoint state changed" << chargePointState;
// switch (chargePointState) {
// case EVC04ModbusTcpConnection::ChargePointStateAvailable:
// case EVC04ModbusTcpConnection::ChargePointStatePreparing:
// case EVC04ModbusTcpConnection::ChargePointStateReserved:
// case EVC04ModbusTcpConnection::ChargePointStateUnavailable:
// case EVC04ModbusTcpConnection::ChargePointStateFaulted:
// thing->setStateValue(evc04PluggedInStateTypeId, false);
// break;
// case EVC04ModbusTcpConnection::ChargePointStateCharging:
// case EVC04ModbusTcpConnection::ChargePointStateSuspendedEVSE:
// case EVC04ModbusTcpConnection::ChargePointStateSuspendedEV:
// case EVC04ModbusTcpConnection::ChargePointStateFinishing:
// thing->setStateValue(evc04PluggedInStateTypeId, true);
// break;
// }
});
connect(evc04Connection, &EVC04ModbusTcpConnection::chargingStateChanged, thing, [thing](EVC04ModbusTcpConnection::ChargingState chargingState) {
qCDebug(dcVestel()) << "Charging state changed:" << chargingState;
thing->setStateValue(evc04ChargingStateTypeId, chargingState == EVC04ModbusTcpConnection::ChargingStateCharging);
});
connect(evc04Connection, &EVC04ModbusTcpConnection::activePowerTotalChanged, thing, [thing](quint16 activePowerTotal) {
qCDebug(dcVestel()) << "Total active power:" << activePowerTotal;
// The wallbox reports some 5-6W even when there's nothing connected. Let's hide that if we're not charging
if (thing->stateValue(evc04ChargingStateTypeId).toBool() == true) {
thing->setStateValue(evc04CurrentPowerStateTypeId, activePowerTotal);
} else {
thing->setStateValue(evc04CurrentPowerStateTypeId, 0);
}
});
connect(evc04Connection, &EVC04ModbusTcpConnection::meterReadingChanged, thing, [thing](quint32 meterReading) {
qCDebug(dcVestel()) << "Meter reading changed:" << meterReading;
thing->setStateValue(evc04TotalEnergyConsumedStateTypeId, meterReading / 10.0);
});
connect(evc04Connection, &EVC04ModbusTcpConnection::sessionMaxCurrentChanged, thing, [](quint16 sessionMaxCurrent) {
// This mostly just reflects what we've been writing to cargingCurrent, so not of much use...
qCDebug(dcVestel()) << "Session max current changed:" << sessionMaxCurrent;
});
connect(evc04Connection, &EVC04ModbusTcpConnection::cableMaxCurrentChanged, thing, [this, thing](quint16 cableMaxCurrent) {
qCDebug(dcVestel()) << "Cable max current changed:" << cableMaxCurrent;
updateEVC04MaxCurrent(thing);
});
connect(evc04Connection, &EVC04ModbusTcpConnection::evseMinCurrentChanged, thing, [thing](quint16 evseMinCurrent) {
qCDebug(dcVestel()) << "EVSE min current changed:" << evseMinCurrent;
thing->setStateMinValue(evc04MaxChargingCurrentStateTypeId, evseMinCurrent);
});
connect(evc04Connection, &EVC04ModbusTcpConnection::evseMaxCurrentChanged, thing, [this, thing](quint16 evseMaxCurrent) {
qCDebug(dcVestel()) << "EVSE max current changed:" << evseMaxCurrent;
updateEVC04MaxCurrent(thing);
});
connect(evc04Connection, &EVC04ModbusTcpConnection::sessionEnergyChanged, thing, [thing](quint32 sessionEnergy) {
qCDebug(dcVestel()) << "Session energy changed:" << sessionEnergy;
thing->setStateValue(evc04SessionEnergyStateTypeId, sessionEnergy / 1000.0);
});
connect(evc04Connection, &EVC04ModbusTcpConnection::chargingCurrentChanged, thing, [thing](quint16 chargingCurrent) {
qCDebug(dcVestel()) << "Charging current changed:" << chargingCurrent;
if (chargingCurrent > 0) {
thing->setStateValue(evc04PowerStateTypeId, true);
thing->setStateValue(evc04MaxChargingCurrentStateTypeId, chargingCurrent);
} else {
thing->setStateValue(evc04PowerStateTypeId, false);
}
});
connect(evc04Connection, &EVC04ModbusTcpConnection::numPhasesChanged, thing, [thing](EVC04ModbusTcpConnection::NumPhases numPhases) {
switch (numPhases) {
case EVC04ModbusTcpConnection::NumPhases1:
thing->setStateValue(evc04PhaseCountStateTypeId, 1);
break;
case EVC04ModbusTcpConnection::NumPhases3:
thing->setStateValue(evc04PhaseCountStateTypeId, 3);
break;
}
});
connect(evc04Connection, &EVC04ModbusTcpConnection::cableStateChanged, thing, [evc04Connection, thing](EVC04ModbusTcpConnection::CableState cableState) {
switch (cableState) {
case EVC04ModbusTcpConnection::CableStateNotConnected:
case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleNotConnected:
thing->setStateValue(evc04PluggedInStateTypeId, false);
break;
case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnected:
case EVC04ModbusTcpConnection::CableStateCableConnectedVehicleConnectedCableLocked:
thing->setStateValue(evc04PluggedInStateTypeId, true);
// The car was plugged in, sync the power state now as the wallbox only allows to set that when the car is connected
if (thing->stateValue(evc04PowerStateTypeId).toBool() == false) {
qCInfo(dcVestel()) << "Car plugged in. Syncing cached power off state to wallbox";
evc04Connection->setChargingCurrent(0);
}
break;
}
});
evc04Connection->connectDevice();
}
void IntegrationPluginVestel::updateEVC04MaxCurrent(Thing *thing)
{
EVC04ModbusTcpConnection *connection = m_evc04Connections.value(thing);
quint16 wallboxMax = connection->maxChargePointPower() > 0 ? connection->maxChargePointPower() / 230 : 32;
quint16 evseMax = connection->evseMaxCurrent() > 0 ? connection->evseMaxCurrent() : wallboxMax;
quint16 cableMax = connection->cableMaxCurrent() > 0 ? connection->cableMaxCurrent() : wallboxMax;
quint8 overallMax = qMin(qMin(wallboxMax, evseMax), cableMax);
qCDebug(dcVestel()) << "Adjusting max current: Wallbox max:" << wallboxMax << "EVSE max:" << evseMax << "cable max:" << cableMax << "Overall:" << overallMax;
thing->setStateMinMaxValues(evc04MaxChargingCurrentStateTypeId, 6, overallMax);
}

View File

@ -0,0 +1,72 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINVESTEL_H
#define INTEGRATIONPLUGINVESTEL_H
#include <plugintimer.h>
#include <integrations/integrationplugin.h>
#include <network/networkdevicemonitor.h>
#include "extern-plugininfo.h"
#include "evc04modbustcpconnection.h"
class IntegrationPluginVestel: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginvestel.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginVestel();
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
void thingRemoved(Thing *thing) override;
private:
void setupEVC04Connection(ThingSetupInfo *info);
void updateEVC04MaxCurrent(Thing *thing);
PluginTimer *m_pluginTimer = nullptr;
QHash<Thing *, EVC04ModbusTcpConnection *> m_evc04Connections;
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
QHash<Thing *, quint32> m_lastWallboxTime;
};
#endif // INTEGRATIONPLUGINVESTEL_H

View File

@ -0,0 +1,120 @@
{
"name": "vestel",
"displayName": "Vestel",
"id": "d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea",
"vendors": [
{
"name": "vestel",
"displayName": "Vestel",
"id": "9a8441f9-f71a-4e3d-a2a0-2f912fa6f047",
"thingClasses": [
{
"name": "evc04",
"displayName": "EVC04",
"id": "396de19c-ef2b-4f32-b551-9d0d153304a4",
"createMethods": ["discovery", "user"],
"interfaces": ["evcharger", "smartmeterconsumer", "connectable"],
"paramTypes": [
{
"id": "afdc55b7-b509-4d0c-ab79-176f2edf8488",
"name":"macAddress",
"displayName": "MAC address",
"type": "QString",
"inputType": "MacAddress",
"defaultValue": ""
}
],
"stateTypes": [
{
"id": "ababb315-4c90-4d7b-8336-de0f22630c32",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "cef29b4b-11e5-4342-9fae-0ac90a70082a",
"name": "pluggedIn",
"displayName": "Plugged in",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "c1be88ee-776f-4d05-8dba-ca8086181086",
"name": "charging",
"displayName": "Charging",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "73a165bc-23b9-487e-9758-505bcc6cf10c",
"name": "phaseCount",
"displayName": "Connected phases",
"type": "uint",
"minValue": 1,
"maxValue": 3,
"defaultValue": 1
},
{
"id": "4646932a-a1f4-4b21-919e-a8c393f4e801",
"name": "currentPower",
"displayName": "Active power",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"cached": false
},
{
"id": "dc6eea53-0ae1-4786-b1c2-f1d5a14be9b0",
"name": "totalEnergyConsumed",
"displayName": "Total consumed energy",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0.0,
"cached": true
},
{
"id": "639f28f3-4956-4d7c-9d72-cbbe8eda0d7a",
"name": "power",
"displayName": "Charging enabled",
"displayNameAction": "Set charging enabled",
"type": "bool",
"defaultValue": false,
"writable": true
},
{
"id": "7f619704-36d8-4898-9625-484f96949e1c",
"name": "maxChargingCurrent",
"displayName": "Maximum charging current",
"displayNameAction": "Set maximum charging current",
"type": "uint",
"unit": "Ampere",
"minValue": 6,
"maxValue": 32,
"defaultValue": 6,
"writable": true
},
{
"id": "b37996f6-5721-419e-b6cc-83a21e945a08",
"name": "sessionEnergy",
"displayName": "Session energy",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "0b4d26c3-558a-4b75-92c8-a2b07606e762",
"name": "version",
"displayName": "Firmware version",
"type": "QString",
"defaultValue": ""
}
]
}
]
}
]
}

13
vestel/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "Bestel",
"tagline": "Connect Vestel wallboxes to nymea.",
"icon": "vestel.png",
"stability": "consumer",
"offline": true,
"technologies": [
"network"
],
"categories": [
"energy"
]
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginVestel</name>
<message>
<location filename="../integrationpluginvestel.cpp" line="47"/>
<source>Unable to discover devices in the network. The system may not be installed correctly.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginvestel.cpp" line="100"/>
<source>The MAC address is not known. Please reconfigure the thing.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginvestel.cpp" line="111"/>
<source>The host address is not known yet. Trying later again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginvestel.cpp" line="294"/>
<source>Error communicating with the wallbox.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>vestel</name>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="42"/>
<source>Active power</source>
<extracomment>The name of the StateType ({4646932a-a1f4-4b21-919e-a8c393f4e801}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="45"/>
<source>Charging</source>
<extracomment>The name of the StateType ({c1be88ee-776f-4d05-8dba-ca8086181086}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="48"/>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="51"/>
<source>Charging enabled</source>
<extracomment>The name of the ParamType (ThingClass: evc04, ActionType: power, ID: {639f28f3-4956-4d7c-9d72-cbbe8eda0d7a})
----------
The name of the StateType ({639f28f3-4956-4d7c-9d72-cbbe8eda0d7a}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="54"/>
<source>Connected</source>
<extracomment>The name of the StateType ({ababb315-4c90-4d7b-8336-de0f22630c32}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="57"/>
<source>Connected phases</source>
<extracomment>The name of the StateType ({73a165bc-23b9-487e-9758-505bcc6cf10c}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="60"/>
<source>EVC04</source>
<extracomment>The name of the ThingClass ({396de19c-ef2b-4f32-b551-9d0d153304a4})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="63"/>
<source>MAC address</source>
<extracomment>The name of the ParamType (ThingClass: evc04, Type: thing, ID: {afdc55b7-b509-4d0c-ab79-176f2edf8488})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="66"/>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="69"/>
<source>Maximum charging current</source>
<extracomment>The name of the ParamType (ThingClass: evc04, ActionType: maxChargingCurrent, ID: {7f619704-36d8-4898-9625-484f96949e1c})
----------
The name of the StateType ({7f619704-36d8-4898-9625-484f96949e1c}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="72"/>
<source>Plugged in</source>
<extracomment>The name of the StateType ({cef29b4b-11e5-4342-9fae-0ac90a70082a}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="75"/>
<source>Session energy</source>
<extracomment>The name of the StateType ({b37996f6-5721-419e-b6cc-83a21e945a08}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="78"/>
<source>Set charging enabled</source>
<extracomment>The name of the ActionType ({639f28f3-4956-4d7c-9d72-cbbe8eda0d7a}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="81"/>
<source>Set maximum charging current</source>
<extracomment>The name of the ActionType ({7f619704-36d8-4898-9625-484f96949e1c}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="84"/>
<source>Total consumed energy</source>
<extracomment>The name of the StateType ({dc6eea53-0ae1-4786-b1c2-f1d5a14be9b0}) of ThingClass evc04</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="87"/>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/vestel/plugininfo.h" line="90"/>
<source>Vestel</source>
<extracomment>The name of the vendor ({9a8441f9-f71a-4e3d-a2a0-2f912fa6f047})
----------
The name of the plugin vestel ({d7d8c2d5-e85d-4a9b-9113-61b43d8bf8ea})</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

BIN
vestel/vestel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

15
vestel/vestel.pro Normal file
View File

@ -0,0 +1,15 @@
include(../plugins.pri)
# Generate modbus connection
MODBUS_CONNECTIONS += evc04-registers.json
#MODBUS_TOOLS_CONFIG += VERBOSE
include(../modbus.pri)
HEADERS += \
evc04discovery.h \
integrationpluginvestel.h
SOURCES += \
evc04discovery.cpp \
integrationpluginvestel.cpp

4
wattsonic/README.md Normal file
View File

@ -0,0 +1,4 @@
# Wattsonic
This plugin allows to connect wattsonic hybrid inverters to nymea

View File

@ -0,0 +1,262 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginwattsonic.h"
#include "plugininfo.h"
#include "wattsonicdiscovery.h"
#include <network/networkdevicediscovery.h>
#include <hardwaremanager.h>
IntegrationPluginWattsonic::IntegrationPluginWattsonic()
{
}
void IntegrationPluginWattsonic::discoverThings(ThingDiscoveryInfo *info)
{
if (info->thingClassId() == inverterThingClassId) {
WattsonicDiscovery *discovery = new WattsonicDiscovery(hardwareManager()->modbusRtuResource(), info);
connect(discovery, &WattsonicDiscovery::discoveryFinished, info, [=](bool modbusRtuMasterAvailable){
if (!modbusRtuMasterAvailable) {
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No suitable Modbus RTU connection available. Please set up a Modbus RTU master with a baudrate of 9600, 8 data bits, 1 stop bit and no parity."));
return;
}
foreach (const WattsonicDiscovery::Result &result, discovery->discoveryResults()) {
QString name = "Wattsonic hybrid inverter";
ThingDescriptor descriptor(inverterThingClassId, name, result.serialNumber);
qCDebug(dcWattsonic()) << "Discovered:" << descriptor.title() << descriptor.description();
ParamList params {
{inverterThingModbusMasterUuidParamTypeId, result.modbusRtuMasterId},
{inverterThingSlaveAddressParamTypeId, result.slaveId}
};
descriptor.setParams(params);
// Check if we already have set up this device
Thing *existingThing = myThings().findByParams(params);
if (existingThing) {
qCDebug(dcWattsonic()) << "This inverter already exists in the system:" << result.serialNumber;
descriptor.setThingId(existingThing->id());
}
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
discovery->startDiscovery();
}
}
void IntegrationPluginWattsonic::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCDebug(dcWattsonic()) << "Setup" << thing << thing->params();
if (info->thing()->thingClassId() == inverterThingClassId) {
if (m_connections.contains(thing)) {
qCDebug(dcWattsonic()) << "Reconfiguring existing thing" << thing->name();
m_connections.take(thing)->deleteLater();
}
setupWattsonicConnection(info);
return;
}
if (info->thing()->thingClassId() == meterThingClassId) {
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->thing()->thingClassId() == batteryThingClassId) {
info->finish(Thing::ThingErrorNoError);
return;
}
}
void IntegrationPluginWattsonic::postSetupThing(Thing *thing)
{
if (thing->thingClassId() == inverterThingClassId) {
Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId);
if (meters.isEmpty()) {
qCInfo(dcWattsonic()) << "No energy meter set up yet. Creating thing...";
ThingDescriptor descriptor(meterThingClassId, "Wattsonic energy meter", QString(), thing->id());
emit autoThingsAppeared({descriptor});
}
Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId);
if (batteries.isEmpty()) {
qCInfo(dcWattsonic()) << "No battery set up yet. Creating thing...";
ThingDescriptor descriptor(batteryThingClassId, "Wattsonic energy storage", QString(), thing->id());
emit autoThingsAppeared({descriptor});
}
}
if (!m_pluginTimer) {
qCDebug(dcWattsonic()) << "Starting plugin timer...";
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this] {
foreach(WattsonicModbusRtuConnection *connection, m_connections) {
qCDebug(dcWattsonic()) << "Updating connection" << connection->modbusRtuMaster()->serialPort() << connection->slaveId();
connection->update();
}
});
m_pluginTimer->start();
}
}
void IntegrationPluginWattsonic::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == inverterThingClassId && m_connections.contains(thing)) {
delete m_connections.take(thing);
}
if (myThings().isEmpty() && m_pluginTimer) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_pluginTimer = nullptr;
}
}
void IntegrationPluginWattsonic::setupWattsonicConnection(ThingSetupInfo *info)
{
Thing *thing = info->thing();
uint slaveId = thing->paramValue(inverterThingSlaveAddressParamTypeId).toUInt();
if (slaveId > 247 || slaveId == 0) {
qCWarning(dcWattsonic()) << "Setup failed, slave ID is not valid" << slaveId;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus address not valid. It must be a value between 1 and 247."));
return;
}
QUuid uuid = thing->paramValue(inverterThingModbusMasterUuidParamTypeId).toUuid();
if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(uuid)) {
qCWarning(dcWattsonic()) << "Setup failed, hardware manager not available";
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("The Modbus RTU resource is not available."));
return;
}
WattsonicModbusRtuConnection *connection = new WattsonicModbusRtuConnection(hardwareManager()->modbusRtuResource()->getModbusRtuMaster(uuid), slaveId, this);
connect(info, &ThingSetupInfo::aborted, connection, &ModbusRtuMaster::deleteLater);
m_connections.insert(thing, connection);
connect(info, &ThingSetupInfo::aborted, this, [=](){
m_connections.take(info->thing())->deleteLater();
});
connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [connection, thing, this](bool reachable){
qCDebug(dcWattsonic()) << "Reachable state changed" << reachable;
if (reachable) {
connection->initialize();
} else {
thing->setStateValue("connected", false);
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
child->setStateValue("connected", true);
}
}
});
connect(connection, &WattsonicModbusRtuConnection::initializationFinished, info, [=](bool success){
qCDebug(dcWattsonic()) << "Initialisation finished" << success;
if (info->isInitialSetup() && !success) {
m_connections.take(info->thing())->deleteLater();
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
info->finish(Thing::ThingErrorNoError);
if (success) {
qCDebug(dcWattsonic) << "Firmware version:" << connection->firmwareVersion();
// info->thing()->setStateValue(inverterCurrentVersionStateTypeId, compact20Connection->firmwareVersion());
}
});
connect(connection, &WattsonicModbusRtuConnection::reachableChanged, thing, [=](bool reachable){
thing->setStateValue(inverterConnectedStateTypeId, reachable);
foreach (Thing *child, myThings().filterByParentId(thing->id())) {
child->setStateValue("connected", reachable);
}
});
connect(connection, &WattsonicModbusRtuConnection::updateFinished, thing, [this, connection, thing](){
qCDebug(dcWattsonic()) << "Update finished:" << thing->name() << connection;
Thing *inverter = thing;
inverter->setStateValue(inverterCurrentPowerStateTypeId, connection->pAC() * -1.0);
inverter->setStateValue(inverterTotalEnergyProducedStateTypeId, connection->totalPVGenerationFromInstallation() * 0.1);
qCInfo(dcWattsonic()) << "Updating inverter:" << inverter->stateValue(inverterCurrentPowerStateTypeId).toDouble() << "W" << inverter->stateValue(inverterTotalEnergyProducedStateTypeId).toDouble() << "kWh";
Things meters = myThings().filterByParentId(thing->id()).filterByThingClassId(meterThingClassId);
if (!meters.isEmpty()) {
Thing *meter = meters.first();
meter->setStateValue(meterCurrentPowerStateTypeId, connection->totalPowerOnMeter() * -1.0);
meter->setStateValue(meterTotalEnergyConsumedStateTypeId, connection->totalEnergyPurchasedFromGrid() / 10.0);
meter->setStateValue(meterTotalEnergyProducedStateTypeId, connection->totalEnergyInjectedToGrid() / 10.0);
meter->setStateValue(meterCurrentPowerPhaseAStateTypeId, connection->phaseAPower() * -1.0);
meter->setStateValue(meterCurrentPowerPhaseBStateTypeId, connection->phaseBPower() * -1.0);
meter->setStateValue(meterCurrentPowerPhaseCStateTypeId, connection->phaseCPower() * -1.0);
meter->setStateValue(meterVoltagePhaseAStateTypeId, connection->gridPhaseAVoltage() / 10.0);
meter->setStateValue(meterVoltagePhaseBStateTypeId, connection->gridPhaseBVoltage() / 10.0);
meter->setStateValue(meterVoltagePhaseCStateTypeId, connection->gridPhaseCVoltage() / 10.0);
// The phase current registers don't seem to contain proper values. Calculating ourselves instead
// meter->setStateValue(meterCurrentPhaseAStateTypeId, connection->gridPhaseACurrent() / 10.0);
// meter->setStateValue(meterCurrentPhaseBStateTypeId, connection->gridPhaseBCurrent() / 10.0);
// meter->setStateValue(meterCurrentPhaseCStateTypeId, connection->gridPhaseCCurrent() / 10.0);
meter->setStateValue(meterCurrentPhaseAStateTypeId, (connection->phaseAPower() * -1.0) / (connection->gridPhaseAVoltage() / 10.0));
meter->setStateValue(meterCurrentPhaseBStateTypeId, (connection->phaseBPower() * -1.0) / (connection->gridPhaseBVoltage() / 10.0));
meter->setStateValue(meterCurrentPhaseCStateTypeId, (connection->phaseCPower() * -1.0) / (connection->gridPhaseCVoltage() / 10.0));
qCInfo(dcWattsonic()) << "Updating meter:" << meter->stateValue(meterCurrentPowerStateTypeId).toDouble() << "W" << meter->stateValue(meterTotalEnergyProducedStateTypeId).toDouble() << "kWh";
}
Things batteries = myThings().filterByParentId(thing->id()).filterByThingClassId(batteryThingClassId);
if (!batteries.isEmpty() && connection->SOC() > 0) {
Thing *battery = batteries.first();
QHash<WattsonicModbusRtuConnection::BatteryMode, QString> map {
{WattsonicModbusRtuConnection::BatteryModeDischarge, "discharging"},
{WattsonicModbusRtuConnection::BatteryModeCharge, "charging"}
};
battery->setStateValue(batteryChargingStateStateTypeId, map.value(connection->batteryMode()));
battery->setStateValue(batteryCurrentPowerStateTypeId, connection->batteryPower() * -1.0);
battery->setStateValue(batteryBatteryLevelStateTypeId, connection->SOC() / 100.0);
battery->setStateValue(batteryBatteryCriticalStateTypeId, connection->SOC() < 500);
}
});
}

View File

@ -0,0 +1,66 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINWATTSONIC_H
#define INTEGRATIONPLUGINWATTSONIC_H
#include <plugintimer.h>
#include <integrations/integrationplugin.h>
#include <network/networkdevicemonitor.h>
#include "extern-plugininfo.h"
#include "wattsonicmodbusrtuconnection.h"
class IntegrationPluginWattsonic: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginwattsonic.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginWattsonic();
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void thingRemoved(Thing *thing) override;
private:
void setupWattsonicConnection(ThingSetupInfo *info);
PluginTimer *m_pluginTimer = nullptr;
QHash<Thing *, WattsonicModbusRtuConnection *> m_connections;
};
#endif // INTEGRATIONPLUGINWATTSONIC_H

View File

@ -0,0 +1,246 @@
{
"name": "Wattsonic",
"displayName": "Wattsonic",
"id": "d0eaf684-001e-4b1c-9e37-122955958de3",
"vendors": [
{
"name": "wattsonic",
"displayName": "Wattsonic",
"id": "c335f9bc-3bc9-46bf-8c50-800cd93e827a",
"thingClasses": [
{
"name": "inverter",
"displayName": "Wattsonic hybrid inverter",
"id": "688bef8d-2ba8-4eb3-b30e-16193eba02fb",
"createMethods": ["discovery", "user"],
"interfaces": ["solarinverter", "connectable"],
"paramTypes": [
{
"id": "55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2",
"name": "slaveAddress",
"displayName": "Modbus slave address",
"type": "uint",
"defaultValue": 1
},
{
"id": "4f1238b5-07e0-4516-b84a-71670141ef81",
"name": "modbusMasterUuid",
"displayName": "Modbus RTU master",
"type": "QUuid",
"defaultValue": ""
}
],
"stateTypes": [
{
"id": "c0f77c00-b82a-478e-826b-bc3204d66100",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "2cbb25e6-c1bd-4216-b354-6ad6fa957e29",
"name": "totalEnergyProduced",
"displayName": "Total energy produced",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "3107958e-07a2-4ebd-83a1-96fb5998cfb9",
"name": "currentPower",
"displayName": "Current power consumption",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"cached": false
}
]
},
{
"id": "e27a9590-0d13-4d7f-b66b-946dad86b8c8",
"name": "meter",
"displayName": "Wattsonic energy meter",
"createMethods": ["auto"],
"interfaces": ["energymeter", "connectable"],
"stateTypes": [
{
"id": "d3bf44be-00d0-4119-ad16-45648bc1532a",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "d5ce5ba0-bc1d-4af3-9afa-cbfb6c0d01ec",
"name": "currentPower",
"displayName": "Current power consumption",
"type": "double",
"unit": "Watt",
"cached": false,
"defaultValue": 0
},
{
"id": "60d486a6-337d-4977-9624-78d99657aea9",
"name": "totalEnergyConsumed",
"displayName": "Total consumed energy",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "3fd2e110-8851-4530-bb53-e8e5b20cd4cb",
"name": "totalEnergyProduced",
"displayName": "Total returned energy",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "1def2f00-8d1c-4d22-b662-a31a552c8a82",
"name": "currentPowerPhaseA",
"displayName": "Power consumption phase A",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"cached": false
},
{
"id": "404549a1-de1e-42e2-8d5d-f581b8674b7d",
"name": "currentPowerPhaseB",
"displayName": "Power consumption phase B",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"cached": false
},
{
"id": "6fccc09c-7c89-45b7-908e-9cbe5bda1c2c",
"name": "currentPowerPhaseC",
"displayName": "Power consumption phase C",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"cached": false
},
{
"id": "12798e83-3093-411e-ab8e-5955956717da",
"name": "voltagePhaseA",
"displayName": "Voltage phase A",
"type": "double",
"unit": "Volt",
"defaultValue": 0,
"cached": false
},
{
"id": "eef67976-2c39-4d79-8b6d-053e369346a8",
"name": "voltagePhaseB",
"displayName": "Voltage phase B",
"type": "double",
"unit": "Volt",
"defaultValue": 0,
"cached": false
},
{
"id": "1e237634-d23c-4c20-b710-b3586bbb988f",
"name": "voltagePhaseC",
"displayName": "Voltage phase C",
"type": "double",
"unit": "Volt",
"defaultValue": 0,
"cached": false
},
{
"id": "80c12358-c2d9-4c1b-9150-b7dc3da8a7ae",
"name": "currentPhaseA",
"displayName": "Current phase A",
"type": "double",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
},
{
"id": "d0914688-59ae-4bd9-97ee-1668e7948ab0",
"name": "currentPhaseB",
"displayName": "Current phase B",
"type": "double",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
},
{
"id": "ecb8072f-dcb0-4e5e-8cc9-95947d3b7185",
"name": "currentPhaseC",
"displayName": "Current phase C",
"type": "double",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
}
]
},
{
"id": "04c4e5fd-7b64-444f-8905-75651833224e",
"name": "battery",
"displayName": "Wattsonic energy storage",
"createMethods": ["auto"],
"interfaces": ["energystorage", "connectable"],
"stateTypes": [
{
"id": "a8673429-043e-4149-8775-c89cab0b63ca",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "76c70da1-484d-4bb2-8070-22e5e141cadd",
"name": "batteryCritical",
"displayName": "Battery critical",
"type": "bool",
"defaultValue": false
},
{
"id": "113cac91-24cf-45eb-93bf-e81145ba6c4e",
"name": "batteryLevel",
"displayName": "Battery level",
"type": "int",
"minValue": 0,
"maxValue": 100,
"unit": "Percentage",
"defaultValue": 0
},
{
"id": "dfaa3890-e3d2-4262-be3d-ece18b520ed9",
"name": "chargingState",
"displayName": "Charging state",
"type": "QString",
"possibleValues": ["idle", "charging", "discharging"],
"defaultValue": "idle"
},
{
"id": "608da87c-e45f-471d-975d-936807a94c9a",
"name": "currentPower",
"displayName": "Current power consumption",
"type": "double",
"unit": "Watt",
"cached": false,
"defaultValue": 0
},
{
"id": "5c652ecd-1302-4796-92fa-672a3d9442c8",
"name": "capacity",
"displayName": "Capacity",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
}
]
}
]
}
]
}

13
wattsonic/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "Wattsonic",
"tagline": "Connect Wattsonic devices to nymea.",
"icon": "wattsonic.svg",
"stability": "consumer",
"offline": true,
"technologies": [
"network"
],
"categories": [
"energy"
]
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginWattsonic</name>
<message>
<location filename="../integrationpluginwattsonic.cpp" line="140"/>
<source>The Modbus address not valid. It must be a value between 1 and 247.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginwattsonic.cpp" line="147"/>
<source>The Modbus RTU resource is not available.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Wattsonic</name>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="33"/>
<source>Connected</source>
<extracomment>The name of the StateType ({c0f77c00-b82a-478e-826b-bc3204d66100}) of ThingClass inverter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="36"/>
<source>Current power consumption</source>
<extracomment>The name of the StateType ({3107958e-07a2-4ebd-83a1-96fb5998cfb9}) of ThingClass inverter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="39"/>
<source>Modbus RTU master</source>
<extracomment>The name of the ParamType (ThingClass: inverter, Type: thing, ID: {4f1238b5-07e0-4516-b84a-71670141ef81})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="42"/>
<source>Modbus slave address</source>
<extracomment>The name of the ParamType (ThingClass: inverter, Type: thing, ID: {55a7d9ed-5f4f-41a2-8dc1-c6a5a79512d2})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="45"/>
<source>Total energy produced</source>
<extracomment>The name of the StateType ({2cbb25e6-c1bd-4216-b354-6ad6fa957e29}) of ThingClass inverter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="48"/>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="51"/>
<source>Wattsonic</source>
<extracomment>The name of the vendor ({c335f9bc-3bc9-46bf-8c50-800cd93e827a})
----------
The name of the plugin Wattsonic ({d0eaf684-001e-4b1c-9e37-122955958de3})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-modbus-Desktop-Debug/wattsonic/plugininfo.h" line="54"/>
<source>Wattsonic hybrid inverter</source>
<extracomment>The name of the ThingClass ({688bef8d-2ba8-4eb3-b30e-16193eba02fb})</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,362 @@
{
"className": "Wattsonic",
"protocol": "RTU",
"endianness": "BigEndian",
"errorLimitUntilNotReachable": 20,
"checkReachableRegister": "serialNumber",
"enums": [
{
"name": "InverterStatus",
"values": [
{
"key": "Wait",
"value": 0
},
{
"key": "Check",
"value": 1
},
{
"key": "OnGrid",
"value": 2
},
{
"key": "Fault",
"value": 3
},
{
"key": "Flash",
"value": 4
},
{
"key": "OffGrid",
"value": 5
}
]
},
{
"name": "BatteryMode",
"values": [
{
"key": "Discharge",
"value": 0
},
{
"key": "Charge",
"value": 1
}
]
}
],
"blocks": [
],
"registers": [
{
"id": "serialNumber",
"address": 10000,
"size": 8,
"type": "string",
"readSchedule": "init",
"registerType": "holdingRegister",
"description": "Serial number",
"access": "RO"
},
{
"id": "firmwareVersion",
"address": 10011,
"size": 2,
"type": "uint32",
"readSchedule": "init",
"registerType": "holdingRegister",
"description": "Firmware version",
"access": "RO"
},
{
"id": "inverterStatus",
"address": 10105,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Inverter status",
"enum": "InverterStatus",
"defaultValue": "InverterStatusWait",
"access": "RO"
},
{
"id": "phaseAPower",
"address": 10994,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Phase A power",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "phaseBPower",
"address": 10996,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Phase B power",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "phaseCPower",
"address": 10998,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Phase C power",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "totalPowerOnMeter",
"address": 11000,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total power on meter",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "totalGridInjectionEnergy",
"address": 11002,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total grid injection energy on meter",
"defaultValue": 0,
"unit": "1/100 kWh",
"access": "RO"
},
{
"id": "totalPurchasingEnergyFromGrid",
"address": 11004,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total purchasing energy from grid on meter",
"defaultValue": 0,
"unit": "1/100 kWh",
"access": "RO"
},
{
"id": "gridPhaseAVoltage",
"address": 11009,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Grid Phase A Voltage",
"defaultValue": 0,
"unit": "1/10 V",
"access": "RO"
},
{
"id": "gridPhaseACurrent",
"address": 11010,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Grid Phase A Current",
"defaultValue": 0,
"unit": "1/10 A",
"access": "RO"
},
{
"id": "gridPhaseBVoltage",
"address": 11011,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Grid Phase B Voltage",
"defaultValue": 0,
"unit": "1/10 V",
"access": "RO"
},
{
"id": "gridPhaseBCurrent",
"address": 11012,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Grid Phase B Current",
"defaultValue": 0,
"unit": "1/10 A",
"access": "RO"
},
{
"id": "gridPhaseCVoltage",
"address": 11013,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Grid Phase C Voltage",
"defaultValue": 0,
"unit": "1/10 V",
"access": "RO"
},
{
"id": "gridPhaseCCurrent",
"address": 11014,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Grid Phase C Current",
"defaultValue": 0,
"unit": "1/10 A",
"access": "RO"
},
{
"id": "pAC",
"address": 11016,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "P_AC",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "totalPVGenerationFromInstallation",
"address": 11020,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total PV Generation from installation",
"defaultValue": 0,
"unit": "1/10 kWh",
"access": "RO"
},
{
"id": "pvInputTotalPower",
"address": 11028,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "PV Total Input Power",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "totalBackupP",
"address": 30230,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total_Backup_P/AC Active Power",
"unit": "1/1000 kW",
"defaultValue": 0,
"access": "RO"
},
{
"id": "batteryMode",
"address": 30256,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Battery mode",
"enum": "BatteryMode",
"defaultValue": "BatteryModeDischarge",
"access": "RO"
},
{
"id": "batteryPower",
"address": 30258,
"size": 2,
"type": "int32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Battery power",
"defaultValue": 0,
"unit": "1/1000 kW",
"access": "RO"
},
{
"id": "totalEnergyInjectedToGrid",
"address": 31102,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total energy injected to grid",
"defaultValue": 0,
"unit": "1/10 kWh",
"access": "RO"
},
{
"id": "totalEnergyPurchasedFromGrid",
"address": 31104,
"size": 2,
"type": "uint32",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Total energy purchased from grid",
"defaultValue": 0,
"unit": "1/10 kWh",
"access": "RO"
},
{
"id": "batteryStrings",
"address": 32001,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "Battery strings",
"defaultValue": 0,
"access": "RO"
},
{
"id": "SOC",
"address": 33000,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "SOC",
"defaultValue": 0,
"unit": "% * 100",
"access": "RO"
},
{
"id": "SOH",
"address": 33001,
"size": 1,
"type": "uint16",
"readSchedule": "update",
"registerType": "holdingRegister",
"description": "SOH",
"defaultValue": 0,
"unit": "% * 100",
"access": "RO"
}
]
}

15
wattsonic/wattsonic.pro Normal file
View File

@ -0,0 +1,15 @@
include(../plugins.pri)
# Generate modbus connection
MODBUS_CONNECTIONS += wattsonic-registers.json
#MODBUS_TOOLS_CONFIG += VERBOSE
include(../modbus.pri)
HEADERS += \
wattsonicdiscovery.h \
integrationpluginwattsonic.h
SOURCES += \
wattsonicdiscovery.cpp \
integrationpluginwattsonic.cpp

View File

@ -0,0 +1,100 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "wattsonicdiscovery.h"
#include "extern-plugininfo.h"
#include <modbusdatautils.h>
QList<int> slaveIdCandidates = {247};
WattsonicDiscovery::WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent):
QObject{parent},
m_modbusRtuResource(modbusRtuResource)
{
}
void WattsonicDiscovery::startDiscovery()
{
qCInfo(dcWattsonic()) << "Discovery: Searching for Wattsonic device on modbus RTU...";
QList<ModbusRtuMaster*> candidateMasters;
foreach (ModbusRtuMaster *master, m_modbusRtuResource->modbusRtuMasters()) {
if (master->baudrate() == 9600 && master->dataBits() == 8 && master->stopBits() == 1 && master->parity() == QSerialPort::NoParity) {
candidateMasters.append(master);
}
}
if (candidateMasters.isEmpty()) {
qCWarning(dcWattsonic()) << "No usable modbus RTU master found.";
emit discoveryFinished(false);
return;
}
foreach (ModbusRtuMaster *master, candidateMasters) {
if (master->connected()) {
tryConnect(master, 0);
} else {
qCWarning(dcWattsonic()) << "Modbus RTU master" << master->modbusUuid().toString() << "is not connected.";
}
}
}
QList<WattsonicDiscovery::Result> WattsonicDiscovery::discoveryResults() const
{
return m_discoveryResults;
}
void WattsonicDiscovery::tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex)
{
quint8 slaveId = slaveIdCandidates.at(slaveIdIndex);
qCDebug(dcWattsonic()) << "Scanning modbus RTU master" << master->modbusUuid() << "Slave ID:" << slaveId;
ModbusRtuReply *reply = master->readHoldingRegister(slaveId, 10000, 8);
connect(reply, &ModbusRtuReply::finished, this, [=](){
if (reply->error() == ModbusRtuReply::NoError) {
QString serialNumber = ModbusDataUtils::convertToString(reply->result(), ModbusDataUtils::ByteOrderBigEndian);
qCDebug(dcWattsonic()) << "Test reply finished!" << reply->error() << reply->result() << serialNumber;
Result result {master->modbusUuid(), serialNumber, slaveId};
m_discoveryResults.append(result);
}
if (slaveIdIndex < slaveIdCandidates.count() - 1) {
tryConnect(master, slaveIdIndex+1);
} else {
emit discoveryFinished(true);
}
});
}

View File

@ -0,0 +1,65 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef WATTSONICDISCOVERY_H
#define WATTSONICDISCOVERY_H
#include <QObject>
#include <hardware/modbus/modbusrtuhardwareresource.h>
class WattsonicDiscovery : public QObject
{
Q_OBJECT
public:
explicit WattsonicDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent = nullptr);
struct Result {
QUuid modbusRtuMasterId;
QString serialNumber;
quint16 slaveId;
};
void startDiscovery();
QList<Result> discoveryResults() const;
signals:
void discoveryFinished(bool modbusRtuMasterAvailable);
private slots:
void tryConnect(ModbusRtuMaster *master, quint16 slaveIdIndex);
private:
ModbusRtuHardwareResource *m_modbusRtuResource = nullptr;
QList<Result> m_discoveryResults;
};
#endif // WATTSONICDISCOVERY_H