Merge PR #681: New Plugin: Easee
commit
2d0dcb8c8d
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugineasee.so
|
||||
easee/translations/*qm usr/share/nymea/translations/
|
||||
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
include(../plugins.pri)
|
||||
|
||||
QT += network websockets
|
||||
|
||||
SOURCES += \
|
||||
integrationplugineasee.cpp \
|
||||
signalrconnection.cpp
|
||||
|
||||
HEADERS += \
|
||||
integrationplugineasee.h \
|
||||
signalrconnection.h
|
||||
|
|
@ -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());
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "Easeex",
|
||||
"tagline": "Integrates Easee wallboxes with nymea.",
|
||||
"icon": "easee.png",
|
||||
"stability": "consumer",
|
||||
"offline": false,
|
||||
"technologies": [
|
||||
"cloud"
|
||||
],
|
||||
"categories": [
|
||||
"energy"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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 \
|
||||
dweetio \
|
||||
dynatrace \
|
||||
easee \
|
||||
elgato \
|
||||
eq-3 \
|
||||
espuino \
|
||||
|
|
|
|||
Loading…
Reference in New Issue