Easee: Various fixes after beta testing

master
Michael Zanetti 2023-07-06 19:37:39 +02:00
parent 17489941cf
commit 0f79c1d3a1
4 changed files with 151 additions and 72 deletions

View File

@ -40,6 +40,9 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QWebSocket> #include <QWebSocket>
QString apiEndpoint = "https://api.easee.com/api";
QString streamEndpoint = "http://streams.easee.com/hubs/chargers";
IntegrationPluginEasee::IntegrationPluginEasee() IntegrationPluginEasee::IntegrationPluginEasee()
{ {
@ -51,7 +54,7 @@ IntegrationPluginEasee::~IntegrationPluginEasee()
void IntegrationPluginEasee::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) void IntegrationPluginEasee::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
{ {
QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/accounts/login"))); QNetworkRequest request(QUrl(QString("%1/%2").arg(apiEndpoint).arg("accounts/login")));
request.setRawHeader("accept", "application/json"); request.setRawHeader("accept", "application/json");
request.setRawHeader("content-type", "application/*+json"); request.setRawHeader("content-type", "application/*+json");
QVariantMap body; QVariantMap body;
@ -63,10 +66,12 @@ void IntegrationPluginEasee::confirmPairing(ThingPairingInfo *info, const QStrin
qCDebug(dcEasee) << "auth reply finished" << reply->error(); qCDebug(dcEasee) << "auth reply finished" << reply->error();
if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
qCWarning(dcEasee) << "Authentication failed. Looks like a wrong password";
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Authentication failed. Please try again.")); info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Authentication failed. Please try again."));
return; return;
} }
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcEasee) << "Unable to connect to the Easee server";
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to contact the easee server. Please try again later.")); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to contact the easee server. Please try again later."));
return; return;
} }
@ -153,10 +158,11 @@ void IntegrationPluginEasee::setupThing(ThingSetupInfo *info)
} }
if (thing->thingClassId() == chargerThingClassId) { if (thing->thingClassId() == chargerThingClassId) {
refreshCurrentState(thing); // We'll need a cache of the maxChargingCurrent as sometimes need that before a executeAction is finished
// initializing it to make sure there's always a value in it.
m_desiredMax[info->thing()] = thing->stateValue(chargerMaxChargingCurrentStateTypeId).toUInt();
} }
info->finish(Thing::ThingErrorNoError); info->finish(Thing::ThingErrorNoError);
} }
@ -181,16 +187,6 @@ void IntegrationPluginEasee::postSetupThing(Thing *thing)
// Refreshing the products // Refreshing the products
refreshProducts(t); 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);
} }
} }
}); });
@ -206,14 +202,15 @@ void IntegrationPluginEasee::postSetupThing(Thing *thing)
qCDebug(dcEasee()) << "Access token:" << accessToken; qCDebug(dcEasee()) << "Access token:" << accessToken;
qCDebug(dcEasee()) << "Token expiry:" << expiry; qCDebug(dcEasee()) << "Token expiry:" << expiry;
SignalRConnection *signalR = new SignalRConnection(QUrl("http://streams.easee.com/hubs/chargers"), accessToken, hardwareManager()->networkManager(), thing); SignalRConnection *signalR = new SignalRConnection(QUrl(streamEndpoint), accessToken, hardwareManager()->networkManager(), thing);
m_signalRConnections.insert(thing, signalR); m_signalRConnections.insert(thing, signalR);
connect(signalR, &SignalRConnection::connectionStateChanged, thing, [=](bool connected){ connect(signalR, &SignalRConnection::connectionStateChanged, thing, [=](bool connected){
foreach (Thing *charger, myThings().filterByParentId(thing->id())) { foreach (Thing *charger, myThings().filterByParentId(thing->id())) {
charger->setStateValue(chargerConnectedStateTypeId, true);
if (connected) { if (connected) {
signalR->subscribe(charger->paramValue(chargerThingIdParamTypeId).toString()); signalR->subscribe(charger->paramValue(chargerThingIdParamTypeId).toString());
} else {
charger->setStateValue(chargerConnectedStateTypeId, false);
} }
} }
}); });
@ -256,12 +253,11 @@ void IntegrationPluginEasee::postSetupThing(Thing *thing)
charger->setStateValue(chargerPluggedInStateTypeId, false); charger->setStateValue(chargerPluggedInStateTypeId, false);
} else if (mode == "B" || mode == "C") { } else if (mode == "B" || mode == "C") {
charger->setStateValue(chargerPluggedInStateTypeId, true); charger->setStateValue(chargerPluggedInStateTypeId, true);
} else {
} }
break; break;
} }
case ObservationPointOutputPhase: case ObservationPointOutputPhase:
charger->setStateValue(chargerPhaseCountStateTypeId, value.toUInt() > 10 ? 3 : 1); charger->setStateValue(chargerPhaseCountStateTypeId, value.toUInt() == 30 ? 3 : 1);
break; break;
case ObservationPointChargerOpMode: case ObservationPointChargerOpMode:
// 2: charging disabled, 3: enabled and charging, 4: enabled but not charging // 2: charging disabled, 3: enabled and charging, 4: enabled but not charging
@ -269,11 +265,25 @@ void IntegrationPluginEasee::postSetupThing(Thing *thing)
charger->setStateValue(chargerPowerStateTypeId, value.toUInt() >= 3); charger->setStateValue(chargerPowerStateTypeId, value.toUInt() >= 3);
break; break;
case ObservationPointDynamicChargerCurrent: case ObservationPointDynamicChargerCurrent:
charger->setStateValue(chargerMaxChargingCurrentStateTypeId, value.toUInt()); // May give us 0 when pausing charging etc, ignoring that.
if (value.toUInt() > 0) {
charger->setStateValue(chargerMaxChargingCurrentStateTypeId, value.toUInt());
// Updating the desired value when it is changed by the wallbox (e.g. through app)
m_desiredMax[charger] = value.toUInt();
}
break; break;
case ObservationPointMaxChargerCurrent: case ObservationPointMaxChargerCurrent:
charger->setStateMaxValue(chargerMaxChargingCurrentStateTypeId, value.toUInt()); m_wallboxMax[chargerId] = value.toUInt();
charger->setStateMinMaxValues(chargerMaxChargingCurrentStateTypeId, 6, qMin(m_wallboxMax.value(chargerId), m_cableRating.value(chargerId, 32)));
break; break;
case ObservationPointCableRating:
m_cableRating[chargerId] = value.toUInt();
charger->setStateMinMaxValues(chargerMaxChargingCurrentStateTypeId, 6, qMin(m_wallboxMax.value(chargerId, 32), value.toUInt()));
break;
case ObservationPointConnectedToCloud:
charger->setStateValue(chargerConnectedStateTypeId, value.toString() == "True" || value.toString() == "1");
break;
default: default:
break; break;
@ -293,6 +303,10 @@ void IntegrationPluginEasee::thingRemoved(Thing *thing)
hardwareManager()->pluginTimerManager()->unregisterTimer(m_timer); hardwareManager()->pluginTimerManager()->unregisterTimer(m_timer);
m_timer = nullptr; m_timer = nullptr;
} }
if (thing->thingClassId() == chargerThingClassId) {
m_desiredMax.take(thing);
}
} }
void IntegrationPluginEasee::executeAction(ThingActionInfo *info) void IntegrationPluginEasee::executeAction(ThingActionInfo *info)
@ -303,22 +317,34 @@ void IntegrationPluginEasee::executeAction(ThingActionInfo *info)
QString chargerId = thing->paramValue(chargerThingIdParamTypeId).toString(); QString chargerId = thing->paramValue(chargerThingIdParamTypeId).toString();
if (info->action().actionTypeId() == chargerPowerActionTypeId) { if (info->action().actionTypeId() == chargerPowerActionTypeId) {
bool power = info->action().paramValue(chargerPowerActionPowerParamTypeId).toBool(); bool power = info->action().paramValue(chargerPowerActionPowerParamTypeId).toBool();
QString actionPath = power ? "start_charging" : "stop_charging"; QString actionPath = power ? "start_charging" : "pause_charging";
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/commands/%2").arg(chargerId).arg(actionPath)); QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/commands/%2").arg(chargerId).arg(actionPath));
qCDebug(dcEasee()) << "Setting power:" << request.url().toString(); qCDebug(dcEasee()) << "Setting power:" << request.url().toString();
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray()); QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info, power](){ connect(reply, &QNetworkReply::finished, info, [=](){
qCDebug(dcEasee()) << "Reply" << reply->error(); qCDebug(dcEasee()) << "Reply" << reply->error();
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
info->thing()->setStateValue(chargerPowerStateTypeId, power); info->thing()->setStateValue(chargerPowerStateTypeId, power);
} }
info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure);
// resume/start charging will for some reason reset the dynamicChargerCurrent... We'll have to re-write ours.
if (power) {
uint maxChargingCurrent = m_desiredMax[info->thing()];
QVariantMap data;
data.insert("dynamicChargerCurrent", maxChargingCurrent);
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId));
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
}
}); });
return; return;
} }
if (info->action().actionTypeId() == chargerMaxChargingCurrentActionTypeId) { if (info->action().actionTypeId() == chargerMaxChargingCurrentActionTypeId) {
uint maxChargingCurrent = info->action().paramValue(chargerMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); uint maxChargingCurrent = info->action().paramValue(chargerMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt();
// We'll need this for resume_charging as that call will for some reason reset it to the max of 32A, so we'll need to immediately write this one again.
m_desiredMax[info->thing()] = maxChargingCurrent;
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId)); QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId));
QVariantMap data; QVariantMap data;
data.insert("dynamicChargerCurrent", maxChargingCurrent); data.insert("dynamicChargerCurrent", maxChargingCurrent);
@ -326,7 +352,7 @@ void IntegrationPluginEasee::executeAction(ThingActionInfo *info)
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact)); QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info, maxChargingCurrent](){ connect(reply, &QNetworkReply::finished, info, [reply, info, maxChargingCurrent](){
qCDebug(dcEasee()) << "Reply" << reply->error(); qCDebug(dcEasee()) << "Set dynamicaChargerCurrent reply" << reply->error();
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
info->thing()->setStateValue(chargerMaxChargingCurrentStateTypeId, maxChargingCurrent); info->thing()->setStateValue(chargerMaxChargingCurrentStateTypeId, maxChargingCurrent);
} }
@ -335,20 +361,54 @@ void IntegrationPluginEasee::executeAction(ThingActionInfo *info)
return; return;
} }
if (info->action().actionTypeId() == chargerDesiredPhaseCountActionTypeId) { if (info->action().actionTypeId() == chargerDesiredPhaseCountActionTypeId) {
uint desiredPhaseCount = info->action().paramValue(chargerMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); uint desiredPhaseCount = info->action().paramValue(chargerDesiredPhaseCountActionDesiredPhaseCountParamTypeId).toUInt();
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId)); bool wasOn = thing->stateValue(chargerPowerStateTypeId).toBool();
QVariantMap data; bool oldMaxCurrent = m_desiredMax.value(info->thing());
data.insert("lockToSinglePhaseCharging", desiredPhaseCount == 1); if (desiredPhaseCount == thing->stateValue(chargerPhaseCountStateTypeId)) {
qCDebug(dcEasee()) << "Setting single phase charging:" << request.url().toString() << QJsonDocument::fromVariant(data).toJson(); qCInfo(dcEasee()) << "effective phases already equals desired ones...";
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact)); info->finish(Thing::ThingErrorNoError);
return;
}
qCDebug(dcEasee()) << "Pausing charging";
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/commands/pause_charging").arg(chargerId));
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info, desiredPhaseCount](){ connect(reply, &QNetworkReply::finished, info, [=](){
qCDebug(dcEasee()) << "Reply" << reply->error();
if (reply->error() == QNetworkReply::NoError) { QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId));
info->thing()->setStateValue(chargerDesiredPhaseCountStateTypeId, desiredPhaseCount);
} QVariantMap data;
info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); data.insert("phaseMode", desiredPhaseCount == 1 ? PhaseModeLockedTo1Phase : PhaseModeLockedTo3Phase);
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, [=](){
qCDebug(dcEasee()) << "Set phaseMode reply" << reply->error();
if (reply->error() == QNetworkReply::NoError) {
info->thing()->setStateValue(chargerDesiredPhaseCountStateTypeId, desiredPhaseCount);
}
info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure);
if (wasOn) {
qCDebug(dcEasee()) << "Resuming charging";
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/commands/resume_charging").arg(chargerId));
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [=](){
qCDebug(dcEasee()) << "Restoring max charger current";
QNetworkRequest request = createRequest(parentThing, QString("chargers/%1/settings").arg(chargerId));
QVariantMap data;
data.insert("dynamicChargerCurrent", oldMaxCurrent);
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);
});
}
});
}); });
return; return;
} }
@ -361,7 +421,7 @@ QNetworkRequest IntegrationPluginEasee::createRequest(Thing *thing, const QStrin
pluginStorage()->beginGroup(thing->id().toString()); pluginStorage()->beginGroup(thing->id().toString());
QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray(); QByteArray accessToken = pluginStorage()->value("accessToken").toByteArray();
pluginStorage()->endGroup(); pluginStorage()->endGroup();
QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/%1").arg(endpoint))); QNetworkRequest request(QUrl(QString("%1/%2").arg(apiEndpoint).arg(endpoint)));
request.setRawHeader("Authorization", "Bearer " + accessToken); request.setRawHeader("Authorization", "Bearer " + accessToken);
request.setRawHeader("accept", "application/json"); request.setRawHeader("accept", "application/json");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/*+json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/*+json");
@ -379,20 +439,21 @@ QNetworkReply *IntegrationPluginEasee::refreshToken(Thing *thing)
// FIXME: Ideally we should use the refresh_token API and not store user/pass in the config, but it seems to not work // 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"))); // QNetworkRequest request(QUrl(QString("%1/%2").arg(apiEndpoint).arg("accounts/refresh_token")));
// request.setRawHeader("accept", "application/json"); // request.setRawHeader("accept", "application/json");
// request.setRawHeader("content-type", "application/*+json"); // request.setRawHeader("content-type", "application/*+json");
// QVariantMap body; // QVariantMap body;
// body.insert("refreshToken", refreshToken); // body.insert("refreshToken", refreshToken);
// body.insert("accessToken", accessToken); // body.insert("accessToken", accessToken);
QNetworkRequest request(QUrl(QString("https://api.easee.cloud/api/accounts/login"))); QNetworkRequest request(QUrl(QString("%1/%2").arg(apiEndpoint).arg("accounts/login")));
request.setRawHeader("accept", "application/json"); request.setRawHeader("accept", "application/json");
request.setRawHeader("content-type", "application/*+json"); request.setRawHeader("content-type", "application/*+json");
QVariantMap body; QVariantMap body;
body.insert("userName", username); body.insert("userName", username);
body.insert("password", password); body.insert("password", password);
qCDebug(dcEasee()) << "Fetching:" << request.url().toString();
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(body).toJson(QJsonDocument::Compact)); QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(body).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
@ -424,7 +485,9 @@ QNetworkReply *IntegrationPluginEasee::refreshToken(Thing *thing)
pluginStorage()->setValue("refreshToken", refreshToken); pluginStorage()->setValue("refreshToken", refreshToken);
pluginStorage()->endGroup(); pluginStorage()->endGroup();
m_signalRConnections.value(thing)->updateToken(accessToken); if (m_signalRConnections.contains(thing)) {
m_signalRConnections.value(thing)->updateToken(accessToken);
}
}); });
@ -506,24 +569,6 @@ void IntegrationPluginEasee::refreshCurrentState(Thing *charger)
QVariantMap map = jsonDoc.toVariant().toMap(); QVariantMap map = jsonDoc.toVariant().toMap();
qCDebug(dcEasee) << "Charger state reply:" << qUtf8Printable(jsonDoc.toJson()); qCDebug(dcEasee) << "Charger state reply:" << qUtf8Printable(jsonDoc.toJson());
charger->setStateValue(chargerConnectedStateTypeId, map.value("isOnline").toBool()); charger->setStateValue(chargerConnectedStateTypeId, map.value("isOnline").toBool());
charger->setStateValue(chargerSignalStrengthStateTypeId, qMax(0, qMin(100, (map.value("wiFiRSSI").toInt() + 100) * 2)));
charger->setStateValue(chargerCurrentPowerStateTypeId, map.value("totalPower").toDouble() * 1000);
charger->setStateValue(chargerPhaseCountStateTypeId, map.value("outputPhase").toUInt() > 10 ? 3 : 1);
charger->setStateValue(chargerChargingStateTypeId, map.value("chargerOpMode").toUInt() == 3);
// 1: unplugged, 2: charging disabled, 3: enabled and charging, 4: enabled but not charging
uint chargerOpMode = map.value("chargerOpMode").toUInt();
charger->setStateValue(chargerPluggedInStateTypeId, chargerOpMode >= 2);
charger->setStateValue(chargerChargingStateTypeId, chargerOpMode == 3);
charger->setStateValue(chargerPowerStateTypeId, chargerOpMode >= 3);
charger->setStateValue(chargerMaxChargingCurrentStateTypeId, map.value("dynamicChargerCurrent").toUInt());
charger->setStateMaxValue(chargerMaxChargingCurrentStateTypeId, 6); // Fixme: where to get this from?
charger->setStateMaxValue(chargerMaxChargingCurrentStateTypeId, 32); // Fixme: where to get this from?
charger->setStateValue(chargerTotalEnergyConsumedStateTypeId, map.value("lifetimeEnergy").toDouble());
charger->setStateValue(chargerSessionEnergyStateTypeId, map.value("sessionEnergy").toDouble());
}); });

View File

@ -212,10 +212,20 @@ public:
ObservationPointLTERSRQ = 222, ObservationPointLTERSRQ = 222,
ObservationPointEQAvailableCurrentP1 = 230, ObservationPointEQAvailableCurrentP1 = 230,
ObservationPointEQAvailableCurrentP2 = 231, ObservationPointEQAvailableCurrentP2 = 231,
ObservationPointEQAvailableCurrentP3 = 232 ObservationPointEQAvailableCurrentP3 = 232,
ObservationPointConnectedToCloud = 250
}; };
Q_ENUM(ObservationPoint) Q_ENUM(ObservationPoint)
enum PhaseMode {
PhaseModeIgnore = 0,
PhaseModeLockedTo1Phase = 1,
PhaseModeAuto = 2,
PhaseModeLockedTo3Phase = 3
};
Q_ENUM(PhaseMode)
explicit IntegrationPluginEasee(); explicit IntegrationPluginEasee();
~IntegrationPluginEasee(); ~IntegrationPluginEasee();
@ -237,6 +247,10 @@ private:
QHash<QString, uint> m_siteIds; // chargerId, siteId QHash<QString, uint> m_siteIds; // chargerId, siteId
PluginTimer *m_timer = nullptr; PluginTimer *m_timer = nullptr;
QHash<QString,uint> m_cableRating;
QHash<QString,uint> m_wallboxMax;
QHash<Thing*,uint> m_desiredMax;
}; };
#endif // INTEGRATIONPLUGINEASEE_H #endif // INTEGRATIONPLUGINEASEE_H

View File

@ -18,13 +18,13 @@ SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessTo
m_socket = new QWebSocket(); m_socket = new QWebSocket();
typedef void (QWebSocket:: *errorSignal)(QAbstractSocket::SocketError); typedef void (QWebSocket:: *errorSignal)(QAbstractSocket::SocketError);
connect(m_socket, static_cast<errorSignal>(&QWebSocket::error), this, [](QAbstractSocket::SocketError error){ connect(m_socket, static_cast<errorSignal>(&QWebSocket::error), this, [](QAbstractSocket::SocketError error){
qCWarning(dcEasee) << "Error in websocket:" << error; qCWarning(dcEasee) << "SingalR: Error in websocket:" << error;
}); });
connect(m_socket, &QWebSocket::stateChanged, this, [=](QAbstractSocket::SocketState state){ connect(m_socket, &QWebSocket::stateChanged, this, [=](QAbstractSocket::SocketState state){
qCDebug(dcEasee) << "Websocket state changed" << state; qCDebug(dcEasee) << "SingalR: Websocket state changed" << state;
if (state == QAbstractSocket::ConnectedState) { if (state == QAbstractSocket::ConnectedState) {
qCDebug(dcEasee) << "Websocket connected"; qCDebug(dcEasee) << "SingalR: Websocket connected";
QVariantMap handshake; QVariantMap handshake;
handshake.insert("protocol", "json"); handshake.insert("protocol", "json");
@ -32,7 +32,9 @@ SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessTo
QByteArray data = encode(handshake); QByteArray data = encode(handshake);
qCDebug(dcEasee) << "Sending handshake" << data; qCDebug(dcEasee) << "Sending handshake" << data;
m_socket->sendTextMessage(data); m_socket->sendTextMessage(data);
m_watchdog->start();
} else if (QAbstractSocket::UnconnectedState) { } else if (QAbstractSocket::UnconnectedState) {
m_watchdog->stop();
QTimer::singleShot(5000, this, [=](){ QTimer::singleShot(5000, this, [=](){
connectToHost(); connectToHost();
}); });
@ -40,9 +42,11 @@ SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessTo
}); });
connect(m_socket, &QWebSocket::binaryMessageReceived, this, [](const QByteArray &message){ connect(m_socket, &QWebSocket::binaryMessageReceived, this, [](const QByteArray &message){
qCDebug(dcEasee) << "Binary message received" << message; qCDebug(dcEasee) << "SingalR: Binary message received" << message;
}); });
connect(m_socket, &QWebSocket::textMessageReceived, this, [=](const QString &message){ connect(m_socket, &QWebSocket::textMessageReceived, this, [=](const QString &message){
qCDebug(dcEasee) << "SingalR: Text message received" << message;
QStringList messages = message.split(QByteArray::fromHex("1E")); QStringList messages = message.split(QByteArray::fromHex("1E"));
foreach (const QString &msg, messages) { foreach (const QString &msg, messages) {
@ -54,13 +58,13 @@ SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessTo
QJsonParseError error; QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(msg.toUtf8(), &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(msg.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCWarning(dcEasee()) << "Unable to parse message from SignalR socket" << error.errorString() << msg; qCWarning(dcEasee()) << "SingalR: Unable to parse message from SignalR socket" << error.errorString() << msg;
continue; continue;
} }
if (m_waitingForHandshakeReply && jsonDoc.toVariant().toMap().isEmpty()) { if (m_waitingForHandshakeReply && jsonDoc.toVariant().toMap().isEmpty()) {
m_waitingForHandshakeReply = false; m_waitingForHandshakeReply = false;
qCDebug(dcEasee()) << "Handshake reply received."; qCDebug(dcEasee()) << "SingalR: Handshake reply received.";
emit connectionStateChanged(true); emit connectionStateChanged(true);
return; return;
} }
@ -72,17 +76,27 @@ SignalRConnection::SignalRConnection(const QUrl &url, const QByteArray &accessTo
break; break;
case 3: case 3:
// Silencing acks to our requests // Silencing acks to our requests
qCDebug(dcEasee()) << "Message ACK received:" << map; qCDebug(dcEasee()) << "SingalR: Message ACK received:" << map;
break;
case 6: case 6:
// Silencing pings // Resetting watchdog
m_watchdog->start();
break; break;
default: default:
qCWarning(dcEasee()) << "Unhandled signalr message type" << map; qCWarning(dcEasee()) << "SingalR: Unhandled SingalR message type" << map;
} }
} }
}); });
connectToHost(); connectToHost();
m_watchdog = new QTimer(this);
m_watchdog->setInterval(30000);
connect(m_watchdog, &QTimer::timeout, this, [=](){
qCWarning(dcEasee()) << "SingalR: Watchdog triggered! Reconnecting web socket stream...";
m_socket->close();
connectToHost();
});
} }
void SignalRConnection::subscribe(const QString &chargerId) void SignalRConnection::subscribe(const QString &chargerId)
@ -92,7 +106,7 @@ void SignalRConnection::subscribe(const QString &chargerId)
map.insert("invocationId", QUuid::createUuid()); map.insert("invocationId", QUuid::createUuid());
map.insert("target", "SubscribeWithCurrentState"); map.insert("target", "SubscribeWithCurrentState");
map.insert("arguments", QVariantList{chargerId, true}); map.insert("arguments", QVariantList{chargerId, true});
qCDebug(dcEasee) << "subscribing to" << chargerId; qCDebug(dcEasee) << "SingalR: subscribing to" << chargerId;
m_socket->sendTextMessage(encode(map)); m_socket->sendTextMessage(encode(map));
} }
@ -104,6 +118,8 @@ bool SignalRConnection::connected() const
void SignalRConnection::updateToken(const QByteArray &accessToken) void SignalRConnection::updateToken(const QByteArray &accessToken)
{ {
m_accessToken = accessToken; m_accessToken = accessToken;
m_socket->close();
connectToHost();
} }
QByteArray SignalRConnection::encode(const QVariantMap &message) QByteArray SignalRConnection::encode(const QVariantMap &message)
@ -118,19 +134,21 @@ void SignalRConnection::connectToHost()
negotiationUrl.setPath(negotiationUrl.path() + "/negotiate"); negotiationUrl.setPath(negotiationUrl.path() + "/negotiate");
QNetworkRequest negotiateRequest(negotiationUrl); QNetworkRequest negotiateRequest(negotiationUrl);
negotiateRequest.setRawHeader("Authorization", "Bearer " + m_accessToken); negotiateRequest.setRawHeader("Authorization", "Bearer " + m_accessToken);
qCDebug(dcEasee()) << "Negotiating:" << negotiationUrl << negotiateRequest.rawHeader("Authorization"); qCDebug(dcEasee()) << "SingalR: Negotiating:" << negotiationUrl << negotiateRequest.rawHeader("Authorization");
QNetworkReply *negotiantionReply = m_nam->post(negotiateRequest, QByteArray()); QNetworkReply *negotiantionReply = m_nam->post(negotiateRequest, QByteArray());
connect(negotiantionReply, &QNetworkReply::finished, this, [=](){ connect(negotiantionReply, &QNetworkReply::finished, this, [=](){
if (negotiantionReply->error() != QNetworkReply::NoError) { if (negotiantionReply->error() != QNetworkReply::NoError) {
qCWarning(dcEasee()) << "Unable to neotiate SignalR channel:" << negotiantionReply->error(); qCWarning(dcEasee()) << "SingalR: Unable to neotiate SignalR channel:" << negotiantionReply->error();
QTimer::singleShot(5000, this, [=](){connectToHost();});
return; return;
} }
QByteArray data = negotiantionReply->readAll(); QByteArray data = negotiantionReply->readAll();
qCDebug(dcEasee) << "Negotiation reply" << data; qCDebug(dcEasee) << "SingalR: Negotiation reply" << data;
QJsonParseError error; QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCWarning(dcEasee()) << "Unable to parse json from negoatiate endpoint" << error.errorString() << data; qCWarning(dcEasee()) << "SingalR: Unable to parse json from negoatiate endpoint" << error.errorString() << data;
QTimer::singleShot(5000, this, [=](){connectToHost();});
return; return;
} }
@ -145,7 +163,7 @@ void SignalRConnection::connectToHost()
wsUrl.setQuery(query); wsUrl.setQuery(query);
QNetworkRequest request(wsUrl); QNetworkRequest request(wsUrl);
request.setRawHeader("Authorization", "Bearer " + m_accessToken); request.setRawHeader("Authorization", "Bearer " + m_accessToken);
qCDebug(dcEasee()) << "Connecting websocket:" << wsUrl.toString(); qCDebug(dcEasee()) << "SingalR: Connecting websocket:" << wsUrl.toString();
m_waitingForHandshakeReply = true; m_waitingForHandshakeReply = true;
#if QT_VERSION >= QT_VERSION_CHECK(5,6,0) #if QT_VERSION >= QT_VERSION_CHECK(5,6,0)
m_socket->open(request); m_socket->open(request);

View File

@ -4,6 +4,7 @@
#include <QObject> #include <QObject>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QWebSocket> #include <QWebSocket>
#include <QTimer>
#include <network/networkaccessmanager.h> #include <network/networkaccessmanager.h>
class SignalRConnection : public QObject class SignalRConnection : public QObject
@ -32,6 +33,7 @@ private:
QByteArray m_accessToken; QByteArray m_accessToken;
NetworkAccessManager *m_nam = nullptr; NetworkAccessManager *m_nam = nullptr;
QWebSocket *m_socket = nullptr; QWebSocket *m_socket = nullptr;
QTimer *m_watchdog = nullptr;
bool m_waitingForHandshakeReply = false; bool m_waitingForHandshakeReply = false;
}; };