diff --git a/bosswerk/README.md b/bosswerk/README.md new file mode 100644 index 00000000..70e52287 --- /dev/null +++ b/bosswerk/README.md @@ -0,0 +1,18 @@ +# Bosswerk + +This integration plugin allows to add Bosswerk Micro Inverters to nymea. + +## Supported devices + +* MI-300 +* MI-600 + +## Requirements + +The solar inverter needs to be connected to the same network as nymea. Once powered on, the +inverter will open a wireless hotspot named AP_XXXXXXXXXX where XXXXXXXXXX is the serial number +written on the casing. The default password for this WiFi is 12345678. After connecting, +open [http://10.10.100.254](http://10.10.100.254) with the browser and use the web interface +on the inverter to connect it to a WiFi with nymea in it. + +Once done so, set up the inverter in nymea as any other thing. diff --git a/bosswerk/bosswerk.png b/bosswerk/bosswerk.png new file mode 100644 index 00000000..e93b8e4e Binary files /dev/null and b/bosswerk/bosswerk.png differ diff --git a/bosswerk/bosswerk.pro b/bosswerk/bosswerk.pro new file mode 100644 index 00000000..e3a95cdb --- /dev/null +++ b/bosswerk/bosswerk.pro @@ -0,0 +1,9 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + integrationpluginbosswerk.cpp \ + +HEADERS += \ + integrationpluginbosswerk.h \ diff --git a/bosswerk/integrationpluginbosswerk.cpp b/bosswerk/integrationpluginbosswerk.cpp new file mode 100644 index 00000000..bbe9dbfc --- /dev/null +++ b/bosswerk/integrationpluginbosswerk.cpp @@ -0,0 +1,221 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "integrationpluginbosswerk.h" +#include "plugininfo.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +IntegrationPluginBosswerk::IntegrationPluginBosswerk() +{ + +} + +IntegrationPluginBosswerk::~IntegrationPluginBosswerk() +{ +} + +void IntegrationPluginBosswerk::discoverThings(ThingDiscoveryInfo *info) +{ + NetworkDeviceDiscoveryReply *reply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(reply, &NetworkDeviceDiscoveryReply::finished, info, [info, reply, this](){ + foreach (const NetworkDeviceInfo &deviceInfo, reply->networkDeviceInfos()) { +// qCDebug(dcBosswerk) << "Discovery result" << deviceInfo; + QString cover_mid = deviceInfo.hostName().split(".").first(); +// qCDebug(dcBosswerk()) << "Cover mid" << cover_mid; + + if (QRegExp("[0-9]{10}").exactMatch(cover_mid)) { + qCDebug(dcBosswerk()) << "Potential result:" << deviceInfo; + ThingDescriptor descriptor(mix00ThingClassId, "MI-300/600", deviceInfo.hostName()); + descriptor.setParams({Param(mix00ThingMacAddressParamTypeId, deviceInfo.macAddress())}); + Thing *existingThing = myThings().findByParams({Param(mix00ThingMacAddressParamTypeId, deviceInfo.macAddress())}); + if (existingThing) { + descriptor.setThingId(existingThing->id()); + } + info->addThingDescriptor(descriptor); + } + } + qCInfo(dcBosswerk) << "Discovery finished." << info->thingDescriptors().count() << "results."; + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginBosswerk::startPairing(ThingPairingInfo *info) +{ + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter your login credentials.")); +} + +void IntegrationPluginBosswerk::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + MacAddress mac(info->params().paramValue(mix00ThingMacAddressParamTypeId).toString()); + + QHash cache = hardwareManager()->networkDeviceDiscovery()->cache(); + + if (!cache.contains(mac)) { + qCWarning(dcBosswerk()) << "MAC" << mac << "not found in network device cache."; + info->finish(Thing::ThingErrorItemNotFound, QT_TR_NOOP("An error happened in the network communication.")); + return; + } + + NetworkDeviceInfo networkDeviceInfo = cache.value(mac); + + QUrl url("http://" + username + ":" + secret + "@" + networkDeviceInfo.address().toString() + "/status.html"); + QNetworkRequest request(url); + + qCDebug(dcBosswerk) << "Requesting" << request.url(); + + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcBosswerk()) << "Error logging in at inverter" << reply->error(); + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to log in at the inverter.")); + return; + } + + QByteArray payload = reply->readAll(); + qCDebug(dcBosswerk()) << "Reply data:" << payload; + + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("username", username); + pluginStorage()->setValue("password", secret); + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginBosswerk::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + NetworkDeviceMonitor *monitor = m_deviceMonitors.take(thing); + if (monitor) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); + } + PluginTimer *timer = m_timers.take(thing); + if (timer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(timer); + } + + monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(MacAddress(thing->paramValue(mix00ThingMacAddressParamTypeId).toString())); + m_deviceMonitors.insert(thing, monitor); + + timer = hardwareManager()->pluginTimerManager()->registerTimer(5); + m_timers.insert(thing, timer); + + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [timer, thing](bool reachable) { + thing->setStateValue(mix00ConnectedStateTypeId, reachable); + if (reachable) { + timer->start(); + } else { + timer->stop(); + thing->setStateValue(mix00CurrentPowerStateTypeId, 0); + } + }); + + connect(timer, &PluginTimer::timeout, thing, [this, thing](){ + pollDevice(thing); + }); + + pollDevice(thing); + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginBosswerk::thingRemoved(Thing *thing) +{ + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_deviceMonitors.take(thing)); + hardwareManager()->pluginTimerManager()->unregisterTimer(m_timers.take(thing)); +} + +void IntegrationPluginBosswerk::pollDevice(Thing *thing) +{ + NetworkDeviceMonitor *monitor = m_deviceMonitors.value(thing); + + pluginStorage()->beginGroup(thing->id().toString()); + QString username = pluginStorage()->value("username").toString(); + QString password = pluginStorage()->value("password").toString(); + pluginStorage()->endGroup(); + + QUrl url("http://" + username + ":" + password + "@" + monitor->networkDeviceInfo().address().toString() + "/status.html"); + qCDebug(dcBosswerk()) << "Requesting" << url.toString(); + QNetworkRequest request(url); + + QNetworkReply *statusReply = hardwareManager()->networkManager()->get(request); + connect(statusReply, &QNetworkReply::finished, statusReply, &QNetworkReply::deleteLater); + connect(statusReply, &QNetworkReply::finished, thing, [=](){ + if (statusReply->error() != QNetworkReply::NoError) { + qCWarning(dcBosswerk) << "Error polling" << thing->name() << ":" << statusReply->error() << statusReply->errorString(); + return; + } + QByteArray data = statusReply->readAll(); + +// qCDebug(dcBosswerk) << "Status:" << qUtf8Printable(data); + foreach (const QString &line, QString(data).split("\n")) { + if (line.startsWith("var ")) { + qCDebug(dcBosswerk()) << "Data line:" << line; + } + if (line.startsWith("var webdata_now_p")) { + bool ok; + double currentPower = line.split("\"").at(1).toDouble(&ok); + if (ok) { + thing->setStateValue(mix00CurrentPowerStateTypeId, -currentPower); + } + } + if (line.startsWith("var webdata_total_e")) { + bool ok; + double totalEnergyProduced = line.split("\"").at(1).toDouble(&ok); + if (ok && totalEnergyProduced != 0) { // When it's not producing anything, it will also give 0 for the total. Ignoring that... + thing->setStateValue(mix00TotalEnergyProducedStateTypeId, totalEnergyProduced); + } + } + if (line.startsWith("var cover_sta_rssi")) { + bool ok; + QString rssiString = line.split("\"").at(1); + rssiString.remove("%"); + int rssi = rssiString.toInt(&ok); + if (ok) { + thing->setStateValue(mix00SignalStrengthStateTypeId, rssi); + } + } + } + }); +} diff --git a/bosswerk/integrationpluginbosswerk.h b/bosswerk/integrationpluginbosswerk.h new file mode 100644 index 00000000..ba1a08f5 --- /dev/null +++ b/bosswerk/integrationpluginbosswerk.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 INTEGRATIONPLUGINMEROSS_H +#define INTEGRATIONPLUGINMEROSS_H + +#include "integrations/integrationplugin.h" +#include "extern-plugininfo.h" + +class PluginTimer; +class NetworkDeviceMonitor; +class QNetworkReply; + +class IntegrationPluginBosswerk: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginbosswerk.json") + Q_INTERFACES(IntegrationPlugin) + +public: + enum Method { + GET, + SET + }; + Q_ENUM(Method) + + explicit IntegrationPluginBosswerk(); + ~IntegrationPluginBosswerk(); + + 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 thingRemoved(Thing *thing) override; + +private slots: + void pollDevice(Thing *thing); + +private: + QHash m_deviceMonitors; + QHash m_timers; +}; + +#endif // INTEGRATIONPLUGINMEROSS_H diff --git a/bosswerk/integrationpluginbosswerk.json b/bosswerk/integrationpluginbosswerk.json new file mode 100644 index 00000000..e0910f63 --- /dev/null +++ b/bosswerk/integrationpluginbosswerk.json @@ -0,0 +1,73 @@ +{ + "name": "bosswerk", + "displayName": "Bosswerk", + "id": "595b7759-336d-4677-a014-1b0fd11f45ea", + "vendors": [ + { + "name": "bosswerk", + "displayName": "Bosswerk", + "id": "26ec1591-cc37-4ac1-b943-04844e002601", + "thingClasses": [ + { + "id": "31ee3e61-eb3f-470b-8957-293fe65f404d", + "name": "mix00", + "displayName": "MI-300/600", + "createMethods": ["discovery"], + "setupMethod": "userandpassword", + "interfaces": [ "solarinverter", "wirelessconnectable" ], + "paramTypes": [ + { + "id": "6fbe5f08-3539-447d-9281-916abe9d8128", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "b1a9bdf7-1c87-4c5d-b7e5-835697e7b7e5", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "4187873d-50dd-4470-8bd1-2787436db84d", + "name": "signalStrength", + "displayName": "Signal strength", + "displayNameEvent": "Signal strength changed", + "type": "uint", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0, + "filter": "adaptive", + "cached": false + }, + { + "id": "044c26ce-67a7-4c81-99b1-4aa35285b109", + "name": "currentPower", + "displayName": "Current power consumption", + "displayNameEvent": "Current power consumption changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "4a596301-3a8d-41de-bc97-275d23c0e5cd", + "name": "totalEnergyProduced", + "displayName": "Total produced energy", + "displayNameEvent": "Total produced energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ] + } + ] + } + ] +} diff --git a/bosswerk/meta.json b/bosswerk/meta.json new file mode 100644 index 00000000..c6aa1fee --- /dev/null +++ b/bosswerk/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Bosswerk", + "tagline": "Integrates Bosswerk solar inverters with nymea.", + "icon": "bosswerk.jpg", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/bosswerk/translations/595b7759-336d-4677-a014-1b0fd11f45ea-en_US.ts b/bosswerk/translations/595b7759-336d-4677-a014-1b0fd11f45ea-en_US.ts new file mode 100644 index 00000000..7f795b8d --- /dev/null +++ b/bosswerk/translations/595b7759-336d-4677-a014-1b0fd11f45ea-en_US.ts @@ -0,0 +1,70 @@ + + + + + IntegrationPluginBosswerk + + + Please enter your login credentials. + + + + + An error happened in the network communication. + + + + + Failed to log in at the inverter. + + + + + bosswerk + + + + Bosswerk + The name of the vendor ({26ec1591-cc37-4ac1-b943-04844e002601}) +---------- +The name of the plugin bosswerk ({595b7759-336d-4677-a014-1b0fd11f45ea}) + + + + + Connected + The name of the StateType ({b1a9bdf7-1c87-4c5d-b7e5-835697e7b7e5}) of ThingClass mix00 + + + + + Current power consumption + The name of the StateType ({044c26ce-67a7-4c81-99b1-4aa35285b109}) of ThingClass mix00 + + + + + MAC address + The name of the ParamType (ThingClass: mix00, Type: thing, ID: {6fbe5f08-3539-447d-9281-916abe9d8128}) + + + + + MI-300/600 + The name of the ThingClass ({31ee3e61-eb3f-470b-8957-293fe65f404d}) + + + + + Signal strength + The name of the StateType ({4187873d-50dd-4470-8bd1-2787436db84d}) of ThingClass mix00 + + + + + Total produced energy + The name of the StateType ({4a596301-3a8d-41de-bc97-275d23c0e5cd}) of ThingClass mix00 + + + + diff --git a/debian/control b/debian/control index ece06626..3fe40c54 100644 --- a/debian/control +++ b/debian/control @@ -99,6 +99,14 @@ Description: nymea integration plugin for bose soundtouch This package contains the nymea integration plugin for bose soundtouch devices. +Package: nymea-plugin-bosswerk +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for Bosswerk micro inverters + This package contains the nymea integration plugin for Bosswerk micro inverters. + + Package: nymea-plugin-coinmarketcap Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-bosswerk.install.in b/debian/nymea-plugin-bosswerk.install.in new file mode 100644 index 00000000..3cf04b21 --- /dev/null +++ b/debian/nymea-plugin-bosswerk.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginbosswerk.so +bosswerk/translations/*qm usr/share/nymea/translations/ diff --git a/nymea-plugins.pro b/nymea-plugins.pro index dae77734..714d90c1 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -9,6 +9,7 @@ PLUGIN_DIRS = \ bluos \ boblight \ bose \ + bosswerk \ coinmarketcap \ commandlauncher \ datetime \