diff --git a/debian/control b/debian/control index ccb78d60..b6e5585c 100644 --- a/debian/control +++ b/debian/control @@ -1035,6 +1035,22 @@ Description: nymea.io plugin to send and receive strings over a serial port . This package will install the nymea.io plugin for serial ports + +Package: nymea-plugin-sma +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for SMA PV-Inverter + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for SMA PV-Inverter + + Package: nymea-plugin-systemmonitor Architecture: any Depends: ${shlibs:Depends}, @@ -1231,6 +1247,7 @@ Depends: nymea-plugin-anel, nymea-plugin-shelly, nymea-plugin-senic, nymea-plugin-somfytahoma, + nymea-plugin-sma, nymea-plugin-sonos, nymea-plugin-solarlog, nymea-plugin-tado, diff --git a/debian/nymea-plugin-sma.install.in b/debian/nymea-plugin-sma.install.in new file mode 100644 index 00000000..106718a1 --- /dev/null +++ b/debian/nymea-plugin-sma.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsma.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index eb04e664..cead44a7 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -53,6 +53,7 @@ PLUGIN_DIRS = \ senic \ serialportcommander \ simulation \ + sma \ somfytahoma \ sonos \ sunposition \ diff --git a/sma/README.md b/sma/README.md new file mode 100644 index 00000000..63ffbd03 --- /dev/null +++ b/sma/README.md @@ -0,0 +1,14 @@ +# SMA + +nymea plug-in for SMA solar equipment. + +## Supported Things + +* Sunny WebBox + +## Requirements + +* The package "nymea-plugin-sma" must be installed. + +## More +https://www.sma.de/en/ diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp new file mode 100644 index 00000000..6dfa1835 --- /dev/null +++ b/sma/integrationpluginsma.cpp @@ -0,0 +1,194 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "integrationpluginsma.h" +#include "plugininfo.h" + +#include "network/networkdevicediscovery.h" + +IntegrationPluginSma::IntegrationPluginSma() +{ + +} + +void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) +{ + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcSma()) << "Failed to discover network devices. The network device discovery is not available."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network.")); + return; + } + + qCDebug(dcSma()) << "Starting network discovery..."; + NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + ThingDescriptors descriptors; + qCDebug(dcSma()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + // Filter for sma hosts + if (!networkDeviceInfo.hostName().toLower().contains("sma")) + continue; + + QString title = networkDeviceInfo.hostName() + " (" + networkDeviceInfo.address().toString() + ")"; + QString description; + if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { + description = networkDeviceInfo.macAddress(); + } else { + description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(sunnyWebBoxThingClassId, title, description); + + // Check for reconfiguration + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(sunnyWebBoxThingMacAddressParamTypeId).toString() == networkDeviceInfo.macAddress()) { + descriptor.setThingId(existingThing->id()); + break; + } + } + + ParamList params; + params << Param(sunnyWebBoxThingHostParamTypeId, networkDeviceInfo.address().toString()); + params << Param(sunnyWebBoxThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + descriptor.setParams(params); + descriptors.append(descriptor); + } + info->addThingDescriptors(descriptors); + 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); + + if (!m_refreshTimer) { + qCDebug(dcSma()) << "Starting refresh timer"; + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(1); + connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSma::onRefreshTimer); + } + }); + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginSma::postSetupThing(Thing *thing) +{ + qCDebug(dcSma()) << "Post setup thing" << thing->name(); + if (thing->thingClassId() == sunnyWebBoxThingClassId) { + SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); + if (!sunnyWebBox) + return; + sunnyWebBox->getPlantOverview(); + thing->setStateValue(sunnyWebBoxConnectedStateTypeId, true); + } +} + +void IntegrationPluginSma::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == sunnyWebBoxThingClassId) { + m_sunnyWebBoxes.take(thing)->deleteLater(); + } + + if (myThings().isEmpty()) { + qCDebug(dcSma()) << "Stopping timer"; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); + m_refreshTimer = nullptr; + } +} + +void IntegrationPluginSma::onRefreshTimer() +{ + Q_FOREACH(Thing *thing, myThings().filterByThingClassId(sunnyWebBoxThingClassId)) { + SunnyWebBox *sunnyWebBox = m_sunnyWebBoxes.value(thing); + sunnyWebBox->getPlantOverview(); + } +} + +void IntegrationPluginSma::onConnectedChanged(bool connected) +{ + Thing *thing = m_sunnyWebBoxes.key(static_cast(sender())); + 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); + } +} diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h new file mode 100644 index 00000000..3c20919f --- /dev/null +++ b/sma/integrationpluginsma.h @@ -0,0 +1,64 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 INTEGRATIONPLUGINSMA_H +#define INTEGRATIONPLUGINSMA_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" +#include "sunnywebbox.h" + +#include + +class IntegrationPluginSma: public IntegrationPlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsma.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginSma(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +private slots: + void onRefreshTimer(); + + void onConnectedChanged(bool connected); + void onPlantOverviewReceived(const QString &messageId, SunnyWebBox::Overview overview); + +private: + PluginTimer *m_refreshTimer = nullptr; + QHash m_sunnyWebBoxes; +}; + +#endif // INTEGRATIONPLUGINSMA_H diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json new file mode 100644 index 00000000..0a542fe4 --- /dev/null +++ b/sma/integrationpluginsma.json @@ -0,0 +1,93 @@ +{ + "id": "b8442bbf-9d3f-4aa2-9443-b3a31ae09bac", + "name": "sma", + "displayName": "SMA", + "vendors": [ + { + "id": "16d5a4a3-36d5-46c0-b7dd-df166ddf5981", + "name": "Sma", + "displayName": "SMA", + "thingClasses": [ + { + "id": "49304127-ce9b-45dd-8511-05030a4ac003", + "name": "sunnyWebBox", + "displayName": "Sunny WebBox", + "createMethods": ["user", "discovery"], + "interfaces": ["smartmeterproducer"], + "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": "hardware address", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + } + ], + "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 + }, + { + "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" + } + ] + } + ] + } + ] +} + diff --git a/sma/meta.json b/sma/meta.json new file mode 100644 index 00000000..b2bee247 --- /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": [ + + ] +} diff --git a/sma/sma.png b/sma/sma.png new file mode 100644 index 00000000..3273ac85 Binary files /dev/null and b/sma/sma.png differ diff --git a/sma/sma.pro b/sma/sma.pro new file mode 100644 index 00000000..5e124771 --- /dev/null +++ b/sma/sma.pro @@ -0,0 +1,11 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + integrationpluginsma.cpp \ + sunnywebbox.cpp + +HEADERS += \ + integrationpluginsma.h \ + sunnywebbox.h diff --git a/sma/sunnywebbox.cpp b/sma/sunnywebbox.cpp new file mode 100644 index 00000000..f2b2c56a --- /dev/null +++ b/sma/sunnywebbox.cpp @@ -0,0 +1,332 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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; + Q_FOREACH(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; + Q_FOREACH(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; + Q_FOREACH(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; +} + +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"; + Q_FOREACH(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(); + Q_FOREACH(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(); + Q_FOREACH(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") { + Q_FOREACH(QString deviceKey, result.keys()) { + QStringList processDataChannels = result.value(deviceKey).toStringList(); + if (!processDataChannels.isEmpty()) + emit processDataChannelsReceived(messageId, deviceKey, processDataChannels); + } + } else if (messageType == "GetProcessData") { + QList devices; + QVariantList devicesList = result.value("devices").toList(); + qCDebug(dcSma()) << "SunnyWebBox: GetProcessData response received"; + Q_FOREACH(QVariant value, devicesList) { + + QString key = value.toMap().value("key").toString(); + QVariantList channelsList = value.toMap().value("channels").toList(); + QHash channels; + Q_FOREACH(QVariant channel, channelsList) { + channels.insert(channel.toMap().value("meta").toString(), channel.toMap().value("value")); + } + emit processDataReceived(messageId, key, channels); + } + } else if (messageType == "GetParameterChannels") { + Q_FOREACH(QString deviceKey, result.keys()) { + QStringList parameterChannels = result.value(deviceKey).toStringList(); + if (!parameterChannels.isEmpty()) + emit parameterChannelsReceived(messageId, deviceKey, parameterChannels); + } + } else if (messageType == "GetParameter"|| messageType == "SetParameter") { + QList devices; + QVariantList devicesList = result.value("devices").toList(); + Q_FOREACH(QVariant value, devicesList) { + + QString key = value.toMap().value("key").toString(); + QVariantList channelsList = value.toMap().value("channels").toList(); + QList parameters; + Q_FOREACH(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) +{ + qCDebug(dcSma()) << "SunnyWebBox: Send message to" << address.toString() << "Procedure:" << procedure << "Params:" << params; + QString requestId = QUuid::createUuid().toString().remove('{').remove('-').left(14); + + 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="); + QNetworkReply *reply = m_networkManager->post(request, data); + 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 00000000..ba1d669a --- /dev/null +++ b/sma/sunnywebbox.h @@ -0,0 +1,115 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 + +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); + +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/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 00000000..6d411ddd --- /dev/null +++ b/sma/translations/b8442bbf-9d3f-4aa2-9443-b3a31ae09bac-en_US.ts @@ -0,0 +1,132 @@ + + + + + IntegrationPluginSma + + + Unable to discover devices in your network. + + + + + sma + + + + Connected + The name of the ParamType (ThingClass: sunnyWebBox, EventType: connected, ID: {c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) +---------- +The name of the StateType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox + + + + + Connected changed + The name of the EventType ({c05e6a1a-252c-4f2b-8b31-09cf113d01c1}) of ThingClass sunnyWebBox + + + + + + Current power + The name of the ParamType (ThingClass: sunnyWebBox, EventType: currentPower, ID: {ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) +---------- +The name of the StateType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox + + + + + Current power changed + The name of the EventType ({ff4ff872-2f0f-4ca4-9fe2-220eeaf16cc2}) of ThingClass sunnyWebBox + + + + + + Day energy produced + The name of the ParamType (ThingClass: sunnyWebBox, EventType: dayEnergyProduced, ID: {16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) +---------- +The name of the StateType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox + + + + + Day energy produced changed + The name of the EventType ({16f34c5c-8dbb-4dcc-9faa-4b782d57226c}) of ThingClass sunnyWebBox + + + + + + Error + The name of the ParamType (ThingClass: sunnyWebBox, EventType: error, ID: {4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) +---------- +The name of the StateType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox + + + + + Error changed + The name of the EventType ({4e64f9ca-7e5a-4897-8035-6f2ae88fde89}) of ThingClass sunnyWebBox + + + + + Host address + The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {864d4162-e3ce-48b8-b8ac-c1b971b52d42}) + + + + + + Mode + The name of the ParamType (ThingClass: sunnyWebBox, EventType: mode, ID: {1974550b-6059-4b0e-83f4-70177e20dac3}) +---------- +The name of the StateType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass sunnyWebBox + + + + + Mode changed + The name of the EventType ({1974550b-6059-4b0e-83f4-70177e20dac3}) of ThingClass sunnyWebBox + + + + + + SMA + The name of the vendor ({16d5a4a3-36d5-46c0-b7dd-df166ddf5981}) +---------- +The name of the plugin sma ({b8442bbf-9d3f-4aa2-9443-b3a31ae09bac}) + + + + + + Total energy produced + The name of the ParamType (ThingClass: sunnyWebBox, EventType: totalEnergyProduced, ID: {0bb4e227-7e38-49ca-9b32-ce4621c9305b}) +---------- +The name of the StateType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox + + + + + Total energy produced changed + The name of the EventType ({0bb4e227-7e38-49ca-9b32-ce4621c9305b}) of ThingClass sunnyWebBox + + + + + Sunny WebBox + The name of the ThingClass ({49304127-ce9b-45dd-8511-05030a4ac003}) + + + + + hardware address + The name of the ParamType (ThingClass: sunnyWebBox, Type: thing, ID: {03f32361-4e13-4597-a346-af8d16a986b3}) + + + +