Merge PR #681: New Plugin: Easee
This commit is contained in:
commit
2d0dcb8c8d
8
debian/control
vendored
8
debian/control
vendored
@ -202,6 +202,14 @@ Description: nymea integration plugin for dynatrace
|
|||||||
This package contains the nymea integration plugin for the dynatrace UFO
|
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
|
Package: nymea-plugin-elgato
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: ${shlibs:Depends},
|
Depends: ${shlibs:Depends},
|
||||||
|
|||||||
2
debian/nymea-plugin-easee.install.in
vendored
Normal file
2
debian/nymea-plugin-easee.install.in
vendored
Normal 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
15
easee/README.md
Normal 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
BIN
easee/easee.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
11
easee/easee.pro
Normal file
11
easee/easee.pro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
include(../plugins.pri)
|
||||||
|
|
||||||
|
QT += network websockets
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
integrationplugineasee.cpp \
|
||||||
|
signalrconnection.cpp
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
integrationplugineasee.h \
|
||||||
|
signalrconnection.h
|
||||||
531
easee/integrationplugineasee.cpp
Normal file
531
easee/integrationplugineasee.cpp
Normal 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());
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
242
easee/integrationplugineasee.h
Normal file
242
easee/integrationplugineasee.h
Normal 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
|
||||||
155
easee/integrationplugineasee.json
Normal file
155
easee/integrationplugineasee.json
Normal 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
13
easee/meta.json
Normal 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
159
easee/signalrconnection.cpp
Normal 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
39
easee/signalrconnection.h
Normal 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
|
||||||
157
easee/translations/471aa296-78de-4917-84ed-c9a4216f5ae9-en_US.ts
Normal file
157
easee/translations/471aa296-78de-4917-84ed-c9a4216f5ae9-en_US.ts
Normal 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>
|
||||||
@ -19,6 +19,7 @@ PLUGIN_DIRS = \
|
|||||||
dht \
|
dht \
|
||||||
dweetio \
|
dweetio \
|
||||||
dynatrace \
|
dynatrace \
|
||||||
|
easee \
|
||||||
elgato \
|
elgato \
|
||||||
eq-3 \
|
eq-3 \
|
||||||
espuino \
|
espuino \
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user