diff --git a/debian/control b/debian/control index 244651b5..99ef84a8 100644 --- a/debian/control +++ b/debian/control @@ -841,6 +841,15 @@ Description: nymea integration plugin to connect to your Tado account This package contains the nymea integration plugin for Tado devices. +Package: nymea-plugin-v2xeamberelectric +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Conflicts: nymea-plugins-translations (<< 1.0.1) +Description: This nymea plugin fetches live and forecasted price data, + providing real-time and future pricing information for integration with smart energy management systems. + + Package: nymea-plugin-wheretheissat Architecture: any Depends: ${misc:Depends}, diff --git a/debian/nymea-plugin-v2xeambereletric.install.in b/debian/nymea-plugin-v2xeambereletric.install.in new file mode 100644 index 00000000..03ccd74f --- /dev/null +++ b/debian/nymea-plugin-v2xeambereletric.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginv2xeamberelectric.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 32eae9f8..5e12641e 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -84,6 +84,7 @@ PLUGIN_DIRS = \ unifi \ usbrelay \ usbrly82 \ + v2xeamberelectric \ wakeonlan \ wemo \ ws2812fx \ diff --git a/v2xeamberelectric/README.md b/v2xeamberelectric/README.md new file mode 100644 index 00000000..f94db2f8 --- /dev/null +++ b/v2xeamberelectric/README.md @@ -0,0 +1,7 @@ +# v2xe_amber_electric_api +-------------------------------- + +Description of the plugin... + +This Nymea plugin fetches live and forecasted price data, providing real-time and future pricing information for integration with smart energy management systems + diff --git a/v2xeamberelectric/integrationpluginv2xeamberelectric.json b/v2xeamberelectric/integrationpluginv2xeamberelectric.json new file mode 100644 index 00000000..f7bcfa44 --- /dev/null +++ b/v2xeamberelectric/integrationpluginv2xeamberelectric.json @@ -0,0 +1,102 @@ +{ + "name": "AmberElectric", + "displayName": "Amber Electric", + "id": "0233c848-6f6f-4907-a51c-81391e4f5356", + "vendors": [ + { + "name": "AmberElectric", + "displayName": "Amber Electric", + "id": "ec1985b9-5891-435d-9264-e8e18d392978", + "thingClasses": [ + { + "name": "V2XeAmberElectric", + "displayName": "Amber Electric - V2Xe Amber Electric", + "id": "11295990-7002-432e-b90c-cd4010548a32", + "setupMethod": "JustAdd", + "createMethods": ["User"], + "interfaces": ["smartmeter", "connectable"], + "paramTypes": [ + { + "id": "fc899f7b-f452-44dc-b72e-ee7bb8034ff0", + "name": "authToken", + "displayName": "auth token", + "type" : "QString", + "defaultValue": "" + }, + { + "id": "c50eac84-714e-4b9c-89d5-0b939b6c18d6", + "name": "currentSite", + "displayName": "current site", + "type" : "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "faf2a6c0-1897-4b9b-903a-e12b281efe24", + "name": "connected", + "displayName": "Reachable", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "42c8a6f8-4f97-4871-8bfb-ac7e6ffffa74", + "name": "Sites", + "displayName": "Sites", + "type": "QString", + "writable": false, + "defaultValue": "" + }, + { + "id": "27c3f8e0-0803-4eb8-ba4f-ce78fb0997a9", + "name": "Type", + "displayName": "type", + "type": "QString", + "defaultValue": "Amber" + }, + { + "id": "6e8d3fcd-c235-4793-95b5-321ca6e012a3", + "name": "forecastprice", + "displayName": "Feedin Price", + "type": "double", + "defaultValue": 0, + "unit": "KiloWattHour", + "suggestLogging": true + }, + { + "id": "a25d3b55-9b93-4465-90e3-5d36adb8fb23", + "name": "currentprice", + "displayName": "Current Price", + "type": "double", + "unit":"KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "cd1e22b2-2154-453a-bbdf-59156893258a", + "name": "currentfeedin", + "displayName": "Current Feedin", + "type": "double", + "unit":"KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "52188fdf-4a4d-4ca8-b02e-cea5ed8c1615", + "name": "futurefeedin", + "displayName": "Future Feedin", + "type": "double", + "unit":"KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + } + ] + } + ] + } + ] +} + + + diff --git a/v2xeamberelectric/v2xeamberelectric.cpp b/v2xeamberelectric/v2xeamberelectric.cpp new file mode 100644 index 00000000..d7911c7b --- /dev/null +++ b/v2xeamberelectric/v2xeamberelectric.cpp @@ -0,0 +1,297 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2025 devendragajjar * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; * + * version 3 of the License. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "v2xeamberelectric.h" +#include "plugininfo.h" +#include "hardwaremanager.h" +#include "network/networkaccessmanager.h" + +#include +#include +#include +#include +#include +#include + + + + +const QString constSite = "01J1XBQFGX57137EH0C6AG040D"; +const QString noOfDataNeed = "5"; +const QString siteHttpLink = "https://api.amber.com.au/v1/sites"; +const QString priceHttpLinkFront = "https://api.amber.com.au/v1/sites/"; +const QString priceHttpLinkBack = "/prices/current?next="+ noOfDataNeed +"&previous=" + noOfDataNeed; +const QString authToken = "f5b2c823de5fed2f4c6bdb7c1b0db4f5"; + + +V2xeAmberElectric::V2xeAmberElectric() +{ + mConsumerKey = authToken; +} + +V2xeAmberElectric::~V2xeAmberElectric() +{ + +} + +void V2xeAmberElectric::init() +{ + // Initialisation can be done here. + qCDebug(dcAmberElectric()) << "Plugin initialized."; +} + + + +void V2xeAmberElectric::setupThing(ThingSetupInfo *info) +{ + // A thing is being set up. Use info->thing() to get details of the thing, do + // the required setup (e.g. connect to the device) and call info->finish() when done. + + qCDebug(dcAmberElectric()) << "Setup thing" << info->thing()->name() << info->thing()->params(); + QString key = info->thing()->paramValue(V2XeAmberElectricThingAuthTokenParamTypeId).toString(); + if (!key.isEmpty()) { + mConsumerKey = key; + qCDebug(dcAmberElectric()) << "mConsumerKey set " << mConsumerKey << "key " << key; + } + + QString site = info->thing()->paramValue(V2XeAmberElectricThingCurrentSiteParamTypeId).toString(); + if (!site.isEmpty()) { + mCurrentSite = site; + qCDebug(dcAmberElectric()) << "mCurrentSite set " << mCurrentSite ; + } + else{ + mCurrentSite = constSite; + } + + // use default key for now + qCDebug(dcAmberElectric()) << "No API key set."; + qCDebug(dcAmberElectric()) << "Either install an API key pacakge (nymea-apikeysprovider-plugin-*) or provide a key in the plugin settings."; + + if (!mPluginTimer) { + mPluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(1800);//TODO make 30 minutes after test for + connect(mPluginTimer, &PluginTimer::timeout, this, &V2xeAmberElectric::onPluginTimer); + } + info->finish(Thing::ThingErrorNoError); +} + +void V2xeAmberElectric::onPluginTimer() +{ + foreach (Thing *thing, myThings()) { + + requestSiteData(thing); + requestSitePriceData(thing); + + } +} + +void V2xeAmberElectric::executeAction(ThingActionInfo *info) +{ + // An action is being executed. Use info->action() to get details about the action, + // do the required operations (e.g. send a command to the network) and call info->finish() when done. + + qCDebug(dcAmberElectric()) << "Executing action for thing" << info->thing() << info->action().actionTypeId().toString() << info->action().params(); + + info->finish(Thing::ThingErrorNoError); +} + +void V2xeAmberElectric::thingRemoved(Thing *thing) +{ + // A thing is being removed from the system. Do the required cleanup + // (e.g. disconnect from the device) here. + + qCDebug(dcAmberElectric()) << "Remove thing" << thing; +} + +void V2xeAmberElectric::requestSitePriceData(Thing* thing, ThingSetupInfo* setup) { + + QString price_http_link = priceHttpLinkFront + mCurrentSite + priceHttpLinkBack; + + mServerUrls[V2XeAmberElectricThingClassId] = price_http_link; + + QNetworkRequest request; + request.setUrl(QUrl(mServerUrls[V2XeAmberElectricThingClassId])); + + // Add the Authorization header with the token + QString token = thing->paramValue(mConsumerKey).toString(); + token = mConsumerKey; + qCDebug(dcAmberElectric) << " using mConsumerKey " <networkManager()->get(request); + connect(reply, &QNetworkReply::finished, this, [this, thing, setup, reply]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status != 200) { + qCWarning(dcAmberElectric) << "Update reply HTTP error:" << status << reply->errorString(); + if (setup) { + setup->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error getting data from server.")); + } else { + thing->setStateValue(V2XeAmberElectricConnectedStateTypeId, false); + } + return; + } + onSitePriceDataReceived(thing, setup, reply); + }); +} + +void V2xeAmberElectric::onSitePriceDataReceived(Thing* thing, ThingSetupInfo* setup, QNetworkReply* reply) { + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcAmberElectric) << "Network error:" << reply->errorString(); + if (setup) { + setup->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Network error.")); + } + return; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcAmberElectric) << "JSON parse error:" << error.errorString(); + if (setup) { + setup->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("JSON parse error.")); + } + return; + } + + if (jsonDoc.isArray()) { + QString channelType; + QJsonArray jsonArray = jsonDoc.array(); + for (const QJsonValue &value : jsonArray) { + if (value.isObject()) { + QJsonObject jsonObject = value.toObject(); + QString type = jsonObject["type"].toString(); //Get type + channelType = jsonObject["channelType"].toString(); + //Current Price + if((type == "CurrentInterval") && channelType == "general"){ + if (jsonObject.contains("perKwh") && jsonObject["perKwh"].isDouble()) { // Get perKwh + double perkwh = jsonObject["perKwh"].toDouble(); + thing->setStateValue(V2XeAmberElectricCurrentpriceStateTypeId, qAbs(perkwh)); + } + } + //Future Price + if((type == "ForecastInterval") && channelType == "general"){ + if (jsonObject.contains("perKwh") && jsonObject["perKwh"].isDouble()) { // Get perKwh + double perkwh = jsonObject["perKwh"].toDouble(); + thing->setStateValue(V2XeAmberElectricForecastpriceStateTypeId, qAbs(perkwh)); + } + } + // Current Feedin //TODO what is feed in price its dummy data + if((type == "CurrentInterval") && (channelType == "feedIn")){ + if (jsonObject.contains("perKwh") && jsonObject["perKwh"].isDouble()) { // Get SportPerKwh + double spotperkwh = jsonObject["perKwh"].toDouble(); + thing->setStateValue(V2XeAmberElectricCurrentfeedinStateTypeId, qAbs(spotperkwh)); + } + } + //Future Feedin + if((type == "ForecastInterval") && (channelType == "feedIn")){ + if (jsonObject.contains("perKwh") && jsonObject["perKwh"].isDouble()) { // Get SportPerKwh + double spotperkwh = jsonObject["perKwh"].toDouble(); + thing->setStateValue(V2XeAmberElectricFuturefeedinStateTypeId, qAbs(spotperkwh)); + } + } + } + } + } + + if (setup) { + setup->finish(Thing::ThingErrorNoError); + } + thing->setStateValue(V2XeAmberElectricConnectedStateTypeId, true); +} + + +void V2xeAmberElectric::requestSiteData(Thing* thing, ThingSetupInfo* setup) { + + mServerUrls[V2XeAmberElectricThingClassId] = siteHttpLink; + + + QNetworkRequest request; + request.setUrl(QUrl(mServerUrls[V2XeAmberElectricThingClassId])); + + // Add the Authorization header with the token + QString token = thing->paramValue(mConsumerKey).toString(); + token = mConsumerKey; + qCDebug(dcAmberElectric) << " using mConsumerKey " <networkManager()->get(request); + connect(reply, &QNetworkReply::finished, this, [this, thing, setup, reply]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status != 200) { + qCWarning(dcAmberElectric) << "Update reply HTTP error:" << status << reply->errorString(); + if (setup) { + setup->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error getting data from server.")); + } else { + thing->setStateValue(V2XeAmberElectricConnectedStateTypeId, false); + } + return; + } + + onSiteDataReceived(thing, setup, reply); + }); +} + +void V2xeAmberElectric::onSiteDataReceived(Thing* thing, ThingSetupInfo* setup, QNetworkReply* reply) { + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcAmberElectric) << "Network error:" << reply->errorString(); + if (setup) { + setup->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Network error.")); + } + return; + } + QVariantList stringList = {}; + QString l_site; + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcAmberElectric) << "JSON parse error:" << error.errorString(); + if (setup) { + setup->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("JSON parse error.")); + } + return; + } + qCDebug(dcAmberElectric) << "onSiteDataReceived " ; + if (jsonDoc.isArray()) { + QJsonArray jsonArray = jsonDoc.array(); + for (const QJsonValue &value : jsonArray) { + if (value.isObject()) { + QJsonObject jsonObject = value.toObject(); + QString type = jsonObject["id"].toString(); //Get type + + if(!type.isEmpty()){ //update + stringList.append(type); + qCDebug(dcAmberElectric) << "type :" << type; + l_site = type; + } + } + } + } + thing->setStateValue(V2XeAmberElectricSitesStateTypeId, l_site); + if (setup) { + setup->finish(Thing::ThingErrorNoError); + } + thing->setStateValue(V2XeAmberElectricConnectedStateTypeId, true); +} + diff --git a/v2xeamberelectric/v2xeamberelectric.h b/v2xeamberelectric/v2xeamberelectric.h new file mode 100644 index 00000000..6774899c --- /dev/null +++ b/v2xeamberelectric/v2xeamberelectric.h @@ -0,0 +1,71 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2025 devendragajjar * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; * + * version 3 of the License. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINEV_CHARGER_H +#define INTEGRATIONPLUGINEV_CHARGER_H + +#include "integrations/integrationplugin.h" +#include "extern-plugininfo.h" +#include "plugintimer.h" +#include "network/networkaccessmanager.h" + +#include +#include +#include +#include +#include + + +class V2xeAmberElectric: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginv2xeamberelectric.json") + Q_INTERFACES(IntegrationPlugin) + + +public: + explicit V2xeAmberElectric(); + ~V2xeAmberElectric(); + + void init() override; + + void setupThing(ThingSetupInfo *info) override; + + void executeAction(ThingActionInfo *info) override; + + void thingRemoved(Thing *thing) override; + +private slots: + void onPluginTimer(); + void requestSitePriceData(Thing* thing, ThingSetupInfo* setup = nullptr); + void requestSiteData(Thing* thing, ThingSetupInfo* setup = nullptr); + +private: + PluginTimer *mPluginTimer = nullptr; + QString mConsumerKey; + QString mCurrentSite; + QHash mServerUrls; + + void onSiteDataReceived(Thing* thing, ThingSetupInfo* setup = nullptr, QNetworkReply* reply = nullptr); + void onSitePriceDataReceived(Thing* thing, ThingSetupInfo* setup = nullptr, QNetworkReply* reply = nullptr); +}; + +#endif // INTEGRATIONPLUGINEV_CHARGER_H diff --git a/v2xeamberelectric/v2xeamberelectric.pro b/v2xeamberelectric/v2xeamberelectric.pro new file mode 100644 index 00000000..fde101af --- /dev/null +++ b/v2xeamberelectric/v2xeamberelectric.pro @@ -0,0 +1,13 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + v2xeamberelectric.cpp + +HEADERS += \ + v2xeamberelectric.h + +DISTFILES += \ + integrationpluginv2xeamberelectric.json +