Merge PR #574: New plugin: powerfox

master
jenkins 2022-07-01 22:48:57 +02:00
commit 37c936fd01
11 changed files with 575 additions and 0 deletions

9
debian/control vendored
View File

@ -474,6 +474,15 @@ Description: nymea integration plugin for philipshue
and connected devices to it.
Package: nymea-plugin-powerfox
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
Description: nymea integration plugin for powerfox
This package contains the nymea integration plugin for the powerfox online service
to integrate energy meters.
Package: nymea-plugin-pushbullet
Architecture: any
Depends: ${shlibs:Depends},

View File

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

View File

@ -50,6 +50,7 @@ PLUGIN_DIRS = \
openweathermap \
osdomotics \
philipshue \
powerfox \
pushbullet \
pushnotifications \
shelly \

13
powerfox/README.md Normal file
View File

@ -0,0 +1,13 @@
# powerfox
This integration connects to the powerfox online api to obtain information about power meters.
Currently only power meters are supported.
## Requirements
* A powerfox online account is required.
* The power meter devices need to be configured with the powerfox app and connected to powerfox
## More
More information [https://www.powerfox.energy/](https://www.powerfox.energy/).

View File

@ -0,0 +1,232 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 "integrationpluginpowerfox.h"
#include "plugininfo.h"
#include "plugintimer.h"
#include <network/networkaccessmanager.h>
#include <QNetworkReply>
#include <QJsonDocument>
IntegrationPluginPowerfox::IntegrationPluginPowerfox()
{
}
IntegrationPluginPowerfox::~IntegrationPluginPowerfox()
{
}
void IntegrationPluginPowerfox::startPairing(ThingPairingInfo *info)
{
info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter the login credentials for your powerfox account."));
}
void IntegrationPluginPowerfox::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &password)
{
// Using /main/current as that one has the highest rate limit and we don't want to lock up the api for the following
// setupThing call as /all/devices can only be called once per minute.
QNetworkRequest request(QUrl("https://backend.powerfox.energy/api/2.0/my/main/current"));
QString concatenated = username + ":" + password;
QByteArray data = concatenated.toLocal8Bit().toBase64();
QString headerData = "Basic " + data;
request.setRawHeader("Authorization", headerData.toLocal8Bit());
qCDebug(dcPowerfox()) << "requesting:" << request.url().toString() << headerData;
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [info, reply, this, username, password](){
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
qCWarning(dcPowerfox()) << "Error fetching devices from account";
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Error logging to powerfox. Please try again."));
return;
}
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcPowerfox()) << "Error getting paired devices" << reply->error() << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure, reply->errorString());
return;
}
qCDebug(dcPowerfox) << "Auth request succeeded";
pluginStorage()->beginGroup(info->thingId().toString());
pluginStorage()->setValue("username", username);
pluginStorage()->setValue("password", password);
pluginStorage()->endGroup();
info->finish(Thing::ThingErrorNoError);
});
}
void IntegrationPluginPowerfox::setupThing(ThingSetupInfo *info)
{
if (info->thing()->thingClassId() == accountThingClassId) {
QNetworkReply *reply = request(info->thing(), "/all/devices"); // Max 1 per minute
connect(reply, &QNetworkReply::finished, info, [info, reply, this](){
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
qCWarning(dcPowerfox()) << "Error fetching devices from account";
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Login error at powerfox.energy."));
return;
}
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcPowerfox()) << "Error fetching devices from account" << reply->error();
info->finish(Thing::ThingErrorAuthenticationFailure, reply->errorString());
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcPowerfox()) << "JSON parse error in response from powerfox:" << error.error << error.errorString() << data;
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to process response from from server."));
return;
}
info->finish(Thing::ThingErrorNoError);
info->thing()->setStateValue(accountConnectedStateTypeId, true);
info->thing()->setStateValue(accountLoggedInStateTypeId, true);
foreach (const QVariant &entry, jsonDoc.toVariant().toList()) {
QVariantMap entryMap = entry.toMap();
QString id = entryMap.value("DeviceId").toString();
bool mainDevice = entryMap.value("MainDevice").toBool();
Division division = static_cast<Division>(entryMap.value("Devision").toInt());
if (!mainDevice) {
qCDebug(dcPowerfox()) << "Skipping non-main device" << qUtf8Printable(QJsonDocument::fromVariant(entryMap).toJson());
continue;
}
if (division != DivisionPowerMeter) {
qCInfo(dcPowerfox()) << "Device type" << division << "is not yet supported.";
continue;
}
Thing *child = myThings().filterByParentId(info->thing()->id()).findByParams({Param(powerMeterThingIdParamTypeId, id)});
if (!child) {
qCDebug(dcPowerfox()) << "Found new power meter device:" << id;
ThingDescriptor descriptor(powerMeterThingClassId, "powerfox power meter", QString(), info->thing()->id());
descriptor.setParams({Param(powerMeterThingIdParamTypeId, id)});
emit autoThingsAppeared({descriptor});
}
}
});
return;
}
info->finish(Thing::ThingErrorNoError);
}
void IntegrationPluginPowerfox::postSetupThing(Thing */*thing*/)
{
if (!m_pollTimer) {
m_pollTimer = hardwareManager()->pluginTimerManager()->registerTimer(4);
connect(m_pollTimer, &PluginTimer::timeout, this, [this](){
foreach (Thing *account, myThings().filterByInterface("account")) {
foreach (Thing *powerMeter, myThings().filterByParentId(account->id()).filterByInterface("energymeter")) {
QUrlQuery query;
query.addQueryItem("unit", "kWh");
// Can be called at max once per 3 secs. Not sure if that's per account or per meter ID yet. Assuming per meter ID for now.
QNetworkReply *reply = request(account, "/" + powerMeter->paramValue(powerMeterThingIdParamTypeId).toString() + "/current", query);
connect(reply, &QNetworkReply::finished, powerMeter, [account, powerMeter, reply](){
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
account->setStateValue(accountConnectedStateTypeId, false);
account->setStateValue(accountLoggedInStateTypeId, false);
}
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcPowerfox()) << "Failed to poll power meter:" << reply->error() << reply->errorString();
powerMeter->setStateValue(powerMeterConnectedStateTypeId, false);
powerMeter->setStateValue(powerMeterCurrentPowerStateTypeId, 0);
powerMeter->setStateValue(powerMeterCurrentPhaseAStateTypeId, 0);
powerMeter->setStateValue(powerMeterVoltagePhaseAStateTypeId, 0);
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcPowerfox()) << "Unable to parse reply from powerfox:" << error.error << error.errorString() << data;
return;
}
account->setStateValue(accountConnectedStateTypeId, true);
QVariantMap map = jsonDoc.toVariant().toMap();
powerMeter->setStateValue(powerMeterConnectedStateTypeId, !map.value("Outdated").toBool());
powerMeter->setStateValue(powerMeterCurrentPowerStateTypeId, map.value("Watt").toDouble());
powerMeter->setStateValue(powerMeterTotalEnergyConsumedStateTypeId, map.value("A_Plus").toDouble());
powerMeter->setStateValue(powerMeterTotalEnergyProducedStateTypeId, map.value("A_Minus").toDouble());
// We don't get voltage/current from the API, let's assume 230V as powerfox is only available in Europe for now
powerMeter->setStateValue(powerMeterVoltagePhaseAStateTypeId, 230);
powerMeter->setStateValue(powerMeterCurrentPhaseAStateTypeId, powerMeter->stateValue(powerMeterCurrentPowerStateTypeId).toDouble() / powerMeter->stateValue(powerMeterVoltagePhaseAStateTypeId).toDouble());
});
}
}
});
}
}
void IntegrationPluginPowerfox::thingRemoved(Thing */*thing*/)
{
if (myThings().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pollTimer);
m_pollTimer = nullptr;
}
}
QNetworkReply *IntegrationPluginPowerfox::request(Thing *thing, const QString &path, const QUrlQuery &query)
{
pluginStorage()->beginGroup(thing->id().toString());
QString username = pluginStorage()->value("username").toString();
QString password = pluginStorage()->value("password").toString();
pluginStorage()->endGroup();
QUrl url;
url.setScheme("https");
url.setHost("backend.powerfox.energy");
url.setPath("/api/2.0/my" + path);
url.setQuery(query);
QNetworkRequest request(url);
QString concatenated = username + ":" + password;
QByteArray data = concatenated.toLocal8Bit().toBase64();
QString headerData = "Basic " + data;
request.setRawHeader("Authorization", headerData.toLocal8Bit());
// qCDebug(dcPowerfox()) << "requesting:" << url.toString() << headerData;
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
return reply;
}

View File

@ -0,0 +1,77 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 INTEGRATIONPLUGINPOWERFOX_H
#define INTEGRATIONPLUGINPOWERFOX_H
#include "integrations/integrationplugin.h"
#include "extern-plugininfo.h"
#include <QUrlQuery>
class PluginTimer;
class QNetworkReply;
class IntegrationPluginPowerfox: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginpowerfox.json")
Q_INTERFACES(IntegrationPlugin)
public:
enum Division {
DivisionUnknown = -1,
DivisionPowerMeter = 0,
DivisionColdWaterMeter = 1,
DivisionWarmWaterMeter = 2,
DivisionHeatMeter = 3,
DivisionGasMeter = 4,
DivisionColdAndWarmWaterMeter = 5
};
Q_ENUM(Division)
explicit IntegrationPluginPowerfox();
~IntegrationPluginPowerfox();
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:
QNetworkReply *request(Thing *thing, const QString &path, const QUrlQuery &query = QUrlQuery());
private:
PluginTimer *m_pollTimer = nullptr;
};
#endif // INTEGRATIONPLUGINPOWERFOX_H

View File

@ -0,0 +1,116 @@
{
"name": "powerfox",
"displayName": "powerfox",
"id": "21cd8abd-1ff0-4156-87c5-7611153c3658",
"vendors": [
{
"name": "powerfox",
"displayName": "powerfox",
"id": "eb764f51-caff-481e-b008-56911f9f8446",
"thingClasses": [
{
"id": "20d0fe05-ae1d-46c0-b34a-f00a121177f7",
"name": "account",
"displayName": "powerfox account",
"createMethods": ["user"],
"setupMethod": "userandpassword",
"interfaces": [ "account" ],
"stateTypes": [
{
"id": "9cde6321-2abf-4a58-a1d6-c7418edb9747",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "634a43c1-e989-4bb2-b438-ea9aa0c2c33b",
"name": "loggedIn",
"displayName": "Logged in",
"displayNameEvent": "Logged in changed",
"type": "bool",
"defaultValue": false
}
]
},
{
"id": "b62c4e71-9e6e-4e7c-ae3c-79c96e4f8e27",
"name": "powerMeter",
"displayName": "powerfox smart meter",
"createMethods": ["auto"],
"interfaces": ["energymeter", "connectable"],
"paramTypes": [
{
"id": "b58b5abd-c878-4c6a-be6e-58e08da76ba2",
"name": "id",
"displayName": "ID",
"type": "QString",
"defaultValue": ""
}
],
"stateTypes": [
{
"id": "3d674ba8-76ff-4025-a7ff-c64b5ffdc08a",
"name": "connected",
"displayName": "Reachable",
"displayNameEvent": "Reachable changed",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "50c0ac31-edc4-4374-b763-1cdebdc84ef2",
"name": "currentPower",
"displayName": "Current power consumption",
"displayNameEvent": "Currant power consumption changed",
"type": "double",
"unit": "Watt",
"defaultValue": 0,
"cached": false
},
{
"id": "3c82dd4e-9485-4c57-87dd-a82f98d4d5ab",
"name": "totalEnergyConsumed",
"displayName": "Total energy consumption",
"displayNameEvent": "Total consumed energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "2c10f3e5-bc9f-4d65-b8d9-b1ff8db54751",
"name": "totalEnergyProduced",
"displayName": "Total energy production",
"displayNameEvent": "Total produced energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "6b62d3f8-6b91-48fd-9bf0-4eca6fb06efb",
"name": "currentPhaseA",
"displayName": "Current",
"displayNameEvent": "Current changed",
"type": "double",
"unit": "Ampere",
"defaultValue": 0,
"cached": false
},
{
"id": "dd8aeb79-606d-409a-9432-79a2fa7bad5c",
"name": "voltagePhaseA",
"displayName": "Voltage (Phase A)",
"displayNameEvent": "Voltage (Phase A) changed",
"type": "double",
"unit": "Volt",
"defaultValue": 0,
"cached": false
}
]
}
]
}
]
}

13
powerfox/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "powerfox",
"tagline": "Connects nymea to powerfox.",
"icon": "powerfox.jpeg",
"stability": "consumer",
"offline": false,
"technologies": [
"cloud"
],
"categories": [
"energy"
]
}

BIN
powerfox/powefox.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

7
powerfox/powerfox.pro Normal file
View File

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

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginPowerfox</name>
<message>
<location filename="../integrationpluginpowerfox.cpp" line="50"/>
<source>Please enter the login credentials for your Powerfox account.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpowerfox.cpp" line="66"/>
<source>Error logging to powerfox. Please try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpowerfox.cpp" line="93"/>
<source>Login error at powerfox.energy.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpowerfox.cpp" line="108"/>
<source>Unable to process response from from server.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>powerfox</name>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="38"/>
<source>Connected</source>
<extracomment>The name of the StateType ({9cde6321-2abf-4a58-a1d6-c7418edb9747}) of ThingClass account</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="41"/>
<source>Current</source>
<extracomment>The name of the StateType ({6b62d3f8-6b91-48fd-9bf0-4eca6fb06efb}) of ThingClass powerMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="44"/>
<source>Current power consumption</source>
<extracomment>The name of the StateType ({50c0ac31-edc4-4374-b763-1cdebdc84ef2}) of ThingClass powerMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="47"/>
<source>ID</source>
<extracomment>The name of the ParamType (ThingClass: powerMeter, Type: thing, ID: {b58b5abd-c878-4c6a-be6e-58e08da76ba2})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="50"/>
<source>Logged in</source>
<extracomment>The name of the StateType ({634a43c1-e989-4bb2-b438-ea9aa0c2c33b}) of ThingClass account</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="53"/>
<source>Reachable</source>
<extracomment>The name of the StateType ({3d674ba8-76ff-4025-a7ff-c64b5ffdc08a}) of ThingClass powerMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="56"/>
<source>Total energy consumption</source>
<extracomment>The name of the StateType ({3c82dd4e-9485-4c57-87dd-a82f98d4d5ab}) of ThingClass powerMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="59"/>
<source>Total energy production</source>
<extracomment>The name of the StateType ({2c10f3e5-bc9f-4d65-b8d9-b1ff8db54751}) of ThingClass powerMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="62"/>
<source>Voltage (Phase A)</source>
<extracomment>The name of the StateType ({dd8aeb79-606d-409a-9432-79a2fa7bad5c}) of ThingClass powerMeter</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="65"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="68"/>
<source>powerfox</source>
<extracomment>The name of the vendor ({eb764f51-caff-481e-b008-56911f9f8446})
----------
The name of the plugin powerfox ({21cd8abd-1ff0-4156-87c5-7611153c3658})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="71"/>
<source>powerfox account</source>
<extracomment>The name of the ThingClass ({20d0fe05-ae1d-46c0-b34a-f00a121177f7})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/powerfox/plugininfo.h" line="74"/>
<source>powerfox smart meter</source>
<extracomment>The name of the ThingClass ({b62c4e71-9e6e-4e7c-ae3c-79c96e4f8e27})</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>