New plugin: Solax inverter, meter and battery

modbus-tool: Introduce queued update request mechanism for TCP connections
This commit is contained in:
Simon Stürz 2023-05-22 14:29:23 +02:00
parent 63387535cb
commit 4d54c662cb
20 changed files with 3227 additions and 380 deletions

8
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

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/

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

@ -352,11 +352,11 @@ def writeBlockGetMethodDeclarations(fileDescriptor, registerDefinitions):
def writePropertyUpdateMethodDeclarations(fileDescriptor, registerDefinitions):
for registerDefinition in registerDefinitions:
if 'readSchedule' in registerDefinition and registerDefinition['readSchedule'] == 'init':
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
writeLine(fileDescriptor, ' void update%s();' % (propertyName[0].upper() + propertyName[1:]))
@ -427,12 +427,23 @@ def writeBlocksUpdateMethodDeclarations(fileDescriptor, blockDefinitions):
def writeRegistersDebugLine(fileDescriptor, debugObjectParamName, registerDefinitions):
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
line = ('" - %s - %s: " << %s->%s()' % (registerDefinition['address'], registerDefinition['description'], debugObjectParamName, propertyName))
registerType = registerDefinition['registerType']
typeString = ''
if registerType == 'holdingRegister':
typeString = 'holding '
elif registerType == 'inputRegister':
typeString = 'input '
elif registerType == 'coils':
typeString = 'coils '
elif registerType == 'discreteInputs':
typeString = 'discrete'
line = ('" - %s %s | %s: " << %s->%s()' % (typeString, registerDefinition['address'], registerDefinition['description'], debugObjectParamName, propertyName))
if 'unit' in registerDefinition and registerDefinition['unit'] != '':
line += (' << " [%s]"' % registerDefinition['unit'])
writeLine(fileDescriptor, ' debug.nospace().noquote() << %s << "\\n";' % (line))
@ -440,8 +451,9 @@ def writeRegistersDebugLine(fileDescriptor, debugObjectParamName, registerDefini
def writePropertyChangedSignals(fileDescriptor, registerDefinitions):
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
@ -455,8 +467,9 @@ def writePropertyChangedSignals(fileDescriptor, registerDefinitions):
def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions):
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
propertyTyp = getCppDataType(registerDefinition)
@ -467,10 +480,10 @@ def writeProtectedPropertyMembers(fileDescriptor, registerDefinitions):
def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions):
propertyVariables = []
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyName = registerDefinition['id']
writeLine(fileDescriptor, ' void process%sRegisterValues(const QVector<quint16> &values);' % (propertyName[0].upper() + propertyName[1:]))
@ -479,10 +492,10 @@ def writePropertyProcessMethodDeclaration(fileDescriptor, registerDefinitions):
def writePropertyProcessMethodImplementations(fileDescriptor, className, registerDefinitions):
propertyVariables = []
for registerDefinition in registerDefinitions:
if not 'R' in registerDefinition['access']:
continue
if 'access' in registerDefinition:
if not 'R' in registerDefinition['access']:
continue
propertyTyp = getCppDataType(registerDefinition)
propertyName = registerDefinition['id']
@ -498,3 +511,55 @@ def writePropertyProcessMethodImplementations(fileDescriptor, className, registe
writeLine(fileDescriptor, ' }')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeSendNextQueuedInitRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::sendNextQueuedInitRequest()' % (className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_initRequestQueue.isEmpty())')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' if (m_currentInitReply)')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' %s::Function function = m_initRequestQueue.dequeue();' % (className))
writeLine(fileDescriptor, ' (this->*function)();')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeEnqueueInitRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::enqueueInitRequest(%s::Function function)' % (className, className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_initRequestQueue.contains(function))')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' m_initRequestQueue.enqueue(function);')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeSendNextQueuedRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::sendNextQueuedRequest()' % (className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_updateRequestQueue.isEmpty())')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' if (m_currentUpdateReply)')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' %s::Function function = m_updateRequestQueue.dequeue();' % (className))
writeLine(fileDescriptor, ' (this->*function)();')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)
def writeEnqueueRequestMethodImplementation(fileDescriptor, className):
writeLine(fileDescriptor, 'void %s::enqueueRequest(%s::Function function)' % (className, className))
writeLine(fileDescriptor, '{')
writeLine(fileDescriptor, ' if (m_updateRequestQueue.contains(function))')
writeLine(fileDescriptor, ' return;')
writeLine(fileDescriptor)
writeLine(fileDescriptor, ' m_updateRequestQueue.enqueue(function);')
writeLine(fileDescriptor, '}')
writeLine(fileDescriptor)

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)
@ -72,6 +76,12 @@ def writeTcpHeaderFile():
writeLine(headerFile)
writeLine(headerFile, ' bool reachable() const;')
writeLine(headerFile)
# Write init and update method declarations
writeLine(headerFile, ' virtual bool initialize();')
writeLine(headerFile, ' virtual bool update();')
writeLine(headerFile)
writeLine(headerFile, ' ModbusDataUtils::ByteOrder endianness() const;')
writeLine(headerFile, ' void setEndianness(ModbusDataUtils::ByteOrder endianness);')
writeLine(headerFile)
@ -109,11 +119,6 @@ def writeTcpHeaderFile():
writeLine(headerFile)
# Write init and update method declarations
writeLine(headerFile, ' virtual bool initialize();')
writeLine(headerFile, ' virtual bool update();')
writeLine(headerFile)
writeLine(headerFile, 'public slots:')
writeLine(headerFile, ' bool connectDevice();')
writeLine(headerFile, ' void disconnectDevice();')
@ -166,6 +171,14 @@ def writeTcpHeaderFile():
writeLine(headerFile, ' ModbusDataUtils::ByteOrder m_stringEndianness = ModbusDataUtils::ByteOrder%s;' % stringEndianness)
writeLine(headerFile, ' quint16 m_slaveId = 1;')
writeLine(headerFile)
if queuedRequests:
writeLine(headerFile, ' QModbusReply *m_currentInitReply = nullptr;')
writeLine(headerFile, ' QQueue<%s::Function> m_initRequestQueue;' % className)
writeLine(headerFile, ' QModbusReply *m_currentUpdateReply = nullptr;')
writeLine(headerFile, ' QQueue<%s::Function> m_updateRequestQueue;' % className)
writeLine(headerFile)
writeLine(headerFile, ' bool m_reachable = false;')
writeLine(headerFile, ' QModbusReply *m_checkRechableReply = nullptr;')
writeLine(headerFile, ' uint m_checkReachableRetries = 0;')
@ -178,16 +191,24 @@ def writeTcpHeaderFile():
writeLine(headerFile, ' QVector<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 +226,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>')
@ -310,8 +332,8 @@ def writeTcpSourceFile():
if 'blocks' in registerJson:
blocks = registerJson['blocks']
writeInitMethodImplementationTcp(sourceFile, className, registerJson['registers'], blocks)
writeUpdateMethodTcp(sourceFile, className, registerJson['registers'], blocks)
writeInitMethodImplementationTcp(sourceFile, className, registerJson['registers'], blocks, queuedRequests)
writeUpdateMethodTcp(sourceFile, className, registerJson['registers'], blocks, queuedRequests)
writeLine(sourceFile, 'bool %s::connectDevice()' % (className))
writeLine(sourceFile, '{')
@ -332,13 +354,13 @@ def writeTcpSourceFile():
writeLine(sourceFile)
# Write update methods
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, registerJson['registers'])
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, registerJson['registers'], queuedRequests, queuedRequestsDelay)
if 'blocks' in registerJson:
for blockDefinition in registerJson['blocks']:
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, blockDefinition['registers'])
writePropertyUpdateMethodImplementationsTcp(sourceFile, className, blockDefinition['registers'], queuedRequests, queuedRequestsDelay)
# Write block update method
writeBlockUpdateMethodImplementationsTcp(sourceFile, className, registerJson['blocks'])
writeBlockUpdateMethodImplementationsTcp(sourceFile, className, registerJson['blocks'], queuedRequests, queuedRequestsDelay)
# Write internal protected property read method implementations
writeInternalPropertyReadMethodImplementationsTcp(sourceFile, className, registerJson['registers'])
@ -377,11 +399,19 @@ def writeTcpSourceFile():
writeTestReachabilityImplementationsTcp(sourceFile, className, registerJson['registers'], checkReachableRegister)
writeLine(sourceFile, 'void %s::verifyInitFinished()' % (className))
writeLine(sourceFile, 'bool %s::verifyInitFinished()' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {')
writeLine(sourceFile, ' finishInitialization(true);')
writeLine(sourceFile, ' }')
if queuedRequests:
writeLine(sourceFile, ' if (m_initRequestQueue.isEmpty() && !m_currentInitReply) {')
writeLine(sourceFile, ' finishInitialization(true);')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
else:
writeLine(sourceFile, ' if (m_pendingInitReplies.isEmpty()) {')
writeLine(sourceFile, ' finishInitialization(true);')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
writeLine(sourceFile, ' return false;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -393,12 +423,19 @@ def writeTcpSourceFile():
writeLine(sourceFile, ' qCWarning(dc%s()) << "Initialization finished of %s" << m_modbusTcpMaster->hostAddress().toString() << "failed.";' % (className, className))
writeLine(sourceFile, ' }')
writeLine(sourceFile)
writeLine(sourceFile, ' // Cleanup init')
writeLine(sourceFile, ' delete m_initObject;')
writeLine(sourceFile, ' m_initObject = nullptr;')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
if queuedRequests:
writeLine(sourceFile, ' m_initRequestQueue.clear();')
else:
writeLine(sourceFile, ' // Cleanup init')
writeLine(sourceFile, ' delete m_initObject;')
writeLine(sourceFile, ' m_initObject = nullptr;')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile)
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' QTimer::singleShot(0, this, [this, success](){')
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' });')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -410,6 +447,10 @@ def writeTcpSourceFile():
writeLine(sourceFile, ' // Cleanup before starting to initialize')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile, ' m_pendingUpdateReplies.clear();')
if queuedRequests:
writeLine(sourceFile, ' m_updateRequestQueue.clear();')
writeLine(sourceFile, ' m_initRequestQueue.clear();')
writeLine(sourceFile, ' m_communicationWorking = false;')
writeLine(sourceFile, ' m_communicationFailedCounter = 0;')
writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;')
@ -419,6 +460,11 @@ def writeTcpSourceFile():
writeLine(sourceFile, ' m_communicationWorking = false;')
writeLine(sourceFile, ' m_communicationFailedCounter = 0;')
writeLine(sourceFile, ' m_checkReachableRetriesCount = 0;')
if queuedRequests:
writeLine(sourceFile, ' m_updateRequestQueue.clear();')
writeLine(sourceFile, ' m_initRequestQueue.clear();')
writeLine(sourceFile, ' }')
writeLine(sourceFile)
writeLine(sourceFile, ' evaluateReachableState();')
@ -426,11 +472,18 @@ def writeTcpSourceFile():
writeLine(sourceFile, '}')
writeLine(sourceFile)
writeLine(sourceFile, 'void %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, 'bool %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {')
writeLine(sourceFile, ' emit updateFinished();')
if queuedRequests:
writeLine(sourceFile, ' if (m_updateRequestQueue.isEmpty() && !m_currentUpdateReply) {')
writeLine(sourceFile, ' emit updateFinished();')
writeLine(sourceFile, ' return true;')
else:
writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {')
writeLine(sourceFile, ' emit updateFinished();')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
writeLine(sourceFile, ' return false;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -461,7 +514,11 @@ def writeTcpSourceFile():
writeLine(sourceFile, '}')
writeLine(sourceFile)
if queuedRequests:
writeSendNextQueuedInitRequestMethodImplementation(sourceFile, className)
writeEnqueueInitRequestMethodImplementation(sourceFile, className)
writeSendNextQueuedRequestMethodImplementation(sourceFile, className)
writeEnqueueRequestMethodImplementation(sourceFile, className)
# Write the debug print
debugObjectParamName = className[0].lower() + className[1:]
@ -622,7 +679,7 @@ def writeRtuHeaderFile():
writeLine(headerFile, ' void verifyInitFinished();')
writeLine(headerFile, ' void finishInitialization(bool success);')
writeLine(headerFile)
writeLine(headerFile, ' void verifyUpdateFinished();')
writeLine(headerFile, ' bool verifyUpdateFinished();')
writeLine(headerFile)
writeLine(headerFile, ' void onReachabilityCheckFailed();')
writeLine(headerFile, ' void evaluateReachableState();')
@ -645,6 +702,7 @@ def writeRtuSourceFile():
writeLicenseHeader(sourceFile)
writeLine(sourceFile, '#include "%s"' % headerFileName)
writeLine(sourceFile)
writeLine(sourceFile, '#include <loggingcategories.h>')
writeLine(sourceFile, '#include <math.h>')
writeLine(sourceFile, '#include <QTimer>')
@ -831,15 +889,26 @@ def writeRtuSourceFile():
writeLine(sourceFile, ' m_initObject = nullptr;')
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile)
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' QTimer::singleShot(0, this, [this, success](){')
writeLine(sourceFile, ' emit initializationFinished(success);')
writeLine(sourceFile, ' });')
if queuedRequests:
writeLine(sourceFile)
writeLine(sourceFile, ' m_pendingInitReplies.clear();')
writeLine(sourceFile, ' m_pendingUpdateReplies.clear();')
writeLine(sourceFile, ' update();')
writeLine(sourceFile, '}')
writeLine(sourceFile)
writeLine(sourceFile, 'void %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, 'bool %s::verifyUpdateFinished()' % (className))
writeLine(sourceFile, '{')
writeLine(sourceFile, ' if (m_pendingUpdateReplies.isEmpty()) {')
writeLine(sourceFile, ' emit updateFinished();')
writeLine(sourceFile, ' return true;')
writeLine(sourceFile, ' }')
writeLine(sourceFile, ' return false;')
writeLine(sourceFile, '}')
writeLine(sourceFile)
@ -977,15 +1046,28 @@ else:
logger.debug('Verified successfully checkReachableRegister: %s' % checkReachableRegister['id'])
queuedRequests = False
queuedRequestsDelay = 0
if 'queuedRequests' in registerJson:
queuedRequests = registerJson['queuedRequests']
if 'queuedRequestsDelay' in registerJson:
queuedRequestsDelay = registerJson['queuedRequestsDelay']
# Inform about parsed and validated configs if debugging enabled
logger.debug('Script path: %s' % scriptPath)
logger.debug('Output directory: %s' % outputDirectory)
logger.debug('Class name prefix: %s' % classNamePrefix)
logger.debug('Endianness: %s' % endianness)
logger.debug('String endianness: %s' % stringEndianness)
logger.debug('Queued requests: %s' % queuedRequests)
logger.debug('Queued requests delay: %s ms' % queuedRequestsDelay)
logger.debug('Error limit until not reachable: %s' % errorLimitUntilNotReachable)
logger.debug('Check reachable register: %s' % checkReachableRegister['id'])
protocol = 'TCP'
if 'protocol' in registerJson:
protocol = registerJson['protocol']
@ -1063,7 +1145,7 @@ projectIncludeFile = open(projectIncludeFilePath, 'w')
writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #')
writeLine(projectIncludeFile, '#')
writeLine(projectIncludeFile, '# This file has been autogenerated.')
writeLine(projectIncludeFile, '# Any changes in this file may be overwritten from qmake.')
writeLine(projectIncludeFile, '# Any changes in this file may be overwritten on a rebuild.')
writeLine(projectIncludeFile, '#')
writeLine(projectIncludeFile, '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #')
writeLine(projectIncludeFile)

View File

@ -21,6 +21,7 @@ PLUGIN_DIRS = \
schrack \
senseair \
sma \
solax \
stiebeleltron \
sunspec \
unipi \

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,472 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <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 {
// Start update cycle
solaxConnection->update();
}
});
connect(solaxConnection, &SolaxModbusTcpConnection::updateFinished, thing, [=](){
qCDebug(dcSolax()) << "Updated" << solaxConnection;
ThingClass meterThingClass = thingClass(solaxMeterThingClassId);
// Check if we have to create the meter for this solax inverter, or remove it due to communication errors
if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).isEmpty()) {
if (solaxConnection->meter1ComState() == 1 && myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 1).isEmpty()) {
qCDebug(dcSolax()) << "There is no meter set up for this inverter. Creating a meter 1 for" << thing << solaxConnection->modbusTcpMaster();
ThingDescriptor descriptor(solaxMeterThingClassId, meterThingClass.displayName(), QString(), thing->id());
ParamList params;
params.append(Param(solaxMeterThingIdParamTypeId, 1));
descriptor.setParams(params);
emit autoThingsAppeared(ThingDescriptors() << descriptor);
}
}
// Note: it is possible to connect an additional inverter to the solax inverter, which will be measured by the meter 2.
// if so, we create a child inverter instead of the second meter thing.
if (solaxConnection->meter2ComState() == 1) {
if (thing->setting(solaxInverterTcpSettingsMeter2InverterParamTypeId).toBool()) {
if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId).isEmpty()) {
// Add the meter 2 as child inverter
emit autoThingsAppeared(ThingDescriptors() << ThingDescriptor(solaxInverterChildThingClassId, "SolaX inverter", QString(), thing->id()));
}
} else {
if (myThings().filterByParentId(thing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 2).isEmpty()) {
ThingDescriptor descriptor(solaxMeterThingClassId, meterThingClass.displayName() + " 2", QString(), thing->id());
ParamList params;
params.append(Param(solaxMeterThingIdParamTypeId, 2));
descriptor.setParams(params);
emit autoThingsAppeared(ThingDescriptors() << descriptor);
}
}
} else {
// Communication error with meter 2, remove any child devices for meter 2 registers
cleanupMeter2(thing);
cleanupChildInverters(thing);
}
// Check if we have to create the battery for the solax inverter
if (solaxConnection->batteryConnected() != 0 && myThings().filterByParentId(thing->id()).filterByThingClassId(solaxBatteryThingClassId).isEmpty()) {
qCDebug(dcSolax()) << "There is a battery connected but not set up yet. Creating a battery...";
ThingClass batteryThingClass = thingClass(solaxBatteryThingClassId);
ThingDescriptor descriptor(solaxBatteryThingClassId, batteryThingClass.displayName(), QString(), thing->id());
emit autoThingsAppeared(ThingDescriptors() << descriptor);
}
// Update inverter states
thing->setStateValue(solaxInverterTcpCurrentPowerStateTypeId, -solaxConnection->inverterPower());
thing->setStateValue(solaxInverterTcpCurrentStateTypeId, -solaxConnection->inverterCurrent());
thing->setStateValue(solaxInverterTcpCurrentVoltageStateTypeId, solaxConnection->inverterVoltage());
thing->setStateValue(solaxInverterTcpTemperatureStateTypeId, solaxConnection->temperature());
thing->setStateValue(solaxInverterTcpFrequencyStateTypeId, solaxConnection->inverterFrequency());
thing->setStateValue(solaxInverterTcpTotalEnergyProducedStateTypeId, solaxConnection->totalEnergyProduced());
// Update the meter 1 if available
Thing *meterThing = getMeterThing(thing, 1);
if (meterThing) {
meterThing->setStateValue(solaxMeterTotalEnergyConsumedStateTypeId, solaxConnection->meterTotalEnergyConsumend());
meterThing->setStateValue(solaxMeterTotalEnergyProducedStateTypeId, solaxConnection->meterTotalEnergyProduced());
// Power
meterThing->setStateValue(solaxMeterCurrentPowerStateTypeId, -solaxConnection->meterPower());
}
// Update inverter 2 states if available
if (thing->setting(solaxInverterTcpSettingsMeter2InverterParamTypeId).toBool()) {
Things childInverters = myThings().filterByParentId(thing->id()).filterByThingClassId(solaxInverterChildThingClassId);
if (!childInverters.isEmpty()) {
Thing *childInverter = childInverters.first();
childInverter->setStateValue(solaxInverterChildCurrentPowerStateTypeId, -solaxConnection->meter2Power());
childInverter->setStateValue(solaxInverterChildTotalEnergyProducedStateTypeId, solaxConnection->meter2EnergyProduced());
}
} else {
// Update the meter 2 if available
meterThing = getMeterThing(thing, 2);
if (meterThing) {
meterThing->setStateValue(solaxMeterTotalEnergyConsumedStateTypeId, solaxConnection->meter2EnergyConsumed());
meterThing->setStateValue(solaxMeterTotalEnergyProducedStateTypeId, solaxConnection->meter2EnergyProduced());
// Power
meterThing->setStateValue(solaxMeterCurrentPowerStateTypeId, solaxConnection->meter2Power());
meterThing->setStateValue(solaxMeterCurrentPowerPhaseAStateTypeId, solaxConnection->meter2PowerR());
meterThing->setStateValue(solaxMeterCurrentPowerPhaseBStateTypeId, solaxConnection->meter2PowerS());
meterThing->setStateValue(solaxMeterCurrentPowerPhaseCStateTypeId, solaxConnection->meter2PowerT());
}
}
// Update the battery if available
Thing *batteryThing = getBatteryThing(thing);
if (batteryThing) {
batteryThing->setStateValue(solaxBatteryVoltageStateTypeId, solaxConnection->batteryVoltage());
batteryThing->setStateValue(solaxBatteryTemperatureStateTypeId, solaxConnection->batteryTemperature());
batteryThing->setStateValue(solaxBatteryBatteryLevelStateTypeId, solaxConnection->batteryCapacity());
batteryThing->setStateValue(solaxBatteryBatteryCriticalStateTypeId, solaxConnection->batteryCapacity() < 5);
double batteryPower = solaxConnection->batteryPower();
batteryThing->setStateValue(solaxBatteryCurrentPowerStateTypeId, solaxConnection->batteryPower());
if (batteryPower == 0) {
batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "idle");
} else if (batteryPower < 0) {
batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "discharging");
} else if (batteryPower > 0) {
batteryThing->setStateValue(solaxBatteryChargingStateStateTypeId, "charging");
}
}
// Run the next update cycle
solaxConnection->update();
});
connect(thing, &Thing::settingChanged, solaxConnection, [this, thing](const ParamTypeId &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() == solaxMeterThingClassId || thing->thingClassId() == solaxBatteryThingClassId || thing->thingClassId() == solaxInverterChildThingClassId) {
Thing *connectionThing = myThings().findById(thing->parentId());
if (connectionThing) {
thing->setStateValue("connected", connectionThing->stateValue("connected"));
}
return;
}
}
void IntegrationPluginSolax::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == solaxInverterTcpThingClassId && m_tcpConnections.contains(thing)) {
SolaxModbusTcpConnection *connection = m_tcpConnections.take(thing);
connection->modbusTcpMaster()->disconnectDevice();
delete connection;
}
// Unregister related hardware resources
if (m_monitors.contains(thing))
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
Thing *IntegrationPluginSolax::getMeterThing(Thing *parentThing, uint meterId)
{
Things meterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, meterId);
if (meterThings.isEmpty())
return nullptr;
return meterThings.first();
}
Thing *IntegrationPluginSolax::getBatteryThing(Thing *parentThing)
{
Things batteryThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxBatteryThingClassId);
if (batteryThings.isEmpty())
return nullptr;
return batteryThings.first();
}
void IntegrationPluginSolax::cleanupMeter2(Thing *parentThing)
{
Things meters = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxMeterThingClassId).filterByParam(solaxMeterThingIdParamTypeId, 2);
if (!meters.isEmpty()) {
emit autoThingDisappeared(meters.first()->id());
}
}
void IntegrationPluginSolax::cleanupChildInverters(Thing *parentThing)
{
Things inverterThings = myThings().filterByParentId(parentThing->id()).filterByThingClassId(solaxInverterChildThingClassId);
if (!inverterThings.isEmpty()) {
foreach (Thing *inverterThing, inverterThings) {
emit autoThingDisappeared(inverterThing->id());
}
}
}

View File

@ -0,0 +1,74 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <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:
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>