Merge PR #681: New Plugin: Easee

master
jenkins 2023-05-16 12:59:06 +02:00
commit 2d0dcb8c8d
13 changed files with 1333 additions and 0 deletions

8
debian/control vendored
View File

@ -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},

2
debian/nymea-plugin-easee.install.in vendored Normal file
View File

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

15
easee/README.md Normal file
View File

@ -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.

BIN
easee/easee.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

11
easee/easee.pro Normal file
View File

@ -0,0 +1,11 @@
include(../plugins.pri)
QT += network websockets
SOURCES += \
integrationplugineasee.cpp \
signalrconnection.cpp
HEADERS += \
integrationplugineasee.h \
signalrconnection.h

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationplugineasee.h"
#include "plugininfo.h"
#include "signalrconnection.h"
#include <network/networkaccessmanager.h>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QWebSocket>
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<ObservationPoint>(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());
});
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINEASEE_H
#define INTEGRATIONPLUGINEASEE_H
#include <integrations/integrationplugin.h>
#include <plugintimer.h>
#include "extern-plugininfo.h"
#include <QTimer>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QWebSocket>
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<Thing*, SignalRConnection*> m_signalRConnections;
QHash<QString, uint> m_circuitIds; // chargerId, circuitId
QHash<QString, uint> m_siteIds; // chargerId, siteId
PluginTimer *m_timer = nullptr;
};
#endif // INTEGRATIONPLUGINEASEE_H

View File

@ -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
}
]
}
]
}
]
}

13
easee/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "Easeex",
"tagline": "Integrates Easee wallboxes with nymea.",
"icon": "easee.png",
"stability": "consumer",
"offline": false,
"technologies": [
"cloud"
],
"categories": [
"energy"
]
}

159
easee/signalrconnection.cpp Normal file
View File

@ -0,0 +1,159 @@
#include "signalrconnection.h"
#include <QNetworkRequest>
#include <QWebSocket>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QTimer>
#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<errorSignal>(&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
});
}

39
easee/signalrconnection.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef SIGNALRCONNECTION_H
#define SIGNALRCONNECTION_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QWebSocket>
#include <network/networkaccessmanager.h>
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

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginEasee</name>
<message>
<location filename="../integrationplugineasee.cpp" line="67"/>
<source>Authentication failed. Please try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationplugineasee.cpp" line="71"/>
<source>Unable to contact the easee server. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationplugineasee.cpp" line="80"/>
<source>Unable to process the response from easee. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>easee</name>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="49"/>
<source>Charger ID</source>
<extracomment>The name of the ParamType (ThingClass: charger, Type: thing, ID: {b9f0573e-bf41-45f0-a53e-b2457d51ecb5})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="52"/>
<source>Charging</source>
<extracomment>The name of the StateType ({d95137ad-4f32-4ffd-851b-593fc40f5c0c}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="55"/>
<source>Connected</source>
<extracomment>The name of the StateType ({568d5896-813e-4f66-b430-ed8c8b1ad8c4}) of ThingClass account</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="58"/>
<source>Current charting power</source>
<extracomment>The name of the StateType ({6b0c22e9-cdb4-49a0-9c18-97b776b63c59}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="61"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="64"/>
<source>Desired phase count</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="67"/>
<source>Logged in</source>
<extracomment>The name of the StateType ({69d01e58-c1a3-4b9c-a4b3-f1de7c67febd}) of ThingClass account</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="70"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="73"/>
<source>Maximum charging current</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="76"/>
<source>Online</source>
<extracomment>The name of the StateType ({2a9b4c84-7b1d-4b32-b26f-84eff54fb04f}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="79"/>
<source>Plugged in</source>
<extracomment>The name of the StateType ({0ac13ef2-e646-491f-9336-f39d5110f8bb}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="82"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="85"/>
<source>Power</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="88"/>
<source>Session energy</source>
<extracomment>The name of the StateType ({2f0c3491-d524-4ee5-8214-8e3d2f750b66}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="91"/>
<source>Set desired phase count</source>
<extracomment>The name of the ActionType ({1baac39e-0e0e-4638-bd8d-09f04fd7bd62}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="94"/>
<source>Set maximum charging current</source>
<extracomment>The name of the ActionType ({1c818574-5aa8-43f5-8fa5-9a620ba86ccc}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="97"/>
<source>Set power</source>
<extracomment>The name of the ActionType ({b5070800-3556-4156-aed5-ecbad7fa455d}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="100"/>
<source>Signal strength</source>
<extracomment>The name of the StateType ({7effe12e-2884-4597-ad0c-6b4aa9cdacfd}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="103"/>
<source>Total energy consumed</source>
<extracomment>The name of the StateType ({b55297a8-f1bd-44eb-8309-691d674ef4a0}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="106"/>
<source>Used phases</source>
<extracomment>The name of the StateType ({3a68d6c5-d48f-4cd6-b9ca-fc21cbcc8808}) of ThingClass charger</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="109"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="112"/>
<source>easee</source>
<extracomment>The name of the vendor ({e43f66da-4e8c-4e2c-b821-c1eb8ac6bbf8})
----------
The name of the plugin easee ({471aa296-78de-4917-84ed-c9a4216f5ae9})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="115"/>
<source>easee account</source>
<extracomment>The name of the ThingClass ({2c93c25e-d12a-4709-b537-b5619ab1145a})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/easee/plugininfo.h" line="118"/>
<source>easee charger</source>
<extracomment>The name of the ThingClass ({9fe4d708-70d2-47bd-8433-54c5f3e8a110})</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -19,6 +19,7 @@ PLUGIN_DIRS = \
dht \
dweetio \
dynatrace \
easee \
elgato \
eq-3 \
espuino \