From 2b3a44cd6ce42cb569f0eb6d722300e5b813fd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 3 Dec 2021 13:15:52 +0100 Subject: [PATCH 01/17] Clean up code and set propper vendor name --- sma/integrationpluginsma.cpp | 88 ++++++++++++++++++----------------- sma/integrationpluginsma.json | 2 +- sma/sunnywebbox.cpp | 29 ++++++------ 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index 6dfa1835..6419d7f7 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -40,49 +40,51 @@ IntegrationPluginSma::IntegrationPluginSma() void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) { - if (!hardwareManager()->networkDeviceDiscovery()->available()) { - qCWarning(dcSma()) << "Failed to discover network devices. The network device discovery is not available."; - info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network.")); - return; - } - - qCDebug(dcSma()) << "Starting network discovery..."; - NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - ThingDescriptors descriptors; - qCDebug(dcSma()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; - foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { - // Filter for sma hosts - if (!networkDeviceInfo.hostName().toLower().contains("sma")) - continue; - - QString title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; - QString description; - if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { - description = networkDeviceInfo.macAddress(); - } else { - description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; - } - - ThingDescriptor descriptor(sunnyWebBoxThingClassId, title, description); - - // Check for reconfiguration - foreach (Thing *existingThing, myThings()) { - if (existingThing->paramValue(sunnyWebBoxThingMacAddressParamTypeId).toString() == networkDeviceInfo.macAddress()) { - descriptor.setThingId(existingThing->id()); - break; - } - } - - ParamList params; - params << Param(sunnyWebBoxThingHostParamTypeId, networkDeviceInfo.address().toString()); - params << Param(sunnyWebBoxThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); - descriptor.setParams(params); - descriptors.append(descriptor); + if (info->thingClassId() == sunnyWebBoxThingClassId) { + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcSma()) << "Failed to discover network devices. The network device discovery is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network.")); + return; } - info->addThingDescriptors(descriptors); - info->finish(Thing::ThingErrorNoError); - }); + + qCDebug(dcSma()) << "Starting network discovery..."; + NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + ThingDescriptors descriptors; + qCDebug(dcSma()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + // Filter for sma hosts + if (!networkDeviceInfo.hostName().toLower().contains("sma")) + continue; + + QString title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; + QString description; + if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { + description = networkDeviceInfo.macAddress(); + } else { + description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(sunnyWebBoxThingClassId, title, description); + + // Check for reconfiguration + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(sunnyWebBoxThingMacAddressParamTypeId).toString() == networkDeviceInfo.macAddress()) { + descriptor.setThingId(existingThing->id()); + break; + } + } + + ParamList params; + params << Param(sunnyWebBoxThingHostParamTypeId, networkDeviceInfo.address().toString()); + params << Param(sunnyWebBoxThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + descriptor.setParams(params); + descriptors.append(descriptor); + } + info->addThingDescriptors(descriptors); + info->finish(Thing::ThingErrorNoError); + }); + } } @@ -160,7 +162,7 @@ void IntegrationPluginSma::thingRemoved(Thing *thing) void IntegrationPluginSma::onRefreshTimer() { - Q_FOREACH(Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { + foreach (Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); sunnyWebBox->getPlantOverview(); } diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index 0a542fe4..5953b4f4 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -6,7 +6,7 @@ { "id": "16d5a4a3-36d5-46c0-b7dd-df166ddf5981", "name": "Sma", - "displayName": "SMA", + "displayName": "SMA Solar Technology AG", "thingClasses": [ { "id": "49304127-ce9b-45dd-8511-05030a4ac003", diff --git a/sma/sunnywebbox.cpp b/sma/sunnywebbox.cpp index f2b2c56a..dd1a6e07 100644 --- a/sma/sunnywebbox.cpp +++ b/sma/sunnywebbox.cpp @@ -69,7 +69,7 @@ QString SunnyWebBox::getProcessData(const QStringList &deviceKeys) { QJsonObject paramsObj; QJsonArray devicesArray; - Q_FOREACH(QString key, deviceKeys) { + foreach (const QString &key, deviceKeys) { QJsonObject deviceObj; deviceObj["key"] = key; devicesArray.append(deviceObj); @@ -93,7 +93,7 @@ QString SunnyWebBox::getParameters(const QStringList &deviceKeys) { QJsonObject paramsObj; QJsonArray devicesArray; - Q_FOREACH(QString key, deviceKeys) { + foreach (const QString &key, deviceKeys) { QJsonObject deviceObj; deviceObj["key"] = key; devicesArray.append(deviceObj); @@ -109,7 +109,7 @@ QString SunnyWebBox::setParameters(const QString &deviceKey, const QHash devices; QVariantList deviceList = result.value("devices").toList(); qCDebug(dcSma()) << "SunnyWebBox: GetDevices" << result.value("totalDevicesReturned").toInt(); - Q_FOREACH(QVariant value, deviceList) { + foreach (const QVariant &value, deviceList) { Device device; QVariantMap map = value.toMap(); device.name = map["name"].toString(); @@ -185,7 +185,7 @@ void SunnyWebBox::parseMessage(const QString &messageId, const QString &messageT device.key = map["key"].toString(); qCDebug(dcSma()) << "SunnyWebBox: - Key" << device.key; QVariantList childrenList = map["children"].toList(); - Q_FOREACH(QVariant childValue, childrenList) { + foreach (const QVariant &childValue, childrenList) { Device child; QVariantMap childMap = childValue.toMap(); device.name = childMap["name"].toString(); @@ -194,44 +194,43 @@ void SunnyWebBox::parseMessage(const QString &messageId, const QString &messageT } devices.append(device); } - if (!devices.isEmpty()) + if (!devices.isEmpty()) { emit devicesReceived(messageId, devices); + } } else if (messageType == "GetProcessDataChannels" || messageType == "GetProDataChannels") { - Q_FOREACH(QString deviceKey, result.keys()) { + foreach (const QString &deviceKey, result.keys()) { QStringList processDataChannels = result.value(deviceKey).toStringList(); if (!processDataChannels.isEmpty()) emit processDataChannelsReceived(messageId, deviceKey, processDataChannels); } } else if (messageType == "GetProcessData") { - QList devices; QVariantList devicesList = result.value("devices").toList(); qCDebug(dcSma()) << "SunnyWebBox: GetProcessData response received"; - Q_FOREACH(QVariant value, devicesList) { + foreach (const QVariant &value, devicesList) { QString key = value.toMap().value("key").toString(); QVariantList channelsList = value.toMap().value("channels").toList(); QHash channels; - Q_FOREACH(QVariant channel, channelsList) { + foreach (const QVariant &channel, channelsList) { channels.insert(channel.toMap().value("meta").toString(), channel.toMap().value("value")); } emit processDataReceived(messageId, key, channels); } } else if (messageType == "GetParameterChannels") { - Q_FOREACH(QString deviceKey, result.keys()) { + foreach (const QString &deviceKey, result.keys()) { QStringList parameterChannels = result.value(deviceKey).toStringList(); if (!parameterChannels.isEmpty()) emit parameterChannelsReceived(messageId, deviceKey, parameterChannels); } } else if (messageType == "GetParameter"|| messageType == "SetParameter") { - QList devices; QVariantList devicesList = result.value("devices").toList(); - Q_FOREACH(QVariant value, devicesList) { + foreach (const QVariant &value, devicesList) { QString key = value.toMap().value("key").toString(); QVariantList channelsList = value.toMap().value("channels").toList(); QList parameters; - Q_FOREACH(QVariant channel, channelsList) { + foreach (const QVariant &channel, channelsList) { Parameter parameter; parameter.meta = channel.toMap().value("meta").toString(); parameter.name = channel.toMap().value("name").toString(); From cd9c048ebda02fe99524d4a7011be06de04011b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 3 Dec 2021 14:16:21 +0100 Subject: [PATCH 02/17] Add speedwire test interface --- sma/integrationpluginsma.cpp | 17 +++++- sma/integrationpluginsma.h | 2 +- sma/integrationpluginsma.json | 57 ++++++++++++++++- sma/sma.pro | 2 + sma/speedwireinterface.cpp | 111 ++++++++++++++++++++++++++++++++++ sma/speedwireinterface.h | 37 ++++++++++++ 6 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 sma/speedwireinterface.cpp create mode 100644 sma/speedwireinterface.h diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index 6419d7f7..0230ec27 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -84,8 +84,23 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptors(descriptors); info->finish(Thing::ThingErrorNoError); }); - } + } else if (info->thingClassId() == speedwireInverterThingClassId) { + SpeedwireInterface *speedwireDiscovery = new SpeedwireInterface(info); + if (!speedwireDiscovery->initialize()) { + qCWarning(dcSma()) << "Could not discovery inverter. The speedwire interface initialization failed."; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to discover the network.")); + return; + } + connect(speedwireDiscovery, &SpeedwireInterface::discoveryFinished, this, [=](){ + qCDebug(dcSma()) << "Speed wire discovery finished."; + + speedwireDiscovery->deleteLater(); + info->finish(Thing::ThingErrorNoError); + }); + + speedwireDiscovery->startDiscovery(); + } } void IntegrationPluginSma::setupThing(ThingSetupInfo *info) diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index 3c20919f..e2c77fd0 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -35,7 +35,7 @@ #include "plugintimer.h" #include "sunnywebbox.h" -#include +#include "speedwireinterface.h" class IntegrationPluginSma: public IntegrationPlugin { Q_OBJECT diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index 5953b4f4..de0a763d 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -12,8 +12,8 @@ "id": "49304127-ce9b-45dd-8511-05030a4ac003", "name": "sunnyWebBox", "displayName": "Sunny WebBox", - "createMethods": ["user", "discovery"], - "interfaces": ["smartmeterproducer"], + "createMethods": ["discovery", "user"], + "interfaces": ["solarinverter"], "paramTypes": [ { "id": "864d4162-e3ce-48b8-b8ac-c1b971b52d42", @@ -85,6 +85,59 @@ "defaultValue": "None" } ] + }, + { + "id": "0c5097af-e136-4430-9fb4-0ccbb30c3e1c", + "name": "speedwireInverter", + "displayName": "SMA Inverter Speedwire", + "createMethods": ["discovery", "user"], + "interfaces": ["solarinverter"], + "paramTypes": [ + { + "id": "d90193e6-a996-4e49-bf6d-564d596d7e74", + "name": "host", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "192.168.0.168" + }, + { + "id": "2780eab7-1f1c-4cc7-a789-a8790329ca9e", + "name": "macAddress", + "displayName": "hardware address", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + } + ], + "stateTypes": [ + { + "id": "35733d27-4fe0-439a-be71-7c1597481659", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "015868bd-cc35-44cf-b631-78ea7c73b967", + "name": "currentPower", + "displayName": "Current power", + "displayNameEvent": "Current power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "f29b6283-873b-45f5-8a14-622d34f11d4f", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "displayNameEvent": "Total energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] } ] } diff --git a/sma/sma.pro b/sma/sma.pro index 5e124771..6a989d92 100644 --- a/sma/sma.pro +++ b/sma/sma.pro @@ -4,8 +4,10 @@ QT += network SOURCES += \ integrationpluginsma.cpp \ + speedwireinterface.cpp \ sunnywebbox.cpp HEADERS += \ integrationpluginsma.h \ + speedwireinterface.h \ sunnywebbox.h diff --git a/sma/speedwireinterface.cpp b/sma/speedwireinterface.cpp new file mode 100644 index 00000000..a7b42b32 --- /dev/null +++ b/sma/speedwireinterface.cpp @@ -0,0 +1,111 @@ +#include "speedwireinterface.h" +#include "extern-plugininfo.h" + +#include + +SpeedwireInterface::SpeedwireInterface(QObject *parent) : + QObject(parent) +{ + m_socket = new QUdpSocket(this); + connect(m_socket, &QUdpSocket::readyRead, this, &SpeedwireInterface::readPendingDatagrams); + connect(m_socket, &QUdpSocket::stateChanged, this, &SpeedwireInterface::onSocketStateChanged); + connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); +} + +SpeedwireInterface::~SpeedwireInterface() +{ + if (m_initialized) { + if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireInterface: Failed to leave multicast group" << m_multicastAddress.toString(); + } + + m_socket->close(); + } +} + +bool SpeedwireInterface::initialize() +{ + // If we already initialized the socket, we are done + if (m_initialized) + return true; + + m_socket->close(); + m_initialized = false; + + if (!m_socket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress)) { + qCWarning(dcSma()) << "SpeedwireInterface: Cannot bind to port" << m_port << m_socket->errorString(); + return false; + } + + if (!m_socket->joinMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireInterface: Failed to join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); + return false; + } + + m_initialized = true; + return true; +} + +bool SpeedwireInterface::initialized() const +{ + return m_initialized; +} + +bool SpeedwireInterface::startDiscovery() +{ + if (m_discoveryRunning) + return true; + + qCDebug(dcSma()) << "SpeedwireInterface: Start discovering network..."; + if (!m_initialized) { + qCDebug(dcSma()) << "SpeedwireInterface: Failed to start discovery because the socket has not been initialized successfully."; + return false; + } + + // Discovery message + QByteArray discoveryDatagram = QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); + if (m_socket->write(discoveryDatagram) < 0) { + qCWarning(dcSma()) << "SpeedwireInterface: Failed to send discovery datagram to multicast address" << m_multicastAddress.toString(); + return false; + } + + m_discoveryRunning = true; + QTimer::singleShot(10000, this, [=](){ + qCDebug(dcSma()) << "SpeedwireInterface: Discovey finished."; + m_discoveryRunning = false; + emit discoveryFinished(); + }); + + return true; +} + +bool SpeedwireInterface::discoveryRunning() const +{ + return m_discoveryRunning; +} + +void SpeedwireInterface::readPendingDatagrams() +{ + QUdpSocket *socket= qobject_cast(sender()); + + QByteArray datagram; + QHostAddress senderAddress; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + qCDebug(dcSma()) << "SpeedwireInterface: Data received from" << senderAddress.toString() << datagram.toHex(); + //emit datagramReceived(senderAddress, datagram); + } +} + +void SpeedwireInterface::onSocketError(QAbstractSocket::SocketError error) +{ + qCDebug(dcSma()) << "SpeedwireInterface: Socket error" << error; +} + +void SpeedwireInterface::onSocketStateChanged(QAbstractSocket::SocketState socketState) +{ + qCDebug(dcSma()) << "SpeedwireInterface: Socket state changed" << socketState; +} diff --git a/sma/speedwireinterface.h b/sma/speedwireinterface.h new file mode 100644 index 00000000..31df61b9 --- /dev/null +++ b/sma/speedwireinterface.h @@ -0,0 +1,37 @@ +#ifndef SPEEDWIREINTERFACE_H +#define SPEEDWIREINTERFACE_H + +#include +#include + +class SpeedwireInterface : public QObject +{ + Q_OBJECT +public: + explicit SpeedwireInterface(QObject *parent = nullptr); + ~SpeedwireInterface(); + + bool initialize(); + bool initialized() const; + + bool startDiscovery(); + bool discoveryRunning() const; + +signals: + void discoveryFinished(); + +private: + QUdpSocket *m_socket = nullptr; + QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + quint16 m_port = 9522; + bool m_initialized = false; + bool m_discoveryRunning = false; + +private slots: + void readPendingDatagrams(); + void onSocketError(QAbstractSocket::SocketError error); + void onSocketStateChanged(QAbstractSocket::SocketState socketState); + +}; + +#endif // SPEEDWIREINTERFACE_H From a30d338de99dc8667bd04fa579f5ee7441893c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 6 Dec 2021 18:23:24 +0100 Subject: [PATCH 03/17] Implement first meter version --- nymea-plugins.pro | 1 + sma/integrationpluginsma.cpp | 86 +++++++- sma/integrationpluginsma.h | 4 +- sma/integrationpluginsma.json | 184 ++++++++++++++++- sma/obisdata.cpp | 6 + sma/obisdata.h | 11 + sma/sma.pro | 6 + sma/speedwirediscovery.cpp | 365 ++++++++++++++++++++++++++++++++++ sma/speedwirediscovery.h | 75 +++++++ sma/speedwireinterface.cpp | 148 ++++++++------ sma/speedwireinterface.h | 64 +++++- sma/speedwiremeter.cpp | 270 +++++++++++++++++++++++++ sma/speedwiremeter.h | 86 ++++++++ 13 files changed, 1228 insertions(+), 78 deletions(-) create mode 100644 sma/obisdata.cpp create mode 100644 sma/obisdata.h create mode 100644 sma/speedwirediscovery.cpp create mode 100644 sma/speedwirediscovery.h create mode 100644 sma/speedwiremeter.cpp create mode 100644 sma/speedwiremeter.h diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 9265169f..24070d4f 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -118,6 +118,7 @@ isEmpty(WITH_PLUGINS) { } PLUGINS-=$${WITHOUT_PLUGINS} +#FIXME: PLUGINS=sma message("Building plugins:") for(plugin, PLUGINS) { exists($${plugin}) { diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index 0230ec27..cdf92103 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -31,13 +31,19 @@ #include "integrationpluginsma.h" #include "plugininfo.h" -#include "network/networkdevicediscovery.h" +#include +#include "speedwirediscovery.h" IntegrationPluginSma::IntegrationPluginSma() { } +void IntegrationPluginSma::init() +{ + +} + void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == sunnyWebBoxThingClassId) { @@ -84,18 +90,44 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptors(descriptors); info->finish(Thing::ThingErrorNoError); }); - } else if (info->thingClassId() == speedwireInverterThingClassId) { - SpeedwireInterface *speedwireDiscovery = new SpeedwireInterface(info); + } else if (info->thingClassId() == speedwireMeterThingClassId) { + SpeedwireDiscovery *speedwireDiscovery = new SpeedwireDiscovery(hardwareManager()->networkDeviceDiscovery(), info); if (!speedwireDiscovery->initialize()) { qCWarning(dcSma()) << "Could not discovery inverter. The speedwire interface initialization failed."; info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to discover the network.")); return; } - connect(speedwireDiscovery, &SpeedwireInterface::discoveryFinished, this, [=](){ + connect(speedwireDiscovery, &SpeedwireDiscovery::discoveryFinished, this, [=](){ qCDebug(dcSma()) << "Speed wire discovery finished."; - speedwireDiscovery->deleteLater(); + + ThingDescriptors descriptors; + foreach (const SpeedwireDiscovery::SpeedwireDiscoveryResult &result, speedwireDiscovery->discoveryResult()) { + if (result.deviceType == SpeedwireInterface::DeviceTypeMeter) { + if (result.serialNumber == 0) + continue; + + ThingDescriptor descriptor(speedwireMeterThingClassId, "SMA Energy Meter", "Serial: " + QString::number(result.serialNumber) + " - " + result.address.toString()); + // We found an energy meter, let's check if we already added this one + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(speedwireMeterThingSerialNumberParamTypeId).toUInt() == result.serialNumber) { + descriptor.setThingId(existingThing->id()); + break; + } + } + + ParamList params; + params << Param(speedwireMeterThingHostParamTypeId, result.address.toString()); + params << Param(speedwireMeterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(speedwireMeterThingSerialNumberParamTypeId, result.serialNumber); + params << Param(speedwireMeterThingModelIdParamTypeId, result.modelId); + descriptor.setParams(params); + descriptors.append(descriptor); + } + } + + info->addThingDescriptors(descriptors); info->finish(Thing::ThingErrorNoError); }); @@ -145,7 +177,49 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSma::onRefreshTimer); } }); - } else { + } else if (thing->thingClassId() == speedwireMeterThingClassId) { + QHostAddress address = QHostAddress(thing->paramValue(speedwireMeterThingHostParamTypeId).toString()); + quint32 serialNumber = static_cast(thing->paramValue(speedwireMeterThingSerialNumberParamTypeId).toUInt()); + quint16 modelId = static_cast(thing->paramValue(speedwireMeterThingModelIdParamTypeId).toUInt()); + + if (m_speedwireMeters.contains(thing)) { + m_speedwireMeters.take(thing)->deleteLater(); + } + + SpeedwireMeter *meter = new SpeedwireMeter(address, modelId, serialNumber, this); + if (!meter->initialize()) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // TODO: reachable state + + connect(meter, &SpeedwireMeter::valuesUpdated, this, [=](){ + thing->setStateValue(speedwireMeterConnectedStateTypeId, true); + thing->setStateValue(speedwireMeterCurrentPowerStateTypeId, meter->currentPower()); + thing->setStateValue(speedwireMeterCurrentPowerPhaseAStateTypeId, meter->currentPowerPhaseA()); + thing->setStateValue(speedwireMeterCurrentPowerPhaseBStateTypeId, meter->currentPowerPhaseB()); + thing->setStateValue(speedwireMeterCurrentPowerPhaseCStateTypeId, meter->currentPowerPhaseC()); + thing->setStateValue(speedwireMeterVoltagePhaseAStateTypeId, meter->voltagePhaseA()); + thing->setStateValue(speedwireMeterVoltagePhaseBStateTypeId, meter->voltagePhaseB()); + thing->setStateValue(speedwireMeterVoltagePhaseCStateTypeId, meter->voltagePhaseC()); + thing->setStateValue(speedwireMeterTotalEnergyConsumedStateTypeId, meter->totalEnergyConsumed()); + thing->setStateValue(speedwireMeterTotalEnergyProducedStateTypeId, meter->totalEnergyProduced()); + thing->setStateValue(speedwireMeterEnergyConsumedPhaseAStateTypeId, meter->energyConsumedPhaseA()); + thing->setStateValue(speedwireMeterEnergyConsumedPhaseBStateTypeId, meter->energyConsumedPhaseB()); + thing->setStateValue(speedwireMeterEnergyConsumedPhaseCStateTypeId, meter->energyConsumedPhaseC()); + thing->setStateValue(speedwireMeterEnergyProducedPhaseAStateTypeId, meter->energyProducedPhaseA()); + thing->setStateValue(speedwireMeterEnergyProducedPhaseBStateTypeId, meter->energyProducedPhaseB()); + thing->setStateValue(speedwireMeterEnergyProducedPhaseCStateTypeId, meter->energyProducedPhaseC()); + thing->setStateValue(speedwireMeterCurrentPhaseAStateTypeId, meter->amperePhaseA()); + thing->setStateValue(speedwireMeterCurrentPhaseBStateTypeId, meter->amperePhaseB()); + thing->setStateValue(speedwireMeterCurrentPhaseCStateTypeId, meter->amperePhaseC()); + thing->setStateValue(speedwireMeterFirmwareVersionStateTypeId, meter->softwareVersion()); + }); + + m_speedwireMeters.insert(thing, meter); + info->finish(Thing::ThingErrorNoError); + }else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index e2c77fd0..516a5e1b 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -35,7 +35,7 @@ #include "plugintimer.h" #include "sunnywebbox.h" -#include "speedwireinterface.h" +#include "speedwiremeter.h" class IntegrationPluginSma: public IntegrationPlugin { Q_OBJECT @@ -45,6 +45,7 @@ class IntegrationPluginSma: public IntegrationPlugin { public: explicit IntegrationPluginSma(); + void init() override; void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; @@ -59,6 +60,7 @@ private slots: private: PluginTimer *m_refreshTimer = nullptr; QHash m_sunnyWebBoxes; + QHash m_speedwireMeters; }; #endif // INTEGRATIONPLUGINSMA_H diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index de0a763d..b751c93d 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -26,7 +26,7 @@ { "id": "03f32361-4e13-4597-a346-af8d16a986b3", "name": "macAddress", - "displayName": "hardware address", + "displayName": "MAC address", "type": "QString", "inputType": "TextLine", "readOnly": true @@ -88,10 +88,10 @@ }, { "id": "0c5097af-e136-4430-9fb4-0ccbb30c3e1c", - "name": "speedwireInverter", - "displayName": "SMA Inverter Speedwire", + "name": "speedwireMeter", + "displayName": "SMA Energy Meter", "createMethods": ["discovery", "user"], - "interfaces": ["solarinverter"], + "interfaces": [ "energymeter" ], "paramTypes": [ { "id": "d90193e6-a996-4e49-bf6d-564d596d7e74", @@ -104,10 +104,26 @@ { "id": "2780eab7-1f1c-4cc7-a789-a8790329ca9e", "name": "macAddress", - "displayName": "hardware address", + "displayName": "MAC address", "type": "QString", "inputType": "TextLine", "readOnly": true + }, + { + "id": "7c81a0c5-9bc6-43bb-a01a-4de5fe656bba", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + }, + { + "id": "abdc114d-1fac-4454-8b82-871ed5cdf28c", + "name": "modelId", + "displayName": "Model ID", + "type": "uint", + "inputType": "TextLine", + "readOnly": true } ], "stateTypes": [ @@ -120,7 +136,61 @@ "defaultValue": false }, { - "id": "015868bd-cc35-44cf-b631-78ea7c73b967", + "id": "44ee2491-8376-41cd-a21d-185c736152ec", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "56ae3555-f874-4c2d-8833-17573dce477a", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "51cbb29b-29f0-480a-9d7d-b8f4e6a205ae", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "45bbdbef-1832-4870-bff5-299e580fb4da", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "b3a4fdd2-b6b8-4c58-9da3-2084ad414022", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "b3655188-3854-4336-ae3c-61d3bda6fc4d", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "d4ac7f37-e30a-44e4-93cb-ad16df18b8f1", "name": "currentPower", "displayName": "Current power", "displayNameEvent": "Current power changed", @@ -129,13 +199,111 @@ "defaultValue": 0 }, { - "id": "f29b6283-873b-45f5-8a14-622d34f11d4f", + "id": "c5d09c63-7461-4fb8-a6fe-bc7aa919be30", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "c52d4422-b521-4804-a7a7-c4398e91e760", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "555e892c-3ca7-4100-9832-6ac13b87eb04", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "displayNameEvent": "Current power phase C changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumed changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "76ca68d8-6781-4d2a-8663-440aec40b4de", "name": "totalEnergyProduced", "displayName": "Total energy produced", "displayNameEvent": "Total energy produced changed", "type": "double", "unit": "KiloWattHour", - "defaultValue": 0 + "defaultValue": 0.00 + }, + { + "id": "b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33", + "name": "energyConsumedPhaseA", + "displayName": "Energy consumed phase A", + "displayNameEvent": "Energy consumed phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "c4e5f569-ac5d-4761-a898-888880bfd59f", + "name": "energyConsumedPhaseB", + "displayName": "Energy consumed phase B", + "displayNameEvent": "Energy consumed phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3", + "name": "energyConsumedPhaseC", + "displayName": "Energy consumed phase C", + "displayNameEvent": "Energy consumed phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "754c3b67-768a-47f7-99d8-f66c198f0835", + "name": "energyProducedPhaseA", + "displayName": "Energy produced phase A", + "displayNameEvent": "Energy produced phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "7eb08c45-24cf-40ce-be28-f3564f087672", + "name": "energyProducedPhaseB", + "displayName": "Energy produced phase B", + "displayNameEvent": "Energy produced phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "1eb2bf01-5ec6-42e5-b348-ac1e95199d14", + "name": "energyProducedPhaseC", + "displayName": "Energy produced phase C", + "displayNameEvent": "Energy produced phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "a685393c-8b7e-42c5-bb41-f9907c074626", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "type": "QString", + "defaultValue": "" } ] } diff --git a/sma/obisdata.cpp b/sma/obisdata.cpp new file mode 100644 index 00000000..5866185f --- /dev/null +++ b/sma/obisdata.cpp @@ -0,0 +1,6 @@ +#include "obisdata.h" + +ObisData::ObisData() +{ + +} diff --git a/sma/obisdata.h b/sma/obisdata.h new file mode 100644 index 00000000..0ebe468e --- /dev/null +++ b/sma/obisdata.h @@ -0,0 +1,11 @@ +#ifndef OBISDATA_H +#define OBISDATA_H + + +class ObisData +{ +public: + ObisData(); +}; + +#endif // OBISDATA_H diff --git a/sma/sma.pro b/sma/sma.pro index 6a989d92..313dbae1 100644 --- a/sma/sma.pro +++ b/sma/sma.pro @@ -4,10 +4,16 @@ QT += network SOURCES += \ integrationpluginsma.cpp \ + obisdata.cpp \ + speedwirediscovery.cpp \ speedwireinterface.cpp \ + speedwiremeter.cpp \ sunnywebbox.cpp HEADERS += \ integrationpluginsma.h \ + obisdata.h \ + speedwirediscovery.h \ speedwireinterface.h \ + speedwiremeter.h \ sunnywebbox.h diff --git a/sma/speedwirediscovery.cpp b/sma/speedwirediscovery.cpp new file mode 100644 index 00000000..be92340a --- /dev/null +++ b/sma/speedwirediscovery.cpp @@ -0,0 +1,365 @@ +#include "speedwirediscovery.h" +#include "extern-plugininfo.h" + +#include +#include + +SpeedwireDiscovery::SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject(parent), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ + // More details: https://github.com/RalfOGit/libspeedwire/ + + + // //! Multicast device discovery request packet, according to SMA documentation. + // //! However, this does not seem to be supported anymore with version 3.x devices + // const unsigned char multicast_request[] = { + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, // 0xffffffff group, 0x0000 length, 0x0020 "SMA Net ?", Version ? + // 0x00, 0x00, 0x00, 0x00 // 0x0000 protocol, 0x00 #long words, 0x00 ctrl + // }; + + // //! Unicast device discovery request packet, according to SMA documentation + // const unsigned char unicast_request[] = { + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x10, // 0x26 length, 0x0010 "SMA Net 2", Version 0 + // 0x60, 0x65, 0x09, 0xa0, 0xff, 0xff, 0xff, 0xff, // 0x6065 protocol, 0x09 #long words, 0xa0 ctrl, 0xffff dst susyID any, 0xffffffff dst serial any + // 0xff, 0xff, 0x00, 0x00, 0x7d, 0x00, 0x52, 0xbe, // 0x0000 dst cntrl, 0x007d src susy id, 0x3a28be52 src serial + // 0x28, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x0000 src cntrl, 0x0000 error code, 0x0000 fragment ID + // 0x01, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // 0x8001 packet ID + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00 + // }; + + // // Request: 534d4100000402a00000000100260010 606509a0 ffffffffffff0000 7d0052be283a0000 000000000180 00020000 00000000 00000000 00000000 => command = 0x00000200, first = 0x00000000; last = 0x00000000; trailer = 0x00000000 + // // Response 534d4100000402a000000001004e0010 606513a0 7d0052be283a00c0 7a01842a71b30000 000000000180 01020000 00000000 00000000 00030000 00ff0000 00000000 01007a01 842a71b3 00000a00 0c000000 00000000 00000000 01010000 00000000 + + + + + + + + + // qCDebug(dcSma()) << "SpeedwireDiscovery: Create speed wire interface for multicast" << m_multicastAddress.toString() << "on port" << m_port; + // QByteArray exampleData = QByteArray::fromHex("534d4100000402a000000001024400106069010e714369aee618a41600010400000000000001080000000021391229100002040000004415000208000000001575a137d800030400000000000003080000000003debed0e800040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e900000000102085200000000"); + // processDatagram(QHostAddress("127.0.0.1"), m_port, exampleData); + + m_multicastSocket = new QUdpSocket(this); + connect(m_multicastSocket, &QUdpSocket::readyRead, this, &SpeedwireDiscovery::readPendingDatagramsMulticast); + connect(m_multicastSocket, &QUdpSocket::stateChanged, this, &SpeedwireDiscovery::onSocketStateChanged); + connect(m_multicastSocket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); + + m_unicastSocket = new QUdpSocket(this); + connect(m_unicastSocket, &QUdpSocket::readyRead, this, &SpeedwireDiscovery::readPendingDatagramsUnicast); + connect(m_unicastSocket, &QUdpSocket::stateChanged, this, &SpeedwireDiscovery::onSocketStateChanged); + connect(m_unicastSocket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); + + m_discoveryTimer.setInterval(1000); + m_discoveryTimer.setSingleShot(false); + connect(&m_discoveryTimer, &QTimer::timeout, this, &SpeedwireDiscovery::sendDiscoveryRequest); +} + +SpeedwireDiscovery::~SpeedwireDiscovery() +{ + if (m_initialized) { + if (!m_multicastSocket->leaveMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to leave multicast group" << m_multicastAddress.toString(); + } + + m_multicastSocket->close(); + } +} + +bool SpeedwireDiscovery::initialize() +{ + m_multicastSocket->close(); + m_initialized = false; + + // Setup multicast socket + if (!m_multicastSocket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not bind multicast socket to port" << m_port << m_multicastSocket->errorString(); + return false; + } + + if (!m_multicastSocket->joinMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not join multicast group" << m_multicastAddress.toString() << m_multicastSocket->errorString(); + return false; + } + + // Setup unicast socket + if (!m_unicastSocket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not bind to port" << m_port << m_multicastSocket->errorString(); + return false; + } + + qCDebug(dcSma()) << "SpeedwireDiscovery: Interface initialized successfully."; + m_initialized = true; + return m_initialized; +} + +bool SpeedwireDiscovery::initialized() const +{ + return m_initialized; +} + +bool SpeedwireDiscovery::startDiscovery() +{ + // 1. Discover all network devices + // 2. Send upd multicast and unicast messages to verify if it is a SMA speedwire device + + if (m_discoveryRunning) + return true; + + if (!m_initialized) { + qCDebug(dcSma()) << "SpeedwireDiscovery: Failed to start discovery because the socket has not been initialized successfully."; + return false; + } + + // CLean up + m_results.clear(); + m_networkDeviceInfos.clear(); + + qCDebug(dcSma()) << "SpeedwireDiscovery: Start discovering network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcSma()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + // 2. Send unicast to all results and start requesting on multicast address + sendUnicastDiscoveryRequest(networkDeviceInfo.address()); + } + + startMulticastDiscovery(); + }); + + return true; +} + +bool SpeedwireDiscovery::discoveryRunning() const +{ + return m_discoveryRunning; +} + +QList SpeedwireDiscovery::discoveryResult() const +{ + return m_results.values(); +} + +void SpeedwireDiscovery::startMulticastDiscovery() +{ + // Start sending multicast messages + sendDiscoveryRequest(); + + m_discoveryRunning = true; + QTimer::singleShot(5000, this, &SpeedwireDiscovery::onDiscoveryProcessFinished); + + m_discoveryTimer.start(); +} + +void SpeedwireDiscovery::sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress) +{ + if (m_unicastSocket->writeDatagram(m_discoveryDatagramUnicast, targetHostAddress, m_port) < 0) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to send unicast discovery datagram to address" << targetHostAddress.toString(); + return; + } + + qCDebug(dcSma()) << "SpeedwireDiscovery: Sent successfully the discovery request to unicast address" << targetHostAddress.toString(); +} + +void SpeedwireDiscovery::readPendingDatagramsMulticast() +{ + QUdpSocket *socket = qobject_cast(sender()); + + QByteArray datagram; + QHostAddress senderAddress; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + qCDebug(dcSma()) << "SpeedwireDiscovery: Received multicast data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + //qCDebug(dcSma()) << "SpeedwireDiscovery: " << datagram.toHex(); + processDatagram(senderAddress, senderPort, datagram); + } +} + +void SpeedwireDiscovery::readPendingDatagramsUnicast() +{ + QUdpSocket *socket = qobject_cast(sender()); + + QByteArray datagram; + QHostAddress senderAddress; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + qCDebug(dcSma()) << "SpeedwireDiscovery: Received unicast data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + //qCDebug(dcSma()) << "SpeedwireDiscovery: " << datagram.toHex(); + processDatagram(senderAddress, senderPort, datagram); + } +} + + +void SpeedwireDiscovery::onSocketError(QAbstractSocket::SocketError error) +{ + qCDebug(dcSma()) << "SpeedwireDiscovery: Socket error" << error; +} + +void SpeedwireDiscovery::onSocketStateChanged(QAbstractSocket::SocketState socketState) +{ + qCDebug(dcSma()) << "SpeedwireDiscovery: Socket state changed" << socketState; +} + +void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quint16 senderPort, const QByteArray &datagram) +{ + // Check min size of SMA datagrams + if (datagram.size() < 18) { + qCDebug(dcSma()) << "SpeedwireDiscovery: Received datagram is to short to be a SMA speedwire message. Ignoring data..."; + return; + } + + // Ignore discovery requests + if (datagram == m_discoveryDatagramMulticast || datagram == m_discoveryDatagramUnicast) + return; + + QDataStream stream(datagram); + stream.setByteOrder(QDataStream::BigEndian); + + SpeedwireInterface::SpeedwireHeader header = SpeedwireInterface::parseHeader(stream); + if (!header.isValid()) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Datagram header is not valid. Ignoring data..."; + return; + } + // Example data: + // 534d4100 0004 02a0 0000 0001 0244 0010 6069 010e 7143 69ae e618a416 00010400000000000001080000000021391229100002040000004415000208000000001575a137d800030400000000000003080000000003debed0e800040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e900000000102085200000000 + + + + qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Header"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Length:" << header.headerLength; + qCDebug(dcSma()) << "SpeedwireDiscovery: Tag0:" << header.tagType; + qCDebug(dcSma()) << "SpeedwireDiscovery: Tag0 version:" << header.tagVersion; + qCDebug(dcSma()) << "SpeedwireDiscovery: Group:" << header.group << (header.group == 1 ? "(default group)" : ""); + qCDebug(dcSma()) << "SpeedwireDiscovery: Data length:" << header.payloadLength << datagram.length(); + qCDebug(dcSma()) << "SpeedwireDiscovery: SMA Net 2 Version" << header.smaNet2Version; + qCDebug(dcSma()) << "SpeedwireDiscovery: Protocol ID" << header.protocolId; + + if (header.protocolId == SpeedwireInterface::ProtocolIdDiscoveryResponse) { + qCDebug(dcSma()) << "SpeedwireDiscovery: Received discovery response from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + // Response: 534d4100 0004 02a0 0000 0001 0002 0000 0001 + // "192.168.178.25:9522" "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0003 0004 0020 0000 0001 0004 0030 c0a8 b219 0004 0040 0000 0000 0002 0070 ef0c 00000000" + // "192.168.178.22:9522" "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0001 0004 0020 0000 0001 0004 0030 c0a8 b216 0004 0040 0000 0001 00000000" + + if (!datagram.startsWith(m_discoveryResponseDatagram)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Received discovery reply but the message start does not match the required schema. Ignoring data..."; + return; + } + + if (!m_results.contains(senderAddress)) { + qCDebug(dcSma()) << "SpeedwireDiscovery: --> Found SMA device on" << senderAddress.toString(); + if (!m_networkDeviceInfos.hasHostAddress(senderAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Found SMA using UDP discovery but the host is not in the network discovery result list. Not adding to results" << senderAddress.toString(); + return; + } + + + SpeedwireDiscoveryResult result; + result.address = senderAddress; + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + result.networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + result.deviceType = SpeedwireInterface::DeviceTypeUnknown; + m_results.insert(senderAddress, result); + } else { + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + } + return; + } + + // We received SMA data, let's parse depending on the protocol id + + if (header.protocolId == SpeedwireInterface::ProtocolIdMeter) { + // Example: 010e 714369ae + quint16 modelId; + quint32 serialNumber; + stream >> modelId >> serialNumber; + qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Meter identifier"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << modelId; + qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << serialNumber; + + if (!m_results.contains(senderAddress)) { + SpeedwireDiscoveryResult result; + result.address = senderAddress; + result.deviceType = SpeedwireInterface::DeviceTypeMeter; + m_results.insert(senderAddress, result); + } + + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + + m_results[senderAddress].modelId = modelId; + m_results[senderAddress].serialNumber = serialNumber; + } else if (header.protocolId == SpeedwireInterface::ProtocolIdInverter) { + quint16 modelId; + quint32 serialNumber; + stream >> modelId >> serialNumber; + qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Inverter identifier"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << modelId; + qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << serialNumber; + + if (!m_results.contains(senderAddress)) { + SpeedwireDiscoveryResult result; + result.address = senderAddress; + result.deviceType = SpeedwireInterface::DeviceTypeInverter; + m_results.insert(senderAddress, result); + } + + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + + m_results[senderAddress].modelId = modelId; + m_results[senderAddress].serialNumber = serialNumber; + } else { + qCWarning(dcSma()) << "SpeedwireDiscovery: Unhandled data received" << datagram.toHex(); + return; + } + +} + +void SpeedwireDiscovery::sendDiscoveryRequest() +{ + if (m_multicastSocket->writeDatagram(m_discoveryDatagramMulticast, m_multicastAddress, m_port) < 0) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to send discovery datagram to multicast address" << m_multicastAddress.toString(); + return; + } + + qCDebug(dcSma()) << "SpeedwireDiscovery: Sent successfully the discovery request to multicast address" << m_multicastAddress.toString(); +} + +void SpeedwireDiscovery::onDiscoveryProcessFinished() +{ + qCDebug(dcSma()) << "SpeedwireDiscovery: Discovey finished. Found" << m_results.count() << "SMA devices in the network"; + m_discoveryTimer.stop(); + m_discoveryRunning = false; + + foreach (const SpeedwireDiscoveryResult &result, m_results) { + qCDebug(dcSma()) << "SpeedwireDiscovery: ============================================"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Device type:" << result.deviceType; + qCDebug(dcSma()) << "SpeedwireDiscovery: Address:" << result.address.toString(); + qCDebug(dcSma()) << "SpeedwireDiscovery: Hostname:" << result.networkDeviceInfo.hostName(); + qCDebug(dcSma()) << "SpeedwireDiscovery: MAC:" << result.networkDeviceInfo.macAddress(); + qCDebug(dcSma()) << "SpeedwireDiscovery: MAC manufacturer:" << result.networkDeviceInfo.macAddressManufacturer(); + qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << result.modelId; + qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << result.serialNumber; + } + + + emit discoveryFinished(); +} diff --git a/sma/speedwirediscovery.h b/sma/speedwirediscovery.h new file mode 100644 index 00000000..c1da61e2 --- /dev/null +++ b/sma/speedwirediscovery.h @@ -0,0 +1,75 @@ +#ifndef SPEEDWIREDISCOVERY_H +#define SPEEDWIREDISCOVERY_H + +#include +#include +#include + +#include + +#include "speedwireinterface.h" + +class SpeedwireDiscovery : public QObject +{ + Q_OBJECT +public: + typedef struct SpeedwireDiscoveryResult { + QHostAddress address; + NetworkDeviceInfo networkDeviceInfo; + SpeedwireInterface::DeviceType deviceType = SpeedwireInterface::DeviceTypeUnknown; + quint16 modelId = 0; + quint32 serialNumber = 0; + } SpeedwireDiscoveryResult; + + explicit SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + ~SpeedwireDiscovery(); + + bool initialize(); + bool initialized() const; + + bool startDiscovery(); + bool discoveryRunning() const; + + QList discoveryResult() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QUdpSocket *m_multicastSocket = nullptr; + QUdpSocket *m_unicastSocket = nullptr; + QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + quint16 m_port = 9522; + bool m_initialized = false; + + // Discovery + QTimer m_discoveryTimer; + bool m_discoveryRunning = false; + NetworkDeviceInfos m_networkDeviceInfos; + QHash m_results; + + // Static discovery datagrams for speedwire + QByteArray m_discoveryDatagramMulticast = QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); + QByteArray m_discoveryResponseDatagram = QByteArray::fromHex("534d4100000402A000000001000200000001"); + + QByteArray m_discoveryDatagramUnicast = QByteArray::fromHex("534d4100000402a00000000100260010606509a0ffffffffffff00007d0052be283a000000000000018000020000000000000000000000000000"); + + void startMulticastDiscovery(); + void sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress); + +private slots: + void readPendingDatagramsMulticast(); + void readPendingDatagramsUnicast(); + void onSocketError(QAbstractSocket::SocketError error); + void onSocketStateChanged(QAbstractSocket::SocketState socketState); + + void processDatagram(const QHostAddress &senderAddress, quint16 senderPort, const QByteArray &datagram); + + void sendDiscoveryRequest(); + + void onDiscoveryProcessFinished(); + +}; + +#endif // SPEEDWIREDISCOVERY_H diff --git a/sma/speedwireinterface.cpp b/sma/speedwireinterface.cpp index a7b42b32..e30db2f8 100644 --- a/sma/speedwireinterface.cpp +++ b/sma/speedwireinterface.cpp @@ -1,49 +1,85 @@ #include "speedwireinterface.h" #include "extern-plugininfo.h" -#include - -SpeedwireInterface::SpeedwireInterface(QObject *parent) : - QObject(parent) +SpeedwireInterface::SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent) : + QObject(parent), + m_address(address), + m_multicast(multicast) { m_socket = new QUdpSocket(this); connect(m_socket, &QUdpSocket::readyRead, this, &SpeedwireInterface::readPendingDatagrams); connect(m_socket, &QUdpSocket::stateChanged, this, &SpeedwireInterface::onSocketStateChanged); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); + + + + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00020058 001e8200 ff208200 00000000 => query device type + // Response 534d4100000402a000000001009e0010 606527a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01020058 01000000 03000000 011e8210 6f89e95f 534e3a20 33303130 35333831 31360000 00000000 00000000 00000000 00000000 + // 011f8208 6f89e95f 411f0001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 => 1f41 solar inverter + // 01208208 6f89e95f 96240000 80240000 81240001 82240000 feffff00 00000000 00000000 00000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00028053 001e2500 ff1e2500 00000000 => query spot dc power + // Response 534d4100000402a000000001005e0010 606517a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01028053 00000000 01000000 011e2540 61a7e95f 57000000 57000000 57000000 57000000 01000000 + // 021e2540 61a7e95f 5e000000 5e000000 5e000000 5e000000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000580 00028053 001f4500 ff214500 00000000 => query spot dc voltage/current + // Response 534d4100000402a00000000100960010 606525a0 7d0042be283a00a1 7a01842a71b30001 000000000580 01028053 02000000 05000000 011f4540 61a7e95f 05610000 05610000 05610000 05610000 01000000 + // 021f4540 61a7e95f 505b0000 505b0000 505b0000 505b0000 01000000 + // 01214540 61a7e95f 60010000 60010000 60010000 60010000 01000000 + // 02214540 61a7e95f 95010000 95010000 95010000 95010000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000680 00020051 00404600 ff424600 00000000 => query spot ac power + // Response 534d4100000402a000000001007a0010 60651ea0 7d0042be283a00a1 7a01842a71b30001 000000000680 01020051 09000000 0b000000 01404640 61a7e95f 38000000 38000000 38000000 38000000 01000000 + // 01414640 61a7e95f 37000000 37000000 37000000 37000000 01000000 + // 01424640 61a7e95f 39000000 39000000 39000000 39000000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000780 00020051 00484600 ff554600 00000000 => query spot ac voltage/current + // Response 534d4100000402a000000001013e0010 60654fa0 7d0042be283a00a1 7a01842a71b30001 000000000780 01020051 0c000000 15000000 01484600 61a7e95f 5a590000 5a590000 5a590000 5a590000 01000000 + // 01494600 61a7e95f cf590000 cf590000 cf590000 cf590000 01000000 + // 014a4600 61a7e95f 7a590000 7a590000 7a590000 7a590000 01000000 + // 014b4600 61a7e95f f19a0000 f19a0000 f19a0000 f19a0000 01000000 + // 014c4600 61a7e95f 3c9b0000 3c9b0000 3c9b0000 3c9b0000 01000000 + // 014d4600 61a7e95f 189b0000 189b0000 189b0000 189b0000 01000000 + // 014e4600 51a7e95f 1d000000 1d000000 1d000000 1d000000 01000000 + // 01534640 61a7e95f 24010000 24010000 24010000 24010000 01000000 + // 01544640 61a7e95f 1e010000 1e010000 1e010000 1e010000 01000000 + // 01554640 61a7e95f 23010000 23010000 23010000 23010000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000980 00028051 00482100 ff482100 00000000 => query device status + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000980 01028051 00000000 00000000 01482108 59c5e95f 33010001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000a80 00028051 00644100 ff644100 00000000 => query grid relay status + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000a80 01028051 07000000 07000000 01644108 59c5e95f 33000001 37010000 fdffff00 feffff00 00000000 00000000 00000000 00000000 00000000 } SpeedwireInterface::~SpeedwireInterface() { - if (m_initialized) { - if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { - qCWarning(dcSma()) << "SpeedwireInterface: Failed to leave multicast group" << m_multicastAddress.toString(); - } - - m_socket->close(); - } + deinitialize(); } bool SpeedwireInterface::initialize() { - // If we already initialized the socket, we are done - if (m_initialized) - return true; - - m_socket->close(); - m_initialized = false; - - if (!m_socket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress)) { - qCWarning(dcSma()) << "SpeedwireInterface: Cannot bind to port" << m_port << m_socket->errorString(); + if (!m_socket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcSma()) << "SpeedwireInterface: Initialization failed. Could not bind to port" << m_port; return false; } - if (!m_socket->joinMulticastGroup(m_multicastAddress)) { - qCWarning(dcSma()) << "SpeedwireInterface: Failed to join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); + if (m_multicast && !m_socket->joinMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); return false; } + qCDebug(dcSma()) << "SpeedwireInterface: Interface initialized successfully."; m_initialized = true; - return true; + return m_initialized; +} + +void SpeedwireInterface::deinitialize() +{ + if (m_initialized) { + if (m_multicast) { + if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to leave multicast group" << m_multicastAddress.toString(); + } + + m_socket->close(); + m_initialized = false; + } + } } bool SpeedwireInterface::initialized() const @@ -51,52 +87,50 @@ bool SpeedwireInterface::initialized() const return m_initialized; } -bool SpeedwireInterface::startDiscovery() +quint16 SpeedwireInterface::sourceModelId() const { - if (m_discoveryRunning) - return true; - - qCDebug(dcSma()) << "SpeedwireInterface: Start discovering network..."; - if (!m_initialized) { - qCDebug(dcSma()) << "SpeedwireInterface: Failed to start discovery because the socket has not been initialized successfully."; - return false; - } - - // Discovery message - QByteArray discoveryDatagram = QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); - if (m_socket->write(discoveryDatagram) < 0) { - qCWarning(dcSma()) << "SpeedwireInterface: Failed to send discovery datagram to multicast address" << m_multicastAddress.toString(); - return false; - } - - m_discoveryRunning = true; - QTimer::singleShot(10000, this, [=](){ - qCDebug(dcSma()) << "SpeedwireInterface: Discovey finished."; - m_discoveryRunning = false; - emit discoveryFinished(); - }); - - return true; + return m_sourceModelId; } -bool SpeedwireInterface::discoveryRunning() const +quint32 SpeedwireInterface::sourceSerialNumber() const { - return m_discoveryRunning; + return m_sourceSerialNumber; +} + +SpeedwireInterface::SpeedwireHeader SpeedwireInterface::parseHeader(QDataStream &stream) +{ + SpeedwireHeader header; + quint16 protocolId; + stream >> header.smaSignature >> header.headerLength; + stream >> header.tagType >> header.tagVersion >> header.group; + stream >> header.payloadLength >> header.smaNet2Version; + stream >> protocolId; + header.protocolId = static_cast(protocolId); + return header; +} + +void SpeedwireInterface::sendData(const QByteArray &data) +{ + m_socket->writeDatagram(data, m_address, m_port); } void SpeedwireInterface::readPendingDatagrams() { - QUdpSocket *socket= qobject_cast(sender()); - QByteArray datagram; QHostAddress senderAddress; quint16 senderPort; - while (socket->hasPendingDatagrams()) { - datagram.resize(socket->pendingDatagramSize()); - socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); - qCDebug(dcSma()) << "SpeedwireInterface: Data received from" << senderAddress.toString() << datagram.toHex(); - //emit datagramReceived(senderAddress, datagram); + while (m_socket->hasPendingDatagrams()) { + datagram.resize(m_socket->pendingDatagramSize()); + m_socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + + // Process only data coming from our target address + if (senderAddress != m_address) + continue; + + qCDebug(dcSma()) << "SpeedwireInterface: Received data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + qCDebug(dcSma()) << "SpeedwireInterface: " << datagram.toHex(); + emit dataReceived(datagram); } } diff --git a/sma/speedwireinterface.h b/sma/speedwireinterface.h index 31df61b9..e619a803 100644 --- a/sma/speedwireinterface.h +++ b/sma/speedwireinterface.h @@ -3,29 +3,81 @@ #include #include +#include + + + class SpeedwireInterface : public QObject { Q_OBJECT public: - explicit SpeedwireInterface(QObject *parent = nullptr); + enum ProtocolId { + ProtocolIdUnknown = 0x0000, + ProtocolIdMeter = 0x6069, + ProtocolIdInverter = 0x6065, + ProtocolIdDiscoveryResponse = 0x0001, + ProtocolIdDiscovery = 0xffff + }; + Q_ENUM(ProtocolId) + + enum DeviceType { + DeviceTypeUnknown, + DeviceTypeMeter, + DeviceTypeInverter + }; + Q_ENUM(DeviceType) + + class SpeedwireHeader + { + public: + SpeedwireHeader() = default; + quint32 smaSignature = 0; + quint16 headerLength = 0; + quint16 tagType = 0; + quint16 tagVersion = 0; + quint16 group = 0; + quint16 payloadLength = 0; + quint16 smaNet2Version = 0; + ProtocolId protocolId = ProtocolIdUnknown; + + inline bool isValid() const { + return smaSignature == 0x534d4100 && protocolId != ProtocolIdUnknown; + } + + }; + + + explicit SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent = nullptr); ~SpeedwireInterface(); bool initialize(); + void deinitialize(); + bool initialized() const; - bool startDiscovery(); - bool discoveryRunning() const; + quint16 sourceModelId() const; + quint32 sourceSerialNumber() const; + + static SpeedwireInterface::SpeedwireHeader parseHeader(QDataStream &stream); + +public slots: + void sendData(const QByteArray &data); signals: - void discoveryFinished(); + void dataReceived(const QByteArray &data); private: QUdpSocket *m_socket = nullptr; - QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + QHostAddress m_address; quint16 m_port = 9522; + QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + bool m_multicast = false; bool m_initialized = false; - bool m_discoveryRunning = false; + + // Requester + quint16 m_sourceModelId = 0x007d; + quint32 m_sourceSerialNumber = 0x3a28be42; private slots: void readPendingDatagrams(); diff --git a/sma/speedwiremeter.cpp b/sma/speedwiremeter.cpp new file mode 100644 index 00000000..330ec6f7 --- /dev/null +++ b/sma/speedwiremeter.cpp @@ -0,0 +1,270 @@ +#include "speedwiremeter.h" +#include "extern-plugininfo.h" + +SpeedwireMeter::SpeedwireMeter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent) : + QObject(parent), + m_address(address), + m_modelId(modelId), + m_serialNumber(serialNumber) +{ + m_interface = new SpeedwireInterface(m_address, true, this); + connect(m_interface, &SpeedwireInterface::dataReceived, this, &SpeedwireMeter::processData); +} + +bool SpeedwireMeter::initialize() +{ + return m_interface->initialize(); +} + +bool SpeedwireMeter::initialized() const +{ + return m_interface->initialized(); +} + +double SpeedwireMeter::currentPower() const +{ + return m_currentPower; +} + +double SpeedwireMeter::totalEnergyProduced() const +{ + return m_totalEnergyProduced; +} + +double SpeedwireMeter::totalEnergyConsumed() const +{ + return m_totalEnergyConsumed; +} + +double SpeedwireMeter::energyConsumedPhaseA() const +{ + return m_energyConsumedPhaseA; +} + +double SpeedwireMeter::energyConsumedPhaseB() const +{ + return m_energyConsumedPhaseB; +} + +double SpeedwireMeter::energyConsumedPhaseC() const +{ + return m_energyConsumedPhaseC; +} + +double SpeedwireMeter::energyProducedPhaseA() const +{ + return m_energyProducedPhaseA; +} + +double SpeedwireMeter::energyProducedPhaseB() const +{ + return m_energyProducedPhaseB; +} + +double SpeedwireMeter::energyProducedPhaseC() const +{ + return m_energyProducedPhaseC; +} + +double SpeedwireMeter::currentPowerPhaseA() const +{ + return m_currentPowerPhaseA; +} + +double SpeedwireMeter::currentPowerPhaseB() const +{ + return m_currentPowerPhaseB; +} + +double SpeedwireMeter::currentPowerPhaseC() const +{ + return m_currentPowerPhaseC; +} + +double SpeedwireMeter::voltagePhaseA() const +{ + return m_voltagePhaseA; +} + +double SpeedwireMeter::voltagePhaseB() const +{ + return m_voltagePhaseB; +} + +double SpeedwireMeter::voltagePhaseC() const +{ + return m_voltagePhaseC; +} + +double SpeedwireMeter::amperePhaseA() const +{ + return m_amperePhaseA; +} + +double SpeedwireMeter::amperePhaseB() const +{ + return m_amperePhaseB; +} + +double SpeedwireMeter::amperePhaseC() const +{ + return m_amperePhaseC; +} + +QString SpeedwireMeter::softwareVersion() const +{ + return m_softwareVersion; +} + +void SpeedwireMeter::processData(const QByteArray &data) +{ + qCDebug(dcSma()) << "Meter: data received" << data.toHex(); + QDataStream stream(data); + stream.setByteOrder(QDataStream::BigEndian); + + SpeedwireInterface::SpeedwireHeader header = SpeedwireInterface::parseHeader(stream); + if (!header.isValid()) { + qCDebug(dcSma()) << "Meter: Datagram header is not valid. Ignoring data..."; + return; + } + + if (header.protocolId != SpeedwireInterface::ProtocolIdMeter) { + qCDebug(dcSma()) << "Meter: received header protocol which is not from the meter protocol. Ignoring data..."; + return; + } + + quint16 modelId; + quint32 serialNumber; + stream >> modelId >> serialNumber; + if (m_modelId != modelId && serialNumber != m_serialNumber) { + qCDebug(dcSma()) << "Meter: received meter data from an other meter. Ignoring data..."; + } + + qCDebug(dcSma()) << "Meter: Model ID:" << modelId; + qCDebug(dcSma()) << "Meter: Serial number:" << serialNumber; + + // Parse the packet data + // Timestamp e618a416 + qCDebug(dcSma()) << "Meter: ======================= Meter measurements"; + quint32 timestamp; + stream >> timestamp; + qCDebug(dcSma()) << "Meter: Timestamp:" << timestamp; + + // Obis data + //00 01 04 00 00000000 00 01 08 00 0000002139122910 00 02 04 00 00004415 00 02 08 00 0000001575a137d8 00 03 04 00 00000000 00 03 08 00 00000003debed0e8 00040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e90000000 01020852 00000000 + while (!stream.atEnd()) { + quint8 measurementChannel; + quint8 measurementIndex; + quint8 measurmentType; + quint8 measurmentTariff; + + stream >> measurementChannel >> measurementIndex >> measurmentType >> measurmentTariff; + + if (measurmentType == 4) { + qint32 measurement; + stream >> measurement; + + if (measurementIndex == 1 && measurement != 0) { + m_currentPower = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power" << m_currentPower << "W"; + } else if (measurementIndex == 2 && measurement != 0) { + m_currentPower = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power" << m_currentPower << "W"; + } else if (measurementIndex == 21 && measurement != 0) { + m_currentPowerPhaseA = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase A" << m_currentPowerPhaseA << "W"; + } else if (measurementIndex == 22 && measurement != 0) { + m_currentPowerPhaseA = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase A" << m_currentPowerPhaseA << "W"; + } else if (measurementIndex == 41 && measurement != 0) { + m_currentPowerPhaseB = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase B" << m_currentPowerPhaseB << "W"; + } else if (measurementIndex == 42 && measurement != 0) { + m_currentPowerPhaseB = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase B" << m_currentPowerPhaseB << "W"; + } else if (measurementIndex == 61 && measurement != 0) { + m_currentPowerPhaseC = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase C" << m_currentPowerPhaseC << "W"; + } else if (measurementIndex == 62 && measurement != 0) { + m_currentPowerPhaseC = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase C" << m_currentPowerPhaseC << "W"; + } else if (measurementIndex == 31) { + m_amperePhaseA = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Ampere phase A" << m_amperePhaseA << "A"; + } else if (measurementIndex == 51) { + m_amperePhaseB = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Ampere phase B" << m_amperePhaseB << "A"; + } else if (measurementIndex == 71) { + m_amperePhaseC = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Ampere phase C" << m_amperePhaseC << "A"; + } else if (measurementIndex == 32) { + m_voltagePhaseA = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Voltage phase A" << m_voltagePhaseA << "V"; + } else if (measurementIndex == 52) { + m_voltagePhaseB = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Voltage phase B" << m_voltagePhaseB << "V"; + } else if (measurementIndex == 72) { + m_voltagePhaseC = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Voltage phase C" << m_voltagePhaseC << "V"; + } else { +// qCDebug(dcSma()) << "Meter: --> Channel:" << measurementChannel << "Index:" << measurementIndex << "Type:" << measurmentType << "Rate:" << measurmentTariff; +// qCDebug(dcSma()) << "Meter: Value:" << measurement; + } + + + } else if (measurmentType == 8) { + qint64 measurement; + stream >> measurement; + + if (measurementIndex == 1 && measurement != 0) { + m_totalEnergyConsumed = measurement / 3600000.0; + qCDebug(dcSma()) << "Total energy consumed" << m_totalEnergyConsumed << "kWh"; + } else if (measurementIndex == 2 && measurement != 0) { + m_totalEnergyProduced = measurement / 3600000.0; + qCDebug(dcSma()) << "Total energy produced" << m_totalEnergyProduced << "kWh"; + } else if (measurementIndex == 21 && measurement != 0) { + m_energyConsumedPhaseA = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy consumed phase A" << m_energyConsumedPhaseA << "kWh"; + } else if (measurementIndex == 41 && measurement != 0) { + m_energyConsumedPhaseB = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy consumed phase B" << m_energyConsumedPhaseB << "kWh"; + } else if (measurementIndex == 61 && measurement != 0) { + m_energyConsumedPhaseC = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy consumed phase C" << m_energyConsumedPhaseC << "kWh"; + } else if (measurementIndex == 22 && measurement != 0) { + m_energyProducedPhaseA = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy produced phase A" << m_energyProducedPhaseA << "kWh"; + } else if (measurementIndex == 42 && measurement != 0) { + m_energyProducedPhaseB = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy produced phase B" << m_energyProducedPhaseB << "kWh"; + } else if (measurementIndex == 62 && measurement != 0) { + m_energyProducedPhaseC = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy produced phase C" << m_energyProducedPhaseC << "kWh"; + } else { +// qCDebug(dcSma()) << "Meter: --> Channel:" << measurementChannel << "Index:" << measurementIndex << "Type:" << measurmentType << "Rate:" << measurmentTariff; +// qCDebug(dcSma()) << "Meter: Value:" << measurement; + } + + } if (measurementChannel == 144 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) { + // Software version + // 90000000 01 02 08 52 + quint8 major, minor, build, revision; + stream >> major >> minor >> build >> revision; + // Revision types: + // S: Special version + // A: Alpha version + // B: Beta version + // R: Release version + // E: Experimental version + // N: No revision + m_softwareVersion = QString("%1.%2.%3-%4").arg(major).arg(minor).arg(build).arg(QChar(revision)); + + qCDebug(dcSma()) << "Meter: Software version" << m_softwareVersion; + } else if (measurementChannel == 0 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) { + // 00 00 00 00 + //qCDebug(dcSma()) << "Meter: End of data reached."; + } + } + + emit valuesUpdated(); +} diff --git a/sma/speedwiremeter.h b/sma/speedwiremeter.h new file mode 100644 index 00000000..97e6e4f7 --- /dev/null +++ b/sma/speedwiremeter.h @@ -0,0 +1,86 @@ +#ifndef SPEEDWIREMETER_H +#define SPEEDWIREMETER_H + +#include + +#include "speedwireinterface.h" + +class SpeedwireMeter : public QObject +{ + Q_OBJECT +public: + explicit SpeedwireMeter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent = nullptr); + + bool initialize(); + bool initialized() const; + + + double currentPower() const; + double totalEnergyProduced() const; + double totalEnergyConsumed() const; + + double energyConsumedPhaseA() const; + double energyConsumedPhaseB() const; + double energyConsumedPhaseC() const; + + double energyProducedPhaseA() const; + double energyProducedPhaseB() const; + double energyProducedPhaseC() const; + + double currentPowerPhaseA() const; + double currentPowerPhaseB() const; + double currentPowerPhaseC() const; + + double voltagePhaseA() const; + double voltagePhaseB() const; + double voltagePhaseC() const; + + double amperePhaseA() const; + double amperePhaseB() const; + double amperePhaseC() const; + + QString softwareVersion() const; + + +signals: + void valuesUpdated(); + +private: + SpeedwireInterface *m_interface = nullptr; + QHostAddress m_address; + bool m_initialized = false; + quint16 m_modelId = 0; + quint32 m_serialNumber = 0; + + double m_currentPower = 0; + double m_totalEnergyProduced = 0; + double m_totalEnergyConsumed = 0; + + double m_energyConsumedPhaseA = 0; + double m_energyConsumedPhaseB = 0; + double m_energyConsumedPhaseC = 0; + + double m_energyProducedPhaseA = 0; + double m_energyProducedPhaseB = 0; + double m_energyProducedPhaseC = 0; + + double m_currentPowerPhaseA = 0; + double m_currentPowerPhaseB = 0; + double m_currentPowerPhaseC = 0; + + double m_voltagePhaseA = 0; + double m_voltagePhaseB = 0; + double m_voltagePhaseC = 0; + + double m_amperePhaseA = 0; + double m_amperePhaseB = 0; + double m_amperePhaseC = 0; + + QString m_softwareVersion; + +private slots: + void processData(const QByteArray &data); + +}; + +#endif // SPEEDWIREMETER_H From 8b9c38b04d3f80ed3960a741a33682e766073eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 9 Dec 2021 07:00:32 +0100 Subject: [PATCH 04/17] Add basics for inverter communication --- nymea-plugins.pro | 2 +- sma/integrationpluginsma.cpp | 66 +++++++++- sma/integrationpluginsma.h | 8 +- sma/integrationpluginsma.json | 149 +++++++++++++++++++++++ sma/obisdata.cpp | 6 - sma/obisdata.h | 11 -- sma/sma.pro | 4 +- sma/speedwireinterface.cpp | 43 +------ sma/speedwireinterface.h | 5 - sma/speedwireinverter.cpp | 221 ++++++++++++++++++++++++++++++++++ sma/speedwireinverter.h | 75 ++++++++++++ sma/speedwiremeter.cpp | 16 +-- sma/speedwiremeter.h | 1 - 13 files changed, 532 insertions(+), 75 deletions(-) delete mode 100644 sma/obisdata.cpp delete mode 100644 sma/obisdata.h create mode 100644 sma/speedwireinverter.cpp create mode 100644 sma/speedwireinverter.h diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 24070d4f..61d4205a 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -118,7 +118,7 @@ isEmpty(WITH_PLUGINS) { } PLUGINS-=$${WITHOUT_PLUGINS} -#FIXME: PLUGINS=sma +# FIXME: PLUGINS=sma message("Building plugins:") for(plugin, PLUGINS) { exists($${plugin}) { diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index cdf92103..a92624c0 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -131,6 +131,48 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) info->finish(Thing::ThingErrorNoError); }); + speedwireDiscovery->startDiscovery(); + } else if (info->thingClassId() == speedwireInverterThingClassId) { + SpeedwireDiscovery *speedwireDiscovery = new SpeedwireDiscovery(hardwareManager()->networkDeviceDiscovery(), info); + if (!speedwireDiscovery->initialize()) { + qCWarning(dcSma()) << "Could not discovery inverter. The speedwire interface initialization failed."; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to discover the network.")); + return; + } + + connect(speedwireDiscovery, &SpeedwireDiscovery::discoveryFinished, this, [=](){ + qCDebug(dcSma()) << "Speed wire discovery finished."; + speedwireDiscovery->deleteLater(); + + ThingDescriptors descriptors; + foreach (const SpeedwireDiscovery::SpeedwireDiscoveryResult &result, speedwireDiscovery->discoveryResult()) { + if (result.deviceType == SpeedwireInterface::DeviceTypeInverter) { + if (result.serialNumber == 0) + continue; + + ThingDescriptor descriptor(speedwireInverterThingClassId, "SMA Inverter", "Serial: " + QString::number(result.serialNumber) + " - " + result.address.toString()); + // We found an energy meter, let's check if we already added this one + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(speedwireInverterThingSerialNumberParamTypeId).toUInt() == result.serialNumber) { + descriptor.setThingId(existingThing->id()); + break; + } + } + + ParamList params; + params << Param(speedwireInverterThingHostParamTypeId, result.address.toString()); + params << Param(speedwireInverterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(speedwireInverterThingSerialNumberParamTypeId, result.serialNumber); + params << Param(speedwireInverterThingModelIdParamTypeId, result.modelId); + descriptor.setParams(params); + descriptors.append(descriptor); + } + } + + info->addThingDescriptors(descriptors); + info->finish(Thing::ThingErrorNoError); + }); + speedwireDiscovery->startDiscovery(); } } @@ -188,6 +230,7 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) SpeedwireMeter *meter = new SpeedwireMeter(address, modelId, serialNumber, this); if (!meter->initialize()) { + qCWarning(dcSma()) << "Setup failed. Could not initialize meter interface."; info->finish(Thing::ThingErrorHardwareFailure); return; } @@ -219,7 +262,28 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) m_speedwireMeters.insert(thing, meter); info->finish(Thing::ThingErrorNoError); - }else { + } else if (thing->thingClassId() == speedwireInverterThingClassId) { + QHostAddress address = QHostAddress(thing->paramValue(speedwireInverterThingHostParamTypeId).toString()); + quint32 serialNumber = static_cast(thing->paramValue(speedwireInverterThingSerialNumberParamTypeId).toUInt()); + quint16 modelId = static_cast(thing->paramValue(speedwireInverterThingModelIdParamTypeId).toUInt()); + + if (m_speedwireInverters.contains(thing)) { + m_speedwireInverters.take(thing)->deleteLater(); + } + + SpeedwireInverter *inverter = new SpeedwireInverter(address, modelId, serialNumber, this); + if (!inverter->initialize()) { + qCWarning(dcSma()) << "Setup failed. Could not initialize inverter interface."; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // TODO: reachable state + m_speedwireInverters.insert(thing, inverter); + info->finish(Thing::ThingErrorNoError); + + inverter->sendLoginRequest(); + } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index 516a5e1b..169fc1e5 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -31,11 +31,12 @@ #ifndef INTEGRATIONPLUGINSMA_H #define INTEGRATIONPLUGINSMA_H -#include "integrations/integrationplugin.h" -#include "plugintimer.h" -#include "sunnywebbox.h" +#include +#include +#include "sunnywebbox.h" #include "speedwiremeter.h" +#include "speedwireinverter.h" class IntegrationPluginSma: public IntegrationPlugin { Q_OBJECT @@ -61,6 +62,7 @@ private: PluginTimer *m_refreshTimer = nullptr; QHash m_sunnyWebBoxes; QHash m_speedwireMeters; + QHash m_speedwireInverters; }; #endif // INTEGRATIONPLUGINSMA_H diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index b751c93d..a2a7311f 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -306,6 +306,155 @@ "defaultValue": "" } ] + }, + { + "id": "b63a0669-f2ac-4769-abea-e14cafb2309a", + "name": "speedwireInverter", + "displayName": "SMA Inverter", + "createMethods": ["discovery", "user"], + "interfaces": [ "solarinverter" ], + "paramTypes": [ + { + "id": "c8098d53-69eb-4d0b-9f07-e43c4a0ea9a9", + "name": "host", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "192.168.0.168" + }, + { + "id": "7df0ab60-0f11-4495-8e0d-508ba2b6d858", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + }, + { + "id": "e42242b4-2811-47f9-b42b-b150ed233217", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + }, + { + "id": "d9892f74-5b93-4c98-8da2-72aca033273a", + "name": "modelId", + "displayName": "Model ID", + "type": "uint", + "inputType": "TextLine", + "readOnly": true + } + ], + "stateTypes": [ + { + "id": "aaff72c3-c70a-4a2f-bed1-89f38cebe442", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "d9a5768b-1bf5-4933-810d-84dd7a688f71", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "fc168dc6-eecf-40b4-b214-3e28da0dbb12", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "2a6c59ca-853a-47d6-96fb-0c85edf32f52", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "4db96fec-737c-4c4b-bf07-5ef2fd62508a", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "0f23fb0e-a440-4ac2-9aff-896bc65feb2c", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "d7ceb482-5df8-4c0c-82bd-62ce7ba22c43", + "name": "currentPower", + "displayName": "Current power", + "displayNameEvent": "Current power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "b366f680-6134-488b-8362-b1b824a8daca", + "name": "currentPowerMpp1", + "displayName": "Current power MPP1", + "displayNameEvent": "Current power MPP1 changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "87d9b654-5558-47a3-9db9-ffd7c23b4774", + "name": "currentPowerMpp2", + "displayName": "Current power MPP2", + "displayNameEvent": "Current power MPP2 changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "51cadd66-2cf1-485a-a2a9-191d11abfbd1", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "displayNameEvent": "Total energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "type": "QString", + "defaultValue": "" + } + ] } ] } diff --git a/sma/obisdata.cpp b/sma/obisdata.cpp deleted file mode 100644 index 5866185f..00000000 --- a/sma/obisdata.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "obisdata.h" - -ObisData::ObisData() -{ - -} diff --git a/sma/obisdata.h b/sma/obisdata.h deleted file mode 100644 index 0ebe468e..00000000 --- a/sma/obisdata.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef OBISDATA_H -#define OBISDATA_H - - -class ObisData -{ -public: - ObisData(); -}; - -#endif // OBISDATA_H diff --git a/sma/sma.pro b/sma/sma.pro index 313dbae1..a64270a2 100644 --- a/sma/sma.pro +++ b/sma/sma.pro @@ -4,16 +4,16 @@ QT += network SOURCES += \ integrationpluginsma.cpp \ - obisdata.cpp \ speedwirediscovery.cpp \ speedwireinterface.cpp \ + speedwireinverter.cpp \ speedwiremeter.cpp \ sunnywebbox.cpp HEADERS += \ integrationpluginsma.h \ - obisdata.h \ speedwirediscovery.h \ speedwireinterface.h \ + speedwireinverter.h \ speedwiremeter.h \ sunnywebbox.h diff --git a/sma/speedwireinterface.cpp b/sma/speedwireinterface.cpp index e30db2f8..23001852 100644 --- a/sma/speedwireinterface.cpp +++ b/sma/speedwireinterface.cpp @@ -6,44 +6,12 @@ SpeedwireInterface::SpeedwireInterface(const QHostAddress &address, bool multica m_address(address), m_multicast(multicast) { + + qCDebug(dcSma()) << "SpeedwireInterface: Create interface for" << address.toString() << (multicast ? "multicast" : "unicast"); m_socket = new QUdpSocket(this); connect(m_socket, &QUdpSocket::readyRead, this, &SpeedwireInterface::readPendingDatagrams); connect(m_socket, &QUdpSocket::stateChanged, this, &SpeedwireInterface::onSocketStateChanged); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); - - - - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00020058 001e8200 ff208200 00000000 => query device type - // Response 534d4100000402a000000001009e0010 606527a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01020058 01000000 03000000 011e8210 6f89e95f 534e3a20 33303130 35333831 31360000 00000000 00000000 00000000 00000000 - // 011f8208 6f89e95f 411f0001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 => 1f41 solar inverter - // 01208208 6f89e95f 96240000 80240000 81240001 82240000 feffff00 00000000 00000000 00000000 00000000 - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00028053 001e2500 ff1e2500 00000000 => query spot dc power - // Response 534d4100000402a000000001005e0010 606517a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01028053 00000000 01000000 011e2540 61a7e95f 57000000 57000000 57000000 57000000 01000000 - // 021e2540 61a7e95f 5e000000 5e000000 5e000000 5e000000 01000000 00000000 - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000580 00028053 001f4500 ff214500 00000000 => query spot dc voltage/current - // Response 534d4100000402a00000000100960010 606525a0 7d0042be283a00a1 7a01842a71b30001 000000000580 01028053 02000000 05000000 011f4540 61a7e95f 05610000 05610000 05610000 05610000 01000000 - // 021f4540 61a7e95f 505b0000 505b0000 505b0000 505b0000 01000000 - // 01214540 61a7e95f 60010000 60010000 60010000 60010000 01000000 - // 02214540 61a7e95f 95010000 95010000 95010000 95010000 01000000 00000000 - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000680 00020051 00404600 ff424600 00000000 => query spot ac power - // Response 534d4100000402a000000001007a0010 60651ea0 7d0042be283a00a1 7a01842a71b30001 000000000680 01020051 09000000 0b000000 01404640 61a7e95f 38000000 38000000 38000000 38000000 01000000 - // 01414640 61a7e95f 37000000 37000000 37000000 37000000 01000000 - // 01424640 61a7e95f 39000000 39000000 39000000 39000000 01000000 00000000 - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000780 00020051 00484600 ff554600 00000000 => query spot ac voltage/current - // Response 534d4100000402a000000001013e0010 60654fa0 7d0042be283a00a1 7a01842a71b30001 000000000780 01020051 0c000000 15000000 01484600 61a7e95f 5a590000 5a590000 5a590000 5a590000 01000000 - // 01494600 61a7e95f cf590000 cf590000 cf590000 cf590000 01000000 - // 014a4600 61a7e95f 7a590000 7a590000 7a590000 7a590000 01000000 - // 014b4600 61a7e95f f19a0000 f19a0000 f19a0000 f19a0000 01000000 - // 014c4600 61a7e95f 3c9b0000 3c9b0000 3c9b0000 3c9b0000 01000000 - // 014d4600 61a7e95f 189b0000 189b0000 189b0000 189b0000 01000000 - // 014e4600 51a7e95f 1d000000 1d000000 1d000000 1d000000 01000000 - // 01534640 61a7e95f 24010000 24010000 24010000 24010000 01000000 - // 01544640 61a7e95f 1e010000 1e010000 1e010000 1e010000 01000000 - // 01554640 61a7e95f 23010000 23010000 23010000 23010000 01000000 00000000 - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000980 00028051 00482100 ff482100 00000000 => query device status - // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000980 01028051 00000000 00000000 01482108 59c5e95f 33010001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 00000000 - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000a80 00028051 00644100 ff644100 00000000 => query grid relay status - // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000a80 01028051 07000000 07000000 01644108 59c5e95f 33000001 37010000 fdffff00 feffff00 00000000 00000000 00000000 00000000 00000000 } SpeedwireInterface::~SpeedwireInterface() @@ -59,7 +27,7 @@ bool SpeedwireInterface::initialize() } if (m_multicast && !m_socket->joinMulticastGroup(m_multicastAddress)) { - qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); + qCWarning(dcSma()) << "SpeedwireInterface: Initialization failed. Could not join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); return false; } @@ -73,7 +41,7 @@ void SpeedwireInterface::deinitialize() if (m_initialized) { if (m_multicast) { if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { - qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to leave multicast group" << m_multicastAddress.toString(); + qCWarning(dcSma()) << "SpeedwireInterface: Failed to leave multicast group" << m_multicastAddress.toString(); } m_socket->close(); @@ -111,6 +79,7 @@ SpeedwireInterface::SpeedwireHeader SpeedwireInterface::parseHeader(QDataStream void SpeedwireInterface::sendData(const QByteArray &data) { + //qCDebug(dcSma()) << "Send data:" << data.toHex(); m_socket->writeDatagram(data, m_address, m_port); } @@ -129,7 +98,7 @@ void SpeedwireInterface::readPendingDatagrams() continue; qCDebug(dcSma()) << "SpeedwireInterface: Received data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); - qCDebug(dcSma()) << "SpeedwireInterface: " << datagram.toHex(); + //qCDebug(dcSma()) << "SpeedwireInterface: " << datagram.toHex(); emit dataReceived(datagram); } } diff --git a/sma/speedwireinterface.h b/sma/speedwireinterface.h index e619a803..bd240edd 100644 --- a/sma/speedwireinterface.h +++ b/sma/speedwireinterface.h @@ -5,9 +5,6 @@ #include #include - - - class SpeedwireInterface : public QObject { Q_OBJECT @@ -44,10 +41,8 @@ public: inline bool isValid() const { return smaSignature == 0x534d4100 && protocolId != ProtocolIdUnknown; } - }; - explicit SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent = nullptr); ~SpeedwireInterface(); diff --git a/sma/speedwireinverter.cpp b/sma/speedwireinverter.cpp new file mode 100644 index 00000000..1184abfe --- /dev/null +++ b/sma/speedwireinverter.cpp @@ -0,0 +1,221 @@ +#include "speedwireinverter.h" +#include "extern-plugininfo.h" + +#include + +SpeedwireInverter::SpeedwireInverter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent) : + QObject(parent), + m_address(address), + m_modelId(modelId), + m_serialNumber(serialNumber) +{ + + m_interface = new SpeedwireInterface(m_address, true, this); + connect(m_interface, &SpeedwireInterface::dataReceived, this, &SpeedwireInverter::processData); + + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00028053 001e2500 ff1e2500 00000000 => query spot dc power + // Response 534d4100000402a000000001005e0010 606517a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01028053 00000000 01000000 011e2540 61a7e95f 57000000 57000000 57000000 57000000 01000000 + // 021e2540 61a7e95f 5e000000 5e000000 5e000000 5e000000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000580 00028053 001f4500 ff214500 00000000 => query spot dc voltage/current + // Response 534d4100000402a00000000100960010 606525a0 7d0042be283a00a1 7a01842a71b30001 000000000580 01028053 02000000 05000000 011f4540 61a7e95f 05610000 05610000 05610000 05610000 01000000 + // 021f4540 61a7e95f 505b0000 505b0000 505b0000 505b0000 01000000 + // 01214540 61a7e95f 60010000 60010000 60010000 60010000 01000000 + // 02214540 61a7e95f 95010000 95010000 95010000 95010000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000680 00020051 00404600 ff424600 00000000 => query spot ac power + // Response 534d4100000402a000000001007a0010 60651ea0 7d0042be283a00a1 7a01842a71b30001 000000000680 01020051 09000000 0b000000 01404640 61a7e95f 38000000 38000000 38000000 38000000 01000000 + // 01414640 61a7e95f 37000000 37000000 37000000 37000000 01000000 + // 01424640 61a7e95f 39000000 39000000 39000000 39000000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000780 00020051 00484600 ff554600 00000000 => query spot ac voltage/current + // Response 534d4100000402a000000001013e0010 60654fa0 7d0042be283a00a1 7a01842a71b30001 000000000780 01020051 0c000000 15000000 01484600 61a7e95f 5a590000 5a590000 5a590000 5a590000 01000000 + // 01494600 61a7e95f cf590000 cf590000 cf590000 cf590000 01000000 + // 014a4600 61a7e95f 7a590000 7a590000 7a590000 7a590000 01000000 + // 014b4600 61a7e95f f19a0000 f19a0000 f19a0000 f19a0000 01000000 + // 014c4600 61a7e95f 3c9b0000 3c9b0000 3c9b0000 3c9b0000 01000000 + // 014d4600 61a7e95f 189b0000 189b0000 189b0000 189b0000 01000000 + // 014e4600 51a7e95f 1d000000 1d000000 1d000000 1d000000 01000000 + // 01534640 61a7e95f 24010000 24010000 24010000 24010000 01000000 + // 01544640 61a7e95f 1e010000 1e010000 1e010000 1e010000 01000000 + // 01554640 61a7e95f 23010000 23010000 23010000 23010000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000980 00028051 00482100 ff482100 00000000 => query device status + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000980 01028051 00000000 00000000 01482108 59c5e95f 33010001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000a80 00028051 00644100 ff644100 00000000 => query grid relay status + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000a80 01028051 07000000 07000000 01644108 59c5e95f 33000001 37010000 fdffff00 feffff00 00000000 00000000 00000000 00000000 00000000 +} + +bool SpeedwireInverter::initialize() +{ + return m_interface->initialize(); +} + +bool SpeedwireInverter::initialized() const +{ + return m_interface->initialized(); +} + +void SpeedwireInverter::sendLoginRequest(const QString &password, bool loginAsUser) +{ + // Request 534d4100000402a000000001003a0010 60650ea0 7a01842a71b30001 7d0042be283a0001 000000000280 0c04fdff 07000000 84030000 00d8e85f 00000000 c1c1c1c18888888888888888 00000000 => login command = 0xfffd040c, first = 0x00000007 (user 7, installer a), last = 0x00000384 (hier timeout), time = 0x5fdf9ae8, 0x00000000, pw 12 bytes + + // Response 534d4100000402a000000001002e0010 60650be0 7d0042be283a0001 7a01842a71b30001 000000000280 0d04fdff 07000000 84030000 00d8e85f 00000000 00000000 => login OK + // Response 534d4100000402a000000001002e0010 60650be0 7d0042be283a0001 7a01842a71b30001 000100000280 0d04fdff 07000000 84030000 fddbe85f 00000000 00000000 => login INVALID PASSWORD + // command 0xfffd040c => 0x400 set? 0x00c bytecount=12? + + // Build the header + QByteArray header = QByteArray::fromHex("534d4100000402a000000001003a001060650ea0"); + + // The payload is little endian encoded + QByteArray payload; + QDataStream payloadStream(&payload, QIODevice::WriteOnly); + payloadStream.setByteOrder(QDataStream::LittleEndian); + + // Destination + payloadStream << m_modelId << m_serialNumber << static_cast(0x0100); + + // Source + payloadStream << m_interface->sourceModelId() << m_interface->sourceSerialNumber() << static_cast(0x0100); + + // Packet information + quint16 errorCode = 0; + quint16 fragmentId = 0; + quint16 packetId = m_packetId++ | 0x8000; + payloadStream << errorCode << fragmentId << packetId; + + // Command + payloadStream << static_cast(CommandQueryLogin); + + // User type: 7 = user, a = installer + payloadStream << (loginAsUser ? static_cast(0x00000007) : static_cast(0x0000000a)); + + // Timeout + payloadStream << static_cast(900); // 1s + + // Current time + payloadStream << static_cast(QDateTime::currentMSecsSinceEpoch() / 1000.0); + + // Zeros + payloadStream << static_cast(0); + + // Password + QByteArray passwordData = password.toUtf8(); + QByteArray encodedPassword(12, loginAsUser ? 0x88 : 0xBB); + for (int i = 0; i < password.count(); i++) { + encodedPassword[i] = passwordData.at(i) + (loginAsUser ? 0x88 : 0xBB); + } + + for (int i = 0; i < encodedPassword.count(); i++) { + payloadStream << static_cast(encodedPassword.at(i)); + } + + // End of data + payloadStream << static_cast(0); + + QByteArray data = header + payload; + qCDebug(dcSma()) << "Inverter: Send login request" << data.toHex(); + m_interface->sendData(data); + + // 534d4100000402a000000001003a001060650ea0 7a01 842a71b3 0001 7d00 42be283a 0001 000000000280 0c04fdff 07000000 84030000 00d8e85f 00000000 c1c1c1c18888888888888888 00000000 => login command = 0xfffd040c, first = 0x00000007 (user 7, installer a), last = 0x00000384 (hier timeout), time = 0x5fdf9ae8, 0x00000000, pw 12 bytes + // 534d4100000402a000000001003a001060650ea0 9013 be52007d 0001 7d00 42be283a 0001 000000000280 0c04fdff 07000000 84030000 cc96b061 00000000 c1c1c1c18888888888888888 00000000 + // 534d4100000402a000000001003a001060650ea0 9013 be52007d 0001 7d00 42be283a 0001 000000000180 0c04fdff 07000000 84030000 ae9db061 00000000 c1c1c1c18888888888888888 00000000 + +} + +void SpeedwireInverter::querySoftwareVersion() +{ + // Request 534d4100000402a00000000100260010 606509a0 7a01 842a71b30001 7d00 42be283a0001 000000000380 00020058 00348200 ff348200 00000000 => query software version + // Response 534d4100000402a000000001004e0010 606513a0 7d00 42be283a00a1 7a01 842a71b30001 000000000380 01020058 0a000000 0a000000 01348200 2ae5e65f 00000000 00000000 feffffff feffffff 040a1003 040a1003 00000000 00000000 00000000 code = 0x00823401 3 (BCD).10 (BCD).10 (BIN) Typ R (Enum) + + // ========= header + + // == 534d4100000402a00000000100260010 + + // 534d4100 : SMA\0 signature + // 0004 : header length + // 02a0 : Tag0 type + // 0000 : Tag version + // 0001 : Group + // 0026 : payload length + // 0010 : SMA Net 2 version + + // == 606509a0 + + // 6065 : inverter protocol id + // 09 : length of long words = payload length / 4 + // a0 : control ? + + + + // ========= payload (big endian) + + // == 7a01842a71b30001 + + // 7a01 : destination model id + // 842a71b3 : destination serial number + // 0001 : destination control field + + // == 7d0042be283a0001 + + // 7d00 : source model id + // 42be283a: source serial number + // 0001 : source control field + + // == 0000 0000 0380 00020058 + + // 0000 : error code + // 0000 : fragment id + // 0380 : packet id + + // 00020058 : command id = CommandQueryDevice + // 00348200 : first register + // ff348200 : last register + + // 00000000 : end of data + + qCDebug(dcSma()) << "Inverter: Query software version..."; + + // The payload is little endian encoded + QByteArray payload; + QDataStream payloadStream(&payload, QIODevice::WriteOnly); + payloadStream.setByteOrder(QDataStream::LittleEndian); + + // Destination + payloadStream << m_modelId << m_serialNumber << static_cast(0x1000); + + // Source + payloadStream << m_interface->sourceModelId() << m_interface->sourceSerialNumber() << static_cast(0x1000); + + // Packet information + quint16 errorCode = 0; + quint16 fragmentId = 0; + m_packetId += 1; + quint16 packetId = m_packetId | 0x8000; + payloadStream << errorCode << fragmentId << packetId; + + // Request information + payloadStream << static_cast(CommandQueryDevice); + payloadStream << static_cast(0x00823400); // Software version first + payloadStream << static_cast(0x008234ff); // Software version last + + // End of data + payloadStream << static_cast(0); + + // Build the header + QByteArray header = QByteArray::fromHex("534d4100000402a00000000100260010606509a0"); + + QByteArray data = header + payload; + m_interface->sendData(data); +} + +void SpeedwireInverter::queryDeviceType() +{ + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00020058 001e8200 ff208200 00000000 => query device type + // Response 534d4100000402a000000001009e0010 606527a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01020058 01000000 03000000 011e8210 6f89e95f 534e3a20 33303130 35333831 31360000 00000000 00000000 00000000 00000000 + // 011f8208 6f89e95f 411f0001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 => 1f41 solar inverter + // 01208208 6f89e95f 96240000 80240000 81240001 82240000 feffff00 00000000 00000000 00000000 00000000 +} + + +void SpeedwireInverter::processData(const QByteArray &data) +{ + qCDebug(dcSma()) << "Inverter: data received" << data.toHex(); + +} diff --git a/sma/speedwireinverter.h b/sma/speedwireinverter.h new file mode 100644 index 00000000..2d1c3ee5 --- /dev/null +++ b/sma/speedwireinverter.h @@ -0,0 +1,75 @@ +#ifndef SPEEDWIREINVERTER_H +#define SPEEDWIREINVERTER_H + +#include + +#include "speedwireinterface.h" + +class SpeedwireInverter : public QObject +{ + Q_OBJECT +public: + enum Command { + CommandQueryAc = 0x51000200, + CommandQueryStatus = 0x51800200, + CommandQueryDevice = 0x58000200, + CommandQueryDc = 0x53800200, + CommandQueryLogin = 0xfffd040c + }; + + + Q_ENUM(Command) + + + class Request + { + public: + Request(); + + SpeedwireInverter::Command command() const; + + quint16 requestId() const; + + private: + SpeedwireInverter::Command m_command; + quint16 m_requestId = 0; + + }; + + + explicit SpeedwireInverter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent = nullptr); + + bool initialize(); + bool initialized() const; + + double currentPower() const; + double totalEnergyProduced() const; + + // Query methods + void sendLoginRequest(const QString &password = "0000", bool loginAsUser = true); + void querySoftwareVersion(); + void queryDeviceType(); + +signals: + void valuesUpdated(); + +private: + SpeedwireInterface *m_interface = nullptr; + QHostAddress m_address; + bool m_initialized = false; + quint16 m_modelId = 0; + quint32 m_serialNumber = 0; + quint16 m_packetId = 0; + + // Properties + double m_currentPower = 0; + double m_totalEnergyProduced = 0; + + QString m_softwareVersion; + +private slots: + void processData(const QByteArray &data); + +}; + +#endif // SPEEDWIREINVERTER_H diff --git a/sma/speedwiremeter.cpp b/sma/speedwiremeter.cpp index 330ec6f7..b460bd63 100644 --- a/sma/speedwiremeter.cpp +++ b/sma/speedwiremeter.cpp @@ -218,28 +218,28 @@ void SpeedwireMeter::processData(const QByteArray &data) if (measurementIndex == 1 && measurement != 0) { m_totalEnergyConsumed = measurement / 3600000.0; - qCDebug(dcSma()) << "Total energy consumed" << m_totalEnergyConsumed << "kWh"; + qCDebug(dcSma()) << "Meter: Total energy consumed" << m_totalEnergyConsumed << "kWh"; } else if (measurementIndex == 2 && measurement != 0) { m_totalEnergyProduced = measurement / 3600000.0; - qCDebug(dcSma()) << "Total energy produced" << m_totalEnergyProduced << "kWh"; + qCDebug(dcSma()) << "Meter: Total energy produced" << m_totalEnergyProduced << "kWh"; } else if (measurementIndex == 21 && measurement != 0) { m_energyConsumedPhaseA = measurement / 3600000.0; - qCDebug(dcSma()) << "Energy consumed phase A" << m_energyConsumedPhaseA << "kWh"; + qCDebug(dcSma()) << "Meter: Energy consumed phase A" << m_energyConsumedPhaseA << "kWh"; } else if (measurementIndex == 41 && measurement != 0) { m_energyConsumedPhaseB = measurement / 3600000.0; - qCDebug(dcSma()) << "Energy consumed phase B" << m_energyConsumedPhaseB << "kWh"; + qCDebug(dcSma()) << "Meter: Energy consumed phase B" << m_energyConsumedPhaseB << "kWh"; } else if (measurementIndex == 61 && measurement != 0) { m_energyConsumedPhaseC = measurement / 3600000.0; - qCDebug(dcSma()) << "Energy consumed phase C" << m_energyConsumedPhaseC << "kWh"; + qCDebug(dcSma()) << "Meter: Energy consumed phase C" << m_energyConsumedPhaseC << "kWh"; } else if (measurementIndex == 22 && measurement != 0) { m_energyProducedPhaseA = measurement / 3600000.0; - qCDebug(dcSma()) << "Energy produced phase A" << m_energyProducedPhaseA << "kWh"; + qCDebug(dcSma()) << "Meter: Energy produced phase A" << m_energyProducedPhaseA << "kWh"; } else if (measurementIndex == 42 && measurement != 0) { m_energyProducedPhaseB = measurement / 3600000.0; - qCDebug(dcSma()) << "Energy produced phase B" << m_energyProducedPhaseB << "kWh"; + qCDebug(dcSma()) << "Meter: Energy produced phase B" << m_energyProducedPhaseB << "kWh"; } else if (measurementIndex == 62 && measurement != 0) { m_energyProducedPhaseC = measurement / 3600000.0; - qCDebug(dcSma()) << "Energy produced phase C" << m_energyProducedPhaseC << "kWh"; + qCDebug(dcSma()) << "Meter: Energy produced phase C" << m_energyProducedPhaseC << "kWh"; } else { // qCDebug(dcSma()) << "Meter: --> Channel:" << measurementChannel << "Index:" << measurementIndex << "Type:" << measurmentType << "Rate:" << measurmentTariff; // qCDebug(dcSma()) << "Meter: Value:" << measurement; diff --git a/sma/speedwiremeter.h b/sma/speedwiremeter.h index 97e6e4f7..82f251c9 100644 --- a/sma/speedwiremeter.h +++ b/sma/speedwiremeter.h @@ -14,7 +14,6 @@ public: bool initialize(); bool initialized() const; - double currentPower() const; double totalEnergyProduced() const; double totalEnergyConsumed() const; From c4ec1ee6c6de267a6faefa6bc450b9a05d3a56ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 9 Dec 2021 22:10:36 +0100 Subject: [PATCH 05/17] Finish basic inverter data parsing and implementation --- nymea-plugins.pro | 1 - sma/integrationpluginsma.cpp | 87 +- sma/integrationpluginsma.h | 5 +- sma/integrationpluginsma.json | 26 +- sma/sma.pro | 5 + sma/speedwire.h | 301 +++++++ sma/speedwirediscovery.cpp | 113 ++- sma/speedwirediscovery.h | 41 +- sma/speedwireinterface.cpp | 54 +- sma/speedwireinterface.h | 68 +- sma/speedwireinverter.cpp | 1356 ++++++++++++++++++++++++++---- sma/speedwireinverter.h | 192 ++++- sma/speedwireinverterreply.cpp | 85 ++ sma/speedwireinverterreply.h | 89 ++ sma/speedwireinverterrequest.cpp | 76 ++ sma/speedwireinverterrequest.h | 62 ++ sma/speedwiremeter.cpp | 80 +- sma/speedwiremeter.h | 41 + 18 files changed, 2340 insertions(+), 342 deletions(-) create mode 100644 sma/speedwire.h create mode 100644 sma/speedwireinverterreply.cpp create mode 100644 sma/speedwireinverterreply.h create mode 100644 sma/speedwireinverterrequest.cpp create mode 100644 sma/speedwireinverterrequest.h diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 61d4205a..9265169f 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -118,7 +118,6 @@ isEmpty(WITH_PLUGINS) { } PLUGINS-=$${WITHOUT_PLUGINS} -# FIXME: PLUGINS=sma message("Building plugins:") for(plugin, PLUGINS) { exists($${plugin}) { diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index a92624c0..a8b7bfe4 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -212,12 +212,6 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) connect(sunnyWebBox, &SunnyWebBox::connectedChanged, this, &IntegrationPluginSma::onConnectedChanged); connect(sunnyWebBox, &SunnyWebBox::plantOverviewReceived, this, &IntegrationPluginSma::onPlantOverviewReceived); m_sunnyWebBoxes.insert(info->thing(), sunnyWebBox); - - if (!m_refreshTimer) { - qCDebug(dcSma()) << "Starting refresh timer"; - m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(1); - connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSma::onRefreshTimer); - } }); } else if (thing->thingClassId() == speedwireMeterThingClassId) { QHostAddress address = QHostAddress(thing->paramValue(speedwireMeterThingHostParamTypeId).toString()); @@ -235,7 +229,9 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) return; } - // TODO: reachable state + connect(meter, &SpeedwireMeter::reachableChanged, this, [=](bool reachable){ + thing->setStateValue(speedwireMeterConnectedStateTypeId, reachable); + }); connect(meter, &SpeedwireMeter::valuesUpdated, this, [=](){ thing->setStateValue(speedwireMeterConnectedStateTypeId, true); @@ -278,11 +274,36 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) return; } - // TODO: reachable state + qCDebug(dcSma()) << "Inverter: Interface initialized successfully."; + + connect(inverter, &SpeedwireInverter::reachableChanged, this, [=](bool reachable){ + thing->setStateValue(speedwireInverterConnectedStateTypeId, reachable); + }); + + connect(inverter, &SpeedwireInverter::valuesUpdated, this, [=](){ + thing->setStateValue(speedwireInverterTotalEnergyProducedStateTypeId, inverter->totalEnergyProduced()); + thing->setStateValue(speedwireInverterEnergyProducedTodayStateTypeId, inverter->todayEnergyProduced()); + thing->setStateValue(speedwireInverterCurrentPowerStateTypeId, -inverter->totalAcPower()); + thing->setStateValue(speedwireInverterFrequencyStateTypeId, inverter->gridFrequency()); + + thing->setStateValue(speedwireInverterVoltagePhaseAStateTypeId, inverter->voltageAcPhase1()); + thing->setStateValue(speedwireInverterVoltagePhaseBStateTypeId, inverter->voltageAcPhase2()); + thing->setStateValue(speedwireInverterVoltagePhaseCStateTypeId, inverter->voltageAcPhase3()); + + thing->setStateValue(speedwireInverterCurrentPhaseAStateTypeId, inverter->currentAcPhase1()); + thing->setStateValue(speedwireInverterCurrentPhaseBStateTypeId, inverter->currentAcPhase2()); + thing->setStateValue(speedwireInverterCurrentPhaseCStateTypeId, inverter->currentAcPhase3()); + + thing->setStateValue(speedwireInverterCurrentPowerMpp1StateTypeId, inverter->powerDcMpp1()); + thing->setStateValue(speedwireInverterCurrentPowerMpp2StateTypeId, inverter->powerDcMpp2()); + + }); + m_speedwireInverters.insert(thing, inverter); info->finish(Thing::ThingErrorNoError); - inverter->sendLoginRequest(); + // Initial refresh data + inverter->refresh(); } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } @@ -295,8 +316,19 @@ void IntegrationPluginSma::postSetupThing(Thing *thing) SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); if (!sunnyWebBox) return; + + setupRefreshTimer(); sunnyWebBox->getPlantOverview(); thing->setStateValue(sunnyWebBoxConnectedStateTypeId, true); + } else if (thing->thingClassId() == speedwireInverterThingClassId) { + SpeedwireInverter *inverter = m_speedwireInverters.value(thing); + if (inverter) { + thing->setStateValue(speedwireInverterConnectedStateTypeId, inverter->reachable()); + } else { + thing->setStateValue(speedwireInverterConnectedStateTypeId, false); + } + + setupRefreshTimer(); } } @@ -306,6 +338,14 @@ void IntegrationPluginSma::thingRemoved(Thing *thing) m_sunnyWebBoxes.take(thing)->deleteLater(); } + if (thing->thingClassId() == speedwireMeterThingClassId && m_speedwireMeters.contains(thing)) { + m_speedwireMeters.take(thing)->deleteLater(); + } + + if (thing->thingClassId() == speedwireInverterThingClassId && m_speedwireInverters.contains(thing)) { + m_speedwireInverters.take(thing)->deleteLater(); + } + if (myThings().isEmpty()) { qCDebug(dcSma()) << "Stopping timer"; hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); @@ -313,14 +353,6 @@ void IntegrationPluginSma::thingRemoved(Thing *thing) } } -void IntegrationPluginSma::onRefreshTimer() -{ - foreach (Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { - SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); - sunnyWebBox->getPlantOverview(); - } -} - void IntegrationPluginSma::onConnectedChanged(bool connected) { Thing *thing = m_sunnyWebBoxes.key(static_cast(sender())); @@ -347,3 +379,24 @@ void IntegrationPluginSma::onPlantOverviewReceived(const QString &messageId, Sun thing->setStateValue(sunnyWebBoxErrorStateTypeId, overview.error); } } + +void IntegrationPluginSma::setupRefreshTimer() +{ + // If already set up... + if (m_refreshTimer) + return; + + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_refreshTimer, &PluginTimer::timeout, this, [=](){ + foreach (Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { + SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); + sunnyWebBox->getPlantOverview(); + } + + foreach (SpeedwireInverter *inverter, m_speedwireInverters) { + inverter->refresh(); + } + }); + + m_refreshTimer->start(); +} diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index 169fc1e5..ef3aedc1 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -53,13 +53,14 @@ public: void thingRemoved(Thing *thing) override; private slots: - void onRefreshTimer(); - void onConnectedChanged(bool connected); void onPlantOverviewReceived(const QString &messageId, SunnyWebBox::Overview overview); + void setupRefreshTimer(); + private: PluginTimer *m_refreshTimer = nullptr; + QHash m_sunnyWebBoxes; QHash m_speedwireMeters; QHash m_speedwireInverters; diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index a2a7311f..bdf7c41b 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -422,8 +422,8 @@ { "id": "b366f680-6134-488b-8362-b1b824a8daca", "name": "currentPowerMpp1", - "displayName": "Current power MPP1", - "displayNameEvent": "Current power MPP1 changed", + "displayName": "DC power MPP1", + "displayNameEvent": "DC power MPP1 changed", "type": "double", "unit": "Watt", "defaultValue": 0 @@ -431,8 +431,8 @@ { "id": "87d9b654-5558-47a3-9db9-ffd7c23b4774", "name": "currentPowerMpp2", - "displayName": "Current power MPP2", - "displayNameEvent": "Current power MPP2 changed", + "displayName": "DC power MPP2", + "displayNameEvent": "DC power MPP2 changed", "type": "double", "unit": "Watt", "defaultValue": 0 @@ -446,6 +446,24 @@ "unit": "KiloWattHour", "defaultValue": 0.00 }, + { + "id": "e8bc8f81-e5c5-4900-b429-93fcaa262fcb", + "name": "energyProducedToday", + "displayName": "Energy produced today", + "displayNameEvent": "Energy produced today changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "fdccf5de-7413-4480-9ca0-1151665dede8", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00 + }, { "id": "6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66", "name": "firmwareVersion", diff --git a/sma/sma.pro b/sma/sma.pro index a64270a2..e663b33a 100644 --- a/sma/sma.pro +++ b/sma/sma.pro @@ -7,13 +7,18 @@ SOURCES += \ speedwirediscovery.cpp \ speedwireinterface.cpp \ speedwireinverter.cpp \ + speedwireinverterreply.cpp \ + speedwireinverterrequest.cpp \ speedwiremeter.cpp \ sunnywebbox.cpp HEADERS += \ integrationpluginsma.h \ + speedwire.h \ speedwirediscovery.h \ speedwireinterface.h \ speedwireinverter.h \ + speedwireinverterreply.h \ + speedwireinverterrequest.h \ speedwiremeter.h \ sunnywebbox.h diff --git a/sma/speedwire.h b/sma/speedwire.h new file mode 100644 index 00000000..3d01a854 --- /dev/null +++ b/sma/speedwire.h @@ -0,0 +1,301 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SPEEDWIRE_H +#define SPEEDWIRE_H + +#include +#include +#include +#include +#include + +class Speedwire +{ + Q_GADGET +public: + enum Command { + CommandIdentify = 0x00000201, + CommandQueryStatus = 0x51800200, + CommandQueryAc = 0x51000200, + CommandQueryDc = 0x53800200, + CommandQueryEnergy = 0x54000200, + CommandQueryDevice = 0x58000200, + CommandQueryDeviceResponse = 58000201, + CommandLogin = 0xfffd040c, + CommandLogout = 0xfffd010e, + CommandLoginResponse = 0x0ffdf40d + }; + Q_ENUM(Command) + + enum ProtocolId { + ProtocolIdUnknown = 0x0000, + ProtocolIdMeter = 0x6069, + ProtocolIdInverter = 0x6065, + ProtocolIdDiscoveryResponse = 0x0001, + ProtocolIdDiscovery = 0xffff + }; + Q_ENUM(ProtocolId) + + enum DeviceClass { + DeviceClassUnknown = 0x0000, + DeviceClassAllDevices = 0x1f40, + DeviceClassSolarInverter = 0x1f41, + DeviceClassWindTurbine = 0x1f42, + DeviceClassBatteryInverter = 0x1f47, + DeviceClassConsumer = 0x1f61, + DeviceClassSensorSystem = 0x1f80, + DeviceClassElectricityMeter = 0x1f81, + DeviceClassCommunicationProduct = 0x1fc0 + }; + Q_ENUM(DeviceClass) + + class Header + { + public: + Header() = default; + quint32 smaSignature = 0; + quint16 headerLength = 0; + quint16 tagType = 0; + quint16 tagVersion = 0; + quint16 group = 0; + quint16 payloadLength = 0; + quint16 smaNet2Version = 0; + ProtocolId protocolId = ProtocolIdUnknown; + + inline bool isValid() const { + return smaSignature == Speedwire::smaSignature() && protocolId != ProtocolIdUnknown; + } + }; + + typedef struct InverterPackage { + quint8 wordCount = 0; + quint8 control = 0; + quint16 destinationModelId = 0; + quint32 destinationSerialNumber = 0; + quint16 destinationControl = 0; + quint16 sourceModelId = 0; + quint32 sourceSerialNumber = 0; + quint16 sourceControl = 0; + quint16 errorCode = 0; + quint16 fragmentId = 0; + quint16 packetId = 0; + quint32 command = 0; + } InverterPackage; + + Speedwire() = default; + + //static QHash deviceTypes = { {0x0000, "Unknwon"} }; + + static quint16 port() { return 9522; } + static QHostAddress multicastAddress() { return QHostAddress("239.12.255.254"); } + static quint32 smaSignature() { return 0x534d4100; } + static quint16 tag0() { return 0x02a0; } + static quint16 tagVersion() { return 0; } + static quint16 smaNet2Version() { return 0x0010; } + + static QString getModelName(quint16 modelIdentifier) { + switch (modelIdentifier) { + case 9015: return "SB 700"; + case 9016: return "SB 700U"; + case 9017: return "SB 1100"; + case 9018: return "SB 1100U"; + case 9019: return "SB 1100LV"; + case 9020: return "SB 1700"; + case 9021: return "SB 1900TLJ"; + case 9022: return "SB 2100TL"; + case 9023: return "SB 2500"; + case 9024: return "SB 2800"; + case 9025: return "SB 2800i"; + case 9026: return "SB 3000"; + case 9027: return "SB 3000US"; + case 9028: return "SB 3300"; + case 9029: return "SB 3300U"; + case 9030: return "SB 3300TL"; + case 9031: return "SB 3300TL HC"; + case 9032: return "SB 3800"; + case 9033: return "SB 3800U"; + case 9034: return "SB 4000US"; + case 9035: return "SB 4200TL"; + case 9036: return "SB 4200TL HC"; + case 9037: return "SB 5000TL"; + case 9038: return "SB 5000TLW"; + case 9039: return "SB 5000TL HC"; + case 9066: return "SB 1200"; + case 9067: return "STP 10000TL-10"; + case 9068: return "STP 12000TL-10"; + case 9069: return "STP 15000TL-10"; + case 9070: return "STP 17000TL-10"; + case 9084: return "WB 3600TL-20"; + case 9085: return "WB 5000TL-20"; + case 9086: return "SB 3800US-10"; + case 9098: return "STP 5000TL-20"; + case 9099: return "STP 6000TL-20"; + case 9100: return "STP 7000TL-20"; + case 9101: return "STP 8000TL-10"; + case 9102: return "STPcase 9000TL-20"; + case 9103: return "STP 8000TL-20"; + case 9104: return "SB 3000TL-JP-21"; + case 9105: return "SB 3500TL-JP-21"; + case 9106: return "SB 4000TL-JP-21"; + case 9107: return "SB 4500TL-JP-21"; + case 9108: return "SCSMC"; + case 9109: return "SB 1600TL-10"; + case 9131: return "STP 20000TL-10"; + case 9139: return "STP 20000TLHE-10"; + case 9140: return "STP 15000TLHE-10"; + case 9157: return "Sunny Island 2012"; + case 9158: return "Sunny Island 2224"; + case 9159: return "Sunny Island 5048"; + case 9160: return "SB 3600TL-20"; + case 9168: return "SC630HE-11"; + case 9169: return "SC500HE-11"; + case 9170: return "SC400HE-11"; + case 9171: return "WB 3000TL-21"; + case 9172: return "WB 3600TL-21"; + case 9173: return "WB 4000TL-21"; + case 9174: return "WB 5000TL-21"; + case 9175: return "SC 250"; + case 9176: return "SMA Meteo Station"; + case 9177: return "SB 240-10"; + case 9179: return "Multigate-10"; + case 9180: return "Multigate-US-10"; + case 9181: return "STP 20000TLEE-10"; + case 9182: return "STP 15000TLEE-10"; + case 9183: return "SB 2000TLST-21"; + case 9184: return "SB 2500TLST-21"; + case 9185: return "SB 3000TLST-21"; + case 9186: return "WB 2000TLST-21"; + case 9187: return "WB 2500TLST-21"; + case 9188: return "WB 3000TLST-21"; + case 9189: return "WTP 5000TL-20"; + case 9190: return "WTP 6000TL-20"; + case 9191: return "WTP 7000TL-20"; + case 9192: return "WTP 8000TL-20"; + case 9193: return "WTPcase 9000TL-20"; + case 9254: return "Sunny Island 3324"; + case 9255: return "Sunny Island 4.0M"; + case 9256: return "Sunny Island 4248"; + case 9257: return "Sunny Island 4248U"; + case 9258: return "Sunny Island 4500"; + case 9259: return "Sunny Island 4548U"; + case 9260: return "Sunny Island 5.4M"; + case 9261: return "Sunny Island 5048U"; + case 9262: return "Sunny Island 6048U"; + case 9278: return "Sunny Island 3.0M"; + case 9279: return "Sunny Island 4.4M"; + case 9281: return "STP 10000TL-20"; + case 9282: return "STP 11000TL-20"; + case 9283: return "STP 12000TL-20"; + case 9284: return "STP 20000TL-30"; + case 9285: return "STP 25000TL-30"; + case 9301: return "SB1.5-1VL-40"; + case 9302: return "SB2.5-1VL-40"; + case 9303: return "SB2.0-1VL-40"; + case 9304: return "SB5.0-1SP-US-40"; + case 9305: return "SB6.0-1SP-US-40"; + case 9306: return "SB8.0-1SP-US-40"; + case 9307: return "Energy Meter"; + default: return "Unknown"; + } + }; + + // Multicast device discovery request packet, according to SMA documentation. + // However, this does not seem to be supported anymore with version 3.x devices + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, // 0xffffffff group, 0x0000 length, 0x0020 "SMA Net ?", Version ? + // 0x00, 0x00, 0x00, 0x00 // 0x0000 protocol, 0x00 #long words, 0x00 ctrl + + // Unicast device discovery request packet, according to SMA documentation + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x10, // 0x26 length, 0x0010 "SMA Net 2", Version 0 + // 0x60, 0x65, 0x09, 0xa0, 0xff, 0xff, 0xff, 0xff, // 0x6065 protocol, 0x09 #long words, 0xa0 ctrl, 0xffff dst susyID any, 0xffffffff dst serial any + // 0xff, 0xff, 0x00, 0x00, 0x7d, 0x00, 0x52, 0xbe, // 0x0000 dst cntrl, 0x007d src susy id, 0x3a28be52 src serial + // 0x28, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x0000 src cntrl, 0x0000 error code, 0x0000 fragment ID + // 0x01, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // 0x8001 packet ID + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00 + + static QByteArray discoveryDatagramMulticast() { return QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); } + static QByteArray discoveryResponseDatagram() { return QByteArray::fromHex("534d4100000402A000000001000200000001"); } + static QByteArray discoveryDatagramUnicast() { return QByteArray::fromHex("534d4100000402a00000000100260010606509a0ffffffffffff00007d0052be283a000000000000018000020000000000000000000000000000"); } + + static Speedwire::Header parseHeader(QDataStream &stream) { + stream.setByteOrder(QDataStream::BigEndian); + Header header; + quint16 protocolId; + stream >> header.smaSignature >> header.headerLength; + stream >> header.tagType >> header.tagVersion >> header.group; + stream >> header.payloadLength >> header.smaNet2Version; + stream >> protocolId; + header.protocolId = static_cast(protocolId); + return header; + }; + + static Speedwire::InverterPackage parseInverterPackage(QDataStream &stream) { + // Make sure the data stream is little endian + stream.setByteOrder(QDataStream::LittleEndian); + InverterPackage package; + stream >> package.wordCount; + stream >> package.control; + stream >> package.destinationModelId; + stream >> package.destinationSerialNumber; + stream >> package.destinationControl; + stream >> package.sourceModelId; + stream >> package.sourceSerialNumber; + stream >> package.sourceControl; + stream >> package.errorCode; + stream >> package.fragmentId; + stream >> package.packetId; + stream >> package.command; + return package; + }; +}; + +inline QDebug operator<<(QDebug debug, const Speedwire::Header &header) +{ + debug.nospace() << "SpeedwireHeader(" << header.protocolId << ", payload size: " << header.payloadLength << ", group: " << header.payloadLength << ")"; + return debug.maybeSpace(); +} + +inline QDebug operator<<(QDebug debug, const Speedwire::InverterPackage &package) +{ + debug.nospace() << "InverterPackage(" << package.sourceSerialNumber; + debug.nospace() << ", Model ID: " << package.sourceModelId; + debug.nospace() << ", command: " << package.command; + debug.nospace() << ", error: " << package.errorCode; + debug.nospace() << ", fragment: " << package.fragmentId; + debug.nospace() << ", package ID: " << package.fragmentId; + debug.nospace() << ")"; + return debug.maybeSpace(); +} + + +#endif // SPEEDWIRE_H diff --git a/sma/speedwirediscovery.cpp b/sma/speedwirediscovery.cpp index be92340a..6ae02bfa 100644 --- a/sma/speedwirediscovery.cpp +++ b/sma/speedwirediscovery.cpp @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "speedwirediscovery.h" #include "extern-plugininfo.h" @@ -11,36 +41,10 @@ SpeedwireDiscovery::SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDisc // More details: https://github.com/RalfOGit/libspeedwire/ - // //! Multicast device discovery request packet, according to SMA documentation. - // //! However, this does not seem to be supported anymore with version 3.x devices - // const unsigned char multicast_request[] = { - // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 - // 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, // 0xffffffff group, 0x0000 length, 0x0020 "SMA Net ?", Version ? - // 0x00, 0x00, 0x00, 0x00 // 0x0000 protocol, 0x00 #long words, 0x00 ctrl - // }; - - // //! Unicast device discovery request packet, according to SMA documentation - // const unsigned char unicast_request[] = { - // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 - // 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x10, // 0x26 length, 0x0010 "SMA Net 2", Version 0 - // 0x60, 0x65, 0x09, 0xa0, 0xff, 0xff, 0xff, 0xff, // 0x6065 protocol, 0x09 #long words, 0xa0 ctrl, 0xffff dst susyID any, 0xffffffff dst serial any - // 0xff, 0xff, 0x00, 0x00, 0x7d, 0x00, 0x52, 0xbe, // 0x0000 dst cntrl, 0x007d src susy id, 0x3a28be52 src serial - // 0x28, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x0000 src cntrl, 0x0000 error code, 0x0000 fragment ID - // 0x01, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // 0x8001 packet ID - // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 0x00, 0x00 - // }; // // Request: 534d4100000402a00000000100260010 606509a0 ffffffffffff0000 7d0052be283a0000 000000000180 00020000 00000000 00000000 00000000 => command = 0x00000200, first = 0x00000000; last = 0x00000000; trailer = 0x00000000 // // Response 534d4100000402a000000001004e0010 606513a0 7d0052be283a00c0 7a01842a71b30000 000000000180 01020000 00000000 00000000 00030000 00ff0000 00000000 01007a01 842a71b3 00000a00 0c000000 00000000 00000000 01010000 00000000 - - - - - - - // qCDebug(dcSma()) << "SpeedwireDiscovery: Create speed wire interface for multicast" << m_multicastAddress.toString() << "on port" << m_port; // QByteArray exampleData = QByteArray::fromHex("534d4100000402a000000001024400106069010e714369aee618a41600010400000000000001080000000021391229100002040000004415000208000000001575a137d800030400000000000003080000000003debed0e800040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e900000000102085200000000"); // processDatagram(QHostAddress("127.0.0.1"), m_port, exampleData); @@ -160,7 +164,7 @@ void SpeedwireDiscovery::startMulticastDiscovery() void SpeedwireDiscovery::sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress) { - if (m_unicastSocket->writeDatagram(m_discoveryDatagramUnicast, targetHostAddress, m_port) < 0) { + if (m_unicastSocket->writeDatagram(Speedwire::discoveryDatagramUnicast(), targetHostAddress, m_port) < 0) { qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to send unicast discovery datagram to address" << targetHostAddress.toString(); return; } @@ -222,38 +226,25 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin } // Ignore discovery requests - if (datagram == m_discoveryDatagramMulticast || datagram == m_discoveryDatagramUnicast) + if (datagram == Speedwire::discoveryDatagramMulticast() || datagram == Speedwire::discoveryDatagramUnicast()) return; QDataStream stream(datagram); - stream.setByteOrder(QDataStream::BigEndian); - - SpeedwireInterface::SpeedwireHeader header = SpeedwireInterface::parseHeader(stream); + Speedwire::Header header = Speedwire::parseHeader(stream); if (!header.isValid()) { qCWarning(dcSma()) << "SpeedwireDiscovery: Datagram header is not valid. Ignoring data..."; return; } - // Example data: - // 534d4100 0004 02a0 0000 0001 0244 0010 6069 010e 7143 69ae e618a416 00010400000000000001080000000021391229100002040000004415000208000000001575a137d800030400000000000003080000000003debed0e800040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e900000000102085200000000 + qCDebug(dcSma()) << "SpeedwireDiscovery:" << header; - - qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Header"; - qCDebug(dcSma()) << "SpeedwireDiscovery: Length:" << header.headerLength; - qCDebug(dcSma()) << "SpeedwireDiscovery: Tag0:" << header.tagType; - qCDebug(dcSma()) << "SpeedwireDiscovery: Tag0 version:" << header.tagVersion; - qCDebug(dcSma()) << "SpeedwireDiscovery: Group:" << header.group << (header.group == 1 ? "(default group)" : ""); - qCDebug(dcSma()) << "SpeedwireDiscovery: Data length:" << header.payloadLength << datagram.length(); - qCDebug(dcSma()) << "SpeedwireDiscovery: SMA Net 2 Version" << header.smaNet2Version; - qCDebug(dcSma()) << "SpeedwireDiscovery: Protocol ID" << header.protocolId; - - if (header.protocolId == SpeedwireInterface::ProtocolIdDiscoveryResponse) { + if (header.protocolId == Speedwire::ProtocolIdDiscoveryResponse) { qCDebug(dcSma()) << "SpeedwireDiscovery: Received discovery response from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); - // Response: 534d4100 0004 02a0 0000 0001 0002 0000 0001 - // "192.168.178.25:9522" "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0003 0004 0020 0000 0001 0004 0030 c0a8 b219 0004 0040 0000 0000 0002 0070 ef0c 00000000" - // "192.168.178.22:9522" "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0001 0004 0020 0000 0001 0004 0030 c0a8 b216 0004 0040 0000 0001 00000000" - if (!datagram.startsWith(m_discoveryResponseDatagram)) { + // "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0003 0004 0020 0000 0001 0004 0030 c0a8 b219 0004 0040 0000 0000 0002 0070 ef0c 00000000" + // "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0001 0004 0020 0000 0001 0004 0030 c0a8 b216 0004 0040 0000 0001 00000000" + + if (!datagram.startsWith(Speedwire::discoveryResponseDatagram())) { qCWarning(dcSma()) << "SpeedwireDiscovery: Received discovery reply but the message start does not match the required schema. Ignoring data..."; return; } @@ -265,7 +256,6 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin return; } - SpeedwireDiscoveryResult result; result.address = senderAddress; if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { @@ -283,14 +273,12 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin // We received SMA data, let's parse depending on the protocol id - if (header.protocolId == SpeedwireInterface::ProtocolIdMeter) { + if (header.protocolId == Speedwire::ProtocolIdMeter) { // Example: 010e 714369ae quint16 modelId; quint32 serialNumber; stream >> modelId >> serialNumber; - qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Meter identifier"; - qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << modelId; - qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << serialNumber; + qCDebug(dcSma()) << "SpeedwireDiscovery: Meter identifier: Model ID:" << modelId << "Serial number:" << serialNumber; if (!m_results.contains(senderAddress)) { SpeedwireDiscoveryResult result; @@ -305,13 +293,10 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin m_results[senderAddress].modelId = modelId; m_results[senderAddress].serialNumber = serialNumber; - } else if (header.protocolId == SpeedwireInterface::ProtocolIdInverter) { - quint16 modelId; - quint32 serialNumber; - stream >> modelId >> serialNumber; - qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Inverter identifier"; - qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << modelId; - qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << serialNumber; + } else if (header.protocolId == Speedwire::ProtocolIdInverter) { + Speedwire::InverterPackage inverterPackage = Speedwire::parseInverterPackage(stream); + // Response from inverter 534d4100 0004 02a0 0000 0001 004e 0010 6065 1390 7d00 52be283a 0000 b500 c2c12e12 0000 0000 00000 1800102000000000000000000000003000000ff0000ecd5ff1f0100b500c2c12e1200000a000c00000000000000030000000101000000000000 + qCDebug(dcSma()) << "SpeedwireDiscovery:" << inverterPackage; if (!m_results.contains(senderAddress)) { SpeedwireDiscoveryResult result; @@ -324,18 +309,17 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); } - m_results[senderAddress].modelId = modelId; - m_results[senderAddress].serialNumber = serialNumber; + m_results[senderAddress].modelId = inverterPackage.sourceModelId; + m_results[senderAddress].serialNumber = inverterPackage.sourceSerialNumber; } else { qCWarning(dcSma()) << "SpeedwireDiscovery: Unhandled data received" << datagram.toHex(); return; } - } void SpeedwireDiscovery::sendDiscoveryRequest() { - if (m_multicastSocket->writeDatagram(m_discoveryDatagramMulticast, m_multicastAddress, m_port) < 0) { + if (m_multicastSocket->writeDatagram(Speedwire::discoveryDatagramMulticast(), m_multicastAddress, m_port) < 0) { qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to send discovery datagram to multicast address" << m_multicastAddress.toString(); return; } @@ -360,6 +344,5 @@ void SpeedwireDiscovery::onDiscoveryProcessFinished() qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << result.serialNumber; } - emit discoveryFinished(); } diff --git a/sma/speedwirediscovery.h b/sma/speedwirediscovery.h index c1da61e2..3c443431 100644 --- a/sma/speedwirediscovery.h +++ b/sma/speedwirediscovery.h @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef SPEEDWIREDISCOVERY_H #define SPEEDWIREDISCOVERY_H @@ -7,6 +37,7 @@ #include +#include "speedwire.h" #include "speedwireinterface.h" class SpeedwireDiscovery : public QObject @@ -39,8 +70,8 @@ private: NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; QUdpSocket *m_multicastSocket = nullptr; QUdpSocket *m_unicastSocket = nullptr; - QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); - quint16 m_port = 9522; + QHostAddress m_multicastAddress = Speedwire::multicastAddress(); + quint16 m_port = Speedwire::port(); bool m_initialized = false; // Discovery @@ -49,12 +80,6 @@ private: NetworkDeviceInfos m_networkDeviceInfos; QHash m_results; - // Static discovery datagrams for speedwire - QByteArray m_discoveryDatagramMulticast = QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); - QByteArray m_discoveryResponseDatagram = QByteArray::fromHex("534d4100000402A000000001000200000001"); - - QByteArray m_discoveryDatagramUnicast = QByteArray::fromHex("534d4100000402a00000000100260010606509a0ffffffffffff00007d0052be283a000000000000018000020000000000000000000000000000"); - void startMulticastDiscovery(); void sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress); diff --git a/sma/speedwireinterface.cpp b/sma/speedwireinterface.cpp index 23001852..4d7196aa 100644 --- a/sma/speedwireinterface.cpp +++ b/sma/speedwireinterface.cpp @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "speedwireinterface.h" #include "extern-plugininfo.h" @@ -43,10 +73,10 @@ void SpeedwireInterface::deinitialize() if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { qCWarning(dcSma()) << "SpeedwireInterface: Failed to leave multicast group" << m_multicastAddress.toString(); } - - m_socket->close(); - m_initialized = false; } + + m_socket->close(); + m_initialized = false; } } @@ -65,22 +95,12 @@ quint32 SpeedwireInterface::sourceSerialNumber() const return m_sourceSerialNumber; } -SpeedwireInterface::SpeedwireHeader SpeedwireInterface::parseHeader(QDataStream &stream) -{ - SpeedwireHeader header; - quint16 protocolId; - stream >> header.smaSignature >> header.headerLength; - stream >> header.tagType >> header.tagVersion >> header.group; - stream >> header.payloadLength >> header.smaNet2Version; - stream >> protocolId; - header.protocolId = static_cast(protocolId); - return header; -} - void SpeedwireInterface::sendData(const QByteArray &data) { - //qCDebug(dcSma()) << "Send data:" << data.toHex(); - m_socket->writeDatagram(data, m_address, m_port); + qCDebug(dcSma()) << "SpeedwireInterface: -->" << m_address.toString() << m_port << data.toHex(); + if (m_socket->writeDatagram(data, m_address, m_port) < 0) { + qCWarning(dcSma()) << "SpeedwireInterface: failed to send data" << m_socket->errorString(); + } } void SpeedwireInterface::readPendingDatagrams() diff --git a/sma/speedwireinterface.h b/sma/speedwireinterface.h index bd240edd..664ce4a5 100644 --- a/sma/speedwireinterface.h +++ b/sma/speedwireinterface.h @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef SPEEDWIREINTERFACE_H #define SPEEDWIREINTERFACE_H @@ -5,19 +35,12 @@ #include #include +#include "speedwire.h" + class SpeedwireInterface : public QObject { Q_OBJECT public: - enum ProtocolId { - ProtocolIdUnknown = 0x0000, - ProtocolIdMeter = 0x6069, - ProtocolIdInverter = 0x6065, - ProtocolIdDiscoveryResponse = 0x0001, - ProtocolIdDiscovery = 0xffff - }; - Q_ENUM(ProtocolId) - enum DeviceType { DeviceTypeUnknown, DeviceTypeMeter, @@ -25,24 +48,6 @@ public: }; Q_ENUM(DeviceType) - class SpeedwireHeader - { - public: - SpeedwireHeader() = default; - quint32 smaSignature = 0; - quint16 headerLength = 0; - quint16 tagType = 0; - quint16 tagVersion = 0; - quint16 group = 0; - quint16 payloadLength = 0; - quint16 smaNet2Version = 0; - ProtocolId protocolId = ProtocolIdUnknown; - - inline bool isValid() const { - return smaSignature == 0x534d4100 && protocolId != ProtocolIdUnknown; - } - }; - explicit SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent = nullptr); ~SpeedwireInterface(); @@ -54,8 +59,6 @@ public: quint16 sourceModelId() const; quint32 sourceSerialNumber() const; - static SpeedwireInterface::SpeedwireHeader parseHeader(QDataStream &stream); - public slots: void sendData(const QByteArray &data); @@ -65,14 +68,14 @@ signals: private: QUdpSocket *m_socket = nullptr; QHostAddress m_address; - quint16 m_port = 9522; - QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + quint16 m_port = Speedwire::port(); + QHostAddress m_multicastAddress = Speedwire::multicastAddress(); bool m_multicast = false; bool m_initialized = false; // Requester quint16 m_sourceModelId = 0x007d; - quint32 m_sourceSerialNumber = 0x3a28be42; + quint32 m_sourceSerialNumber = 0x3a28be52; private slots: void readPendingDatagrams(); @@ -81,4 +84,5 @@ private slots: }; + #endif // SPEEDWIREINTERFACE_H diff --git a/sma/speedwireinverter.cpp b/sma/speedwireinverter.cpp index 1184abfe..24c90b06 100644 --- a/sma/speedwireinverter.cpp +++ b/sma/speedwireinverter.cpp @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "speedwireinverter.h" #include "extern-plugininfo.h" @@ -10,9 +40,416 @@ SpeedwireInverter::SpeedwireInverter(const QHostAddress &address, quint16 modelI m_serialNumber(serialNumber) { - m_interface = new SpeedwireInterface(m_address, true, this); + qCDebug(dcSma()) << "Inverter: setup interface on" << m_address.toString(); + m_interface = new SpeedwireInterface(m_address, false, this); connect(m_interface, &SpeedwireInterface::dataReceived, this, &SpeedwireInverter::processData); +} + +bool SpeedwireInverter::initialize() +{ + return m_interface->initialize(); +} + +bool SpeedwireInverter::initialized() const +{ + return m_interface->initialized(); +} + +SpeedwireInverter::State SpeedwireInverter::state() const +{ + return m_state; +} + +bool SpeedwireInverter::reachable() const +{ + return m_reachable; +} + +QString SpeedwireInverter::modelName() const +{ + return m_modelName; +} + +double SpeedwireInverter::totalAcPower() const +{ + return m_totalAcPower; +} + +double SpeedwireInverter::gridFrequency() const +{ + return m_gridFrequency; +} + +double SpeedwireInverter::totalEnergyProduced() const +{ + return m_totalEnergyProduced; +} + +double SpeedwireInverter::todayEnergyProduced() const +{ + return m_todayEnergyProduced; +} + +double SpeedwireInverter::voltageAcPhase1() const +{ + return m_voltageAcPhase1; +} + +double SpeedwireInverter::voltageAcPhase2() const +{ + return m_voltageAcPhase2; +} + +double SpeedwireInverter::voltageAcPhase3() const +{ + return m_voltageAcPhase3; +} + +double SpeedwireInverter::currentAcPhase1() const +{ + return m_currentAcPhase1; +} + +double SpeedwireInverter::currentAcPhase2() const +{ + return m_currentAcPhase2; +} + +double SpeedwireInverter::currentAcPhase3() const +{ + return m_currentAcPhase3; +} + +double SpeedwireInverter::powerAcPhase1() const +{ + return m_powerAcPhase1; +} + +double SpeedwireInverter::powerAcPhase2() const +{ + return m_powerAcPhase2; +} + +double SpeedwireInverter::powerAcPhase3() const +{ + return m_powerAcPhase3; +} + +double SpeedwireInverter::powerDcMpp1() const +{ + return m_powerDcMpp1; +} + +double SpeedwireInverter::powerDcMpp2() const +{ + return m_powerDcMpp2; +} + +SpeedwireInverterReply *SpeedwireInverter::sendIdentifyRequest() +{ + // Request 534d4100000402a000000001002600106065 09 a0 ffff ffffffff 0000 7d00 52be283a 0000 0000 0000 0180 00020000 000000000000000000000000 + // Response 534d4100000402a000000001004e00106065 13 90 7d00 52be283a 0000 b500 c2c12e12 0000 0000 0000 0180 01020000 00000000000000000003000000ff0000ecd5ff1f0100b500c2c12e1200000a000c00000000000000030000000101000000000000 + + qCDebug(dcSma()) << "Inverter: Sending identify request to" << m_address.toString(); + SpeedwireInverterRequest request; + request.setPacketId(0x8001); + request.setCommand(Speedwire::CommandIdentify); + request.setRequestData(Speedwire::discoveryDatagramUnicast()); + return createReply(request); +} + +SpeedwireInverterReply *SpeedwireInverter::sendLoginRequest(const QString &password, bool loginAsUser) +{ + qCDebug(dcSma()) << "Inverter: Sending login request as" << (loginAsUser ? "user" : "installer") << "using password" << password; + + // Request: 534d4100000402a000000001003a001060650ea0 b500 c2c12e12 0001 7d00 52be283a 0001 0000 0000 0180 0c04fdff0 7000000 84030000 3408b261 00000000 b8b8b8b8888888888888888800000000 // Login request + + // Response: 534d4100000402a000000001003a001060650ed0 7d00 52be283a 0001 b500 c2c12e12 0001 0000000001800 d04fdff0 7000000 84030000 3408b261 00000000 b8b8b8b8888888888888888800000000 // Login OK + // Response: 534d4100000402a000000001003a001060650ed0 7d00 52be283a 0001 b500 c2c12e12 0001 0001000001800 d04fdff0 7000000 84030000 b709b261 00000000 b8b8b8b9888888888888888800000000 // Login FAILED, error 1 + + // Build the header + QByteArray datagram; + QDataStream stream(&datagram, QIODevice::WriteOnly); + buildDefaultHeader(stream, 58, 0xa0); + + // Reset the packet id counter, otherwise there will be no response + //m_packetId = 0; + quint16 packetId = m_packetId++ | 0x8000; + Speedwire::Command command = Speedwire::CommandLogin; + + // The payload is little endian encoded + buildPackage(stream, command, packetId); + + // User type: 7 = user, a = installer + stream << (loginAsUser ? static_cast(0x00000007) : static_cast(0x0000000a)); + // Timeout + stream << static_cast(900); // 900 ms + // Current time + stream << static_cast(QDateTime::currentMSecsSinceEpoch() / 1000.0); + // Zeros + stream << static_cast(0); + + // Encode password + QByteArray passwordData = password.toUtf8(); + QByteArray encodedPassword(12, loginAsUser ? 0x88 : 0xBB); + for (int i = 0; i < password.count(); i++) { + encodedPassword[i] = (passwordData.at(i) + (loginAsUser ? 0x88 : 0xBB) % 0xff); + } + + // Add endoced password + for (int i = 0; i < encodedPassword.count(); i++) { + stream << static_cast(encodedPassword.at(i)); + } + + // End of data + stream << static_cast(0); + + // Final datagram + SpeedwireInverterRequest request; + request.setPacketId(packetId); + request.setCommand(command); + request.setRequestData(datagram); + return createReply(request); +} + +SpeedwireInverterReply *SpeedwireInverter::sendLogoutRequest() +{ + // Request 534d4100000402a000000001 0022 0010 6065 08a0 ffffffffffff0003 7d0052be283a0003 00000000 0280 0e01fdff ffffffff 00000000 => logoff command = 0xfffd01e0 + + // 534d4100000402a000000001 0022 0010 6065 08a0 ffffffffffff0003 7d0052be283a0003 00000000 0480 0e01fdff ffffffff 00000000 + + // Request 534d4100000402a000000001 0022 0010 6065 08a0 ffffffffffff0003 7d0040be283a0003 000000000380 e001fdff ffffffff 00000000 + // Request 534d4100000402a000000001 0022 0010 6065 08a0 ffffffffffff0003 7d0042be283a0003 000000000180 e001fdff ffffffff 00000000 + // 534d4100000402a000000001 0022 0010 6065 08a0 ffffffffffff0003 7d0052be283a0003 000000000080 0e01fdff ffffffff 00000000 + + // Build the header + QByteArray datagram; + QDataStream stream(&datagram, QIODevice::WriteOnly); + buildDefaultHeader(stream, 34); + + // Reset the packet id counter, otherwise there will be no response + quint16 packetId = m_packetId++ | 0x8000; + Speedwire::Command command = Speedwire::CommandLogout; + + // The payload is little endian encoded + stream.setByteOrder(QDataStream::LittleEndian); + + // Target + // stream << static_cast(m_modelId); + // stream << static_cast(m_serialNumber); + stream << static_cast(0xffff); + stream << static_cast(0xffffffff); + stream << static_cast(0x0300); + + // Source + stream << m_interface->sourceModelId(); + stream << m_interface->sourceSerialNumber(); + stream << static_cast(0x0300); + + stream << static_cast(0); + stream << static_cast(0); + stream << packetId; + + stream << command; + // Only first word + stream << static_cast(0xffffffff); + stream << static_cast(0); + + SpeedwireInverterRequest request; + request.setPacketId(packetId); + request.setCommand(command); + request.setRequestData(datagram); + request.setRetries(0); + return createReply(request); +} + +SpeedwireInverterReply *SpeedwireInverter::sendSoftwareVersionRequest() +{ + qCDebug(dcSma()) << "Inverter: Sending software version request to" << m_address.toString(); + + // Request 534d4100000402a00000000100260010 6065 09a0 7a01 842a71b3 0001 7d00 42be283a 0001 000000000380 00020058 00348200 ff348200 00000000 => query software version + // Response 534d4100000402a000000001004e0010 6065 13a0 7d00 42be283a 00a1 7a01 842a71b3 0001 000000000380 01020058 0a000000 0a000000 01348200 2ae5e65f 00000000 00000000 feffffff feffffff 040a1003 040a1003 00000000 00000000 00000000 code = 0x00823401 3 (BCD).10 (BCD).10 (BIN) Typ R (Enum) + + // Build the header + QByteArray datagram; + QDataStream stream(&datagram, QIODevice::WriteOnly); + buildDefaultHeader(stream, 38, 0xa0); + + // Reset the packet id counter, otherwise there will be no response + quint16 packetId = m_packetId++ | 0x8000; + Speedwire::Command command = Speedwire::CommandQueryDevice; + + // The payload is little endian encoded + buildPackage(stream, command, packetId); + + // First and last word + stream << static_cast(0x00823400); + stream << static_cast(0x008234ff); + + // End of data + stream << static_cast(0); + + // Final datagram + SpeedwireInverterRequest request; + request.setPacketId(packetId); + request.setCommand(command); + request.setRequestData(datagram); + return createReply(request); +} + +SpeedwireInverterReply *SpeedwireInverter::sendDeviceTypeRequest() +{ + qCDebug(dcSma()) << "Inverter: Sending software version request to" << m_address.toString(); + // Build the header + QByteArray datagram; + QDataStream stream(&datagram, QIODevice::WriteOnly); + buildDefaultHeader(stream, 38, 0xa0); + + // Reset the packet id counter, otherwise there will be no response + quint16 packetId = m_packetId++ | 0x8000; + Speedwire::Command command = Speedwire::CommandQueryDevice; + + // The payload is little endian encoded + buildPackage(stream, command, packetId); + + // 2 words + stream << static_cast(0x00821e00); + stream << static_cast(0x008220ff); + + // End of data + stream << static_cast(0); + + // Final datagram + SpeedwireInverterRequest request; + request.setPacketId(packetId); + request.setCommand(command); + request.setRequestData(datagram); + return createReply(request); +} + +void SpeedwireInverter::refresh() +{ + // Only refresh if not already busy... + if (m_state != StateIdle && m_state != StateDisconnected) + return; + + // Run the state machine + setState(StateInitializing); +} + +void SpeedwireInverter::sendNextReply() +{ + // Pending reply + if (m_currentReply) + return; + + // No reply left + if (m_replyQueue.isEmpty()) + return; + + // Pick the next reply and send request + m_currentReply = m_replyQueue.dequeue(); + qCDebug(dcSma()) << "Inverter: --> Sending" << m_currentReply->request().command() << "package ID:" << m_currentReply->request().packetId(); + m_interface->sendData(m_currentReply->request().requestData()); + m_currentReply->startWaiting(); +} + +SpeedwireInverterReply *SpeedwireInverter::createReply(const SpeedwireInverterRequest &request) +{ + SpeedwireInverterReply *reply = new SpeedwireInverterReply(request, this); + connect(reply, &SpeedwireInverterReply::timeout, this, &SpeedwireInverter::onReplyTimeout); + connect(reply, &SpeedwireInverterReply::finished, this, &SpeedwireInverter::onReplyFinished); + // Make sure the reply gets deleted once finished + connect(reply, &SpeedwireInverterReply::finished, reply, &SpeedwireInverterReply::deleteLater); + + // Schedule request + m_replyQueue.enqueue(reply); + sendNextReply(); + + return reply; +} + +void SpeedwireInverter::buildDefaultHeader(QDataStream &stream, quint16 payloadSize, quint8 control) +{ + // Header (big endian) + // 534d4100000402a00000000100260010606509a0 + + // 534d4100 : SMA\0 signature + // 0004 : header length + // 02a0 : Tag0 type + // 0000 : Tag version + // 0001 : Group + // 0026 : payload length + // 0010 : SMA Net 2 version + // 6065 : inverter protocol id + // 09 : length of long words = (payload length - 2) / 4 + // a0 : control ? + + stream.setByteOrder(QDataStream::BigEndian); + stream << Speedwire::smaSignature(); + stream << static_cast(4); // Header length + stream << Speedwire::tag0(); + stream << Speedwire::tagVersion(); + stream << static_cast(1); // Group 1 = default + stream << payloadSize; + stream << Speedwire::smaNet2Version(); + stream << static_cast(Speedwire::ProtocolIdInverter); + stream << static_cast((payloadSize - 2) / 4); // wordCount + stream << control; +} + +void SpeedwireInverter::buildPackage(QDataStream &stream, quint32 command, quint16 packetId) +{ + // ========= package header (little endian) + // == 7a01842a71b30001 + + // 7a01 : destination model id + // 842a71b3 : destination serial number + // 0001 : destination control field + + // == 7d0042be283a0001 + + // 7d00 : source model id + // 42be283a: source serial number + // 0001 : source control field + + // == 0000 0000 0380 00020058 + + // 0000 : error code + // 0000 : fragment id + // 0380 : packet id + // 00020058 : command id = CommandQueryDevice + + stream.setByteOrder(QDataStream::LittleEndian); + // Destination + stream << m_modelId << m_serialNumber; + // Destination Ctrl + stream << static_cast(0x0100); + // Source + stream << m_interface->sourceModelId() << m_interface->sourceSerialNumber(); + // Destination Ctrl + stream << static_cast(0x0100); + + // Packet information + quint16 errorCode = 0; + quint16 fragmentId = 0; + stream << errorCode << fragmentId << packetId; + + // Command + stream << static_cast(command); +} + +SpeedwireInverterReply *SpeedwireInverter::sendQueryRequest(Speedwire::Command command, quint32 firstWord, quint32 secondWord) +{ + qCDebug(dcSma()) << "Inverter: Sending query request to" << m_address.toString(); + + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000380 00020058 00348200 ff348200 00000000 => query software version + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000380 01020058 0a000000 0a000000 01348200 2ae5e65f 00000000 00000000 feffffff feffffff 040a1003 040a1003 00000000 00000000 00000000 code = 0x00823401 3 (BCD).10 (BCD).10 (BIN) Typ R (Enum) + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00020058 001e8200 ff208200 00000000 => query device type + // Response 534d4100000402a000000001009e0010 606527a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01020058 01000000 03000000 011e8210 6f89e95f 534e3a20 33303130 35333831 31360000 00000000 00000000 00000000 00000000 + // 011f8208 6f89e95f 411f0001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 => 1f41 solar inverter + // 01208208 6f89e95f 96240000 80240000 81240001 82240000 feffff00 00000000 00000000 00000000 00000000 // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00028053 001e2500 ff1e2500 00000000 => query spot dc power // Response 534d4100000402a000000001005e0010 606517a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01028053 00000000 01000000 011e2540 61a7e95f 57000000 57000000 57000000 57000000 01000000 // 021e2540 61a7e95f 5e000000 5e000000 5e000000 5e000000 01000000 00000000 @@ -40,182 +477,783 @@ SpeedwireInverter::SpeedwireInverter(const QHostAddress &address, quint16 modelI // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000980 01028051 00000000 00000000 01482108 59c5e95f 33010001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 00000000 // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000a80 00028051 00644100 ff644100 00000000 => query grid relay status // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000a80 01028051 07000000 07000000 01644108 59c5e95f 33000001 37010000 fdffff00 feffff00 00000000 00000000 00000000 00000000 00000000 -} -bool SpeedwireInverter::initialize() -{ - return m_interface->initialize(); -} - -bool SpeedwireInverter::initialized() const -{ - return m_interface->initialized(); -} - -void SpeedwireInverter::sendLoginRequest(const QString &password, bool loginAsUser) -{ - // Request 534d4100000402a000000001003a0010 60650ea0 7a01842a71b30001 7d0042be283a0001 000000000280 0c04fdff 07000000 84030000 00d8e85f 00000000 c1c1c1c18888888888888888 00000000 => login command = 0xfffd040c, first = 0x00000007 (user 7, installer a), last = 0x00000384 (hier timeout), time = 0x5fdf9ae8, 0x00000000, pw 12 bytes - - // Response 534d4100000402a000000001002e0010 60650be0 7d0042be283a0001 7a01842a71b30001 000000000280 0d04fdff 07000000 84030000 00d8e85f 00000000 00000000 => login OK - // Response 534d4100000402a000000001002e0010 60650be0 7d0042be283a0001 7a01842a71b30001 000100000280 0d04fdff 07000000 84030000 fddbe85f 00000000 00000000 => login INVALID PASSWORD - // command 0xfffd040c => 0x400 set? 0x00c bytecount=12? // Build the header - QByteArray header = QByteArray::fromHex("534d4100000402a000000001003a001060650ea0"); + QByteArray datagram; + QDataStream stream(&datagram, QIODevice::WriteOnly); + buildDefaultHeader(stream); - // The payload is little endian encoded - QByteArray payload; - QDataStream payloadStream(&payload, QIODevice::WriteOnly); - payloadStream.setByteOrder(QDataStream::LittleEndian); - - // Destination - payloadStream << m_modelId << m_serialNumber << static_cast(0x0100); - - // Source - payloadStream << m_interface->sourceModelId() << m_interface->sourceSerialNumber() << static_cast(0x0100); - - // Packet information - quint16 errorCode = 0; - quint16 fragmentId = 0; + // Reset the packet id counter, otherwise there will be no response quint16 packetId = m_packetId++ | 0x8000; - payloadStream << errorCode << fragmentId << packetId; - - // Command - payloadStream << static_cast(CommandQueryLogin); - - // User type: 7 = user, a = installer - payloadStream << (loginAsUser ? static_cast(0x00000007) : static_cast(0x0000000a)); - - // Timeout - payloadStream << static_cast(900); // 1s - - // Current time - payloadStream << static_cast(QDateTime::currentMSecsSinceEpoch() / 1000.0); - - // Zeros - payloadStream << static_cast(0); - - // Password - QByteArray passwordData = password.toUtf8(); - QByteArray encodedPassword(12, loginAsUser ? 0x88 : 0xBB); - for (int i = 0; i < password.count(); i++) { - encodedPassword[i] = passwordData.at(i) + (loginAsUser ? 0x88 : 0xBB); - } - - for (int i = 0; i < encodedPassword.count(); i++) { - payloadStream << static_cast(encodedPassword.at(i)); - } - - // End of data - payloadStream << static_cast(0); - - QByteArray data = header + payload; - qCDebug(dcSma()) << "Inverter: Send login request" << data.toHex(); - m_interface->sendData(data); - - // 534d4100000402a000000001003a001060650ea0 7a01 842a71b3 0001 7d00 42be283a 0001 000000000280 0c04fdff 07000000 84030000 00d8e85f 00000000 c1c1c1c18888888888888888 00000000 => login command = 0xfffd040c, first = 0x00000007 (user 7, installer a), last = 0x00000384 (hier timeout), time = 0x5fdf9ae8, 0x00000000, pw 12 bytes - // 534d4100000402a000000001003a001060650ea0 9013 be52007d 0001 7d00 42be283a 0001 000000000280 0c04fdff 07000000 84030000 cc96b061 00000000 c1c1c1c18888888888888888 00000000 - // 534d4100000402a000000001003a001060650ea0 9013 be52007d 0001 7d00 42be283a 0001 000000000180 0c04fdff 07000000 84030000 ae9db061 00000000 c1c1c1c18888888888888888 00000000 - -} - -void SpeedwireInverter::querySoftwareVersion() -{ - // Request 534d4100000402a00000000100260010 606509a0 7a01 842a71b30001 7d00 42be283a0001 000000000380 00020058 00348200 ff348200 00000000 => query software version - // Response 534d4100000402a000000001004e0010 606513a0 7d00 42be283a00a1 7a01 842a71b30001 000000000380 01020058 0a000000 0a000000 01348200 2ae5e65f 00000000 00000000 feffffff feffffff 040a1003 040a1003 00000000 00000000 00000000 code = 0x00823401 3 (BCD).10 (BCD).10 (BIN) Typ R (Enum) - - // ========= header - - // == 534d4100000402a00000000100260010 - - // 534d4100 : SMA\0 signature - // 0004 : header length - // 02a0 : Tag0 type - // 0000 : Tag version - // 0001 : Group - // 0026 : payload length - // 0010 : SMA Net 2 version - - // == 606509a0 - - // 6065 : inverter protocol id - // 09 : length of long words = payload length / 4 - // a0 : control ? - - - - // ========= payload (big endian) - - // == 7a01842a71b30001 - - // 7a01 : destination model id - // 842a71b3 : destination serial number - // 0001 : destination control field - - // == 7d0042be283a0001 - - // 7d00 : source model id - // 42be283a: source serial number - // 0001 : source control field - - // == 0000 0000 0380 00020058 - - // 0000 : error code - // 0000 : fragment id - // 0380 : packet id - - // 00020058 : command id = CommandQueryDevice - // 00348200 : first register - // ff348200 : last register - - // 00000000 : end of data - - qCDebug(dcSma()) << "Inverter: Query software version..."; // The payload is little endian encoded - QByteArray payload; - QDataStream payloadStream(&payload, QIODevice::WriteOnly); - payloadStream.setByteOrder(QDataStream::LittleEndian); + buildPackage(stream, command, packetId); - // Destination - payloadStream << m_modelId << m_serialNumber << static_cast(0x1000); - - // Source - payloadStream << m_interface->sourceModelId() << m_interface->sourceSerialNumber() << static_cast(0x1000); - - // Packet information - quint16 errorCode = 0; - quint16 fragmentId = 0; - m_packetId += 1; - quint16 packetId = m_packetId | 0x8000; - payloadStream << errorCode << fragmentId << packetId; - - // Request information - payloadStream << static_cast(CommandQueryDevice); - payloadStream << static_cast(0x00823400); // Software version first - payloadStream << static_cast(0x008234ff); // Software version last + // First and second word + stream << firstWord; + stream << secondWord; // End of data - payloadStream << static_cast(0); + stream << static_cast(0); - // Build the header - QByteArray header = QByteArray::fromHex("534d4100000402a00000000100260010606509a0"); - - QByteArray data = header + payload; - m_interface->sendData(data); + // Final datagram + SpeedwireInverterRequest request; + request.setPacketId(packetId); + request.setCommand(command); + request.setRequestData(datagram); + return createReply(request); } -void SpeedwireInverter::queryDeviceType() +void SpeedwireInverter::processSoftwareVersionResponse(const QByteArray &response) { - // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00020058 001e8200 ff208200 00000000 => query device type - // Response 534d4100000402a000000001009e0010 606527a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01020058 01000000 03000000 011e8210 6f89e95f 534e3a20 33303130 35333831 31360000 00000000 00000000 00000000 00000000 - // 011f8208 6f89e95f 411f0001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 => 1f41 solar inverter - // 01208208 6f89e95f 96240000 80240000 81240001 82240000 feffff00 00000000 00000000 00000000 00000000 + // 07000000 07000000 01348200 2ff5b261 00000000 00000000 feffffff feffffff 00055302 00055302 00000000 00000000 00000000 + qCDebug(dcSma()) << "Inverter: Process software version request response" << response.toHex(); + // TODO: +// QDataStream stream(response); +// stream.setByteOrder(QDataStream::LittleEndian); + +// // First +// quint32 firstWord; +// quint32 secondWord; +// stream >> firstWord >> secondWord; +// quint8 byte1, byte2, byte3, byte4; +// stream >> byte1 >> byte2 >> byte3 >> byte4; + + // BCD + // 00 82 34 01 ?? +// QString softwareVersion = QString("%1.%2.%3.%4").arg(byte1).arg(byte2).arg(byte3).arg(byte4); +// qCDebug(dcSma()) << "Inverter: Software version" << softwareVersion; + } +void SpeedwireInverter::processDeviceTypeResponse(const QByteArray &response) +{ + // Request 534d4100000402a00000000100260010 606509a0 b500 c2c12e12 0001 7d00 43be283a 0001 0000 0000 0280 0002 0058 001e8200 ff208200 00000000 + // Response 534d4100000402a00000000100c60010 60653190 7d00 43be283a 00a1 b500 c2c12e12 0001 0000 0000 0280 0102 0058 00000000 03000000 + // 011e8210 85f2b661 534e3a20333035303534313436000000 feffff00 00000000 00000000 00000000 // SN: 305054146 + // 011f8208 85f2b661 411f 0001 421f 0000 feff ff00 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 // 411f -> 1f41 -> device type soar inverter + // 01208208 85f2b661 8a23 0000 8b23 0000 8c23 0001 8e23 0000 8f23 0000 e523 0000 e623 0000 e723 0000 // 238c -> 0001 -> STP 7000TL-20 + // 01208208 85f2b661 e923 0000 e823 0000 4124 0000 4224 0000 4324 0000 feff ff00 0000 0000 0000 0000 + // 00000000 + + // Response data: 00000000 03000000 -> first, last, then data + // 011e8210 3799b961 534e3a20 33303530 35343134 36000000 feffff00 00000000 00000000 00000000 // serialnumber as text + // 011f8208 3799b961 411f 0001 421f 0000 feff ff00 0000000000000000000000000000000000000000 // device class + // 01208208 3799b961 8a23 0000 8b23 0000 8c23 0001 8e23 0000 8f23 0000 e523 0000 e623 0000 e723 0000 // device model + // 01208208 3799b961 e923 0000 e823 0000 4124 0000 4224 0000 4324 0000 feff ff00 0000000000000000 // device model + // 00000000 // End of data + + + qCDebug(dcSma()) << "Inverter: Process device type response" << response.toHex(); + // TODO: + +} + +void SpeedwireInverter::processAcPowerResponse(const QByteArray &response) +{ + // Request 534d4100000402a00000000100260010606509a0 b500 c2c12e12 0001 7d00 52be283a 0001 0000 0000 0180 00020051 00404600 ff424600 00000000 + + // Response 534d4100000402a0000000010026001060650990 7d00 52be283a 00e1 b500 c2c12e12 0001 ffff 0000 0180 01020051 00404600 ff424600 00000000 // Error: login required + + // Response 534d4100000402a000000001007a001060651e90 7d00 52be283a 00a1 b500 c2c12e12 0001 0000 0000 0580 01020051 + // No sun + // 07000000 09000000 + // 01 4046 40 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 01 4146 40 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 01 4246 40 7503ba61 00000080 00000080 00000080 00000080 01000000 00000000 + + // Sun + // 07000000 09000000 + // 01 4046 40 77fbba61 23000000 23000000 23000000 23000000 01000000 + // 01 4146 40 77fbba61 23000000 23000000 23000000 23000000 01000000 + // 01 4246 40 77fbba61 23000000 23000000 23000000 23000000 01000000 + // 00000000 + + // 40464001 + qCDebug(dcSma()) << "Inverter: Process AC power query response"; // << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + + // Each line has 7 words + quint32 measurementId; + quint32 measurementType; // ? + + while (!stream.atEnd()) { + // First row + stream >> measurementId; + + // End of data, we are done + if (measurementId == 0) + return; + + // Unknown + stream >> measurementType; + + quint8 measurmentNumber = static_cast(measurementId & 0xff); + measurementId = measurementId & 0x00ffff00; + + // Read measurent lines + if (measurementId == 0x464000 && measurmentNumber == 0x01) { + quint32 powerAcPhase1; + stream >> powerAcPhase1; + m_powerAcPhase1 = readValue(powerAcPhase1, 1000.0); + qCDebug(dcSma()) << "Inverter: Power AC phase 1" << m_powerAcPhase1 << "W"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x464100 && measurmentNumber == 0x01) { + quint32 powerAcPhase2; + stream >> powerAcPhase2; + m_powerAcPhase2 = readValue(powerAcPhase2, 1000.0); + qCDebug(dcSma()) << "Inverter: Power AC phase 2" << m_powerAcPhase2 << "W"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x464200 && measurmentNumber == 0x01) { + quint32 powerAcPhase3; + stream >> powerAcPhase3; + m_powerAcPhase3 = readValue(powerAcPhase3, 1000.0); + qCDebug(dcSma()) << "Inverter: Power AC phase 3" << m_powerAcPhase3 << "W"; + readUntilEndOfMeasurement(stream); + } + } +} + + +void SpeedwireInverter::processAcVoltageCurrentResponse(const QByteArray &response) +{ + // No sun + // 0a000000 0f000000 + // 01 4846 00 7503ba61 ffffffff ffffffff ffffffff ffffffff 01000000 + // 01 4946 00 7503ba61 ffffffff ffffffff ffffffff ffffffff 01000000 + // 01 4a46 00 7503ba61 ffffffff ffffffff ffffffff ffffffff 01000000 + // 01 5046 00 7503ba61 ffffffff ffffffff ffffffff ffffffff 01000000 + // 01 5146 00 7503ba61 ffffffff ffffffff ffffffff ffffffff 01000000 + // 01 5246 00 7503ba61 ffffffff ffffffff ffffffff ffffffff 01000000 + // 00000000 + + // Sun + // 0a000000 0f000000 + // 01484600 c1f0ba61 f9580000 f9580000 f9580000 f9580000 01000000 + // 01494600 c1f0ba61 ff580000 ff580000 ff580000 ff580000 01000000 + // 014a4600 c1f0ba61 02590000 02590000 02590000 02590000 01000000 + // 01504600 c1f0ba61 00000000 00000000 00000000 00000000 01000000 + // 01514600 c1f0ba61 00000000 00000000 00000000 00000000 01000000 + // 01524600 c1f0ba61 00000000 00000000 00000000 00000000 01000000 + // 00000000 + + qCDebug(dcSma()) << "Inverter: Process AC voltage/current query response"; // << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + + // Each line has 7 words + quint32 measurementId; + quint32 measurementType; // ? + + while (!stream.atEnd()) { + // First row + stream >> measurementId; + + // End of data, we are done + if (measurementId == 0) + return; + + // Unknown + stream >> measurementType; + + quint8 measurmentNumber = static_cast(measurementId & 0xff); + measurementId = measurementId & 0x00ffff00; + + // Read measurent lines + if (measurementId == 0x464800 && measurmentNumber == 0x01) { + quint32 voltageAcPhase1; + stream >> voltageAcPhase1; + m_voltageAcPhase1 = readValue(voltageAcPhase1, 100.0); + qCDebug(dcSma()) << "Inverter: Voltage AC phase 1" << m_voltageAcPhase1 << "V"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x464900 && measurmentNumber == 0x01) { + quint32 voltageAcPhase2; + stream >> voltageAcPhase2; + m_voltageAcPhase2 = readValue(voltageAcPhase2, 100.0); + qCDebug(dcSma()) << "Inverter: Voltage AC phase 2" << m_voltageAcPhase2 << "V"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x464a00 && measurmentNumber == 0x01) { + quint32 voltageAcPhase3; + stream >> voltageAcPhase3; + m_voltageAcPhase3 = readValue(voltageAcPhase3, 100.0); + qCDebug(dcSma()) << "Inverter: Voltage AC phase 3" << m_voltageAcPhase3 << "V"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x465000 && measurmentNumber == 0x01) { + quint32 currentAcPhase1; + stream >> currentAcPhase1; + m_currentAcPhase1 = readValue(currentAcPhase1, 1000.0); + qCDebug(dcSma()) << "Inverter: Current AC phase 1" << m_currentAcPhase1 << "A"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x465100 && measurmentNumber == 0x01) { + quint32 currentAcPhase2; + stream >> currentAcPhase2; + m_currentAcPhase2 = readValue(currentAcPhase2, 1000.0); + qCDebug(dcSma()) << "Inverter: Current AC phase 2" << m_currentAcPhase2 << "A"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x465200 && measurmentNumber == 0x01) { + quint32 currentAcPhase3; + stream >> currentAcPhase3; + m_currentAcPhase3 = readValue(currentAcPhase3, 1000.0); + qCDebug(dcSma()) << "Inverter: Current AC phase 3" << m_currentAcPhase3 << "A"; + readUntilEndOfMeasurement(stream); + } + } +} + +void SpeedwireInverter::processAcTotalPowerResponse(const QByteArray &response) +{ + // 00000000 00000000 + // 013f2640 8606bb61 16010000 16010000 16010000 16010000 01000000 + // 00000000 + qCDebug(dcSma()) << "Inverter: Process AC total power query response"; // << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + + // Each line has 7 words + quint32 measurementId; + quint32 measurementType; // ? + + while (!stream.atEnd()) { + // First row + stream >> measurementId; + + // End of data, we are done + if (measurementId == 0) + return; + + // Unknown + stream >> measurementType; + + quint8 measurmentNumber = static_cast(measurementId & 0xff); + measurementId = measurementId & 0x00ffff00; + + // Read measurent lines + if (measurementId == 0x263f00 && measurmentNumber == 0x01) { + quint32 totalAcPower; + stream >> totalAcPower; + m_totalAcPower = readValue(totalAcPower, 1000.0); + qCDebug(dcSma()) << "Inverter: Total AC power" << m_totalAcPower << "W"; + readUntilEndOfMeasurement(stream); + } + } +} + + +void SpeedwireInverter::processDcPowerResponse(const QByteArray &response) +{ + // No sun + // 00000000 01000000 + // 011e2540 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 021e2540 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 00000000 + + // Sun + // 00000000 01000000 + // 011e2540 7b0dbb61 8a000000 8a000000 8a000000 8a000000 01000000 + // 021e2540 7b0dbb61 8f000000 8f000000 8f000000 8f000000 01000000 + // 00000000 + qCDebug(dcSma()) << "Inverter: Process DC power query response"; // << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + + // Each line has 7 words + quint32 measurementId; + quint32 measurementType; // ? + + while (!stream.atEnd()) { + // First row + stream >> measurementId; + + // End of data, we are done + if (measurementId == 0) + return; + + // Unknown + stream >> measurementType; + + quint8 measurmentNumber = static_cast(measurementId & 0xff); + measurementId = measurementId & 0x00ffff00; + + // Read measurent lines + if (measurementId == 0x251e00 && measurmentNumber == 0x01) { + quint32 powerMpp1; + stream >> powerMpp1; + m_powerDcMpp1 = readValue(powerMpp1, 1000.0); + qCDebug(dcSma()) << "Inverter: DC power MPP1" << m_powerDcMpp1 << "W"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x251e00 && measurmentNumber == 0x02) { + quint32 powerMpp2; + stream >> powerMpp2; + m_powerDcMpp2 = readValue(powerMpp2, 1000.0); + qCDebug(dcSma()) << "Inverter: DC power MPP2" << m_powerDcMpp2 << "W"; + readUntilEndOfMeasurement(stream); + } + } +} + +void SpeedwireInverter::processDcVoltageCurrentResponse(const QByteArray &response) +{ + // 02000000 05000000 + // 011f4540 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 021f4540 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 01214540 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 02214540 7503ba61 00000080 00000080 00000080 00000080 01000000 + // 00000000 + + // 02000000 05000000 + // 011f4540 160ebb61 009a0000 009a0000 009a0000 009a0000 01000000 + // 021f4540 160ebb61 02a00000 02a00000 02a00000 02a00000 01000000 + // 01214540 160ebb61 76010000 76010000 76010000 76010000 01000000 + // 02214540 160ebb61 77010000 77010000 77010000 77010000 01000000 + // 00000000 + + qCDebug(dcSma()) << "Inverter: Process DC voltage/current response"; // << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + + // Each line has 7 words + quint32 measurementId; + quint32 measurementType; // ? + + while (!stream.atEnd()) { + // First row + stream >> measurementId; + + // End of data, we are done + if (measurementId == 0) + return; + + // Unknown + stream >> measurementType; + + quint8 measurmentNumber = static_cast(measurementId & 0xff); + measurementId = measurementId & 0x00ffff00; + + // Read measurent lines + if (measurementId == 0x451f00 && measurmentNumber == 0x01) { + quint32 voltageMpp1; + stream >> voltageMpp1; + m_voltageDcMpp1 = readValue(voltageMpp1, 100.0); + qCDebug(dcSma()) << "Inverter: DC voltage MPP1" << m_voltageDcMpp1 << "V"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x451e00 && measurmentNumber == 0x02) { + quint32 voltageMpp2; + stream >> voltageMpp2; + m_voltageDcMpp2 = readValue(voltageMpp2, 100.0); + qCDebug(dcSma()) << "Inverter: DC voltage MPP2" << m_voltageDcMpp2 << "V"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x452100 && measurmentNumber == 0x01) { + quint32 currentMpp1; + stream >> currentMpp1; + m_currentDcMpp1 = readValue(currentMpp1, 1000.0); + qCDebug(dcSma()) << "Inverter: DC current MPP1" << m_currentDcMpp1 << "A"; + readUntilEndOfMeasurement(stream); + } else if (measurementId == 0x452100 && measurmentNumber == 0x02) { + quint32 currentMpp2; + stream >> currentMpp2; + m_currentDcMpp2 = readValue(currentMpp2, 1000.0); + qCDebug(dcSma()) << "Inverter: DC current MPP2" << m_currentDcMpp2 << "A"; + readUntilEndOfMeasurement(stream); + } + } +} + +void SpeedwireInverter::processEnergyProductionResponse(const QByteArray &response) +{ + // 00000000 01000000 + // 01012600 6f03ba61 4d147402 0000000 00122260 01523ba6 17c05000 00000000 + // 000000000 + + qCDebug(dcSma()) << "Inverter: Process energy production response";// << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + quint32 dataId, dataType; + stream >> dataId >> dataType; + + quint32 unknwonWord; + quint32 totalEnergy, todayEnergy; + stream >> totalEnergy >> unknwonWord >> unknwonWord >> unknwonWord >> todayEnergy; + + m_todayEnergyProduced = readValue(todayEnergy, 1000.0); + m_totalEnergyProduced = readValue(totalEnergy, 1000.0); + qCDebug(dcSma()) << "Inverter: Energy total:" << m_totalEnergyProduced << "kWh"; + qCDebug(dcSma()) << "Inverter: Energy today:" << m_todayEnergyProduced << "kWh"; +} + +void SpeedwireInverter::processGridFrequencyResponse(const QByteArray &response) +{ + // 10000000 10000000 + // 01574600 c20cbb61 89130000 89130000 89130000 89130000 010000000 + // 0000000 + qCDebug(dcSma()) << "Inverter: Process grid frequency response"; // << response.toHex(); + QDataStream stream(response); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 firstWord, secondWord; + stream >> firstWord >> secondWord; + + // Each line has 7 words + quint32 measurementId; + quint32 measurementType; // ? + + while (!stream.atEnd()) { + // First row + stream >> measurementId; + + // End of data, we are done + if (measurementId == 0) + return; + + // Unknown + stream >> measurementType; + + quint8 measurmentNumber = static_cast(measurementId & 0xff); + measurementId = measurementId & 0x00ffff00; + + // Read measurent lines + if (measurementId == 0x465700 && measurmentNumber == 0x01) { + quint32 frequency; + stream >> frequency; + m_gridFrequency = readValue(frequency, 100.0); + qCDebug(dcSma()) << "Inverter: Grid frequency" << m_gridFrequency << "Hz"; + readUntilEndOfMeasurement(stream); + } + } +} + +void SpeedwireInverter::processInverterStatusResponse(const QByteArray &response) +{ + // 00000000 00000000 + // 01482108 b527bb61 23000000 2f010000 33010001 c7010000 feffff00 00000000 00000000 00000000 + // 00000000 + qCDebug(dcSma()) << "Inverter: Process inverter status response" << response.toHex(); + // TODO: +} + +void SpeedwireInverter::readUntilEndOfMeasurement(QDataStream &stream) +{ + // Read until end of line (0x01000000) + quint32 word; + while (!stream.atEnd()) { + stream >> word; + if (word == 1) { + return; + } + } +} + +double SpeedwireInverter::readValue(quint32 value, double divisor) +{ + if (value == 0x80000000 || value == 0xffffffff) + return 0; + + return value / divisor; +} + +void SpeedwireInverter::setReachable(bool reachable) +{ + if (m_reachable == reachable) + return; + + m_reachable = reachable; + emit reachableChanged(m_reachable); +} void SpeedwireInverter::processData(const QByteArray &data) { - qCDebug(dcSma()) << "Inverter: data received" << data.toHex(); + if (data.size() < 18) { + qCDebug(dcSma()) << "Inverter: The received datagram is to short to be a SMA speedwire message. Ignoring data..."; + return; + } + QDataStream stream(data); + Speedwire::Header header = Speedwire::parseHeader(stream); + if (!header.isValid()) { + qCWarning(dcSma()) << "Inverter: Datagram header is not valid. Ignoring data..."; + return; + } + + if (header.protocolId != Speedwire::ProtocolIdInverter) { + qCWarning(dcSma()) << "Inverter: Received datagram from different protocol" << header.protocolId << "Ignoring data..."; + return; + } + + Speedwire::InverterPackage package = Speedwire::parseInverterPackage(stream); + if (package.sourceModelId != m_modelId || package.sourceSerialNumber != m_serialNumber) { + qCWarning(dcSma()) << "Inverter: Received datagram from different inverter" << package.sourceSerialNumber << "Ignoring data..."; + return; + } + + qCDebug(dcSma()) << "Inverter: <-- Received" << static_cast(package.command) << "Packet ID:" << package.packetId; + //qCDebug(dcSma()) << "Inverter:" << data.toHex(); + if (m_currentReply && m_currentReply->request().packetId() == package.packetId) { + qCDebug(dcSma()) << "Inverter: Received response for current reply" << static_cast(m_currentReply->request().command()) << "Packet ID:" << m_currentReply->request().packetId(); + m_currentReply->m_responseData = data; + m_currentReply->m_responseHeader = header; + m_currentReply->m_responsePackage = package; + // Set the payload + while (!stream.atEnd()) { + quint8 byte; + stream >> byte; + m_currentReply->m_responsePayload.append(byte); + } + + if (package.errorCode != 0) { + m_currentReply->finishReply(SpeedwireInverterReply::ErrorInverterError); + } else { + m_currentReply->finishReply(SpeedwireInverterReply::ErrorNoError); + } + } else { + if (m_currentReply) { + qCWarning(dcSma()) << "Inverter: Received unexpected data: waiting for" << static_cast(m_currentReply->request().command()) << "Packet ID:" << m_currentReply->request().packetId(); + } else { + qCWarning(dcSma()) << "Inverter: Received unexpected data: not waiting for any response."; + } + qCWarning(dcSma()) << "Inverter:" << header; + qCWarning(dcSma()) << "Inverter:" << package; + qCWarning(dcSma()) << "Inverter:" << data.toHex(); + } +} + +void SpeedwireInverter::onReplyTimeout() +{ + SpeedwireInverterReply *reply = qobject_cast(sender()); + qCDebug(dcSma()) << "Inverter: Reply timeout" << reply->request().packetId() << reply->request().command(); + reply->m_retries += 1; + if (reply->m_retries <= reply->m_maxRetries) { + qCDebug(dcSma()) << "Inverter: Resend request" << reply->m_retries << "/" << reply->m_maxRetries; + m_replyQueue.prepend(reply); + m_currentReply = nullptr; + sendNextReply(); + } else { + if (reply->m_maxRetries == 0) { + qCWarning(dcSma()) << "Inverter: No response received for request. Finish reply with" << SpeedwireInverterReply::ErrorTimeout; + } else { + qCWarning(dcSma()) << "Inverter: No response received for request after" << reply->m_maxRetries << "attempts. Finish reply with" << SpeedwireInverterReply::ErrorTimeout; + } + // Finish with timeout error + reply->finishReply(SpeedwireInverterReply::ErrorTimeout); + } +} + +void SpeedwireInverter::onReplyFinished() +{ + SpeedwireInverterReply *reply = qobject_cast(sender()); + if (m_currentReply == reply) { + // Note: the reply is self deleting on finished + m_currentReply = nullptr; + sendNextReply(); + } +} + +void SpeedwireInverter::setState(State state) +{ + if (m_state == state) + return; + + qCDebug(dcSma()) << "Inverter: State changed" << state; + m_state = state; + emit stateChanged(m_state); + + switch (m_state) { + case StateIdle: + break; + case StateDisconnected: + setReachable(false); + break; + case StateInitializing: { + // Try to fetch ac power + qCDebug(dcSma()) << "Inverter: Request AC power..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryAc, 0x00464000, 0x004642ff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + if (reply->error() == SpeedwireInverterReply::ErrorTimeout) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + if (reply->error() == SpeedwireInverterReply::ErrorInverterError) { + qCDebug(dcSma()) << "Inverter: Query data request finished with inverter error. Try to login..."; + setState(StateLogin); + return; + } + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processAcPowerResponse(reply->responseData()); + + if (deviceInformationFetched) { + setState(StateQueryData); + } else { + setState(StateGetInformation); + } + }); + break; + } + + case StateLogin: { + setState(StateLogin); + SpeedwireInverterReply *loginReply = sendLoginRequest(); + connect(loginReply, &SpeedwireInverterReply::finished, this, [=](){ + if (loginReply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to login to inverter:" << loginReply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Login request finished successfully."; + setReachable(true); + + // Logged in successfully, reinit the data fetch process + setState(StateInitializing); + }); + break; + } + case StateGetInformation: { + SpeedwireInverterReply *softwareVersionReply = sendSoftwareVersionRequest(); + connect(softwareVersionReply, &SpeedwireInverterReply::finished, this, [=](){ + if (softwareVersionReply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to get software version from inverter:" << softwareVersionReply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Software version request finished successfully."; + processSoftwareVersionResponse(softwareVersionReply->responsePayload()); + + SpeedwireInverterReply *deviceTypeReply = sendDeviceTypeRequest(); + connect(deviceTypeReply, &SpeedwireInverterReply::finished, this, [=](){ + if (deviceTypeReply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to get device information from inverter:" << deviceTypeReply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Get device information finished successfully."; + processDeviceTypeResponse(deviceTypeReply->responsePayload()); + deviceInformationFetched = true; + setState(StateQueryData); + }); + }); + break; + } + case StateQueryData: { + // Query inverter status + qCDebug(dcSma()) << "Inverter: Request inverter status..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryStatus, 0x00214800, 0x002148FF); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to get status from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Get inverter status request finished successfully" << reply->request().command(); + processInverterStatusResponse(reply->responsePayload()); + + // Query AC voltage / current + qCDebug(dcSma()) << "Inverter: Request AC voltage and current..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryAc, 0x00464800, 0x004655ff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processAcVoltageCurrentResponse(reply->responsePayload()); + + // Query DC power + qCDebug(dcSma()) << "Inverter: Request DC power..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryDc, 0x00251e00, 0x00251eff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processDcPowerResponse(reply->responsePayload()); + + // Query DC voltage/current + qCDebug(dcSma()) << "Inverter: Request DC voltage and current..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryDc, 0x00451f00, 0x004521ff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processDcVoltageCurrentResponse(reply->responsePayload()); + + // Query energy production + qCDebug(dcSma()) << "Inverter: Request energy production..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryEnergy, 0x00260100, 0x002622ff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processEnergyProductionResponse(reply->responsePayload()); + + // Query total AC power + qCDebug(dcSma()) << "Inverter: Request total AC power..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryAc, 0x00263f00, 0x00263fff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processAcTotalPowerResponse(reply->responsePayload()); + + // Query grid frequency + qCDebug(dcSma()) << "Inverter: Request grid frequency..."; + SpeedwireInverterReply *reply = sendQueryRequest(Speedwire::CommandQueryAc, 0x00465700, 0x004657ff); + connect(reply, &SpeedwireInverterReply::finished, this, [=](){ + if (reply->error() != SpeedwireInverterReply::ErrorNoError) { + qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + setState(StateDisconnected); + return; + } + + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); + processGridFrequencyResponse(reply->responsePayload()); + + setReachable(true); + emit valuesUpdated(); + setState(StateIdle); + }); + }); + }); + }); + }); + }); + }); + break; + } + } } diff --git a/sma/speedwireinverter.h b/sma/speedwireinverter.h index 2d1c3ee5..518baf07 100644 --- a/sma/speedwireinverter.h +++ b/sma/speedwireinverter.h @@ -1,56 +1,114 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef SPEEDWIREINVERTER_H #define SPEEDWIREINVERTER_H #include +#include +#include "speedwire.h" #include "speedwireinterface.h" +#include "speedwireinverterreply.h" +#include "speedwireinverterrequest.h" class SpeedwireInverter : public QObject { Q_OBJECT public: - enum Command { - CommandQueryAc = 0x51000200, - CommandQueryStatus = 0x51800200, - CommandQueryDevice = 0x58000200, - CommandQueryDc = 0x53800200, - CommandQueryLogin = 0xfffd040c + enum State { + StateIdle, + StateDisconnected, + StateInitializing, + StateLogin, + StateGetInformation, + StateQueryData }; - - - Q_ENUM(Command) - - - class Request - { - public: - Request(); - - SpeedwireInverter::Command command() const; - - quint16 requestId() const; - - private: - SpeedwireInverter::Command m_command; - quint16 m_requestId = 0; - - }; - + Q_ENUM(State) explicit SpeedwireInverter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent = nullptr); bool initialize(); bool initialized() const; - double currentPower() const; + State state() const; + + bool reachable() const; + + Speedwire::DeviceClass deviceClass() const; + QString modelName() const; + + double totalAcPower() const; + + double gridFrequency() const; + double totalEnergyProduced() const; + double todayEnergyProduced() const; + + double voltageAcPhase1() const; + double voltageAcPhase2() const; + double voltageAcPhase3() const; + + double currentAcPhase1() const; + double currentAcPhase2() const; + double currentAcPhase3() const; + + double powerAcPhase1() const; + double powerAcPhase2() const; + double powerAcPhase3() const; + + double powerDcMpp1() const; + double powerDcMpp2() const; + + double voltageDcMpp1() const; + double voltageDcMpp2() const; + + double currentDcMpp1() const; + double currentDcMpp2() const; // Query methods - void sendLoginRequest(const QString &password = "0000", bool loginAsUser = true); - void querySoftwareVersion(); - void queryDeviceType(); + SpeedwireInverterReply *sendIdentifyRequest(); + SpeedwireInverterReply *sendLoginRequest(const QString &password = "0000", bool loginAsUser = true); + SpeedwireInverterReply *sendLogoutRequest(); + SpeedwireInverterReply *sendSoftwareVersionRequest(); + SpeedwireInverterReply *sendDeviceTypeRequest(); + + // Start connecting + void startConnecting(); + +public slots: + void refresh(); signals: + void reachableChanged(bool reachable); + void stateChanged(State state); void valuesUpdated(); private: @@ -59,17 +117,83 @@ private: bool m_initialized = false; quint16 m_modelId = 0; quint32 m_serialNumber = 0; - quint16 m_packetId = 0; + + bool m_reachable = false; + State m_state = StateDisconnected; + quint16 m_packetId = 1; + + bool deviceInformationFetched = false; + + SpeedwireInverterReply *m_currentReply = nullptr; + QQueue m_replyQueue; // Properties - double m_currentPower = 0; - double m_totalEnergyProduced = 0; - + Speedwire::DeviceClass m_deviceClass = Speedwire::DeviceClassUnknown; + QString m_modelName; QString m_softwareVersion; + double m_totalAcPower = 0; + double m_totalEnergyProduced = 0; + double m_todayEnergyProduced = 0; + + double m_gridFrequency = 0; + + double m_voltageAcPhase1 = 0; + double m_voltageAcPhase2 = 0; + double m_voltageAcPhase3 = 0; + + double m_currentAcPhase1 = 0; + double m_currentAcPhase2 = 0; + double m_currentAcPhase3 = 0; + + double m_powerAcPhase1 = 0; + double m_powerAcPhase2 = 0; + double m_powerAcPhase3 = 0; + + double m_powerDcMpp1 = 0; + double m_powerDcMpp2 = 0; + + double m_voltageDcMpp1 = 0; + double m_voltageDcMpp2 = 0; + + double m_currentDcMpp1 = 0; + double m_currentDcMpp2 = 0; + + void setState(State state); + + void sendNextReply(); + SpeedwireInverterReply *createReply(const SpeedwireInverterRequest &request); + + // Request builder function + void buildDefaultHeader(QDataStream &stream, quint16 payloadSize = 38, quint8 control = 0xa0); + void buildPackage(QDataStream &stream, quint32 command, quint16 packetId); + + // Send generic request for internal use + SpeedwireInverterReply *sendQueryRequest(Speedwire::Command command, quint32 firstWord, quint32 secondWord); + + // Response process methods + void processSoftwareVersionResponse(const QByteArray &response); + void processDeviceTypeResponse(const QByteArray &response); + void processAcPowerResponse(const QByteArray &response); + void processAcVoltageCurrentResponse(const QByteArray &response); + void processAcTotalPowerResponse(const QByteArray &response); + void processDcPowerResponse(const QByteArray &response); + void processDcVoltageCurrentResponse(const QByteArray &response); + void processEnergyProductionResponse(const QByteArray &response); + void processGridFrequencyResponse(const QByteArray &response); + void processInverterStatusResponse(const QByteArray &response); + + void readUntilEndOfMeasurement(QDataStream &stream); + double readValue(quint32 value, double divisor); + + void setReachable(bool reachable); + private slots: void processData(const QByteArray &data); + void onReplyTimeout(); + void onReplyFinished(); + }; #endif // SPEEDWIREINVERTER_H diff --git a/sma/speedwireinverterreply.cpp b/sma/speedwireinverterreply.cpp new file mode 100644 index 00000000..e65e0f50 --- /dev/null +++ b/sma/speedwireinverterreply.cpp @@ -0,0 +1,85 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "speedwireinverterreply.h" +#include "extern-plugininfo.h" + +SpeedwireInverterReply::SpeedwireInverterReply(const SpeedwireInverterRequest &request, QObject *parent) : + QObject(parent), + m_request(request) +{ + m_maxRetries = m_request.retries(); + + m_timer.setInterval(m_timeout); + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, this, &SpeedwireInverterReply::timeout); +} + +SpeedwireInverterRequest SpeedwireInverterReply::request() const +{ + return m_request; +} + +SpeedwireInverterReply::Error SpeedwireInverterReply::error() const +{ + return m_error; +} + +QByteArray SpeedwireInverterReply::responseData() const +{ + return m_responseData; +} + +Speedwire::Header SpeedwireInverterReply::responseHeader() const +{ + return m_responseHeader; +} + +Speedwire::InverterPackage SpeedwireInverterReply::responsePackage() const +{ + return m_responsePackage; +} + +QByteArray SpeedwireInverterReply::responsePayload() const +{ + return m_responsePayload; +} + +void SpeedwireInverterReply::startWaiting() +{ + m_timer.start(); +} + +void SpeedwireInverterReply::finishReply(Error error) +{ + m_timer.stop(); + m_error = error; + emit finished(); +} diff --git a/sma/speedwireinverterreply.h b/sma/speedwireinverterreply.h new file mode 100644 index 00000000..f2f8078f --- /dev/null +++ b/sma/speedwireinverterreply.h @@ -0,0 +1,89 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SPEEDWIREINVERTERREPLY_H +#define SPEEDWIREINVERTERREPLY_H + +#include +#include + +#include "speedwireinverterrequest.h" + +class SpeedwireInverterReply : public QObject +{ + Q_OBJECT + + friend class SpeedwireInverter; + +public: + enum Error { + ErrorNoError, // Response on, no error + ErrorInverterError, // Inverter returned error + ErrorTimeout // Request timeouted + }; + Q_ENUM(Error) + + // Request + SpeedwireInverterRequest request() const; + + Error error() const; + + // Response + QByteArray responseData() const; + Speedwire::Header responseHeader() const; + Speedwire::InverterPackage responsePackage() const; + QByteArray responsePayload() const; + +signals: + void finished(); + void timeout(); + +private: + explicit SpeedwireInverterReply(const SpeedwireInverterRequest &request, QObject *parent = nullptr); + + QTimer m_timer; + Error m_error = ErrorNoError; + SpeedwireInverterRequest m_request; + quint8 m_retries = 0; + quint8 m_maxRetries = 3; + int m_timeout = 3000; + + QByteArray m_responseData; + Speedwire::Header m_responseHeader; + Speedwire::InverterPackage m_responsePackage; + QByteArray m_responsePayload; + + + void finishReply(Error error); + void startWaiting(); + +}; + +#endif // SPEEDWIREINVERTERREPLY_H diff --git a/sma/speedwireinverterrequest.cpp b/sma/speedwireinverterrequest.cpp new file mode 100644 index 00000000..fd1befb8 --- /dev/null +++ b/sma/speedwireinverterrequest.cpp @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "speedwireinverterrequest.h" + +SpeedwireInverterRequest::SpeedwireInverterRequest() +{ + +} + +Speedwire::Command SpeedwireInverterRequest::command() const +{ + return m_command; +} + +void SpeedwireInverterRequest::setCommand(Speedwire::Command command) +{ + m_command = command; +} + +quint16 SpeedwireInverterRequest::packetId() const +{ + return m_packetId; +} + +void SpeedwireInverterRequest::setPacketId(quint16 packetId) +{ + m_packetId = packetId; +} + +QByteArray SpeedwireInverterRequest::requestData() const +{ + return m_requestData; +} + +void SpeedwireInverterRequest::setRequestData(const QByteArray &requestData) +{ + m_requestData = requestData; +} + +quint8 SpeedwireInverterRequest::retries() const +{ + return m_retries; +} + +void SpeedwireInverterRequest::setRetries(quint8 retries) +{ + m_retries = retries; +} diff --git a/sma/speedwireinverterrequest.h b/sma/speedwireinverterrequest.h new file mode 100644 index 00000000..bd6cb22a --- /dev/null +++ b/sma/speedwireinverterrequest.h @@ -0,0 +1,62 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SPEEDWIREINVERTERREQUEST_H +#define SPEEDWIREINVERTERREQUEST_H + +#include + +#include "speedwire.h" + +class SpeedwireInverterRequest +{ +public: + explicit SpeedwireInverterRequest(); + + Speedwire::Command command() const; + void setCommand(Speedwire::Command command); + + quint16 packetId() const; + void setPacketId(quint16 packetId); + + QByteArray requestData() const; + void setRequestData(const QByteArray &requestData); + + quint8 retries() const; + void setRetries(quint8 retries); + +private: + Speedwire::Command m_command; + quint16 m_packetId = 0; + QByteArray m_requestData; + quint8 m_retries = 2; // Default try 2 times before timeout +}; + +#endif // SPEEDWIREINVERTERREQUEST_H diff --git a/sma/speedwiremeter.cpp b/sma/speedwiremeter.cpp index b460bd63..152ae871 100644 --- a/sma/speedwiremeter.cpp +++ b/sma/speedwiremeter.cpp @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "speedwiremeter.h" #include "extern-plugininfo.h" @@ -9,11 +39,20 @@ SpeedwireMeter::SpeedwireMeter(const QHostAddress &address, quint16 modelId, qui { m_interface = new SpeedwireInterface(m_address, true, this); connect(m_interface, &SpeedwireInterface::dataReceived, this, &SpeedwireMeter::processData); + + // Reachable timestamp + m_timer.setInterval(5000); + m_timer.setSingleShot(false); + connect(&m_timer, &QTimer::timeout, this, &SpeedwireMeter::evaluateReachable); } bool SpeedwireMeter::initialize() { - return m_interface->initialize(); + bool initSuccess = m_interface->initialize(); + if (initSuccess) + m_timer.start(); + + return initSuccess; } bool SpeedwireMeter::initialized() const @@ -21,6 +60,11 @@ bool SpeedwireMeter::initialized() const return m_interface->initialized(); } +bool SpeedwireMeter::reachable() const +{ + return m_reachable; +} + double SpeedwireMeter::currentPower() const { return m_currentPower; @@ -116,19 +160,45 @@ QString SpeedwireMeter::softwareVersion() const return m_softwareVersion; } +void SpeedwireMeter::evaluateReachable() +{ + // Note: the meter sends every second the data on the multicast + qint64 currentTimestamp = QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000; + // If the meter has not sent data within the last 5 seconds it seems not to be reachable + bool reachable = false; + if (currentTimestamp - m_lastSeenTimestamp < 10) { + reachable = true; + } + + if (m_reachable != reachable) { + qCDebug(dcSma()) << "Meter: reachable changed to" << reachable; + m_reachable = reachable; + emit reachableChanged(m_reachable); + } + + // Restart the timer + if (m_reachable) { + m_timer.start(); + } else { + // Reachable will be triggered automatically once data arrives + // No need to run the timer all the time + m_timer.stop(); + } +} + void SpeedwireMeter::processData(const QByteArray &data) { qCDebug(dcSma()) << "Meter: data received" << data.toHex(); QDataStream stream(data); stream.setByteOrder(QDataStream::BigEndian); - SpeedwireInterface::SpeedwireHeader header = SpeedwireInterface::parseHeader(stream); + Speedwire::Header header = Speedwire::parseHeader(stream); if (!header.isValid()) { qCDebug(dcSma()) << "Meter: Datagram header is not valid. Ignoring data..."; return; } - if (header.protocolId != SpeedwireInterface::ProtocolIdMeter) { + if (header.protocolId != Speedwire::ProtocolIdMeter) { qCDebug(dcSma()) << "Meter: received header protocol which is not from the meter protocol. Ignoring data..."; return; } @@ -266,5 +336,9 @@ void SpeedwireMeter::processData(const QByteArray &data) } } + // Save the current timestamp for reachable evaluation + m_lastSeenTimestamp = QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000; + evaluateReachable(); + emit valuesUpdated(); } diff --git a/sma/speedwiremeter.h b/sma/speedwiremeter.h index 82f251c9..0599cf3b 100644 --- a/sma/speedwiremeter.h +++ b/sma/speedwiremeter.h @@ -1,7 +1,39 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef SPEEDWIREMETER_H #define SPEEDWIREMETER_H #include +#include +#include #include "speedwireinterface.h" @@ -14,6 +46,8 @@ public: bool initialize(); bool initialized() const; + bool reachable() const; + double currentPower() const; double totalEnergyProduced() const; double totalEnergyConsumed() const; @@ -42,6 +76,7 @@ public: signals: + void reachableChanged(bool reachable); void valuesUpdated(); private: @@ -51,6 +86,10 @@ private: quint16 m_modelId = 0; quint32 m_serialNumber = 0; + QTimer m_timer; + bool m_reachable = false; + qint64 m_lastSeenTimestamp = 0; + double m_currentPower = 0; double m_totalEnergyProduced = 0; double m_totalEnergyConsumed = 0; @@ -77,7 +116,9 @@ private: QString m_softwareVersion; + private slots: + void evaluateReachable(); void processData(const QByteArray &data); }; From 07d6bfc77064802872f7aac06a563eaf0d05fce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 14 Jan 2022 12:25:28 +0100 Subject: [PATCH 06/17] Fix power measurment divisor --- sma/speedwireinverter.cpp | 6 +++--- sma/speedwireinverter.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sma/speedwireinverter.cpp b/sma/speedwireinverter.cpp index 24c90b06..ef3fc805 100644 --- a/sma/speedwireinverter.cpp +++ b/sma/speedwireinverter.cpp @@ -739,7 +739,7 @@ void SpeedwireInverter::processAcTotalPowerResponse(const QByteArray &response) if (measurementId == 0x263f00 && measurmentNumber == 0x01) { quint32 totalAcPower; stream >> totalAcPower; - m_totalAcPower = readValue(totalAcPower, 1000.0); + m_totalAcPower = readValue(totalAcPower); qCDebug(dcSma()) << "Inverter: Total AC power" << m_totalAcPower << "W"; readUntilEndOfMeasurement(stream); } @@ -788,13 +788,13 @@ void SpeedwireInverter::processDcPowerResponse(const QByteArray &response) if (measurementId == 0x251e00 && measurmentNumber == 0x01) { quint32 powerMpp1; stream >> powerMpp1; - m_powerDcMpp1 = readValue(powerMpp1, 1000.0); + m_powerDcMpp1 = readValue(powerMpp1); qCDebug(dcSma()) << "Inverter: DC power MPP1" << m_powerDcMpp1 << "W"; readUntilEndOfMeasurement(stream); } else if (measurementId == 0x251e00 && measurmentNumber == 0x02) { quint32 powerMpp2; stream >> powerMpp2; - m_powerDcMpp2 = readValue(powerMpp2, 1000.0); + m_powerDcMpp2 = readValue(powerMpp2); qCDebug(dcSma()) << "Inverter: DC power MPP2" << m_powerDcMpp2 << "W"; readUntilEndOfMeasurement(stream); } diff --git a/sma/speedwireinverter.h b/sma/speedwireinverter.h index 518baf07..8750202b 100644 --- a/sma/speedwireinverter.h +++ b/sma/speedwireinverter.h @@ -184,7 +184,7 @@ private: void processInverterStatusResponse(const QByteArray &response); void readUntilEndOfMeasurement(QDataStream &stream); - double readValue(quint32 value, double divisor); + double readValue(quint32 value, double divisor = 1.0); void setReachable(bool reachable); From 4b3b20c5731e6fb44bb85083bf794ab346b7ba9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 17 Jan 2022 09:43:28 +0100 Subject: [PATCH 07/17] Update translations --- ...42bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts | 563 ++++++++++++++++-- 1 file changed, 523 insertions(+), 40 deletions(-) diff --git a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts index 6d411ddd..5f1fa2ba 100644 --- a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts +++ b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts @@ -4,46 +4,235 @@ IntegrationPluginSma - + Unable to discover devices in your network. + + + + Unable to discover the network. + + sma - - + + + + + + Connected - The name of the ParamType (ThingClass: sunnyWebBox, EventType: connected, ID: {c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) + The name of the ParamType (ThingClass: speedwireInverter, EventType: connected, ID: {aaff72c3-c70a-4a2f-bed1-89f38cebe442}) +---------- +The name of the StateType ({aaff72c3-c70a-4a2f-bed1-89f38cebe442}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: connected, ID: {35733d27-4fe0-439a-be71-7c1597481659}) +---------- +The name of the StateType ({35733d27-4fe0-439a-be71-7c1597481659}) of ThingClass speedwireMeter +---------- +The name of the ParamType (ThingClass: sunnyWebBox, EventType: connected, ID: {c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) ---------- The name of the StateType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox - + + + Connected changed - The name of the EventType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox + The name of the EventType ({aaff72c3-c70a-4a2f-bed1-89f38cebe442}) of ThingClass speedwireInverter +---------- +The name of the EventType ({35733d27-4fe0-439a-be71-7c1597481659}) of ThingClass speedwireMeter +---------- +The name of the EventType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox - - + + + + + Current phase A + The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPhaseA, ID: {2a6c59ca-853a-47d6-96fb-0c85edf32f52}) +---------- +The name of the StateType ({2a6c59ca-853a-47d6-96fb-0c85edf32f52}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPhaseA, ID: {45bbdbef-1832-4870-bff5-299e580fb4da}) +---------- +The name of the StateType ({45bbdbef-1832-4870-bff5-299e580fb4da}) of ThingClass speedwireMeter + + + + + + Current phase A changed + The name of the EventType ({2a6c59ca-853a-47d6-96fb-0c85edf32f52}) of ThingClass speedwireInverter +---------- +The name of the EventType ({45bbdbef-1832-4870-bff5-299e580fb4da}) of ThingClass speedwireMeter + + + + + + + + Current phase B + The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPhaseB, ID: {4db96fec-737c-4c4b-bf07-5ef2fd62508a}) +---------- +The name of the StateType ({4db96fec-737c-4c4b-bf07-5ef2fd62508a}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPhaseB, ID: {b3a4fdd2-b6b8-4c58-9da3-2084ad414022}) +---------- +The name of the StateType ({b3a4fdd2-b6b8-4c58-9da3-2084ad414022}) of ThingClass speedwireMeter + + + + + + Current phase B changed + The name of the EventType ({4db96fec-737c-4c4b-bf07-5ef2fd62508a}) of ThingClass speedwireInverter +---------- +The name of the EventType ({b3a4fdd2-b6b8-4c58-9da3-2084ad414022}) of ThingClass speedwireMeter + + + + + + + + Current phase C + The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPhaseC, ID: {0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) +---------- +The name of the StateType ({0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPhaseC, ID: {b3655188-3854-4336-ae3c-61d3bda6fc4d}) +---------- +The name of the StateType ({b3655188-3854-4336-ae3c-61d3bda6fc4d}) of ThingClass speedwireMeter + + + + + + Current phase C changed + The name of the EventType ({0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) of ThingClass speedwireInverter +---------- +The name of the EventType ({b3655188-3854-4336-ae3c-61d3bda6fc4d}) of ThingClass speedwireMeter + + + + + + + + + Current power - The name of the ParamType (ThingClass: sunnyWebBox, EventType: currentPower, ID: {ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) + The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPower, ID: {d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) +---------- +The name of the StateType ({d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPower, ID: {d4ac7f37-e30a-44e4-93cb-ad16df18b8f1}) +---------- +The name of the StateType ({d4ac7f37-e30a-44e4-93cb-ad16df18b8f1}) of ThingClass speedwireMeter +---------- +The name of the ParamType (ThingClass: sunnyWebBox, EventType: currentPower, ID: {ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) ---------- The name of the StateType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox - + + + Current power changed - The name of the EventType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox + The name of the EventType ({d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) of ThingClass speedwireInverter +---------- +The name of the EventType ({d4ac7f37-e30a-44e4-93cb-ad16df18b8f1}) of ThingClass speedwireMeter +---------- +The name of the EventType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox - - + + + Current power phase A + The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPowerPhaseA, ID: {c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) +---------- +The name of the StateType ({c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) of ThingClass speedwireMeter + + + + + Current power phase A changed + The name of the EventType ({c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) of ThingClass speedwireMeter + + + + + + Current power phase B + The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPowerPhaseB, ID: {c52d4422-b521-4804-a7a7-c4398e91e760}) +---------- +The name of the StateType ({c52d4422-b521-4804-a7a7-c4398e91e760}) of ThingClass speedwireMeter + + + + + Current power phase B changed + The name of the EventType ({c52d4422-b521-4804-a7a7-c4398e91e760}) of ThingClass speedwireMeter + + + + + + Current power phase C + The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPowerPhaseC, ID: {555e892c-3ca7-4100-9832-6ac13b87eb04}) +---------- +The name of the StateType ({555e892c-3ca7-4100-9832-6ac13b87eb04}) of ThingClass speedwireMeter + + + + + Current power phase C changed + The name of the EventType ({555e892c-3ca7-4100-9832-6ac13b87eb04}) of ThingClass speedwireMeter + + + + + + DC power MPP1 + The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPowerMpp1, ID: {b366f680-6134-488b-8362-b1b824a8daca}) +---------- +The name of the StateType ({b366f680-6134-488b-8362-b1b824a8daca}) of ThingClass speedwireInverter + + + + + DC power MPP1 changed + The name of the EventType ({b366f680-6134-488b-8362-b1b824a8daca}) of ThingClass speedwireInverter + + + + + + DC power MPP2 + The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPowerMpp2, ID: {87d9b654-5558-47a3-9db9-ffd7c23b4774}) +---------- +The name of the StateType ({87d9b654-5558-47a3-9db9-ffd7c23b4774}) of ThingClass speedwireInverter + + + + + DC power MPP2 changed + The name of the EventType ({87d9b654-5558-47a3-9db9-ffd7c23b4774}) of ThingClass speedwireInverter + + + + + Day energy produced The name of the ParamType (ThingClass: sunnyWebBox, EventType: dayEnergyProduced, ID: {16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) ---------- @@ -51,14 +240,119 @@ The name of the StateType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass - + Day energy produced changed The name of the EventType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox - - + + + Energy consumed phase A + The name of the ParamType (ThingClass: speedwireMeter, EventType: energyConsumedPhaseA, ID: {b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) +---------- +The name of the StateType ({b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) of ThingClass speedwireMeter + + + + + Energy consumed phase A changed + The name of the EventType ({b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) of ThingClass speedwireMeter + + + + + + Energy consumed phase B + The name of the ParamType (ThingClass: speedwireMeter, EventType: energyConsumedPhaseB, ID: {c4e5f569-ac5d-4761-a898-888880bfd59f}) +---------- +The name of the StateType ({c4e5f569-ac5d-4761-a898-888880bfd59f}) of ThingClass speedwireMeter + + + + + Energy consumed phase B changed + The name of the EventType ({c4e5f569-ac5d-4761-a898-888880bfd59f}) of ThingClass speedwireMeter + + + + + + Energy consumed phase C + The name of the ParamType (ThingClass: speedwireMeter, EventType: energyConsumedPhaseC, ID: {aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) +---------- +The name of the StateType ({aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) of ThingClass speedwireMeter + + + + + Energy consumed phase C changed + The name of the EventType ({aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) of ThingClass speedwireMeter + + + + + + Energy produced phase A + The name of the ParamType (ThingClass: speedwireMeter, EventType: energyProducedPhaseA, ID: {754c3b67-768a-47f7-99d8-f66c198f0835}) +---------- +The name of the StateType ({754c3b67-768a-47f7-99d8-f66c198f0835}) of ThingClass speedwireMeter + + + + + Energy produced phase A changed + The name of the EventType ({754c3b67-768a-47f7-99d8-f66c198f0835}) of ThingClass speedwireMeter + + + + + + Energy produced phase B + The name of the ParamType (ThingClass: speedwireMeter, EventType: energyProducedPhaseB, ID: {7eb08c45-24cf-40ce-be28-f3564f087672}) +---------- +The name of the StateType ({7eb08c45-24cf-40ce-be28-f3564f087672}) of ThingClass speedwireMeter + + + + + Energy produced phase B changed + The name of the EventType ({7eb08c45-24cf-40ce-be28-f3564f087672}) of ThingClass speedwireMeter + + + + + + Energy produced phase C + The name of the ParamType (ThingClass: speedwireMeter, EventType: energyProducedPhaseC, ID: {1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) +---------- +The name of the StateType ({1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) of ThingClass speedwireMeter + + + + + Energy produced phase C changed + The name of the EventType ({1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) of ThingClass speedwireMeter + + + + + + Energy produced today + The name of the ParamType (ThingClass: speedwireInverter, EventType: energyProducedToday, ID: {e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) +---------- +The name of the StateType ({e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) of ThingClass speedwireInverter + + + + + Energy produced today changed + The name of the EventType ({e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) of ThingClass speedwireInverter + + + + + Error The name of the ParamType (ThingClass: sunnyWebBox, EventType: error, ID: {4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) ---------- @@ -66,20 +360,77 @@ The name of the StateType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass - + Error changed The name of the EventType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox - - Host address - The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {864d4162-e3ce-48b8-b8ac-c1b971b52d42}) + + + + + Firmware version + The name of the ParamType (ThingClass: speedwireInverter, EventType: firmwareVersion, ID: {6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) +---------- +The name of the StateType ({6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: firmwareVersion, ID: {a685393c-8b7e-42c5-bb41-f9907c074626}) +---------- +The name of the StateType ({a685393c-8b7e-42c5-bb41-f9907c074626}) of ThingClass speedwireMeter - - + + + Firmware version changed + The name of the EventType ({6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) of ThingClass speedwireInverter +---------- +The name of the EventType ({a685393c-8b7e-42c5-bb41-f9907c074626}) of ThingClass speedwireMeter + + + + + + Frequency + The name of the ParamType (ThingClass: speedwireInverter, EventType: frequency, ID: {fdccf5de-7413-4480-9ca0-1151665dede8}) +---------- +The name of the StateType ({fdccf5de-7413-4480-9ca0-1151665dede8}) of ThingClass speedwireInverter + + + + + Frequency changed + The name of the EventType ({fdccf5de-7413-4480-9ca0-1151665dede8}) of ThingClass speedwireInverter + + + + + + + Host address + The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {c8098d53-69eb-4d0b-9f07-e43c4a0ea9a9}) +---------- +The name of the ParamType (ThingClass: speedwireMeter, Type: thing, ID: {d90193e6-a996-4e49-bf6d-564d596d7e74}) +---------- +The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {864d4162-e3ce-48b8-b8ac-c1b971b52d42}) + + + + + + + MAC address + The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {7df0ab60-0f11-4495-8e0d-508ba2b6d858}) +---------- +The name of the ParamType (ThingClass: speedwireMeter, Type: thing, ID: {2780eab7-1f1c-4cc7-a789-a8790329ca9e}) +---------- +The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {03f32361-4e13-4597-a346-af8d16a986b3}) + + + + + Mode The name of the ParamType (ThingClass: sunnyWebBox, EventType: mode, ID: {1974550b-6059-4b0e-83f4-70177e20dac3}) ---------- @@ -87,46 +438,178 @@ The name of the StateType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass - + Mode changed The name of the EventType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass sunnyWebBox - - - SMA - The name of the vendor ({16d5a4a3-36d5-46c0-b7dd-df166ddf5981}) + + + Model ID + The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {d9892f74-5b93-4c98-8da2-72aca033273a}) ---------- -The name of the plugin sma ({b8442bbf-9d3f-4aa2-9443-b3a31ae09bac}) +The name of the ParamType (ThingClass: speedwireMeter, Type: thing, ID: {abdc114d-1fac-4454-8b82-871ed5cdf28c}) - - + + SMA + The name of the plugin sma ({b8442bbf-9d3f-4aa2-9443-b3a31ae09bac}) + + + + + SMA Energy Meter + The name of the ThingClass ({0c5097af-e136-4430-9fb4-0ccbb30c3e1c}) + + + + + SMA Inverter + The name of the ThingClass ({b63a0669-f2ac-4769-abea-e14cafb2309a}) + + + + + SMA Solar Technology AG + The name of the vendor ({16d5a4a3-36d5-46c0-b7dd-df166ddf5981}) + + + + + + Serial number + The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {e42242b4-2811-47f9-b42b-b150ed233217}) +---------- +The name of the ParamType (ThingClass: speedwireMeter, Type: thing, ID: {7c81a0c5-9bc6-43bb-a01a-4de5fe656bba}) + + + + + + Total energy consumed + The name of the ParamType (ThingClass: speedwireMeter, EventType: totalEnergyConsumed, ID: {4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) +---------- +The name of the StateType ({4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) of ThingClass speedwireMeter + + + + + Total energy consumed changed + The name of the EventType ({4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) of ThingClass speedwireMeter + + + + + + + + + Total energy produced - The name of the ParamType (ThingClass: sunnyWebBox, EventType: totalEnergyProduced, ID: {0bb4e227-7e38-49ca-9b32-ce4621c9305b}) + The name of the ParamType (ThingClass: speedwireInverter, EventType: totalEnergyProduced, ID: {51cadd66-2cf1-485a-a2a9-191d11abfbd1}) +---------- +The name of the StateType ({51cadd66-2cf1-485a-a2a9-191d11abfbd1}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: totalEnergyProduced, ID: {76ca68d8-6781-4d2a-8663-440aec40b4de}) +---------- +The name of the StateType ({76ca68d8-6781-4d2a-8663-440aec40b4de}) of ThingClass speedwireMeter +---------- +The name of the ParamType (ThingClass: sunnyWebBox, EventType: totalEnergyProduced, ID: {0bb4e227-7e38-49ca-9b32-ce4621c9305b}) ---------- The name of the StateType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox - + + + Total energy produced changed - The name of the EventType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox + The name of the EventType ({51cadd66-2cf1-485a-a2a9-191d11abfbd1}) of ThingClass speedwireInverter +---------- +The name of the EventType ({76ca68d8-6781-4d2a-8663-440aec40b4de}) of ThingClass speedwireMeter +---------- +The name of the EventType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox - + + + + + Voltage phase A + The name of the ParamType (ThingClass: speedwireInverter, EventType: voltagePhaseA, ID: {6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) +---------- +The name of the StateType ({6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: voltagePhaseA, ID: {44ee2491-8376-41cd-a21d-185c736152ec}) +---------- +The name of the StateType ({44ee2491-8376-41cd-a21d-185c736152ec}) of ThingClass speedwireMeter + + + + + + Voltage phase A changed + The name of the EventType ({6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) of ThingClass speedwireInverter +---------- +The name of the EventType ({44ee2491-8376-41cd-a21d-185c736152ec}) of ThingClass speedwireMeter + + + + + + + + Voltage phase B + The name of the ParamType (ThingClass: speedwireInverter, EventType: voltagePhaseB, ID: {d9a5768b-1bf5-4933-810d-84dd7a688f71}) +---------- +The name of the StateType ({d9a5768b-1bf5-4933-810d-84dd7a688f71}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: voltagePhaseB, ID: {56ae3555-f874-4c2d-8833-17573dce477a}) +---------- +The name of the StateType ({56ae3555-f874-4c2d-8833-17573dce477a}) of ThingClass speedwireMeter + + + + + + Voltage phase B changed + The name of the EventType ({d9a5768b-1bf5-4933-810d-84dd7a688f71}) of ThingClass speedwireInverter +---------- +The name of the EventType ({56ae3555-f874-4c2d-8833-17573dce477a}) of ThingClass speedwireMeter + + + + + + + + Voltage phase C + The name of the ParamType (ThingClass: speedwireInverter, EventType: voltagePhaseC, ID: {fc168dc6-eecf-40b4-b214-3e28da0dbb12}) +---------- +The name of the StateType ({fc168dc6-eecf-40b4-b214-3e28da0dbb12}) of ThingClass speedwireInverter +---------- +The name of the ParamType (ThingClass: speedwireMeter, EventType: voltagePhaseC, ID: {51cbb29b-29f0-480a-9d7d-b8f4e6a205ae}) +---------- +The name of the StateType ({51cbb29b-29f0-480a-9d7d-b8f4e6a205ae}) of ThingClass speedwireMeter + + + + + + Voltage phase C changed + The name of the EventType ({fc168dc6-eecf-40b4-b214-3e28da0dbb12}) of ThingClass speedwireInverter +---------- +The name of the EventType ({51cbb29b-29f0-480a-9d7d-b8f4e6a205ae}) of ThingClass speedwireMeter + + + + Sunny WebBox The name of the ThingClass ({49304127-ce9b-45dd-8511-05030a4ac003}) - - - hardware address - The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {03f32361-4e13-4597-a346-af8d16a986b3}) - - From 2e364521f81f293014c65c0ace66183093bf8781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 17 Jan 2022 09:50:27 +0100 Subject: [PATCH 08/17] Update README and plugin meta infos --- sma/README.md | 4 ++++ sma/meta.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sma/README.md b/sma/README.md index 63ffbd03..eff2e336 100644 --- a/sma/README.md +++ b/sma/README.md @@ -5,6 +5,10 @@ nymea plug-in for SMA solar equipment. ## Supported Things * Sunny WebBox +* SMA speedwire Meters +* SMA speedwire Inverters + +> Note: the SMA battery equipment is still missing due to testing possibilities. Will be added as soon someone can with the apropriate setup. ## Requirements diff --git a/sma/meta.json b/sma/meta.json index b2bee247..4755166a 100644 --- a/sma/meta.json +++ b/sma/meta.json @@ -8,6 +8,6 @@ "network" ], "categories": [ - + "Energy" ] } From e518d8327966249369f7eec38411bd99bda23463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 19 Jan 2022 12:24:00 +0100 Subject: [PATCH 09/17] Update speedwire default param values --- sma/integrationpluginsma.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index bdf7c41b..b5b344c7 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -107,7 +107,8 @@ "displayName": "MAC address", "type": "QString", "inputType": "TextLine", - "readOnly": true + "readOnly": true, + "defaultValue": "" }, { "id": "7c81a0c5-9bc6-43bb-a01a-4de5fe656bba", @@ -115,7 +116,8 @@ "displayName": "Serial number", "type": "QString", "inputType": "TextLine", - "readOnly": true + "readOnly": true, + "defaultValue": "" }, { "id": "abdc114d-1fac-4454-8b82-871ed5cdf28c", @@ -123,7 +125,8 @@ "displayName": "Model ID", "type": "uint", "inputType": "TextLine", - "readOnly": true + "readOnly": true, + "defaultValue": "" } ], "stateTypes": [ @@ -328,7 +331,8 @@ "displayName": "MAC address", "type": "QString", "inputType": "TextLine", - "readOnly": true + "readOnly": true, + "defaultValue": "" }, { "id": "e42242b4-2811-47f9-b42b-b150ed233217", @@ -336,7 +340,8 @@ "displayName": "Serial number", "type": "QString", "inputType": "TextLine", - "readOnly": true + "readOnly": true, + "defaultValue": "" }, { "id": "d9892f74-5b93-4c98-8da2-72aca033273a", @@ -344,7 +349,8 @@ "displayName": "Model ID", "type": "uint", "inputType": "TextLine", - "readOnly": true + "readOnly": true, + "defaultValue": 0 } ], "stateTypes": [ From d1eaeb62e6484089f7cc20ad9cbe7d2a7dc6aa4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 18 Feb 2022 06:24:45 +0100 Subject: [PATCH 10/17] Fix meta category --- sma/meta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sma/meta.json b/sma/meta.json index 4755166a..be875944 100644 --- a/sma/meta.json +++ b/sma/meta.json @@ -8,6 +8,6 @@ "network" ], "categories": [ - "Energy" + "energy" ] } From 76437b34c1670037cf5d74b691ec43bf80b94806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 18 Feb 2022 08:52:38 +0100 Subject: [PATCH 11/17] Add login during setup for SMA inverters --- sma/integrationpluginsma.cpp | 62 ++++++++++++++++++++++++++++++++--- sma/integrationpluginsma.h | 4 +++ sma/integrationpluginsma.json | 1 + sma/speedwireinverter.cpp | 32 +++++++++++++----- sma/speedwireinverter.h | 9 +++-- 5 files changed, 91 insertions(+), 17 deletions(-) diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index a8b7bfe4..ff4392c5 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -177,6 +177,32 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) } } +void IntegrationPluginSma::startPairing(ThingPairingInfo *info) +{ + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter the password of your inverter. If no password has been explicitly set, leave it empty to use the default password for SMA inverters.")); +} + +void IntegrationPluginSma::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + Q_UNUSED(username) + + // Init with the default password + QString password = "0000"; + if (!secret.isEmpty()) { + qCDebug(dcSma()) << "Pairing: Using password" << secret; + password = secret; + } else { + qCDebug(dcSma()) << "Pairing: Secret is empty. Using default password" << password; + } + + // Just store details, we'll test the login in setupDevice + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("password", password); + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); +} + void IntegrationPluginSma::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); @@ -213,7 +239,9 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) connect(sunnyWebBox, &SunnyWebBox::plantOverviewReceived, this, &IntegrationPluginSma::onPlantOverviewReceived); m_sunnyWebBoxes.insert(info->thing(), sunnyWebBox); }); + } else if (thing->thingClassId() == speedwireMeterThingClassId) { + QHostAddress address = QHostAddress(thing->paramValue(speedwireMeterThingHostParamTypeId).toString()); quint32 serialNumber = static_cast(thing->paramValue(speedwireMeterThingSerialNumberParamTypeId).toUInt()); quint16 modelId = static_cast(thing->paramValue(speedwireMeterThingModelIdParamTypeId).toUInt()); @@ -258,7 +286,9 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) m_speedwireMeters.insert(thing, meter); info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == speedwireInverterThingClassId) { + QHostAddress address = QHostAddress(thing->paramValue(speedwireInverterThingHostParamTypeId).toString()); quint32 serialNumber = static_cast(thing->paramValue(speedwireInverterThingSerialNumberParamTypeId).toUInt()); quint16 modelId = static_cast(thing->paramValue(speedwireInverterThingModelIdParamTypeId).toUInt()); @@ -276,6 +306,31 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) qCDebug(dcSma()) << "Inverter: Interface initialized successfully."; + QString password; + pluginStorage()->beginGroup(info->thing()->id().toString()); + password = pluginStorage()->value("password", "0000").toString(); + pluginStorage()->endGroup(); + + // Connection exists only as long info exists + connect(inverter, &SpeedwireInverter::loginFinished, info, [=](bool success){ + if (!success) { + qCWarning(dcSma()) << "Failed to set up inverter. Wrong password."; + delete inverter; + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to log in with the given password. Please try again.")); + return; + } + + qCDebug(dcSma()) << "Inverter set up successfully."; + m_speedwireInverters.insert(thing, inverter); + info->finish(Thing::ThingErrorNoError); + // Note: the data is already refreshing here + }); + + // Make sure an aborted setup will clean up the object + connect(info, &ThingSetupInfo::aborted, inverter, &SpeedwireInverter::deleteLater); + + + // Runtime connections connect(inverter, &SpeedwireInverter::reachableChanged, this, [=](bool reachable){ thing->setStateValue(speedwireInverterConnectedStateTypeId, reachable); }); @@ -296,14 +351,11 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) thing->setStateValue(speedwireInverterCurrentPowerMpp1StateTypeId, inverter->powerDcMpp1()); thing->setStateValue(speedwireInverterCurrentPowerMpp2StateTypeId, inverter->powerDcMpp2()); - }); - m_speedwireInverters.insert(thing, inverter); - info->finish(Thing::ThingErrorNoError); + qCDebug(dcSma()) << "Inverter: Start connecting using password" << password; + inverter->startConnecting(password); - // Initial refresh data - inverter->refresh(); } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index ef3aedc1..d695ae3d 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -48,6 +48,10 @@ public: void init() override; void discoverThings(ThingDiscoveryInfo *info) override; + + void startPairing(ThingPairingInfo *info) override; + void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override; + void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index b5b344c7..e268d48a 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -315,6 +315,7 @@ "name": "speedwireInverter", "displayName": "SMA Inverter", "createMethods": ["discovery", "user"], + "setupMethod": "EnterPin", "interfaces": [ "solarinverter" ], "paramTypes": [ { diff --git a/sma/speedwireinverter.cpp b/sma/speedwireinverter.cpp index ef3fc805..488cddec 100644 --- a/sma/speedwireinverter.cpp +++ b/sma/speedwireinverter.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -39,11 +39,9 @@ SpeedwireInverter::SpeedwireInverter(const QHostAddress &address, quint16 modelI m_modelId(modelId), m_serialNumber(serialNumber) { - qCDebug(dcSma()) << "Inverter: setup interface on" << m_address.toString(); m_interface = new SpeedwireInterface(m_address, false, this); connect(m_interface, &SpeedwireInterface::dataReceived, this, &SpeedwireInverter::processData); - } bool SpeedwireInverter::initialize() @@ -197,7 +195,7 @@ SpeedwireInverterReply *SpeedwireInverter::sendLoginRequest(const QString &passw encodedPassword[i] = (passwordData.at(i) + (loginAsUser ? 0x88 : 0xBB) % 0xff); } - // Add endoced password + // Add encoded password for (int i = 0; i < encodedPassword.count(); i++) { stream << static_cast(encodedPassword.at(i)); } @@ -328,6 +326,12 @@ SpeedwireInverterReply *SpeedwireInverter::sendDeviceTypeRequest() return createReply(request); } +void SpeedwireInverter::startConnecting(const QString &password) +{ + m_password = password; + refresh(); +} + void SpeedwireInverter::refresh() { // Only refresh if not already busy... @@ -1082,10 +1086,16 @@ void SpeedwireInverter::setState(State state) if (reply->error() != SpeedwireInverterReply::ErrorNoError) { if (reply->error() == SpeedwireInverterReply::ErrorTimeout) { qCWarning(dcSma()) << "Inverter: Failed to query data from inverter:" << reply->request().command() << reply->error(); + + // TODO: try to send identify request and retry 3 times before giving up, + // still need to figure out why the inverter stops responding sometimes and how we can + // make it communicative again, a reconfugre always fixes this issue...somehow... + setState(StateDisconnected); return; } + // Reachable, but received an inverter error, probably not logged if (reply->error() == SpeedwireInverterReply::ErrorInverterError) { qCDebug(dcSma()) << "Inverter: Query data request finished with inverter error. Try to login..."; setState(StateLogin); @@ -1093,10 +1103,14 @@ void SpeedwireInverter::setState(State state) } } + // We where able to read data...emit the signal for the setup just incase + emit loginFinished(true); + qCDebug(dcSma()) << "Inverter: Query request finished successfully" << reply->request().command(); processAcPowerResponse(reply->responseData()); - if (deviceInformationFetched) { + + if (m_deviceInformationFetched) { setState(StateQueryData); } else { setState(StateGetInformation); @@ -1104,18 +1118,18 @@ void SpeedwireInverter::setState(State state) }); break; } - case StateLogin: { - setState(StateLogin); - SpeedwireInverterReply *loginReply = sendLoginRequest(); + SpeedwireInverterReply *loginReply = sendLoginRequest(m_password); connect(loginReply, &SpeedwireInverterReply::finished, this, [=](){ if (loginReply->error() != SpeedwireInverterReply::ErrorNoError) { qCWarning(dcSma()) << "Inverter: Failed to login to inverter:" << loginReply->error(); + emit loginFinished(false); setState(StateDisconnected); return; } qCDebug(dcSma()) << "Inverter: Login request finished successfully."; + emit loginFinished(true); setReachable(true); // Logged in successfully, reinit the data fetch process @@ -1145,7 +1159,7 @@ void SpeedwireInverter::setState(State state) qCDebug(dcSma()) << "Inverter: Get device information finished successfully."; processDeviceTypeResponse(deviceTypeReply->responsePayload()); - deviceInformationFetched = true; + m_deviceInformationFetched = true; setState(StateQueryData); }); }); diff --git a/sma/speedwireinverter.h b/sma/speedwireinverter.h index 8750202b..50988b3c 100644 --- a/sma/speedwireinverter.h +++ b/sma/speedwireinverter.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -101,19 +101,22 @@ public: SpeedwireInverterReply *sendDeviceTypeRequest(); // Start connecting - void startConnecting(); + void startConnecting(const QString &password = "0000"); public slots: void refresh(); signals: void reachableChanged(bool reachable); + void loginFinished(bool success); void stateChanged(State state); void valuesUpdated(); private: SpeedwireInterface *m_interface = nullptr; QHostAddress m_address; + QString m_password; + bool m_initialized = false; quint16 m_modelId = 0; quint32 m_serialNumber = 0; @@ -122,7 +125,7 @@ private: State m_state = StateDisconnected; quint16 m_packetId = 1; - bool deviceInformationFetched = false; + bool m_deviceInformationFetched = false; SpeedwireInverterReply *m_currentReply = nullptr; QQueue m_replyQueue; From c8bf9dd471b8ad621807c28cc3909b02acb2f6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 18 Feb 2022 09:02:25 +0100 Subject: [PATCH 12/17] Update translations --- .../b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts index 5f1fa2ba..b80852b3 100644 --- a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts +++ b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts @@ -14,6 +14,16 @@ Unable to discover the network. + + + Please enter the password of your inverter. If no password has been explicitly set, leave it empty to use the default password for SMA inverters. + + + + + Failed to log in with the given password. Please try again. + + sma From b6e9d7a1a1c84683727e2b0571ca9060ab26d70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 21 Feb 2022 09:36:07 +0100 Subject: [PATCH 13/17] Rename Package to packet and fix connections --- sma/integrationpluginsma.cpp | 8 +++--- sma/speedwire.h | 48 +++++++++++++++++----------------- sma/speedwirediscovery.cpp | 8 +++--- sma/speedwireinverter.cpp | 30 ++++++++++----------- sma/speedwireinverter.h | 2 +- sma/speedwireinverterreply.cpp | 4 +-- sma/speedwireinverterreply.h | 4 +-- 7 files changed, 52 insertions(+), 52 deletions(-) diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index ff4392c5..465af7be 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -257,11 +257,11 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) return; } - connect(meter, &SpeedwireMeter::reachableChanged, this, [=](bool reachable){ + connect(meter, &SpeedwireMeter::reachableChanged, thing, [=](bool reachable){ thing->setStateValue(speedwireMeterConnectedStateTypeId, reachable); }); - connect(meter, &SpeedwireMeter::valuesUpdated, this, [=](){ + connect(meter, &SpeedwireMeter::valuesUpdated, thing, [=](){ thing->setStateValue(speedwireMeterConnectedStateTypeId, true); thing->setStateValue(speedwireMeterCurrentPowerStateTypeId, meter->currentPower()); thing->setStateValue(speedwireMeterCurrentPowerPhaseAStateTypeId, meter->currentPowerPhaseA()); @@ -331,11 +331,11 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) // Runtime connections - connect(inverter, &SpeedwireInverter::reachableChanged, this, [=](bool reachable){ + connect(inverter, &SpeedwireInverter::reachableChanged, thing, [=](bool reachable){ thing->setStateValue(speedwireInverterConnectedStateTypeId, reachable); }); - connect(inverter, &SpeedwireInverter::valuesUpdated, this, [=](){ + connect(inverter, &SpeedwireInverter::valuesUpdated, thing, [=](){ thing->setStateValue(speedwireInverterTotalEnergyProducedStateTypeId, inverter->totalEnergyProduced()); thing->setStateValue(speedwireInverterEnergyProducedTodayStateTypeId, inverter->todayEnergyProduced()); thing->setStateValue(speedwireInverterCurrentPowerStateTypeId, -inverter->totalAcPower()); diff --git a/sma/speedwire.h b/sma/speedwire.h index 3d01a854..05fd3603 100644 --- a/sma/speedwire.h +++ b/sma/speedwire.h @@ -95,7 +95,7 @@ public: } }; - typedef struct InverterPackage { + typedef struct InverterPacket { quint8 wordCount = 0; quint8 control = 0; quint16 destinationModelId = 0; @@ -108,7 +108,7 @@ public: quint16 fragmentId = 0; quint16 packetId = 0; quint32 command = 0; - } InverterPackage; + } InverterPacket; Speedwire() = default; @@ -259,23 +259,23 @@ public: return header; }; - static Speedwire::InverterPackage parseInverterPackage(QDataStream &stream) { + static Speedwire::InverterPacket parseInverterPacket(QDataStream &stream) { // Make sure the data stream is little endian stream.setByteOrder(QDataStream::LittleEndian); - InverterPackage package; - stream >> package.wordCount; - stream >> package.control; - stream >> package.destinationModelId; - stream >> package.destinationSerialNumber; - stream >> package.destinationControl; - stream >> package.sourceModelId; - stream >> package.sourceSerialNumber; - stream >> package.sourceControl; - stream >> package.errorCode; - stream >> package.fragmentId; - stream >> package.packetId; - stream >> package.command; - return package; + InverterPacket packet; + stream >> packet.wordCount; + stream >> packet.control; + stream >> packet.destinationModelId; + stream >> packet.destinationSerialNumber; + stream >> packet.destinationControl; + stream >> packet.sourceModelId; + stream >> packet.sourceSerialNumber; + stream >> packet.sourceControl; + stream >> packet.errorCode; + stream >> packet.fragmentId; + stream >> packet.packetId; + stream >> packet.command; + return packet; }; }; @@ -285,14 +285,14 @@ inline QDebug operator<<(QDebug debug, const Speedwire::Header &header) return debug.maybeSpace(); } -inline QDebug operator<<(QDebug debug, const Speedwire::InverterPackage &package) +inline QDebug operator<<(QDebug debug, const Speedwire::InverterPacket &packet) { - debug.nospace() << "InverterPackage(" << package.sourceSerialNumber; - debug.nospace() << ", Model ID: " << package.sourceModelId; - debug.nospace() << ", command: " << package.command; - debug.nospace() << ", error: " << package.errorCode; - debug.nospace() << ", fragment: " << package.fragmentId; - debug.nospace() << ", package ID: " << package.fragmentId; + debug.nospace() << "InverterPacket(" << packet.sourceSerialNumber; + debug.nospace() << ", Model ID: " << packet.sourceModelId; + debug.nospace() << ", command: " << packet.command; + debug.nospace() << ", error: " << packet.errorCode; + debug.nospace() << ", fragment: " << packet.fragmentId; + debug.nospace() << ", packet ID: " << packet.fragmentId; debug.nospace() << ")"; return debug.maybeSpace(); } diff --git a/sma/speedwirediscovery.cpp b/sma/speedwirediscovery.cpp index 6ae02bfa..301c6cfe 100644 --- a/sma/speedwirediscovery.cpp +++ b/sma/speedwirediscovery.cpp @@ -294,9 +294,9 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin m_results[senderAddress].modelId = modelId; m_results[senderAddress].serialNumber = serialNumber; } else if (header.protocolId == Speedwire::ProtocolIdInverter) { - Speedwire::InverterPackage inverterPackage = Speedwire::parseInverterPackage(stream); + Speedwire::InverterPacket inverterPacket = Speedwire::parseInverterPacket(stream); // Response from inverter 534d4100 0004 02a0 0000 0001 004e 0010 6065 1390 7d00 52be283a 0000 b500 c2c12e12 0000 0000 00000 1800102000000000000000000000003000000ff0000ecd5ff1f0100b500c2c12e1200000a000c00000000000000030000000101000000000000 - qCDebug(dcSma()) << "SpeedwireDiscovery:" << inverterPackage; + qCDebug(dcSma()) << "SpeedwireDiscovery:" << inverterPacket; if (!m_results.contains(senderAddress)) { SpeedwireDiscoveryResult result; @@ -309,8 +309,8 @@ void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quin m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); } - m_results[senderAddress].modelId = inverterPackage.sourceModelId; - m_results[senderAddress].serialNumber = inverterPackage.sourceSerialNumber; + m_results[senderAddress].modelId = inverterPacket.sourceModelId; + m_results[senderAddress].serialNumber = inverterPacket.sourceSerialNumber; } else { qCWarning(dcSma()) << "SpeedwireDiscovery: Unhandled data received" << datagram.toHex(); return; diff --git a/sma/speedwireinverter.cpp b/sma/speedwireinverter.cpp index 488cddec..39329c97 100644 --- a/sma/speedwireinverter.cpp +++ b/sma/speedwireinverter.cpp @@ -177,7 +177,7 @@ SpeedwireInverterReply *SpeedwireInverter::sendLoginRequest(const QString &passw Speedwire::Command command = Speedwire::CommandLogin; // The payload is little endian encoded - buildPackage(stream, command, packetId); + buildPacket(stream, command, packetId); // User type: 7 = user, a = installer stream << (loginAsUser ? static_cast(0x00000007) : static_cast(0x0000000a)); @@ -279,7 +279,7 @@ SpeedwireInverterReply *SpeedwireInverter::sendSoftwareVersionRequest() Speedwire::Command command = Speedwire::CommandQueryDevice; // The payload is little endian encoded - buildPackage(stream, command, packetId); + buildPacket(stream, command, packetId); // First and last word stream << static_cast(0x00823400); @@ -309,7 +309,7 @@ SpeedwireInverterReply *SpeedwireInverter::sendDeviceTypeRequest() Speedwire::Command command = Speedwire::CommandQueryDevice; // The payload is little endian encoded - buildPackage(stream, command, packetId); + buildPacket(stream, command, packetId); // 2 words stream << static_cast(0x00821e00); @@ -354,7 +354,7 @@ void SpeedwireInverter::sendNextReply() // Pick the next reply and send request m_currentReply = m_replyQueue.dequeue(); - qCDebug(dcSma()) << "Inverter: --> Sending" << m_currentReply->request().command() << "package ID:" << m_currentReply->request().packetId(); + qCDebug(dcSma()) << "Inverter: --> Sending" << m_currentReply->request().command() << "packet ID:" << m_currentReply->request().packetId(); m_interface->sendData(m_currentReply->request().requestData()); m_currentReply->startWaiting(); } @@ -403,9 +403,9 @@ void SpeedwireInverter::buildDefaultHeader(QDataStream &stream, quint16 payloadS stream << control; } -void SpeedwireInverter::buildPackage(QDataStream &stream, quint32 command, quint16 packetId) +void SpeedwireInverter::buildPacket(QDataStream &stream, quint32 command, quint16 packetId) { - // ========= package header (little endian) + // ========= packet header (little endian) // == 7a01842a71b30001 // 7a01 : destination model id @@ -492,7 +492,7 @@ SpeedwireInverterReply *SpeedwireInverter::sendQueryRequest(Speedwire::Command c quint16 packetId = m_packetId++ | 0x8000; // The payload is little endian encoded - buildPackage(stream, command, packetId); + buildPacket(stream, command, packetId); // First and second word stream << firstWord; @@ -995,19 +995,19 @@ void SpeedwireInverter::processData(const QByteArray &data) return; } - Speedwire::InverterPackage package = Speedwire::parseInverterPackage(stream); - if (package.sourceModelId != m_modelId || package.sourceSerialNumber != m_serialNumber) { - qCWarning(dcSma()) << "Inverter: Received datagram from different inverter" << package.sourceSerialNumber << "Ignoring data..."; + Speedwire::InverterPacket packet = Speedwire::parseInverterPacket(stream); + if (packet.sourceModelId != m_modelId || packet.sourceSerialNumber != m_serialNumber) { + qCWarning(dcSma()) << "Inverter: Received datagram from different inverter" << packet.sourceSerialNumber << "Ignoring data..."; return; } - qCDebug(dcSma()) << "Inverter: <-- Received" << static_cast(package.command) << "Packet ID:" << package.packetId; + qCDebug(dcSma()) << "Inverter: <-- Received" << static_cast(packet.command) << "Packet ID:" << packet.packetId; //qCDebug(dcSma()) << "Inverter:" << data.toHex(); - if (m_currentReply && m_currentReply->request().packetId() == package.packetId) { + if (m_currentReply && m_currentReply->request().packetId() == packet.packetId) { qCDebug(dcSma()) << "Inverter: Received response for current reply" << static_cast(m_currentReply->request().command()) << "Packet ID:" << m_currentReply->request().packetId(); m_currentReply->m_responseData = data; m_currentReply->m_responseHeader = header; - m_currentReply->m_responsePackage = package; + m_currentReply->m_responsePacket = packet; // Set the payload while (!stream.atEnd()) { quint8 byte; @@ -1015,7 +1015,7 @@ void SpeedwireInverter::processData(const QByteArray &data) m_currentReply->m_responsePayload.append(byte); } - if (package.errorCode != 0) { + if (packet.errorCode != 0) { m_currentReply->finishReply(SpeedwireInverterReply::ErrorInverterError); } else { m_currentReply->finishReply(SpeedwireInverterReply::ErrorNoError); @@ -1027,7 +1027,7 @@ void SpeedwireInverter::processData(const QByteArray &data) qCWarning(dcSma()) << "Inverter: Received unexpected data: not waiting for any response."; } qCWarning(dcSma()) << "Inverter:" << header; - qCWarning(dcSma()) << "Inverter:" << package; + qCWarning(dcSma()) << "Inverter:" << packet; qCWarning(dcSma()) << "Inverter:" << data.toHex(); } } diff --git a/sma/speedwireinverter.h b/sma/speedwireinverter.h index 50988b3c..ce2d34e5 100644 --- a/sma/speedwireinverter.h +++ b/sma/speedwireinverter.h @@ -169,7 +169,7 @@ private: // Request builder function void buildDefaultHeader(QDataStream &stream, quint16 payloadSize = 38, quint8 control = 0xa0); - void buildPackage(QDataStream &stream, quint32 command, quint16 packetId); + void buildPacket(QDataStream &stream, quint32 command, quint16 packetId); // Send generic request for internal use SpeedwireInverterReply *sendQueryRequest(Speedwire::Command command, quint32 firstWord, quint32 secondWord); diff --git a/sma/speedwireinverterreply.cpp b/sma/speedwireinverterreply.cpp index e65e0f50..95b76114 100644 --- a/sma/speedwireinverterreply.cpp +++ b/sma/speedwireinverterreply.cpp @@ -62,9 +62,9 @@ Speedwire::Header SpeedwireInverterReply::responseHeader() const return m_responseHeader; } -Speedwire::InverterPackage SpeedwireInverterReply::responsePackage() const +Speedwire::InverterPacket SpeedwireInverterReply::responsePacket() const { - return m_responsePackage; + return m_responsePacket; } QByteArray SpeedwireInverterReply::responsePayload() const diff --git a/sma/speedwireinverterreply.h b/sma/speedwireinverterreply.h index f2f8078f..b35dc5bd 100644 --- a/sma/speedwireinverterreply.h +++ b/sma/speedwireinverterreply.h @@ -58,7 +58,7 @@ public: // Response QByteArray responseData() const; Speedwire::Header responseHeader() const; - Speedwire::InverterPackage responsePackage() const; + Speedwire::InverterPacket responsePacket() const; QByteArray responsePayload() const; signals: @@ -77,7 +77,7 @@ private: QByteArray m_responseData; Speedwire::Header m_responseHeader; - Speedwire::InverterPackage m_responsePackage; + Speedwire::InverterPacket m_responsePacket; QByteArray m_responsePayload; From 8c05586901ba6d802a225c7a72f59df03e03f9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 21 Feb 2022 09:44:57 +0100 Subject: [PATCH 14/17] Improve password handling and clean up in case the password is not valid. --- sma/integrationpluginsma.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index 465af7be..db6b75dd 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -186,13 +186,19 @@ void IntegrationPluginSma::confirmPairing(ThingPairingInfo *info, const QString { Q_UNUSED(username) + // On speedwire the password length has a maximum of 12 characters + if (secret.count() > 12) { + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The password can not be longer than 12 characters.")); + return; + } + // Init with the default password QString password = "0000"; if (!secret.isEmpty()) { qCDebug(dcSma()) << "Pairing: Using password" << secret; password = secret; } else { - qCDebug(dcSma()) << "Pairing: Secret is empty. Using default password" << password; + qCDebug(dcSma()) << "Pairing: The given password is empty. Using default password" << password; } // Just store details, we'll test the login in setupDevice @@ -315,6 +321,12 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) connect(inverter, &SpeedwireInverter::loginFinished, info, [=](bool success){ if (!success) { qCWarning(dcSma()) << "Failed to set up inverter. Wrong password."; + + // Remove invalid password from settings + pluginStorage()->beginGroup(info->thing()->id().toString()); + pluginStorage()->remove(""); + pluginStorage()->endGroup(); + delete inverter; info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to log in with the given password. Please try again.")); return; @@ -329,7 +341,6 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) // Make sure an aborted setup will clean up the object connect(info, &ThingSetupInfo::aborted, inverter, &SpeedwireInverter::deleteLater); - // Runtime connections connect(inverter, &SpeedwireInverter::reachableChanged, thing, [=](bool reachable){ thing->setStateValue(speedwireInverterConnectedStateTypeId, reachable); @@ -410,6 +421,7 @@ void IntegrationPluginSma::onConnectedChanged(bool connected) Thing *thing = m_sunnyWebBoxes.key(static_cast(sender())); if (!thing) return; + thing->setStateValue(sunnyWebBoxConnectedStateTypeId, connected); } @@ -446,6 +458,7 @@ void IntegrationPluginSma::setupRefreshTimer() } foreach (SpeedwireInverter *inverter, m_speedwireInverters) { + // Note: refresh will not be triggered if there is already a refresh process running inverter->refresh(); } }); From 3dadc6586d78b8c1aea40dce59513967cfae6832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 21 Feb 2022 09:47:29 +0100 Subject: [PATCH 15/17] Update translations --- ...42bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts | 502 ++++-------------- 1 file changed, 93 insertions(+), 409 deletions(-) diff --git a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts index b80852b3..861aab46 100644 --- a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts +++ b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts @@ -20,7 +20,12 @@ - + + The password can not be longer than 12 characters. + + + + Failed to log in with the given password. Please try again. @@ -28,396 +33,159 @@ sma - - - - - - + + + Connected - The name of the ParamType (ThingClass: speedwireInverter, EventType: connected, ID: {aaff72c3-c70a-4a2f-bed1-89f38cebe442}) ----------- -The name of the StateType ({aaff72c3-c70a-4a2f-bed1-89f38cebe442}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: connected, ID: {35733d27-4fe0-439a-be71-7c1597481659}) + The name of the StateType ({aaff72c3-c70a-4a2f-bed1-89f38cebe442}) of ThingClass speedwireInverter ---------- The name of the StateType ({35733d27-4fe0-439a-be71-7c1597481659}) of ThingClass speedwireMeter ---------- -The name of the ParamType (ThingClass: sunnyWebBox, EventType: connected, ID: {c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) ----------- The name of the StateType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox - - - - Connected changed - The name of the EventType ({aaff72c3-c70a-4a2f-bed1-89f38cebe442}) of ThingClass speedwireInverter ----------- -The name of the EventType ({35733d27-4fe0-439a-be71-7c1597481659}) of ThingClass speedwireMeter ----------- -The name of the EventType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox - - - - - - - + + Current phase A - The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPhaseA, ID: {2a6c59ca-853a-47d6-96fb-0c85edf32f52}) ----------- -The name of the StateType ({2a6c59ca-853a-47d6-96fb-0c85edf32f52}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPhaseA, ID: {45bbdbef-1832-4870-bff5-299e580fb4da}) + The name of the StateType ({2a6c59ca-853a-47d6-96fb-0c85edf32f52}) of ThingClass speedwireInverter ---------- The name of the StateType ({45bbdbef-1832-4870-bff5-299e580fb4da}) of ThingClass speedwireMeter - - - Current phase A changed - The name of the EventType ({2a6c59ca-853a-47d6-96fb-0c85edf32f52}) of ThingClass speedwireInverter ----------- -The name of the EventType ({45bbdbef-1832-4870-bff5-299e580fb4da}) of ThingClass speedwireMeter - - - - - - - + + Current phase B - The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPhaseB, ID: {4db96fec-737c-4c4b-bf07-5ef2fd62508a}) ----------- -The name of the StateType ({4db96fec-737c-4c4b-bf07-5ef2fd62508a}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPhaseB, ID: {b3a4fdd2-b6b8-4c58-9da3-2084ad414022}) + The name of the StateType ({4db96fec-737c-4c4b-bf07-5ef2fd62508a}) of ThingClass speedwireInverter ---------- The name of the StateType ({b3a4fdd2-b6b8-4c58-9da3-2084ad414022}) of ThingClass speedwireMeter - - - Current phase B changed - The name of the EventType ({4db96fec-737c-4c4b-bf07-5ef2fd62508a}) of ThingClass speedwireInverter ----------- -The name of the EventType ({b3a4fdd2-b6b8-4c58-9da3-2084ad414022}) of ThingClass speedwireMeter - - - - - - - + + Current phase C - The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPhaseC, ID: {0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) ----------- -The name of the StateType ({0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPhaseC, ID: {b3655188-3854-4336-ae3c-61d3bda6fc4d}) + The name of the StateType ({0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) of ThingClass speedwireInverter ---------- The name of the StateType ({b3655188-3854-4336-ae3c-61d3bda6fc4d}) of ThingClass speedwireMeter - - - Current phase C changed - The name of the EventType ({0f23fb0e-a440-4ac2-9aff-896bc65feb2c}) of ThingClass speedwireInverter ----------- -The name of the EventType ({b3655188-3854-4336-ae3c-61d3bda6fc4d}) of ThingClass speedwireMeter - - - - - - - - - + + + Current power - The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPower, ID: {d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) ----------- -The name of the StateType ({d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPower, ID: {d4ac7f37-e30a-44e4-93cb-ad16df18b8f1}) + The name of the StateType ({d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) of ThingClass speedwireInverter ---------- The name of the StateType ({d4ac7f37-e30a-44e4-93cb-ad16df18b8f1}) of ThingClass speedwireMeter ---------- -The name of the ParamType (ThingClass: sunnyWebBox, EventType: currentPower, ID: {ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) ----------- The name of the StateType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox - - - - Current power changed - The name of the EventType ({d7ceb482-5df8-4c0c-82bd-62ce7ba22c43}) of ThingClass speedwireInverter ----------- -The name of the EventType ({d4ac7f37-e30a-44e4-93cb-ad16df18b8f1}) of ThingClass speedwireMeter ----------- -The name of the EventType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox - - - - - + Current power phase A - The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPowerPhaseA, ID: {c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) ----------- -The name of the StateType ({c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) of ThingClass speedwireMeter + The name of the StateType ({c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) of ThingClass speedwireMeter - - Current power phase A changed - The name of the EventType ({c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) of ThingClass speedwireMeter - - - - - + Current power phase B - The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPowerPhaseB, ID: {c52d4422-b521-4804-a7a7-c4398e91e760}) ----------- -The name of the StateType ({c52d4422-b521-4804-a7a7-c4398e91e760}) of ThingClass speedwireMeter + The name of the StateType ({c52d4422-b521-4804-a7a7-c4398e91e760}) of ThingClass speedwireMeter - - Current power phase B changed - The name of the EventType ({c52d4422-b521-4804-a7a7-c4398e91e760}) of ThingClass speedwireMeter - - - - - + Current power phase C - The name of the ParamType (ThingClass: speedwireMeter, EventType: currentPowerPhaseC, ID: {555e892c-3ca7-4100-9832-6ac13b87eb04}) ----------- -The name of the StateType ({555e892c-3ca7-4100-9832-6ac13b87eb04}) of ThingClass speedwireMeter + The name of the StateType ({555e892c-3ca7-4100-9832-6ac13b87eb04}) of ThingClass speedwireMeter - - Current power phase C changed - The name of the EventType ({555e892c-3ca7-4100-9832-6ac13b87eb04}) of ThingClass speedwireMeter - - - - - + DC power MPP1 - The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPowerMpp1, ID: {b366f680-6134-488b-8362-b1b824a8daca}) ----------- -The name of the StateType ({b366f680-6134-488b-8362-b1b824a8daca}) of ThingClass speedwireInverter + The name of the StateType ({b366f680-6134-488b-8362-b1b824a8daca}) of ThingClass speedwireInverter - - DC power MPP1 changed - The name of the EventType ({b366f680-6134-488b-8362-b1b824a8daca}) of ThingClass speedwireInverter - - - - - + DC power MPP2 - The name of the ParamType (ThingClass: speedwireInverter, EventType: currentPowerMpp2, ID: {87d9b654-5558-47a3-9db9-ffd7c23b4774}) ----------- -The name of the StateType ({87d9b654-5558-47a3-9db9-ffd7c23b4774}) of ThingClass speedwireInverter + The name of the StateType ({87d9b654-5558-47a3-9db9-ffd7c23b4774}) of ThingClass speedwireInverter - - DC power MPP2 changed - The name of the EventType ({87d9b654-5558-47a3-9db9-ffd7c23b4774}) of ThingClass speedwireInverter - - - - - + Day energy produced - The name of the ParamType (ThingClass: sunnyWebBox, EventType: dayEnergyProduced, ID: {16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) ----------- -The name of the StateType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox + The name of the StateType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox - - Day energy produced changed - The name of the EventType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox - - - - - + Energy consumed phase A - The name of the ParamType (ThingClass: speedwireMeter, EventType: energyConsumedPhaseA, ID: {b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) ----------- -The name of the StateType ({b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) of ThingClass speedwireMeter + The name of the StateType ({b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) of ThingClass speedwireMeter - - Energy consumed phase A changed - The name of the EventType ({b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) of ThingClass speedwireMeter - - - - - + Energy consumed phase B - The name of the ParamType (ThingClass: speedwireMeter, EventType: energyConsumedPhaseB, ID: {c4e5f569-ac5d-4761-a898-888880bfd59f}) ----------- -The name of the StateType ({c4e5f569-ac5d-4761-a898-888880bfd59f}) of ThingClass speedwireMeter + The name of the StateType ({c4e5f569-ac5d-4761-a898-888880bfd59f}) of ThingClass speedwireMeter - - Energy consumed phase B changed - The name of the EventType ({c4e5f569-ac5d-4761-a898-888880bfd59f}) of ThingClass speedwireMeter - - - - - + Energy consumed phase C - The name of the ParamType (ThingClass: speedwireMeter, EventType: energyConsumedPhaseC, ID: {aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) ----------- -The name of the StateType ({aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) of ThingClass speedwireMeter + The name of the StateType ({aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) of ThingClass speedwireMeter - - Energy consumed phase C changed - The name of the EventType ({aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) of ThingClass speedwireMeter - - - - - + Energy produced phase A - The name of the ParamType (ThingClass: speedwireMeter, EventType: energyProducedPhaseA, ID: {754c3b67-768a-47f7-99d8-f66c198f0835}) ----------- -The name of the StateType ({754c3b67-768a-47f7-99d8-f66c198f0835}) of ThingClass speedwireMeter + The name of the StateType ({754c3b67-768a-47f7-99d8-f66c198f0835}) of ThingClass speedwireMeter - - Energy produced phase A changed - The name of the EventType ({754c3b67-768a-47f7-99d8-f66c198f0835}) of ThingClass speedwireMeter - - - - - + Energy produced phase B - The name of the ParamType (ThingClass: speedwireMeter, EventType: energyProducedPhaseB, ID: {7eb08c45-24cf-40ce-be28-f3564f087672}) ----------- -The name of the StateType ({7eb08c45-24cf-40ce-be28-f3564f087672}) of ThingClass speedwireMeter + The name of the StateType ({7eb08c45-24cf-40ce-be28-f3564f087672}) of ThingClass speedwireMeter - - Energy produced phase B changed - The name of the EventType ({7eb08c45-24cf-40ce-be28-f3564f087672}) of ThingClass speedwireMeter - - - - - + Energy produced phase C - The name of the ParamType (ThingClass: speedwireMeter, EventType: energyProducedPhaseC, ID: {1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) ----------- -The name of the StateType ({1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) of ThingClass speedwireMeter + The name of the StateType ({1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) of ThingClass speedwireMeter - - Energy produced phase C changed - The name of the EventType ({1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) of ThingClass speedwireMeter - - - - - + Energy produced today - The name of the ParamType (ThingClass: speedwireInverter, EventType: energyProducedToday, ID: {e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) ----------- -The name of the StateType ({e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) of ThingClass speedwireInverter + The name of the StateType ({e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) of ThingClass speedwireInverter - - Energy produced today changed - The name of the EventType ({e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) of ThingClass speedwireInverter - - - - - + Error - The name of the ParamType (ThingClass: sunnyWebBox, EventType: error, ID: {4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) ----------- -The name of the StateType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox + The name of the StateType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox - - Error changed - The name of the EventType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox - - - - - - - + + Firmware version - The name of the ParamType (ThingClass: speedwireInverter, EventType: firmwareVersion, ID: {6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) ----------- -The name of the StateType ({6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: firmwareVersion, ID: {a685393c-8b7e-42c5-bb41-f9907c074626}) + The name of the StateType ({6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) of ThingClass speedwireInverter ---------- The name of the StateType ({a685393c-8b7e-42c5-bb41-f9907c074626}) of ThingClass speedwireMeter - - - Firmware version changed - The name of the EventType ({6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66}) of ThingClass speedwireInverter ----------- -The name of the EventType ({a685393c-8b7e-42c5-bb41-f9907c074626}) of ThingClass speedwireMeter - - - - - + Frequency - The name of the ParamType (ThingClass: speedwireInverter, EventType: frequency, ID: {fdccf5de-7413-4480-9ca0-1151665dede8}) ----------- -The name of the StateType ({fdccf5de-7413-4480-9ca0-1151665dede8}) of ThingClass speedwireInverter + The name of the StateType ({fdccf5de-7413-4480-9ca0-1151665dede8}) of ThingClass speedwireInverter - - Frequency changed - The name of the EventType ({fdccf5de-7413-4480-9ca0-1151665dede8}) of ThingClass speedwireInverter - - - - - - + + + Host address The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {c8098d53-69eb-4d0b-9f07-e43c4a0ea9a9}) ---------- @@ -427,9 +195,9 @@ The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {864d4162-e - - - + + + MAC address The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {7df0ab60-0f11-4495-8e0d-508ba2b6d858}) ---------- @@ -439,23 +207,14 @@ The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {03f32361-4 - - + Mode - The name of the ParamType (ThingClass: sunnyWebBox, EventType: mode, ID: {1974550b-6059-4b0e-83f4-70177e20dac3}) ----------- -The name of the StateType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass sunnyWebBox + The name of the StateType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass sunnyWebBox - - Mode changed - The name of the EventType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass sunnyWebBox - - - - - + + Model ID The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {d9892f74-5b93-4c98-8da2-72aca033273a}) ---------- @@ -463,32 +222,32 @@ The name of the ParamType (ThingClass: speedwireMeter, Type: thing, ID: {abdc114 - + SMA The name of the plugin sma ({b8442bbf-9d3f-4aa2-9443-b3a31ae09bac}) - + SMA Energy Meter The name of the ThingClass ({0c5097af-e136-4430-9fb4-0ccbb30c3e1c}) - + SMA Inverter The name of the ThingClass ({b63a0669-f2ac-4769-abea-e14cafb2309a}) - + SMA Solar Technology AG The name of the vendor ({16d5a4a3-36d5-46c0-b7dd-df166ddf5981}) - - + + Serial number The name of the ParamType (ThingClass: speedwireInverter, Type: thing, ID: {e42242b4-2811-47f9-b42b-b150ed233217}) ---------- @@ -496,130 +255,55 @@ The name of the ParamType (ThingClass: speedwireMeter, Type: thing, ID: {7c81a0c - - + + Sunny WebBox + The name of the ThingClass ({49304127-ce9b-45dd-8511-05030a4ac003}) + + + + Total energy consumed - The name of the ParamType (ThingClass: speedwireMeter, EventType: totalEnergyConsumed, ID: {4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) ----------- -The name of the StateType ({4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) of ThingClass speedwireMeter + The name of the StateType ({4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) of ThingClass speedwireMeter - - Total energy consumed changed - The name of the EventType ({4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) of ThingClass speedwireMeter - - - - - - - - - + + + Total energy produced - The name of the ParamType (ThingClass: speedwireInverter, EventType: totalEnergyProduced, ID: {51cadd66-2cf1-485a-a2a9-191d11abfbd1}) ----------- -The name of the StateType ({51cadd66-2cf1-485a-a2a9-191d11abfbd1}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: totalEnergyProduced, ID: {76ca68d8-6781-4d2a-8663-440aec40b4de}) + The name of the StateType ({51cadd66-2cf1-485a-a2a9-191d11abfbd1}) of ThingClass speedwireInverter ---------- The name of the StateType ({76ca68d8-6781-4d2a-8663-440aec40b4de}) of ThingClass speedwireMeter ---------- -The name of the ParamType (ThingClass: sunnyWebBox, EventType: totalEnergyProduced, ID: {0bb4e227-7e38-49ca-9b32-ce4621c9305b}) ----------- The name of the StateType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox - - - - Total energy produced changed - The name of the EventType ({51cadd66-2cf1-485a-a2a9-191d11abfbd1}) of ThingClass speedwireInverter ----------- -The name of the EventType ({76ca68d8-6781-4d2a-8663-440aec40b4de}) of ThingClass speedwireMeter ----------- -The name of the EventType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox - - - - - - - + + Voltage phase A - The name of the ParamType (ThingClass: speedwireInverter, EventType: voltagePhaseA, ID: {6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) ----------- -The name of the StateType ({6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: voltagePhaseA, ID: {44ee2491-8376-41cd-a21d-185c736152ec}) + The name of the StateType ({6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) of ThingClass speedwireInverter ---------- The name of the StateType ({44ee2491-8376-41cd-a21d-185c736152ec}) of ThingClass speedwireMeter - - - Voltage phase A changed - The name of the EventType ({6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5}) of ThingClass speedwireInverter ----------- -The name of the EventType ({44ee2491-8376-41cd-a21d-185c736152ec}) of ThingClass speedwireMeter - - - - - - - + + Voltage phase B - The name of the ParamType (ThingClass: speedwireInverter, EventType: voltagePhaseB, ID: {d9a5768b-1bf5-4933-810d-84dd7a688f71}) ----------- -The name of the StateType ({d9a5768b-1bf5-4933-810d-84dd7a688f71}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: voltagePhaseB, ID: {56ae3555-f874-4c2d-8833-17573dce477a}) + The name of the StateType ({d9a5768b-1bf5-4933-810d-84dd7a688f71}) of ThingClass speedwireInverter ---------- The name of the StateType ({56ae3555-f874-4c2d-8833-17573dce477a}) of ThingClass speedwireMeter - - - Voltage phase B changed - The name of the EventType ({d9a5768b-1bf5-4933-810d-84dd7a688f71}) of ThingClass speedwireInverter ----------- -The name of the EventType ({56ae3555-f874-4c2d-8833-17573dce477a}) of ThingClass speedwireMeter - - - - - - - + + Voltage phase C - The name of the ParamType (ThingClass: speedwireInverter, EventType: voltagePhaseC, ID: {fc168dc6-eecf-40b4-b214-3e28da0dbb12}) ----------- -The name of the StateType ({fc168dc6-eecf-40b4-b214-3e28da0dbb12}) of ThingClass speedwireInverter ----------- -The name of the ParamType (ThingClass: speedwireMeter, EventType: voltagePhaseC, ID: {51cbb29b-29f0-480a-9d7d-b8f4e6a205ae}) + The name of the StateType ({fc168dc6-eecf-40b4-b214-3e28da0dbb12}) of ThingClass speedwireInverter ---------- The name of the StateType ({51cbb29b-29f0-480a-9d7d-b8f4e6a205ae}) of ThingClass speedwireMeter - - - - Voltage phase C changed - The name of the EventType ({fc168dc6-eecf-40b4-b214-3e28da0dbb12}) of ThingClass speedwireInverter ----------- -The name of the EventType ({51cbb29b-29f0-480a-9d7d-b8f4e6a205ae}) of ThingClass speedwireMeter - - - - - Sunny WebBox - The name of the ThingClass ({49304127-ce9b-45dd-8511-05030a4ac003}) - - From 461ae1906ade2476bd3b58e2f271c22e26113bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 21 Feb 2022 09:50:00 +0100 Subject: [PATCH 16/17] Update README --- sma/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sma/README.md b/sma/README.md index eff2e336..bbb61b63 100644 --- a/sma/README.md +++ b/sma/README.md @@ -8,11 +8,12 @@ nymea plug-in for SMA solar equipment. * SMA speedwire Meters * SMA speedwire Inverters -> Note: the SMA battery equipment is still missing due to testing possibilities. Will be added as soon someone can with the apropriate setup. +> Note: the SMA battery equipment is still missing due to testing possibilities. Will be added as soon someone can with the appropriate setup. ## Requirements * The package "nymea-plugin-sma" must be installed. +* The speedwire port `9522` must not be clocked for UDP packages in the network. ## More https://www.sma.de/en/ From 8ddb88625f16aa303c226ea530025ca7700ffbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 21 Feb 2022 13:04:58 +0100 Subject: [PATCH 17/17] Update copyright year --- sma/integrationpluginsma.cpp | 2 +- sma/integrationpluginsma.h | 2 +- sma/speedwire.h | 2 +- sma/speedwirediscovery.cpp | 2 +- sma/speedwirediscovery.h | 2 +- sma/speedwireinterface.cpp | 2 +- sma/speedwireinterface.h | 2 +- sma/speedwireinverterreply.cpp | 2 +- sma/speedwireinverterreply.h | 2 +- sma/speedwireinverterrequest.cpp | 2 +- sma/speedwireinverterrequest.h | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index db6b75dd..de3c7036 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index d695ae3d..fc509d0b 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwire.h b/sma/speedwire.h index 05fd3603..df92f5c5 100644 --- a/sma/speedwire.h +++ b/sma/speedwire.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwirediscovery.cpp b/sma/speedwirediscovery.cpp index 301c6cfe..4fde8409 100644 --- a/sma/speedwirediscovery.cpp +++ b/sma/speedwirediscovery.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwirediscovery.h b/sma/speedwirediscovery.h index 3c443431..0c181ddf 100644 --- a/sma/speedwirediscovery.h +++ b/sma/speedwirediscovery.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwireinterface.cpp b/sma/speedwireinterface.cpp index 4d7196aa..a5fa1d77 100644 --- a/sma/speedwireinterface.cpp +++ b/sma/speedwireinterface.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwireinterface.h b/sma/speedwireinterface.h index 664ce4a5..fa242873 100644 --- a/sma/speedwireinterface.h +++ b/sma/speedwireinterface.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwireinverterreply.cpp b/sma/speedwireinverterreply.cpp index 95b76114..b7b2941a 100644 --- a/sma/speedwireinverterreply.cpp +++ b/sma/speedwireinverterreply.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwireinverterreply.h b/sma/speedwireinverterreply.h index b35dc5bd..3f6a3a16 100644 --- a/sma/speedwireinverterreply.h +++ b/sma/speedwireinverterreply.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwireinverterrequest.cpp b/sma/speedwireinverterrequest.cpp index fd1befb8..6797c719 100644 --- a/sma/speedwireinverterrequest.cpp +++ b/sma/speedwireinverterrequest.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. diff --git a/sma/speedwireinverterrequest.h b/sma/speedwireinverterrequest.h index bd6cb22a..be0b7216 100644 --- a/sma/speedwireinverterrequest.h +++ b/sma/speedwireinverterrequest.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea.