diff --git a/debian/control b/debian/control index 5d1547af..d56a6ac5 100644 --- a/debian/control +++ b/debian/control @@ -202,6 +202,14 @@ Description: nymea integration plugin for dynatrace This package contains the nymea integration plugin for the dynatrace UFO +Package: nymea-plugin-easee +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for easee wallboxes + This package contains the nymea integration plugin for easee wallboxes + + Package: nymea-plugin-elgato Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-easee.install.in b/debian/nymea-plugin-easee.install.in new file mode 100644 index 00000000..24d7b2ff --- /dev/null +++ b/debian/nymea-plugin-easee.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugineasee.so +easee/translations/*qm usr/share/nymea/translations/ diff --git a/easee/README.md b/easee/README.md new file mode 100644 index 00000000..520fbf6c --- /dev/null +++ b/easee/README.md @@ -0,0 +1,15 @@ +# Easee + +This integration allows nymea to control Easee wallboxes. + +## Supported things + +All models of Easee wallboxes are supported. + +## Requirements and setup + +It is required to create an account at easee and connect the wallbox to that account. Generally, setting up the Wallbox using the Easee app will provide everything necessary. +Set up an "easee account" in nymea using your easee account credentials. All connected wallboxes will appear in nymea. + + + diff --git a/easee/easee.png b/easee/easee.png new file mode 100644 index 00000000..d8307aeb Binary files /dev/null and b/easee/easee.png differ diff --git a/easee/easee.pro b/easee/easee.pro new file mode 100644 index 00000000..05cdefde --- /dev/null +++ b/easee/easee.pro @@ -0,0 +1,11 @@ +include(../plugins.pri) + +QT += network websockets + +SOURCES += \ + integrationplugineasee.cpp \ + signalrconnection.cpp + +HEADERS += \ + integrationplugineasee.h \ + signalrconnection.h diff --git a/easee/integrationplugineasee.cpp b/easee/integrationplugineasee.cpp new file mode 100644 index 00000000..891bd227 --- /dev/null +++ b/easee/integrationplugineasee.cpp @@ -0,0 +1,531 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 "integrationplugineasee.h" +#include "plugininfo.h" +#include "signalrconnection.h" + +#include + +#include +#include +#include +#include + +IntegrationPluginEasee::IntegrationPluginEasee() +{ + +} + +IntegrationPluginEasee::~IntegrationPluginEasee() +{ +} + +void IntegrationPluginEasee::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/accounts/login"))); + request.setRawHeader("accept", "application/json"); + request.setRawHeader("content-type", "application/*+json"); + QVariantMap body; + body.insert("userName", username); + body.insert("password", secret); + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(body).toJson(QJsonDocument::Compact)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [this, info, reply, username, secret](){ + qCDebug(dcEasee) << "auth reply finished" << reply->error(); + + if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { + info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Authentication failed. Please try again.")); + return; + } + if (reply->error() != QNetworkReply::NoError) { + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to contact the easee server. Please try again later.")); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee) << "Unable to parse json:" << error.errorString() << data; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to process the response from easee. Please try again later.")); + return; + } + + QVariantMap map = jsonDoc.toVariant().toMap(); + QByteArray accessToken = map.value("accessToken").toByteArray(); + int expiresIn = map.value("expiresIn").toInt(); + QByteArray refreshToken = map.value("refreshToken").toByteArray(); + + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("accessToken", accessToken); + pluginStorage()->setValue("expiry", QDateTime::currentDateTime().addSecs(expiresIn)); + pluginStorage()->setValue("refreshToken", refreshToken); + + // FIXME: the refresh_token api call seems to not work... So we'll store user/pass in the config for now + pluginStorage()->setValue("username", username); + pluginStorage()->setValue("password", secret); + + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginEasee::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == accountThingClassId) { + pluginStorage()->beginGroup(info->thing()->id().toString()); + QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray(); + QByteArray refreshToken = pluginStorage()->value("refreshToken").toByteArray(); + QDateTime expiry = pluginStorage()->value("expiry").toDateTime(); + pluginStorage()->endGroup(); + + if (expiry < QDateTime::currentDateTime()) { + QNetworkReply *reply = this->refreshToken(thing); + connect(reply, &QNetworkReply::finished, info, [=](){ + setupThing(info); + }); + return; + } + + QNetworkRequest request = createRequest(thing, "accounts/profile"); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [this, thing, reply](){ + qCDebug(dcEasee) << "profile info finished" << reply->error(); + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcEasee) << "Unable to contact easee server..."; + thing->setStateValue(accountConnectedStateTypeId, false); + thing->setStateValue(accountLoggedInStateTypeId, false); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee) << "Unable to parse json:" << error.errorString() << data; + thing->setStateValue(accountConnectedStateTypeId, false); + thing->setStateValue(accountLoggedInStateTypeId, false); + return; + } + + thing->setStateValue(accountConnectedStateTypeId, true); + thing->setStateValue(accountLoggedInStateTypeId, true); + + QVariantMap map = jsonDoc.toVariant().toMap(); + qCDebug(dcEasee) << "Profile reply:" << data; + + refreshProducts(thing); + + }); + } + + if (thing->thingClassId() == chargerThingClassId) { + refreshCurrentState(thing); + } + + + info->finish(Thing::ThingErrorNoError); + +} + +void IntegrationPluginEasee::postSetupThing(Thing *thing) +{ + if (!m_timer) { + m_timer = hardwareManager()->pluginTimerManager()->registerTimer(60); + connect(m_timer, &PluginTimer::timeout, [this](){ + foreach (Thing *t, myThings()) { + if (t->thingClassId() == accountThingClassId) { + + // Refreshing the token if it is about to expire + pluginStorage()->beginGroup(t->id().toString()); + QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray(); + QByteArray refreshToken = pluginStorage()->value("refreshToken").toByteArray(); + QDateTime expiry = pluginStorage()->value("expiry").toDateTime(); + pluginStorage()->endGroup(); + if (expiry < QDateTime::currentDateTime().addSecs(120)) { + this->refreshToken(t); + } + + // Refreshing the products + refreshProducts(t); + + if (!m_signalRConnections.value(t)->connected()) { + // If the SignalR connection fails for whatever reason, let's poll + foreach (Thing *child, myThings().filterByParentId(t->id())) { + refreshCurrentState(child); + } + } + } else if (t->thingClassId() == chargerThingClassId) { + // We'll be using the SignalR connection instead for updates. + //refreshCurrentState(t); + } + } + }); + } + + + if (thing->thingClassId() == accountThingClassId) { + pluginStorage()->beginGroup(thing->id().toString()); + QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray(); + QDateTime expiry = pluginStorage()->value("expiry").toDateTime(); + pluginStorage()->endGroup(); + + qCDebug(dcEasee()) << "Access token:" << accessToken; + qCDebug(dcEasee()) << "Token expiry:" << expiry; + + SignalRConnection *signalR = new SignalRConnection(QUrl("http://streams.easee.com/hubs/chargers"), accessToken, hardwareManager()->networkManager(), thing); + m_signalRConnections.insert(thing, signalR); + + connect(signalR, &SignalRConnection::connectionStateChanged, thing, [=](bool connected){ + foreach (Thing *charger, myThings().filterByParentId(thing->id())) { + charger->setStateValue(chargerConnectedStateTypeId, true); + if (connected) { + signalR->subscribe(charger->paramValue(chargerThingIdParamTypeId).toString()); + } + } + }); + + connect(signalR, &SignalRConnection::dataReceived, thing, [=](const QVariantMap &data){ + if (data.value("target").toString() != "ProductUpdate") { + qCWarning(dcEasee()) << "Unhandled SignalR notification:" << data; + return; + } + + foreach (const QVariant &argumentVariant, data.value("arguments").toList()) { + QVariantMap arg = argumentVariant.toMap(); + QString chargerId = arg.value("mid").toString(); + ObservationPoint dataId = static_cast(arg.value("id").toUInt()); + QVariant value = arg.value("value"); + Thing *charger = myThings().filterByParentId(thing->id()).findByParams({Param(chargerThingIdParamTypeId, chargerId)}); + if (!charger) { + qCWarning(dcEasee()) << "Cannot find charger" << chargerId; + continue; + } + qCDebug(dcEasee()) << "SignalR data point:" << dataId << value; + + switch (dataId) { + case ObservationPointTotalPower: + charger->setStateValue(chargerCurrentPowerStateTypeId, value.toDouble() * 1000); + break; + case ObservationPointSessionEnergy: + charger->setStateValue(chargerSessionEnergyStateTypeId, value.toDouble()); + break; + case ObservationPointLifetimeEnergy: + charger->setStateValue(chargerTotalEnergyConsumedStateTypeId, value.toDouble()); + break; + case ObservationPointWiFiRSSI: + charger->setStateValue(chargerSignalStrengthStateTypeId, qMin(100, qMax(0, ((value.toInt() + 100) * 2)))); + break; + case ObservationPointPilotMode: { + QString mode = value.toString(); + qCDebug(dcEasee()) << "CP mode:" << mode; + if (mode == "A") { + charger->setStateValue(chargerPluggedInStateTypeId, false); + } else if (mode == "B" || mode == "C") { + charger->setStateValue(chargerPluggedInStateTypeId, true); + } else { + } + break; + } + case ObservationPointOutputPhase: + charger->setStateValue(chargerPhaseCountStateTypeId, value.toUInt() > 10 ? 3 : 1); + break; + case ObservationPointChargerOpMode: + // 2: charging disabled, 3: enabled and charging, 4: enabled but not charging + charger->setStateValue(chargerChargingStateTypeId, value.toUInt() == 3); + charger->setStateValue(chargerPowerStateTypeId, value.toUInt() >= 3); + break; + case ObservationPointDynamicChargerCurrent: + charger->setStateValue(chargerMaxChargingCurrentStateTypeId, value.toUInt()); + break; + case ObservationPointMaxChargerCurrent: + charger->setStateMaxValue(chargerMaxChargingCurrentStateTypeId, value.toUInt()); + break; + + default: + break; + } + } + }); + } +} + +void IntegrationPluginEasee::thingRemoved(Thing *thing) +{ + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->remove(""); + pluginStorage()->endGroup(); + + if (myThings().isEmpty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_timer); + m_timer = nullptr; + } +} + +void IntegrationPluginEasee::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + if (thing->thingClassId() == chargerThingClassId) { + Thing *parentThing = myThings().findById(thing->parentId()); + QString chargerId = thing->paramValue(chargerThingIdParamTypeId).toString(); + if (info->action().actionTypeId() == chargerPowerActionTypeId) { + bool power = info->action().paramValue(chargerPowerActionPowerParamTypeId).toBool(); + QString actionPath = power ? "start_charging" : "stop_charging"; + QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/commands/%2").arg(chargerId).arg(actionPath)); + qCDebug(dcEasee()) << "Setting power:" << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [reply, info, power](){ + qCDebug(dcEasee()) << "Reply" << reply->error(); + if (reply->error() == QNetworkReply::NoError) { + info->thing()->setStateValue(chargerPowerStateTypeId, power); + } + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + return; + } + if (info->action().actionTypeId() == chargerMaxChargingCurrentActionTypeId) { + uint maxChargingCurrent = info->action().paramValue(chargerMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId)); + QVariantMap data; + data.insert("dynamicChargerCurrent", maxChargingCurrent); + qCDebug(dcEasee()) << "Setting max current:" << request.url().toString() << QJsonDocument::fromVariant(data).toJson(); + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [reply, info, maxChargingCurrent](){ + qCDebug(dcEasee()) << "Reply" << reply->error(); + if (reply->error() == QNetworkReply::NoError) { + info->thing()->setStateValue(chargerMaxChargingCurrentStateTypeId, maxChargingCurrent); + } + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + return; + } + if (info->action().actionTypeId() == chargerDesiredPhaseCountActionTypeId) { + uint desiredPhaseCount = info->action().paramValue(chargerMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId)); + QVariantMap data; + data.insert("lockToSinglePhaseCharging", desiredPhaseCount == 1); + qCDebug(dcEasee()) << "Setting single phase charging:" << request.url().toString() << QJsonDocument::fromVariant(data).toJson(); + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [reply, info, desiredPhaseCount](){ + qCDebug(dcEasee()) << "Reply" << reply->error(); + if (reply->error() == QNetworkReply::NoError) { + info->thing()->setStateValue(chargerDesiredPhaseCountStateTypeId, desiredPhaseCount); + } + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + return; + } + + } + info->finish(Thing::ThingErrorNoError); +} + +QNetworkRequest IntegrationPluginEasee::createRequest(Thing *thing, const QString &endpoint) +{ + pluginStorage()->beginGroup(thing->id().toString()); + QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray(); + pluginStorage()->endGroup(); + QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/%1").arg(endpoint))); + request.setRawHeader("Authorization", "Bearer " + accessToken); + request.setRawHeader("accept", "application/json"); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/*+json"); + return request; +} + +QNetworkReply *IntegrationPluginEasee::refreshToken(Thing *thing) +{ + pluginStorage()->beginGroup(thing->id().toString()); + QByteArray refreshToken = pluginStorage()->value("refreshToken").toByteArray(); + QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray(); + QString username = pluginStorage()->value("username").toString(); + QString password = pluginStorage()->value("password").toString(); + pluginStorage()->endGroup(); + + + // FIXME: Ideally we should use the refresh_token API and not store user/pass in the config, but it seems to not work +// QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/accounts/refresh_token"))); +// request.setRawHeader("accept", "application/json"); +// request.setRawHeader("content-type", "application/*+json"); +// QVariantMap body; +// body.insert("refreshToken", refreshToken); +// body.insert("accessToken", accessToken); + + QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/accounts/login"))); + request.setRawHeader("accept", "application/json"); + request.setRawHeader("content-type", "application/*+json"); + QVariantMap body; + body.insert("userName", username); + body.insert("password", password); + + + QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(body).toJson(QJsonDocument::Compact)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + qCDebug(dcEasee) << "Token refresh finished" << reply->error(); + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcEasee) << "Unable to contact easee server..."; + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee) << "Unable to parse json:" << error.errorString() << data; + return; + } + + QVariantMap map = jsonDoc.toVariant().toMap(); + qCDebug(dcEasee) << "Token refresh reply:" << data; + + QByteArray accessToken = map.value("accessToken").toByteArray(); + int expiresIn = map.value("expiresIn").toInt(); + QByteArray refreshToken = map.value("refreshToken").toByteArray(); + + pluginStorage()->beginGroup(thing->id().toString()); + pluginStorage()->setValue("accessToken", accessToken); + pluginStorage()->setValue("expiry", QDateTime::currentDateTime().addSecs(expiresIn)); + pluginStorage()->setValue("refreshToken", refreshToken); + pluginStorage()->endGroup(); + + m_signalRConnections.value(thing)->updateToken(accessToken); + + }); + + return reply; +} + +void IntegrationPluginEasee::refreshProducts(Thing *account) +{ + QNetworkRequest request = createRequest(account, "accounts/products"); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, account, [this, account, reply](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcEasee) << "Unable to refresh products:" << reply->error() << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee) << "Unable to parse json for products:" << error.errorString() << data; + return; + } + + QVariantList list = jsonDoc.toVariant().toList(); + qCDebug(dcEasee) << "Products reply:" << qUtf8Printable(data); + + foreach (const QVariant &siteVariant, list) { + QVariantMap site = siteVariant.toMap(); + foreach (const QVariant &circuitVariant, site.value("circuits").toList()) { + QVariantMap circuit = circuitVariant.toMap(); +// double maxChartingCurrentLimit = circuit.value("ratedCurrent").toDouble(); + uint circuitId = circuit.value("id").toUInt(); + uint siteId = circuit.value("siteId").toUInt(); + foreach (const QVariant &chargerVariant, circuit.value("chargers").toList()) { + QVariantMap charger = chargerVariant.toMap(); + QString id = charger.value("id").toString(); + QString name = charger.value("name").toString(); + + ParamList params{Param(chargerThingIdParamTypeId, id)}; + Thing *existingThing = myThings().filterByParentId(account->id()).findByParams(params); + if (!existingThing) { + ThingDescriptor descriptor(chargerThingClassId, name, QString(), account->id()); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); + } + m_siteIds[id] = siteId; + m_circuitIds[id] = circuitId; + } + } + } + }); + +} + +void IntegrationPluginEasee::refreshCurrentState(Thing *charger) +{ + Thing *parentThing = myThings().findById(charger->parentId()); + QString chargerId = charger->paramValue(chargerThingIdParamTypeId).toString(); + QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/state").arg(chargerId)); + + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, charger, [charger, reply](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcEasee) << "Unable to fetch charger state:" << reply->error() << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee) << "Unable to parse json for charger state:" << error.errorString() << data; + return; + } + + QVariantMap map = jsonDoc.toVariant().toMap(); + qCDebug(dcEasee) << "Charger state reply:" << qUtf8Printable(jsonDoc.toJson()); + charger->setStateValue(chargerConnectedStateTypeId, map.value("isOnline").toBool()); + charger->setStateValue(chargerSignalStrengthStateTypeId, qMax(0, qMin(100, (map.value("wiFiRSSI").toInt() + 100) * 2))); + charger->setStateValue(chargerCurrentPowerStateTypeId, map.value("totalPower").toDouble() * 1000); + charger->setStateValue(chargerPhaseCountStateTypeId, map.value("outputPhase").toUInt() > 10 ? 3 : 1); + charger->setStateValue(chargerChargingStateTypeId, map.value("chargerOpMode").toUInt() == 3); + + // 1: unplugged, 2: charging disabled, 3: enabled and charging, 4: enabled but not charging + uint chargerOpMode = map.value("chargerOpMode").toUInt(); + charger->setStateValue(chargerPluggedInStateTypeId, chargerOpMode >= 2); + charger->setStateValue(chargerChargingStateTypeId, chargerOpMode == 3); + charger->setStateValue(chargerPowerStateTypeId, chargerOpMode >= 3); + + charger->setStateValue(chargerMaxChargingCurrentStateTypeId, map.value("dynamicChargerCurrent").toUInt()); + charger->setStateMaxValue(chargerMaxChargingCurrentStateTypeId, 6); // Fixme: where to get this from? + charger->setStateMaxValue(chargerMaxChargingCurrentStateTypeId, 32); // Fixme: where to get this from? + + charger->setStateValue(chargerTotalEnergyConsumedStateTypeId, map.value("lifetimeEnergy").toDouble()); + charger->setStateValue(chargerSessionEnergyStateTypeId, map.value("sessionEnergy").toDouble()); + + }); + + +} + diff --git a/easee/integrationplugineasee.h b/easee/integrationplugineasee.h new file mode 100644 index 00000000..cca914fb --- /dev/null +++ b/easee/integrationplugineasee.h @@ -0,0 +1,242 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, 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 INTEGRATIONPLUGINEASEE_H +#define INTEGRATIONPLUGINEASEE_H + +#include +#include + +#include "extern-plugininfo.h" + +#include +#include +#include +#include + +class SignalRConnection; + +class IntegrationPluginEasee: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugineasee.json") + Q_INTERFACES(IntegrationPlugin) + +public: + enum ObservationPoint { + ObservationPointSelfTestResult = 1, + ObservationPointSelfTestDetails = 2, + ObservationPointWiFiEvent = 10, + ObservationPointChargerOfflineReason = 11, + ObservationPointEaseeLinkCommandResponse = 13, + ObservationPointEaseeLinkDataReceived = 14, + ObservationPointLocalPreAuthorizeEnabled = 15, + ObservationPointLocalAuthorizeOfflineEnabled = 16, + ObservationPointAllowOfflineTxForUnknownId = 17, + ObservationPointErraticEvMaxToggles = 18, + ObservationPointBackplateType = 19, + ObservationPointSiteStructure = 20, + ObservationPointDetectedPowerGridType = 21, + ObservationPointCircuitMaxCurrentP1 = 22, + ObservationPointCircuitMaxCurrentP2 = 23, + ObservationPointCircuitMaxCurrentP3 = 24, + ObservationPointLocation = 25, + ObservationPointSiteIdString = 26, + ObservationPointSiteIdNumeric = 27, + ObservationPointLockCablePermanently = 30, + ObservationPointIsEnabled = 31, + ObservationPointCircuitSequenceNumber = 33, + ObservationPointSinglePhaseNumber = 34, + ObservationPointEnable3PhasesDeprecated = 35, + ObservationPointWiFiSSID = 36, + ObservationPointEnableIdCurrent = 37, + ObservationPointPhaseMode = 38, + ObservationPointForced3PhaseOnITWithGndFault = 39, + ObservationPointLedStripBrightness = 40, + ObservationPointLocalAuthorizationRequired = 41, + ObservationPointAuthorizationRequired = 42, + ObservationPointRemoteStartRequired = 43, + ObservationPointSmartButtonEnabled = 44, + ObservationPointOfflineChargingMode = 45, + ObservationPointLedMode = 46, + ObservationPointMaxChargerCurrent = 47, + ObservationPointDynamicChargerCurrent = 48, + ObservationPointMaxCurrentOfflineFallbackP1 = 50, + ObservationPointMaxCurrentOfflineFallbackP2 = 51, + ObservationPointMaxCurrentOfflineFallbackP3 = 52, + ObservationPointListenToControlPulse = 56, + ObservationPointControlPulseRTT = 57, + ObservationPointChargingSchedule = 62, + ObservationPointPairedEqualizer = 65, + ObservationPointWiFiApEnabled = 68, + ObservationPointPairedUserIdToken = 69, + ObservationPointCircuitTotalAllocatedPhaseConductorCurrentL1 = 70, + ObservationPointCircuitTotalAllocatedPhaseConductorCurrentL2 = 71, + ObservationPointCircuitTotalAllocatedPhaseConductorCurrentL3 = 72, + ObservationPointCircuitAllocatedPhaseConductorCurrentL1 = 73, + ObservationPointCircuitAllocatedPhaseConductorCurrentL2 = 74, + ObservationPointCircuitAllocatedPhaseConductorCurrentL3 = 75, + ObservationPointNumberOfCarsConnected = 76, + ObservationPointNumberOfCarsCharging = 77, + ObservationPointNumberOfCarsInQueue = 78, + ObservationPointNumberOfCarsFullyCharged = 79, + ObservationPointSoftwareRelease = 80, + ObservationPointICCID = 81, + ObservationPointModemFwId = 82, + ObservationPointOTAErrorCode = 83, + ObservationPointMobileNetworkOperator = 84, + ObservationPointRebootReason = 89, + ObservationPointPowerPCBVersion = 90, + ObservationPointCOMPCBVersion = 91, + ObservationPointReasonForNoCurrent = 96, + ObservationPointLoadBalancingNumberOfConnectedCharger = 97, + ObservationPointUDPNumOfConnectedNodes = 98, + ObservationPointLocalConnection = 99, + ObservationPointPilotMode = 100, + ObservationPointCarConnectedDeprecated = 101, + ObservationPointSmartCharging = 102, + ObservationPointCableLocked = 103, + ObservationPointCableRating = 104, + ObservationPointPilotHigh = 105, + ObservationPointPilotLow = 106, + ObservationPointBackPlateId = 107, + ObservationPointUserIdTokenReversed = 108, + ObservationPointChargerOpMode = 109, + ObservationPointOutputPhase = 110, + ObservationPointDynamicCircuitCurrentP1 = 111, + ObservationPointDynamicCircuitCurrentP2 = 112, + ObservationPointDynamicCircuitCurrentP3 = 113, + ObservationPointOutputCurrent = 114, + ObservationPointDeratedCurrent = 115, + ObservationPointDeratingActive = 116, + ObservationPointDebugString = 117, + ObservationPointErrorString = 118, + ObservationPointErrorCode = 119, + ObservationPointTotalPower = 120, + ObservationPointSessionEnergy = 121, + ObservationPointEnergyPerHour = 122, + ObservationPointLegacyEVStatus = 123, + ObservationPointLifetimeEnergy = 124, + ObservationPointLifetimeRelaySwitches = 125, + ObservationPointLifetimeHours = 126, + ObservationPointDynamicCurrentOfflineFallbackDeprecated = 127, + ObservationPointUserIdToken = 128, + ObservationPointChargingSession = 129, + ObservationPointCellRSSI = 130, + ObservationPointCellRAT = 131, + ObservationPointWiFiRSSI = 132, + ObservationPointCellAddress = 133, + ObservationPointWiFiAddress = 134, + ObservationPointWiFiType = 135, + ObservationPointLocalRSSI = 136, + ObservationPointMasterBackplateId = 137, + ObservationPointLocalTXPower = 138, + ObservationPointLocalState = 139, + ObservationPointFoundWiFi = 140, + ObservationPointChargerRAT = 141, + ObservationPointCellularInterfaceErrorCount = 142, + ObservationPointCellularInterfaceResetCount = 143, + ObservationPointWiFiInterfaceErrorCount = 144, + ObservationPointWiFiInterfaceResetCount = 145, + ObservationPointLocalNodeType = 146, + ObservationPointLocalRadioChannel = 147, + ObservationPointLocalShortAddress = 148, + ObservationPointLocalParentAddrOrNumOfNodes = 149, + ObservationPointTempMax = 150, + ObservationPointTempAmbientPowerBoard = 151, + ObservationPointTempInputT2 = 152, + ObservationPointTempInputT3 = 153, + ObservationPointTempInputT4 = 154, + ObservationPointTempInputT5 = 155, + ObservationPointTempOutputN = 160, + ObservationPointTempOutputL1 = 161, + ObservationPointTempOutputL2 = 162, + ObservationPointTempOutputL3 = 163, + ObservationPointTempAmbient = 170, + ObservationPointLightAmbient = 171, + ObservationPointIntRelHumidity = 172, + ObservationPointBackplateLocked = 173, + ObservationPointCurrentMotor = 174, + ObservationPointBackplateHallSensor = 175, + ObservationPointInCurrentT2 = 182, + ObservationPointInCurrentT3 = 183, + ObservationPointInCurrentT4 = 184, + ObservationPointInCurrentT5 = 185, + ObservationPointInVoltT1T2 = 190, + ObservationPointInVoltT1T3 = 191, + ObservationPointInVoltT1T4 = 192, + ObservationPointInVoltT1T5 = 193, + ObservationPointInVoltT2T3 = 194, + ObservationPointInVoltT2T4 = 195, + ObservationPointInVoltT2T5 = 196, + ObservationPointInVoltT3T4 = 197, + ObservationPointInVoltT3T5 = 198, + ObservationPointInVoltT4T5 = 199, + ObservationPointOutVoltPin12 = 202, + ObservationPointOutVoltPin13 = 203, + ObservationPointOutVoltPin14 = 204, + ObservationPointOutVoltPin15 = 205, + ObservationPointVoltLevel33 = 210, + ObservationPointVoltLevel5 = 211, + ObservationPointVoltLevel12 = 212, + ObservationPointLTERSRP = 220, + ObservationPointLTESINR = 221, + ObservationPointLTERSRQ = 222, + ObservationPointEQAvailableCurrentP1 = 230, + ObservationPointEQAvailableCurrentP2 = 231, + ObservationPointEQAvailableCurrentP3 = 232 + }; + Q_ENUM(ObservationPoint) + + explicit IntegrationPluginEasee(); + ~IntegrationPluginEasee(); + +// 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; + void executeAction(ThingActionInfo *info) override; + +private: + QNetworkRequest createRequest(Thing *thing, const QString &endpoint); + QNetworkReply *refreshToken(Thing *thing); + void refreshProducts(Thing *account); + void refreshCurrentState(Thing *charger); + + QHash m_signalRConnections; + QHash m_circuitIds; // chargerId, circuitId + QHash m_siteIds; // chargerId, siteId + + PluginTimer *m_timer = nullptr; +}; + +#endif // INTEGRATIONPLUGINEASEE_H diff --git a/easee/integrationplugineasee.json b/easee/integrationplugineasee.json new file mode 100644 index 00000000..5f9a2943 --- /dev/null +++ b/easee/integrationplugineasee.json @@ -0,0 +1,155 @@ +{ + "name": "easee", + "displayName": "Easee", + "id": "471aa296-78de-4917-84ed-c9a4216f5ae9", + "vendors": [ + { + "name": "easee", + "displayName": "Easee", + "id": "e43f66da-4e8c-4e2c-b821-c1eb8ac6bbf8", + "thingClasses": [ + { + "id": "2c93c25e-d12a-4709-b537-b5619ab1145a", + "name": "account", + "displayName": "Easee account", + "createMethods": ["user"], + "setupMethod": "userandpassword", + "interfaces": [ "account" ], + "providedInterfaces": ["evcharger"], + "stateTypes": [ + { + "id": "568d5896-813e-4f66-b430-ed8c8b1ad8c4", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "69d01e58-c1a3-4b9c-a4b3-f1de7c67febd", + "name": "loggedIn", + "displayName": "Logged in", + "type": "bool", + "defaultValue": false + } + ] + }, + { + "id": "9fe4d708-70d2-47bd-8433-54c5f3e8a110", + "name": "charger", + "displayName": "Easee charger", + "createMethods": ["auto"], + "interfaces": ["evcharger", "smartmeterconsumer", "wirelessconnectable"], + "paramTypes": [ + { + "id": "b9f0573e-bf41-45f0-a53e-b2457d51ecb5", + "name": "id", + "displayName": "Charger ID", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "1c818574-5aa8-43f5-8fa5-9a620ba86ccc", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "minValue": 6, + "maxValue": 16, + "defaultValue": 6, + "writable": true + }, + { + "id": "b5070800-3556-4156-aed5-ecbad7fa455d", + "name": "power", + "displayName": "Power", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "6b0c22e9-cdb4-49a0-9c18-97b776b63c59", + "name": "currentPower", + "displayName": "Current charting power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "0ac13ef2-e646-491f-9336-f39d5110f8bb", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false + }, + { + "id": "d95137ad-4f32-4ffd-851b-593fc40f5c0c", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false + }, + { + "id": "b55297a8-f1bd-44eb-8309-691d674ef4a0", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "2f0c3491-d524-4ee5-8214-8e3d2f750b66", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "3a68d6c5-d48f-4cd6-b9ca-fc21cbcc8808", + "name": "phaseCount", + "displayName": "Used phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "1baac39e-0e0e-4638-bd8d-09f04fd7bd62", + "name": "desiredPhaseCount", + "displayName": "Desired phase count", + "displayNameAction": "Set desired phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "possibleValues": [1,3], + "writable": true, + "defaultValue": 3 + }, + { + "id": "2a9b4c84-7b1d-4b32-b26f-84eff54fb04f", + "name": "connected", + "displayName": "Online", + "type": "bool", + "defaultValue": false + }, + { + "id": "7effe12e-2884-4597-ad0c-6b4aa9cdacfd", + "name": "signalStrength", + "displayName": "Signal strength", + "type": "uint", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": false + } + ] + } + ] + } + ] +} diff --git a/easee/meta.json b/easee/meta.json new file mode 100644 index 00000000..565dadde --- /dev/null +++ b/easee/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Easeex", + "tagline": "Integrates Easee wallboxes with nymea.", + "icon": "easee.png", + "stability": "consumer", + "offline": false, + "technologies": [ + "cloud" + ], + "categories": [ + "energy" + ] +} diff --git a/easee/signalrconnection.cpp b/easee/signalrconnection.cpp new file mode 100644 index 00000000..886741c1 --- /dev/null +++ b/easee/signalrconnection.cpp @@ -0,0 +1,159 @@ +#include "signalrconnection.h" + +#include +#include +#include +#include +#include +#include + +#include "extern-plugininfo.h" + +SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessToken, NetworkAccessManager *nam, QObject *parent) + : QObject{parent}, + m_url(url), + m_accessToken(accessToken), + m_nam(nam) +{ + m_socket = new QWebSocket(); + typedef void (QWebSocket:: *errorSignal)(QAbstractSocket::SocketError); + connect(m_socket, static_cast(&QWebSocket::error), this, [](QAbstractSocket::SocketError error){ + qCWarning(dcEasee) << "Error in websocket:" << error; + }); + connect(m_socket, &QWebSocket::stateChanged, this, [=](QAbstractSocket::SocketState state){ + qCDebug(dcEasee) << "Websocket state changed" << state; + + if (state == QAbstractSocket::ConnectedState) { + qCDebug(dcEasee) << "Websocket connected"; + + QVariantMap handshake; + handshake.insert("protocol", "json"); + handshake.insert("version", 1); + QByteArray data = encode(handshake); + qCDebug(dcEasee) << "Sending handshake" << data; + m_socket->sendTextMessage(data); + } else if (QAbstractSocket::UnconnectedState) { + QTimer::singleShot(5000, this, [=](){ + connectToHost(); + }); + } + + }); + connect(m_socket, &QWebSocket::binaryMessageReceived, this, [](const QByteArray &message){ + qCDebug(dcEasee) << "Binary message received" << message; + }); + connect(m_socket, &QWebSocket::textMessageReceived, this, [=](const QString &message){ + QStringList messages = message.split(QByteArray::fromHex("1E")); + + foreach (const QString &msg, messages) { + if (msg.isEmpty()) { + continue; + } +// qCDebug(dcEasee()) << "Received message:" << msg; + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(msg.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee()) << "Unable to parse message from SignalR socket" << error.errorString() << msg; + continue; + } + + if (m_waitingForHandshakeReply && jsonDoc.toVariant().toMap().isEmpty()) { + m_waitingForHandshakeReply = false; + qCDebug(dcEasee()) << "Handshake reply received."; + emit connectionStateChanged(true); + return; + } + + QVariantMap map = jsonDoc.toVariant().toMap(); + switch (map.value("type").toUInt()) { + case 1: + emit dataReceived(map); + break; + case 3: + // Silencing acks to our requests + qCDebug(dcEasee()) << "Message ACK received:" << map; + case 6: + // Silencing pings + break; + default: + qCWarning(dcEasee()) << "Unhandled signalr message type" << map; + } + } + }); + + connectToHost(); +} + +void SignalRConnection::subscribe(const QString &chargerId) +{ + QVariantMap map; + map.insert("type", 1); + map.insert("invocationId", QUuid::createUuid()); + map.insert("target", "SubscribeWithCurrentState"); + map.insert("arguments", QVariantList{chargerId, true}); + qCDebug(dcEasee) << "subscribing to" << chargerId; + m_socket->sendTextMessage(encode(map)); +} + +bool SignalRConnection::connected() const +{ + return m_socket->state() == QAbstractSocket::ConnectedState; +} + +void SignalRConnection::updateToken(const QByteArray &accessToken) +{ + m_accessToken = accessToken; +} + +QByteArray SignalRConnection::encode(const QVariantMap &message) +{ + return QJsonDocument::fromVariant(message).toJson(QJsonDocument::Compact).append(QByteArray::fromHex("1E")); +} + +void SignalRConnection::connectToHost() +{ + QUrl negotiationUrl = m_url; + negotiationUrl.setScheme("https"); + negotiationUrl.setPath(negotiationUrl.path() + "/negotiate"); + QNetworkRequest negotiateRequest(negotiationUrl); + negotiateRequest.setRawHeader("Authorization", "Bearer " + m_accessToken); + qCDebug(dcEasee()) << "Negotiating:" << negotiationUrl << negotiateRequest.rawHeader("Authorization"); + QNetworkReply *negotiantionReply = m_nam->post(negotiateRequest, QByteArray()); + connect(negotiantionReply, &QNetworkReply::finished, this, [=](){ + if (negotiantionReply->error() != QNetworkReply::NoError) { + qCWarning(dcEasee()) << "Unable to neotiate SignalR channel:" << negotiantionReply->error(); + return; + } + QByteArray data = negotiantionReply->readAll(); + qCDebug(dcEasee) << "Negotiation reply" << data; + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcEasee()) << "Unable to parse json from negoatiate endpoint" << error.errorString() << data; + return; + } + + QVariantMap map = jsonDoc.toVariant().toMap(); + QString connectionId = map.value("connectionId").toString(); + + + QUrl wsUrl = m_url; + wsUrl.setScheme("wss"); + QUrlQuery query; + query.addQueryItem("id", connectionId); + wsUrl.setQuery(query); + QNetworkRequest request(wsUrl); + request.setRawHeader("Authorization", "Bearer " + m_accessToken); + qCDebug(dcEasee()) << "Connecting websocket:" << wsUrl.toString(); + m_waitingForHandshakeReply = true; +#if QT_VERSION >= QT_VERSION_CHECK(5,6,0) + m_socket->open(request); +#else + qCWarning(dcEasee()) << "This plugin requires at least Qt 5.6 to establish a signal R connection. Updating values won't work."; +#endif + + }); + +} + diff --git a/easee/signalrconnection.h b/easee/signalrconnection.h new file mode 100644 index 00000000..19ef1c11 --- /dev/null +++ b/easee/signalrconnection.h @@ -0,0 +1,39 @@ +#ifndef SIGNALRCONNECTION_H +#define SIGNALRCONNECTION_H + +#include +#include +#include +#include + +class SignalRConnection : public QObject +{ + Q_OBJECT +public: + explicit SignalRConnection(const QUrl &url, const QByteArray &accessToken, NetworkAccessManager *nam, QObject *parent = nullptr); + + void subscribe(const QString &chargerId); + bool connected() const; + + void updateToken(const QByteArray &accessToken); + +signals: + void connectionStateChanged(bool connected); + void dataReceived(const QVariantMap &data); + +private: + QByteArray encode(const QVariantMap &message); + +private slots: + void connectToHost(); + +private: + QUrl m_url; + QByteArray m_accessToken; + NetworkAccessManager *m_nam = nullptr; + QWebSocket *m_socket = nullptr; + + bool m_waitingForHandshakeReply = false; +}; + +#endif // SIGNALRCONNECTION_H diff --git a/easee/translations/471aa296-78de-4917-84ed-c9a4216f5ae9-en_US.ts b/easee/translations/471aa296-78de-4917-84ed-c9a4216f5ae9-en_US.ts new file mode 100644 index 00000000..35b6623e --- /dev/null +++ b/easee/translations/471aa296-78de-4917-84ed-c9a4216f5ae9-en_US.ts @@ -0,0 +1,157 @@ + + + + + IntegrationPluginEasee + + + Authentication failed. Please try again. + + + + + Unable to contact the easee server. Please try again later. + + + + + Unable to process the response from easee. Please try again later. + + + + + easee + + + Charger ID + The name of the ParamType (ThingClass: charger, Type: thing, ID: {b9f0573e-bf41-45f0-a53e-b2457d51ecb5}) + + + + + Charging + The name of the StateType ({d95137ad-4f32-4ffd-851b-593fc40f5c0c}) of ThingClass charger + + + + + Connected + The name of the StateType ({568d5896-813e-4f66-b430-ed8c8b1ad8c4}) of ThingClass account + + + + + Current charting power + The name of the StateType ({6b0c22e9-cdb4-49a0-9c18-97b776b63c59}) of ThingClass charger + + + + + + Desired phase count + The name of the ParamType (ThingClass: charger, ActionType: desiredPhaseCount, ID: {1baac39e-0e0e-4638-bd8d-09f04fd7bd62}) +---------- +The name of the StateType ({1baac39e-0e0e-4638-bd8d-09f04fd7bd62}) of ThingClass charger + + + + + Logged in + The name of the StateType ({69d01e58-c1a3-4b9c-a4b3-f1de7c67febd}) of ThingClass account + + + + + + Maximum charging current + The name of the ParamType (ThingClass: charger, ActionType: maxChargingCurrent, ID: {1c818574-5aa8-43f5-8fa5-9a620ba86ccc}) +---------- +The name of the StateType ({1c818574-5aa8-43f5-8fa5-9a620ba86ccc}) of ThingClass charger + + + + + Online + The name of the StateType ({2a9b4c84-7b1d-4b32-b26f-84eff54fb04f}) of ThingClass charger + + + + + Plugged in + The name of the StateType ({0ac13ef2-e646-491f-9336-f39d5110f8bb}) of ThingClass charger + + + + + + Power + The name of the ParamType (ThingClass: charger, ActionType: power, ID: {b5070800-3556-4156-aed5-ecbad7fa455d}) +---------- +The name of the StateType ({b5070800-3556-4156-aed5-ecbad7fa455d}) of ThingClass charger + + + + + Session energy + The name of the StateType ({2f0c3491-d524-4ee5-8214-8e3d2f750b66}) of ThingClass charger + + + + + Set desired phase count + The name of the ActionType ({1baac39e-0e0e-4638-bd8d-09f04fd7bd62}) of ThingClass charger + + + + + Set maximum charging current + The name of the ActionType ({1c818574-5aa8-43f5-8fa5-9a620ba86ccc}) of ThingClass charger + + + + + Set power + The name of the ActionType ({b5070800-3556-4156-aed5-ecbad7fa455d}) of ThingClass charger + + + + + Signal strength + The name of the StateType ({7effe12e-2884-4597-ad0c-6b4aa9cdacfd}) of ThingClass charger + + + + + Total energy consumed + The name of the StateType ({b55297a8-f1bd-44eb-8309-691d674ef4a0}) of ThingClass charger + + + + + Used phases + The name of the StateType ({3a68d6c5-d48f-4cd6-b9ca-fc21cbcc8808}) of ThingClass charger + + + + + + easee + The name of the vendor ({e43f66da-4e8c-4e2c-b821-c1eb8ac6bbf8}) +---------- +The name of the plugin easee ({471aa296-78de-4917-84ed-c9a4216f5ae9}) + + + + + easee account + The name of the ThingClass ({2c93c25e-d12a-4709-b537-b5619ab1145a}) + + + + + easee charger + The name of the ThingClass ({9fe4d708-70d2-47bd-8433-54c5f3e8a110}) + + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 94ca9d1e..969f2796 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -19,6 +19,7 @@ PLUGIN_DIRS = \ dht \ dweetio \ dynatrace \ + easee \ elgato \ eq-3 \ espuino \