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}) + + + +