From e4210add11aa3fc524c6c8b7c6621f325f77ecca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 17 Nov 2022 12:27:11 +0100 Subject: [PATCH 1/3] Move nymea-plugin-sma from nymea-plugins into this repository --- debian/control | 8 + debian/nymea-plugin-sma.install.in | 2 + sma/README.md | 19 + sma/integrationpluginsma.cpp | 467 ++++++ sma/integrationpluginsma.h | 73 + sma/integrationpluginsma.json | 510 +++++++ sma/meta.json | 13 + sma/sma.png | Bin 0 -> 41281 bytes sma/sma.pro | 26 + sma/speedwire.h | 301 ++++ sma/speedwirediscovery.cpp | 349 +++++ sma/speedwirediscovery.h | 100 ++ sma/speedwireinterface.cpp | 134 ++ sma/speedwireinterface.h | 88 ++ sma/speedwireinverter.cpp | 1273 +++++++++++++++++ sma/speedwireinverter.h | 202 +++ sma/speedwireinverterreply.cpp | 85 ++ sma/speedwireinverterreply.h | 89 ++ sma/speedwireinverterrequest.cpp | 76 + sma/speedwireinverterrequest.h | 62 + sma/speedwiremeter.cpp | 344 +++++ sma/speedwiremeter.h | 126 ++ sma/sunnywebbox.cpp | 345 +++++ sma/sunnywebbox.h | 120 ++ sma/sunnywebboxdiscovery.cpp | 160 +++ sma/sunnywebboxdiscovery.h | 71 + ...42bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts | 309 ++++ 27 files changed, 5352 insertions(+) create mode 100644 debian/nymea-plugin-sma.install.in create mode 100644 sma/README.md create mode 100644 sma/integrationpluginsma.cpp create mode 100644 sma/integrationpluginsma.h create mode 100644 sma/integrationpluginsma.json create mode 100644 sma/meta.json create mode 100644 sma/sma.png create mode 100644 sma/sma.pro create mode 100644 sma/speedwire.h create mode 100644 sma/speedwirediscovery.cpp create mode 100644 sma/speedwirediscovery.h create mode 100644 sma/speedwireinterface.cpp create mode 100644 sma/speedwireinterface.h create mode 100644 sma/speedwireinverter.cpp create mode 100644 sma/speedwireinverter.h create mode 100644 sma/speedwireinverterreply.cpp create mode 100644 sma/speedwireinverterreply.h create mode 100644 sma/speedwireinverterrequest.cpp create mode 100644 sma/speedwireinverterrequest.h create mode 100644 sma/speedwiremeter.cpp create mode 100644 sma/speedwiremeter.h create mode 100644 sma/sunnywebbox.cpp create mode 100644 sma/sunnywebbox.h create mode 100644 sma/sunnywebboxdiscovery.cpp create mode 100644 sma/sunnywebboxdiscovery.h create mode 100644 sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts 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/sma/README.md b/sma/README.md new file mode 100644 index 0000000..bbb61b6 --- /dev/null +++ b/sma/README.md @@ -0,0 +1,19 @@ +# SMA + +nymea plug-in for SMA solar equipment. + +## Supported Things + +* Sunny WebBox +* SMA speedwire Meters +* SMA speedwire Inverters + +> Note: the SMA battery equipment is still missing due to testing possibilities. Will be added as soon someone can with the appropriate setup. + +## Requirements + +* The package "nymea-plugin-sma" must be installed. +* The speedwire port `9522` must not be clocked for UDP packages in the network. + +## More +https://www.sma.de/en/ diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp new file mode 100644 index 0000000..843008a --- /dev/null +++ b/sma/integrationpluginsma.cpp @@ -0,0 +1,467 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "speedwirediscovery.h" +#include "sunnywebboxdiscovery.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 Inverter", "Serial: " + QString::number(result.serialNumber) + " - " + result.address.toString()); + // We found an energy meter, let's check if we already added this one + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(speedwireInverterThingSerialNumberParamTypeId).toUInt() == result.serialNumber) { + descriptor.setThingId(existingThing->id()); + break; + } + } + + ParamList params; + params << Param(speedwireInverterThingHostParamTypeId, result.address.toString()); + params << Param(speedwireInverterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(speedwireInverterThingSerialNumberParamTypeId, result.serialNumber); + params << Param(speedwireInverterThingModelIdParamTypeId, result.modelId); + descriptor.setParams(params); + descriptors.append(descriptor); + } + } + + info->addThingDescriptors(descriptors); + info->finish(Thing::ThingErrorNoError); + }); + + speedwireDiscovery->startDiscovery(); + } +} + +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) + + // 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 { + 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; + + setupRefreshTimer(); + sunnyWebBox->getPlantOverview(); + thing->setStateValue(sunnyWebBoxConnectedStateTypeId, true); + } else if (thing->thingClassId() == speedwireInverterThingClassId) { + SpeedwireInverter *inverter = m_speedwireInverters.value(thing); + if (inverter) { + thing->setStateValue(speedwireInverterConnectedStateTypeId, inverter->reachable()); + } else { + thing->setStateValue(speedwireInverterConnectedStateTypeId, false); + } + + setupRefreshTimer(); + } +} + +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 (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(2); + connect(m_refreshTimer, &PluginTimer::timeout, this, [=](){ + foreach (Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { + SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); + sunnyWebBox->getPlantOverview(); + } + + foreach (SpeedwireInverter *inverter, m_speedwireInverters) { + // Note: refresh will not be triggered if there is already a refresh process running + inverter->refresh(); + } + }); + + m_refreshTimer->start(); +} diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h new file mode 100644 index 0000000..fc509d0 --- /dev/null +++ b/sma/integrationpluginsma.h @@ -0,0 +1,73 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "sunnywebbox.h" +#include "speedwiremeter.h" +#include "speedwireinverter.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(); + +private: + PluginTimer *m_refreshTimer = nullptr; + + QHash m_sunnyWebBoxes; + QHash m_speedwireMeters; + QHash m_speedwireInverters; +}; + +#endif // INTEGRATIONPLUGINSMA_H diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json new file mode 100644 index 0000000..80cc5aa --- /dev/null +++ b/sma/integrationpluginsma.json @@ -0,0 +1,510 @@ +{ + "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": "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", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2", + "name": "currentPower", + "displayName": "Current power", + "displayNameEvent": "Current power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "16f34c5c-8dbb-4dcc-9faa-4b782d57226c", + "name": "dayEnergyProduced", + "displayName": "Day energy produced", + "displayNameEvent": "Day energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "0bb4e227-7e38-49ca-9b32-ce4621c9305b", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "displayNameEvent": "Total energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "1974550b-6059-4b0e-83f4-70177e20dac3", + "name": "mode", + "displayName": "Mode", + "displayNameEvent": "Mode changed", + "type": "QString", + "defaultValue": "MPP" + }, + { + "id": "4e64f9ca-7e5a-4897-8035-6f2ae88fde89", + "name": "error", + "displayName": "Error", + "displayNameEvent": "Error changed", + "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", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "44ee2491-8376-41cd-a21d-185c736152ec", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "56ae3555-f874-4c2d-8833-17573dce477a", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "51cbb29b-29f0-480a-9d7d-b8f4e6a205ae", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "45bbdbef-1832-4870-bff5-299e580fb4da", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "b3a4fdd2-b6b8-4c58-9da3-2084ad414022", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "b3655188-3854-4336-ae3c-61d3bda6fc4d", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "d4ac7f37-e30a-44e4-93cb-ad16df18b8f1", + "name": "currentPower", + "displayName": "Current power", + "displayNameEvent": "Current power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "c5d09c63-7461-4fb8-a6fe-bc7aa919be30", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "c52d4422-b521-4804-a7a7-c4398e91e760", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "555e892c-3ca7-4100-9832-6ac13b87eb04", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "displayNameEvent": "Current power phase C changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumed changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "76ca68d8-6781-4d2a-8663-440aec40b4de", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "displayNameEvent": "Total energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33", + "name": "energyConsumedPhaseA", + "displayName": "Energy consumed phase A", + "displayNameEvent": "Energy consumed phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "c4e5f569-ac5d-4761-a898-888880bfd59f", + "name": "energyConsumedPhaseB", + "displayName": "Energy consumed phase B", + "displayNameEvent": "Energy consumed phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3", + "name": "energyConsumedPhaseC", + "displayName": "Energy consumed phase C", + "displayNameEvent": "Energy consumed phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "754c3b67-768a-47f7-99d8-f66c198f0835", + "name": "energyProducedPhaseA", + "displayName": "Energy produced phase A", + "displayNameEvent": "Energy produced phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "7eb08c45-24cf-40ce-be28-f3564f087672", + "name": "energyProducedPhaseB", + "displayName": "Energy produced phase B", + "displayNameEvent": "Energy produced phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "1eb2bf01-5ec6-42e5-b348-ac1e95199d14", + "name": "energyProducedPhaseC", + "displayName": "Energy produced phase C", + "displayNameEvent": "Energy produced phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "a685393c-8b7e-42c5-bb41-f9907c074626", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "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", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "d9a5768b-1bf5-4933-810d-84dd7a688f71", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "fc168dc6-eecf-40b4-b214-3e28da0dbb12", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "2a6c59ca-853a-47d6-96fb-0c85edf32f52", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "4db96fec-737c-4c4b-bf07-5ef2fd62508a", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "0f23fb0e-a440-4ac2-9aff-896bc65feb2c", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0, + "cached": false + }, + { + "id": "d7ceb482-5df8-4c0c-82bd-62ce7ba22c43", + "name": "currentPower", + "displayName": "Current power", + "displayNameEvent": "Current power changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "b366f680-6134-488b-8362-b1b824a8daca", + "name": "currentPowerMpp1", + "displayName": "DC power MPP1", + "displayNameEvent": "DC power MPP1 changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "87d9b654-5558-47a3-9db9-ffd7c23b4774", + "name": "currentPowerMpp2", + "displayName": "DC power MPP2", + "displayNameEvent": "DC power MPP2 changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "51cadd66-2cf1-485a-a2a9-191d11abfbd1", + "name": "totalEnergyProduced", + "displayName": "Total energy produced", + "displayNameEvent": "Total energy produced changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "e8bc8f81-e5c5-4900-b429-93fcaa262fcb", + "name": "energyProducedToday", + "displayName": "Energy produced today", + "displayNameEvent": "Energy produced today changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "fdccf5de-7413-4480-9ca0-1151665dede8", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "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/sma.png b/sma/sma.png new file mode 100644 index 0000000000000000000000000000000000000000..3273ac8549dcf43bd49062a7c02406298c63b81d GIT binary patch literal 41281 zcmd3O_dnI||G$(Hp)!+0vXZQ9LJ_j}UI)oISvmGDrIMrUy&Ze+t-&cXj=f9hIQDj| z?{#?fzJ30LuYTxudphTOUgI(DkNe|(d8eVKKt@7OLO?)3rugKs76HM9QUZeW)WjFT z6B~Up2JqiSOJ#+}1Sj}^X-zqi1O>6dym*@l0Bp^zeZeiKOKIt-3B(|h2-AtI#%%Z|37~&r1ZiGMmk-RE+}$F(EA^yMxl=> zH@8FDp1eQ8m1MHEH84<~&V1yTYijTAlUrt@x3BV&I5{(LXlY3(F0%>6MsadtD_hir zX~<7MI0K_HVE*EgM5P$3*bzJM3wu&I{4S64Z22%vC=oNtCBrSFaFG*3qL|DkBy@Uc z1O#a@cS>Id&x#hYkwpkRl|n34G-|n_Qymg|pf5klZ5$V=>$1W=XJ` zaw~D2KEC?pPU*ehqH2_i9?JBbd11dqPahNLrL~p^&WcL19zD_el*RYeg(bJzvlDLH#g_~vqbM@l)2COL;#%|1p-tN7p4g0Hie=6`lc`V=w$egp591O%;aDo|5!#Fr!3 z73&A#G^Y;(HROWa&LL+1D0<6kN&K6X%nb$N-mfl6#;ktq|6VhDr&MiL6w&LFWUh!y z%({T~Ivs$Z_1YJ029#pdRzeo@zqik{+$m)hL!eFi$0IgWZO(S3HD2bJIDJ;sl+};t z-=JKt;X!kvYSM}VtU^M`L}w!|(3TU$%!-OAd8=z>VD6ngtS$X`uap5*XWU;9k)iT$ z$hOxe;QKuutdcC86aOuVz{KbYwE<$**SP;{gzvv8rFqLBTVF6NghVi2o1;Ekd(PuH z@|lY$mv&`b5r*M&T1L5&4+WFS%E@&Ad!hr>^1L-=BT6NS4!MxH{nd z%;rvwmn@p!gnA1<*Voca%T(M5!^>1M(9&8B*!8`^aI;RbzZN`tL2G zv$x7i@;|#pq{rvNupq3c{%&LOrJmDtPOrYhFfFVa_8B*6RBjipq|~W`Iy-bxHe1qr zD3^;*yw#X17Yf8<9H%ldq$2k75vn-fZTDqKco+OE!UcwsbP$4Iq84$?!r1|*K_jAd zZO2}cdBaxdRn2QP$KO*VBF-F9gXNJvR16WxX7@hp!dJ}?oZKZpB{qjJVe0yJ9gS`b zm(MFjh4Z`+c9N#15_V$W;B z+;bE+Gs|n~HY?^p`e*NBJ>n7PPd_)~@kkr0=q=wLjg+mmhOJv19Z#G8ZHrfSTl?#R z{?EM$+3(=TT8S3yhm(seNy1f5R>7x!cr0N5uD|%-r+PN=Q)^Fqp?r zyydj=G53OIMEg6LxChnE;){yfLjDcL*Iq#slVz^7=pg?1R^eos`7FEY@Ik0>0^;nT zph+C~m*JFg=@^D%JuM?->Y-b8inmix2Umg0e={n5xqx4s7HbnLXBV6%AfdrML|$qK zq4%6y7;KZ?bNxEaVpa{)&Ph$({@qS{wfxVeJYQzupM8qft1b z-l*BSkMyPYp)P%(MXc>n$3+Ye${gM?!V9sow zV9MH644#>lf|MN?T0Pk$`sdqB24eWgl9Uyqx{zChKXCu!MFuqg*|;xX7Eu~ZM-Ozt z-;V7qOH1c8o}J+u3-;`I{CeStu{c48bpMTxd;j~^dpBSKOo~c^kPM{Rv2FZHqF`9E z{@IZJ&vfxGtj}5pYZx?9rs0_F9qcHY^*2Upf74V496w5Sv|LmMxk&AIp?lJP^6sBLX>d%3G{-yx^qY zMTa;tnp9Y|tau-q{#&ukn{`6ven)hE?T#K`BROqmeIcheE6Nu2pr&Sq+s9E zHwm79%~N43V-Td(*_48*(3A$}S?B0`G`Kg1Rt9*@4!-@H&_}EK)_T<1WBOcQvP9g*S-b|zflQ0?yKWg7Pugn{rO0y*0y0vi`sLadUa6m6p&xtAt zm47cXoVZtdv0<-}J`tEc<_zZ%h*F4<79R6R708n_h!yKgn_2vOeb3iue8SO-NT^>g z9NXJnC~({ItoygGq7*gkrQWq$Nk(4QJNMDo&SvI(j@caQFs+{vuPmJS-{lijXX4 zBc;(3nnJ6H*RaE5Agw1N&YlL^6T^d|Z_EsrL<3k+^#WG*Z?AGMinv1!ZT4d!s=Imtf<&`@R8$Dftu zzxUbS`a@FK6oN?jw;+1rr$NySt;5AtO(Eg&|Mw|1<=N9apc2IYG_rmq($gU@Zx|wH z1UdV^1)L68zM=HbIyBCw>qB)lpl7z~sR-tT;{0!f2W;TwFG&6agzA@{22p<>E*3)Z zC!f86K*+-S$A2c0^f>50G+Che-(aowR0d}sETtzpL%#%t^8fpoTpa#916}6?VYwrB zJdShm8C-XKr)~qhFmNobm2mOS{Rb*H$gp{jWD0*<-KM6zk)Bm++a|AYhkc{V+%j5M zcT8U^-!CLQd2cmBYgyd+F$ijJ`ZL)bn=_jHU42#qrL3mLlI!<>iJZq>8JNmFhHOcT zY!mOf@|^tY9P?YVZB0GzBczBJeyJeOdH422^g?u04Cm~MGZi9X0jj8y$X~j6%x25tz6fAvwov6=@}=^mXk;?_x%+Lbki$R(}0zc&k(#} z1Xh6^DQSBwZ>xd2H|gwEUA!`8r0CLzWcjfWS&r6@N>0i6bhN>$*cK9Q*Ymyz=e*9Y z8egLZ5ISGdefS?6p%-&vEC+KKj*_mSUsl#&n+FA43~Yu|qS0N+8CTrvS2o<0U>|32 zuC9q9I;#`U5}KZeVtYXVuB9Tm7hK7gsxB*DHBi*#3fGO*Q>sj>tdENFE~`hvZknR2 z!WbJCl(@$m^__Z+3x?+le|{lrP(_g%VK}#=w0MW2ZP?o7usc$9-iu8{;MK81`HJOl z-0bGYHpgtTYIp99*vM#dSoJbW)P5u+B_J?(M@Y)d$;o5nLH;*wT~;E`4UI$24k$Wq zzUgq7(x5OKHdPi`?E~l)%~cT#GhVkq_q|klq&E{zk>YsxQ;o`iCSGH}(4u*4wZ4Fp zy;MdY6_Zz=;rFI&!92iALP#y)GaK;CR&fjDc>+iK*r|u=h#Of&y$E!s2icmhms@q? zeAw2{x6NaM`%JJQl461CX0!~6N5It4;S0yF+JxXCVtN% z|9A>LdF!$rU;63?hD-7Nd*b0q{rQ{Sz<`{k8Y|?UJWz`k$)5f6`(G{mP=rWZ__OSt3EJ*2Q* zk$x;M`!ssnq_xUuZ0xIa{S9s)t?YEYIg+ zzb~`gf3x;@;_VvWq(tt|?S{`0bDWDv_Xe3PfR ztD*C__LGflg6KroRy6!KmOS0cGj^!MKIy4=JEyEfmCm!w$EQ(VExlFd=i0v7(z3z& zo+Q1_$QL*Rwhi@oQDN9)82Mw@?ssWVI|)7So#&tImqt}1X5F-VpIh}(SVQjLw0ol9 zipqTv-%7$cU7sI{A+{l=*tBEt*#mAV=ZSis_VFrZwtkCi75k(}+`A0={M4(NsFgac zRB3pKc$J|3z{tIly3yMfOIH!s5}a3|6(=n@Lb*AQdCZ9~FAHc5a11vaD=6%AS`mVR z!r&DlX-X{c=Kb&w{oVSNB9Jz;c#VCA7e#o+g$ERolm-z40ur*OMk4$P%mY~ip@Yds z5_H`&oBXe-?=sR&$qAd`VmmH?!lIOzh~`0&jr*HtM>xGIJELiMRGRY!-F#z+Lca01 zAljwyQ>Me#%E5b$&rRBc+{dD2tZ@DXGhikqXn z>H_{=q4XRsi772rs<1S%mO54GmO7j7tL-3qK-=N1YXm?@ZO%!dc&?WK$+qVQi;DBqm7A3G2 zt9H+t2iZJm?!%H8Mba^X{XYTVkY3YTl+z7t#Lj8{GO zF|cuB5ZlNY_~RM&H_=ACUF>-RVerX>bYVqKGc&r(IvuR%Z9@+z8Ec0?~DW94@SOVK@L zUhAziLi`atB(A_F-*Aq}ee2w`J&Tpa(LA^b;VSs~7KQxoqQY^x@C%K*T6`FzLGB*i z&h35er=D@_H?a-hfCrWOXlZiOHikKPSVC8 zPOPlW`~js9F*%h%=}_RI%F)c&3esxk9&fivzI4-OG={FY)QwO%0yt3{JUDG*xqwFi z^6RCMNKwg1E-{*ahU5H(z~JYvKHcqP%YXAGO#!#>sR8}0#Q*7jF`EYlip3qN6|Uor6d_Kd{b(uc!|ABw!;x^}j%CntU3%^@xnpN)=U`yDm5N;LX#U zyi_D&OP@(dxV0kTgVsf=sN9N!H*2em1Oziu?4{7;FEn8wPBTC%yW^x=asbGSNBl}S zRPo6vQL9f^sIKx!Y|K!6(dgLx<=D@1S)leEAKipRuEsBmsgvWWQl6q(6A+ne5rm{e z`vEKmoVSyBRTCT6eqBHE>jBu=WbQRY=+iF?yeVMlQrvr63FYL16A>Df z#a$whue@Kr`W`l1!VeTfpofVZS2<5wu7YZ{n=hHpE>l+*fP8f$;$BK^jmQF-o-^W0 zjh^Lg7WfI@K{$nyR&ln7Wd_a>n_M9O&IM_;*9}mSq~X}qTox~bL(Bdt99!z9P^Tp~nBUJt2awP`$)^gH?&?IRdU zoX&RZ_R8EFq53LS?Gy5N@RGLm4FG1!^+Sc1hTa#ojny8c39m@y;>n>sgZR7o`Bc=` zivR4{vbD)^iAl*DhD8oYvB-&aYvKRvre{w$PE9XUS4Gz+Us6Fin*!U*arA5O&`6)! zg99^55pcBSKwrz}1~?mdHmq&P60NC0Iyr{S`^lo6EX3~(4Kh1HmC~RmK;zWkHyhhr#wK47iGnyZ8I|;x+dnnR?H7MhiKnlS?=^hj81ulS9A1di%Fumz1`8 z_r+I=+RZV~$=olVOWPjLf_S8!@Xhp=={}07VQ2G|vYYscNZ5;|P*GU_@a}6v974ki z?AKOruS(8C)ogYYww-Zj8g6(~YWkh2LHgkVFg#}piBjn6Pe+?73b|#ViY^!qH`hE# zZORYuufCh+labG$pgmuN4op2VeLd}O#@l0(-*K1YR_~a(R(>KQd3TdO$DECK0kA6o zjH9knT8qEo5G+bfyXjLY0Lg5Bkx=%1GPM25T@84a2fH}TUgw1Tec11Vbl{9@cS@lq zo^k3myFY{=nPb&7Ohc+I0oq3aJNfg4Ba66AT8D`u%>Zu&NXup;l6PWec-Wo6ZeVuR z_G|Gge?QZbAG_ZPchPvzi)fzOcYE4D!aUU|S>z}G9z&d){{0LU!9N7rf4rKRSYqph zXBvplQ5o<#KJ<^dueBJD(Bv)&DA$58dg=kb<*C zL4tFw0Y+8(>1at>%D`~W_P4pz&u)sAyz>f(M1<>`Jo6&$h1I4sS}7B)oZ)@XeZ8YU z(WCxKbEY6553q_&!9N)N04UsVGa(USQ3a-7q7#06*#1~P*V?`uLzf$LSuR;s!C9`R z+HP^+{wX; zy-g3M2l-A?8!>n977%{yF>b7GJ$kNUVLI{6Vv2`qOk|cWU7zZykX}jB6%dF7+@G8# z|A!wlTVHBUjtT4fssJ!UtD|X{ZOrJe?9{v6URK}IV}#_H_9>HBASqx_sFB1~S5q^Q zL+`JyEv|o#Z0;*keZpXvAC@wDV{sz?9fr;=BJ%TXkCHeIDkJNSqNW#7RJ0DK=VS@a zDL)`Y1w{A7V;iZTiRQ)pkW6_!hw+>gX&kJIw(Df0N`32dMpzKp9P@tsl<|yz{oAVs z^rps?AwE7hrQhr_CAQ$?jeQp45Wn*G;+}-9wJL$EK4Xd>I<<{xX!;rNFd9u0htTB0 zS9CMI=IEQL#r1ztpu_R#WEYpilf7}VL>;Qw|MwA5N~1OJ#{?mj0k_HV1cauI7z6WG za6`#G4U@_ybX(910I@b)dcpX_S4r;CQPM*4RvgeODA5wrO9{8fkGkUBv8*`#y4SLh}6H?2^7@|FK?wyy1?tGMH;-hEQKyIhx)8g(sD~ zEVK%b=1KefG)~cMY$)KYA3{A()Hl}(9UT(yiud<$eqw6g`s7waHY6WC`!~o&TGS?@ zu(4c8MEWphhe6!_PJD8?fL7sALSf#zT>Ab+oUdi(Z9xzRF?9adm?-n+!>+5MTIH@_ z`;|blQ$g{8ypG^IPY~Fe*7}maE@tJ0VOf;{uU{pgCR>?X1>HclTI zeQa zQYFkN@91*~Z!EeN-b4u}N^bdML!=+%23utqRDa74`I_78)%|K^L5OarlBi%^F4@46 z$8IZ4=5@$=0HB1{c)|IihrV1c?A3EXue|!)K${PCIRxPyP@|tlb>TBva876_VgOJw zU=J6AewnH$tTiwwhnt7gf2*|(d%E!?A_~A-xn$#bJZJgx3Yg;CcONwLtPWx({OeyN zVu<-zg~ZHMu^hm1Jt(fMUN4Wl>VT&In^5eaf?8`x`ap&By$2r-B+sY42_mi#I~CDz zAAm|wMlh7Emh@Cd!3?d_go8NYmCHkIBk^I^g-X)^1d{)7L+5IgoI0o{`)`)Aj39Z0AMXV(I0Jt@HSfrh}jZZo&{5MqD!0iG86q*M*~Pudq6W|7M- z#;Xjl&=&#U)oeaBsJ4@nPnB2l;L9Sw`JCdjCm>|Lh5B1vb|RU-TnN~ZUE0?@RC)lT zm2L#6PwBuD)5d0p?fRIGIU=8EBp!e7_Zp`Mw z?&Hq4{$B6H-t$4eQtQ(^%~J&=ht&n0(tsxKAAf^B*TK5YYOL zarC1=LQ+vxiJgw%ti!H$BulO3;m;ZK+zEg-$(9HDK{`^-qb&fX0;t`l9GYBpq?tji z7MQ?&9~aj$aujdBQJaoGhVx)Dceo4qAio*`ssJ46c@pDb`Jll%UCQmQ9zF<58e?sK z=ogtk5A+=ccA)WpHWg3nW;&Za(8=|ygjbjyJbjJ1V?)=+tB>cf@Qsvk5W25r&$-hW zm>LObdMil+z+J8(K6T&y$=XULz95D8y#TP93`N|%7>!mDi-~?4;1%$R74w=RmQL*j zGC1yzUxhvbe^{-ci4Xl&HVX#3_RFH)C(!gj<*MgCvkr7}gvI!@tRQ}?2|xjzj5gzP zq9!z6F#UTmOWmb##^LFOq`1a}3d;h}zOoVRi+@@70ASY@5N58i#Vjv~PS%t(tDMG- zKxM7Pwl@O1_n*XfivqN{mGFE2xSkTYKQQ~4LWr@3=izZ z+vQXlMIKN8r7>~Za4A0Q`UzURb`u|G;)88dKV<|Faf7h)Qf_6XAuq?z=MTH8vF8+1 zfP4Gx*hmu-m8ZcgOT8(I1Oyrqz*C=rzB$OScL7K)@mr;z97{}$d}(-xkvS^s+`eIl z8UEkOy;FT{DL!Op``3A*NWzT+v@jeJUWuj{lC<9QW0h~a5J zWAOU?CPt)Yw{PI2PBZVvI&!u(d`z&Y1k~qf{tq_*opPC96QtiCllnAwq}co6SFA*3 z{_>0?OR`8oy28}mP<}ezp%_yKQXSytU-FX<`DC_=+dV~-DVK~rO{0+)zW-?0Mc+4Wo1r?C0mn(y`5jO`Igefu3JN8QwkXUN$!g!t2q`4h8F zN9}@lzf6tt@BxYcgT0aTqf<`~4hDWjb;EI(De-v2e><gb|X;a~rfM19c5>Q;}q2p+04bE1-F@ zg3ser)SuUhjmr*kEuhY>hdl(bVK4^JI3ng2!}lE`K}~f+`FhU$GSpk`1j{o3#+{S?LVxx(KmhYJTc~p zsInC5QZJvkMvx83Fkl9$Zc*+{SAVmqLVAWKaw zT_7Z10mx)`tMYYz;l_|d@Zeyj|I6dmA+xcKE>XY3#Bn^A4R|C~)XS`wG)wm+OaYgE znm<1%<7xD7zjmle*sXnGyrIuiY%ZH`#}Lt!kwye z6Lf+w#5vDLVZFT&k!v_}04exikpOu9YIfj=E-ehTqhP#qA^-?oNHXnL_pLulv?FYyl)QsF20v1SA&8Due?x(DBUzfW6u@ zM+Y=q&c`RlPW`a+5}y844Lz{dtmwqZU zaSK3Zql~?)^p%VaRGUF7&?3pTcUf7t=IXIzeAU^=b%O7Yz2{rTQiy8y*J4Bb#|qiKCgOUsis zUiAU&bc_6HkfNe`KDt?HKQ09_upMSz5(n**N4uzbK|N_-hwOvn{92n1DKbPhS+6oT z9sf{L><0n(N{(1M$Q&?4+r?Lx-gtcKEjB0goFsb@g$U=iD|k-Ublu6jFi??MLcUOTH zjTreZmZVnOl5kaMW?ZDHejYQGF2Ewa*w2iHBjKTg-$lx=$9*=|E>c>3T|HL zG{hSKc6i8z0aL9oy>q}c^a`Np%eX;@rmFpT7rc{CHR@^m>si5?zXsqg=*d|}>uMW- z`X*n_9^|`-tao3R>u#(&fi7ZidS=eCe>b@gNg;#d?LvW6^#s1Il#QLNhf6=W0AImX zu~hf)H|uFGCQWaB8SM@@iu9Jl5Fi(E6o0>sw>c2SVx;}UrH;EDhPWB4Rnz4}uJTAY z5GsqWvk%4?F9V*8%h3WGpPXLokdT02#yqY&{%-8ryH}<=zBCfVrdZd_S-NQNY@rP3 z`|a*PRUdN9avkja##oLz<=F2rM~CQ7TBp(+C^Iku*j%+`*ddw<>!?&dO1&(w^_kbP zPeQuskz_fbpvxk-MXu`U=q0-z7VfR_H?3pt%D(Fn0-eP5wwc+`rTF%^Iym|UN{Y>H zA?P}UlUpR}uZoJrP;wYr8bmu*XbT0;WTOJe6*oQ(7Z>4sd17gR>IKnU1`vZ|#^%L? zrs3+@-}29;s^49lP)a@Aac|m0-;C=JRSsgKG=O=m01elinDp_tfs!j(VlDWFakjpX zRj)eiW70MGWgKc)i@fNfBJP;ZDtILe8ECxE)X#^fGBm8g67iJ zjUfM{F*?c8*7`(^a|D52hl;dAJ}&cGlZR`GIgUp%+kk+~W(#OEE(tiHhwa>m2Py&W zOUmp(NHXO!#dPwbo7@i+u$^{1OXpr4s<&I>BGgIUe%EG`^Hjo(+Ya&owx`G2%g1I}u<+OAky1hhy&lXey7 z%KHbiOS35RDh=kr;4IQ5_tZv^5Arz@Z%ySbeyE{|ID|NStM>S)Z81oT% zV)4+e_yKGK?&J7S(hk^Wm1^ckfTmC6f{4`PL8$~Lpj$L<*Zb|&a3wgx)uSbLNlEw8 z>y{ZRC>LEbR|y1#QvdrRTAoq>q_&gfeC&#`Q$5@F?ULj=E6tPY98W9c@LA4mJMR%@9__Vj}^XWip*MwUBCvp}i#I9|NH@ zxaki0Syui&)W0jHDP(It7G|OZw*$o;+~M=~H;Z{1RH{SAgj;Nq$|yJ|a){)LRz@f+ z(QLY2?oV~1#-HPBqsm@BD@H)6^m|bN?At)SWF#m>-7@7wJ^{X}0`xu&!BWgy*5m7s zX4nece|o9{qXwEBCcnLa>B?sd8n*`2-)ibvNjO@XRL^}uT~gm2iSjz9rsdatlWlQq z#o_#MQcT_tAO>lb&Z%eg!gkvGP0%YN%z^SG7cauF@|4$`WioK`9*EYNZhA#>{`docKZWrZO@NH&8TYtWQFxVIfQGxb^D#YnaKptv z#v*0a{=?_GP0B~VSD44yP4j;Npr$ZB1(@VB&_yf9ss|I><7@dz^~QoJX|J|?kwY^y zPg9Uoz+ZB*6*}JhW#na7Io1Fg?i_x1sHr?XQBL=5B~peElM@DY-z zuX`3ENvrrYC&!>dp-V5e$}t}tH^5Vg*L zNX@{U5AR=T^Fsb=<8Pq=_V@0R_p3@a`Gt9Pvi~?2PoEz~Ei@9~)}H`55OVm1Bgo22 zUY9)z;EGd4>!0jB%?AB};=TpEG#`)(=c96;0Xbass3?Fm1*4u7li+LHzamtTR!<^Q zj#`^J3jxsvmpcF*34oLzf!(;VIlS7BZ*z!0SW7ZXs*B2eJxB!zfwoI;yl`Q!bV(;* zr6-GU^+ti|UPXIeaMxnH8@5t242w@F1hw_S^7djrd*`@%jeCwHirDr3Qk-a|X6851 zNnbf1uwH&IoD7Ve+Z?nC$(ag9vvO~?lUtCG5(H|<2fhAS;oE5zhC@r(`%SY-3R|0n zg}c7a0KCHy_A_j-FkK`^e936WZ1eIxMQ@K|@1k+9)?PdN-c)Ua*-&xL2VDpjr_`b` zBtY@Gg9}DgmqHVXd2K<5A3%OQw>Uc&t+!my#mSAvrWOi9W*Gp+Rs&V>X+=y+TPi)G z4P}1nFy;DW+S~mNeVC^S3u9u&>znS0h^JMcm7A~;d6SA3VkfQ?HYylYRTx8Q03sJ^ zdcOnAVig3U=t0rEo{~r^+9FVA9)A2T&FBSlERy!vcK6wV&b^@gva>t6xDGukvL=514^Ypa_h{EJw#Nk!(6Ldto}3fE>` z7BGsdAY=M=yJS(&I1IQ^92s-3_!;5va|kbq+lt4y;b~Lv1!2&g90SD5V1^6mrbVbt zeDGTPyKWS4d>or-P=8v8D^H%#Ome-k7PnEok51Fa4m)KENg`n;9FM8vv_eIZU`)&xA6pbg7Xt zYaXPqJGAoET%LZSR%uH=Us4g1x92?Res!6bv&uePlia7Y2Rge_07?sQz>1>zd2Dw- zXC~ZtN(S3OHRx_V{8}OGqc&Vwl^PgJ$6&Acg)n4TI#?dr3_086`uS{mcs@&Sk zq6r@S?$icuc##4Ja;&1O3G|Ujj0b!b{ed`@zCX{J;QC1z(vitVD9qI7F^1?IVD2QvoPXd6_CT)-Iz){0*!_)_H!Th_=<}eHmI0&mGtECw{ zRW`z=aE9q0Lb#{P^(mSkC>vU(oVL8d1t{Yjxu0e)WW(L>e$U_E3N!YVYMA;~GP0N& zkp+5Ht^!N(?`!wpKL`D(yd4xr8(!zRqw;mnwPd6|d56kibmH+H%(tm#kkmo`LiKn1 zY`wmO$kG=>;F&dW8n$tC1^=LREwF~QNO0roXciq^j}tl3!I`Qr`tWuQDs1l*wa zNmGg(rNI(VeBvqe!Eep8J&{OmK}h;oN^03ihur2anZ>C&1{0DJ2TFz@@b?e$mM-_^ zcJ7)D{K1f(UI)>8I58q%T$DH@0-0@mxxP1}C_%x{2ks>N8XhhT1qHmO=SX(=g*PGl zTSWytuFRU-4{$>!y8G+N`}xZ|QA1?cEzfG+c0X)>0+S0_C6M@ReF@nC%FA)UUEr^s zfxzA;Qc@@WbgW|GD(Di@5Yb$P;`utC)C6Q#pE_o|w|lKq6)t>vbHDJ2$J5N!hh0O@ zakCc)4kR7=E+_!WBiTJ$mmlF=pd^crnP7AWx6WFa!i;J*1I^7#ipN6s_`g)!kqO?C z?gWN;2zEN<0NGB;#KR9dIxDB!zzkG~&Oe$Xw2s!7y*iTj3NIcmttK=(zrsB67szl* ziSC4Na*y>=zx};Yp*y}cSfA`4FgZqVK7ff}!pn+jBXoLQneG9l@iF%`fMgv9Uz4Ys zLBn_#S(R5vM8E#QgyK~l0e;O!mkLPQXJkkcuqrd;`1+UX2DC?D*FLlHyEE3!*bA(D1*aDV)xBI{w97 z1ragC_m^@)3ypoow=dpiW?#&(em}1FoJ=KXdpBI)79^c>Udsvf11|mVzKRg-2+TetHl`+Zd6Z%)iQ%TiNV73Js9;ZkVxPvK1sf=T zvM)*Pf8f<^RNV$oGP*HQ_F71!Q&B6VOP-`6iKG=AN81=C3yKX#->ll-5r`GX}I!L7e8?H{6*}q0XIs z#UZ&pMF>Ogg09zMjM;MU{<=>wUSa_8v8QnyDGHBqBp<~^OPhyrW#v5PyeVk=!dpa@Z-4xr2f5sve@ zMelI|nAnA_ZINZMtP|fd@wDFDcfiNQW>;Che%)$y_KB&H-WZBK=XW$Xz8v2gu~Z+1 z=AN!!1ijrJ><^FSQA2%We9i|~kN{*s0JTM~rezWyDBlVMFIUEs>XM9y1)xl^206~O zPwFc?SQ|K=Kv32FhX*BUyOnZ2ZY1UO1wP}I+5rJTVi%pF)eFry0hbtZkJo?XgPRWE zQsRpxy0?+6)*&POUw~j*PxV!bpwqG%_Y~d8{c$pU9darVnPtFNo|73?9dGiYtNv*L zz($;axr8o{m}eZHZ06fI7TTxwY2aH$R(HAfmIOVKCooZjWw@2KTOr66lTmO%>n`(D#mB1;|BE6zT~R9d9t8aeKLv| zMX~&}dqo!LOaiD30_VU5m12t)Z`n@MJC7DS7AmX&Kj@O7OFDPn?fkD%cJ&$?5^Vj- z@_;C(8fcdc3XC5P2eU?lt6A~8lU}n;?hEJiQ?w!JmER?W@5*H2JOHi85Q1PFK1GCI zou2WIUoSO&fXx5|=Qj4|#Az%1)`A%|QL8x6v|#E(j*ECtris~%Jh=}s0QaD!rAe2r z6)Bh5;TBsz&5kvfe2cbe+UnCfn%|ky^9_3p*yfZVBKtwsQFxR#ULohc9lO6~Cf5ApPbtUsf2`gArNwN+JO-(jWwF{k1r}?_NTTNGN_9N=vcK?24grSB2J9oGfPH z860pux>yQLI+4^Iv6RpnA!GIFAULBZ_V`wCaNjEusvA8_F6Rh{g%4NAJ-dp}D{KM2 z4v4>RrK;P1vUgn71ac~)EcP3Kt<$SF0h$S*OsYLrNyNi@01&W?0>i5hpxvxVtkyXq zd=Ta|a24inf+J{;1+pcuqJXRhNFu-%_=9Umo##ELKQVAA|l=*qlU;*L4~W(!j76Sxts zp`g^+{b1sDZ$Ry1P!xte7)Nr}cF8NAs$JwCPV^Tvffa)bf1P{29F8)e1o_PJktjye z#xw91UJ*7#Lv@*de(KJG%_xX6zM%&o%;3SNS-it;SaguyoG31`6O?>5=-VEtyq-Cw zr)uO22B+%8P=p{Jy6;`4Gze6{C$UC136NY$JY}8ee5E)`>LiJVIvnP~6{4I2prfG?WvchJdG%@x ztIf%zw9Hk$hk>p=yFNW$l1vng4_h~V3hw=oNL^hxC=ihr!X5T{jJW~|&%V>Lq!su1 zDYC@b5wCE#jub`B&v9c%ZL=vq;?Bh$?2WmN`Qqx{fT&2b>1;57lDNqs9 z8@(JtNLiP;QCeuKT|^6smZVG8+d@5j+`%6_2nJQR&lMJs9E^6FGJF0!$p2Q^#VcRE~=s)M%CIpm(Hx21+)AK?&oEUtkXz1F>m2s z$)~{3*JKhA9#TRJ0{eu|j#J~-7CiLx+pr>dVEj2lZsjshthZY?XnV4`#3%79j4omUf))z`8rJ6ps(%4edA=N2|2IrTPeiE1jk@{`saM2y8YH zRt5Ez*!)(mxwa;= zm!pL%nX%OXj56#wO^1*ltmfc z2GNhE*2ni8_C4v3<`*0TQg$QR6vIm(1qvqa2pd%e6goxG8W4?Q>cQEe`LAX>LNvdw z2oexv;CqQNDG^!v0lt~VOZqMw4xEbbaJ?nrLUA38)jUt7Y{8#{u?zC~S}aR)p?mtF zCo-0%P~bjcEhn0-(S0@?BH3B9;Oah-C@jYI>y z1yl`%(O?xdC+m7)Oe06i((bc!l5XSQ>Cfja-LK961`l>$<|}=b+mk`qSv~v8Awg2= z$gal*X%*a-($3x_8c>9xTQ+j)6Eu5a`BFC)V)FJvtnzfc=y&*)$90$5DJt#F0gC%8 z3c66794xv50qzCuJ0+sn-u>>QS)Gr`jGuMfIeELv+zDah$C=ms%;Yo?;?~)6Tk#8) zEPRET?Qr;8&)r=0Cq|LRE)?& z2-bw=-6x-$2X7h^oUQCD0?iei0uI5vx!cQ zg-nMkltcWF$zrz$C^#s{T<=Ou3e^LJhx@YhSmAjEmWJ~%s7C`rtGIfL*dozf-Snxr z%#$OxDf?xI=RlcKNbf!>6f{_K8%I)@&`+q!@NM9V%q$LPzx_0Uf*>$g7Id|v{#e`3 zOHaFhMj89BSM|}4Wm>(~FTBJ3eQ4y^T@6|^er?|_(x1lf;R@-6qQ;;#ozXjg3jl z5(yBOvnCp~c=9ag62}!C%a_Sa-SGxcsWcy#Z>e6w;P!OPu+st$QW(J^DLlE-pOYzN z&}7;vybMT-r0NxE%fXU&W~Q6bQFM|4M>g5n8p5~!tlZS69Ta&%NN_bgV?om3TjBc* zhVFwJ30-aPb?tMe;?{PH(i0hll~GsOE3_@t>KRi6zu}L7{?~nGNj62vhLMV{sd*sw z!F3)Kz$fspijchTfagc3iN(h%Yrqx3NUcapUEK)az#$-Eo6kK1vdj&TQV!OJMj#?uN5i#vCHhlxf6dK%6P!9zmzMZ7i+9(J1Cg& z?s8?QOV((n%|fv-cJxsJS`DJI@L|>9dIfstC5Vt#-3{Ez8e&Afq3{-*^eK;m8vN2Pnmrc zF$r#4V#;nP&xs4bmMJNK1Dp($d}7sD#pu(mm;h35Xyq-Jx_#OHBCgXY#)P@4K%3 z9qfaBxYyBIi!n#s`MYC`hcz&29In8Eg?rZ%MrOW(8NC!%rP_afE~VsXo^-l?;7jmS z{VD945J~o!?#9||#Z5hi9>Os~VQ#CC}OpcbA!(o6Zh%^601n}J>dn=jC&RW0zRgYs8f+_^0AU!Ai;iHlAcI8Lu>*qEs+8^p_@*Q13~ z3>_jY@zuCxV^v6}=a|E{xTz9#F)q`FRn@t*4t{Ct-WU!w^=)V%os?Or&PN{}4!UyW zkr1PNG$dcX^@X*vG>_~m)(g6Bu4lc)R7xs)?M8tGC)=KV+kJBKjKmL<4<_v6m)A`C zG*u84XDI2g7Hq(eEH`~!yeyUhBG|1c*lL?JUR?#kMwssvuKUAS&aC>f4jVqJ=!3oG z53jR^tl`?{a<@0Um=eG(sqoVLHJqa;zoxnEac^v*#a4aRH1be~&qnSJ%jIY7f5(!e zHa5@_NtNc^Rkze>u#KbT`_0vt+>O&b9aU(sE80kP2EBYHV4)!U3oAhjK|bBy;=D(n zMCV~9@1gh;usyA?7f!?ZhO}shVXuh@)oEi1bGyeoMDMf!IIK|c)j0S$Att0b^V8qH z)WxFxzqZ{RLt7JXULqwmpNnt-_o3g4b`Ij1);Z}g;Y^@G9m2v=9ax)uRU=8s5!*-E zaoE~h%&Elfn?rT)eA!QxnC(di^)30vVbxRJ?6ytRLYZebeiqIly;@%As(1h1X@`VhZYp3^R!&YB@Yns6V<6 z9?$*K9G54UP_A}7 z_Y!!Dn2S`-i-TpEaIMdD(V63A?$D>%=%N|)x0TawWAp?k)7{N*T*V)G{P?NB=1d-InNi- zj3@mWId3SyCh^O5bJP9HxJ3}S1U=nlMw$}PnL8yN4t_H0Y17@ey<>`W+T!N0)-Q%H z)kSrAET#xoI5c;)eRq!z=**!v&i>3!%uPkNAg5iaSaET%tQD(Ga$0N09T2a_ODMHC z1IQ3LN<=dVNIUV}zEkQjJE!HjNu%H0rSJJ*yrNp~>LY=t-c|rBtSB@SiM(vXNe}k> z9UfU~4#Z!(Xw5#Bmc1V+aZpw?jjy(R=2IUcxx0yuL(WNqpdh;^?}_lzm0QUOcb{${ z$cph(3BCH}sH2>4t>zz|v{e2o35}k&xI$~by&c2QKNt+q7__LjK!)BwbI};Nz{5|t z-|lo{!BWgdHemAc$7%(`iy2IvUo$RaXAW((s$Jua*@lzKGKr$rZ=cc4?*c-KepMhCsGngg)#j;Xp2PR6ENv3jcL?S&yP+Vw|f49mg} zvTfAyoJz|B=NO7eYl{z@<)oy0O75IT3?ka=plq8PfhJra>-WW~j7mfKmPGk&KO3V5 zo+0}*g}T7~mB|k01rzqxe)g5?mV(W?V8b-g< zeP6gSVT;I(dmwjgv5KGo$34w?#^w%bWye|hjFp&FQ6v2FN_BN`k1wVjYJVVS9n&uB z{aLWc#LArO^zi6Z*ewxJ`$#X3?rAl-5~(h_?$#^G-=_Gx-i`XJ>C!4{>!mq|4d$ur z&-||HO;!%WnF}`ug`+`ZKzUJRFM@N>CD!AU>hHFnnB6OOq87%zp~MeY2I~Hd>ti+O z%%!tEW;dTfbQ2TPUPpGgyG!IR;_hPvq0tyA1%6+^>&DII#`wj|#Jz|ByRN!-x)N4p z=ikHOmY8yzm&!i;Jrn#s0&K-23MY%&a(IQQJ_Z6Q|o>4s2Ko-k;;ovwf4>W*Ek5?_K|kqt_A!ZVJ_J0Z<9H(1 z72z!ps%H%y`WgmHgSs09A@QNcOS>jRu5%Fq6;%MKTMvqa3hfrXx;bu1RS?niTM$yn zHlDpUOj@>+_x!|S?YY;olBWINCatOp4WK~9(bRLHn5t8NT{-yuHm+J11B&&O$FoTS zi!IoYZ4~#d+MRc6(8eI~FVEOXF)p3LQ|r?-nqna9ho2q)*tYjK{23!=BB!wr9h z&6(6i2ta-Row$en7g;93wTUh?dH?xV_V816#!Ce3t@@b_cLF=lTgB@uv>&#bzI##t zy>3C~(rxljN8MGunFB5GsU$%=>Kyzciou_?>6ots-XBYkbMp#Ic~NDWL%wX zI;b8=({+f(gUGz0aG;y}pt;Rv@9ok>j9lZdIk)X`#kX{AJIH@8-rE&APj!3-nTzKx zJ$Td*`9esv)XL-qHO&hylOJKe3g?kP>slC_)5W*`?vP<*YL!xdD(_Ywzd!Nj-X(8i zNp5!!H>WX3)BS0N@KR1cA8@@B0@G_%okl$fQR0;eaKi>3{M0zw4-EvFJM&`HTg4fR zN$-=T>v3e}rTWcn@yy{!HTvu1oHG|)1>s!hyv)_JLu-qylzrMxxa1IB#ZM7FI#p-= zLB52JrLM5WTeS@gO^Q*U~8Qd@Ae}AV$A=rEE zqbgtRg(WNXmql*cFIF&uy}t>Ds781l%7zj5GRZDS0-zFR|>4!x7KPkkGKp zohD9iCL@G+DOSN_JRkKc<<+O?R^Qb$Ly!{j4g?E59dYsKBR7jL^W>(I;CS04Qvu*$ zQxanuis|0umDgZZ8nr6jo*oYQ3NJ?PsLyMB;=9?wIX#g_%ES98g?K6L@z}$N3A=f4 zu@XzJWY56jX%&cUdn*k8v`s>g1;Dc|TsenlxuLSISKIZos zg{n*>fdGjKh{nz|vv~rPxG;3T+Cq?cFhz-7+T8^?e-n=>t!cjb zEhk#HTm4KxvFZ3F_W<>G-ObtiJl0#=xuS-9L2_yXi;?~tJ@1}6L0z6RhCH{1TVuW7HPdl}U0=PR(L_*s1bT|;JK77%AI_;? z8hq3}TVQ44K?wry*m-)H--U$*U1dJ()uvs=%7V6}qX<{sVY_g|Q3t7!&n;KC8+GnN zsw{QJ0}#ipDvVt;@3JN0_4UiL-V4qeA&GnO5fFLD6t)8dD6n6#pyR&*T@#9@fQmh ze?hG9wNKY2utUcRWI`$E@>Y$D(^EW@0oc5(&E56~<$qE;=Bq-rjFO#rP(w#!lUWEQXDD4)T)^+H2dH)!E zE^#XxQlH^Rqg+vW4#D#bv%RTV;!Pi%uwROd-93;*;25Nx4irtTQ$3#b5fr&#Z+OZL zQwgs0)f%!w%_T)4f-q}UkB=b>?VTq&oUc!lGYAj#$>)I&u=dJ_u-!=&qRnzQp`B7Z z6!GHHo+&Z^Ysi)z{o8QgYd-S2ZDr;DDTHhW-J44zmf4Z`{NvAJinA2a!nW>Hb9wXM zge2w&C-WW-4?0agv+~4jff?-I&&L*vxS@CXu!Yf=M!s@-v+N; zX*SC-EvU^Gfvl_w$vY8y0N&*})l5@p3WAPG4AV*gT7t0gOA+6La-FU!^&O<0-ly~d z9PAH&c8ik*^I4mfXr5%@#G-qD|NG6Y`$pYk65wm$vXDc~erB;&MJx8>5T}K8 zKWHeaqQMPgBtXKBNpBsr({N9RqZdaziY1o6lo9q$ZO=P%g{99_c=yO1twpI>`DatC zFEA}L<@T-clk;JU+h;pJMLaWG+e0!XPal(bz7VSG%jQ>~XOz0(n*F;F- zIVQeL_MR7sB!1(UL;m4Ej9DK}%1Hv%t$^w?r(Ol<=?{Fh6?4B=H14j1@pS)ik-DPL zsJnPC>1jCizfoDos#Olpoe!FQo_8sk97mV-&hJDmgBjj9%*ZA?7@sV2&184Q^T^6w zCV%?ul}we)#)EY&2%-6Jp-If4dUN=Ee%Z-%91*-mlUdH4ipEVws0Ec6(M#9bG&2$t5@@;_HXknVUr%o?D?UZVapf+$5*(ycp?DJ32J*heD!% zU&F4POYV2)9IB*7YVz@K@r(RQqeO>Es>9DLq@isG<(ni)Qz|iD)z_>*UcF*hmhxuu zZQ@R%%ftzQgb;z;14sA7N}`t0zWTqt02vKLOF0&&ox1slgQuUm>vVNWAQ}TYte6 z$iY*rpZsonDGQ2SX~mUKKAd|>Auo((-48GYyX0B!Rf3c*k|}nYmp3R{PR4ZDmZ+;A zc5g8Klowm=(6yfDG=|floDys9kL-KQ-tes6_)P7cy0sr^)^xq6>LEDlh0fawB7)9J zfo$3O_lrhKe=PPx!L88MIuWuj8L-mahVGdysqa>cR<}>Nv@wGKhoaO6Hlx&dFOQ78CRT6Sw8S<&APJAFhq>K8 zvcUEeC8&U-P`{pD(Wt@aU)f%Ol&73K#eW_grnS2DX051=^%CSJ;2-hUvnKz22vdji zzb|ZR!2LknpHXUwHOB*edZ8x+_m2V3|33@}@qZ-RTi0T zZ5u+HA9M|iRQp7U zR689-es3IIuJ&|U?fPu|>GXfzTdn_sOdgru{8&y6mPV#viL~QV3Jr@%Yc0HDl$r{5 zL~(GQQTnkdYorca_&TR{OG;%cciFA!=SfT)wY^%zx9>1;6&1a}O=0(^>^q7I)C5P~ zAM2dTp>Ti^Wz6$puUPmuzn)vkx9J(#xwf8P3Js4;=}CHMlyaLHigD>u*^gAz+&%`8 z4BmM&2el?-266JF+w|$mYFdlot=x{y(jwEtX3`2$N?OugO{?*HhL&O%zh1>q;AqJI z9*(j*Xa=bxb%;FI3luy&7fKX7M$?yonsKWQhs%=xN!&Jc4PFF`EH*&PIS*?nkAMOA90 z=|g%(kZ`~{Uhc)?XLjp92UW_dUYSWtV3^%ZPp_j4oiWKQq?Lj4Zi?ZF8hq%m0Al$t ze1XRup^OF z;wvoLTT=OViUU(j=bgMeC@p7vPZ0IyKd%JG++gIm7y3wGp))x+LufX=+NKjG@$~sr*@@S zUOAMkn0h~xLFLiY0H~(R!ekExj$Hs%Sj2?VYFBYkhWJ)*Y;Q@zlj*kt(kW>tP;buR zB*2GV%BD<3dHazf^2Z{hx6nortQ1tbMuZ}Uw`YJl;xR4q3W`U~H@DC%N|=u~w^-R# zFwOTt$Rh$yqkL}R7NWI^DNoI)F1KG?!>r#2P35A3jH7)5j+4)xgv)DYpxFe<5`hCx zs-!mtmGM!&$LtrsBBLPHHIdPtDyPPw3jdquc-4@f@(P_bD?hIP%GPik)xxZ(E2jDN zJn!y=9u;QXnl<-<{`A(U7sKkkJkS=*f1x&m_UBTfMhs)gX4eiQ9DzzUeCY#MhPE&9aks#8Ifa<4O5H zZIfmZ&8cNAIRxNA*%=-i1j70Zv1e-JUPSp&;dVtF4wWhHBuSGCi@jS@E4Yea9*|$? z_^%s~G2t%FD3It4GkY$VzK|)DY2g(%4-P2uh)ThS zkIgKn>Dug6lUQ54n6(Uf;^U8-&Zg-yiB86%T9%?`v%J^0+!MsSUYJ+=M2xwsKCdda znOr7vU!5UhcA5D6%AKgtYjoS_=mtI!HE%3=&260a*5EIOggV(d_m<*~s`O`TG*O;8 zWpi#QCH_RkPEnZB`#^?Lck~H*Oxv3)#2s~-24!{shTs^!Uz-ys{h{!y=&2QWvM=N+ z1IJ6}9aPNwNruuLaf@?B?)r=MJu99BD?(o|03 z8Sbv%BdMl;rUdD%z!&a27>-F@uUo_`i6e$%A{pYXkeW0@@*^i@4@cMwoi3td>( z(*;NTRFD*m1LY51T2@>csFrzY3ICP<;N^0~m657I zI|3g^9I2n_nA;`fq_OswmP`9DDW}w`-Udv>OGPM7&fmsL@M7%nM_~_Rlj*|k)xDo(GnUP_e_w~r@$skdulxQ z(Mx}A?HxtS!5c?2mheb-rLMgFr+Hr#@$qR{CheqNX9`-3_*h+6S5JrFIW)52B6H0h zeabb%j|8ZjQ2hZeo*0#}b8Pcjxz6`h$#73Cji2wk!!6osfq)ayB_;YwmLnMklZxHv z@Q{QCNHqBqtPyH{S*az@69qpzB-&WOrdUc>wHK5T#kQzlRi`xjBlIFeQoJ@3IDPAZ%pO^y( ztNV*j#P7n=Y5fw#Lq`g&mW(0_Ey7HH?c4F=4#Ft$H#p8ZzdWrlY$0lc#yc_Kmj@83(iV%p3{QCsWTkk}Y~ z*Y{|_eLEWbq%XV{3Dza&HvJt_70XE2Q`eh&qu?u9!do1eYP{Ol?(Bx4zgRalvj-f) zZ*$ku=1oyPTuvhiBtZr zg4YF!Hz3;J@iJJ(h;Y8NcT$rs*`K66%dLZkpvopvy(vN{X2&h2s5v z>Slkwu3R52iP2T_wXXeoV8QpZ}QYoE#;({SZcxx6X>h`>))$TPjTPjo{KVRgL?5;O-xQ=RD=syG`0B zGo{yMB~R2b<(fr%WKu9>!n?61*e5p~z2aflYz|FjmvvYSBww7PHV<*GeRczC!Fdj8<)*uGXxdYT*+9}z<_lF>hUE+)}IK1e!~T5T`}+aY_1DUE0Di| zTv9k2d5HJ&X&4^ne3-huj+ZCd4vJrAj1oD8yQSh{|*qUzL!TJ*1)mD|#?6Q;BY z+BQe`c1g28?{r_y-p?e>egglEELL;wH%qRI0x3xF7f7OUV3|sJh&gJ_Y~v%cwCcwI z944G1f$eg>8UY-+eRav>^VgR5PHbeFU7zAQHt(*IJZ1)pC!CBw~&U4Z`wzu?H z(phl3niBaRYXcK)xrbS~`I-sA%AhqkC{*dgN_8ks#VqPZp()r`lcgSHAT2%T*oslK z()w~x zgd3?~u`a=75%}R_dD&mUIUd-99Jxv|IA!?F2EmFL!|KS}_4Xr`G5k-{+IeB~aLQXv z@YK04bjPAl&r~3>1=T0qhRjTfrcII~15U+^ewg$D(t+)&NwG`;n}mWl69D}_fSd)7 z?Gxuw?yT?}G+K>bnpvY6Y80{Kaz!fEUT z31Wgy;2{hY4?ze{E4{f8g%--0OVpO^v9YJkn$#bqU48Xqy><5qa0YyJ2l6#O;s=P8 z&i#*sM0XK5*;4L?<(S9jYUTRY5ui|Ss1WE&YFn0csj9_c@#h#2{Cx&aMUo7jR2yYZ zM{?0Kzs^^N=A5#cxsxZi_v==$~T^3;@KZ4$I0SkAf zbGjAPpf(@TYIgps7u{co(F|G0*^wBX){i0E0H1&vrk_TpKbx0Se@c$BM;B3mt*u_B zt}tgd#P(Tvn4_a%POPlCSaP zC61R=71NQ2)xyY`4sQZ{0iNn)sX8$+hPF->K_y3|Ss6{f}SzAaM}#P0|yni7PzG_~o7| z{?NRhSbgy@rz+_zV!KbtzTC7F|%EP({o|54NP&CM- z@63AZ;S&IPxKsHOfF<%NRe}7C_PhSMZABh%1Ule@=O6ptYww&~ujC38QG?ZyO9?1q zzK?dC4vkkmL2j=5r!fNA=XSB{t-AkU^{W=QG2@5`_@p6Wg0C6eS|~TuP#Cp067yfy zPp>~E&HP5qpe3M>?j=xcrz-G@u*(1Uh(4XATd=Q9B)!le3rtbpwD2B0Y@EZ6IUBN9F$Ovfc`Dk@nNrzsdqu zf%A`H-?m7MO56)k<9y$jDjp*Z_E55!h=AARmxMhY#--a&#qmmxl@VSY@&1L|N-2`@ z@f8#)cj(ehv_QJ^d&A(qUQwK~ZHmYT{`yiQhTa0wEQ?Uua#-}eE=S&@i=?0EZbWeyfoc&KK~%ApZi}kH z7jnF|e<6HF#Y!RrsQ(P`sPHP=XOo8g^;Q@Cm#jiKiK%&H00%O7i#PhwakUq#*$^Nz z172zcP7=xco)J{Uha6r4L)}fW*GsAPQuQ7cNkQ+a<@!;PH42Q~>hLQ=3`U$L&0XB; zxZwH{axDQ`Bzr{Ex4Q=pL&2Za3$W=J?6Z~WM>E2ak&~zXuoS!uC$7T@!lmzSj=ds= z`}5n1o=9(%;Kc<04)%`9x9hU8tUHCyR|g8)9B#t!hw$_&`%Gx$%dO4#?97ClEalE-mltu zD^=wfM17E}HZuh_^qT#?z9ELz7ozGDB7J;@|6p;f>z-KsmG7JYU~Rc1}Q2zXlBBj;?k@&(@%k&EXpy{(&@>XoYwtONEYwl{BF~Y78{C}{7pI$Tr z&xc%5F(lC)y?_QfW~i&f><);%_HvZ4Fxy&FvbqQpi>rWl@nLXUxWApbz!W<1?1oVF z#F0m$R-CuYN_eWRJuQr3v8C)jcN0FjflB!0WA$UNz34Ugit9McaPreXC|9^IP)dX1 zq3YqYT#&$Ic`jK&DTyEtFV`p_C_}Dm1&*NNKLJDfOha;O@z7#nvsbaGxkZ+T>P*;Q z&tkO|^^_8Mt?Xe_LF8+2#A-l$gKw{v97es{P)Ot67MsxxA&0%B-o;ghT+i_13F|>x zd>%Dim`i~smtSBEL&92VI&Im>Lv|8xhah&_*WClUY2LwZ=dBxvdioo;l_8C>3; zko{>hfE?DxH0lM#qCExqZ^!<*YFX_cWdPgEy=GK#%%H7@zfN3+Mzt@MAce)F-$ux-BVbX^#5oEsy@bsR3qnZH7|-h_)An4+W+YgQ=C8>s~v_%dxdN) z$Ye%GfEtYxT5(xJIDlZ9{-ZC|&RBy{-u%FI&dB1Ad|!X!^W2Rigl6ihuM7)syat>JvNV2jY#Voha^4h zwk5tdLW{5uS6e+BUYxuHS0U3N{Rfus6(q_sfT&Za@4*OK?!7mQZ{fps_1P=8%iN&l z&~q~5u({2E(Y?|pDhd?mbjtu(D4$=1!yKd_EVe(u@}SXu--F-HWt2;Y3$BU5*3!TWq??4x!}|BZmKBAm0m%&d^^U);tb^p+ zIkLTtzEGE2<*rcbW`tSr8@I;X8%RDR-D^Qqa}oGF<`!lRhd57ZNlvr zht*z3DA?VFQC9$}v369|>LE4-1%i<9{WEp!*6XGRj*Grylq7yeUYHRyCUM3@?~C*H zdnD2K0U4Z8ek@fY`l7kD6QJ48Ig6A7&$jQ02lSf=UV=$|kXMpu zZz3jd6`oF6e6PY>JIzUT=EjV6xpbTNzf4305&wc{2I?7{(kE96qC|D{T;hA!f~TVi zY~fk0Cv3qa;4EUe&WZu2Z>WEc&4C8UtTXf%m5>UaEwOa%R{&s7TDsX=uzRustX9Wl zu7^9ss2kl-Px5x2oCYjTlYECB5F+uq^*}-xeZ2aHzMi5@hu5*#)gu|X@cRF)DhuUS z&#F|HAPb2IbnijEJ%so`edKKXK!b^d=YSRuZ5`d__T09Xx|$p)-+Z`#v4nSt9iZ|6foQ>^`I(pD8R)RBmNlJ?=N_kn5Ck?OtcOPR9Ti2fnfP@jwQ%4%QZ zWv?}OWorE4U$3kXZ&MJa@8}V*8=*cv*H`$`3922cFOgot({{RINm}PvNIhIm~2@$<9vr)^f zF-fJnrZ_=B>0E}p+yT8Iq;sp*#vJZhfB$dfr+p6aI&b+}ZjW!2683rXUDAR^6l6|W zy*+gi0zhu*SRZ^(H9GkG1k3JA!eSiAdiYMx+;!m*&(X9I{th*0C9=CZPz+yNQ0u*D zi_>%a!SB+_xjqdhUh762E$R>4L?TXv^yAhq$JbkC`BM4ET|t)6tF<3sk7%?Nm@ z!IL2)5isG)0g0@w?&$Y0YG@a0hiR9?DIYCB1dc@TEr+xI;q#g5n~Ktxf2bA`yckcO zX*^5-lcxMX*0KhIH}Q~Y?j$X-I8NEby^zR&SD&VF%#{liz71GkiPqqlSQ-!p`QB#> zcWC*09XmDi^tz7fGr@~&N`EJw@`&xe(mLEsIJf}sa3J!6vVc7(-h$e*vrea^w{!RO zg1X{!7#-wX`=`$=6GDwr=%hqO`fR~dT@0V$?lh*%*vIQ%=(h#6gQMY!j=OR#bxV`8 zJVqwPtNEL3w`ouLZnCwVU{HNReC36r;_HWR8Kf2G&&sTxB_O@a%N-@&YEd7dz9uJr zGl79L>0D`rvXimNjH}q$5BEy)x%#;_4!jyU&Ulos9#L29Kkb;TUU4^Grc@J8EMdo% zcrbeQj|4s18BrGk`WonP7~CU?7D#5MuXfw7v_G;e*lT4hve&?SAhsvv#$LsNjo&xF zzCqg44yPp#KdtBw;77yk+5ww`i z9iHiSZ@Msl)T-X4@k<#Y#@E~AxdwhAL6oJV4FbElu?72|`m| zokwH2uj96$^%0LyLX)uxZ`djZtZ=n&x8|FW(Ik&aFKqWbc8-3&YO`T;d~cfD&COyKwp%ECk~Hgi?6ReR*Lb#l z^HZT&c#9qAmhZM5VX!)~$mOy_WZn-0|4N?M$PIFEcABzeAHPFSRcXkpVJWdp8P@4d zLsB^tcK+x<9k={HuJamyPsqjfd?~4Z5nZ|ZnW|tc<6Y+3v$^UsUG|5nnl8irT^jsP z5E}U0z6;O5M|Jf^I@6Tu$_tvl&l0tlhDm??dP3^879DU7ynr9qX%-E6u2hHVdUfk~ zl8XbpnQeD<{Lj!Shy+(JibhvwipBocKPX2yi)v}|!EZ6`>&ZE2%Pj7wAR1w_Pf^H; z{m!Z3uo~FyUfX*-r9DjfQ8mSJ_T>AO5a$()Wr{+aiqQLHTUm%m4eB@yYM*5ZSeVNT zc+6J1VzRlihi!V7u6ttVG4tD-?Nv7(h`FZW)?@yHM;K?ateP^7%v7>eBA4Ri+}Hk; z@g2>foEv*NuP5JFSi`HhA^4gD%J0NUQelfxf$ojpbk1!QJ7Y|`vCAO-A72V#g2&4G zO(qt{r6nZH5yss8cwVEhlE*}@ZJ%hA(HpytS+7?uptvr5l&vqndYkWgr^rrk4ycKN zA9=2+CPg*}b83}S31!hN>HG`p%-HXj{e-#LtL&{GkMoo~pX#JPp0xpHjhPJ~pf2f9 zi|qNGX23<^_IGx^>TlGh_YCuxHRL^h)xj7)#~IlU$@A)yuXYem^|$f7hOCPPnv)e4 z;rp&yu;GP3AW}o_Gg+J;``;yV>i84keqGb$a^llDlkCB6yS+uHmg7Ud%QT+GBBq#y z`6MYxLsUV^OvrcVc5VG)xfBOSAQwmFION8Brj2TzjksHty0v%>KQ}M=ox$jeLBKk@ z58fwR3x7@J4d-{6DSaCJpAVxkpT`K%O}}1?p<|(_Si_I}RMSYyS=44uUf{QnC0Dk) z(lX_t-|unJxG{V5<8gXx8N=%1)6|jYlJglWEVehnbiA8259S3HyI2kmsLalv??Y^l z!K=Flotv^0w~R;3 z)Ql{j4o|EE9pP#zbJeWgm)Z*NClzo#(dS&N&!g`7g%=$k&|^2~sR|#c#S0`mUF;|^ zWx&So+PSfn3O?8oIUY-DP?KO*SP}}NXSx2Ze<^57Jz7!d{p__}?7^mH-LS0i);nV&QRr2HYZ6Hk&v3$6tcajd8U$8)m#{}Jv3@}P_3O>P?Zg&yzXkO z=Qca`xw^%=`R8g}4o)sU&Y2J4Ss37clpH1Ms&u$LL1o`{Pw4sHR0@``*G_`k^QOqG zPFytw$j3c}WlA`Ro=;ch$uRm{Q>$Ql$0-r?kSIM(xiSb0Sdsz>>LXo5v#Zp-nEZF& zfgbpL_$s@Q6`RW+SCb&%SXeSGw{I02NLgml7s*wHY|L%q+h&ZT*`C;b==07exFysQ zINjBj-~8Y(=+u#x(kp10*`4cje_r6ImJVx>VkgE}_$L$2dtkMjex`XIfK2*)9Gc-? z_L)9=qs?$NyRSo_aV3<}Xyz%Rd8HG2DAkcq@%-D#Gv zK^a~xy4k9y=%tdjcSXvNgZ?i9%F$JSvTyZxyg7J`=_Nz`C>%$?*WGBU;XAx7Vxk*X z>y5<}DZ>)v@M=+xn$T0*xa--36uBB%JjQZuk&?@ClRKk=;82R}$&ZayhpW0V_&*E8 zi#)kEzaqUqWYN>EL8)NC;bbg3-+YE>T!5Z3*D}RXAtSbX{W_Mg+qo@!zUp83d9j0N zoZ(dIsXi3+{+F+1zkZ4|5uI5e0s!KD<>~}g!Gd2?k-e6lp}}2jzc;VQtiDkx_zYU9 zuIk_AMI>SMBpQA>uboD2nN%Gn587c>Em8|BC64Mgb@wATJmOlFdIMwq+Yr&GHk#&M3{=T0Bomj_XvB};U* zJ*oD|b*1H%^^o(T!@qn3RXT+u(JIiCF=o-wh7;cr>nEcqEqFmg_IYYD z{ID5g@4A6K5sJQw5!etDWkCgt?>0Xxi{H{r(hbzOv-i5h0^2DIqIV=RNpo zI4`jAXLH=a{rbt*$3L3Avs`Tm)Ve0SuAk_7Qo%rumLiLnZkDFR`Qo@yLE+W?r>t}e zqM=~crBhZTQe@EZfwmhmi~z2T3R{>1{WY`i}7w7BSJ8qEd$+`K7OX z*V7bwDn-qdEG#(0$6>%zCPlUEx_VD&YU+V=^OD&pC_>lIM1_OjX%liKsT_ARXjI{c zK4P8Nq1Wd0EKSahf9{;ly5m%z6FqB)9;frp+h!_KhTj+}q|7e0bU9;ZDX&6U`1NA| zW`H4q#Kmx9}@Ui{dnoR*(l8#r|FtFD}A z9OG2iMV_nGb{(92B_BL5xIM&)z-2Q5x{g+v)iU*@o%QJMx%q6wJd0;C-wqBGN^trS zBpf?+m{X7H!S18aYmoG|GU^IU zrj6Ry9S)}?g5QysF+0yDeN4k-bP0UtKBF-fPr;sd)kv);=-uuNv*JL(`3|+ZS_K^=* zZaCUEY45ZYQ)DX&B^RA1N?qOSNS@7-;W}uM#DVl0&Uw+j&xPpGO^R1|76}^OMc2wE zN~<=aba@pRi!8KS5Gzh7FCzA7r3~!^hzrG1p~SgW&)1)$5rr>x=*VJiT5}yAP^rLl zZtTY41E=PmmG&V4R-@nQtAGb*jXQUa3XXqRveHlax!tljEjEu^pDB4WaQeap4dy`v zWfTMqaz$tHR5(e?TssgOZ+S%&7hStLrYwZCD5}Y7Yp29}DJ_3AvK`hK|K(E@^rCdH z711sMkMK70)WVQQVh4((F&=8CR}SQtufT6y?%lzxe06w{XSVE@>?+vA*SME*I*ZH{ z;=veKz&x|S3j_nmw~%}Nlj=E5?k)QBhJxyHc-jpTCLIxt(C}z3gs7#|hv(@a_JQivN zI}?8jirld88C-W}>n69GINU8Gi(XA>n?zm#D9+%~he*Rf$?sa&=|h|IaEFP0sP zB8WR!Fd#N7D_MO~~&#}2oWI@54;HB69QXm%rZ3=HPjgFv0)eyyz?bBF8z36t1i(B zZ&>PkB2*?nnE-_j*G4NuQd%?7m)?k zLhDj1WADS}B+HSQ!|U>PPnUg@@XusPfu1cFRZLHTv73@&-D#oG_Lo2o%ku9p{CpE`fHAjz-4spUjo0I%Jxmi?xAf~ItlYnDQX+|aN^Dhqm8no%r+Y7HXDtcY2?Ux&5!T$=^&ad1#v*-YO% zw)L>fk+D0aPhL~+ft09{(4XD&8Z0F*(OCPLcn`IAE8dVofjw}?ZeQqYim@J#+urI= zkIla+sx56+?)?|Ar}3x54NU%oC(VWt;$JoVih0yqn2sD4U8vAA^sf5LzG96&Ka~81 zcU9clW1`10#g_4xVPGz~rMQ)&9l7g8xZyMB}2v} z*lcsF9|;S6QhKY`PDgGil%!xEE%wT5DvHS=%mfs_mMS69b(D_jc%|IY@zbE*#_&wU((q;4XQ#<(iK!ncr7VVzTn7u#DY*k$LVRD`DAYD7%6C5K2X zf4$Q)!CKp;v z%rdhzBQ`%@M^}2+YuVSxsKy)*fv3AP|6KO0ca{EYgB$#d2f4U57VNt`>t7ZrtEIRH zA6PE?PCu=Q`bl~rdFNwvxBbkIYp#1cIE69LnIG|5Y~9-&ad^RdBjCJfs#sHJa>(1F z2Vsq-7d7OtyFWfyO;8*K?XHT@!R}O!PZ7#lCe$tOZJiRk*_UKkC$hisO{p1ML>Jco zW?$FT5u)?YuHN=;r!iZjIELL_aj5!}9O=3xU^Z?5VOQdB3n{i7s=S+k5*{X|@`=!3 zxU0x?!@-!~bc|ems9%@4yF*kUSl`=JNqRY^^RG%!Nw`wO^ap{H|EsrSZEE6*#yZt@ z#8Ahnhy~FhCI%1%l46>+*z$72YmA~%Fs23w4>1ZMz#x*AYC2ZYEDH!ofWm5dMNlFr z5UdJi0~iSb20?rfp=b%?YQb26-c9@s-A}t`&*R>^=bn4cx#!Lfrg?vTX{`Gwt|SsG zyd}|{jln*zGYjz?;chBOk=Sp^hRE*d`^(a(~ zKAW=SNQvOJEImaMrOz3ud|6f?!dET7Oay;+%?p?CdY6c|9pb13NEGw->0?7E zVQLzNh843ZLqwQ>W@e}88~WZ6>z3q*I{m9J$2P=?uO8gKepF#xi6T!QZmj5#h4=lI zVd{R6-QESpJ;I{X0U=XGh-Y+?zE63Bb-i@Xt9$1^cTTus3Ka2I`v&Z`uUp~0?%Y-i z>qw)@7WDGLGfl4Xa}?14hIMsP009}*X*onrJoU=O1C+mj$?Kg;9lnm>Ee|&eqe1rX zQ}bX30L#v3U}HFni~%r4)U1+7<9F>~hh1QOcJv&yu|Rrf@+pA@++_)JgWxvdHpKFs zp-?x}p}%@O9F`a?8%ptW877DlwLEwX4;_q7thd;Y%Ob0a&Nw(-&TW|H1(xl2EuE+XP9y z_*P_tIf4VD`0Bouk?T+!G)qTY z^=ay)vlFs`wH=m2fsA4r-0Fb^ap;e$9laE3phN*|>z>+=7^^7}dEBG!=vf1~D7JMe z0T*<*1$%m9G!8w?kW<)Y9p-M)25bOw-;9Esc(Cg>OC#Ni|DZHcSxpGOWBTIW!a||p75mUrXxm}k9`K%}#(v%w6kSP?O=W2y1 zxx8U3L7ImTHV-XHTy+qLzXkbyx5pHOSBSC}fQCv2TmnigorcO<)!m8$v(Du z-1Vvg|Bwon;8Eedj|c. +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SPEEDWIRE_H +#define SPEEDWIRE_H + +#include +#include +#include +#include +#include + +class Speedwire +{ + Q_GADGET +public: + enum Command { + CommandIdentify = 0x00000201, + CommandQueryStatus = 0x51800200, + CommandQueryAc = 0x51000200, + CommandQueryDc = 0x53800200, + CommandQueryEnergy = 0x54000200, + CommandQueryDevice = 0x58000200, + CommandQueryDeviceResponse = 58000201, + CommandLogin = 0xfffd040c, + CommandLogout = 0xfffd010e, + CommandLoginResponse = 0x0ffdf40d + }; + Q_ENUM(Command) + + enum ProtocolId { + ProtocolIdUnknown = 0x0000, + ProtocolIdMeter = 0x6069, + ProtocolIdInverter = 0x6065, + ProtocolIdDiscoveryResponse = 0x0001, + ProtocolIdDiscovery = 0xffff + }; + Q_ENUM(ProtocolId) + + enum DeviceClass { + DeviceClassUnknown = 0x0000, + DeviceClassAllDevices = 0x1f40, + DeviceClassSolarInverter = 0x1f41, + DeviceClassWindTurbine = 0x1f42, + DeviceClassBatteryInverter = 0x1f47, + DeviceClassConsumer = 0x1f61, + DeviceClassSensorSystem = 0x1f80, + DeviceClassElectricityMeter = 0x1f81, + DeviceClassCommunicationProduct = 0x1fc0 + }; + Q_ENUM(DeviceClass) + + class Header + { + public: + Header() = default; + quint32 smaSignature = 0; + quint16 headerLength = 0; + quint16 tagType = 0; + quint16 tagVersion = 0; + quint16 group = 0; + quint16 payloadLength = 0; + quint16 smaNet2Version = 0; + ProtocolId protocolId = ProtocolIdUnknown; + + inline bool isValid() const { + return smaSignature == Speedwire::smaSignature() && protocolId != ProtocolIdUnknown; + } + }; + + typedef struct 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; } + + static QString getModelName(quint16 modelIdentifier) { + switch (modelIdentifier) { + case 9015: return "SB 700"; + case 9016: return "SB 700U"; + case 9017: return "SB 1100"; + case 9018: return "SB 1100U"; + case 9019: return "SB 1100LV"; + case 9020: return "SB 1700"; + case 9021: return "SB 1900TLJ"; + case 9022: return "SB 2100TL"; + case 9023: return "SB 2500"; + case 9024: return "SB 2800"; + case 9025: return "SB 2800i"; + case 9026: return "SB 3000"; + case 9027: return "SB 3000US"; + case 9028: return "SB 3300"; + case 9029: return "SB 3300U"; + case 9030: return "SB 3300TL"; + case 9031: return "SB 3300TL HC"; + case 9032: return "SB 3800"; + case 9033: return "SB 3800U"; + case 9034: return "SB 4000US"; + case 9035: return "SB 4200TL"; + case 9036: return "SB 4200TL HC"; + case 9037: return "SB 5000TL"; + case 9038: return "SB 5000TLW"; + case 9039: return "SB 5000TL HC"; + case 9066: return "SB 1200"; + case 9067: return "STP 10000TL-10"; + case 9068: return "STP 12000TL-10"; + case 9069: return "STP 15000TL-10"; + case 9070: return "STP 17000TL-10"; + case 9084: return "WB 3600TL-20"; + case 9085: return "WB 5000TL-20"; + case 9086: return "SB 3800US-10"; + case 9098: return "STP 5000TL-20"; + case 9099: return "STP 6000TL-20"; + case 9100: return "STP 7000TL-20"; + case 9101: return "STP 8000TL-10"; + case 9102: return "STPcase 9000TL-20"; + case 9103: return "STP 8000TL-20"; + case 9104: return "SB 3000TL-JP-21"; + case 9105: return "SB 3500TL-JP-21"; + case 9106: return "SB 4000TL-JP-21"; + case 9107: return "SB 4500TL-JP-21"; + case 9108: return "SCSMC"; + case 9109: return "SB 1600TL-10"; + case 9131: return "STP 20000TL-10"; + case 9139: return "STP 20000TLHE-10"; + case 9140: return "STP 15000TLHE-10"; + case 9157: return "Sunny Island 2012"; + case 9158: return "Sunny Island 2224"; + case 9159: return "Sunny Island 5048"; + case 9160: return "SB 3600TL-20"; + case 9168: return "SC630HE-11"; + case 9169: return "SC500HE-11"; + case 9170: return "SC400HE-11"; + case 9171: return "WB 3000TL-21"; + case 9172: return "WB 3600TL-21"; + case 9173: return "WB 4000TL-21"; + case 9174: return "WB 5000TL-21"; + case 9175: return "SC 250"; + case 9176: return "SMA Meteo Station"; + case 9177: return "SB 240-10"; + case 9179: return "Multigate-10"; + case 9180: return "Multigate-US-10"; + case 9181: return "STP 20000TLEE-10"; + case 9182: return "STP 15000TLEE-10"; + case 9183: return "SB 2000TLST-21"; + case 9184: return "SB 2500TLST-21"; + case 9185: return "SB 3000TLST-21"; + case 9186: return "WB 2000TLST-21"; + case 9187: return "WB 2500TLST-21"; + case 9188: return "WB 3000TLST-21"; + case 9189: return "WTP 5000TL-20"; + case 9190: return "WTP 6000TL-20"; + case 9191: return "WTP 7000TL-20"; + case 9192: return "WTP 8000TL-20"; + case 9193: return "WTPcase 9000TL-20"; + case 9254: return "Sunny Island 3324"; + case 9255: return "Sunny Island 4.0M"; + case 9256: return "Sunny Island 4248"; + case 9257: return "Sunny Island 4248U"; + case 9258: return "Sunny Island 4500"; + case 9259: return "Sunny Island 4548U"; + case 9260: return "Sunny Island 5.4M"; + case 9261: return "Sunny Island 5048U"; + case 9262: return "Sunny Island 6048U"; + case 9278: return "Sunny Island 3.0M"; + case 9279: return "Sunny Island 4.4M"; + case 9281: return "STP 10000TL-20"; + case 9282: return "STP 11000TL-20"; + case 9283: return "STP 12000TL-20"; + case 9284: return "STP 20000TL-30"; + case 9285: return "STP 25000TL-30"; + case 9301: return "SB1.5-1VL-40"; + case 9302: return "SB2.5-1VL-40"; + case 9303: return "SB2.0-1VL-40"; + case 9304: return "SB5.0-1SP-US-40"; + case 9305: return "SB6.0-1SP-US-40"; + case 9306: return "SB8.0-1SP-US-40"; + case 9307: return "Energy Meter"; + default: return "Unknown"; + } + }; + + // Multicast device discovery request packet, according to SMA documentation. + // However, this does not seem to be supported anymore with version 3.x devices + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, // 0xffffffff group, 0x0000 length, 0x0020 "SMA Net ?", Version ? + // 0x00, 0x00, 0x00, 0x00 // 0x0000 protocol, 0x00 #long words, 0x00 ctrl + + // Unicast device discovery request packet, according to SMA documentation + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x10, // 0x26 length, 0x0010 "SMA Net 2", Version 0 + // 0x60, 0x65, 0x09, 0xa0, 0xff, 0xff, 0xff, 0xff, // 0x6065 protocol, 0x09 #long words, 0xa0 ctrl, 0xffff dst susyID any, 0xffffffff dst serial any + // 0xff, 0xff, 0x00, 0x00, 0x7d, 0x00, 0x52, 0xbe, // 0x0000 dst cntrl, 0x007d src susy id, 0x3a28be52 src serial + // 0x28, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x0000 src cntrl, 0x0000 error code, 0x0000 fragment ID + // 0x01, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // 0x8001 packet ID + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00 + + static QByteArray discoveryDatagramMulticast() { return QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); } + static QByteArray discoveryResponseDatagram() { return QByteArray::fromHex("534d4100000402A000000001000200000001"); } + static QByteArray discoveryDatagramUnicast() { return QByteArray::fromHex("534d4100000402a00000000100260010606509a0ffffffffffff00007d0052be283a000000000000018000020000000000000000000000000000"); } + + static Speedwire::Header parseHeader(QDataStream &stream) { + stream.setByteOrder(QDataStream::BigEndian); + Header header; + quint16 protocolId; + stream >> header.smaSignature >> header.headerLength; + stream >> header.tagType >> header.tagVersion >> header.group; + stream >> header.payloadLength >> header.smaNet2Version; + stream >> protocolId; + header.protocolId = static_cast(protocolId); + return header; + }; + + static Speedwire::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/speedwirediscovery.cpp b/sma/speedwirediscovery.cpp new file mode 100644 index 0000000..42a1dd0 --- /dev/null +++ b/sma/speedwirediscovery.cpp @@ -0,0 +1,349 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +#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/speedwirediscovery.h b/sma/speedwirediscovery.h new file mode 100644 index 0000000..0c181dd --- /dev/null +++ b/sma/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/speedwireinterface.cpp b/sma/speedwireinterface.cpp new file mode 100644 index 0000000..a5fa1d7 --- /dev/null +++ b/sma/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/speedwireinterface.h b/sma/speedwireinterface.h new file mode 100644 index 0000000..fa24287 --- /dev/null +++ b/sma/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/speedwireinverter.cpp b/sma/speedwireinverter.cpp new file mode 100644 index 0000000..39329c9 --- /dev/null +++ b/sma/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/speedwireinverter.h b/sma/speedwireinverter.h new file mode 100644 index 0000000..ce2d34e --- /dev/null +++ b/sma/speedwireinverter.h @@ -0,0 +1,202 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "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; + + Speedwire::DeviceClass deviceClass() const; + QString modelName() const; + + double totalAcPower() const; + + double gridFrequency() const; + + double totalEnergyProduced() const; + double todayEnergyProduced() const; + + double voltageAcPhase1() const; + double voltageAcPhase2() const; + double voltageAcPhase3() const; + + double currentAcPhase1() const; + double currentAcPhase2() const; + double currentAcPhase3() const; + + double powerAcPhase1() const; + double powerAcPhase2() const; + double powerAcPhase3() const; + + double powerDcMpp1() const; + double powerDcMpp2() const; + + double voltageDcMpp1() const; + double voltageDcMpp2() const; + + double currentDcMpp1() const; + double currentDcMpp2() const; + + // Query methods + 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 + Speedwire::DeviceClass m_deviceClass = Speedwire::DeviceClassUnknown; + QString m_modelName; + QString m_softwareVersion; + + double m_totalAcPower = 0; + double m_totalEnergyProduced = 0; + double m_todayEnergyProduced = 0; + + double m_gridFrequency = 0; + + double m_voltageAcPhase1 = 0; + double m_voltageAcPhase2 = 0; + double m_voltageAcPhase3 = 0; + + double m_currentAcPhase1 = 0; + double m_currentAcPhase2 = 0; + double m_currentAcPhase3 = 0; + + double m_powerAcPhase1 = 0; + double m_powerAcPhase2 = 0; + double m_powerAcPhase3 = 0; + + double m_powerDcMpp1 = 0; + double m_powerDcMpp2 = 0; + + double m_voltageDcMpp1 = 0; + double m_voltageDcMpp2 = 0; + + double m_currentDcMpp1 = 0; + double m_currentDcMpp2 = 0; + + void setState(State state); + + void sendNextReply(); + SpeedwireInverterReply *createReply(const SpeedwireInverterRequest &request); + + // Request builder function + void buildDefaultHeader(QDataStream &stream, quint16 payloadSize = 38, quint8 control = 0xa0); + void 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/speedwireinverterreply.cpp b/sma/speedwireinverterreply.cpp new file mode 100644 index 0000000..b7b2941 --- /dev/null +++ b/sma/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/speedwireinverterreply.h b/sma/speedwireinverterreply.h new file mode 100644 index 0000000..3f6a3a1 --- /dev/null +++ b/sma/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/speedwireinverterrequest.cpp b/sma/speedwireinverterrequest.cpp new file mode 100644 index 0000000..6797c71 --- /dev/null +++ b/sma/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/speedwireinverterrequest.h b/sma/speedwireinverterrequest.h new file mode 100644 index 0000000..be0b721 --- /dev/null +++ b/sma/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/speedwiremeter.cpp b/sma/speedwiremeter.cpp new file mode 100644 index 0000000..152ae87 --- /dev/null +++ b/sma/speedwiremeter.cpp @@ -0,0 +1,344 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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" + +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 + quint8 major, minor, build, revision; + stream >> major >> minor >> build >> revision; + // Revision types: + // S: Special version + // A: Alpha version + // B: Beta version + // R: Release version + // E: Experimental version + // N: No revision + m_softwareVersion = QString("%1.%2.%3-%4").arg(major).arg(minor).arg(build).arg(QChar(revision)); + + qCDebug(dcSma()) << "Meter: Software version" << m_softwareVersion; + } else if (measurementChannel == 0 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) { + // 00 00 00 00 + //qCDebug(dcSma()) << "Meter: End of data reached."; + } + } + + // Save the current timestamp for reachable evaluation + m_lastSeenTimestamp = QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000; + evaluateReachable(); + + emit valuesUpdated(); +} diff --git a/sma/speedwiremeter.h b/sma/speedwiremeter.h new file mode 100644 index 0000000..0599cf3 --- /dev/null +++ b/sma/speedwiremeter.h @@ -0,0 +1,126 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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.cpp b/sma/sunnywebbox.cpp new file mode 100644 index 0000000..d148ea2 --- /dev/null +++ b/sma/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.h b/sma/sunnywebbox.h new file mode 100644 index 0000000..9981aeb --- /dev/null +++ b/sma/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/sunnywebboxdiscovery.cpp b/sma/sunnywebboxdiscovery.cpp new file mode 100644 index 0000000..f9a1f76 --- /dev/null +++ b/sma/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/sunnywebboxdiscovery.h b/sma/sunnywebboxdiscovery.h new file mode 100644 index 0000000..066e4cd --- /dev/null +++ b/sma/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 + + + + From 5a1b6b773ef3005f314b8fe2aaf93ccba8f58eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 18 Nov 2022 01:34:41 +0100 Subject: [PATCH 2/3] Restructure sma plugin and add modbus inverter support --- nymea-plugins-modbus.pro | 1 + sma/README.md | 3 +- sma/integrationpluginsma.cpp | 320 ++++++++++++++++-- sma/integrationpluginsma.h | 21 +- sma/integrationpluginsma.json | 205 ++++++++--- sma/modbus/smamodbusdiscovery.cpp | 176 ++++++++++ sma/modbus/smamodbusdiscovery.h | 82 +++++ sma/sma-inverter-registers.json | 269 +++++++++++++++ sma/sma.h | 239 +++++++++++++ sma/sma.pro | 42 ++- sma/{ => speedwire}/speedwire.h | 118 ------- sma/{ => speedwire}/speedwirediscovery.cpp | 1 - sma/{ => speedwire}/speedwirediscovery.h | 0 sma/{ => speedwire}/speedwireinterface.cpp | 0 sma/{ => speedwire}/speedwireinterface.h | 0 sma/{ => speedwire}/speedwireinverter.cpp | 0 sma/{ => speedwire}/speedwireinverter.h | 5 +- .../speedwireinverterreply.cpp | 0 sma/{ => speedwire}/speedwireinverterreply.h | 0 .../speedwireinverterrequest.cpp | 0 .../speedwireinverterrequest.h | 0 sma/{ => speedwire}/speedwiremeter.cpp | 15 +- sma/{ => speedwire}/speedwiremeter.h | 1 - sma/{ => sunnywebbox}/sunnywebbox.cpp | 0 sma/{ => sunnywebbox}/sunnywebbox.h | 0 .../sunnywebboxdiscovery.cpp | 0 sma/{ => sunnywebbox}/sunnywebboxdiscovery.h | 0 27 files changed, 1276 insertions(+), 222 deletions(-) create mode 100644 sma/modbus/smamodbusdiscovery.cpp create mode 100644 sma/modbus/smamodbusdiscovery.h create mode 100644 sma/sma-inverter-registers.json create mode 100644 sma/sma.h rename sma/{ => speedwire}/speedwire.h (61%) rename sma/{ => speedwire}/speedwirediscovery.cpp (99%) rename sma/{ => speedwire}/speedwirediscovery.h (100%) rename sma/{ => speedwire}/speedwireinterface.cpp (100%) rename sma/{ => speedwire}/speedwireinterface.h (100%) rename sma/{ => speedwire}/speedwireinverter.cpp (100%) rename sma/{ => speedwire}/speedwireinverter.h (98%) rename sma/{ => speedwire}/speedwireinverterreply.cpp (100%) rename sma/{ => speedwire}/speedwireinverterreply.h (100%) rename sma/{ => speedwire}/speedwireinverterrequest.cpp (100%) rename sma/{ => speedwire}/speedwireinverterrequest.h (100%) rename sma/{ => speedwire}/speedwiremeter.cpp (96%) rename sma/{ => speedwire}/speedwiremeter.h (99%) rename sma/{ => sunnywebbox}/sunnywebbox.cpp (100%) rename sma/{ => sunnywebbox}/sunnywebbox.h (100%) rename sma/{ => sunnywebbox}/sunnywebboxdiscovery.cpp (100%) rename sma/{ => sunnywebbox}/sunnywebboxdiscovery.h (100%) 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 index bbb61b6..ad56627 100644 --- a/sma/README.md +++ b/sma/README.md @@ -7,13 +7,14 @@ nymea plug-in for SMA solar equipment. * 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 clocked for UDP packages in the network. +* 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 index 843008a..44bc212 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -30,8 +30,11 @@ #include "integrationpluginsma.h" #include "plugininfo.h" -#include "speedwirediscovery.h" -#include "sunnywebboxdiscovery.h" + +#include "sma.h" +#include "speedwire/speedwirediscovery.h" +#include "sunnywebbox/sunnywebboxdiscovery.h" +#include "modbus/smamodbusdiscovery.h" #include @@ -150,7 +153,7 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) if (result.serialNumber == 0) continue; - ThingDescriptor descriptor(speedwireInverterThingClassId, "SMA Inverter", "Serial: " + QString::number(result.serialNumber) + " - " + result.address.toString()); + 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) { @@ -174,6 +177,42 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) }); 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(); } } @@ -186,27 +225,29 @@ void IntegrationPluginSma::confirmPairing(ThingPairingInfo *info, const QString { Q_UNUSED(username) - // On speedwire the password length has a maximum of 12 characters - if (secret.count() > 12) { - info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The password can not be longer than 12 characters.")); - return; + 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); } - - // 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) @@ -367,6 +408,59 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) 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()); } @@ -380,15 +474,26 @@ void IntegrationPluginSma::postSetupThing(Thing *thing) if (!sunnyWebBox) return; - setupRefreshTimer(); sunnyWebBox->getPlantOverview(); - thing->setStateValue(sunnyWebBoxConnectedStateTypeId, true); + thing->setStateValue("connected", true); + setupRefreshTimer(); + } else if (thing->thingClassId() == speedwireInverterThingClassId) { SpeedwireInverter *inverter = m_speedwireInverters.value(thing); if (inverter) { - thing->setStateValue(speedwireInverterConnectedStateTypeId, inverter->reachable()); + thing->setStateValue("connected", inverter->reachable()); } else { - thing->setStateValue(speedwireInverterConnectedStateTypeId, false); + 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(); @@ -409,6 +514,14 @@ void IntegrationPluginSma::thingRemoved(Thing *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); @@ -450,7 +563,7 @@ void IntegrationPluginSma::setupRefreshTimer() if (m_refreshTimer) return; - m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(5); connect(m_refreshTimer, &PluginTimer::timeout, this, [=](){ foreach (Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); @@ -461,7 +574,158 @@ void IntegrationPluginSma::setupRefreshTimer() // 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 index fc509d0..50609bf 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -33,10 +33,15 @@ #include #include +#include -#include "sunnywebbox.h" -#include "speedwiremeter.h" -#include "speedwireinverter.h" +#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 @@ -62,12 +67,22 @@ private slots: 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 index 80cc5aa..76a31b5 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -38,15 +38,14 @@ "id": "c05e6a1a-252c-4f2b-8b31-09cf113d01c1", "name": "connected", "displayName": "Connected", - "displayNameEvent": "Connected changed", "type": "bool", - "defaultValue": false + "defaultValue": false, + "cached": false }, { "id": "ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2", "name": "currentPower", "displayName": "Current power", - "displayNameEvent": "Current power changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -56,7 +55,6 @@ "id": "16f34c5c-8dbb-4dcc-9faa-4b782d57226c", "name": "dayEnergyProduced", "displayName": "Day energy produced", - "displayNameEvent": "Day energy produced changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0 @@ -65,7 +63,6 @@ "id": "0bb4e227-7e38-49ca-9b32-ce4621c9305b", "name": "totalEnergyProduced", "displayName": "Total energy produced", - "displayNameEvent": "Total energy produced changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0 @@ -74,7 +71,6 @@ "id": "1974550b-6059-4b0e-83f4-70177e20dac3", "name": "mode", "displayName": "Mode", - "displayNameEvent": "Mode changed", "type": "QString", "defaultValue": "MPP" }, @@ -82,7 +78,6 @@ "id": "4e64f9ca-7e5a-4897-8035-6f2ae88fde89", "name": "error", "displayName": "Error", - "displayNameEvent": "Error changed", "type": "QString", "defaultValue": "None" } @@ -136,7 +131,6 @@ "id": "35733d27-4fe0-439a-be71-7c1597481659", "name": "connected", "displayName": "Connected", - "displayNameEvent": "Connected changed", "type": "bool", "defaultValue": false }, @@ -144,7 +138,6 @@ "id": "44ee2491-8376-41cd-a21d-185c736152ec", "name": "voltagePhaseA", "displayName": "Voltage phase A", - "displayNameEvent": "Voltage phase A changed", "type": "double", "unit": "Volt", "defaultValue": 0, @@ -154,7 +147,6 @@ "id": "56ae3555-f874-4c2d-8833-17573dce477a", "name": "voltagePhaseB", "displayName": "Voltage phase B", - "displayNameEvent": "Voltage phase B changed", "type": "double", "unit": "Volt", "defaultValue": 0, @@ -164,7 +156,6 @@ "id": "51cbb29b-29f0-480a-9d7d-b8f4e6a205ae", "name": "voltagePhaseC", "displayName": "Voltage phase C", - "displayNameEvent": "Voltage phase C changed", "type": "double", "unit": "Volt", "defaultValue": 0, @@ -174,7 +165,6 @@ "id": "45bbdbef-1832-4870-bff5-299e580fb4da", "name": "currentPhaseA", "displayName": "Current phase A", - "displayNameEvent": "Current phase A changed", "type": "double", "unit": "Ampere", "defaultValue": 0, @@ -184,7 +174,6 @@ "id": "b3a4fdd2-b6b8-4c58-9da3-2084ad414022", "name": "currentPhaseB", "displayName": "Current phase B", - "displayNameEvent": "Current phase B changed", "type": "double", "unit": "Ampere", "defaultValue": 0, @@ -194,7 +183,6 @@ "id": "b3655188-3854-4336-ae3c-61d3bda6fc4d", "name": "currentPhaseC", "displayName": "Current phase C", - "displayNameEvent": "Current phase C changed", "type": "double", "unit": "Ampere", "defaultValue": 0, @@ -204,7 +192,6 @@ "id": "d4ac7f37-e30a-44e4-93cb-ad16df18b8f1", "name": "currentPower", "displayName": "Current power", - "displayNameEvent": "Current power changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -214,7 +201,6 @@ "id": "c5d09c63-7461-4fb8-a6fe-bc7aa919be30", "name": "currentPowerPhaseA", "displayName": "Current power phase A", - "displayNameEvent": "Current power phase A changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -224,7 +210,6 @@ "id": "c52d4422-b521-4804-a7a7-c4398e91e760", "name": "currentPowerPhaseB", "displayName": "Current power phase B", - "displayNameEvent": "Current power phase B changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -234,7 +219,6 @@ "id": "555e892c-3ca7-4100-9832-6ac13b87eb04", "name": "currentPowerPhaseC", "displayName": "Current power phase C", - "displayNameEvent": "Current power phase C changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -244,7 +228,6 @@ "id": "4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d", "name": "totalEnergyConsumed", "displayName": "Total energy consumed", - "displayNameEvent": "Total energy consumed changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -253,7 +236,6 @@ "id": "76ca68d8-6781-4d2a-8663-440aec40b4de", "name": "totalEnergyProduced", "displayName": "Total energy produced", - "displayNameEvent": "Total energy produced changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -262,7 +244,6 @@ "id": "b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33", "name": "energyConsumedPhaseA", "displayName": "Energy consumed phase A", - "displayNameEvent": "Energy consumed phase A changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -271,7 +252,6 @@ "id": "c4e5f569-ac5d-4761-a898-888880bfd59f", "name": "energyConsumedPhaseB", "displayName": "Energy consumed phase B", - "displayNameEvent": "Energy consumed phase B changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -280,7 +260,6 @@ "id": "aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3", "name": "energyConsumedPhaseC", "displayName": "Energy consumed phase C", - "displayNameEvent": "Energy consumed phase C changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -289,7 +268,6 @@ "id": "754c3b67-768a-47f7-99d8-f66c198f0835", "name": "energyProducedPhaseA", "displayName": "Energy produced phase A", - "displayNameEvent": "Energy produced phase A changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -298,7 +276,6 @@ "id": "7eb08c45-24cf-40ce-be28-f3564f087672", "name": "energyProducedPhaseB", "displayName": "Energy produced phase B", - "displayNameEvent": "Energy produced phase B changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -307,7 +284,6 @@ "id": "1eb2bf01-5ec6-42e5-b348-ac1e95199d14", "name": "energyProducedPhaseC", "displayName": "Energy produced phase C", - "displayNameEvent": "Energy produced phase C changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -316,7 +292,6 @@ "id": "a685393c-8b7e-42c5-bb41-f9907c074626", "name": "firmwareVersion", "displayName": "Firmware version", - "displayNameEvent": "Firmware version changed", "type": "QString", "defaultValue": "" } @@ -371,7 +346,6 @@ "id": "aaff72c3-c70a-4a2f-bed1-89f38cebe442", "name": "connected", "displayName": "Connected", - "displayNameEvent": "Connected changed", "type": "bool", "defaultValue": false }, @@ -379,7 +353,6 @@ "id": "6ef4eb16-a3d6-4bc9-972d-5e7cb81173a5", "name": "voltagePhaseA", "displayName": "Voltage phase A", - "displayNameEvent": "Voltage phase A changed", "type": "double", "unit": "Volt", "defaultValue": 0, @@ -389,7 +362,6 @@ "id": "d9a5768b-1bf5-4933-810d-84dd7a688f71", "name": "voltagePhaseB", "displayName": "Voltage phase B", - "displayNameEvent": "Voltage phase B changed", "type": "double", "unit": "Volt", "defaultValue": 0, @@ -399,7 +371,6 @@ "id": "fc168dc6-eecf-40b4-b214-3e28da0dbb12", "name": "voltagePhaseC", "displayName": "Voltage phase C", - "displayNameEvent": "Voltage phase C changed", "type": "double", "unit": "Volt", "defaultValue": 0, @@ -409,7 +380,6 @@ "id": "2a6c59ca-853a-47d6-96fb-0c85edf32f52", "name": "currentPhaseA", "displayName": "Current phase A", - "displayNameEvent": "Current phase A changed", "type": "double", "unit": "Ampere", "defaultValue": 0, @@ -419,7 +389,6 @@ "id": "4db96fec-737c-4c4b-bf07-5ef2fd62508a", "name": "currentPhaseB", "displayName": "Current phase B", - "displayNameEvent": "Current phase B changed", "type": "double", "unit": "Ampere", "defaultValue": 0, @@ -429,7 +398,6 @@ "id": "0f23fb0e-a440-4ac2-9aff-896bc65feb2c", "name": "currentPhaseC", "displayName": "Current phase C", - "displayNameEvent": "Current phase C changed", "type": "double", "unit": "Ampere", "defaultValue": 0, @@ -439,7 +407,6 @@ "id": "d7ceb482-5df8-4c0c-82bd-62ce7ba22c43", "name": "currentPower", "displayName": "Current power", - "displayNameEvent": "Current power changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -449,7 +416,6 @@ "id": "b366f680-6134-488b-8362-b1b824a8daca", "name": "currentPowerMpp1", "displayName": "DC power MPP1", - "displayNameEvent": "DC power MPP1 changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -459,7 +425,6 @@ "id": "87d9b654-5558-47a3-9db9-ffd7c23b4774", "name": "currentPowerMpp2", "displayName": "DC power MPP2", - "displayNameEvent": "DC power MPP2 changed", "type": "double", "unit": "Watt", "defaultValue": 0, @@ -469,7 +434,6 @@ "id": "51cadd66-2cf1-485a-a2a9-191d11abfbd1", "name": "totalEnergyProduced", "displayName": "Total energy produced", - "displayNameEvent": "Total energy produced changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -478,7 +442,6 @@ "id": "e8bc8f81-e5c5-4900-b429-93fcaa262fcb", "name": "energyProducedToday", "displayName": "Energy produced today", - "displayNameEvent": "Energy produced today changed", "type": "double", "unit": "KiloWattHour", "defaultValue": 0.00 @@ -487,7 +450,6 @@ "id": "fdccf5de-7413-4480-9ca0-1151665dede8", "name": "frequency", "displayName": "Frequency", - "displayNameEvent": "Frequency changed", "type": "double", "unit": "Hertz", "defaultValue": 0.00, @@ -497,7 +459,168 @@ "id": "6d76cc7b-9e00-4561-be7b-4e2a6b8f7b66", "name": "firmwareVersion", "displayName": "Firmware version", - "displayNameEvent": "Firmware version changed", + "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/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.pro b/sma/sma.pro index 7e031fc..ce064d7 100644 --- a/sma/sma.pro +++ b/sma/sma.pro @@ -2,25 +2,33 @@ include(../plugins.pri) QT += network +# Generate modbus connection +MODBUS_CONNECTIONS += sma-inverter-registers.json +MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + SOURCES += \ integrationpluginsma.cpp \ - speedwirediscovery.cpp \ - speedwireinterface.cpp \ - speedwireinverter.cpp \ - speedwireinverterreply.cpp \ - speedwireinverterrequest.cpp \ - speedwiremeter.cpp \ - sunnywebbox.cpp \ - sunnywebboxdiscovery.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 \ - speedwire.h \ - speedwirediscovery.h \ - speedwireinterface.h \ - speedwireinverter.h \ - speedwireinverterreply.h \ - speedwireinverterrequest.h \ - speedwiremeter.h \ - sunnywebbox.h \ - sunnywebboxdiscovery.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.h b/sma/speedwire/speedwire.h similarity index 61% rename from sma/speedwire.h rename to sma/speedwire/speedwire.h index df92f5c..b1f1734 100644 --- a/sma/speedwire.h +++ b/sma/speedwire/speedwire.h @@ -64,19 +64,6 @@ public: }; Q_ENUM(ProtocolId) - enum DeviceClass { - DeviceClassUnknown = 0x0000, - DeviceClassAllDevices = 0x1f40, - DeviceClassSolarInverter = 0x1f41, - DeviceClassWindTurbine = 0x1f42, - DeviceClassBatteryInverter = 0x1f47, - DeviceClassConsumer = 0x1f61, - DeviceClassSensorSystem = 0x1f80, - DeviceClassElectricityMeter = 0x1f81, - DeviceClassCommunicationProduct = 0x1fc0 - }; - Q_ENUM(DeviceClass) - class Header { public: @@ -121,111 +108,6 @@ public: static quint16 tagVersion() { return 0; } static quint16 smaNet2Version() { return 0x0010; } - static QString getModelName(quint16 modelIdentifier) { - switch (modelIdentifier) { - case 9015: return "SB 700"; - case 9016: return "SB 700U"; - case 9017: return "SB 1100"; - case 9018: return "SB 1100U"; - case 9019: return "SB 1100LV"; - case 9020: return "SB 1700"; - case 9021: return "SB 1900TLJ"; - case 9022: return "SB 2100TL"; - case 9023: return "SB 2500"; - case 9024: return "SB 2800"; - case 9025: return "SB 2800i"; - case 9026: return "SB 3000"; - case 9027: return "SB 3000US"; - case 9028: return "SB 3300"; - case 9029: return "SB 3300U"; - case 9030: return "SB 3300TL"; - case 9031: return "SB 3300TL HC"; - case 9032: return "SB 3800"; - case 9033: return "SB 3800U"; - case 9034: return "SB 4000US"; - case 9035: return "SB 4200TL"; - case 9036: return "SB 4200TL HC"; - case 9037: return "SB 5000TL"; - case 9038: return "SB 5000TLW"; - case 9039: return "SB 5000TL HC"; - case 9066: return "SB 1200"; - case 9067: return "STP 10000TL-10"; - case 9068: return "STP 12000TL-10"; - case 9069: return "STP 15000TL-10"; - case 9070: return "STP 17000TL-10"; - case 9084: return "WB 3600TL-20"; - case 9085: return "WB 5000TL-20"; - case 9086: return "SB 3800US-10"; - case 9098: return "STP 5000TL-20"; - case 9099: return "STP 6000TL-20"; - case 9100: return "STP 7000TL-20"; - case 9101: return "STP 8000TL-10"; - case 9102: return "STPcase 9000TL-20"; - case 9103: return "STP 8000TL-20"; - case 9104: return "SB 3000TL-JP-21"; - case 9105: return "SB 3500TL-JP-21"; - case 9106: return "SB 4000TL-JP-21"; - case 9107: return "SB 4500TL-JP-21"; - case 9108: return "SCSMC"; - case 9109: return "SB 1600TL-10"; - case 9131: return "STP 20000TL-10"; - case 9139: return "STP 20000TLHE-10"; - case 9140: return "STP 15000TLHE-10"; - case 9157: return "Sunny Island 2012"; - case 9158: return "Sunny Island 2224"; - case 9159: return "Sunny Island 5048"; - case 9160: return "SB 3600TL-20"; - case 9168: return "SC630HE-11"; - case 9169: return "SC500HE-11"; - case 9170: return "SC400HE-11"; - case 9171: return "WB 3000TL-21"; - case 9172: return "WB 3600TL-21"; - case 9173: return "WB 4000TL-21"; - case 9174: return "WB 5000TL-21"; - case 9175: return "SC 250"; - case 9176: return "SMA Meteo Station"; - case 9177: return "SB 240-10"; - case 9179: return "Multigate-10"; - case 9180: return "Multigate-US-10"; - case 9181: return "STP 20000TLEE-10"; - case 9182: return "STP 15000TLEE-10"; - case 9183: return "SB 2000TLST-21"; - case 9184: return "SB 2500TLST-21"; - case 9185: return "SB 3000TLST-21"; - case 9186: return "WB 2000TLST-21"; - case 9187: return "WB 2500TLST-21"; - case 9188: return "WB 3000TLST-21"; - case 9189: return "WTP 5000TL-20"; - case 9190: return "WTP 6000TL-20"; - case 9191: return "WTP 7000TL-20"; - case 9192: return "WTP 8000TL-20"; - case 9193: return "WTPcase 9000TL-20"; - case 9254: return "Sunny Island 3324"; - case 9255: return "Sunny Island 4.0M"; - case 9256: return "Sunny Island 4248"; - case 9257: return "Sunny Island 4248U"; - case 9258: return "Sunny Island 4500"; - case 9259: return "Sunny Island 4548U"; - case 9260: return "Sunny Island 5.4M"; - case 9261: return "Sunny Island 5048U"; - case 9262: return "Sunny Island 6048U"; - case 9278: return "Sunny Island 3.0M"; - case 9279: return "Sunny Island 4.4M"; - case 9281: return "STP 10000TL-20"; - case 9282: return "STP 11000TL-20"; - case 9283: return "STP 12000TL-20"; - case 9284: return "STP 20000TL-30"; - case 9285: return "STP 25000TL-30"; - case 9301: return "SB1.5-1VL-40"; - case 9302: return "SB2.5-1VL-40"; - case 9303: return "SB2.0-1VL-40"; - case 9304: return "SB5.0-1SP-US-40"; - case 9305: return "SB6.0-1SP-US-40"; - case 9306: return "SB8.0-1SP-US-40"; - case 9307: return "Energy Meter"; - default: return "Unknown"; - } - }; // Multicast device discovery request packet, according to SMA documentation. // However, this does not seem to be supported anymore with version 3.x devices diff --git a/sma/speedwirediscovery.cpp b/sma/speedwire/speedwirediscovery.cpp similarity index 99% rename from sma/speedwirediscovery.cpp rename to sma/speedwire/speedwirediscovery.cpp index 42a1dd0..83547f1 100644 --- a/sma/speedwirediscovery.cpp +++ b/sma/speedwire/speedwirediscovery.cpp @@ -32,7 +32,6 @@ #include "extern-plugininfo.h" #include -#include SpeedwireDiscovery::SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : QObject(parent), diff --git a/sma/speedwirediscovery.h b/sma/speedwire/speedwirediscovery.h similarity index 100% rename from sma/speedwirediscovery.h rename to sma/speedwire/speedwirediscovery.h diff --git a/sma/speedwireinterface.cpp b/sma/speedwire/speedwireinterface.cpp similarity index 100% rename from sma/speedwireinterface.cpp rename to sma/speedwire/speedwireinterface.cpp diff --git a/sma/speedwireinterface.h b/sma/speedwire/speedwireinterface.h similarity index 100% rename from sma/speedwireinterface.h rename to sma/speedwire/speedwireinterface.h diff --git a/sma/speedwireinverter.cpp b/sma/speedwire/speedwireinverter.cpp similarity index 100% rename from sma/speedwireinverter.cpp rename to sma/speedwire/speedwireinverter.cpp diff --git a/sma/speedwireinverter.h b/sma/speedwire/speedwireinverter.h similarity index 98% rename from sma/speedwireinverter.h rename to sma/speedwire/speedwireinverter.h index ce2d34e..dd814d6 100644 --- a/sma/speedwireinverter.h +++ b/sma/speedwire/speedwireinverter.h @@ -34,6 +34,7 @@ #include #include +#include "sma.h" #include "speedwire.h" #include "speedwireinterface.h" #include "speedwireinverterreply.h" @@ -62,7 +63,7 @@ public: bool reachable() const; - Speedwire::DeviceClass deviceClass() const; + Sma::DeviceClass deviceClass() const; QString modelName() const; double totalAcPower() const; @@ -131,7 +132,7 @@ private: QQueue m_replyQueue; // Properties - Speedwire::DeviceClass m_deviceClass = Speedwire::DeviceClassUnknown; + Sma::DeviceClass m_deviceClass = Sma::DeviceClassUnknown; QString m_modelName; QString m_softwareVersion; diff --git a/sma/speedwireinverterreply.cpp b/sma/speedwire/speedwireinverterreply.cpp similarity index 100% rename from sma/speedwireinverterreply.cpp rename to sma/speedwire/speedwireinverterreply.cpp diff --git a/sma/speedwireinverterreply.h b/sma/speedwire/speedwireinverterreply.h similarity index 100% rename from sma/speedwireinverterreply.h rename to sma/speedwire/speedwireinverterreply.h diff --git a/sma/speedwireinverterrequest.cpp b/sma/speedwire/speedwireinverterrequest.cpp similarity index 100% rename from sma/speedwireinverterrequest.cpp rename to sma/speedwire/speedwireinverterrequest.cpp diff --git a/sma/speedwireinverterrequest.h b/sma/speedwire/speedwireinverterrequest.h similarity index 100% rename from sma/speedwireinverterrequest.h rename to sma/speedwire/speedwireinverterrequest.h diff --git a/sma/speedwiremeter.cpp b/sma/speedwire/speedwiremeter.cpp similarity index 96% rename from sma/speedwiremeter.cpp rename to sma/speedwire/speedwiremeter.cpp index 152ae87..5178b56 100644 --- a/sma/speedwiremeter.cpp +++ b/sma/speedwire/speedwiremeter.cpp @@ -31,6 +31,8 @@ #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), @@ -318,16 +320,9 @@ void SpeedwireMeter::processData(const QByteArray &data) } if (measurementChannel == 144 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) { // Software version // 90000000 01 02 08 52 - quint8 major, minor, build, revision; - stream >> major >> minor >> build >> revision; - // Revision types: - // S: Special version - // A: Alpha version - // B: Beta version - // R: Release version - // E: Experimental version - // N: No revision - m_softwareVersion = QString("%1.%2.%3-%4").arg(major).arg(minor).arg(build).arg(QChar(revision)); + 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) { diff --git a/sma/speedwiremeter.h b/sma/speedwire/speedwiremeter.h similarity index 99% rename from sma/speedwiremeter.h rename to sma/speedwire/speedwiremeter.h index 0599cf3..63989c3 100644 --- a/sma/speedwiremeter.h +++ b/sma/speedwire/speedwiremeter.h @@ -74,7 +74,6 @@ public: QString softwareVersion() const; - signals: void reachableChanged(bool reachable); void valuesUpdated(); diff --git a/sma/sunnywebbox.cpp b/sma/sunnywebbox/sunnywebbox.cpp similarity index 100% rename from sma/sunnywebbox.cpp rename to sma/sunnywebbox/sunnywebbox.cpp diff --git a/sma/sunnywebbox.h b/sma/sunnywebbox/sunnywebbox.h similarity index 100% rename from sma/sunnywebbox.h rename to sma/sunnywebbox/sunnywebbox.h diff --git a/sma/sunnywebboxdiscovery.cpp b/sma/sunnywebbox/sunnywebboxdiscovery.cpp similarity index 100% rename from sma/sunnywebboxdiscovery.cpp rename to sma/sunnywebbox/sunnywebboxdiscovery.cpp diff --git a/sma/sunnywebboxdiscovery.h b/sma/sunnywebbox/sunnywebboxdiscovery.h similarity index 100% rename from sma/sunnywebboxdiscovery.h rename to sma/sunnywebbox/sunnywebboxdiscovery.h From a60c361df7566121a502e89c5ed0cef6f9e15c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 29 Nov 2022 09:40:11 +0100 Subject: [PATCH 3/3] Update name of WebBox --- sma/integrationpluginsma.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index 76a31b5..e79f74f 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -11,7 +11,7 @@ { "id": "49304127-ce9b-45dd-8511-05030a4ac003", "name": "sunnyWebBox", - "displayName": "Sunny WebBox", + "displayName": "SMA Sunny WebBox", "createMethods": ["discovery", "user"], "interfaces": ["solarinverter"], "paramTypes": [