SunSpec: Update to networkdevice interface

sunsprec-networkdevice-interface
Simon Stürz 2024-12-19 10:07:08 +01:00
parent 36f9f3c512
commit 2a27534279
5 changed files with 122 additions and 53 deletions

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Copyright 2013 - 2024, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -70,12 +70,18 @@ IntegrationPluginSunSpec::IntegrationPluginSunSpec()
void IntegrationPluginSunSpec::init()
{
// SunSpec connection params
m_connectionPortParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingPortParamTypeId);
m_connectionPortParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingPortParamTypeId);
m_connectionMacAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingMacAddressParamTypeId);
m_connectionMacAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingMacAddressParamTypeId);
m_connectionHostNameParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingHostNameParamTypeId);
m_connectionHostNameParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingHostNameParamTypeId);
m_connectionAddressParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingAddressParamTypeId);
m_connectionAddressParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingAddressParamTypeId);
m_connectionPortParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingPortParamTypeId);
m_connectionPortParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingPortParamTypeId);
m_connectionSlaveIdParamTypeIds.insert(sunspecConnectionThingClassId, sunspecConnectionThingSlaveIdParamTypeId);
m_connectionSlaveIdParamTypeIds.insert(solarEdgeConnectionThingClassId, solarEdgeConnectionThingSlaveIdParamTypeId);
@ -141,7 +147,7 @@ void IntegrationPluginSunSpec::discoverThings(ThingDiscoveryInfo *info)
SunSpecDiscovery *discovery = new SunSpecDiscovery(hardwareManager()->networkDeviceDiscovery(), slaveIds, byteOrder, info);
// Note: we could add here more
connect(discovery, &SunSpecDiscovery::discoveryFinished, info, [=](){
connect(discovery, &SunSpecDiscovery::discoveryFinished, info, [this, info, discovery](){
foreach (const SunSpecDiscovery::Result &result, discovery->results()) {
// Extract the manufacturer: we pick the first manufacturer name of the first common model having a manufacturer name for now
@ -166,6 +172,7 @@ void IntegrationPluginSunSpec::discoverThings(ThingDiscoveryInfo *info)
// Full support of meter, inverter and storage will be provided in the kostal plugin which makes
// use of the native modbus communication from kostal.
if (hasManufacturer(result.modelManufacturers, "kostal")) {
qCWarning(dcSunSpec()) << "Skipping Kostal manufacturer on SunSpec. Please use the native kostal integration plugin in order to add it to the system.";
continue;
}
}
@ -177,26 +184,40 @@ void IntegrationPluginSunSpec::discoverThings(ThingDiscoveryInfo *info)
title.append("SunSpec connection");
QString description;
if (result.networkDeviceInfo.macAddressManufacturer().isEmpty()) {
description = result.networkDeviceInfo.macAddress();
} else {
description = result.networkDeviceInfo.macAddress() + " (" + result.networkDeviceInfo.macAddressManufacturer() + ")";
MacAddressInfo macInfo;
switch (result.networkDeviceInfo.monitorMode()) {
case NetworkDeviceInfo::MonitorModeMac:
macInfo = result.networkDeviceInfo.macAddressInfos().constFirst();
description = result.networkDeviceInfo.address().toString();
if (!macInfo.vendorName().isEmpty())
description += " - " + result.networkDeviceInfo.macAddressInfos().constFirst().vendorName();
break;
case NetworkDeviceInfo::MonitorModeHostName:
description = result.networkDeviceInfo.hostName();
break;
case NetworkDeviceInfo::MonitorModeIp:
description = "Interface: " + result.networkDeviceInfo.networkInterface().name();
break;
}
ThingDescriptor descriptor(info->thingClassId(), title, description);
// Check if we already have set up this device
Things existingThings = myThings().filterByParam(m_connectionMacAddressParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.macAddress());
if (existingThings.count() == 1) {
qCDebug(dcSunSpec()) << "This thing already exists in the system." << existingThings.first() << result.networkDeviceInfo;
descriptor.setThingId(existingThings.first()->id());
}
ParamList params;
params << Param(m_connectionMacAddressParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.thingParamValueMacAddress());
params << Param(m_connectionHostNameParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.thingParamValueHostName());
params << Param(m_connectionAddressParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.thingParamValueAddress());
params << Param(m_connectionPortParamTypeIds.value(info->thingClassId()), result.port);
params << Param(m_connectionMacAddressParamTypeIds.value(info->thingClassId()), result.networkDeviceInfo.macAddress());
params << Param(m_connectionSlaveIdParamTypeIds.value(info->thingClassId()), result.slaveId);
descriptor.setParams(params);
// Check if we already have set up this device
Thing *existingThing = myThings().findByParams(params);
if (existingThing) {
qCDebug(dcSunSpec()) << "This thing already exists in the system:" << result.networkDeviceInfo;
descriptor.setThingId(existingThing->id());
}
info->addThingDescriptor(descriptor);
}
@ -213,7 +234,8 @@ void IntegrationPluginSunSpec::setupThing(ThingSetupInfo *info)
qCDebug(dcSunSpec()) << "Setup thing" << thing;
qCDebug(dcSunSpec()) << thing->params();
if (thing->thingClassId() == sunspecConnectionThingClassId || thing->thingClassId() == solarEdgeConnectionThingClassId) {
if (thing->thingClassId() == sunspecConnectionThingClassId ||
thing->thingClassId() == solarEdgeConnectionThingClassId) {
// Handle reconfigure
if (m_sunSpecConnections.contains(thing->id())) {
@ -225,16 +247,17 @@ void IntegrationPluginSunSpec::setupThing(ThingSetupInfo *info)
}
}
MacAddress macAddress = MacAddress(thing->paramValue(m_connectionMacAddressParamTypeIds.value(thing->thingClassId())).toString());
if (!macAddress.isValid()) {
qCWarning(dcSunSpec()) << "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 connection."));
// Create the monitor
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing);
if (!monitor) {
qCWarning(dcSunSpec()) << "Unable to register monitor with the given params" << thing->params();
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Unable to set up the connection with this configuration, please reconfigure the connection."));
return;
}
// Create the monitor
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
m_monitors.insert(thing, monitor);
QHostAddress address = monitor->networkDeviceInfo().address();
if (address.isNull() && info->isInitialSetup()) {
qCWarning(dcSunSpec()) << "Cannot set up thing. The host address is not known and this is an initial setup";
@ -305,9 +328,9 @@ void IntegrationPluginSunSpec::setupThing(ThingSetupInfo *info)
}
}
} else if (thing->thingClassId() == sunspecThreePhaseInverterThingClassId
|| thing->thingClassId() == sunspecSplitPhaseInverterThingClassId
|| thing->thingClassId() == sunspecSinglePhaseInverterThingClassId ) {
} else if (thing->thingClassId() == sunspecThreePhaseInverterThingClassId ||
thing->thingClassId() == sunspecSplitPhaseInverterThingClassId ||
thing->thingClassId() == sunspecSinglePhaseInverterThingClassId ) {
Thing *thing = info->thing();
uint modelId = thing->paramValue(m_modelIdParamTypeIds.value(thing->thingClassId())).toInt();
@ -420,9 +443,8 @@ void IntegrationPluginSunSpec::thingRemoved(Thing *thing)
Q_ASSERT_X(false, "thingRemoved", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
}
if (m_monitors.contains(thing)) {
if (m_monitors.contains(thing))
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
if (myThings().isEmpty()) {
qCDebug(dcSunSpec()) << "Stopping refresh timer";

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Copyright 2013 - 2024, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -62,9 +62,10 @@ public:
private:
// SunSpec Connection params map
QHash<ThingClassId, ParamTypeId> m_connectionIpParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_connectionPortParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_connectionMacAddressParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_connectionAddressParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_connectionHostNameParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_connectionPortParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_connectionSlaveIdParamTypeIds;
// SunSpec thing params map

View File

@ -40,15 +40,32 @@
"displayName": "SunSpec Generic",
"id": "f51853f3-8815-4cf3-b337-45cda1f3e6d5",
"createMethods": [ "Discovery" ],
"interfaces": ["gateway"],
"interfaces": [ "gateway", "networkdevice" ],
"providedInterfaces": [ "solarinverter", "energymeter", "energystorage"],
"paramTypes": [
{
"id": "3567b389-9d42-48f9-a29b-d18388fb36a1",
"name": "address",
"displayName": "Host address",
"type": "QString",
"inputType": "IPv4Address",
"defaultValue": ""
},
{
"id": "1667b0ed-9a2b-47c6-a01f-690caee55ffa",
"name": "hostName",
"displayName": "Host name",
"type": "QString",
"inputType": "TextLine",
"defaultValue": ""
},
{
"id": "f65d6c36-1672-44cb-b52a-62d71837ae67",
"name":"macAddress",
"displayName": "MAC address",
"type": "QString",
"defaultValue": "00:00:00:00:00:00"
"inputType": "MacAddress",
"defaultValue": ""
},
{
"id": "1fa4fc9c-f6be-47c7-928a-bcefc1142eec",
@ -1458,15 +1475,32 @@
"displayName": "SolarEdge",
"id": "7a92bf65-b443-4491-a012-2bec35eb5bf0",
"createMethods": [ "Discovery" ],
"interfaces": ["gateway"],
"interfaces": [ "gateway", "networkdevice" ],
"providedInterfaces": [ "solarinverter", "energymeter", "energystorage" ],
"paramTypes": [
{
"id": "9c2bafd0-6d56-42e0-8ef3-c4940b4f18b5",
"name": "address",
"displayName": "Host address",
"type": "QString",
"inputType": "IPv4Address",
"defaultValue": ""
},
{
"id": "2e22c369-8476-4908-9cdc-186472f4d472",
"name": "hostName",
"displayName": "Host name",
"type": "QString",
"inputType": "TextLine",
"defaultValue": ""
},
{
"id": "bb395c12-54d6-4139-b0a6-e31b28bc4d2e",
"name":"macAddress",
"displayName": "MAC address",
"type": "QString",
"defaultValue": "00:00:00:00:00:00"
"inputType": "MacAddress",
"defaultValue": ""
},
{
"id": "1bcede48-d167-4ced-8f1b-bea6302dd43f",

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Copyright 2013 - 2024, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -65,16 +65,16 @@ void SunSpecDiscovery::startDiscovery()
m_startDateTime = QDateTime::currentDateTime();
// Imedialty check any new device gets discovered
connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &SunSpecDiscovery::checkNetworkDevice);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &SunSpecDiscovery::checkNetworkDevice);
// Check what might be left on finished
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [this, discoveryReply](){
qCDebug(dcSunSpec()) << "Discovery: Network discovery finished. Give some time for pending discovery checks to finish...";
m_networkDeviceInfos = discoveryReply->networkDeviceInfos();
// Give the last connections added right before the network discovery finished a chance to check the device...
QTimer::singleShot(3000, this, [this](){
qCDebug(dcSunSpec()) << "Discovery: Grace period timer triggered";
finishDiscovery();
});
});
@ -121,7 +121,7 @@ void SunSpecDiscovery::testNextConnection(const QHostAddress &address)
connectionTimer->start();
}
void SunSpecDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
void SunSpecDiscovery::checkNetworkDevice(const QHostAddress &address)
{
// Create a connection queue for this network device
@ -132,13 +132,13 @@ void SunSpecDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevice
foreach (quint16 slaveId, m_slaveIds) {
SunSpecConnection *connection = new SunSpecConnection(networkDeviceInfo.address(), port, slaveId, m_byteOrder, this);
SunSpecConnection *connection = new SunSpecConnection(address, port, slaveId, m_byteOrder, this);
connection->setNumberOfRetries(1);
connection->setTimeout(500);
m_connections.append(connection);
connectionQueue.enqueue(connection);
connect(connection, &SunSpecConnection::connectedChanged, this, [=](bool connected){
connect(connection, &SunSpecConnection::connectedChanged, this, [this, connection, connectionQueue, address](bool connected){
if (!connected) {
// Disconnected ... done with this connection
cleanupConnection(connection);
@ -155,7 +155,9 @@ void SunSpecDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevice
// Modbus TCP connected, try to discovery sunspec models...
connect(connection, &SunSpecConnection::discoveryFinished, this, [=](bool success){
if (!success) {
qCDebug(dcSunSpec()) << "Discovery: SunSpec discovery failed on" << QString("%1:%2").arg(networkDeviceInfo.address().toString()).arg(port) << "slave ID:" << slaveId << "Continue...";;
qCDebug(dcSunSpec()) << "Discovery: SunSpec discovery failed on"
<< QString("%1:%2").arg(address.toString()).arg(connection->port())
<< "slave ID:" << connection->slaveId() << "Continue...";
cleanupConnection(connection);
return;
}
@ -163,7 +165,7 @@ void SunSpecDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevice
// Success, we found some sunspec models here, let's read some infomation from the models
Result result;
result.networkDeviceInfo = networkDeviceInfo;
result.address = address;
result.port = connection->port();
result.slaveId = connection->slaveId();
@ -186,7 +188,9 @@ void SunSpecDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevice
// Run SunSpec discovery on connection...
if (!connection->startDiscovery()) {
qCDebug(dcSunSpec()) << "Discovery: Unable to discover SunSpec data on connection" << QString("%1:%2").arg(networkDeviceInfo.address().toString()).arg(port) << "slave ID:" << slaveId << "Continue...";;
qCDebug(dcSunSpec()) << "Discovery: Unable to discover SunSpec data on connection"
<< QString("%1:%2").arg(address.toString()).arg(connection->port())
<< "slave ID:" << connection->slaveId() << "Continue...";
cleanupConnection(connection);
}
});
@ -194,15 +198,17 @@ void SunSpecDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDevice
// If we get any error...skip this host...
connect(connection->modbusTcpClient(), &QModbusTcpClient::errorOccurred, this, [=](QModbusDevice::Error error){
if (error != QModbusDevice::NoError) {
qCDebug(dcSunSpec()) << "Discovery: Connection error on" << QString("%1:%2").arg(networkDeviceInfo.address().toString()).arg(port) << "slave ID:" << slaveId << "Continue...";;
qCDebug(dcSunSpec()) << "Discovery: Connection error on"
<< QString("%1:%2").arg(address.toString()).arg(connection->port())
<< "slave ID:" << connection->slaveId() << "Continue...";
cleanupConnection(connection);
}
});
}
}
m_pendingConnectionAttempts[networkDeviceInfo.address()] = connectionQueue;
testNextConnection(networkDeviceInfo.address());
m_pendingConnectionAttempts[address] = connectionQueue;
testNextConnection(address);
}
void SunSpecDiscovery::cleanupConnection(SunSpecConnection *connection)
@ -223,10 +229,15 @@ void SunSpecDiscovery::finishDiscovery()
{
qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
// Fill in all network device infos we have
for (int i = 0; i < m_results.count(); i++)
m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address);
// Cleanup any leftovers...we don't care any more
foreach (SunSpecConnection *connection, m_connections)
cleanupConnection(connection);
qCInfo(dcSunSpec()) << "Discovery: Finished the discovery process. Found" << m_results.count() << "SunSpec devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
qCInfo(dcSunSpec()) << "Discovery: Finished the discovery process. Found" << m_results.count()
<< "SunSpec devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
emit discoveryFinished();
}

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Copyright 2013 - 2024, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
@ -44,6 +44,7 @@ class SunSpecDiscovery : public QObject
public:
explicit SunSpecDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, const QList<quint16> &slaveIds, SunSpecDataPoint::ByteOrder byteOrder = SunSpecDataPoint::ByteOrderLittleEndian, QObject *parent = nullptr);
typedef struct Result {
QHostAddress address;
NetworkDeviceInfo networkDeviceInfo;
quint16 port;
quint16 slaveId;
@ -67,13 +68,13 @@ private:
QDateTime m_startDateTime;
QHash<QHostAddress, QQueue<SunSpecConnection *>> m_pendingConnectionAttempts;
QHash<SunSpecConnection *, QTimer *> m_connectionTimers;
QList<SunSpecConnection *> m_connections;
NetworkDeviceInfos m_networkDeviceInfos;
QList<Result> m_results;
void testNextConnection(const QHostAddress &address);
void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
void checkNetworkDevice(const QHostAddress &address);
void cleanupConnection(SunSpecConnection *connection);
void finishDiscovery();