New Plugin: Bosswerk micro inverters

master
Michael Zanetti 2022-06-02 16:00:58 +02:00
parent 1f411c2244
commit 7d915296f4
11 changed files with 488 additions and 0 deletions

18
bosswerk/README.md Normal file
View File

@ -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.

BIN
bosswerk/bosswerk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

9
bosswerk/bosswerk.pro Normal file
View File

@ -0,0 +1,9 @@
include(../plugins.pri)
QT += network
SOURCES += \
integrationpluginbosswerk.cpp \
HEADERS += \
integrationpluginbosswerk.h \

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <plugintimer.h>
#include <network/networkdevicediscovery.h>
#include <network/networkaccessmanager.h>
#include <network/networkdevicediscoveryreply.h>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QMetaEnum>
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<MacAddress, NetworkDeviceInfo> 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);
}
}
}
});
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<Thing*, NetworkDeviceMonitor*> m_deviceMonitors;
QHash<Thing*, PluginTimer*> m_timers;
};
#endif // INTEGRATIONPLUGINMEROSS_H

View File

@ -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
}
]
}
]
}
]
}

13
bosswerk/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "Bosswerk",
"tagline": "Integrates Bosswerk solar inverters with nymea.",
"icon": "bosswerk.jpg",
"stability": "consumer",
"offline": true,
"technologies": [
"network"
],
"categories": [
"energy"
]
}

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginBosswerk</name>
<message>
<location filename="../integrationpluginbosswerk.cpp" line="82"/>
<source>Please enter your login credentials.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginbosswerk.cpp" line="93"/>
<source>An error happened in the network communication.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginbosswerk.cpp" line="108"/>
<source>Failed to log in at the inverter.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>bosswerk</name>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="33"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="36"/>
<source>Bosswerk</source>
<extracomment>The name of the vendor ({26ec1591-cc37-4ac1-b943-04844e002601})
----------
The name of the plugin bosswerk ({595b7759-336d-4677-a014-1b0fd11f45ea})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="39"/>
<source>Connected</source>
<extracomment>The name of the StateType ({b1a9bdf7-1c87-4c5d-b7e5-835697e7b7e5}) of ThingClass mix00</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="42"/>
<source>Current power consumption</source>
<extracomment>The name of the StateType ({044c26ce-67a7-4c81-99b1-4aa35285b109}) of ThingClass mix00</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="45"/>
<source>MAC address</source>
<extracomment>The name of the ParamType (ThingClass: mix00, Type: thing, ID: {6fbe5f08-3539-447d-9281-916abe9d8128})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="48"/>
<source>MI-300/600</source>
<extracomment>The name of the ThingClass ({31ee3e61-eb3f-470b-8957-293fe65f404d})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="51"/>
<source>Signal strength</source>
<extracomment>The name of the StateType ({4187873d-50dd-4470-8bd1-2787436db84d}) of ThingClass mix00</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="54"/>
<source>Total produced energy</source>
<extracomment>The name of the StateType ({4a596301-3a8d-41de-bc97-275d23c0e5cd}) of ThingClass mix00</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

8
debian/control vendored
View File

@ -90,6 +90,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},

View File

@ -0,0 +1,2 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginbosswerk.so
bosswerk/translations/*qm usr/share/nymea/translations/

View File

@ -8,6 +8,7 @@ PLUGIN_DIRS = \
bluos \
boblight \
bose \
bosswerk \
coinmarketcap \
commandlauncher \
datetime \