From 094f2e14effc223a60952ed1602a2b8a1444b7e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 21 Jul 2022 10:08:32 +0200 Subject: [PATCH 01/11] Add goe discovery and prepare V2 integration --- goecharger/goecharger.pro | 2 + goecharger/goediscovery.cpp | 273 ++++++ goecharger/goediscovery.h | 93 ++ goecharger/integrationplugingoecharger.cpp | 994 +++++++++++++------- goecharger/integrationplugingoecharger.h | 53 +- goecharger/integrationplugingoecharger.json | 98 +- 6 files changed, 1093 insertions(+), 420 deletions(-) create mode 100644 goecharger/goediscovery.cpp create mode 100644 goecharger/goediscovery.h diff --git a/goecharger/goecharger.pro b/goecharger/goecharger.pro index a8122ee7..75764686 100644 --- a/goecharger/goecharger.pro +++ b/goecharger/goecharger.pro @@ -5,7 +5,9 @@ QT += network PKGCONFIG += nymea-mqtt SOURCES += \ + goediscovery.cpp \ integrationplugingoecharger.cpp \ HEADERS += \ + goediscovery.h \ integrationplugingoecharger.h \ diff --git a/goecharger/goediscovery.cpp b/goecharger/goediscovery.cpp new file mode 100644 index 00000000..1c98e288 --- /dev/null +++ b/goecharger/goediscovery.cpp @@ -0,0 +1,273 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "goediscovery.h" +#include "extern-plugininfo.h" + +#include +#include + +GoeDiscovery::GoeDiscovery(NetworkAccessManager *networkAccessManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject(parent), + m_networkAccessManager(networkAccessManager), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ + m_gracePeriodTimer.setSingleShot(true); + m_gracePeriodTimer.setInterval(3000); + connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ + qCDebug(dcGoECharger()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); +} + +GoeDiscovery::~GoeDiscovery() +{ + qCDebug(dcGoECharger()) << "Discovery: destroy discovery object"; + cleanupPendingReplies(); +} + +void GoeDiscovery::startDiscovery() +{ + // Clean up + m_discoveryResults.clear(); + m_verifiedNetworkDeviceInfos.clear(); + m_gracePeriodTimer.stop(); + + m_startDateTime = QDateTime::currentDateTime(); + + qCInfo(dcGoECharger()) << "Discovery: Start discovering the network..."; + m_discoveryReply = m_networkDeviceDiscovery->discover(); + + // Check if all network device infos which might already be discovered here to save time... + foreach (const NetworkDeviceInfo &networkDeviceInfo, m_discoveryReply->networkDeviceInfos()) { + checkNetworkDevice(networkDeviceInfo); + } + + // Test any network device beeing discovered + connect(m_discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &GoeDiscovery::checkNetworkDevice); + + // When the network discovery has finished, we process the rest and give some time to finish the pending replies + connect(m_discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + // The network device discovery is done + m_discoveredNetworkDeviceInfos = m_discoveryReply->networkDeviceInfos(); + m_discoveryReply = nullptr; + + // Check if all network device infos have been verified + foreach (const NetworkDeviceInfo &networkDeviceInfo, m_discoveredNetworkDeviceInfos) { + if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) + continue; + + checkNetworkDevice(networkDeviceInfo); + } + + // If there might be some response after the grace period time, + // we don't care any more since there might just waiting for some timeouts... + // If there would be a device, it would have responded. + m_gracePeriodTimer.start(); + }); +} + +QList GoeDiscovery::discoveryResults() const +{ + return m_discoveryResults.values(); +} + +QNetworkRequest GoeDiscovery::buildRequestV1(const QHostAddress &address) +{ + QUrl requestUrl; + requestUrl.setScheme("http"); + requestUrl.setHost(address.toString()); + requestUrl.setPath("/status"); + + return QNetworkRequest(requestUrl); +} + +QNetworkRequest GoeDiscovery::buildRequestV2(const QHostAddress &address) +{ + QUrl requestUrl; + requestUrl.setScheme("http"); + requestUrl.setHost(address.toString()); + requestUrl.setPath("/api/status"); + + return QNetworkRequest(requestUrl); +} + +void GoeDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo) +{ + // Make sure we have not checked this host yet + if (m_verifiedNetworkDeviceInfos.contains(networkDeviceInfo)) + return; + + qCDebug(dcGoECharger()) << "Discovery: Start inspecting" << networkDeviceInfo.address().toString(); + checkNetworkDeviceApiV2(networkDeviceInfo); + checkNetworkDeviceApiV1(networkDeviceInfo); + + m_verifiedNetworkDeviceInfos.append(networkDeviceInfo); +} + +void GoeDiscovery::checkNetworkDeviceApiV1(const NetworkDeviceInfo &networkDeviceInfo) +{ + // First check if API V1 is available: http:///status + QNetworkReply *reply = m_networkAccessManager->get(buildRequestV1(networkDeviceInfo.address())); + m_pendingReplies.append(reply); + connect(reply, &QNetworkReply::finished, this, [=](){ + m_pendingReplies.removeAll(reply); + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V1 verification HTTP error" << reply->errorString() << "Continue..."; + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V1 verification invalid JSON data. Continue..."; + return; + } + + // Verify if we have the required values in the response map + // https://github.com/goecharger/go-eCharger-API-v1/blob/master/go-eCharger%20API%20v1%20EN.md + QVariantMap responseMap = jsonDoc.toVariant().toMap(); + if (responseMap.contains("fwv") && responseMap.contains("sse") && responseMap.contains("nrg") && responseMap.contains("amp")) { + // Looks like we have found a go-e V1 api endpoint, nice + qCDebug(dcGoECharger()) << "Discovery: --> Found API V1 on" << networkDeviceInfo.address().toString(); + + if (m_discoveryResults.contains(networkDeviceInfo.address())) { + // We use the information from API V2 since there are more information available + m_discoveryResults[networkDeviceInfo.address()].apiAvailableV1 = true; + } else { + GoeDiscovery::Result result; + result.serialNumber = responseMap.value("sse").toString(); + result.firmwareVersion = responseMap.value("fwv").toString(); + result.networkDeviceInfo = networkDeviceInfo; + result.apiAvailableV1 = true; + m_discoveryResults[networkDeviceInfo.address()] = result; + } + } else { + qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V1 verification returned JSON data but not the right one. Continue..."; + } + + // Check if we are done with all checks + verifyDiscoveryFinished(); + }); +} + +void GoeDiscovery::checkNetworkDeviceApiV2(const NetworkDeviceInfo &networkDeviceInfo) +{ + // Check if API V2 is available: http:///api/status + qCDebug(dcGoECharger()) << "Discovery: verify API V2 on" << networkDeviceInfo.address().toString(); + QNetworkReply *reply = m_networkAccessManager->get(buildRequestV2(networkDeviceInfo.address())); + m_pendingReplies.append(reply); + connect(reply, &QNetworkReply::finished, this, [=](){ + m_pendingReplies.removeAll(reply); + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V2 verification HTTP error" << reply->errorString() << "Continue..."; + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V2 verification invalid JSON data. Continue..."; + return; + } + + // Verify if we have the required values in the response map + // https://github.com/goecharger/go-eCharger-API-v2/blob/main/http-en.md + QVariantMap responseMap = jsonDoc.toVariant().toMap(); + if (responseMap.contains("fwv") && responseMap.contains("sse") && responseMap.contains("typ") && responseMap.contains("fna")) { + // Looks like we have found a go-e V2 api endpoint, nice + qCDebug(dcGoECharger()) << "Discovery: --> Found API V2 on" << networkDeviceInfo.address().toString(); + + GoeDiscovery::Result result; + result.serialNumber = responseMap.value("sse").toString(); + result.firmwareVersion = responseMap.value("fwv").toString(); + result.manufacturer = responseMap.value("oem").toString(); + result.product = responseMap.value("typ").toString(); + result.friendlyName = responseMap.value("fna").toString(); + result.networkDeviceInfo = networkDeviceInfo; + result.apiAvailableV2 = true; + + if (m_discoveryResults.contains(networkDeviceInfo.address())) { + result.apiAvailableV1 = m_discoveryResults.value(networkDeviceInfo.address()).apiAvailableV1; + } + // Overwrite result from V1 since V2 contains more information + m_discoveryResults[networkDeviceInfo.address()] = result; + } else { + qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V2 verification returned JSON data but not the right one. Continue..."; + } + + // Check if we are done with all checks + verifyDiscoveryFinished(); + }); +} + +void GoeDiscovery::verifyDiscoveryFinished() +{ + if (!m_discoveryReply && m_verifiedNetworkDeviceInfos.count() == m_discoveredNetworkDeviceInfos.count()) { + finishDiscovery(); + } +} + +void GoeDiscovery::cleanupPendingReplies() +{ + foreach (QNetworkReply *reply, m_pendingReplies) { + m_pendingReplies.removeAll(reply); + reply->abort(); + } +} + +void GoeDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + qCInfo(dcGoECharger()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() << "go-eChargers in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + m_gracePeriodTimer.stop(); + cleanupPendingReplies(); + emit discoveryFinished(); +} + +QDebug operator<<(QDebug dbg, const GoeDiscovery::Result &result) +{ + dbg.nospace() << "GoeDiscovery:Result(" << result.product; + dbg.nospace() << ", " << result.manufacturer; + dbg.nospace() << ", Version: " << result.firmwareVersion; + dbg.nospace() << ", SN: " << result.serialNumber; + dbg.nospace() << ", V1: " << result.apiAvailableV1; + dbg.nospace() << ", V2: " << result.apiAvailableV2; + dbg.nospace() << ", " << result.networkDeviceInfo.address().toString(); + dbg.nospace() << ", " << result.networkDeviceInfo.macAddress(); + dbg.nospace() << ") "; + return dbg.maybeSpace(); +} diff --git a/goecharger/goediscovery.h b/goecharger/goediscovery.h new file mode 100644 index 00000000..7f951f4c --- /dev/null +++ b/goecharger/goediscovery.h @@ -0,0 +1,93 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef GOEDISCOVERY_H +#define GOEDISCOVERY_H + +#include +#include + +#include +#include + +class GoeDiscovery : public QObject +{ + Q_OBJECT +public: + typedef struct Result { + QString product = "go-eCharger"; + QString manufacturer = "go-e"; + QString friendlyName; + QString serialNumber; + QString firmwareVersion; + NetworkDeviceInfo networkDeviceInfo; + bool apiAvailableV1 = false; + bool apiAvailableV2 = false; + } Result; + + explicit GoeDiscovery(NetworkAccessManager *networkAccessManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + ~GoeDiscovery(); + + void startDiscovery(); + + QList discoveryResults() const; + + static QNetworkRequest buildRequestV1(const QHostAddress &address); + static QNetworkRequest buildRequestV2(const QHostAddress &address); + +signals: + void discoveryFinished(); + +private: + QDateTime m_startDateTime; + QTimer m_gracePeriodTimer; + NetworkAccessManager *m_networkAccessManager = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + NetworkDeviceDiscoveryReply *m_discoveryReply = nullptr; + + QHash m_discoveryResults; + NetworkDeviceInfos m_discoveredNetworkDeviceInfos; + NetworkDeviceInfos m_verifiedNetworkDeviceInfos; + QList m_pendingReplies; + +private slots: + void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo); + void checkNetworkDeviceApiV1(const NetworkDeviceInfo &networkDeviceInfo); + void checkNetworkDeviceApiV2(const NetworkDeviceInfo &networkDeviceInfo); + + void verifyDiscoveryFinished(); + void cleanupPendingReplies(); + + void finishDiscovery(); +}; + +QDebug operator<<(QDebug debug, const GoeDiscovery::Result &result); + +#endif // GOEDISCOVERY_H diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 06dd199c..0f675d1d 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -1,6 +1,6 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -38,7 +38,11 @@ #include #include -// API documentation: https://github.com/goecharger/go-eCharger-API-v1 +#include "goediscovery.h" + +// API documentation: +// V1: https://github.com/goecharger/go-eCharger-API-v1 +// V2: https://github.com/goecharger/go-eCharger-API-v2 IntegrationPluginGoECharger::IntegrationPluginGoECharger() { @@ -53,44 +57,36 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) return; } - // Perform a network device discovery and filter for "go-eCharger" hosts - NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); - connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { - - qCDebug(dcGoECharger()) << "Checking discovered" << networkDeviceInfo; - // Filter by hostname - if (!networkDeviceInfo.hostName().toLower().contains("go-echarger")) - continue; - - // We need also the mac address - if (networkDeviceInfo.macAddress().isEmpty()) - continue; - + GoeDiscovery *discovery = new GoeDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), this); + connect(discovery, &GoeDiscovery::discoveryFinished, discovery, &GoeDiscovery::deleteLater); + connect(discovery, &GoeDiscovery::discoveryFinished, info, [=](){ + foreach (const GoeDiscovery::Result &result, discovery->discoveryResults()) { QString title; - if (networkDeviceInfo.hostName().isEmpty()) { - title = networkDeviceInfo.address().toString(); + if (!result.product.isEmpty() && !result.friendlyName.isEmpty() && result.friendlyName != result.product) { + // We use the friendly name for the title, since the user seems to have given a name + title = result.friendlyName; } else { - title = "go-eCharger"; + title = result.product; } - QString description; - if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { - description = networkDeviceInfo.address().toString(); - } else { - description = networkDeviceInfo.address().toString() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; + // There might be an other OEM than go-e, let's use this information since the user might not look for go-e (whitelabel) + if (!result.manufacturer.isEmpty()) { + title += " (" + result.manufacturer + ")"; } + QString description = "Serial: " + result.serialNumber + ", V: " + result.firmwareVersion + " - " + result.networkDeviceInfo.address().toString(); + qCDebug(dcGoECharger()) << "-->" << title << description; ThingDescriptor descriptor(goeHomeThingClassId, title, description); ParamList params; - params << Param(goeHomeThingIpAddressParamTypeId, networkDeviceInfo.address().toString()); - params << Param(goeHomeThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + params << Param(goeHomeThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(goeHomeThingSerialNumberParamTypeId, result.serialNumber); + params << Param(goeHomeThingApiVersionParamTypeId, result.apiAvailableV2 ? 2 : 1); // always use v2 if available... descriptor.setParams(params); // Check if we already have set up this device - Things existingThings = myThings().filterByParam(goeHomeThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); + Things existingThings = myThings().filterByParam(goeHomeThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { - qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system" << networkDeviceInfo; + qCDebug(dcGoECharger()) << "This wallbox already exists in the system!" << result.networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); } @@ -99,53 +95,100 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) info->finish(Thing::ThingErrorNoError); }); + + // Start the discovery process + discovery->startDiscovery(); } void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); - if (thing->thingClassId() == goeHomeThingClassId) { - QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [=](){ - if (reply->error() != QNetworkReply::NoError) { - qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); - info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); - return; - } + qCDebug(dcGoECharger()) << "Set up" << thing << thing->params(); - QByteArray data = reply->readAll(); - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) { - qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); - info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The wallbox returned invalid data.")); - return; - } + MacAddress macAddress = MacAddress(thing->paramValue(goeHomeThingMacAddressParamTypeId).toString()); + if (!macAddress.isValid()) { + qCWarning(dcGoECharger()) << "The configured mac address is not valid" << thing->params(); + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing.")); + return; + } - qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); - QVariantMap statusMap = jsonDoc.toVariant().toMap(); - if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + // Create the monitor + NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); + m_monitors.insert(thing, monitor); + QHostAddress address = getHostAddress(thing); + if (address.isNull()) { + qCWarning(dcGoECharger()) << "Cannot set up go-eCharger. The host address is not known yet. Maybe it will be available in the next run..."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); + return; + } - // Handle reconfigure - if (m_channels.contains(thing)) { - hardwareManager()->mqttProvider()->releaseChannel(m_channels.take(thing)); - // Continue with normal setup + // Clean up in case the setup gets aborted + connect(info, &ThingSetupInfo::aborted, monitor, [=](){ + if (m_monitors.contains(thing)) { + qCDebug(dcGoECharger()) << "Unregister monitor because setup has been aborted."; + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ + qCDebug(dcGoECharger()) << "Network device monitor reachable changed for" << thing->name() << reachable; + if (reachable && thing->setupComplete() && !thing->stateValue("connected").toBool()) { + + // The device is reachable again and we have already set it up. + // Update data and optionally reconfigure the mqtt channel + + QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString() << reply->readAll(); + return; } - // Verify mqtt client and set it up - qCDebug(dcGoECharger()) << "Setup using MQTT connection for" << thing; - QHostAddress address = QHostAddress(thing->paramValue(goeHomeThingIpAddressParamTypeId).toString()); - setupMqttChannel(info, address, statusMap); - } else { - // Since we are not using mqtt, we are done with the setup, the refresh timer will be configured in post setup - info->finish(Thing::ThingErrorNoError); - qCDebug(dcGoECharger()) << "Setup using HTTP finished successfully"; - update(thing, statusMap); - thing->setStateValue(goeHomeConnectedStateTypeId, true); + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + return; + } + + qCDebug(dcGoECharger()) << "Initial status map" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Compact)); + QVariantMap statusMap = jsonDoc.toVariant().toMap(); + + ApiVersion apiVersion = getApiVersion(thing); + switch (apiVersion) { + case ApiVersion1: + if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + // Verify mqtt client and set it up + qCDebug(dcGoECharger()) << "Setup using MQTT connection for" << thing; + reconfigureMqttChannelV1(thing, statusMap); + } else { + // Since we are not using mqtt, we are done with the setup, the refresh timer will be configured in post setup + qCDebug(dcGoECharger()) << "Setup using HTTP finished successfully"; + updateV1(thing, statusMap); + } + break; + case ApiVersion2: + + break; + } + }); + } + }); + + // Wait for the monitor to be ready + if (monitor->reachable()) { + // Thing already reachable...let's finish the setup + setupGoeHome(info); + } else { + qCDebug(dcGoECharger()) << "Wait for the network monitor to get reachable"; + connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ + if (reachable) { + setupGoeHome(info); } }); - return; } Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); @@ -167,8 +210,8 @@ void IntegrationPluginGoECharger::postSetupThing(Thing *thing) void IntegrationPluginGoECharger::thingRemoved(Thing *thing) { // Cleanup mqtt channels if set up - if (m_channels.contains(thing)) { - hardwareManager()->mqttProvider()->releaseChannel(m_channels.take(thing)); + if (m_mqttChannelsV1.contains(thing)) { + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV1.take(thing)); } // Cleanup possible pending replies @@ -187,272 +230,286 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); Action action = info->action(); + QHostAddress address = getHostAddress(thing); + ApiVersion apiVersion = getApiVersion(thing); if (thing->thingClassId() != goeHomeThingClassId) { info->finish(Thing::ThingErrorThingClassNotFound); return; } - if (!thing->stateValue(goeHomeConnectedStateTypeId).toBool()) { + if (!thing->stateValue("connected").toBool()) { qCWarning(dcGoECharger()) << thing << "failed to execute action. The device seems not to be connected."; info->finish(Thing::ThingErrorHardwareNotAvailable); return; } - if (thing->stateValue(goeHomeSerialNumberStateTypeId).toString().isEmpty()) { - qCDebug(dcGoECharger()) << "Could not execute action because the serial number is missing."; - info->finish(Thing::ThingErrorHardwareFailure); - return; - } - - if (action.actionTypeId() == goeHomePowerActionTypeId) { - bool power = action.paramValue(goeHomePowerActionPowerParamTypeId).toBool(); - qCDebug(dcGoECharger()) << "Setting charging allowed to" << power; - // Set the allow value - QString configuration = QString("alw=%1").arg(power ? 1: 0); - sendActionRequest(thing, info, configuration); - return; - } else if (action.actionTypeId() == goeHomeMaxChargingCurrentActionTypeId) { - uint maxChargingCurrent = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); - qCDebug(dcGoECharger()) << "Setting max charging current to" << maxChargingCurrent << "A"; - // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement amx - // Maybe check if the user sets it or a automatism - QString configuration = QString("amp=%1").arg(maxChargingCurrent); - sendActionRequest(thing, info, configuration); - return; - } else if (action.actionTypeId() == goeHomeAbsoluteMaxAmpereActionTypeId) { - uint maxAmpere = action.paramValue(goeHomeAbsoluteMaxAmpereActionAbsoluteMaxAmpereParamTypeId).toUInt(); - qCDebug(dcGoECharger()) << "Setting absolute maximal charging amperes to" << maxAmpere << "A"; - QString configuration = QString("ama=%1").arg(maxAmpere); - sendActionRequest(thing, info, configuration); - return; - } else if (action.actionTypeId() == goeHomeCloudActionTypeId) { - bool enabled = action.paramValue(goeHomeCloudActionCloudParamTypeId).toBool(); - qCDebug(dcGoECharger()) << "Set cloud" << (enabled ? "enabled" : "disabled"); - QString configuration = QString("cdi=%1").arg(enabled ? 1: 0); - sendActionRequest(thing, info, configuration); - return; - } else if (action.actionTypeId() == goeHomeLedBrightnessActionTypeId) { - quint8 brightness = action.paramValue(goeHomeLedBrightnessActionLedBrightnessParamTypeId).toUInt(); - qCDebug(dcGoECharger()) << "Set led brightnss to" << brightness << "/" << 255; - QString configuration = QString("lbr=%1").arg(brightness); - sendActionRequest(thing, info, configuration); - return; - } else if (action.actionTypeId() == goeHomeLedEnergySaveActionTypeId) { - bool enabled = action.paramValue(goeHomeLedEnergySaveActionLedEnergySaveParamTypeId).toBool(); - qCDebug(dcGoECharger()) << "Set led energy saving" << (enabled ? "enabled" : "disabled"); - QString configuration = QString("lse=%1").arg(enabled ? 1: 0); - sendActionRequest(thing, info, configuration); - return; - } else { - info->finish(Thing::ThingErrorActionTypeNotFound); - } -} - -void IntegrationPluginGoECharger::onClientConnected(MqttChannel *channel) -{ - Thing *thing = m_channels.key(channel); - if (!thing) { - qCWarning(dcGoECharger()) << "Received a client connect for an unknown thing. Ignoring the event."; - return; - } - - qCDebug(dcGoECharger()) << thing << "connected"; - thing->setStateValue(goeHomeConnectedStateTypeId, true); -} - -void IntegrationPluginGoECharger::onClientDisconnected(MqttChannel *channel) -{ - Thing *thing = m_channels.key(channel); - if (!thing) { - qCWarning(dcGoECharger()) << "Received a client disconnect for an unknown thing. Ignoring the event."; - return; - } - - qCDebug(dcGoECharger()) << thing << "connected"; - thing->setStateValue(goeHomeConnectedStateTypeId, false); -} - -void IntegrationPluginGoECharger::onPublishReceived(MqttChannel *channel, const QString &topic, const QByteArray &payload) -{ - Thing *thing = m_channels.key(channel); - if (!thing) { - qCWarning(dcGoECharger()) << "Received a MQTT client publish from an unknown thing. Ignoring the event."; - return; - } - - qCDebug(dcGoECharger()) << thing << "publish received" << topic; - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &error); - if (error.error != QJsonParseError::NoError) { - qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(payload) << error.errorString(); - return; - } - - QString serialNumber = thing->stateValue(goeHomeSerialNumberStateTypeId).toString(); - if (topic == QString("go-eCharger/%1/status").arg(serialNumber)) { - update(thing, jsonDoc.toVariant().toMap()); - } else { - qCDebug(dcGoECharger()) << "Unhandled topic publish received:" << topic << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Compact)); - } -} - -void IntegrationPluginGoECharger::update(Thing *thing, const QVariantMap &statusMap) -{ - if (thing->thingClassId() == goeHomeThingClassId) { - // Parse status map and update states... - CarState carState = static_cast(statusMap.value("car").toUInt()); - switch (carState) { - case CarStateReadyNoCar: - thing->setStateValue(goeHomeCarStatusStateTypeId, "Ready but no vehicle connected"); - thing->setStateValue(goeHomePluggedInStateTypeId, false); - break; - case CarStateCharging: - thing->setStateValue(goeHomeCarStatusStateTypeId, "Vehicle loads"); - thing->setStateValue(goeHomePluggedInStateTypeId, true); - break; - case CarStateWaitForCar: - thing->setStateValue(goeHomeCarStatusStateTypeId, "Waiting for vehicle"); - thing->setStateValue(goeHomePluggedInStateTypeId, false); - break; - case CarStateChargedCarConnected: - thing->setStateValue(goeHomeCarStatusStateTypeId, "Charging finished and vehicle still connected"); - thing->setStateValue(goeHomePluggedInStateTypeId, true); - break; - } - - thing->setStateValue(goeHomeChargingStateTypeId, carState == CarStateCharging); - - Access accessStatus = static_cast(statusMap.value("ast").toUInt()); - switch (accessStatus) { - case AccessOpen: - thing->setStateValue(goeHomeAccessStateTypeId, "Open"); - break; - case AccessRfid: - thing->setStateValue(goeHomeAccessStateTypeId, "RFID"); - break; - case AccessAuto: - thing->setStateValue(goeHomeAccessStateTypeId, "Automatic"); - break; - } - - QVariantList temperatureSensorList = statusMap.value("tma").toList(); - if (temperatureSensorList.count() >= 1) - thing->setStateValue(goeHomeTemperatureSensor1StateTypeId, temperatureSensorList.at(0).toDouble()); - - if (temperatureSensorList.count() >= 2) - thing->setStateValue(goeHomeTemperatureSensor2StateTypeId, temperatureSensorList.at(1).toDouble()); - - if (temperatureSensorList.count() >= 3) - thing->setStateValue(goeHomeTemperatureSensor3StateTypeId, temperatureSensorList.at(2).toDouble()); - - if (temperatureSensorList.count() >= 4) - thing->setStateValue(goeHomeTemperatureSensor4StateTypeId, temperatureSensorList.at(3).toDouble()); - - thing->setStateValue(goeHomeTotalEnergyConsumedStateTypeId, statusMap.value("eto").toUInt() / 10.0); - thing->setStateValue(goeHomeSessionEnergyStateTypeId, statusMap.value("dws").toUInt() / 360000.0); - thing->setStateValue(goeHomePowerStateTypeId, (statusMap.value("alw").toUInt() == 0 ? false : true)); - thing->setStateValue(goeHomeUpdateAvailableStateTypeId, (statusMap.value("upd").toUInt() == 0 ? false : true)); - thing->setStateValue(goeHomeCloudStateTypeId, (statusMap.value("cdi").toUInt() == 0 ? false : true)); - thing->setStateValue(goeHomeFirmwareVersionStateTypeId, statusMap.value("fwv").toString()); - // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement this - thing->setStateValue(goeHomeMaxChargingCurrentStateTypeId, statusMap.value("amp").toUInt()); - thing->setStateValue(goeHomeLedBrightnessStateTypeId, statusMap.value("lbr").toUInt()); - thing->setStateValue(goeHomeLedEnergySaveStateTypeId, statusMap.value("lse").toBool()); - thing->setStateValue(goeHomeSerialNumberStateTypeId, statusMap.value("sse").toString()); - thing->setStateValue(goeHomeAdapterConnectedStateTypeId, (statusMap.value("adi").toUInt() == 0 ? false : true)); - - uint amaLimit = statusMap.value("ama").toUInt(); - uint cableLimit = statusMap.value("cbl").toUInt(); - - thing->setStateValue(goeHomeAbsoluteMaxAmpereStateTypeId, amaLimit); - thing->setStateValue(goeHomeCableType2AmpereStateTypeId, cableLimit); - - // Set the limit for the max charging amps - if (cableLimit != 0) { - thing->setStateMaxValue(goeHomeMaxChargingCurrentStateTypeId, qMin(amaLimit, cableLimit)); + switch (apiVersion) { + case ApiVersion1: { + if (action.actionTypeId() == goeHomePowerActionTypeId) { + bool power = action.paramValue(goeHomePowerActionPowerParamTypeId).toBool(); + qCDebug(dcGoECharger()) << "Setting charging allowed to" << power; + // Set the allow value + QString configuration = QString("alw=%1").arg(power ? 1: 0); + sendActionRequestV1(thing, info, configuration); + return; + } else if (action.actionTypeId() == goeHomeMaxChargingCurrentActionTypeId) { + uint maxChargingCurrent = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + qCDebug(dcGoECharger()) << "Setting max charging current to" << maxChargingCurrent << "A"; + // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement amx + // Maybe check if the user sets it or a automatism + QString configuration = QString("amp=%1").arg(maxChargingCurrent); + sendActionRequestV1(thing, info, configuration); + return; } else { - thing->setStateMaxValue(goeHomeMaxChargingCurrentStateTypeId, amaLimit); - } - - // Parse nrg array - uint voltagePhaseA = 0; uint voltagePhaseB = 0; uint voltagePhaseC = 0; - double amperePhaseA = 0; double amperePhaseB = 0; double amperePhaseC = 0; - double currentPower = 0; double powerPhaseA = 0; double powerPhaseB = 0; double powerPhaseC = 0; - - QVariantList measurementList = statusMap.value("nrg").toList(); - if (measurementList.count() >= 1) - voltagePhaseA = measurementList.at(0).toUInt(); - - if (measurementList.count() >= 2) - voltagePhaseB = measurementList.at(1).toUInt(); - - if (measurementList.count() >= 3) - voltagePhaseC = measurementList.at(2).toUInt(); - - if (measurementList.count() >= 5) - amperePhaseA = measurementList.at(4).toUInt() / 10.0; // 0,1 A value 123 -> 12,3 A - - if (measurementList.count() >= 6) - amperePhaseB = measurementList.at(5).toUInt() / 10.0; // 0,1 A value 123 -> 12,3 A - - if (measurementList.count() >= 7) - amperePhaseC = measurementList.at(6).toUInt() / 10.0; // 0,1 A value 123 -> 12,3 A - - if (measurementList.count() >= 8) - powerPhaseA = measurementList.at(7).toUInt() * 100.0; // 0.1kW -> W - - if (measurementList.count() >= 9) - powerPhaseB = measurementList.at(8).toUInt() * 100.0; // 0.1kW -> W - - if (measurementList.count() >= 10) - powerPhaseC = measurementList.at(9).toUInt() * 100.0; // 0.1kW -> W - - if (measurementList.count() >= 12) - currentPower = measurementList.at(11).toUInt() * 10.0; // 0.01kW -> W - - // Update all states - thing->setStateValue(goeHomeVoltagePhaseAStateTypeId, voltagePhaseA); - thing->setStateValue(goeHomeVoltagePhaseBStateTypeId, voltagePhaseB); - thing->setStateValue(goeHomeVoltagePhaseCStateTypeId, voltagePhaseC); - thing->setStateValue(goeHomeCurrentPhaseAStateTypeId, amperePhaseA); - thing->setStateValue(goeHomeCurrentPhaseBStateTypeId, amperePhaseB); - thing->setStateValue(goeHomeCurrentPhaseCStateTypeId, amperePhaseC); - thing->setStateValue(goeHomeCurrentPowerPhaseAStateTypeId, powerPhaseA); - thing->setStateValue(goeHomeCurrentPowerPhaseBStateTypeId, powerPhaseB); - thing->setStateValue(goeHomeCurrentPowerPhaseCStateTypeId, powerPhaseC); - - thing->setStateValue(goeHomeCurrentPowerStateTypeId, currentPower); - - // Check how many phases are actually charging, and update the phase count only if something happens on the phases (current or power) - if (amperePhaseA != 0 || amperePhaseB != 0 || amperePhaseC != 0) { - uint phaseCount = 0; - if (amperePhaseA != 0) - phaseCount += 1; - - if (amperePhaseB != 0) - phaseCount += 1; - - if (amperePhaseC != 0) - phaseCount += 1; - - thing->setStateValue(goeHomePhaseCountStateTypeId, phaseCount); + info->finish(Thing::ThingErrorActionTypeNotFound); } + break; } + case ApiVersion2: + // TODO + break; + } + +} + + +void IntegrationPluginGoECharger::setupGoeHome(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString() << reply->readAll(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The wallbox returned invalid data.")); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + return; + } + + QHostAddress address = getHostAddress(thing); + ApiVersion apiVersion = getApiVersion(thing); + + switch (apiVersion) { + case ApiVersion1: { + // Handle reconfigure + if (m_mqttChannelsV1.contains(thing)) + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV1.take(thing)); + + if (m_pendingReplies.contains(thing)) + m_pendingReplies.take(thing)->abort(); + + qCDebug(dcGoECharger()) << "Initial status map" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Compact)); + QVariantMap statusMap = jsonDoc.toVariant().toMap(); + if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + // Verify mqtt client and set it up + qCDebug(dcGoECharger()) << "Setup using MQTT connection for" << thing; + setupMqttChannelV1(info, address, statusMap); + } else { + // Since we are not using mqtt, we are done with the setup, the refresh timer will be configured in post setup + info->finish(Thing::ThingErrorNoError); + + qCDebug(dcGoECharger()) << "Setup using HTTP finished successfully"; + updateV1(thing, statusMap); + thing->setStateValue(goeHomeConnectedStateTypeId, true); + } + break; + } + case ApiVersion2: + // TODO + break; + } + }); } QNetworkRequest IntegrationPluginGoECharger::buildStatusRequest(Thing *thing) { - QHostAddress address = QHostAddress(thing->paramValue(goeHomeThingIpAddressParamTypeId).toString()); + QHostAddress address = getHostAddress(thing); + ApiVersion apiVersion = getApiVersion(thing); + QUrl requestUrl; requestUrl.setScheme("http"); requestUrl.setHost(address.toString()); - requestUrl.setPath("/status"); + + switch (apiVersion) { + case ApiVersion1: + requestUrl.setPath("/status"); + break; + case ApiVersion2: + requestUrl.setPath("/api/status"); + break; + } return QNetworkRequest(requestUrl); } -QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequest(const QHostAddress &address, const QString &configuration) +QHostAddress IntegrationPluginGoECharger::getHostAddress(Thing *thing) +{ + if (m_monitors.contains(thing)) + return m_monitors.value(thing)->networkDeviceInfo().address(); + + return QHostAddress(); +} + +IntegrationPluginGoECharger::ApiVersion IntegrationPluginGoECharger::getApiVersion(Thing *thing) +{ + return static_cast(thing->paramValue(goeHomeThingApiVersionParamTypeId).toUInt()); +} + +void IntegrationPluginGoECharger::updateV1(Thing *thing, const QVariantMap &statusMap) +{ + // Parse status map and update states... + CarState carState = static_cast(statusMap.value("car").toUInt()); + switch (carState) { + case CarStateUnknown: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Unknown"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + break; + case CarStateReadyNoCar: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Ready but no vehicle connected"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + break; + case CarStateCharging: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Vehicle loads"); + thing->setStateValue(goeHomePluggedInStateTypeId, true); + break; + case CarStateWaitForCar: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Waiting for vehicle"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + break; + case CarStateChargedCarConnected: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Charging finished and vehicle still connected"); + thing->setStateValue(goeHomePluggedInStateTypeId, true); + break; + } + + thing->setStateValue(goeHomeChargingStateTypeId, carState == CarStateCharging); + + Access accessStatus = static_cast(statusMap.value("ast").toUInt()); + switch (accessStatus) { + case AccessOpen: + thing->setStateValue(goeHomeAccessStateTypeId, "Open"); + break; + case AccessRfid: + thing->setStateValue(goeHomeAccessStateTypeId, "RFID"); + break; + case AccessAuto: + thing->setStateValue(goeHomeAccessStateTypeId, "Automatic"); + break; + } + + QVariantList temperatureSensorList = statusMap.value("tma").toList(); + if (temperatureSensorList.count() >= 1) + thing->setStateValue(goeHomeTemperatureSensor1StateTypeId, temperatureSensorList.at(0).toDouble()); + + if (temperatureSensorList.count() >= 2) + thing->setStateValue(goeHomeTemperatureSensor2StateTypeId, temperatureSensorList.at(1).toDouble()); + + if (temperatureSensorList.count() >= 3) + thing->setStateValue(goeHomeTemperatureSensor3StateTypeId, temperatureSensorList.at(2).toDouble()); + + if (temperatureSensorList.count() >= 4) + thing->setStateValue(goeHomeTemperatureSensor4StateTypeId, temperatureSensorList.at(3).toDouble()); + + thing->setStateValue(goeHomeTotalEnergyConsumedStateTypeId, statusMap.value("eto").toUInt() / 10.0); + thing->setStateValue(goeHomeSessionEnergyStateTypeId, statusMap.value("dws").toUInt() / 360000.0); + thing->setStateValue(goeHomePowerStateTypeId, (statusMap.value("alw").toUInt() == 0 ? false : true)); + thing->setStateValue(goeHomeUpdateAvailableStateTypeId, (statusMap.value("upd").toUInt() == 0 ? false : true)); + thing->setStateValue(goeHomeFirmwareVersionStateTypeId, statusMap.value("fwv").toString()); + // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement this + thing->setStateValue(goeHomeMaxChargingCurrentStateTypeId, statusMap.value("amp").toUInt()); + thing->setStateValue(goeHomeAdapterConnectedStateTypeId, (statusMap.value("adi").toUInt() == 0 ? false : true)); + + uint amaLimit = statusMap.value("ama").toUInt(); + uint cableLimit = statusMap.value("cbl").toUInt(); + + thing->setStateValue(goeHomeAbsoluteMaxAmpereStateTypeId, amaLimit); + thing->setStateValue(goeHomeCableType2AmpereStateTypeId, cableLimit); + + // Set the limit for the max charging amps + if (cableLimit != 0) { + thing->setStateMaxValue(goeHomeMaxChargingCurrentStateTypeId, qMin(amaLimit, cableLimit)); + } else { + thing->setStateMaxValue(goeHomeMaxChargingCurrentStateTypeId, amaLimit); + } + + // Parse nrg array + uint voltagePhaseA = 0; uint voltagePhaseB = 0; uint voltagePhaseC = 0; + double amperePhaseA = 0; double amperePhaseB = 0; double amperePhaseC = 0; + double currentPower = 0; double powerPhaseA = 0; double powerPhaseB = 0; double powerPhaseC = 0; + + QVariantList measurementList = statusMap.value("nrg").toList(); + if (measurementList.count() >= 1) + voltagePhaseA = measurementList.at(0).toUInt(); + + if (measurementList.count() >= 2) + voltagePhaseB = measurementList.at(1).toUInt(); + + if (measurementList.count() >= 3) + voltagePhaseC = measurementList.at(2).toUInt(); + + if (measurementList.count() >= 5) + amperePhaseA = measurementList.at(4).toUInt() / 10.0; // 0,1 A value 123 -> 12,3 A + + if (measurementList.count() >= 6) + amperePhaseB = measurementList.at(5).toUInt() / 10.0; // 0,1 A value 123 -> 12,3 A + + if (measurementList.count() >= 7) + amperePhaseC = measurementList.at(6).toUInt() / 10.0; // 0,1 A value 123 -> 12,3 A + + if (measurementList.count() >= 8) + powerPhaseA = measurementList.at(7).toUInt() * 100.0; // 0.1kW -> W + + if (measurementList.count() >= 9) + powerPhaseB = measurementList.at(8).toUInt() * 100.0; // 0.1kW -> W + + if (measurementList.count() >= 10) + powerPhaseC = measurementList.at(9).toUInt() * 100.0; // 0.1kW -> W + + if (measurementList.count() >= 12) + currentPower = measurementList.at(11).toUInt() * 10.0; // 0.01kW -> W + + // Update all states + thing->setStateValue(goeHomeVoltagePhaseAStateTypeId, voltagePhaseA); + thing->setStateValue(goeHomeVoltagePhaseBStateTypeId, voltagePhaseB); + thing->setStateValue(goeHomeVoltagePhaseCStateTypeId, voltagePhaseC); + thing->setStateValue(goeHomeCurrentPhaseAStateTypeId, amperePhaseA); + thing->setStateValue(goeHomeCurrentPhaseBStateTypeId, amperePhaseB); + thing->setStateValue(goeHomeCurrentPhaseCStateTypeId, amperePhaseC); + thing->setStateValue(goeHomeCurrentPowerPhaseAStateTypeId, powerPhaseA); + thing->setStateValue(goeHomeCurrentPowerPhaseBStateTypeId, powerPhaseB); + thing->setStateValue(goeHomeCurrentPowerPhaseCStateTypeId, powerPhaseC); + + thing->setStateValue(goeHomeCurrentPowerStateTypeId, currentPower); + + // Check how many phases are actually charging, and update the phase count only if something happens on the phases (current or power) + if (amperePhaseA != 0 || amperePhaseB != 0 || amperePhaseC != 0) { + uint phaseCount = 0; + if (amperePhaseA != 0) + phaseCount += 1; + + if (amperePhaseB != 0) + phaseCount += 1; + + if (amperePhaseC != 0) + phaseCount += 1; + + thing->setStateValue(goeHomePhaseCountStateTypeId, phaseCount); + } +} + + +QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequestV1(const QHostAddress &address, const QString &configuration) { QUrl requestUrl; requestUrl.setScheme("http"); @@ -464,10 +521,13 @@ QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequest(const QHo return QNetworkRequest(requestUrl); } -void IntegrationPluginGoECharger::sendActionRequest(Thing *thing, ThingActionInfo *info, const QString &configuration) +void IntegrationPluginGoECharger::sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration) { - // Lets use rest here since we get a reply on the rest request. For using MQTT publish to topic "go-eCharger//cmd/req" - QNetworkRequest request = buildConfigurationRequest(QHostAddress(thing->paramValue(goeHomeThingIpAddressParamTypeId).toString()), configuration); + // Lets use rest here since we get a reply on the rest request. + // For using MQTT publish to topic "go-eCharger//cmd/req" + QHostAddress address = getHostAddress(thing); + + QNetworkRequest request = buildConfigurationRequestV1(address, configuration); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [=](){ @@ -487,13 +547,14 @@ void IntegrationPluginGoECharger::sendActionRequest(Thing *thing, ThingActionInf } info->finish(Thing::ThingErrorNoError); - update(thing, jsonDoc.toVariant().toMap()); + updateV1(thing, jsonDoc.toVariant().toMap()); }); } -void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap) +void IntegrationPluginGoECharger::setupMqttChannelV1(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap) { Thing *thing = info->thing(); + QString serialNumber = statusMap.value("sse").toString(); QString clientId = QString("go-eCharger:%1:%2").arg(serialNumber).arg(statusMap.value("rbc").toInt()); QString statusTopic = QString("go-eCharger/%1/status").arg(serialNumber); @@ -507,13 +568,13 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q return; } - m_channels.insert(thing, channel); - connect(channel, &MqttChannel::clientConnected, this, &IntegrationPluginGoECharger::onClientConnected); - connect(channel, &MqttChannel::clientDisconnected, this, &IntegrationPluginGoECharger::onClientDisconnected); - connect(channel, &MqttChannel::publishReceived, this, &IntegrationPluginGoECharger::onPublishReceived); + m_mqttChannelsV1.insert(thing, channel); + connect(channel, &MqttChannel::clientConnected, this, &IntegrationPluginGoECharger::onMqttClientV1Connected); + connect(channel, &MqttChannel::clientDisconnected, this, &IntegrationPluginGoECharger::onMqttClientV1Disconnected); + connect(channel, &MqttChannel::publishReceived, this, &IntegrationPluginGoECharger::onMqttPublishV1Received); // Configure the mqtt server on the go-e - QNetworkRequest request = buildConfigurationRequest(address, QString("mcs=%1").arg(channel->serverAddress().toString())); + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mcs=%1").arg(channel->serverAddress().toString())); qCDebug(dcGoECharger()) << "Configure nymea mqtt server address on" << thing << request.url().toString(); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -543,7 +604,7 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } // Configure the port - QNetworkRequest request = buildConfigurationRequest(address, QString("mcp=%1").arg(channel->serverPort())); + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mcp=%1").arg(channel->serverPort())); qCDebug(dcGoECharger()) << "Configure nymea mqtt server port on" << thing << request.url().toString(); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -573,7 +634,7 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } // Username - QNetworkRequest request = buildConfigurationRequest(address, QString("mcu=%1").arg(channel->username())); + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mcu=%1").arg(channel->username())); qCDebug(dcGoECharger()) << "Configure nymea mqtt server user name on" << thing << request.url().toString(); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -603,7 +664,7 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } // Password - QNetworkRequest request = buildConfigurationRequest(address, QString("mck=%1").arg(channel->password())); + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mck=%1").arg(channel->password())); qCDebug(dcGoECharger()) << "Configure nymea mqtt server password on" << thing << request.url().toString(); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -633,7 +694,7 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } // Enable MQTT - QNetworkRequest request = buildConfigurationRequest(address, QString("mce=1")); + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mce=1")); qCDebug(dcGoECharger()) << "Enable custom mqtt server on" << thing << request.url().toString(); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -666,7 +727,176 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q info->finish(Thing::ThingErrorNoError); qCDebug(dcGoECharger()) << "Configuration of MQTT for" << thing << "finished successfully"; // Update states... - update(thing, statusMap); + updateV1(thing, statusMap); + }); + }); + }); + }); + }); +} + +void IntegrationPluginGoECharger::reconfigureMqttChannelV1(Thing *thing, const QVariantMap &statusMap) +{ + QString serialNumber = thing->paramValue(goeHomeThingSerialNumberParamTypeId).toString(); + QHostAddress address = getHostAddress(thing); + qCDebug(dcGoECharger()) << "Reconfigure mqtt channel for" << thing; + + // At least in version 30.1 + QString clientId = QString("go-eCharger:%1:%2").arg(serialNumber).arg(statusMap.value("rbc").toInt()); + QString statusTopic = QString("/go-eCharger/%1/status").arg(serialNumber); + QString commandTopic = QString("/go-eCharger/%1/cmd/req").arg(serialNumber); + qCDebug(dcGoECharger()) << "Setting up mqtt channel for" << thing << address.toString() << statusTopic << commandTopic; + + MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(clientId, address, {statusTopic, commandTopic}); + if (!channel) { + qCWarning(dcGoECharger()) << "Failed to create MQTT channel for" << thing; + return; + } + + m_mqttChannelsV1.insert(thing, channel); + connect(channel, &MqttChannel::clientConnected, this, &IntegrationPluginGoECharger::onMqttClientV1Connected); + connect(channel, &MqttChannel::clientDisconnected, this, &IntegrationPluginGoECharger::onMqttClientV1Disconnected); + connect(channel, &MqttChannel::publishReceived, this, &IntegrationPluginGoECharger::onMqttPublishV1Received); + + // Configure the mqtt server on the go-e + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mcs=%1").arg(channel->serverAddress().toString())); + qCDebug(dcGoECharger()) << "Configure nymea mqtt server address on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + return; + } + + // Verify response matches the requsted value + if (jsonDoc.toVariant().toMap().value("mcs").toString() != channel->serverAddress().toString()) { + qCWarning(dcGoECharger()) << "Configured MQTT server but the response does not match with requested server address" << channel->serverAddress().toString(); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->serverAddress().toString(); + } + + // Configure the port + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mcp=%1").arg(channel->serverPort())); + qCDebug(dcGoECharger()) << "Configure nymea mqtt server port on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + return; + } + + // Verify response matches the requsted value + if (jsonDoc.toVariant().toMap().value("mcp").toUInt() != channel->serverPort()) { + qCWarning(dcGoECharger()) << "Configured MQTT server but the response does not match with requested server port" << channel->serverPort(); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->serverPort(); + } + + // Username + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mcu=%1").arg(channel->username())); + qCDebug(dcGoECharger()) << "Configure nymea mqtt server user name on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + return; + } + + // Verify response matches the requsted value + if (jsonDoc.toVariant().toMap().value("mcu").toString() != channel->username()) { + qCWarning(dcGoECharger()) << "Configured MQTT server but the response does not match with requested server username" << channel->username(); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->username(); + } + + // Password + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mck=%1").arg(channel->password())); + qCDebug(dcGoECharger()) << "Configure nymea mqtt server password on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + return; + } + + // Verify response matches the requsted value + if (jsonDoc.toVariant().toMap().value("mck").toString() != channel->password()) { + qCWarning(dcGoECharger()) << "Configured MQTT server but the response does not match with requested server password" << channel->password(); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->password(); + } + + // Enable MQTT + QNetworkRequest request = buildConfigurationRequestV1(address, QString("mce=1")); + qCDebug(dcGoECharger()) << "Enable custom mqtt server on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + return; + } + + // Verify response matches the requsted value + QVariantMap statusMap = jsonDoc.toVariant().toMap(); + if (statusMap.value("mce").toInt() != 1) { + qCWarning(dcGoECharger()) << "Configured MQTT server but the response does not match with requested value 1"; + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server enabled" << thing; + } + + qCDebug(dcGoECharger()) << "Configuration of MQTT for" << thing << "finished successfully"; + // Update states... + updateV1(thing, statusMap); }); }); }); @@ -682,44 +912,102 @@ void IntegrationPluginGoECharger::refreshHttp() continue; // Poll thing which if not using mqtt - if (!thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) + continue; - // Make sure there is not a request pending for this thing, otherwise wait for the next refresh - if (m_pendingReplies.contains(thing) && m_pendingReplies.value(thing)) - continue; + // Make sure there is not a request pending for this thing, otherwise wait for the next refresh + if (m_pendingReplies.contains(thing) && m_pendingReplies.value(thing)) + continue; - qCDebug(dcGoECharger()) << "Refresh HTTP status from" << thing; - QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); - m_pendingReplies.insert(thing, reply); + qCDebug(dcGoECharger()) << "Refresh HTTP status from" << thing; + QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); + m_pendingReplies.insert(thing, reply); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, thing, [=](){ - // We are done with this thing reply - m_pendingReplies.remove(thing); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + // We are done with this thing reply + m_pendingReplies.remove(thing); - if (reply->error() != QNetworkReply::NoError) { - qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); - thing->setStateValue(goeHomeConnectedStateTypeId, false); - return; - } + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString(); + thing->setStateValue("connected", false); + return; + } - QByteArray data = reply->readAll(); - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) { - qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); - thing->setStateValue(goeHomeConnectedStateTypeId, false); - return; - } + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + thing->setStateValue("connected", false); + return; + } - // Valid json data received, connected true - thing->setStateValue(goeHomeConnectedStateTypeId, true); + ApiVersion apiVersion = getApiVersion(thing); - qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); - QVariantMap statusMap = jsonDoc.toVariant().toMap(); - update(thing, statusMap); - }); - } + // Valid json data received, connected true + thing->setStateValue("connected", true); + + qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); + QVariantMap statusMap = jsonDoc.toVariant().toMap(); + switch (apiVersion) { + case ApiVersion1: + updateV1(thing, statusMap); + break; + case ApiVersion2: + // TODO: + break; + } + }); + } +} + + +void IntegrationPluginGoECharger::onMqttClientV1Connected(MqttChannel *channel) +{ + Thing *thing = m_mqttChannelsV1.key(channel); + if (!thing) { + qCWarning(dcGoECharger()) << "Received a client connect for an unknown thing. Ignoring the event."; + return; + } + + qCDebug(dcGoECharger()) << thing << "connected"; + thing->setStateValue("connected", true); +} + +void IntegrationPluginGoECharger::onMqttClientV1Disconnected(MqttChannel *channel) +{ + Thing *thing = m_mqttChannelsV1.key(channel); + if (!thing) { + qCWarning(dcGoECharger()) << "Received a client disconnect for an unknown thing. Ignoring the event."; + return; + } + + qCDebug(dcGoECharger()) << thing << "connected"; + thing->setStateValue("connected", false); +} + +void IntegrationPluginGoECharger::onMqttPublishV1Received(MqttChannel *channel, const QString &topic, const QByteArray &payload) +{ + Thing *thing = m_mqttChannelsV1.key(channel); + if (!thing) { + qCWarning(dcGoECharger()) << "Received a MQTT client publish from an unknown thing. Ignoring the event."; + return; + } + + qCDebug(dcGoECharger()) << thing << "publish received" << topic; + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to parse status data for thing " << thing->name() << qUtf8Printable(payload) << error.errorString(); + return; + } + + QString serialNumber = thing->paramValue(goeHomeThingSerialNumberParamTypeId).toString(); + if (topic == QString("go-eCharger/%1/status").arg(serialNumber)) { + updateV1(thing, jsonDoc.toVariant().toMap()); + } else { + qCDebug(dcGoECharger()) << "Unhandled topic publish received:" << topic << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Compact)); } } diff --git a/goecharger/integrationplugingoecharger.h b/goecharger/integrationplugingoecharger.h index dac79095..8ce430a2 100644 --- a/goecharger/integrationplugingoecharger.h +++ b/goecharger/integrationplugingoecharger.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2021, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -33,11 +33,12 @@ #include -#include +#include #include #include +#include +#include #include -#include class IntegrationPluginGoECharger: public IntegrationPlugin { @@ -47,7 +48,14 @@ class IntegrationPluginGoECharger: public IntegrationPlugin Q_INTERFACES(IntegrationPlugin) public: + enum ApiVersion { + ApiVersion1 = 1, + ApiVersion2 = 2 + }; + Q_ENUM(ApiVersion) + enum CarState { + CarStateUnknown = 0, CarStateReadyNoCar = 1, CarStateCharging = 2, CarStateWaitForCar = 3, @@ -55,6 +63,13 @@ public: }; Q_ENUM(CarState) + enum ForceState { + ForceStateNeutral = 0, + ForceStateOff = 1, + ForceStateOn = 2 + }; + Q_ENUM(ForceState) + enum Access { AccessOpen = 0, AccessRfid = 1, @@ -83,25 +98,39 @@ public: void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; private: PluginTimer *m_refreshTimer = nullptr; - QHash m_channels; - QHash m_pendingReplies; - void update(Thing *thing, const QVariantMap &statusMap); + QHash m_mqttChannelsV1; + QHash m_mqttChannelsV2; + + QHash m_pendingReplies; + QHash m_monitors; + + // General methods + void setupGoeHome(ThingSetupInfo *info); QNetworkRequest buildStatusRequest(Thing *thing); - QNetworkRequest buildConfigurationRequest(const QHostAddress &address, const QString &configuration); - void sendActionRequest(Thing *thing, ThingActionInfo *info, const QString &configuration); - void setupMqttChannel(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap); + QHostAddress getHostAddress(Thing *thing); + ApiVersion getApiVersion(Thing *thing); + + // API V1 + void updateV1(Thing *thing, const QVariantMap &statusMap); + void sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration); + QNetworkRequest buildConfigurationRequestV1(const QHostAddress &address, const QString &configuration); + void setupMqttChannelV1(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap); + void reconfigureMqttChannelV1(Thing *thing, const QVariantMap &statusMap); + private slots: void refreshHttp(); - void onClientConnected(MqttChannel* channel); - void onClientDisconnected(MqttChannel* channel); - void onPublishReceived(MqttChannel* channel, const QString &topic, const QByteArray &payload); + // API V1 + void onMqttClientV1Connected(MqttChannel* channel); + void onMqttClientV1Disconnected(MqttChannel* channel); + void onMqttPublishV1Received(MqttChannel* channel, const QString &topic, const QByteArray &payload); }; diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index 2a322312..c29dbb75 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -15,24 +15,34 @@ "createMethods": ["Discovery", "User"], "interfaces": ["evcharger", "smartmeterconsumer", "connectable"], "paramTypes": [ - { - "id": "4342b72c-99d0-41a5-abc6-ea6c1cc1352c", - "name":"ipAddress", - "displayName": "IP address", - "type": "QString" - }, { "id": "0e30e30f-ad96-417e-b739-cac85f75de39", "name":"macAddress", "displayName": "MAC address", "type": "QString" }, + { + "id": "74abaff3-39e6-40be-b3c3-f41911d17e02", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + }, { "id": "848613a6-8a17-4082-ba77-3b4421170a4f", "name":"useMqtt", "displayName": "Use MQTT interface", "type": "bool", - "defaultValue": false + "defaultValue": true + }, + { + "id": "3ad014e2-c948-406e-99be-eba1d866ea20", + "name":"apiVersion", + "displayName": "API Version", + "type": "uint", + "minValue": 1, + "maxValue": 2, + "defaultValue": 1 } ], "stateTypes":[ @@ -52,6 +62,7 @@ "displayNameEvent": "Car status changed", "type": "QString", "possibleValues": [ + "Unknown", "Ready but no vehicle connected", "Vehicle loads", "Waiting for vehicle", @@ -122,24 +133,25 @@ "name": "absoluteMaxAmpere", "displayName": "Maximal ampere", "displayNameEvent": "Maximal ampere changed", - "displayNameAction": "Set maximal ampere", "type": "uint", "unit": "Ampere", "minValue": 6, "maxValue": 32, - "defaultValue": 20, - "writable": true, + "defaultValue": 32, "suggestLogging": true }, { - "id": "ac849296-3f70-4b1b-aa30-127d774667bb", - "name": "cloud", - "displayName": "Cloud enabled", - "displayNameAction": "Set cloud enabled", - "displayNameEvent": "Cloud enabled changed", - "type": "bool", - "defaultValue": true, - "writable": true + "id": "ede9251d-662e-4d4b-90e3-db3d33c823d3", + "name": "modelMaxAmpere", + "displayName": "Model maximal ampere", + "displayNameEvent": "Maximal ampere model changed", + "type": "uint", + "unit": "Ampere", + "minValue": 16, + "maxValue": 32, + "defaultValue": 16, + "suggestLogging": true, + "cached": true }, { "id": "08b107bc-1284-455d-9e5a-6a1c3adc389f", @@ -166,7 +178,8 @@ "displayNameEvent": "Cable ampere encoding changed", "type": "uint", "unit": "Ampere", - "defaultValue": 0 + "defaultValue": 0, + "cached": true }, { "id": "d8f5abb6-5db3-4040-8829-553b1d881ce4", @@ -175,7 +188,8 @@ "displayNameEvent": "Total energy changed", "type": "double", "unit": "KiloWattHour", - "defaultValue": 0.0 + "defaultValue": 0.0, + "cached": true }, { "id": "e8258831-ad89-4d27-b295-e8c10dd42b76", @@ -277,6 +291,15 @@ "unit": "Volt", "defaultValue": 0.00 }, + { + "id": "28f59f96-4b30-4606-9a04-80c82abc346b", + "name": "frequency", + "displayName": "Frequency", + "displayNameEvent": "Frequency changed", + "type": "double", + "unit": "Hertz", + "defaultValue": 50.0 + }, { "id": "b78d805a-f97c-4c9d-a647-5fc98f8d6dd1", "name": "phaseCount", @@ -287,28 +310,6 @@ "maxValue": 3, "defaultValue": 1 }, - { - "id": "b06479d5-7a38-4fbd-867e-e55bdb54651b", - "name": "ledBrightness", - "displayName": "Led brightness", - "displayNameAction": "Set led brightness", - "displayNameEvent": "Led brightness changed", - "type": "int", - "minValue": 0, - "maxValue": 255, - "defaultValue": 255, - "writable": true - }, - { - "id": "048a4c98-3ee4-4d02-ad48-6d70f31fce8c", - "name": "ledEnergySave", - "displayName": "Led energy saving enabled", - "displayNameAction": "Set led energy saving enabled", - "displayNameEvent": "Led energy saving enabled enabled changed", - "type": "bool", - "defaultValue": true, - "writable": true - }, { "id": "2bf1ebf1-0d8c-4209-ad35-4114d9861832", "name": "temperatureSensor1", @@ -353,15 +354,6 @@ "type": "QString", "defaultValue": "", "cached": true - }, - { - "id": "8ecdf24b-daca-4b7a-98b5-3236f1e6ad85", - "name": "serialNumber", - "displayName": "Serial number", - "displayNameEvent": "Serial number changed", - "type": "QString", - "defaultValue": "", - "cached": true } ] } @@ -369,7 +361,3 @@ } ] } - - - - From 6ddb84de974d0b0197c6c004f1cd1c3e90bc077f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 21 Jul 2022 11:48:56 +0200 Subject: [PATCH 02/11] Add API V2 parallel to V1 --- goecharger/integrationplugingoecharger.cpp | 540 ++++++++++++++++++++- goecharger/integrationplugingoecharger.h | 11 +- 2 files changed, 533 insertions(+), 18 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 0f675d1d..85ebf0b4 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -171,7 +171,15 @@ void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) } break; case ApiVersion2: - + if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + // Verify mqtt client and set it up + qCDebug(dcGoECharger()) << "Setup using MQTT connection for" << thing; + reconfigureMqttChannelV2(thing); + } else { + // Since we are not using mqtt, we are done with the setup, the refresh timer will be configured in post setup + qCDebug(dcGoECharger()) << "Setup using HTTP finished successfully"; + updateV2(thing, statusMap); + } break; } }); @@ -210,10 +218,18 @@ void IntegrationPluginGoECharger::postSetupThing(Thing *thing) void IntegrationPluginGoECharger::thingRemoved(Thing *thing) { // Cleanup mqtt channels if set up - if (m_mqttChannelsV1.contains(thing)) { - hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV1.take(thing)); + switch (getApiVersion(thing)) { + case ApiVersion1: + if (m_mqttChannelsV1.contains(thing)) { + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV1.take(thing)); + } + break; + case ApiVersion2: + + break; } + // Cleanup possible pending replies if (m_pendingReplies.contains(thing) && m_pendingReplies.value(thing)) { m_pendingReplies.take(thing)->abort(); @@ -230,8 +246,8 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); Action action = info->action(); - QHostAddress address = getHostAddress(thing); ApiVersion apiVersion = getApiVersion(thing); + QHostAddress address = getHostAddress(thing); if (thing->thingClassId() != goeHomeThingClassId) { info->finish(Thing::ThingErrorThingClassNotFound); @@ -245,7 +261,7 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) } switch (apiVersion) { - case ApiVersion1: { + case ApiVersion1: if (action.actionTypeId() == goeHomePowerActionTypeId) { bool power = action.paramValue(goeHomePowerActionPowerParamTypeId).toBool(); qCDebug(dcGoECharger()) << "Setting charging allowed to" << power; @@ -265,12 +281,84 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) info->finish(Thing::ThingErrorActionTypeNotFound); } break; - } case ApiVersion2: - // TODO + if (action.actionTypeId() == goeHomePowerActionTypeId) { + bool power = action.paramValue(goeHomePowerActionPowerParamTypeId).toBool(); + qCDebug(dcGoECharger()) << "Setting charging allowed to" << power; + QUrlQuery configuration; + configuration.addQueryItem("frc", (power ? "0": "1")); + QNetworkRequest request = buildConfigurationRequestV2(address, configuration); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, info, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "Execute action failed. HTTP reply returned error:" << thing->name() << reply->errorString() << reply->readAll(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Execute action failed. Failed to parse data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The wallbox returned invalid data.")); + return; + } + + QVariantMap responseCode = jsonDoc.toVariant().toMap(); + if (responseCode.value("frc", false).toBool()) { + qCDebug(dcGoECharger()) << "Execute action finished successfully. Power" << power; + info->finish(Thing::ThingErrorNoError); + thing->setStateValue("power", power); + } else { + qCWarning(dcGoECharger()) << "Action finished with error:" << responseCode.value("frc").toString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + + return; + } else if (action.actionTypeId() == goeHomeMaxChargingCurrentActionTypeId) { + uint ampere = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + qCDebug(dcGoECharger()) << "Setting max charging current to" << ampere << "A"; + // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement amx + // Maybe check if the user sets it or a automatism + QUrlQuery configuration; + configuration.addQueryItem("amp", QString::number(ampere)); + QNetworkRequest request = buildConfigurationRequestV2(address, configuration); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + + connect(reply, &QNetworkReply::finished, info, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "Execute action failed. HTTP status reply returned error:" << thing->name() << reply->errorString() << reply->readAll(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Execute action failed. Failed to parse data for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The wallbox returned invalid data.")); + return; + } + + QVariantMap responseCode = jsonDoc.toVariant().toMap(); + if (responseCode.value("amp", false).toBool()) { + qCDebug(dcGoECharger()) << "Execute action finished successfully. Charging current" << ampere; + info->finish(Thing::ThingErrorNoError); + thing->setStateValue("maxChargingCurrent", ampere); + } else { + qCWarning(dcGoECharger()) << "Action finished with error:" << responseCode.value("amp").toString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } else { + info->finish(Thing::ThingErrorActionTypeNotFound); + } break; } - } @@ -313,20 +401,40 @@ void IntegrationPluginGoECharger::setupGoeHome(ThingSetupInfo *info) QVariantMap statusMap = jsonDoc.toVariant().toMap(); if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { // Verify mqtt client and set it up - qCDebug(dcGoECharger()) << "Setup using MQTT connection for" << thing; + qCDebug(dcGoECharger()) << "Setting up using MQTT connection for" << thing; setupMqttChannelV1(info, address, statusMap); } else { // Since we are not using mqtt, we are done with the setup, the refresh timer will be configured in post setup info->finish(Thing::ThingErrorNoError); qCDebug(dcGoECharger()) << "Setup using HTTP finished successfully"; + thing->setStateValue("connected", true); updateV1(thing, statusMap); - thing->setStateValue(goeHomeConnectedStateTypeId, true); } break; } case ApiVersion2: - // TODO + // Handle reconfigure + if (m_mqttChannelsV2.contains(thing)) + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + + if (m_pendingReplies.contains(thing)) + m_pendingReplies.take(thing)->abort(); + + qCDebug(dcGoECharger()) << "Initial status map" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Compact)); + QVariantMap statusMap = jsonDoc.toVariant().toMap(); + if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + // Verify mqtt client and set it up + qCDebug(dcGoECharger()) << "Setting up using MQTT connection for" << thing; + setupMqttChannelV2(info, address, statusMap); + } else { + // Since we are not using mqtt, we are done with the setup, the refresh timer will be configured in post setup + info->finish(Thing::ThingErrorNoError); + + qCDebug(dcGoECharger()) << "Setup using HTTP finished successfully"; + thing->setStateValue("connected", true); + updateV2(thing, statusMap); + } break; } }); @@ -371,10 +479,6 @@ void IntegrationPluginGoECharger::updateV1(Thing *thing, const QVariantMap &stat // Parse status map and update states... CarState carState = static_cast(statusMap.value("car").toUInt()); switch (carState) { - case CarStateUnknown: - thing->setStateValue(goeHomeCarStatusStateTypeId, "Unknown"); - thing->setStateValue(goeHomePluggedInStateTypeId, false); - break; case CarStateReadyNoCar: thing->setStateValue(goeHomeCarStatusStateTypeId, "Ready but no vehicle connected"); thing->setStateValue(goeHomePluggedInStateTypeId, false); @@ -391,6 +495,10 @@ void IntegrationPluginGoECharger::updateV1(Thing *thing, const QVariantMap &stat thing->setStateValue(goeHomeCarStatusStateTypeId, "Charging finished and vehicle still connected"); thing->setStateValue(goeHomePluggedInStateTypeId, true); break; + default: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Unknown"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + break; } thing->setStateValue(goeHomeChargingStateTypeId, carState == CarStateCharging); @@ -508,7 +616,6 @@ void IntegrationPluginGoECharger::updateV1(Thing *thing, const QVariantMap &stat } } - QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequestV1(const QHostAddress &address, const QString &configuration) { QUrl requestUrl; @@ -904,6 +1011,383 @@ void IntegrationPluginGoECharger::reconfigureMqttChannelV1(Thing *thing, const Q }); } +void IntegrationPluginGoECharger::updateV2(Thing *thing, const QVariantMap &statusMap) +{ + if (statusMap.contains("car")) { + CarState carState = static_cast(statusMap.value("car").toUInt()); + switch (carState) { + case CarStateReadyNoCar: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Ready but no vehicle connected"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + break; + case CarStateCharging: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Vehicle loads"); + thing->setStateValue(goeHomePluggedInStateTypeId, true); + break; + case CarStateWaitForCar: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Waiting for vehicle"); + thing->setStateValue(goeHomePluggedInStateTypeId, true); + break; + case CarStateChargedCarConnected: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Charging finished and vehicle still connected"); + thing->setStateValue(goeHomePluggedInStateTypeId, true); + break; + default: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Unknown"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + break; + } + + thing->setStateValue(goeHomeChargingStateTypeId, carState == CarStateCharging); + } + + if (statusMap.contains("ast")) { + Access accessStatus = static_cast(statusMap.value("ast").toUInt()); + switch (accessStatus) { + case AccessOpen: + thing->setStateValue(goeHomeAccessStateTypeId, "Open"); + break; + case AccessRfid: + thing->setStateValue(goeHomeAccessStateTypeId, "RFID"); + break; + case AccessAuto: + thing->setStateValue(goeHomeAccessStateTypeId, "Automatic"); + break; + } + } + + if (statusMap.contains("tma")) { + QVariantList temperatureSensorList = statusMap.value("tma").toList(); + if (temperatureSensorList.count() >= 1) + thing->setStateValue(goeHomeTemperatureSensor1StateTypeId, temperatureSensorList.at(0).toDouble()); + + if (temperatureSensorList.count() >= 2) + thing->setStateValue(goeHomeTemperatureSensor2StateTypeId, temperatureSensorList.at(1).toDouble()); + + if (temperatureSensorList.count() >= 3) + thing->setStateValue(goeHomeTemperatureSensor3StateTypeId, temperatureSensorList.at(2).toDouble()); + + if (temperatureSensorList.count() >= 4) + thing->setStateValue(goeHomeTemperatureSensor4StateTypeId, temperatureSensorList.at(3).toDouble()); + } + + if (statusMap.contains("eto")) + thing->setStateValue(goeHomeTotalEnergyConsumedStateTypeId, statusMap.value("eto").toUInt() / 1000.0); // Wh -> kWh + + if (statusMap.contains("wh")) + thing->setStateValue(goeHomeSessionEnergyStateTypeId, statusMap.value("wh").toUInt() / 1000.0); // Wh -> kWh + + if (statusMap.contains("alw")) + thing->setStateValue(goeHomePowerStateTypeId, (statusMap.value("alw").toUInt() == 0 ? false : true)); + + if (statusMap.contains("upd")) + thing->setStateValue(goeHomeUpdateAvailableStateTypeId, (statusMap.value("upd").toUInt() == 0 ? false : true)); + + if (statusMap.contains("fwv")) + thing->setStateValue(goeHomeFirmwareVersionStateTypeId, statusMap.value("fwv").toString()); + + // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement this + if (statusMap.contains("amp")) + thing->setStateValue(goeHomeMaxChargingCurrentStateTypeId, statusMap.value("amp").toUInt()); + + if (statusMap.contains("adi")) + thing->setStateValue(goeHomeAdapterConnectedStateTypeId, (statusMap.value("adi").toUInt() == 0 ? false : true)); + + if (statusMap.contains("fhz")) + thing->setStateValue(goeHomeFrequencyStateTypeId, statusMap.value("fhz").toDouble()); + + if (statusMap.contains("cbl")) + thing->setStateValue(goeHomeCableType2AmpereStateTypeId, statusMap.value("cbl").toUInt()); + + if (statusMap.contains("ama")) + thing->setStateValue(goeHomeAbsoluteMaxAmpereStateTypeId, statusMap.value("ama").toUInt()); + + if (statusMap.contains("var")) { + uint variant = statusMap.value("var").toUInt(); + uint variantLimit = 16; // 11 kW + if (variant == 22) // 22 kW + variantLimit = 32; + + thing->setStateValue(goeHomeModelMaxAmpereStateTypeId, variantLimit); + } + + if (statusMap.contains("cbl") || statusMap.contains("ama")) { + // Check charging limits + uint amaLimit = thing->stateValue(goeHomeAbsoluteMaxAmpereStateTypeId).toUInt(); + uint cableLimit = thing->stateValue(goeHomeCableType2AmpereStateTypeId).toUInt(); + uint modelLimit = thing->stateValue(goeHomeModelMaxAmpereStateTypeId).toUInt(); + + uint finalLimit = 0; + if (cableLimit != 0) { + finalLimit = qMin(amaLimit, cableLimit); + } else { + finalLimit = amaLimit; + } + // Check hardware variant: 11 -> 16A and 22 -> 32A + if (modelLimit != 0) + finalLimit = qMin(finalLimit, modelLimit); + + thing->setStateMaxValue(goeHomeMaxChargingCurrentStateTypeId, finalLimit); + } + + if (statusMap.contains("pnp") && statusMap.value("pnp").toUInt() != 0) + thing->setStateValue(goeHomePhaseCountStateTypeId, statusMap.value("pnp").toUInt()); + + if (statusMap.contains("nrg")) { + // Parse nrg array + uint voltagePhaseA = 0; uint voltagePhaseB = 0; uint voltagePhaseC = 0; + double amperePhaseA = 0; double amperePhaseB = 0; double amperePhaseC = 0; + double currentPower = 0; double powerPhaseA = 0; double powerPhaseB = 0; double powerPhaseC = 0; + QVariantList measurementList = statusMap.value("nrg").toList(); + + if (measurementList.count() >= 1) + voltagePhaseA = measurementList.at(0).toUInt(); + + if (measurementList.count() >= 2) + voltagePhaseB = measurementList.at(1).toUInt(); + + if (measurementList.count() >= 3) + voltagePhaseC = measurementList.at(2).toUInt(); + + if (measurementList.count() >= 5) + amperePhaseA = measurementList.at(4).toUInt(); + + if (measurementList.count() >= 6) + amperePhaseB = measurementList.at(5).toUInt(); + + if (measurementList.count() >= 7) + amperePhaseC = measurementList.at(6).toUInt(); + + if (measurementList.count() >= 8) + powerPhaseA = measurementList.at(7).toUInt(); + if (measurementList.count() >= 9) + powerPhaseB = measurementList.at(8).toUInt() ; + + if (measurementList.count() >= 10) + powerPhaseC = measurementList.at(9).toUInt(); + + if (measurementList.count() >= 12) + currentPower = measurementList.at(11).toUInt(); + + // Update all states + thing->setStateValue(goeHomeVoltagePhaseAStateTypeId, voltagePhaseA); + thing->setStateValue(goeHomeVoltagePhaseBStateTypeId, voltagePhaseB); + thing->setStateValue(goeHomeVoltagePhaseCStateTypeId, voltagePhaseC); + thing->setStateValue(goeHomeCurrentPhaseAStateTypeId, amperePhaseA); + thing->setStateValue(goeHomeCurrentPhaseBStateTypeId, amperePhaseB); + thing->setStateValue(goeHomeCurrentPhaseCStateTypeId, amperePhaseC); + thing->setStateValue(goeHomeCurrentPowerPhaseAStateTypeId, powerPhaseA); + thing->setStateValue(goeHomeCurrentPowerPhaseBStateTypeId, powerPhaseB); + thing->setStateValue(goeHomeCurrentPowerPhaseCStateTypeId, powerPhaseC); + + thing->setStateValue(goeHomeCurrentPowerStateTypeId, currentPower); + + // Check how many phases are actually charging, and update the phase count only if something happens on the phases (current or power) + if (amperePhaseA != 0 || amperePhaseB != 0 || amperePhaseC != 0) { + uint phaseCount = 0; + if (amperePhaseA != 0) + phaseCount += 1; + + if (amperePhaseB != 0) + phaseCount += 1; + + if (amperePhaseC != 0) + phaseCount += 1; + + // Use this loginc only if we don't have pnp available + if (!statusMap.contains("pnp") || statusMap.value("pnp").toUInt() == 0) { + thing->setStateValue(goeHomePhaseCountStateTypeId, phaseCount); + } + } + } +} + +QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequestV2(const QHostAddress &address, const QUrlQuery &configuration) +{ + QUrl requestUrl; + requestUrl.setScheme("http"); + requestUrl.setHost(address.toString()); + requestUrl.setPath("/api/set"); + requestUrl.setQuery(configuration); + return QNetworkRequest(requestUrl); +} + +void IntegrationPluginGoECharger::setupMqttChannelV2(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap) +{ + Thing *thing = info->thing(); + + QString serialNumber = thing->paramValue(goeHomeThingSerialNumberParamTypeId).toString(); + + // At least in version 51.1 + QString clientId = QString("go-echarger_%1").arg(serialNumber); + QString statusTopic = QString("/go-eCharger/%1/#").arg(serialNumber); + qCDebug(dcGoECharger()) << "Setting up mqtt channel for" << thing << address.toString() << statusTopic; + + // Somehow limited to 8 characters... + QString username = QString::fromUtf8(QUuid::createUuid().toByteArray().toHex().left(8)); + QString password = QString::fromUtf8(QUuid::createUuid().toByteArray().toHex().left(8)); + + MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(clientId, username, password, address, {statusTopic}); + if (!channel) { + qCWarning(dcGoECharger()) << "Failed to create MQTT channel for" << thing; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error creating MQTT channel. Please check MQTT server settings.")); + return; + } + + m_mqttChannelsV2.insert(thing, channel); + connect(channel, &MqttChannel::clientConnected, this, &IntegrationPluginGoECharger::onMqttClientV2Connected); + connect(channel, &MqttChannel::clientDisconnected, this, &IntegrationPluginGoECharger::onMqttClientV2Disconnected); + + // Build the mqtt url + QUrl mqttUrl; + mqttUrl.setScheme("mqtt"); + mqttUrl.setHost(channel->serverAddress().toString()); + mqttUrl.setPort(channel->serverPort()); + mqttUrl.setUserName(channel->username()); + mqttUrl.setPassword(channel->password()); + + // The query item must be JSON encoded, meaning: strings need quouts... for whatever reason... + QUrlQuery query; + query.addQueryItem("mcu", "\"" + mqttUrl.toString() + "\""); + query.addQueryItem("mce", "true"); + + QNetworkRequest request = buildConfigurationRequestV2(address, query); + qCDebug(dcGoECharger()) << "Configure nymea mqtt server address on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString() << reply->readAll(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to set mqtt configuration for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The wallbox returned invalid data.")); + return; + } + + qCDebug(dcGoECharger()) << qUtf8Printable(jsonDoc.toJson()); + + info->finish(Thing::ThingErrorNoError); + qCDebug(dcGoECharger()) << "Configuration of MQTT for" << thing << "finished successfully"; + // Update states... + updateV2(thing, statusMap); + + // From now on we listen to topics + connect(channel, &MqttChannel::publishReceived, thing, [=](MqttChannel* channel, const QString &topic, const QByteArray &payload){ + QString propertyKey = topic.split("/").last(); + QString propertyValue = QString::fromUtf8(payload); + // Well...no better idea for now to keep the APIs / parsing methods + // compatible trought different APIs and protocols + QString statusMapJsonString = QString("{\"%1\":%2}").arg(propertyKey).arg(propertyValue); + QJsonDocument jsonDoc = QJsonDocument::fromJson(statusMapJsonString.toUtf8()); + + // Mute the spaming stuff.. + if (propertyKey != "fhz" && propertyKey != "rssi" && propertyKey != "utc" && propertyKey != "loc" && propertyKey != "rbt") { + qCDebug(dcGoECharger()) << thing->name() << channel->clientId() << "publish received" << topic << payload; + //qCDebug(dcGoECharger()) << "-->" << jsonDoc.toJson(QJsonDocument::Compact); + } + updateV2(thing, jsonDoc.toVariant().toMap()); + }); + }); +} + +void IntegrationPluginGoECharger::reconfigureMqttChannelV2(Thing *thing) +{ + QString serialNumber = thing->paramValue(goeHomeThingSerialNumberParamTypeId).toString(); + QHostAddress address = getHostAddress(thing); + + qCDebug(dcGoECharger()) << "Reconfigure mqtt channel for" << thing; + // At least in version 51.1 + QString clientId = QString("go-echarger_%1").arg(serialNumber); + QString statusTopic = QString("/go-eCharger/%1/#").arg(serialNumber); + qCDebug(dcGoECharger()) << "Reconfigure mqtt channel for" << thing << address.toString() << statusTopic; + + // Somehow limited to 8 characters... + QString username = QString::fromUtf8(QUuid::createUuid().toByteArray().toHex().left(8)); + QString password = QString::fromUtf8(QUuid::createUuid().toByteArray().toHex().left(8)); + + MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(clientId, username, password, address, {statusTopic}); + if (!channel) { + qCWarning(dcGoECharger()) << "Failed to create MQTT channel for" << thing; + return; + } + + // Clean up existing channel if there is one + if (m_mqttChannelsV2.contains(thing)) { + qCDebug(dcGoECharger()) << "Release old mqtt channel..."; + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + } + + m_mqttChannelsV2.insert(thing, channel); + connect(channel, &MqttChannel::clientConnected, this, &IntegrationPluginGoECharger::onMqttClientV2Connected); + connect(channel, &MqttChannel::clientDisconnected, this, &IntegrationPluginGoECharger::onMqttClientV2Disconnected); + + // Build the mqtt url + QUrl mqttUrl; + mqttUrl.setScheme("mqtt"); + mqttUrl.setHost(channel->serverAddress().toString()); + mqttUrl.setPort(channel->serverPort()); + mqttUrl.setUserName(channel->username()); + mqttUrl.setPassword(channel->password()); + + // The query item must be JSON encoded, meaning: strings need quouts... for whatever reason... + QUrlQuery query; + query.addQueryItem("mcu", "\"" + mqttUrl.toString() + "\""); + + QNetworkRequest request = buildConfigurationRequestV2(address, query); + qCDebug(dcGoECharger()) << "Configure nymea mqtt server address on" << thing << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [=](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcGoECharger()) << "HTTP status reply returned error:" << reply->errorString() << reply->readAll(); + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcGoECharger()) << "Failed to set mqtt configuration for thing " << thing->name() << qUtf8Printable(data) << error.errorString(); + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + return; + } + + QVariantMap responseCode = jsonDoc.toVariant().toMap(); + if (!responseCode.value("mcu", false).toBool()) { + qCWarning(dcGoECharger()) << "Failed to configure mqtt on" << thing; + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + return; + } + + qCDebug(dcGoECharger()) << "Configuration of MQTT for" << thing << "finished successfully"; + // From now on we listen to topics + connect(channel, &MqttChannel::publishReceived, thing, [=](MqttChannel* channel, const QString &topic, const QByteArray &payload){ + QString propertyKey = topic.split("/").last(); + QString propertyValue = QString::fromUtf8(payload); + // Well...no better idea for now to keep the APIs / parsing methods + // compatible trought different APIs and protocols + QString statusMapJsonString = QString("{\"%1\":%2}").arg(propertyKey).arg(propertyValue); + QJsonDocument jsonDoc = QJsonDocument::fromJson(statusMapJsonString.toUtf8()); + + // Mute the spaming stuff.. + if (propertyKey != "fhz" && propertyKey != "rssi" && propertyKey != "utc" && propertyKey != "loc" && propertyKey != "rbt") { + qCDebug(dcGoECharger()) << thing->name() << channel->clientId() << "publish received" << topic << payload; + //qCDebug(dcGoECharger()) << "-->" << jsonDoc.toJson(QJsonDocument::Compact); + } + updateV2(thing, jsonDoc.toVariant().toMap()); + }); + + }); +} + void IntegrationPluginGoECharger::refreshHttp() { // Update all things which don't use mqtt @@ -955,7 +1439,7 @@ void IntegrationPluginGoECharger::refreshHttp() updateV1(thing, statusMap); break; case ApiVersion2: - // TODO: + updateV2(thing, statusMap); break; } }); @@ -1011,4 +1495,26 @@ void IntegrationPluginGoECharger::onMqttPublishV1Received(MqttChannel *channel, } } +void IntegrationPluginGoECharger::onMqttClientV2Connected(MqttChannel *channel) +{ + Thing *thing = m_mqttChannelsV2.key(channel); + if (!thing) { + qCWarning(dcGoECharger()) << "Received a client disconnect for an unknown thing. Ignoring the event."; + return; + } + qCDebug(dcGoECharger()) << thing << "connected"; + thing->setStateValue("connected", false); +} + +void IntegrationPluginGoECharger::onMqttClientV2Disconnected(MqttChannel *channel) +{ + Thing *thing = m_mqttChannelsV1.key(channel); + if (!thing) { + qCWarning(dcGoECharger()) << "Received a client disconnect for an unknown thing. Ignoring the event."; + return; + } + + qCDebug(dcGoECharger()) << thing << "connected"; + thing->setStateValue("connected", false); +} diff --git a/goecharger/integrationplugingoecharger.h b/goecharger/integrationplugingoecharger.h index 8ce430a2..fd9e02ae 100644 --- a/goecharger/integrationplugingoecharger.h +++ b/goecharger/integrationplugingoecharger.h @@ -118,11 +118,16 @@ private: // API V1 void updateV1(Thing *thing, const QVariantMap &statusMap); - void sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration); QNetworkRequest buildConfigurationRequestV1(const QHostAddress &address, const QString &configuration); + void sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration); void setupMqttChannelV1(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap); void reconfigureMqttChannelV1(Thing *thing, const QVariantMap &statusMap); + // API V2 + void updateV2(Thing *thing, const QVariantMap &statusMap); + QNetworkRequest buildConfigurationRequestV2(const QHostAddress &address, const QUrlQuery &configuration); + void setupMqttChannelV2(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap); + void reconfigureMqttChannelV2(Thing *thing); private slots: void refreshHttp(); @@ -132,6 +137,10 @@ private slots: void onMqttClientV1Disconnected(MqttChannel* channel); void onMqttPublishV1Received(MqttChannel* channel, const QString &topic, const QByteArray &payload); + // API V2 + void onMqttClientV2Connected(MqttChannel* channel); + void onMqttClientV2Disconnected(MqttChannel* channel); + }; #endif // INTEGRATIONPLUGINGOECHARGER_H From de9bf5d3dbf254efa952e4630128e1a68d38ead9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 21 Jul 2022 12:00:33 +0200 Subject: [PATCH 03/11] Update README --- goecharger/README.md | 18 +++++++++++++----- goecharger/integrationplugingoecharger.cpp | 1 + goecharger/integrationplugingoecharger.json | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/goecharger/README.md b/goecharger/README.md index 93eaa813..406e19c8 100644 --- a/goecharger/README.md +++ b/goecharger/README.md @@ -2,23 +2,31 @@ nymea plugin for go-eCharger smart wallbox for electic vehicles. -If you are using the original go-e App or other client services to communicate with the wallbox, disable MQTT during the setup in order to make sure all services are able to communicate with the wallbox. There is no support for multiple MQTT clients on go-e devices, thus nymea defaults to HTTP to prevent constant reconfiguration trough the clients. +In order to make nymea work with go-e, please make sure you have enable `API V2` in the official app. -The preferred way of communicating would be MQTT, default is HTTP. +If you are using the original go-e App or other client services to communicate with the wallbox, disable MQTT during the setup in order to make +sure all services are able to communicate with the wallbox. +There is no support for multiple MQTT clients on go-e devices, thus nymea defaults to HTTP to prevent constant +reconfiguration trough the clients. + +The preferred way of communicating would be MQTT (API V2), default is HTTP (API V1). ## Supported Things -* go-eCharger Home +* go-eCharger Home (Hardware V1 and V2 using `API V1`) +* go-eCharger Home (Hardware V3 using `API V2`) ## Requirements * The package "nymea-plugin-goecharger" must be installed. * The device must be in the same local area network as nymea. -* The Firmware version has to be at least `030.00`. +* The Firmware version has to be at least `030.0` (API V1). +* The Firmware version has to be at least `051.1` (API V2). ## Developer documentation -The documentation of the API can be found [here](https://github.com/goecharger/go-eCharger-API-v1). +The documentation of the API V1 can be found [here](https://github.com/goecharger/go-eCharger-API-v1). +The documentation of the API v2 can be found [here](https://github.com/goecharger/go-eCharger-API-v2). ## More diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 85ebf0b4..480d95a6 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -852,6 +852,7 @@ void IntegrationPluginGoECharger::reconfigureMqttChannelV1(Thing *thing, const Q QString clientId = QString("go-eCharger:%1:%2").arg(serialNumber).arg(statusMap.value("rbc").toInt()); QString statusTopic = QString("/go-eCharger/%1/status").arg(serialNumber); QString commandTopic = QString("/go-eCharger/%1/cmd/req").arg(serialNumber); + qCDebug(dcGoECharger()) << "Setting up mqtt channel for" << thing << address.toString() << statusTopic << commandTopic; MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(clientId, address, {statusTopic, commandTopic}); diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index c29dbb75..97ff8607 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -33,7 +33,7 @@ "name":"useMqtt", "displayName": "Use MQTT interface", "type": "bool", - "defaultValue": true + "defaultValue": false }, { "id": "3ad014e2-c948-406e-99be-eba1d866ea20", From 5c9e7888ed47fd86e30c9131bf80d75f4b92b530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 21 Jul 2022 14:17:09 +0200 Subject: [PATCH 04/11] Fix execute actions for APIv2 --- goecharger/integrationplugingoecharger.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 480d95a6..8d9c589e 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -225,7 +225,9 @@ void IntegrationPluginGoECharger::thingRemoved(Thing *thing) } break; case ApiVersion2: - + if (m_mqttChannelsV2.contains(thing)) { + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + } break; } @@ -285,13 +287,16 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) if (action.actionTypeId() == goeHomePowerActionTypeId) { bool power = action.paramValue(goeHomePowerActionPowerParamTypeId).toBool(); qCDebug(dcGoECharger()) << "Setting charging allowed to" << power; + // Warning: using QUrlQuery not always works here due to standard mixing from go-e: + // The url query has to be JSON encoded, i.e. /set?fna="mein charger" QUrlQuery configuration; + // 0 neutral (prefere on), 1 off, 2 on force configuration.addQueryItem("frc", (power ? "0": "1")); QNetworkRequest request = buildConfigurationRequestV2(address, configuration); - QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { - qCWarning(dcGoECharger()) << "Execute action failed. HTTP reply returned error:" << thing->name() << reply->errorString() << reply->readAll(); + qCWarning(dcGoECharger()) << "Execute action failed. TP reply returned error:" << thing->name() << reply->errorString() << reply->readAll(); info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); return; } @@ -320,12 +325,12 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) } else if (action.actionTypeId() == goeHomeMaxChargingCurrentActionTypeId) { uint ampere = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); qCDebug(dcGoECharger()) << "Setting max charging current to" << ampere << "A"; - // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement amx - // Maybe check if the user sets it or a automatism + // Warning: using QUrlQuery not always works here due to standard mixing from go-e: + // The url query has to be JSON encoded, i.e. /set?fna="mein charger" QUrlQuery configuration; configuration.addQueryItem("amp", QString::number(ampere)); QNetworkRequest request = buildConfigurationRequestV2(address, configuration); - QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { From d13e90d72255b9042cb5e9b71f59524c8c27fa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 21 Jul 2022 14:23:04 +0200 Subject: [PATCH 05/11] Disable too verbose debug prints --- goecharger/integrationplugingoecharger.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 8d9c589e..b5dafda1 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -638,7 +638,6 @@ void IntegrationPluginGoECharger::sendActionRequestV1(Thing *thing, ThingActionI // Lets use rest here since we get a reply on the rest request. // For using MQTT publish to topic "go-eCharger//cmd/req" QHostAddress address = getHostAddress(thing); - QNetworkRequest request = buildConfigurationRequestV1(address, configuration); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -1409,8 +1408,9 @@ void IntegrationPluginGoECharger::refreshHttp() if (m_pendingReplies.contains(thing) && m_pendingReplies.value(thing)) continue; - qCDebug(dcGoECharger()) << "Refresh HTTP status from" << thing; - QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); + QNetworkRequest request = buildStatusRequest(thing); + qCDebug(dcGoECharger()) << "Refresh HTTP status from" << thing->name() << request.url().toString(); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); m_pendingReplies.insert(thing, reply); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); @@ -1434,11 +1434,10 @@ void IntegrationPluginGoECharger::refreshHttp() } ApiVersion apiVersion = getApiVersion(thing); - // Valid json data received, connected true thing->setStateValue("connected", true); - qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); + //qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); QVariantMap statusMap = jsonDoc.toVariant().toMap(); switch (apiVersion) { case ApiVersion1: From 1a7ee36e2d83bb610986a53b1bd95b8b8ed53269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 21 Jul 2022 16:38:51 +0200 Subject: [PATCH 06/11] Update mqtt connected state to thing after setup --- goecharger/integrationplugingoecharger.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index b5dafda1..0cbe1aef 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -212,6 +212,22 @@ void IntegrationPluginGoECharger::postSetupThing(Thing *thing) connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginGoECharger::refreshHttp); m_refreshTimer->start(); } + + if (thing->paramValue(goeHomeThingUseMqttParamTypeId).toBool()) { + // make sure the connected state has been set properly + switch (getApiVersion(thing)) { + case ApiVersion1: + if (m_mqttChannelsV1.contains(thing)) { + thing->setStateValue("connected", m_mqttChannelsV1.value(thing)->isConnected()); + } + break; + case ApiVersion2: + if (m_mqttChannelsV2.contains(thing)) { + thing->setStateValue("connected", m_mqttChannelsV2.value(thing)->isConnected()); + } + break; + } + } } } From c3466b6b2e2210b9ba403aa0a2b7c4b473b722f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 22 Jul 2022 08:24:23 +0200 Subject: [PATCH 07/11] Fix connected state for mqtt and clean up replies properly --- goecharger/integrationplugingoecharger.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 0cbe1aef..ccc03c9e 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -310,6 +310,8 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) configuration.addQueryItem("frc", (power ? "0": "1")); QNetworkRequest request = buildConfigurationRequestV2(address, configuration); QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(info, &ThingActionInfo::aborted, reply, &QNetworkReply::abort); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { qCWarning(dcGoECharger()) << "Execute action failed. TP reply returned error:" << thing->name() << reply->errorString() << reply->readAll(); @@ -347,7 +349,8 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) configuration.addQueryItem("amp", QString::number(ampere)); QNetworkRequest request = buildConfigurationRequestV2(address, configuration); QNetworkReply *reply = hardwareManager()->networkManager()->get(request); - + connect(info, &ThingActionInfo::aborted, reply, &QNetworkReply::abort); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { qCWarning(dcGoECharger()) << "Execute action failed. HTTP status reply returned error:" << thing->name() << reply->errorString() << reply->readAll(); @@ -387,6 +390,7 @@ void IntegrationPluginGoECharger::setupGoeHome(ThingSetupInfo *info) { Thing *thing = info->thing(); QNetworkReply *reply = hardwareManager()->networkManager()->get(buildStatusRequest(thing)); + connect(info, &ThingSetupInfo::aborted, reply, &QNetworkReply::abort); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { @@ -656,6 +660,7 @@ void IntegrationPluginGoECharger::sendActionRequestV1(Thing *thing, ThingActionI QHostAddress address = getHostAddress(thing); QNetworkRequest request = buildConfigurationRequestV1(address, configuration); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(info, &ThingActionInfo::aborted, reply, &QNetworkReply::abort); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { @@ -1525,7 +1530,7 @@ void IntegrationPluginGoECharger::onMqttClientV2Connected(MqttChannel *channel) } qCDebug(dcGoECharger()) << thing << "connected"; - thing->setStateValue("connected", false); + thing->setStateValue("connected", true); } void IntegrationPluginGoECharger::onMqttClientV2Disconnected(MqttChannel *channel) From 2716e5445159c93854584a9eb28cb7a5bec5413a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 28 Jul 2022 10:03:28 +0200 Subject: [PATCH 08/11] Update translations --- goecharger/integrationplugingoecharger.cpp | 1 - ...fca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts | 562 ++++-------------- 2 files changed, 120 insertions(+), 443 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index ccc03c9e..1020ff15 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -247,7 +247,6 @@ void IntegrationPluginGoECharger::thingRemoved(Thing *thing) break; } - // Cleanup possible pending replies if (m_pendingReplies.contains(thing) && m_pendingReplies.value(thing)) { m_pendingReplies.take(thing)->abort(); diff --git a/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts b/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts index 8aa774e4..b6f4282b 100644 --- a/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts +++ b/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts @@ -4,583 +4,244 @@ GoECharger - - + Access - The name of the ParamType (ThingClass: goeHome, EventType: access, ID: {d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201}) ----------- -The name of the StateType ({d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201}) of ThingClass goeHome + The name of the StateType ({d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201}) of ThingClass goeHome - - Access changed - The name of the EventType ({d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201}) of ThingClass goeHome - - - - - + Adapter connected - The name of the ParamType (ThingClass: goeHome, EventType: adapterConnected, ID: {d557e59e-ca22-4aff-bf80-dfee44db0f69}) ----------- -The name of the StateType ({d557e59e-ca22-4aff-bf80-dfee44db0f69}) of ThingClass goeHome + The name of the StateType ({d557e59e-ca22-4aff-bf80-dfee44db0f69}) of ThingClass goeHome - - Adapter connected changed - The name of the EventType ({d557e59e-ca22-4aff-bf80-dfee44db0f69}) of ThingClass goeHome - - - - - - - + + + Allow charging The name of the ParamType (ThingClass: goeHome, ActionType: power, ID: {8a7ab9f1-0143-494c-98ee-69f94125fe42}) ---------- The name of the ActionType ({8a7ab9f1-0143-494c-98ee-69f94125fe42}) of ThingClass goeHome ---------- -The name of the ParamType (ThingClass: goeHome, EventType: power, ID: {8a7ab9f1-0143-494c-98ee-69f94125fe42}) ----------- The name of the StateType ({8a7ab9f1-0143-494c-98ee-69f94125fe42}) of ThingClass goeHome - - Allow charging changed - The name of the EventType ({8a7ab9f1-0143-494c-98ee-69f94125fe42}) of ThingClass goeHome - - - - - + Cable ampere encoding - The name of the ParamType (ThingClass: goeHome, EventType: cableType2Ampere, ID: {f9091651-1522-4387-b300-906abd907fb3}) ----------- -The name of the StateType ({f9091651-1522-4387-b300-906abd907fb3}) of ThingClass goeHome + The name of the StateType ({f9091651-1522-4387-b300-906abd907fb3}) of ThingClass goeHome - - Cable ampere encoding changed - The name of the EventType ({f9091651-1522-4387-b300-906abd907fb3}) of ThingClass goeHome - - - - - + Car plugged in - The name of the ParamType (ThingClass: goeHome, EventType: pluggedIn, ID: {6cb155b1-0831-47bc-8657-17ca68716684}) ----------- -The name of the StateType ({6cb155b1-0831-47bc-8657-17ca68716684}) of ThingClass goeHome + The name of the StateType ({6cb155b1-0831-47bc-8657-17ca68716684}) of ThingClass goeHome - - Car plugged in changed - The name of the EventType ({6cb155b1-0831-47bc-8657-17ca68716684}) of ThingClass goeHome - - - - - + Car state - The name of the ParamType (ThingClass: goeHome, EventType: carStatus, ID: {c69053bc-3a53-4e76-868b-ccf0958e9e44}) ----------- -The name of the StateType ({c69053bc-3a53-4e76-868b-ccf0958e9e44}) of ThingClass goeHome + The name of the StateType ({c69053bc-3a53-4e76-868b-ccf0958e9e44}) of ThingClass goeHome - - Car status changed - The name of the EventType ({c69053bc-3a53-4e76-868b-ccf0958e9e44}) of ThingClass goeHome - - - - - + Charging - The name of the ParamType (ThingClass: goeHome, EventType: charging, ID: {48c6cdb8-9fc1-4c14-95df-3e2c62e59361}) ----------- -The name of the StateType ({48c6cdb8-9fc1-4c14-95df-3e2c62e59361}) of ThingClass goeHome + The name of the StateType ({48c6cdb8-9fc1-4c14-95df-3e2c62e59361}) of ThingClass goeHome - - Charging changed - The name of the EventType ({48c6cdb8-9fc1-4c14-95df-3e2c62e59361}) of ThingClass goeHome - - - - - - + + Charging current The name of the ParamType (ThingClass: goeHome, ActionType: maxChargingCurrent, ID: {446fb786-bfbe-4938-963c-73d02184573f}) ---------- -The name of the ParamType (ThingClass: goeHome, EventType: maxChargingCurrent, ID: {446fb786-bfbe-4938-963c-73d02184573f}) ----------- The name of the StateType ({446fb786-bfbe-4938-963c-73d02184573f}) of ThingClass goeHome - - Charging current changed - The name of the EventType ({446fb786-bfbe-4938-963c-73d02184573f}) of ThingClass goeHome - - - - - - - Cloud enabled - The name of the ParamType (ThingClass: goeHome, ActionType: cloud, ID: {ac849296-3f70-4b1b-aa30-127d774667bb}) ----------- -The name of the ParamType (ThingClass: goeHome, EventType: cloud, ID: {ac849296-3f70-4b1b-aa30-127d774667bb}) ----------- -The name of the StateType ({ac849296-3f70-4b1b-aa30-127d774667bb}) of ThingClass goeHome - - - - - Cloud enabled changed - The name of the EventType ({ac849296-3f70-4b1b-aa30-127d774667bb}) of ThingClass goeHome - - - - - + Connected - The name of the ParamType (ThingClass: goeHome, EventType: connected, ID: {a5afaad5-78bf-4cac-b98d-7eae31aac518}) ----------- -The name of the StateType ({a5afaad5-78bf-4cac-b98d-7eae31aac518}) of ThingClass goeHome + The name of the StateType ({a5afaad5-78bf-4cac-b98d-7eae31aac518}) of ThingClass goeHome - - Connected changed - The name of the EventType ({a5afaad5-78bf-4cac-b98d-7eae31aac518}) of ThingClass goeHome - - - - - + Current power - The name of the ParamType (ThingClass: goeHome, EventType: currentPower, ID: {f00cfcab-9271-48fa-b843-89244c9551ae}) ----------- -The name of the StateType ({f00cfcab-9271-48fa-b843-89244c9551ae}) of ThingClass goeHome + The name of the StateType ({f00cfcab-9271-48fa-b843-89244c9551ae}) of ThingClass goeHome - - Current power changed - The name of the EventType ({f00cfcab-9271-48fa-b843-89244c9551ae}) of ThingClass goeHome - - - - - + Current power phase A - The name of the ParamType (ThingClass: goeHome, EventType: currentPowerPhaseA, ID: {c6f68517-c4cd-415d-9455-b1731f7d9a1a}) ----------- -The name of the StateType ({c6f68517-c4cd-415d-9455-b1731f7d9a1a}) of ThingClass goeHome + The name of the StateType ({c6f68517-c4cd-415d-9455-b1731f7d9a1a}) of ThingClass goeHome - - Current power phase A changed - The name of the EventType ({c6f68517-c4cd-415d-9455-b1731f7d9a1a}) of ThingClass goeHome - - - - - + Current power phase B - The name of the ParamType (ThingClass: goeHome, EventType: currentPowerPhaseB, ID: {92005049-9ab9-4d7d-a7b6-6ab1a36c5f5f}) ----------- -The name of the StateType ({92005049-9ab9-4d7d-a7b6-6ab1a36c5f5f}) of ThingClass goeHome + The name of the StateType ({92005049-9ab9-4d7d-a7b6-6ab1a36c5f5f}) of ThingClass goeHome - - Current power phase B changed - The name of the EventType ({92005049-9ab9-4d7d-a7b6-6ab1a36c5f5f}) of ThingClass goeHome - - - - - + Current power phase C - The name of the ParamType (ThingClass: goeHome, EventType: currentPowerPhaseC, ID: {1076fbd0-f42b-46e3-adc9-004361d6cd51}) ----------- -The name of the StateType ({1076fbd0-f42b-46e3-adc9-004361d6cd51}) of ThingClass goeHome + The name of the StateType ({1076fbd0-f42b-46e3-adc9-004361d6cd51}) of ThingClass goeHome - - Current power phase C changed - The name of the EventType ({1076fbd0-f42b-46e3-adc9-004361d6cd51}) of ThingClass goeHome - - - - - + Firmware version - The name of the ParamType (ThingClass: goeHome, EventType: firmwareVersion, ID: {5d18b48d-b886-409e-ab2e-336d9c94a55c}) ----------- -The name of the StateType ({5d18b48d-b886-409e-ab2e-336d9c94a55c}) of ThingClass goeHome + The name of the StateType ({5d18b48d-b886-409e-ab2e-336d9c94a55c}) of ThingClass goeHome - - Firmware version changed - The name of the EventType ({5d18b48d-b886-409e-ab2e-336d9c94a55c}) of ThingClass goeHome - - - - - IP address - The name of the ParamType (ThingClass: goeHome, Type: thing, ID: {4342b72c-99d0-41a5-abc6-ea6c1cc1352c}) - - - - - - - Led brightness - The name of the ParamType (ThingClass: goeHome, ActionType: ledBrightness, ID: {b06479d5-7a38-4fbd-867e-e55bdb54651b}) ----------- -The name of the ParamType (ThingClass: goeHome, EventType: ledBrightness, ID: {b06479d5-7a38-4fbd-867e-e55bdb54651b}) ----------- -The name of the StateType ({b06479d5-7a38-4fbd-867e-e55bdb54651b}) of ThingClass goeHome - - - - - Led brightness changed - The name of the EventType ({b06479d5-7a38-4fbd-867e-e55bdb54651b}) of ThingClass goeHome - - - - - - - Led energy saving enabled - The name of the ParamType (ThingClass: goeHome, ActionType: ledEnergySave, ID: {048a4c98-3ee4-4d02-ad48-6d70f31fce8c}) ----------- -The name of the ParamType (ThingClass: goeHome, EventType: ledEnergySave, ID: {048a4c98-3ee4-4d02-ad48-6d70f31fce8c}) ----------- -The name of the StateType ({048a4c98-3ee4-4d02-ad48-6d70f31fce8c}) of ThingClass goeHome - - - - - Led energy saving enabled enabled changed - The name of the EventType ({048a4c98-3ee4-4d02-ad48-6d70f31fce8c}) of ThingClass goeHome - - - - + MAC address The name of the ParamType (ThingClass: goeHome, Type: thing, ID: {0e30e30f-ad96-417e-b739-cac85f75de39}) - - - + Maximal ampere - The name of the ParamType (ThingClass: goeHome, ActionType: absoluteMaxAmpere, ID: {58cd977d-22df-48c9-829a-81554130d607}) ----------- -The name of the ParamType (ThingClass: goeHome, EventType: absoluteMaxAmpere, ID: {58cd977d-22df-48c9-829a-81554130d607}) ----------- -The name of the StateType ({58cd977d-22df-48c9-829a-81554130d607}) of ThingClass goeHome + The name of the StateType ({58cd977d-22df-48c9-829a-81554130d607}) of ThingClass goeHome - - Maximal ampere changed - The name of the EventType ({58cd977d-22df-48c9-829a-81554130d607}) of ThingClass goeHome - - - - - + Number of charging phases - The name of the ParamType (ThingClass: goeHome, EventType: phaseCount, ID: {b78d805a-f97c-4c9d-a647-5fc98f8d6dd1}) ----------- -The name of the StateType ({b78d805a-f97c-4c9d-a647-5fc98f8d6dd1}) of ThingClass goeHome + The name of the StateType ({b78d805a-f97c-4c9d-a647-5fc98f8d6dd1}) of ThingClass goeHome - - Number of charging phases changed - The name of the EventType ({b78d805a-f97c-4c9d-a647-5fc98f8d6dd1}) of ThingClass goeHome - - - - - + Phase A current - The name of the ParamType (ThingClass: goeHome, EventType: currentPhaseA, ID: {c8aab9e2-ba53-43b9-95db-e2c3edc97e33}) ----------- -The name of the StateType ({c8aab9e2-ba53-43b9-95db-e2c3edc97e33}) of ThingClass goeHome + The name of the StateType ({c8aab9e2-ba53-43b9-95db-e2c3edc97e33}) of ThingClass goeHome - - Phase A current changed - The name of the EventType ({c8aab9e2-ba53-43b9-95db-e2c3edc97e33}) of ThingClass goeHome - - - - - Phase A volatage changed - The name of the EventType ({76da8f16-44a4-4242-b78b-09c9bb127623}) of ThingClass goeHome - - - - - + Phase A voltage - The name of the ParamType (ThingClass: goeHome, EventType: voltagePhaseA, ID: {76da8f16-44a4-4242-b78b-09c9bb127623}) ----------- -The name of the StateType ({76da8f16-44a4-4242-b78b-09c9bb127623}) of ThingClass goeHome + The name of the StateType ({76da8f16-44a4-4242-b78b-09c9bb127623}) of ThingClass goeHome - - + Phase B current - The name of the ParamType (ThingClass: goeHome, EventType: currentPhaseB, ID: {f11ac403-728d-48f3-8669-0e684faf9890}) ----------- -The name of the StateType ({f11ac403-728d-48f3-8669-0e684faf9890}) of ThingClass goeHome + The name of the StateType ({f11ac403-728d-48f3-8669-0e684faf9890}) of ThingClass goeHome - - Phase B current changed - The name of the EventType ({f11ac403-728d-48f3-8669-0e684faf9890}) of ThingClass goeHome - - - - - + Phase B voltage - The name of the ParamType (ThingClass: goeHome, EventType: voltagePhaseB, ID: {7df01eb4-0d4d-400c-b1bc-001ca83a6a3c}) ----------- -The name of the StateType ({7df01eb4-0d4d-400c-b1bc-001ca83a6a3c}) of ThingClass goeHome + The name of the StateType ({7df01eb4-0d4d-400c-b1bc-001ca83a6a3c}) of ThingClass goeHome - - Phase B voltage changed - The name of the EventType ({7df01eb4-0d4d-400c-b1bc-001ca83a6a3c}) of ThingClass goeHome - - - - - + Phase C current - The name of the ParamType (ThingClass: goeHome, EventType: currentPhaseC, ID: {55295e1c-50b0-400b-82e4-b3417b5ed4d1}) ----------- -The name of the StateType ({55295e1c-50b0-400b-82e4-b3417b5ed4d1}) of ThingClass goeHome + The name of the StateType ({55295e1c-50b0-400b-82e4-b3417b5ed4d1}) of ThingClass goeHome - - Phase C current changed - The name of the EventType ({55295e1c-50b0-400b-82e4-b3417b5ed4d1}) of ThingClass goeHome - - - - - + Phase C voltage - The name of the ParamType (ThingClass: goeHome, EventType: voltagePhaseC, ID: {31814cfe-626d-4168-802b-b7fc6592fc79}) ----------- -The name of the StateType ({31814cfe-626d-4168-802b-b7fc6592fc79}) of ThingClass goeHome + The name of the StateType ({31814cfe-626d-4168-802b-b7fc6592fc79}) of ThingClass goeHome - - Phase C voltage changed - The name of the EventType ({31814cfe-626d-4168-802b-b7fc6592fc79}) of ThingClass goeHome - - - - - + Serial number - The name of the ParamType (ThingClass: goeHome, EventType: serialNumber, ID: {8ecdf24b-daca-4b7a-98b5-3236f1e6ad85}) ----------- -The name of the StateType ({8ecdf24b-daca-4b7a-98b5-3236f1e6ad85}) of ThingClass goeHome + The name of the ParamType (ThingClass: goeHome, Type: thing, ID: {74abaff3-39e6-40be-b3c3-f41911d17e02}) - - Serial number changed - The name of the EventType ({8ecdf24b-daca-4b7a-98b5-3236f1e6ad85}) of ThingClass goeHome - - - - - + Session energy - The name of the ParamType (ThingClass: goeHome, EventType: sessionEnergy, ID: {e8258831-ad89-4d27-b295-e8c10dd42b76}) ----------- -The name of the StateType ({e8258831-ad89-4d27-b295-e8c10dd42b76}) of ThingClass goeHome + The name of the StateType ({e8258831-ad89-4d27-b295-e8c10dd42b76}) of ThingClass goeHome - - Session energy changed - The name of the EventType ({e8258831-ad89-4d27-b295-e8c10dd42b76}) of ThingClass goeHome - - - - + Set charging current The name of the ActionType ({446fb786-bfbe-4938-963c-73d02184573f}) of ThingClass goeHome - - Set cloud enabled - The name of the ActionType ({ac849296-3f70-4b1b-aa30-127d774667bb}) of ThingClass goeHome - - - - - Set led brightness - The name of the ActionType ({b06479d5-7a38-4fbd-867e-e55bdb54651b}) of ThingClass goeHome - - - - - Set led energy saving enabled - The name of the ActionType ({048a4c98-3ee4-4d02-ad48-6d70f31fce8c}) of ThingClass goeHome - - - - - Set maximal ampere - The name of the ActionType ({58cd977d-22df-48c9-829a-81554130d607}) of ThingClass goeHome - - - - - + Temperature 1 - The name of the ParamType (ThingClass: goeHome, EventType: temperatureSensor1, ID: {2bf1ebf1-0d8c-4209-ad35-4114d9861832}) ----------- -The name of the StateType ({2bf1ebf1-0d8c-4209-ad35-4114d9861832}) of ThingClass goeHome + The name of the StateType ({2bf1ebf1-0d8c-4209-ad35-4114d9861832}) of ThingClass goeHome - - Temperature 1 changed - The name of the EventType ({2bf1ebf1-0d8c-4209-ad35-4114d9861832}) of ThingClass goeHome - - - - - + Temperature 2 - The name of the ParamType (ThingClass: goeHome, EventType: temperatureSensor2, ID: {558e273a-4028-495a-902a-e4e932a0ae24}) ----------- -The name of the StateType ({558e273a-4028-495a-902a-e4e932a0ae24}) of ThingClass goeHome + The name of the StateType ({558e273a-4028-495a-902a-e4e932a0ae24}) of ThingClass goeHome - - Temperature 2 changed - The name of the EventType ({558e273a-4028-495a-902a-e4e932a0ae24}) of ThingClass goeHome - - - - - + Temperature 3 - The name of the ParamType (ThingClass: goeHome, EventType: temperatureSensor3, ID: {dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb}) ----------- -The name of the StateType ({dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb}) of ThingClass goeHome + The name of the StateType ({dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb}) of ThingClass goeHome - - Temperature 3 changed - The name of the EventType ({dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb}) of ThingClass goeHome - - - - - + Temperature 4 - The name of the ParamType (ThingClass: goeHome, EventType: temperatureSensor4, ID: {1953e29f-fe28-4016-9b05-f4baf4c311ff}) ----------- -The name of the StateType ({1953e29f-fe28-4016-9b05-f4baf4c311ff}) of ThingClass goeHome + The name of the StateType ({1953e29f-fe28-4016-9b05-f4baf4c311ff}) of ThingClass goeHome - - Temperature 4 changed - The name of the EventType ({1953e29f-fe28-4016-9b05-f4baf4c311ff}) of ThingClass goeHome - - - - - + Total energy - The name of the ParamType (ThingClass: goeHome, EventType: totalEnergyConsumed, ID: {d8f5abb6-5db3-4040-8829-553b1d881ce4}) ----------- -The name of the StateType ({d8f5abb6-5db3-4040-8829-553b1d881ce4}) of ThingClass goeHome + The name of the StateType ({d8f5abb6-5db3-4040-8829-553b1d881ce4}) of ThingClass goeHome - - Total energy changed - The name of the EventType ({d8f5abb6-5db3-4040-8829-553b1d881ce4}) of ThingClass goeHome - - - - - + Update available - The name of the ParamType (ThingClass: goeHome, EventType: updateAvailable, ID: {08b107bc-1284-455d-9e5a-6a1c3adc389f}) ----------- -The name of the StateType ({08b107bc-1284-455d-9e5a-6a1c3adc389f}) of ThingClass goeHome + The name of the StateType ({08b107bc-1284-455d-9e5a-6a1c3adc389f}) of ThingClass goeHome - - Update available changed - The name of the EventType ({08b107bc-1284-455d-9e5a-6a1c3adc389f}) of ThingClass goeHome + + API Version + The name of the ParamType (ThingClass: goeHome, Type: thing, ID: {3ad014e2-c948-406e-99be-eba1d866ea20}) - + + Frequency + The name of the StateType ({28f59f96-4b30-4606-9a04-80c82abc346b}) of ThingClass goeHome + + + + + Model maximal ampere + The name of the StateType ({ede9251d-662e-4d4b-90e3-db3d33c823d3}) of ThingClass goeHome + + + + Use MQTT interface The name of the ParamType (ThingClass: goeHome, Type: thing, ID: {848613a6-8a17-4082-ba77-3b4421170a4f}) - + go-e The name of the vendor ({c2cf9998-3584-489f-8d82-68a0baed2064}) - + go-eCharger The name of the plugin GoECharger ({a1dfca21-3f41-4a67-bc8c-c8b333411bd9}) - + go-eCharger Home The name of the ThingClass ({3b663d51-fdb5-4944-b409-c07f7933877e}) @@ -589,43 +250,60 @@ The name of the StateType ({08b107bc-1284-455d-9e5a-6a1c3adc389f}) of ThingClass IntegrationPluginGoECharger - + The network device discovery is not available. - - - - - - - + + The MAC address is not known. Please reconfigure the thing. + + + + + The host address is not known yet. Trying later again. + + + + + + + + + + + + + The wallbox does not seem to be reachable. - - - - - - - + + + + + + + + + + The wallbox returned invalid data. - + + Error creating MQTT channel. Please check MQTT server settings. - - - - - + + + + + Error while configuring MQTT settings on the wallbox. From b9221ee746e0bd9b6efbb4080ff32faaab518298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 28 Jul 2022 14:20:33 +0200 Subject: [PATCH 09/11] Handle reconfigure duplicated reference holding for goe charger --- goecharger/integrationplugingoecharger.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 1020ff15..23508284 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -112,6 +112,10 @@ void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) return; } + // Handle reconfigure + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + // Create the monitor NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); m_monitors.insert(thing, monitor); From 9996d2db9f71241b70a65395382c910bfa3ef049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 10 Aug 2022 11:26:55 +0200 Subject: [PATCH 10/11] Set state value before finishing action in order to prevent flickering sliders --- goecharger/integrationplugingoecharger.cpp | 11 ++++++----- goecharger/integrationplugingoecharger.h | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 23508284..2485fd99 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -288,7 +288,7 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) qCDebug(dcGoECharger()) << "Setting charging allowed to" << power; // Set the allow value QString configuration = QString("alw=%1").arg(power ? 1: 0); - sendActionRequestV1(thing, info, configuration); + sendActionRequestV1(thing, info, configuration, QVariant(power)); return; } else if (action.actionTypeId() == goeHomeMaxChargingCurrentActionTypeId) { uint maxChargingCurrent = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); @@ -296,7 +296,7 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) // FIXME: check if we can use amx since it is better for pv charging, not all version seen implement amx // Maybe check if the user sets it or a automatism QString configuration = QString("amp=%1").arg(maxChargingCurrent); - sendActionRequestV1(thing, info, configuration); + sendActionRequestV1(thing, info, configuration, QVariant(maxChargingCurrent)); return; } else { info->finish(Thing::ThingErrorActionTypeNotFound); @@ -334,8 +334,8 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) QVariantMap responseCode = jsonDoc.toVariant().toMap(); if (responseCode.value("frc", false).toBool()) { qCDebug(dcGoECharger()) << "Execute action finished successfully. Power" << power; - info->finish(Thing::ThingErrorNoError); thing->setStateValue("power", power); + info->finish(Thing::ThingErrorNoError); } else { qCWarning(dcGoECharger()) << "Action finished with error:" << responseCode.value("frc").toString(); info->finish(Thing::ThingErrorHardwareFailure); @@ -373,8 +373,8 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) QVariantMap responseCode = jsonDoc.toVariant().toMap(); if (responseCode.value("amp", false).toBool()) { qCDebug(dcGoECharger()) << "Execute action finished successfully. Charging current" << ampere; - info->finish(Thing::ThingErrorNoError); thing->setStateValue("maxChargingCurrent", ampere); + info->finish(Thing::ThingErrorNoError); } else { qCWarning(dcGoECharger()) << "Action finished with error:" << responseCode.value("amp").toString(); info->finish(Thing::ThingErrorHardwareFailure); @@ -656,7 +656,7 @@ QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequestV1(const Q return QNetworkRequest(requestUrl); } -void IntegrationPluginGoECharger::sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration) +void IntegrationPluginGoECharger::sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration, const QVariant &value) { // Lets use rest here since we get a reply on the rest request. // For using MQTT publish to topic "go-eCharger//cmd/req" @@ -681,6 +681,7 @@ void IntegrationPluginGoECharger::sendActionRequestV1(Thing *thing, ThingActionI return; } + thing->setStateValue(info->action().actionTypeId(), value); info->finish(Thing::ThingErrorNoError); updateV1(thing, jsonDoc.toVariant().toMap()); }); diff --git a/goecharger/integrationplugingoecharger.h b/goecharger/integrationplugingoecharger.h index fd9e02ae..ef3da050 100644 --- a/goecharger/integrationplugingoecharger.h +++ b/goecharger/integrationplugingoecharger.h @@ -119,7 +119,7 @@ private: // API V1 void updateV1(Thing *thing, const QVariantMap &statusMap); QNetworkRequest buildConfigurationRequestV1(const QHostAddress &address, const QString &configuration); - void sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration); + void sendActionRequestV1(Thing *thing, ThingActionInfo *info, const QString &configuration, const QVariant &value); void setupMqttChannelV1(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap); void reconfigureMqttChannelV1(Thing *thing, const QVariantMap &statusMap); From 90a6c87e5a4ca87b0e4333b2de456a627d492221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 10 Aug 2022 12:13:36 +0200 Subject: [PATCH 11/11] Fix discovery, update documentation and debug prints according to review --- goecharger/README.md | 7 +++--- goecharger/goediscovery.cpp | 26 +++++----------------- goecharger/goediscovery.h | 2 -- goecharger/integrationplugingoecharger.cpp | 2 +- 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/goecharger/README.md b/goecharger/README.md index 406e19c8..92e7d4a8 100644 --- a/goecharger/README.md +++ b/goecharger/README.md @@ -2,12 +2,11 @@ nymea plugin for go-eCharger smart wallbox for electic vehicles. -In order to make nymea work with go-e, please make sure you have enable `API V2` in the official app. +In order to integrate go-eChargers with nymea, please make sure the `API V2` is enabled in the go-eCharger app. If you are using the original go-e App or other client services to communicate with the wallbox, disable MQTT during the setup in order to make -sure all services are able to communicate with the wallbox. -There is no support for multiple MQTT clients on go-e devices, thus nymea defaults to HTTP to prevent constant -reconfiguration trough the clients. +sure all services are able to communicate with the wallbox. +Please note that that using the `MQTT interface` for connecting, may prevent other applications or services to connect to the go-eCharger wallbox. The preferred way of communicating would be MQTT (API V2), default is HTTP (API V1). diff --git a/goecharger/goediscovery.cpp b/goecharger/goediscovery.cpp index 1c98e288..4475f8c4 100644 --- a/goecharger/goediscovery.cpp +++ b/goecharger/goediscovery.cpp @@ -39,12 +39,7 @@ GoeDiscovery::GoeDiscovery(NetworkAccessManager *networkAccessManager, NetworkDe m_networkAccessManager(networkAccessManager), m_networkDeviceDiscovery(networkDeviceDiscovery) { - m_gracePeriodTimer.setSingleShot(true); - m_gracePeriodTimer.setInterval(3000); - connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){ - qCDebug(dcGoECharger()) << "Discovery: Grace period timer triggered."; - finishDiscovery(); - }); + } GoeDiscovery::~GoeDiscovery() @@ -58,7 +53,6 @@ void GoeDiscovery::startDiscovery() // Clean up m_discoveryResults.clear(); m_verifiedNetworkDeviceInfos.clear(); - m_gracePeriodTimer.stop(); m_startDateTime = QDateTime::currentDateTime(); @@ -90,7 +84,10 @@ void GoeDiscovery::startDiscovery() // If there might be some response after the grace period time, // we don't care any more since there might just waiting for some timeouts... // If there would be a device, it would have responded. - m_gracePeriodTimer.start(); + QTimer::singleShot(3000, this, [this](){ + qCDebug(dcGoECharger()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); }); } @@ -176,8 +173,6 @@ void GoeDiscovery::checkNetworkDeviceApiV1(const NetworkDeviceInfo &networkDevic qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V1 verification returned JSON data but not the right one. Continue..."; } - // Check if we are done with all checks - verifyDiscoveryFinished(); }); } @@ -228,19 +223,9 @@ void GoeDiscovery::checkNetworkDeviceApiV2(const NetworkDeviceInfo &networkDevic } else { qCDebug(dcGoECharger()) << "Discovery:" << networkDeviceInfo.address().toString() << "API V2 verification returned JSON data but not the right one. Continue..."; } - - // Check if we are done with all checks - verifyDiscoveryFinished(); }); } -void GoeDiscovery::verifyDiscoveryFinished() -{ - if (!m_discoveryReply && m_verifiedNetworkDeviceInfos.count() == m_discoveredNetworkDeviceInfos.count()) { - finishDiscovery(); - } -} - void GoeDiscovery::cleanupPendingReplies() { foreach (QNetworkReply *reply, m_pendingReplies) { @@ -253,7 +238,6 @@ void GoeDiscovery::finishDiscovery() { qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); qCInfo(dcGoECharger()) << "Discovery: Finished the discovery process. Found" << m_discoveryResults.count() << "go-eChargers in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); - m_gracePeriodTimer.stop(); cleanupPendingReplies(); emit discoveryFinished(); } diff --git a/goecharger/goediscovery.h b/goecharger/goediscovery.h index 7f951f4c..1c05d8c4 100644 --- a/goecharger/goediscovery.h +++ b/goecharger/goediscovery.h @@ -67,7 +67,6 @@ signals: private: QDateTime m_startDateTime; - QTimer m_gracePeriodTimer; NetworkAccessManager *m_networkAccessManager = nullptr; NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; NetworkDeviceDiscoveryReply *m_discoveryReply = nullptr; @@ -82,7 +81,6 @@ private slots: void checkNetworkDeviceApiV1(const NetworkDeviceInfo &networkDeviceInfo); void checkNetworkDeviceApiV2(const NetworkDeviceInfo &networkDeviceInfo); - void verifyDiscoveryFinished(); void cleanupPendingReplies(); void finishDiscovery(); diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 2485fd99..7d7194fa 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -103,7 +103,7 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); - qCDebug(dcGoECharger()) << "Set up" << thing << thing->params(); + qCDebug(dcGoECharger()) << "Setting up" << thing << thing->params(); MacAddress macAddress = MacAddress(thing->paramValue(goeHomeThingMacAddressParamTypeId).toString()); if (!macAddress.isValid()) {