diff --git a/debian/control b/debian/control
index 3fe40c54..de77f4b2 100644
--- a/debian/control
+++ b/debian/control
@@ -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},
diff --git a/debian/nymea-plugin-powerfox.install.in b/debian/nymea-plugin-powerfox.install.in
new file mode 100644
index 00000000..77faf19d
--- /dev/null
+++ b/debian/nymea-plugin-powerfox.install.in
@@ -0,0 +1,2 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginpowerfox.so
+powerfox/translations/*qm usr/share/nymea/translations/
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index 714d90c1..d0802476 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -50,6 +50,7 @@ PLUGIN_DIRS = \
openweathermap \
osdomotics \
philipshue \
+ powerfox \
pushbullet \
pushnotifications \
shelly \
diff --git a/powerfox/README.md b/powerfox/README.md
new file mode 100644
index 00000000..12f5c5ce
--- /dev/null
+++ b/powerfox/README.md
@@ -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/).
diff --git a/powerfox/integrationpluginpowerfox.cpp b/powerfox/integrationpluginpowerfox.cpp
new file mode 100644
index 00000000..6334e2d5
--- /dev/null
+++ b/powerfox/integrationpluginpowerfox.cpp
@@ -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 .
+*
+* 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
+#include
+#include
+
+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(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;
+}
+
diff --git a/powerfox/integrationpluginpowerfox.h b/powerfox/integrationpluginpowerfox.h
new file mode 100644
index 00000000..cf703002
--- /dev/null
+++ b/powerfox/integrationpluginpowerfox.h
@@ -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 .
+*
+* 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
+
+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
diff --git a/powerfox/integrationpluginpowerfox.json b/powerfox/integrationpluginpowerfox.json
new file mode 100644
index 00000000..75202a6e
--- /dev/null
+++ b/powerfox/integrationpluginpowerfox.json
@@ -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
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/powerfox/meta.json b/powerfox/meta.json
new file mode 100644
index 00000000..fe35036e
--- /dev/null
+++ b/powerfox/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "powerfox",
+ "tagline": "Connects nymea to powerfox.",
+ "icon": "powerfox.jpeg",
+ "stability": "consumer",
+ "offline": false,
+ "technologies": [
+ "cloud"
+ ],
+ "categories": [
+ "energy"
+ ]
+}
diff --git a/powerfox/powefox.jpeg b/powerfox/powefox.jpeg
new file mode 100644
index 00000000..908e9912
Binary files /dev/null and b/powerfox/powefox.jpeg differ
diff --git a/powerfox/powerfox.pro b/powerfox/powerfox.pro
new file mode 100644
index 00000000..d112b2e1
--- /dev/null
+++ b/powerfox/powerfox.pro
@@ -0,0 +1,7 @@
+include(../plugins.pri)
+
+QT += network
+
+SOURCES += integrationpluginpowerfox.cpp
+
+HEADERS += integrationpluginpowerfox.h
diff --git a/powerfox/translations/21cd8abd-1ff0-4156-87c5-7611153c3658-en_US.ts b/powerfox/translations/21cd8abd-1ff0-4156-87c5-7611153c3658-en_US.ts
new file mode 100644
index 00000000..14a888ff
--- /dev/null
+++ b/powerfox/translations/21cd8abd-1ff0-4156-87c5-7611153c3658-en_US.ts
@@ -0,0 +1,105 @@
+
+
+
+
+ IntegrationPluginPowerfox
+
+
+ Please enter the login credentials for your Powerfox account.
+
+
+
+
+ Error logging to powerfox. Please try again.
+
+
+
+
+ Login error at powerfox.energy.
+
+
+
+
+ Unable to process response from from server.
+
+
+
+
+ powerfox
+
+
+ Connected
+ The name of the StateType ({9cde6321-2abf-4a58-a1d6-c7418edb9747}) of ThingClass account
+
+
+
+
+ Current
+ The name of the StateType ({6b62d3f8-6b91-48fd-9bf0-4eca6fb06efb}) of ThingClass powerMeter
+
+
+
+
+ Current power consumption
+ The name of the StateType ({50c0ac31-edc4-4374-b763-1cdebdc84ef2}) of ThingClass powerMeter
+
+
+
+
+ ID
+ The name of the ParamType (ThingClass: powerMeter, Type: thing, ID: {b58b5abd-c878-4c6a-be6e-58e08da76ba2})
+
+
+
+
+ Logged in
+ The name of the StateType ({634a43c1-e989-4bb2-b438-ea9aa0c2c33b}) of ThingClass account
+
+
+
+
+ Reachable
+ The name of the StateType ({3d674ba8-76ff-4025-a7ff-c64b5ffdc08a}) of ThingClass powerMeter
+
+
+
+
+ Total energy consumption
+ The name of the StateType ({3c82dd4e-9485-4c57-87dd-a82f98d4d5ab}) of ThingClass powerMeter
+
+
+
+
+ Total energy production
+ The name of the StateType ({2c10f3e5-bc9f-4d65-b8d9-b1ff8db54751}) of ThingClass powerMeter
+
+
+
+
+ Voltage (Phase A)
+ The name of the StateType ({dd8aeb79-606d-409a-9432-79a2fa7bad5c}) of ThingClass powerMeter
+
+
+
+
+
+ powerfox
+ The name of the vendor ({eb764f51-caff-481e-b008-56911f9f8446})
+----------
+The name of the plugin powerfox ({21cd8abd-1ff0-4156-87c5-7611153c3658})
+
+
+
+
+ powerfox account
+ The name of the ThingClass ({20d0fe05-ae1d-46c0-b34a-f00a121177f7})
+
+
+
+
+ powerfox smart meter
+ The name of the ThingClass ({b62c4e71-9e6e-4e7c-ae3c-79c96e4f8e27})
+
+
+
+