diff --git a/debian/control b/debian/control
index 7eab6a1..f129778 100644
--- a/debian/control
+++ b/debian/control
@@ -185,6 +185,14 @@ Description: nymea integration plugin for Schrack wallboxes
This package contains the nymea integration plugin for Schrack wallboxes.
+Package: nymea-plugin-sma
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends}
+Description: nymea integration plugin for SMA solar inverters and meters
+ This package contains the nymea integration plugin for SMA solar inverters and meters.
+
+
Package: nymea-plugin-stiebeleltron
Architecture: any
Section: libs
diff --git a/debian/nymea-plugin-sma.install.in b/debian/nymea-plugin-sma.install.in
new file mode 100644
index 0000000..4090f19
--- /dev/null
+++ b/debian/nymea-plugin-sma.install.in
@@ -0,0 +1,2 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsma.so
+sma/translations/*qm usr/share/nymea/translations/
diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro
index 1176e20..5387f04 100644
--- a/nymea-plugins-modbus.pro
+++ b/nymea-plugins-modbus.pro
@@ -18,6 +18,7 @@ PLUGIN_DIRS = \
mypv \
phoenixconnect \
schrack \
+ sma \
stiebeleltron \
sunspec \
unipi \
diff --git a/sma/README.md b/sma/README.md
new file mode 100644
index 0000000..ad56627
--- /dev/null
+++ b/sma/README.md
@@ -0,0 +1,20 @@
+# SMA
+
+nymea plug-in for SMA solar equipment.
+
+## Supported Things
+
+* Sunny WebBox
+* SMA speedwire Meters
+* SMA speedwire Inverters
+* SMA inverters using modbus
+
+> 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 blocked for UDP packages in the network.
+
+## More
+https://www.sma.de/en/
diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp
new file mode 100644
index 0000000..44bc212
--- /dev/null
+++ b/sma/integrationpluginsma.cpp
@@ -0,0 +1,731 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 "integrationpluginsma.h"
+#include "plugininfo.h"
+
+#include "sma.h"
+#include "speedwire/speedwirediscovery.h"
+#include "sunnywebbox/sunnywebboxdiscovery.h"
+#include "modbus/smamodbusdiscovery.h"
+
+#include
+
+IntegrationPluginSma::IntegrationPluginSma()
+{
+
+}
+
+void IntegrationPluginSma::init()
+{
+
+}
+
+void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info)
+{
+ 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;
+ }
+
+ qCDebug(dcSma()) << "Starting Sunny WebBox discovery...";
+ SunnyWebBoxDiscovery *webBoxDiscovery = new SunnyWebBoxDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info);
+ connect(webBoxDiscovery, &SunnyWebBoxDiscovery::discoveryFinished, this, [=](){
+ webBoxDiscovery->deleteLater();
+ ThingDescriptors descriptors;
+ foreach (const NetworkDeviceInfo &networkDeviceInfo, webBoxDiscovery->discoveryResults()) {
+ 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);
+ });
+
+ webBoxDiscovery->startDiscovery();
+
+ } 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, &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);
+ });
+
+ 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::getModelName(result.modelId), "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();
+ } else if (info->thingClassId() == modbusInverterThingClassId) {
+ if (!hardwareManager()->networkDeviceDiscovery()->available()) {
+ qCWarning(dcSma()) << "The network discovery is not available on this platform.";
+ info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available."));
+ return;
+ }
+
+ // Create a discovery with the info as parent for auto deleting the object once the discovery info is done
+ SmaModbusDiscovery *discovery = new SmaModbusDiscovery(hardwareManager()->networkDeviceDiscovery(), 502, 3, info);
+ connect(discovery, &SmaModbusDiscovery::discoveryFinished, info, [=](){
+ foreach (const SmaModbusDiscovery::SmaModbusDiscoveryResult &result, discovery->discoveryResults()) {
+
+ ThingDescriptor descriptor(modbusInverterThingClassId, "SMA inverter " + result.productName, QT_TR_NOOP("Serial: ") + result.serialNumber + " (" + result.networkDeviceInfo.address().toString() + ")");
+ qCDebug(dcSma()) << "Discovered:" << descriptor.title() << descriptor.description();
+
+ // Note: use the serial and not the mac address as identifier because more than one inverter might be behind a network device
+ Things existingThings = myThings().filterByParam(modbusInverterThingSerialNumberParamTypeId, result.serialNumber);
+ if (existingThings.count() == 1) {
+ qCDebug(dcSma()) << "This SMA inverter already exists in the system:" << result.serialNumber;
+ descriptor.setThingId(existingThings.first()->id());
+ }
+
+ ParamList params;
+ params << Param(modbusInverterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
+ params << Param(modbusInverterThingPortParamTypeId, result.port);
+ params << Param(modbusInverterThingSlaveIdParamTypeId, result.modbusAddress);
+ params << Param(modbusInverterThingSerialNumberParamTypeId, result.serialNumber);
+ descriptor.setParams(params);
+ info->addThingDescriptor(descriptor);
+ }
+
+ info->finish(Thing::ThingErrorNoError);
+ });
+
+ // Start the discovery process
+ discovery->startDiscovery();
+ }
+}
+
+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)
+
+ if (info->thingClassId() == speedwireInverterThingClassId) {
+ // 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: The given password 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();
+ qCDebug(dcSma()) << "Setup thing" << thing << thing->params();
+
+ if (thing->thingClassId() == sunnyWebBoxThingClassId) {
+ // Check if a Sunny WebBox is already added with this mac address
+ foreach (SunnyWebBox *sunnyWebBox, m_sunnyWebBoxes.values()) {
+ if (sunnyWebBox->macAddress() == thing->paramValue(sunnyWebBoxThingMacAddressParamTypeId).toString()){
+ qCWarning(dcSma()) << "Thing with mac address" << thing->paramValue(sunnyWebBoxThingMacAddressParamTypeId).toString() << " already added!";
+ info->finish(Thing::ThingErrorThingInUse);
+ return;
+ }
+ }
+
+ if (m_sunnyWebBoxes.contains(thing)) {
+ qCDebug(dcSma()) << "Setup after reconfiguration, cleaning up...";
+ m_sunnyWebBoxes.take(thing)->deleteLater();
+ }
+
+ SunnyWebBox *sunnyWebBox = new SunnyWebBox(hardwareManager()->networkManager(), QHostAddress(thing->paramValue(sunnyWebBoxThingHostParamTypeId).toString()), this);
+ sunnyWebBox->setMacAddress(thing->paramValue(sunnyWebBoxThingMacAddressParamTypeId).toString());
+
+ connect(info, &ThingSetupInfo::aborted, sunnyWebBox, &SunnyWebBox::deleteLater);
+ connect(sunnyWebBox, &SunnyWebBox::destroyed, this, [thing, this] { m_sunnyWebBoxes.remove(thing);});
+
+ QString requestId = sunnyWebBox->getPlantOverview();
+ connect(sunnyWebBox, &SunnyWebBox::plantOverviewReceived, info, [=] (const QString &messageId, SunnyWebBox::Overview overview) {
+ qCDebug(dcSma()) << "Received plant overview" << messageId << "Finish setup";
+ Q_UNUSED(overview)
+
+ info->finish(Thing::ThingErrorNoError);
+ connect(sunnyWebBox, &SunnyWebBox::connectedChanged, this, &IntegrationPluginSma::onConnectedChanged);
+ 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());
+
+ if (m_speedwireMeters.contains(thing)) {
+ m_speedwireMeters.take(thing)->deleteLater();
+ }
+
+ 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;
+ }
+
+ connect(meter, &SpeedwireMeter::reachableChanged, thing, [=](bool reachable){
+ thing->setStateValue(speedwireMeterConnectedStateTypeId, reachable);
+ });
+
+ connect(meter, &SpeedwireMeter::valuesUpdated, thing, [=](){
+ 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 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;
+ }
+
+ 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.";
+
+ // 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;
+ }
+
+ 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, thing, [=](bool reachable){
+ thing->setStateValue(speedwireInverterConnectedStateTypeId, reachable);
+ });
+
+ connect(inverter, &SpeedwireInverter::valuesUpdated, thing, [=](){
+ 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());
+ });
+
+ qCDebug(dcSma()) << "Inverter: Start connecting using password" << password;
+ inverter->startConnecting(password);
+
+ } else if (thing->thingClassId() == modbusInverterThingClassId) {
+
+ // Handle reconfigure
+ if (m_modbusInverters.contains(thing)) {
+ qCDebug(dcSma()) << "Reconfiguring existing thing" << thing->name();
+ m_modbusInverters.take(thing)->deleteLater();
+
+ if (m_monitors.contains(thing)) {
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+ }
+ }
+
+ MacAddress macAddress = MacAddress(thing->paramValue(modbusInverterThingMacAddressParamTypeId).toString());
+ if (!macAddress.isValid()) {
+ qCWarning(dcSma()) << "The configured mac address is not valid" << thing->params();
+ info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing."));
+ return;
+ }
+
+ // Create the monitor
+ NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
+ m_monitors.insert(thing, monitor);
+
+ QHostAddress address = monitor->networkDeviceInfo().address();
+ if (address.isNull()) {
+ qCWarning(dcSma()) << "Cannot set up sma modbus inverter. The host address is not known yet. Maybe it will be available in the next run...";
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+ info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying again later."));
+ return;
+ }
+
+ // Clean up in case the setup gets aborted
+ connect(info, &ThingSetupInfo::aborted, monitor, [=](){
+ if (m_monitors.contains(thing)) {
+ qCDebug(dcSma()) << "Unregister monitor because setup has been aborted.";
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+ }
+ });
+
+ // Wait for the monitor to be ready
+ if (monitor->reachable()) {
+ // Thing already reachable...let's continue with the setup
+ setupModbusInverterConnection(info);
+ } else {
+ qCDebug(dcSma()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "...";
+ connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){
+ if (reachable) {
+ qCDebug(dcSma()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup...";
+ setupModbusInverterConnection(info);
+ }
+ });
+ }
+
+ } else {
+ Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
+ }
+}
+
+void IntegrationPluginSma::postSetupThing(Thing *thing)
+{
+ qCDebug(dcSma()) << "Post setup thing" << thing->name();
+ if (thing->thingClassId() == sunnyWebBoxThingClassId) {
+ SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing);
+ if (!sunnyWebBox)
+ return;
+
+ sunnyWebBox->getPlantOverview();
+ thing->setStateValue("connected", true);
+ setupRefreshTimer();
+
+ } else if (thing->thingClassId() == speedwireInverterThingClassId) {
+ SpeedwireInverter *inverter = m_speedwireInverters.value(thing);
+ if (inverter) {
+ thing->setStateValue("connected", inverter->reachable());
+ } else {
+ thing->setStateValue("connected", false);
+ }
+
+ setupRefreshTimer();
+
+ } else if (thing->thingClassId() == modbusInverterThingClassId) {
+ SmaInverterModbusTcpConnection *connection = m_modbusInverters.value(thing);
+ if (connection) {
+ thing->setStateValue("connected", connection->reachable());
+ } else {
+ thing->setStateValue("connected", false);
+ }
+
+ setupRefreshTimer();
+ }
+}
+
+void IntegrationPluginSma::thingRemoved(Thing *thing)
+{
+ if (thing->thingClassId() == sunnyWebBoxThingClassId) {
+ 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 (thing->thingClassId() == modbusInverterThingClassId && m_modbusInverters.contains(thing)) {
+ m_modbusInverters.take(thing)->deleteLater();
+ }
+
+ if (m_monitors.contains(thing)) {
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
+ }
+
+ if (myThings().isEmpty()) {
+ qCDebug(dcSma()) << "Stopping timer";
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
+ m_refreshTimer = nullptr;
+ }
+}
+
+void IntegrationPluginSma::onConnectedChanged(bool connected)
+{
+ Thing *thing = m_sunnyWebBoxes.key(static_cast(sender()));
+ if (!thing)
+ return;
+
+ thing->setStateValue(sunnyWebBoxConnectedStateTypeId, connected);
+}
+
+void IntegrationPluginSma::onPlantOverviewReceived(const QString &messageId, SunnyWebBox::Overview overview)
+{
+ Q_UNUSED(messageId)
+
+ qCDebug(dcSma()) << "Plant overview received" << overview.status;
+ Thing *thing = m_sunnyWebBoxes.key(static_cast(sender()));
+ if (!thing)
+ return;
+
+ thing->setStateValue(sunnyWebBoxCurrentPowerStateTypeId, overview.power);
+ thing->setStateValue(sunnyWebBoxDayEnergyProducedStateTypeId, overview.dailyYield);
+ thing->setStateValue(sunnyWebBoxTotalEnergyProducedStateTypeId, overview.totalYield);
+ thing->setStateValue(sunnyWebBoxModeStateTypeId, overview.status);
+ if (!overview.error.isEmpty()){
+ qCDebug(dcSma()) << "Received error" << overview.error;
+ thing->setStateValue(sunnyWebBoxErrorStateTypeId, overview.error);
+ }
+}
+
+void IntegrationPluginSma::setupRefreshTimer()
+{
+ // If already set up...
+ if (m_refreshTimer)
+ return;
+
+ m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(5);
+ 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) {
+ // Note: refresh will not be triggered if there is already a refresh process running
+ inverter->refresh();
+ }
+
+ foreach (SmaInverterModbusTcpConnection *connection, m_modbusInverters) {
+ connection->update();
+ }
+ });
+
+ m_refreshTimer->start();
+}
+
+void IntegrationPluginSma::setupModbusInverterConnection(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+
+ QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address();
+ uint port = thing->paramValue(modbusInverterThingPortParamTypeId).toUInt();
+ quint16 slaveId = thing->paramValue(modbusInverterThingSlaveIdParamTypeId).toUInt();
+
+ qCDebug(dcSma()) << "Setting up SMA inverter on" << address.toString() << port << "unit ID:" << slaveId;
+ SmaInverterModbusTcpConnection *connection = new SmaInverterModbusTcpConnection(address, port, slaveId, this);
+ connect(info, &ThingSetupInfo::aborted, connection, &SmaInverterModbusTcpConnection::deleteLater);
+
+ // Reconnect on monitor reachable changed
+ NetworkDeviceMonitor *monitor = m_monitors.value(thing);
+ connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
+ qCDebug(dcSma()) << "Network device monitor reachable changed for" << thing->name() << reachable;
+ if (!thing->setupComplete())
+ return;
+
+ if (reachable && !thing->stateValue("connected").toBool()) {
+ connection->setHostAddress(monitor->networkDeviceInfo().address());
+ connection->connectDevice();
+ } else if (!reachable) {
+ // Note: We disable autoreconnect explicitly and we will
+ // connect the device once the monitor says it is reachable again
+ connection->disconnectDevice();
+ }
+ });
+
+ connect(connection, &SmaInverterModbusTcpConnection::reachableChanged, thing, [this, thing, connection](bool reachable){
+ qCDebug(dcSma()) << "Reachable changed to" << reachable << "for" << thing;
+ if (reachable) {
+ // Connected true will be set after successfull init
+ connection->initialize();
+ } else {
+ thing->setStateValue("connected", false);
+ foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
+ childThing->setStateValue("connected", false);
+ }
+ }
+ });
+
+ connect(connection, &SmaInverterModbusTcpConnection::initializationFinished, thing, [=](bool success){
+ if (!thing->setupComplete())
+ return;
+
+ thing->setStateValue("connected", success);
+ foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
+ childThing->setStateValue("connected", success);
+ }
+
+ if (!success) {
+ // Try once to reconnect the device
+ connection->reconnectDevice();
+ }
+ });
+
+ connect(connection, &SmaInverterModbusTcpConnection::initializationFinished, info, [=](bool success){
+ if (!success) {
+ qCWarning(dcSma()) << "Connection init finished with errors" << thing->name() << connection->hostAddress().toString();
+ hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor);
+ connection->deleteLater();
+ info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the inverter."));
+ return;
+ }
+
+ qCDebug(dcSma()) << "Connection init finished successfully" << connection;
+ m_modbusInverters.insert(thing, connection);
+ info->finish(Thing::ThingErrorNoError);
+
+ // Set connected true
+ thing->setStateValue("connected", true);
+ foreach (Thing *childThing, myThings().filterByParentId(thing->id())) {
+ childThing->setStateValue("connected", true);
+ }
+
+ connect(connection, &SmaInverterModbusTcpConnection::updateFinished, thing, [=](){
+ qCDebug(dcSma()) << "Updated" << connection;
+
+ // Grid voltage
+ if (isModbusValueValid(connection->gridVoltagePhaseA()))
+ thing->setStateValue(modbusInverterVoltagePhaseAStateTypeId, connection->gridVoltagePhaseA() / 100.0);
+
+ if (isModbusValueValid(connection->gridVoltagePhaseB()))
+ thing->setStateValue(modbusInverterVoltagePhaseBStateTypeId, connection->gridVoltagePhaseB() / 100.0);
+
+ if (isModbusValueValid(connection->gridVoltagePhaseC()))
+ thing->setStateValue(modbusInverterVoltagePhaseCStateTypeId, connection->gridVoltagePhaseC() / 100.0);
+
+ // Grid current
+ if (isModbusValueValid(connection->gridCurrentPhaseA()))
+ thing->setStateValue(modbusInverterCurrentPhaseAStateTypeId, connection->gridCurrentPhaseA() / 1000.0);
+
+ if (isModbusValueValid(connection->gridCurrentPhaseB()))
+ thing->setStateValue(modbusInverterCurrentPhaseBStateTypeId, connection->gridCurrentPhaseB() / 1000.0);
+
+ if (isModbusValueValid(connection->gridCurrentPhaseC()))
+ thing->setStateValue(modbusInverterCurrentPhaseCStateTypeId, connection->gridCurrentPhaseC() / 1000.0);
+
+ // Phase power
+ if (isModbusValueValid(connection->currentPowerPhaseA()))
+ thing->setStateValue(modbusInverterCurrentPowerPhaseAStateTypeId, connection->currentPowerPhaseA());
+
+ if (isModbusValueValid(connection->currentPowerPhaseB()))
+ thing->setStateValue(modbusInverterCurrentPowerPhaseBStateTypeId, connection->currentPowerPhaseB());
+
+ if (isModbusValueValid(connection->currentPowerPhaseC()))
+ thing->setStateValue(modbusInverterCurrentPowerPhaseCStateTypeId, connection->currentPowerPhaseC());
+
+ // Others
+ if (isModbusValueValid(connection->totalYield()))
+ thing->setStateValue(modbusInverterTotalEnergyProducedStateTypeId, connection->totalYield() / 1000.0); // kWh
+
+ if (isModbusValueValid(connection->dailyYield()))
+ thing->setStateValue(modbusInverterEnergyProducedTodayStateTypeId, connection->dailyYield() / 1000.0); // kWh
+
+ // Power
+ if (isModbusValueValid(connection->currentPower()))
+ thing->setStateValue(modbusInverterCurrentPowerStateTypeId, -connection->currentPower());
+
+ // Version
+ thing->setStateValue(modbusInverterFirmwareVersionStateTypeId, Sma::buildSoftwareVersionString(connection->softwarePackage()));
+ });
+
+ // Update registers
+ connection->update();
+ });
+
+ connection->connectDevice();
+}
+
+bool IntegrationPluginSma::isModbusValueValid(quint32 value)
+{
+ return value != 0xffffffff;
+}
+
+bool IntegrationPluginSma::isModbusValueValid(qint32 value)
+{
+ return value != static_cast(0x80000000);
+}
+
+bool IntegrationPluginSma::isModbusValueValid(quint64 value)
+{
+ return value != 0xffffffffffffffff;
+}
+
diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h
new file mode 100644
index 0000000..50609bf
--- /dev/null
+++ b/sma/integrationpluginsma.h
@@ -0,0 +1,88 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 INTEGRATIONPLUGINSMA_H
+#define INTEGRATIONPLUGINSMA_H
+
+#include
+#include
+#include
+
+#include "extern-plugininfo.h"
+
+#include "sunnywebbox/sunnywebbox.h"
+#include "speedwire/speedwiremeter.h"
+#include "speedwire/speedwireinverter.h"
+
+#include "smainvertermodbustcpconnection.h"
+
+class IntegrationPluginSma: public IntegrationPlugin {
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsma.json")
+ Q_INTERFACES(IntegrationPlugin)
+
+public:
+ explicit IntegrationPluginSma();
+
+ 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;
+
+private slots:
+ void onConnectedChanged(bool connected);
+ void onPlantOverviewReceived(const QString &messageId, SunnyWebBox::Overview overview);
+
+ void setupRefreshTimer();
+
+ void setupModbusInverterConnection(ThingSetupInfo *info);
+
+private:
+ PluginTimer *m_refreshTimer = nullptr;
+
+ QHash m_monitors;
+
+ QHash m_sunnyWebBoxes;
+ QHash m_speedwireMeters;
+ QHash m_speedwireInverters;
+ QHash m_modbusInverters;
+
+ // Sma modbus data validation
+ bool isModbusValueValid(quint32 value);
+ bool isModbusValueValid(qint32 value);
+ bool isModbusValueValid(quint64 value);
+};
+
+#endif // INTEGRATIONPLUGINSMA_H
diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json
new file mode 100644
index 0000000..e79f74f
--- /dev/null
+++ b/sma/integrationpluginsma.json
@@ -0,0 +1,633 @@
+{
+ "id": "b8442bbf-9d3f-4aa2-9443-b3a31ae09bac",
+ "name": "sma",
+ "displayName": "SMA",
+ "vendors": [
+ {
+ "id": "16d5a4a3-36d5-46c0-b7dd-df166ddf5981",
+ "name": "Sma",
+ "displayName": "SMA Solar Technology AG",
+ "thingClasses": [
+ {
+ "id": "49304127-ce9b-45dd-8511-05030a4ac003",
+ "name": "sunnyWebBox",
+ "displayName": "SMA Sunny WebBox",
+ "createMethods": ["discovery", "user"],
+ "interfaces": ["solarinverter"],
+ "paramTypes": [
+ {
+ "id": "864d4162-e3ce-48b8-b8ac-c1b971b52d42",
+ "name": "host",
+ "displayName": "Host address",
+ "type": "QString",
+ "inputType": "IPv4Address",
+ "defaultValue": "192.168.0.168"
+ },
+ {
+ "id": "03f32361-4e13-4597-a346-af8d16a986b3",
+ "name": "macAddress",
+ "displayName": "MAC address",
+ "type": "QString",
+ "inputType": "TextLine",
+ "readOnly": true,
+ "defaultValue": "00:00:00:00:00:00"
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "c05e6a1a-252c-4f2b-8b31-09cf113d01c1",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2",
+ "name": "currentPower",
+ "displayName": "Current power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "16f34c5c-8dbb-4dcc-9faa-4b782d57226c",
+ "name": "dayEnergyProduced",
+ "displayName": "Day energy produced",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0
+ },
+ {
+ "id": "0bb4e227-7e38-49ca-9b32-ce4621c9305b",
+ "name": "totalEnergyProduced",
+ "displayName": "Total energy produced",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0
+ },
+ {
+ "id": "1974550b-6059-4b0e-83f4-70177e20dac3",
+ "name": "mode",
+ "displayName": "Mode",
+ "type": "QString",
+ "defaultValue": "MPP"
+ },
+ {
+ "id": "4e64f9ca-7e5a-4897-8035-6f2ae88fde89",
+ "name": "error",
+ "displayName": "Error",
+ "type": "QString",
+ "defaultValue": "None"
+ }
+ ]
+ },
+ {
+ "id": "0c5097af-e136-4430-9fb4-0ccbb30c3e1c",
+ "name": "speedwireMeter",
+ "displayName": "SMA Energy Meter",
+ "createMethods": ["discovery", "user"],
+ "interfaces": [ "energymeter" ],
+ "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": "MAC address",
+ "type": "QString",
+ "inputType": "TextLine",
+ "readOnly": true,
+ "defaultValue": ""
+ },
+ {
+ "id": "7c81a0c5-9bc6-43bb-a01a-4de5fe656bba",
+ "name": "serialNumber",
+ "displayName": "Serial number",
+ "type": "QString",
+ "inputType": "TextLine",
+ "readOnly": true,
+ "defaultValue": ""
+ },
+ {
+ "id": "abdc114d-1fac-4454-8b82-871ed5cdf28c",
+ "name": "modelId",
+ "displayName": "Model ID",
+ "type": "uint",
+ "inputType": "TextLine",
+ "readOnly": true,
+ "defaultValue": ""
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "35733d27-4fe0-439a-be71-7c1597481659",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false
+ },
+ {
+ "id": "44ee2491-8376-41cd-a21d-185c736152ec",
+ "name": "voltagePhaseA",
+ "displayName": "Voltage phase A",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "56ae3555-f874-4c2d-8833-17573dce477a",
+ "name": "voltagePhaseB",
+ "displayName": "Voltage phase B",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "51cbb29b-29f0-480a-9d7d-b8f4e6a205ae",
+ "name": "voltagePhaseC",
+ "displayName": "Voltage phase C",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "45bbdbef-1832-4870-bff5-299e580fb4da",
+ "name": "currentPhaseA",
+ "displayName": "Current phase A",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "b3a4fdd2-b6b8-4c58-9da3-2084ad414022",
+ "name": "currentPhaseB",
+ "displayName": "Current phase B",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "b3655188-3854-4336-ae3c-61d3bda6fc4d",
+ "name": "currentPhaseC",
+ "displayName": "Current phase C",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "d4ac7f37-e30a-44e4-93cb-ad16df18b8f1",
+ "name": "currentPower",
+ "displayName": "Current power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "c5d09c63-7461-4fb8-a6fe-bc7aa919be30",
+ "name": "currentPowerPhaseA",
+ "displayName": "Current power phase A",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "c52d4422-b521-4804-a7a7-c4398e91e760",
+ "name": "currentPowerPhaseB",
+ "displayName": "Current power phase B",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "555e892c-3ca7-4100-9832-6ac13b87eb04",
+ "name": "currentPowerPhaseC",
+ "displayName": "Current power phase C",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d",
+ "name": "totalEnergyConsumed",
+ "displayName": "Total energy consumed",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "76ca68d8-6781-4d2a-8663-440aec40b4de",
+ "name": "totalEnergyProduced",
+ "displayName": "Total energy produced",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33",
+ "name": "energyConsumedPhaseA",
+ "displayName": "Energy consumed phase A",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "c4e5f569-ac5d-4761-a898-888880bfd59f",
+ "name": "energyConsumedPhaseB",
+ "displayName": "Energy consumed phase B",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3",
+ "name": "energyConsumedPhaseC",
+ "displayName": "Energy consumed phase C",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "754c3b67-768a-47f7-99d8-f66c198f0835",
+ "name": "energyProducedPhaseA",
+ "displayName": "Energy produced phase A",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "7eb08c45-24cf-40ce-be28-f3564f087672",
+ "name": "energyProducedPhaseB",
+ "displayName": "Energy produced phase B",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "1eb2bf01-5ec6-42e5-b348-ac1e95199d14",
+ "name": "energyProducedPhaseC",
+ "displayName": "Energy produced phase C",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "a685393c-8b7e-42c5-bb41-f9907c074626",
+ "name": "firmwareVersion",
+ "displayName": "Firmware version",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ]
+ },
+ {
+ "id": "b63a0669-f2ac-4769-abea-e14cafb2309a",
+ "name": "speedwireInverter",
+ "displayName": "SMA Inverter",
+ "createMethods": ["discovery", "user"],
+ "setupMethod": "EnterPin",
+ "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,
+ "defaultValue": ""
+ },
+ {
+ "id": "e42242b4-2811-47f9-b42b-b150ed233217",
+ "name": "serialNumber",
+ "displayName": "Serial number",
+ "type": "QString",
+ "inputType": "TextLine",
+ "readOnly": true,
+ "defaultValue": ""
+ },
+ {
+ "id": "d9892f74-5b93-4c98-8da2-72aca033273a",
+ "name": "modelId",
+ "displayName": "Model ID",
+ "type": "uint",
+ "inputType": "TextLine",
+ "readOnly": true,
+ "defaultValue": 0
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "aaff72c3-c70a-4a2f-bed1-89f38cebe442",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false
+ },
+ {
+ "id": "6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5",
+ "name": "voltagePhaseA",
+ "displayName": "Voltage phase A",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "d9a5768b-1bf5-4933-810d-84dd7a688f71",
+ "name": "voltagePhaseB",
+ "displayName": "Voltage phase B",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "fc168dc6-eecf-40b4-b214-3e28da0dbb12",
+ "name": "voltagePhaseC",
+ "displayName": "Voltage phase C",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "2a6c59ca-853a-47d6-96fb-0c85edf32f52",
+ "name": "currentPhaseA",
+ "displayName": "Current phase A",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "4db96fec-737c-4c4b-bf07-5ef2fd62508a",
+ "name": "currentPhaseB",
+ "displayName": "Current phase B",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "0f23fb0e-a440-4ac2-9aff-896bc65feb2c",
+ "name": "currentPhaseC",
+ "displayName": "Current phase C",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "d7ceb482-5df8-4c0c-82bd-62ce7ba22c43",
+ "name": "currentPower",
+ "displayName": "Current power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "b366f680-6134-488b-8362-b1b824a8daca",
+ "name": "currentPowerMpp1",
+ "displayName": "DC power MPP1",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "87d9b654-5558-47a3-9db9-ffd7c23b4774",
+ "name": "currentPowerMpp2",
+ "displayName": "DC power MPP2",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "51cadd66-2cf1-485a-a2a9-191d11abfbd1",
+ "name": "totalEnergyProduced",
+ "displayName": "Total energy produced",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "e8bc8f81-e5c5-4900-b429-93fcaa262fcb",
+ "name": "energyProducedToday",
+ "displayName": "Energy produced today",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "fdccf5de-7413-4480-9ca0-1151665dede8",
+ "name": "frequency",
+ "displayName": "Frequency",
+ "type": "double",
+ "unit": "Hertz",
+ "defaultValue": 0.00,
+ "cached": false
+ },
+ {
+ "id": "6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66",
+ "name": "firmwareVersion",
+ "displayName": "Firmware version",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ]
+ },
+ {
+ "id": "12e0429e-e8ce-48bd-a11c-faaf0bd71856",
+ "name": "modbusInverter",
+ "displayName": "SMA Inverter (Modbus)",
+ "createMethods": ["discovery", "user"],
+ "interfaces": [ "solarinverter" ],
+ "paramTypes": [
+ {
+ "id": "3cea46a0-9535-4612-9971-19167109e63c",
+ "name":"macAddress",
+ "displayName": "MAC address",
+ "type": "QString",
+ "inputType": "MacAddress",
+ "defaultValue": ""
+ },
+ {
+ "id": "18ded0c1-308e-4a13-a12c-cf9a8ed5a26c",
+ "name":"port",
+ "displayName": "Port",
+ "type": "int",
+ "defaultValue": 502
+ },
+ {
+ "id": "6322db2a-0554-4f83-9509-39870ad89027",
+ "name":"slaveId",
+ "displayName": "Slave ID",
+ "type": "int",
+ "defaultValue": 3
+ },
+ {
+ "id": "563f2b12-b784-4a2c-856f-57a2b5ce2e9d",
+ "name":"serialNumber",
+ "displayName": "Serial number",
+ "type": "QString",
+ "defaultValue": "",
+ "readOnly": true
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "3c60e2a7-31f3-4b0b-a3f9-ede042e82f22",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "5a717aff-6bdb-4679-94d6-ec1bce7fa2af",
+ "name": "voltagePhaseA",
+ "displayName": "Voltage phase A",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "34eb5b54-7683-42ff-8320-9b2527d6381c",
+ "name": "voltagePhaseB",
+ "displayName": "Voltage phase B",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "2bd0a069-9d16-4d58-9f78-df682d92005d",
+ "name": "voltagePhaseC",
+ "displayName": "Voltage phase C",
+ "type": "double",
+ "unit": "Volt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "48d4a7b7-b09a-4255-83dd-9eab8ea3a51c",
+ "name": "currentPhaseA",
+ "displayName": "Current phase A",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "479b27c4-01fc-45ef-a462-b8d8499b3422",
+ "name": "currentPhaseB",
+ "displayName": "Current phase B",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "f82bbba1-c68a-4c43-a3e5-10b00ed924d7",
+ "name": "currentPhaseC",
+ "displayName": "Current phase C",
+ "type": "double",
+ "unit": "Ampere",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "225beb67-95ca-495c-aca8-cd3fd4efedd5",
+ "name": "currentPower",
+ "displayName": "Current power",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "9283d5a9-b185-4678-beb1-1c6ce6f76930",
+ "name": "currentPowerPhaseA",
+ "displayName": "Current power phase A",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "8a87319c-f6ab-4eb1-bb17-a65f80289a56",
+ "name": "currentPowerPhaseB",
+ "displayName": "Current power phase B",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "1f930456-5947-476c-b74b-480f1e81a799",
+ "name": "currentPowerPhaseC",
+ "displayName": "Current power phase C",
+ "type": "double",
+ "unit": "Watt",
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "5e0ed108-7e93-4724-a831-319109d9daf8",
+ "name": "totalEnergyProduced",
+ "displayName": "Total energy produced",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "b8fb66fa-46b5-4ed7-82a7-29fe5257caa9",
+ "name": "energyProducedToday",
+ "displayName": "Energy produced today",
+ "type": "double",
+ "unit": "KiloWattHour",
+ "defaultValue": 0.00
+ },
+ {
+ "id": "3f290cbc-0578-479a-ab98-d89b5549184d",
+ "name": "firmwareVersion",
+ "displayName": "Firmware version",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/sma/meta.json b/sma/meta.json
new file mode 100644
index 0000000..be87594
--- /dev/null
+++ b/sma/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "SMA",
+ "tagline": "Connect to SMA solar equipment.",
+ "icon": "sma.png",
+ "stability": "consumer",
+ "offline": true,
+ "technologies": [
+ "network"
+ ],
+ "categories": [
+ "energy"
+ ]
+}
diff --git a/sma/modbus/smamodbusdiscovery.cpp b/sma/modbus/smamodbusdiscovery.cpp
new file mode 100644
index 0000000..c2621d8
--- /dev/null
+++ b/sma/modbus/smamodbusdiscovery.cpp
@@ -0,0 +1,176 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 "smamodbusdiscovery.h"
+#include "extern-plugininfo.h"
+
+#include "sma.h"
+
+
+SmaModbusDiscovery::SmaModbusDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, quint16 modbusAddress,QObject *parent)
+ : QObject{parent},
+ m_networkDeviceDiscovery{networkDeviceDiscovery},
+ m_port{port},
+ m_modbusAddress{modbusAddress}
+{
+
+}
+
+void SmaModbusDiscovery::startDiscovery()
+{
+ qCInfo(dcSma()) << "Discovery: Start searching for SMA modbus inverters in the network...";
+ NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
+
+ // Imedialty check any new device gets discovered
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &SmaModbusDiscovery::checkNetworkDevice);
+
+ // Check what might be left on finished
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
+ qCDebug(dcSma()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
+
+ // Send a report request to nework device info not sent already...
+ foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
+ if (!m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) {
+ checkNetworkDevice(networkDeviceInfo);
+ }
+ }
+
+ // Give the last connections added right before the network discovery finished a chance to check the device...
+ QTimer::singleShot(3000, this, [this](){
+ qCDebug(dcSma()) << "Discovery: Grace period timer triggered.";
+ finishDiscovery();
+ });
+ });
+}
+
+QList SmaModbusDiscovery::discoveryResults() const
+{
+ return m_discoveryResults;
+}
+
+void SmaModbusDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
+{
+ // Create a kostal connection and try to initialize it.
+ // Only if initialized successfully and all information have been fetched correctly from
+ // the device we can assume this is what we are locking for (ip, port, modbus address, correct registers).
+ // We cloud tough also filter the result only for certain software versions, manufactueres or whatever...
+
+ if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo))
+ return;
+
+ SmaInverterModbusTcpConnection *connection = new SmaInverterModbusTcpConnection(networkDeviceInfo.address(), m_port, m_modbusAddress, this);
+ m_connections.append(connection);
+ m_verifiedNetworkDeviceInfos.append(networkDeviceInfo);
+
+ connect(connection, &SmaInverterModbusTcpConnection::reachableChanged, this, [=](bool reachable){
+ if (!reachable) {
+ // Disconnected ... done with this connection
+ cleanupConnection(connection);
+ return;
+ }
+
+ // Modbus TCP connected...ok, let's try to initialize it!
+ connect(connection, &SmaInverterModbusTcpConnection::initializationFinished, this, [=](bool success){
+ if (!success) {
+ qCDebug(dcSma()) << "Discovery: Initialization failed on" << networkDeviceInfo.address().toString() << "Continue...";;
+ cleanupConnection(connection);
+ return;
+ }
+
+ if (connection->deviceClass() != Sma::DeviceClassSolarInverter) {
+ qCDebug(dcSma()) << "Discovery: Initialization successfull for" << networkDeviceInfo.address().toString() << "but the device class is not an inverter. Continue...";;
+ cleanupConnection(connection);
+ return;
+ }
+
+ SmaModbusDiscoveryResult result;
+ result.productName = Sma::getModelName(connection->modelIdentifier());
+ result.deviceName = connection->deviceName();
+ result.serialNumber = QString::number(connection->serialNumber());
+ result.port = m_port;
+ result.modbusAddress = m_modbusAddress;
+ result.softwareVersion = Sma::buildSoftwareVersionString(connection->softwarePackage());
+ result.networkDeviceInfo = networkDeviceInfo;
+ m_discoveryResults.append(result);
+
+ qCDebug(dcSma()) << "Discovery: --> Found" << result.productName;
+ qCDebug(dcSma()) << " Device name:" << result.deviceName;
+ qCDebug(dcSma()) << " Serial number:" << result.serialNumber;
+ qCDebug(dcSma()) << " Software version:" << result.softwareVersion;
+ qCDebug(dcSma()) << " " << result.networkDeviceInfo;
+
+ // Done with this connection
+ cleanupConnection(connection);
+ });
+
+ // Initializing...
+ if (!connection->initialize()) {
+ qCDebug(dcSma()) << "Discovery: Unable to initialize connection on" << networkDeviceInfo.address().toString() << "Continue...";;
+ cleanupConnection(connection);
+ }
+ });
+
+ // If we get any error...skip this host...
+ connect(connection, &SmaInverterModbusTcpConnection::connectionErrorOccurred, this, [=](QModbusDevice::Error error){
+ if (error != QModbusDevice::NoError) {
+ qCDebug(dcSma()) << "Discovery: Connection error on" << networkDeviceInfo.address().toString() << "Continue...";;
+ cleanupConnection(connection);
+ }
+ });
+
+ // If check reachability failed...skip this host...
+ connect(connection, &SmaInverterModbusTcpConnection::checkReachabilityFailed, this, [=](){
+ qCDebug(dcSma()) << "Discovery: Check reachability failed on" << networkDeviceInfo.address().toString() << "Continue...";;
+ cleanupConnection(connection);
+ });
+
+ // Try to connect, maybe it works, maybe not...
+ connection->connectDevice();
+}
+
+void SmaModbusDiscovery::cleanupConnection(SmaInverterModbusTcpConnection *connection)
+{
+ m_connections.removeAll(connection);
+ connection->disconnectDevice();
+ connection->deleteLater();
+}
+
+void SmaModbusDiscovery::finishDiscovery()
+{
+ qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
+
+ // Cleanup any leftovers...we don't care any more
+ foreach (SmaInverterModbusTcpConnection *connection, m_connections)
+ cleanupConnection(connection);
+
+ qCInfo(dcSma()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() << "SMA inverters in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
+ emit discoveryFinished();
+}
diff --git a/sma/modbus/smamodbusdiscovery.h b/sma/modbus/smamodbusdiscovery.h
new file mode 100644
index 0000000..0ed6290
--- /dev/null
+++ b/sma/modbus/smamodbusdiscovery.h
@@ -0,0 +1,82 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 SMAMODBUSDISCOVERY_H
+#define SMAMODBUSDISCOVERY_H
+
+#include
+#include
+
+#include
+
+#include "smainvertermodbustcpconnection.h"
+
+class SmaModbusDiscovery : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SmaModbusDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 502, quint16 modbusAddress = 3, QObject *parent = nullptr);
+ typedef struct SmaModbusDiscoveryResult {
+ QString productName;
+ QString deviceName;
+ QString serialNumber;
+ quint16 port;
+ quint16 modbusAddress;
+ QString softwareVersion;
+ NetworkDeviceInfo networkDeviceInfo;
+ } SmaModbusDiscoveryResult;
+
+ void startDiscovery();
+
+ QList discoveryResults() const;
+
+signals:
+ void discoveryFinished();
+
+private:
+ NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
+ quint16 m_port;
+ quint16 m_modbusAddress;
+
+ QDateTime m_startDateTime;
+ NetworkDeviceInfos m_verifiedNetworkDeviceInfos;
+
+ QList m_connections;
+
+ QList m_discoveryResults;
+
+ void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
+ void cleanupConnection(SmaInverterModbusTcpConnection *connection);
+
+ void finishDiscovery();
+
+};
+
+#endif // SMAMODBUSDISCOVERY_H
diff --git a/sma/sma-inverter-registers.json b/sma/sma-inverter-registers.json
new file mode 100644
index 0000000..6132087
--- /dev/null
+++ b/sma/sma-inverter-registers.json
@@ -0,0 +1,269 @@
+{
+ "className": "SmaInverter",
+ "protocol": "TCP",
+ "endianness": "BigEndian",
+ "errorLimitUntilNotReachable": 20,
+ "checkReachableRegister": "totalYield",
+ "enums": [
+ {
+ "name": "Condition",
+ "values": [
+ {
+ "key": "Fault",
+ "value": 35
+ },
+ {
+ "key": "Off",
+ "value": 303
+ },
+ {
+ "key": "Ok",
+ "value": 307
+ },
+ {
+ "key": "Warning",
+ "value": 455
+ }
+ ]
+ },
+ {
+ "name": "RecommendedAction",
+ "values": [
+ {
+ "key": "ContactManufacturer",
+ "value": 336
+ },
+ {
+ "key": "ContactInstaller",
+ "value": 337
+ },
+ {
+ "key": "Invalid",
+ "value": 338
+ },
+ {
+ "key": "None",
+ "value": 887
+ }
+ ]
+ }
+ ],
+ "blocks": [
+ {
+ "id": "identification",
+ "readSchedule": "init",
+ "registers": [
+ {
+ "id": "deviceClass",
+ "address": 30051,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Device class",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "modelIdentifier",
+ "address": 30053,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Device type (model identifier)",
+ "defaultValue": "0",
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "information",
+ "readSchedule": "init",
+ "registers": [
+ {
+ "id": "serialNumber",
+ "address": 30057,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Serial number",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "softwarePackage",
+ "address": 30059,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Firmware version",
+ "defaultValue": "0",
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "yield",
+ "readSchedule": "update",
+ "registers": [
+ {
+ "id": "totalYield",
+ "address": 30513,
+ "size": 4,
+ "type": "uint64",
+ "readSchedule": "update",
+ "registerType": "holdingRegister",
+ "description": "Total yield",
+ "unit": "Wh",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "dailyYield",
+ "address": 30517,
+ "size": 4,
+ "type": "uint64",
+ "readSccalchedule": "update",
+ "registerType": "holdingRegister",
+ "description": "Today yield",
+ "unit": "Wh",
+ "defaultValue": "0",
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "data",
+ "readSchedule": "update",
+ "registers": [
+ {
+ "id": "currentPower",
+ "address": 30775,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Current power",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "currentPowerPhaseA",
+ "address": 30777,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Current power L1",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "currentPowerPhaseB",
+ "address": 30779,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Current power L2",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "currentPowerPhaseC",
+ "address": 30781,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Current power L3",
+ "unit": "W",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "gridVoltagePhaseA",
+ "address": 30783,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Grid voltage L1",
+ "unit": "V",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "gridVoltagePhaseB",
+ "address": 30785,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Grid voltage L2",
+ "unit": "V",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "gridVoltagePhaseC",
+ "address": 30787,
+ "size": 2,
+ "type": "uint32",
+ "registerType": "holdingRegister",
+ "description": "Grid voltage L3",
+ "unit": "V",
+ "defaultValue": "0",
+ "access": "RO"
+ }
+ ]
+ },
+ {
+ "id": "gridCurrent",
+ "readSchedule": "update",
+ "registers": [
+ {
+ "id": "gridCurrentPhaseA",
+ "address": 30977,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Grid current L1",
+ "unit": "A",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "gridCurrentPhaseB",
+ "address": 30979,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Grid current L2",
+ "unit": "A",
+ "defaultValue": "0",
+ "access": "RO"
+ },
+ {
+ "id": "gridCurrentPhaseC",
+ "address": 30981,
+ "size": 2,
+ "type": "int32",
+ "registerType": "holdingRegister",
+ "description": "Grid current L3",
+ "unit": "A",
+ "defaultValue": "0",
+ "access": "RO"
+ }
+ ]
+ }
+ ],
+ "registers": [
+ {
+ "id": "deviceName",
+ "address": 40631,
+ "size": 32,
+ "type": "string",
+ "readSchedule": "init",
+ "registerType": "holdingRegister",
+ "description": "Device name",
+ "access": "RO"
+ }
+ ]
+}
diff --git a/sma/sma.h b/sma/sma.h
new file mode 100644
index 0000000..fef1fcf
--- /dev/null
+++ b/sma/sma.h
@@ -0,0 +1,239 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 SMA_H
+#define SMA_H
+
+#include
+#include
+#include
+#include
+
+class Sma
+{
+ Q_GADGET
+
+public:
+ enum DeviceClass {
+ DeviceClassUnknown = 0x0000,
+ DeviceClassAllDevices = 0x1f40,
+ DeviceClassSolarInverter = 0x1f41,
+ DeviceClassWindTurbine = 0x1f42,
+ DeviceClassBatteryInverter = 0x1f47,
+ DeviceClassConsumer = 0x1f61,
+ DeviceClassSensorSystem = 0x1f80,
+ DeviceClassElectricityMeter = 0x1f81,
+ DeviceClassCommunicationProduct = 0x1fc0
+ };
+ Q_ENUM(DeviceClass)
+
+ inline static QString buildSoftwareVersionString(quint32 versionData) {
+
+ // Software version
+ QByteArray rawData;
+ QDataStream stream(&rawData, QIODevice::ReadWrite);
+ stream << versionData;
+
+ quint8 major = static_cast(rawData.at(0));
+ quint8 minor = static_cast(rawData.at(1));
+ quint8 build = static_cast(rawData.at(2));
+ quint8 revision = static_cast(rawData.at(3));;
+
+ // Revision types:
+ // 0 -> N: No revision
+ // 1 -> E: Experimental version
+ // 2 -> A: Alpha version
+ // 3 -> B: Beta version
+ // 4 -> R: Release version
+ // 5 -> S: Special version
+ QChar revisionCharacter;
+ switch (revision) {
+ case 0:
+ revisionCharacter = 'N';
+ break;
+ case 1:
+ revisionCharacter = 'E';
+ break;
+ case 2:
+ revisionCharacter = 'A';
+ break;
+ case 3:
+ revisionCharacter = 'B';
+ break;
+ case 4:
+ revisionCharacter = 'R';
+ break;
+ case 5:
+ revisionCharacter = 'S';
+ break;
+ }
+
+ return QString("%1.%2.%3-%4").arg(major).arg(minor).arg(build).arg(revisionCharacter);
+ }
+
+ inline static QString getModelName(quint16 modelIdentifier) {
+ switch (modelIdentifier) {
+ // Modbus
+ case 9225: return "SB 5000SE-10";
+ case 9226: return "SB 3600SE-10";
+ case 9165: return "SB 3600TL-21";
+ case 9075: return "SB 4000TL-21";
+ case 9076: return "SB 5000TL-21";
+ case 9162: return "SB 3500TL-JP-22";
+ case 9164: return "SB 4500TL-JP-22";
+ case 9198: return "SB 3000TL-US-22";
+ case 9199: return "SB 3800TL-US-22";
+ case 9200: return "SB 4000TL-US-22";
+ case 9201: return "SB 5000TL-US-22";
+ case 9274: return "SB 6000TL-US-22";
+ case 9275: return "SB 7000TL-US-22";
+ case 9293: return "SB 7700TL-US-22";
+ case 9222: return "STP 10000TLEE-JP-10";
+ case 9194: return "STP 12000TL-US-10";
+ case 9195: return "STP 15000TL-US-10";
+ case 9196: return "STP 20000TL-US-10";
+ case 9197: return "STP 24000TL-US-10";
+ case 9310: return "STP 30000TL-US-10";
+ case 9271: return "STP 20000TLEE-JP-11";
+ case 9272: return "STP 10000TLEE-JP-11";
+ case 9354: return "STP 24500TL-JP-30";
+ case 9311: return "STP 25000TL-JP-30";
+ case 9223: return "SI6.0H-11";
+ case 9224: return "SI8.0H-11";
+
+ // Speedwire / Modbus
+ 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 9074: return "SB 3000TL-21";
+ 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";
+ }
+ };
+};
+
+#endif // SMA_H
diff --git a/sma/sma.png b/sma/sma.png
new file mode 100644
index 0000000..3273ac8
Binary files /dev/null and b/sma/sma.png differ
diff --git a/sma/sma.pro b/sma/sma.pro
new file mode 100644
index 0000000..ce064d7
--- /dev/null
+++ b/sma/sma.pro
@@ -0,0 +1,34 @@
+include(../plugins.pri)
+
+QT += network
+
+# Generate modbus connection
+MODBUS_CONNECTIONS += sma-inverter-registers.json
+MODBUS_TOOLS_CONFIG += VERBOSE
+include(../modbus.pri)
+
+SOURCES += \
+ integrationpluginsma.cpp \
+ modbus/smamodbusdiscovery.cpp \
+ speedwire/speedwirediscovery.cpp \
+ speedwire/speedwireinterface.cpp \
+ speedwire/speedwireinverter.cpp \
+ speedwire/speedwireinverterreply.cpp \
+ speedwire/speedwireinverterrequest.cpp \
+ speedwire/speedwiremeter.cpp \
+ sunnywebbox/sunnywebbox.cpp \
+ sunnywebbox/sunnywebboxdiscovery.cpp
+
+HEADERS += \
+ integrationpluginsma.h \
+ modbus/smamodbusdiscovery.h \
+ sma.h \
+ speedwire/speedwire.h \
+ speedwire/speedwirediscovery.h \
+ speedwire/speedwireinterface.h \
+ speedwire/speedwireinverter.h \
+ speedwire/speedwireinverterreply.h \
+ speedwire/speedwireinverterrequest.h \
+ speedwire/speedwiremeter.h \
+ sunnywebbox/sunnywebbox.h \
+ sunnywebbox/sunnywebboxdiscovery.h
diff --git a/sma/speedwire/speedwire.h b/sma/speedwire/speedwire.h
new file mode 100644
index 0000000..b1f1734
--- /dev/null
+++ b/sma/speedwire/speedwire.h
@@ -0,0 +1,183 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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)
+
+ 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 InverterPacket {
+ 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;
+ } InverterPacket;
+
+ 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; }
+
+
+ // 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::InverterPacket parseInverterPacket(QDataStream &stream) {
+ // Make sure the data stream is little endian
+ stream.setByteOrder(QDataStream::LittleEndian);
+ 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;
+ };
+};
+
+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::InverterPacket &packet)
+{
+ 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();
+}
+
+
+#endif // SPEEDWIRE_H
diff --git a/sma/speedwire/speedwirediscovery.cpp b/sma/speedwire/speedwirediscovery.cpp
new file mode 100644
index 0000000..83547f1
--- /dev/null
+++ b/sma/speedwire/speedwirediscovery.cpp
@@ -0,0 +1,348 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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"
+
+#include
+
+SpeedwireDiscovery::SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) :
+ QObject(parent),
+ m_networkDeviceDiscovery(networkDeviceDiscovery)
+{
+ // More details: https://github.com/RalfOGit/libspeedwire/
+
+
+
+ // // 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, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
+ 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(Speedwire::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 == Speedwire::discoveryDatagramMulticast() || datagram == Speedwire::discoveryDatagramUnicast())
+ return;
+
+ QDataStream stream(datagram);
+ Speedwire::Header header = Speedwire::parseHeader(stream);
+ if (!header.isValid()) {
+ qCWarning(dcSma()) << "SpeedwireDiscovery: Datagram header is not valid. Ignoring data...";
+ return;
+ }
+
+ qCDebug(dcSma()) << "SpeedwireDiscovery:" << header;
+
+ if (header.protocolId == Speedwire::ProtocolIdDiscoveryResponse) {
+ qCDebug(dcSma()) << "SpeedwireDiscovery: Received discovery response from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort);
+
+ // "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;
+ }
+
+ 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 == Speedwire::ProtocolIdMeter) {
+ // Example: 010e 714369ae
+ quint16 modelId;
+ quint32 serialNumber;
+ stream >> modelId >> serialNumber;
+ qCDebug(dcSma()) << "SpeedwireDiscovery: Meter identifier: Model ID:" << modelId << "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 == Speedwire::ProtocolIdInverter) {
+ 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:" << inverterPacket;
+
+ 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 = inverterPacket.sourceModelId;
+ m_results[senderAddress].serialNumber = inverterPacket.sourceSerialNumber;
+ } else {
+ qCWarning(dcSma()) << "SpeedwireDiscovery: Unhandled data received" << datagram.toHex();
+ return;
+ }
+}
+
+void SpeedwireDiscovery::sendDiscoveryRequest()
+{
+ 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;
+ }
+
+ 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/speedwire/speedwirediscovery.h b/sma/speedwire/speedwirediscovery.h
new file mode 100644
index 0000000..0c181dd
--- /dev/null
+++ b/sma/speedwire/speedwirediscovery.h
@@ -0,0 +1,100 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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
+
+#include
+#include
+#include
+
+#include
+
+#include "speedwire.h"
+#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 = Speedwire::multicastAddress();
+ quint16 m_port = Speedwire::port();
+ bool m_initialized = false;
+
+ // Discovery
+ QTimer m_discoveryTimer;
+ bool m_discoveryRunning = false;
+ NetworkDeviceInfos m_networkDeviceInfos;
+ QHash m_results;
+
+ 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/speedwire/speedwireinterface.cpp b/sma/speedwire/speedwireinterface.cpp
new file mode 100644
index 0000000..a5fa1d7
--- /dev/null
+++ b/sma/speedwire/speedwireinterface.cpp
@@ -0,0 +1,134 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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"
+
+SpeedwireInterface::SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent) :
+ QObject(parent),
+ 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)));
+}
+
+SpeedwireInterface::~SpeedwireInterface()
+{
+ deinitialize();
+}
+
+bool SpeedwireInterface::initialize()
+{
+ 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_multicast && !m_socket->joinMulticastGroup(m_multicastAddress)) {
+ qCWarning(dcSma()) << "SpeedwireInterface: 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 m_initialized;
+}
+
+void SpeedwireInterface::deinitialize()
+{
+ if (m_initialized) {
+ if (m_multicast) {
+ if (!m_socket->leaveMulticastGroup(m_multicastAddress)) {
+ qCWarning(dcSma()) << "SpeedwireInterface: Failed to leave multicast group" << m_multicastAddress.toString();
+ }
+ }
+
+ m_socket->close();
+ m_initialized = false;
+ }
+}
+
+bool SpeedwireInterface::initialized() const
+{
+ return m_initialized;
+}
+
+quint16 SpeedwireInterface::sourceModelId() const
+{
+ return m_sourceModelId;
+}
+
+quint32 SpeedwireInterface::sourceSerialNumber() const
+{
+ return m_sourceSerialNumber;
+}
+
+void SpeedwireInterface::sendData(const QByteArray &data)
+{
+ 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()
+{
+ QByteArray datagram;
+ QHostAddress senderAddress;
+ quint16 senderPort;
+
+ 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);
+ }
+}
+
+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/speedwire/speedwireinterface.h b/sma/speedwire/speedwireinterface.h
new file mode 100644
index 0000000..fa24287
--- /dev/null
+++ b/sma/speedwire/speedwireinterface.h
@@ -0,0 +1,88 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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
+
+#include
+#include
+#include
+
+#include "speedwire.h"
+
+class SpeedwireInterface : public QObject
+{
+ Q_OBJECT
+public:
+ enum DeviceType {
+ DeviceTypeUnknown,
+ DeviceTypeMeter,
+ DeviceTypeInverter
+ };
+ Q_ENUM(DeviceType)
+
+ explicit SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent = nullptr);
+ ~SpeedwireInterface();
+
+ bool initialize();
+ void deinitialize();
+
+ bool initialized() const;
+
+ quint16 sourceModelId() const;
+ quint32 sourceSerialNumber() const;
+
+public slots:
+ void sendData(const QByteArray &data);
+
+signals:
+ void dataReceived(const QByteArray &data);
+
+private:
+ QUdpSocket *m_socket = nullptr;
+ QHostAddress m_address;
+ 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 = 0x3a28be52;
+
+private slots:
+ void readPendingDatagrams();
+ void onSocketError(QAbstractSocket::SocketError error);
+ void onSocketStateChanged(QAbstractSocket::SocketState socketState);
+
+};
+
+
+#endif // SPEEDWIREINTERFACE_H
diff --git a/sma/speedwire/speedwireinverter.cpp b/sma/speedwire/speedwireinverter.cpp
new file mode 100644
index 0000000..39329c9
--- /dev/null
+++ b/sma/speedwire/speedwireinverter.cpp
@@ -0,0 +1,1273 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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"
+
+#include
+
+SpeedwireInverter::SpeedwireInverter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent) :
+ QObject(parent),
+ m_address(address),
+ 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()
+{
+ 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
+ buildPacket(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 encoded 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
+ buildPacket(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
+ buildPacket(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::startConnecting(const QString &password)
+{
+ m_password = password;
+ refresh();
+}
+
+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() << "packet 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::buildPacket(QDataStream &stream, quint32 command, quint16 packetId)
+{
+ // ========= packet 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
+ // 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
+
+
+ // Build the header
+ QByteArray datagram;
+ QDataStream stream(&datagram, QIODevice::WriteOnly);
+ buildDefaultHeader(stream);
+
+ // Reset the packet id counter, otherwise there will be no response
+ quint16 packetId = m_packetId++ | 0x8000;
+
+ // The payload is little endian encoded
+ buildPacket(stream, command, packetId);
+
+ // First and second word
+ stream << firstWord;
+ stream << secondWord;
+
+ // 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::processSoftwareVersionResponse(const QByteArray &response)
+{
+ // 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);
+ 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);
+ 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);
+ 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)
+{
+ 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::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(packet.command) << "Packet ID:" << packet.packetId;
+ //qCDebug(dcSma()) << "Inverter:" << data.toHex();
+ 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_responsePacket = packet;
+ // Set the payload
+ while (!stream.atEnd()) {
+ quint8 byte;
+ stream >> byte;
+ m_currentReply->m_responsePayload.append(byte);
+ }
+
+ if (packet.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:" << packet;
+ 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();
+
+ // 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);
+ return;
+ }
+ }
+
+ // 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 (m_deviceInformationFetched) {
+ setState(StateQueryData);
+ } else {
+ setState(StateGetInformation);
+ }
+ });
+ break;
+ }
+ case StateLogin: {
+ 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
+ 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());
+ m_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/speedwire/speedwireinverter.h b/sma/speedwire/speedwireinverter.h
new file mode 100644
index 0000000..dd814d6
--- /dev/null
+++ b/sma/speedwire/speedwireinverter.h
@@ -0,0 +1,203 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 "sma.h"
+#include "speedwire.h"
+#include "speedwireinterface.h"
+#include "speedwireinverterreply.h"
+#include "speedwireinverterrequest.h"
+
+class SpeedwireInverter : public QObject
+{
+ Q_OBJECT
+public:
+ enum State {
+ StateIdle,
+ StateDisconnected,
+ StateInitializing,
+ StateLogin,
+ StateGetInformation,
+ StateQueryData
+ };
+ Q_ENUM(State)
+
+ explicit SpeedwireInverter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent = nullptr);
+
+ bool initialize();
+ bool initialized() const;
+
+ State state() const;
+
+ bool reachable() const;
+
+ Sma::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
+ SpeedwireInverterReply *sendIdentifyRequest();
+ SpeedwireInverterReply *sendLoginRequest(const QString &password = "0000", bool loginAsUser = true);
+ SpeedwireInverterReply *sendLogoutRequest();
+ SpeedwireInverterReply *sendSoftwareVersionRequest();
+ SpeedwireInverterReply *sendDeviceTypeRequest();
+
+ // Start connecting
+ 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;
+
+ bool m_reachable = false;
+ State m_state = StateDisconnected;
+ quint16 m_packetId = 1;
+
+ bool m_deviceInformationFetched = false;
+
+ SpeedwireInverterReply *m_currentReply = nullptr;
+ QQueue m_replyQueue;
+
+ // Properties
+ Sma::DeviceClass m_deviceClass = Sma::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 buildPacket(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 = 1.0);
+
+ void setReachable(bool reachable);
+
+private slots:
+ void processData(const QByteArray &data);
+
+ void onReplyTimeout();
+ void onReplyFinished();
+
+};
+
+#endif // SPEEDWIREINVERTER_H
diff --git a/sma/speedwire/speedwireinverterreply.cpp b/sma/speedwire/speedwireinverterreply.cpp
new file mode 100644
index 0000000..b7b2941
--- /dev/null
+++ b/sma/speedwire/speedwireinverterreply.cpp
@@ -0,0 +1,85 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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::InverterPacket SpeedwireInverterReply::responsePacket() const
+{
+ return m_responsePacket;
+}
+
+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/speedwire/speedwireinverterreply.h b/sma/speedwire/speedwireinverterreply.h
new file mode 100644
index 0000000..3f6a3a1
--- /dev/null
+++ b/sma/speedwire/speedwireinverterreply.h
@@ -0,0 +1,89 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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::InverterPacket responsePacket() 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::InverterPacket m_responsePacket;
+ QByteArray m_responsePayload;
+
+
+ void finishReply(Error error);
+ void startWaiting();
+
+};
+
+#endif // SPEEDWIREINVERTERREPLY_H
diff --git a/sma/speedwire/speedwireinverterrequest.cpp b/sma/speedwire/speedwireinverterrequest.cpp
new file mode 100644
index 0000000..6797c71
--- /dev/null
+++ b/sma/speedwire/speedwireinverterrequest.cpp
@@ -0,0 +1,76 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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/speedwire/speedwireinverterrequest.h b/sma/speedwire/speedwireinverterrequest.h
new file mode 100644
index 0000000..be0b721
--- /dev/null
+++ b/sma/speedwire/speedwireinverterrequest.h
@@ -0,0 +1,62 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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/speedwire/speedwiremeter.cpp b/sma/speedwire/speedwiremeter.cpp
new file mode 100644
index 0000000..5178b56
--- /dev/null
+++ b/sma/speedwire/speedwiremeter.cpp
@@ -0,0 +1,339 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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"
+
+#include "sma.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);
+
+ // Reachable timestamp
+ m_timer.setInterval(5000);
+ m_timer.setSingleShot(false);
+ connect(&m_timer, &QTimer::timeout, this, &SpeedwireMeter::evaluateReachable);
+}
+
+bool SpeedwireMeter::initialize()
+{
+ bool initSuccess = m_interface->initialize();
+ if (initSuccess)
+ m_timer.start();
+
+ return initSuccess;
+}
+
+bool SpeedwireMeter::initialized() const
+{
+ return m_interface->initialized();
+}
+
+bool SpeedwireMeter::reachable() const
+{
+ return m_reachable;
+}
+
+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::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);
+
+ Speedwire::Header header = Speedwire::parseHeader(stream);
+ if (!header.isValid()) {
+ qCDebug(dcSma()) << "Meter: Datagram header is not valid. Ignoring data...";
+ return;
+ }
+
+ if (header.protocolId != Speedwire::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()) << "Meter: Total energy consumed" << m_totalEnergyConsumed << "kWh";
+ } else if (measurementIndex == 2 && measurement != 0) {
+ m_totalEnergyProduced = measurement / 3600000.0;
+ qCDebug(dcSma()) << "Meter: Total energy produced" << m_totalEnergyProduced << "kWh";
+ } else if (measurementIndex == 21 && measurement != 0) {
+ m_energyConsumedPhaseA = measurement / 3600000.0;
+ qCDebug(dcSma()) << "Meter: Energy consumed phase A" << m_energyConsumedPhaseA << "kWh";
+ } else if (measurementIndex == 41 && measurement != 0) {
+ m_energyConsumedPhaseB = measurement / 3600000.0;
+ qCDebug(dcSma()) << "Meter: Energy consumed phase B" << m_energyConsumedPhaseB << "kWh";
+ } else if (measurementIndex == 61 && measurement != 0) {
+ m_energyConsumedPhaseC = measurement / 3600000.0;
+ qCDebug(dcSma()) << "Meter: Energy consumed phase C" << m_energyConsumedPhaseC << "kWh";
+ } else if (measurementIndex == 22 && measurement != 0) {
+ m_energyProducedPhaseA = measurement / 3600000.0;
+ qCDebug(dcSma()) << "Meter: Energy produced phase A" << m_energyProducedPhaseA << "kWh";
+ } else if (measurementIndex == 42 && measurement != 0) {
+ m_energyProducedPhaseB = measurement / 3600000.0;
+ qCDebug(dcSma()) << "Meter: Energy produced phase B" << m_energyProducedPhaseB << "kWh";
+ } else if (measurementIndex == 62 && measurement != 0) {
+ m_energyProducedPhaseC = measurement / 3600000.0;
+ 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;
+ }
+
+ } if (measurementChannel == 144 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) {
+ // Software version
+ // 90000000 01 02 08 52
+ quint32 versionData;
+ stream >> versionData;
+ m_softwareVersion = Sma::buildSoftwareVersionString(versionData);
+
+ 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.";
+ }
+ }
+
+ // Save the current timestamp for reachable evaluation
+ m_lastSeenTimestamp = QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000;
+ evaluateReachable();
+
+ emit valuesUpdated();
+}
diff --git a/sma/speedwire/speedwiremeter.h b/sma/speedwire/speedwiremeter.h
new file mode 100644
index 0000000..63989c3
--- /dev/null
+++ b/sma/speedwire/speedwiremeter.h
@@ -0,0 +1,125 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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"
+
+class SpeedwireMeter : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SpeedwireMeter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent = nullptr);
+
+ bool initialize();
+ bool initialized() const;
+
+ bool reachable() 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 reachableChanged(bool reachable);
+ void valuesUpdated();
+
+private:
+ SpeedwireInterface *m_interface = nullptr;
+ QHostAddress m_address;
+ bool m_initialized = false;
+ 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;
+
+ 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 evaluateReachable();
+ void processData(const QByteArray &data);
+
+};
+
+#endif // SPEEDWIREMETER_H
diff --git a/sma/sunnywebbox/sunnywebbox.cpp b/sma/sunnywebbox/sunnywebbox.cpp
new file mode 100644
index 0000000..d148ea2
--- /dev/null
+++ b/sma/sunnywebbox/sunnywebbox.cpp
@@ -0,0 +1,345 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2020, 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 "sunnywebbox.h"
+#include "extern-plugininfo.h"
+
+#include "QJsonDocument"
+#include "QJsonObject"
+#include "QJsonArray"
+
+SunnyWebBox::SunnyWebBox(NetworkAccessManager *networkAccessManager, const QHostAddress &hostAddress, QObject *parrent) :
+ QObject(parrent),
+ m_hostAddresss(hostAddress),
+ m_networkManager(networkAccessManager)
+{
+ qCDebug(dcSma()) << "SunnyWebBox: Creating Sunny Web Box connection";
+}
+
+SunnyWebBox::~SunnyWebBox()
+{
+ qCDebug(dcSma()) << "SunnyWebBox: Deleting Sunny Web Box connection";
+}
+
+QString SunnyWebBox::getPlantOverview()
+{
+ return sendMessage(m_hostAddresss, "GetPlantOverview");
+}
+
+QString SunnyWebBox::getDevices()
+{
+ return sendMessage(m_hostAddresss, "GetDevices");
+}
+
+QString SunnyWebBox::getProcessDataChannels(const QString &deviceId)
+{
+ QJsonObject params;
+ params["device"] = deviceId;
+ return sendMessage(m_hostAddresss, "GetProcessDataChannels", params);
+}
+
+QString SunnyWebBox::getProcessData(const QStringList &deviceKeys)
+{
+ QJsonObject paramsObj;
+ QJsonArray devicesArray;
+ foreach (const QString &key, deviceKeys) {
+ QJsonObject deviceObj;
+ deviceObj["key"] = key;
+ devicesArray.append(deviceObj);
+ }
+ paramsObj["devices"] = devicesArray;
+ return sendMessage(m_hostAddresss, "GetProcessData", paramsObj);
+}
+
+QString SunnyWebBox::getParameterChannels(const QString &deviceKey)
+{
+ QJsonObject paramsObj;
+ QJsonArray devicesArray;
+ QJsonObject deviceObj;
+ deviceObj["key"] = deviceKey;
+ devicesArray.append(deviceObj);
+ paramsObj["devices"] = devicesArray;
+ return sendMessage(m_hostAddresss, "GetParameterChannels", paramsObj);
+}
+
+QString SunnyWebBox::getParameters(const QStringList &deviceKeys)
+{
+ QJsonObject paramsObj;
+ QJsonArray devicesArray;
+ foreach (const QString &key, deviceKeys) {
+ QJsonObject deviceObj;
+ deviceObj["key"] = key;
+ devicesArray.append(deviceObj);
+ }
+ paramsObj["devices"] = devicesArray;
+ return sendMessage(m_hostAddresss, "GetParameter", paramsObj);
+}
+
+QString SunnyWebBox::setParameters(const QString &deviceKey, const QHash &channels)
+{
+ QJsonObject paramsObj;
+ QJsonArray devicesArray;
+ QJsonObject deviceObj;
+ deviceObj["key"] = deviceKey;
+ QJsonArray channelsArray;
+ foreach (const QString &key, channels.keys()) {
+ QJsonObject channelObj;
+ channelObj["meta"] = key;
+ channelObj["value"] = channels.value(key).toString();
+ channelsArray.append(channelObj);
+ }
+ deviceObj["channels"] = channelsArray;
+ devicesArray.append(deviceObj);
+ paramsObj["devices"] = devicesArray;
+ return sendMessage(m_hostAddresss, "SetParameter", paramsObj);
+}
+
+QHostAddress SunnyWebBox::hostAddress() const
+{
+ return m_hostAddresss;
+}
+
+void SunnyWebBox::setHostAddress(const QHostAddress &address)
+{
+ qCDebug(dcSma()) << "SunnyWebBox: Setting host address to" << address.toString();
+ m_hostAddresss = address;
+}
+
+QString SunnyWebBox::macAddress() const
+{
+ return m_macAddress;
+}
+
+void SunnyWebBox::setMacAddress(const QString &macAddress)
+{
+ m_macAddress = macAddress;
+}
+
+QNetworkReply *SunnyWebBox::sendRequest(const QHostAddress &address, const QString &procedure, const QJsonObject ¶ms, const QString &requestId)
+{
+ qCDebug(dcSma()) << "SunnyWebBox: Send message to" << address.toString() << "Procedure:" << procedure << "Params:" << params;
+
+ QString finalRequestId = requestId;
+ if (finalRequestId.isEmpty())
+ finalRequestId = generateRequestId();
+
+ QJsonDocument doc;
+ QJsonObject obj;
+ obj["format"] = "JSON";
+ obj["id"] = requestId;
+ obj["proc"] = procedure;
+ obj["version"] = "1.0";
+
+ if (!params.isEmpty()) {
+ obj.insert("params", params);
+ }
+ doc.setObject(obj);
+
+ QUrl url;
+ url.setHost(address.toString());
+ url.setPath("/rpc");
+ url.setPort(80);
+ url.setScheme("http");
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ QByteArray data = doc.toJson(QJsonDocument::JsonFormat::Compact);
+ data.prepend("RPC=");
+ return m_networkManager->post(request, data);
+}
+
+QString SunnyWebBox::generateRequestId()
+{
+ return QUuid::createUuid().toString().remove('{').remove('-').left(14);
+}
+
+void SunnyWebBox::parseMessage(const QString &messageId, const QString &messageType, const QVariantMap &result)
+{
+ if (messageType == "GetPlantOverview") {
+ Overview overview;
+ QVariantList overviewList = result.value("overview").toList();
+ qCDebug(dcSma()) << "SunnyWebBox: GetPlantOverview";
+ foreach (const QVariant &value, overviewList) {
+ QVariantMap map = value.toMap();
+
+ if (map["meta"].toString() == "GriPwr") {
+ overview.power = map["value"].toString().toInt();
+ QString unit = map["unit"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Power" << overview.power << unit;
+ } else if (map["meta"].toString() == "GriEgyTdy") {
+ overview.dailyYield = map["value"].toString().toDouble();
+ QString unit = map["unit"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Daily yield" << overview.dailyYield << unit;
+ } else if (map["meta"].toString() == "GriEgyTot") {
+ overview.totalYield = map["value"].toString().toDouble();
+ QString unit = map["unit"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Total yield" << overview.totalYield << unit;
+ } else if (map["meta"].toString() == "OpStt") {
+ overview.status = map["value"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Status" << overview.status;
+ } else if (map["meta"].toString() == "Msg") {
+ overview.error = map["value"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Error" << overview.error;
+ }
+ }
+ emit plantOverviewReceived(messageId, overview);
+
+ } else if (messageType == "GetDevices") {
+ QList devices;
+ QVariantList deviceList = result.value("devices").toList();
+ qCDebug(dcSma()) << "SunnyWebBox: GetDevices" << result.value("totalDevicesReturned").toInt();
+ foreach (const QVariant &value, deviceList) {
+ Device device;
+ QVariantMap map = value.toMap();
+ device.name = map["name"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Name" << device.name;
+ device.key = map["key"].toString();
+ qCDebug(dcSma()) << "SunnyWebBox: - Key" << device.key;
+ QVariantList childrenList = map["children"].toList();
+ foreach (const QVariant &childValue, childrenList) {
+ Device child;
+ QVariantMap childMap = childValue.toMap();
+ device.name = childMap["name"].toString();
+ device.key = childMap["key"].toString();
+ device.childrens.append(child);
+ }
+ devices.append(device);
+ }
+ if (!devices.isEmpty()) {
+ emit devicesReceived(messageId, devices);
+ }
+ } else if (messageType == "GetProcessDataChannels" ||
+ messageType == "GetProDataChannels") {
+ foreach (const QString &deviceKey, result.keys()) {
+ QStringList processDataChannels = result.value(deviceKey).toStringList();
+ if (!processDataChannels.isEmpty())
+ emit processDataChannelsReceived(messageId, deviceKey, processDataChannels);
+ }
+ } else if (messageType == "GetProcessData") {
+ QVariantList devicesList = result.value("devices").toList();
+ qCDebug(dcSma()) << "SunnyWebBox: GetProcessData response received";
+ foreach (const QVariant &value, devicesList) {
+
+ QString key = value.toMap().value("key").toString();
+ QVariantList channelsList = value.toMap().value("channels").toList();
+ QHash channels;
+ 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") {
+ 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") {
+ QVariantList devicesList = result.value("devices").toList();
+ foreach (const QVariant &value, devicesList) {
+
+ QString key = value.toMap().value("key").toString();
+ QVariantList channelsList = value.toMap().value("channels").toList();
+ QList parameters;
+ foreach (const QVariant &channel, channelsList) {
+ Parameter parameter;
+ parameter.meta = channel.toMap().value("meta").toString();
+ parameter.name = channel.toMap().value("name").toString();
+ parameter.unit = channel.toMap().value("unit").toString();
+ parameter.min = channel.toMap().value("min").toDouble();
+ parameter.max = channel.toMap().value("max").toDouble();
+ parameter.value = channel.toMap().value("value").toDouble();
+ parameters.append(parameter);
+ }
+ emit parametersReceived(messageId, key, parameters);
+ }
+ } else {
+ qCWarning(dcSma()) << "SunnyWebBox: Unknown message type" << messageType;
+ }
+}
+
+void SunnyWebBox::setConnectionStatus(bool connected)
+{
+ if (m_connected != connected) {
+ qCDebug(dcSma()) << "SunnyWebBox: Connection status changed" << connected;
+ m_connected = connected;
+ emit connectedChanged(m_connected);
+ }
+}
+
+QString SunnyWebBox::sendMessage(const QHostAddress &address, const QString &procedure)
+{
+ return sendMessage(address, procedure, QJsonObject());
+}
+
+QString SunnyWebBox::sendMessage(const QHostAddress &address, const QString &procedure, const QJsonObject ¶ms)
+{
+ QString requestId = generateRequestId();
+ QNetworkReply *reply = sendRequest(m_hostAddresss, procedure, params, requestId);
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [this, address, requestId, reply]{
+
+ if (reply->error() != QNetworkReply::NoError) {
+ setConnectionStatus(false);
+ return;
+ }
+ setConnectionStatus(true);
+
+ QByteArray data = reply->readAll();
+ qCDebug(dcSma()) << "SunnyWebBox: Received reply" << data;
+
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcSma()) << "SunnyWebBox: Could not parse JSON" << error.errorString();
+ return;
+ }
+ if (!doc.isObject()) {
+ qCWarning(dcSma()) << "SunnyWebBox: JSON is not an Object";
+ return;
+ }
+ QVariantMap map = doc.toVariant().toMap();
+ if (map["version"] != "1.0") {
+ qCWarning(dcSma()) << "SunnyWebBox: API version not supported" << map["version"];
+ return;
+ }
+
+ if (map.contains("proc") && map.contains("result")) {
+ QString requestType = map["proc"].toString();
+ QString requestId = map["id"].toString();
+ QVariantMap result = map.value("result").toMap();
+ parseMessage(requestId, requestType, result);
+ } else if (map.contains("proc") && map.contains("error")) {
+ } else {
+ qCWarning(dcSma()) << "SunnyWebBox: Missing proc or result value";
+ }
+ });
+ return requestId;
+}
+
diff --git a/sma/sunnywebbox/sunnywebbox.h b/sma/sunnywebbox/sunnywebbox.h
new file mode 100644
index 0000000..9981aeb
--- /dev/null
+++ b/sma/sunnywebbox/sunnywebbox.h
@@ -0,0 +1,120 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2020, 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 SUNNYWEBBOX_H
+#define SUNNYWEBBOX_H
+
+#include "integrations/thing.h"
+#include "network/networkaccessmanager.h"
+
+#include
+#include
+#include
+#include
+
+class SunnyWebBox : public QObject
+{
+ Q_OBJECT
+
+public:
+ struct Overview {
+ int power;
+ double dailyYield;
+ int totalYield;
+ QString status;
+ QString error;
+ };
+
+ struct Device {
+ QString key;
+ QString name;
+ QList childrens;
+ };
+
+ struct Channel {
+ QString meta;
+ QString name;
+ QVariant value;
+ QString unit;
+ };
+
+ struct Parameter {
+ QString meta;
+ QString name;
+ QString unit;
+ double min;
+ double max;
+ double value;
+ };
+
+ explicit SunnyWebBox(NetworkAccessManager *networkAccessManager, const QHostAddress &hostAddress, QObject *parrent = 0);
+ ~SunnyWebBox();
+
+ QString getPlantOverview(); // Returns an object with the following plant data: PAC, E-TODAY, E-TOTAL, MODE, ERROR
+ QString getDevices(); // Returns a hierarchical list of all detected plant devices.
+ QString getProcessDataChannels(const QString &deviceKey); //Returns a list with the meta names of the available process data channels for a particular device type.
+ QString getProcessData(const QStringList &deviceKeys); //Returns process data for up to 5 devices per request.
+ QString getParameterChannels(const QString &deviceKey); //Returns a list with the meta names of the available parameter channels for a particular device type
+ QString getParameters(const QStringList &deviceKeys); //Returns the parameter values of up to 5 devices
+ QString setParameters(const QString &deviceKeys, const QHash &channels); //Sets parameter values
+
+ QHostAddress hostAddress() const;
+ void setHostAddress(const QHostAddress &address);
+
+ QString macAddress() const;
+ void setMacAddress(const QString &macAddress);
+
+ QNetworkReply *sendRequest(const QHostAddress &address, const QString &procedure, const QJsonObject ¶ms = QJsonObject(), const QString &requestId = QString());
+
+ static QString generateRequestId();
+
+private:
+ bool m_connected = false;
+ QHostAddress m_hostAddresss;
+ QString m_macAddress;
+ NetworkAccessManager *m_networkManager = nullptr;
+
+ QString sendMessage(const QHostAddress &address, const QString &procedure);
+ QString sendMessage(const QHostAddress &address, const QString &procedure, const QJsonObject ¶ms);
+ void parseMessage(const QString &messageId, const QString &messageType, const QVariantMap &result);
+ void setConnectionStatus(bool connected);
+
+signals:
+ void connectedChanged(bool connected);
+
+ void plantOverviewReceived(const QString &messageId, Overview overview);
+ void devicesReceived(const QString &messageId, QList devices);
+ void processDataChannelsReceived(const QString &messageId, const QString &deviceKey, QStringList processDataChanels);
+ void processDataReceived(const QString &messageId, const QString &deviceKey, const QHash &channels);
+ void parameterChannelsReceived(const QString &messageId, const QString &deviceKey, QStringList parameterChannels);
+ void parametersReceived(const QString &messageId, const QString &deviceKey, const QList ¶meters);
+};
+
+#endif // SUNNYWEBBOX_H
diff --git a/sma/sunnywebbox/sunnywebboxdiscovery.cpp b/sma/sunnywebbox/sunnywebboxdiscovery.cpp
new file mode 100644
index 0000000..f9a1f76
--- /dev/null
+++ b/sma/sunnywebbox/sunnywebboxdiscovery.cpp
@@ -0,0 +1,160 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 "sunnywebboxdiscovery.h"
+#include "sunnywebbox.h"
+
+#include "extern-plugininfo.h"
+
+#include
+
+SunnyWebBoxDiscovery::SunnyWebBoxDiscovery(NetworkAccessManager *networkAccessManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) :
+ QObject(parent),
+ m_networkAccessManager(networkAccessManager),
+ m_networkDeviceDiscovery(networkDeviceDiscovery)
+{
+
+}
+
+void SunnyWebBoxDiscovery::startDiscovery()
+{
+ // Clean up
+ m_discoveryResults.clear();
+ m_verifiedNetworkDeviceInfos.clear();
+
+ m_startDateTime = QDateTime::currentDateTime();
+
+ qCInfo(dcSma()) << "Discovery: SunnyWebBox: Starting network discovery...";
+ m_discoveryReply = m_networkDeviceDiscovery->discover();
+
+ // Test any network device beeing discovered
+ connect(m_discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &SunnyWebBoxDiscovery::checkNetworkDevice);
+
+ // When the network discovery has finished, we process the rest and give some time to finish the pending replies
+ connect(m_discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
+ // The network device discovery is done
+ m_discoveredNetworkDeviceInfos = m_discoveryReply->networkDeviceInfos();
+ m_discoveryReply->deleteLater();
+ m_discoveryReply = nullptr;
+
+ // Check if all network device infos have been verified
+ foreach (const NetworkDeviceInfo &networkDeviceInfo, m_discoveredNetworkDeviceInfos) {
+ if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo))
+ continue;
+
+ checkNetworkDevice(networkDeviceInfo);
+ }
+
+ // If there might be some response after the grace period time,
+ // we don't care any more since there might just waiting for some timeouts...
+ // If there would be a device, if would have responded.
+ QTimer::singleShot(3000, this, [this](){
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: Grace period timer triggered.";
+ finishDiscovery();
+ });
+ });
+}
+
+NetworkDeviceInfos SunnyWebBoxDiscovery::discoveryResults() const
+{
+ return m_discoveryResults;
+}
+
+void SunnyWebBoxDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
+{
+ if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo))
+ return;
+
+ m_verifiedNetworkDeviceInfos.append(networkDeviceInfo);
+
+ // Make a simple request and verify if it worked and the expected data gets returned.
+ SunnyWebBox webBox(m_networkAccessManager, networkDeviceInfo.address(), this);
+ QNetworkReply *reply = webBox.sendRequest(networkDeviceInfo.address(), "GetPlantOverview");
+ m_pendingReplies.append(reply);
+ connect(reply, &QNetworkReply::finished, this, [=](){
+ m_pendingReplies.removeAll(reply);
+ reply->deleteLater();
+
+ // Check HTTP reply
+ if (reply->error() != QNetworkReply::NoError) {
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: Checked" << networkDeviceInfo.address().toString()
+ << "and a HTTP error occurred:" << reply->errorString() << "Continue...";
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+
+ // Check JSON
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: Checked" << networkDeviceInfo.address().toString()
+ << "and received invalid JSON data:" << error.errorString() << "Continue...";
+ return;
+ }
+
+ if (!jsonDoc.isObject()) {
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: Response JSON is not an Object" << networkDeviceInfo.address().toString() << "Continue...";
+ return;
+ }
+
+ QVariantMap map = jsonDoc.toVariant().toMap();
+ if (map["version"] != "1.0") {
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: API version not supported on" << networkDeviceInfo.address().toString() << "Continue...";;
+ return;
+ }
+
+ if (map.contains("proc") && map.contains("result")) {
+ // Ok, seems to be a Sunny WebBox we are talking to...add to the discovery results...
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: --> Found Sunny WebBox on" << networkDeviceInfo;
+ m_discoveryResults.append(networkDeviceInfo);
+ } else {
+ qCDebug(dcSma()) << "Discovery: SunnyWebBox: Missing proc or result value in response from" << networkDeviceInfo.address().toString() << "Continue...";
+ return;
+ }
+ });
+}
+
+void SunnyWebBoxDiscovery::cleanupPendingReplies()
+{
+ foreach (QNetworkReply *reply, m_pendingReplies) {
+ reply->abort();
+ }
+}
+
+void SunnyWebBoxDiscovery::finishDiscovery()
+{
+ qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
+ qCInfo(dcSma()) << "Discovery: SunnyWebBox: Finished the discovery process. Found" << m_discoveryResults.count()
+ << "Sunny WebBoxes in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
+
+ cleanupPendingReplies();
+ emit discoveryFinished();
+}
diff --git a/sma/sunnywebbox/sunnywebboxdiscovery.h b/sma/sunnywebbox/sunnywebboxdiscovery.h
new file mode 100644
index 0000000..066e4cd
--- /dev/null
+++ b/sma/sunnywebbox/sunnywebboxdiscovery.h
@@ -0,0 +1,71 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* 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 SUNNYWEBBOXDISCOVERY_H
+#define SUNNYWEBBOXDISCOVERY_H
+
+#include
+
+#include
+#include
+
+class SunnyWebBoxDiscovery : public QObject
+{
+ Q_OBJECT
+public:
+ explicit SunnyWebBoxDiscovery(NetworkAccessManager *networkAccessManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr);
+
+ void startDiscovery();
+
+ NetworkDeviceInfos discoveryResults() const;
+
+signals:
+ void discoveryFinished();
+
+private slots:
+ void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
+ void cleanupPendingReplies();
+ void finishDiscovery();
+
+private:
+ NetworkAccessManager *m_networkAccessManager = nullptr;
+ NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
+ NetworkDeviceDiscoveryReply *m_discoveryReply = nullptr;
+
+ NetworkDeviceInfos m_discoveryResults;
+ NetworkDeviceInfos m_discoveredNetworkDeviceInfos;
+ NetworkDeviceInfos m_verifiedNetworkDeviceInfos;
+
+ QDateTime m_startDateTime;
+ QList m_pendingReplies;
+
+};
+
+#endif // SUNNYWEBBOXDISCOVERY_H
diff --git a/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts
new file mode 100644
index 0000000..861aab4
--- /dev/null
+++ b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts
@@ -0,0 +1,309 @@
+
+
+
+
+ IntegrationPluginSma
+
+
+ Unable to discover devices in your network.
+
+
+
+
+
+ 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.
+
+
+
+
+ The password can not be longer than 12 characters.
+
+
+
+
+ Failed to log in with the given password. Please try again.
+
+
+
+
+ sma
+
+
+
+
+ Connected
+ 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 StateType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox
+
+
+
+
+
+ Current phase A
+ 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 B
+ 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 C
+ 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 power
+ 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 StateType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox
+
+
+
+
+ Current power phase A
+ The name of the StateType ({c5d09c63-7461-4fb8-a6fe-bc7aa919be30}) of ThingClass speedwireMeter
+
+
+
+
+ Current power phase B
+ The name of the StateType ({c52d4422-b521-4804-a7a7-c4398e91e760}) of ThingClass speedwireMeter
+
+
+
+
+ Current power phase C
+ The name of the StateType ({555e892c-3ca7-4100-9832-6ac13b87eb04}) of ThingClass speedwireMeter
+
+
+
+
+ DC power MPP1
+ The name of the StateType ({b366f680-6134-488b-8362-b1b824a8daca}) of ThingClass speedwireInverter
+
+
+
+
+ DC power MPP2
+ The name of the StateType ({87d9b654-5558-47a3-9db9-ffd7c23b4774}) of ThingClass speedwireInverter
+
+
+
+
+ Day energy produced
+ The name of the StateType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox
+
+
+
+
+ Energy consumed phase A
+ The name of the StateType ({b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33}) of ThingClass speedwireMeter
+
+
+
+
+ Energy consumed phase B
+ The name of the StateType ({c4e5f569-ac5d-4761-a898-888880bfd59f}) of ThingClass speedwireMeter
+
+
+
+
+ Energy consumed phase C
+ The name of the StateType ({aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3}) of ThingClass speedwireMeter
+
+
+
+
+ Energy produced phase A
+ The name of the StateType ({754c3b67-768a-47f7-99d8-f66c198f0835}) of ThingClass speedwireMeter
+
+
+
+
+ Energy produced phase B
+ The name of the StateType ({7eb08c45-24cf-40ce-be28-f3564f087672}) of ThingClass speedwireMeter
+
+
+
+
+ Energy produced phase C
+ The name of the StateType ({1eb2bf01-5ec6-42e5-b348-ac1e95199d14}) of ThingClass speedwireMeter
+
+
+
+
+ Energy produced today
+ The name of the StateType ({e8bc8f81-e5c5-4900-b429-93fcaa262fcb}) of ThingClass speedwireInverter
+
+
+
+
+ Error
+ The name of the StateType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox
+
+
+
+
+
+ Firmware version
+ 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
+
+
+
+
+ Frequency
+ The name of the StateType ({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 StateType ({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})
+----------
+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})
+
+
+
+
+ Sunny WebBox
+ The name of the ThingClass ({49304127-ce9b-45dd-8511-05030a4ac003})
+
+
+
+
+ Total energy consumed
+ The name of the StateType ({4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d}) of ThingClass speedwireMeter
+
+
+
+
+
+
+ Total energy produced
+ 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 StateType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox
+
+
+
+
+
+ Voltage phase A
+ 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 B
+ 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 C
+ 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
+
+
+
+