diff --git a/goecharger/README.md b/goecharger/README.md index 93eaa813..92e7d4a8 100644 --- a/goecharger/README.md +++ b/goecharger/README.md @@ -2,23 +2,30 @@ 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 integrate go-eChargers with nymea, please make sure the `API V2` is enabled in the go-eCharger 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. +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). ## 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/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..4475f8c4 --- /dev/null +++ b/goecharger/goediscovery.cpp @@ -0,0 +1,257 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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) +{ + +} + +GoeDiscovery::~GoeDiscovery() +{ + qCDebug(dcGoECharger()) << "Discovery: destroy discovery object"; + cleanupPendingReplies(); +} + +void GoeDiscovery::startDiscovery() +{ + // Clean up + m_discoveryResults.clear(); + m_verifiedNetworkDeviceInfos.clear(); + + 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. + QTimer::singleShot(3000, this, [this](){ + qCDebug(dcGoECharger()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + }); +} + +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..."; + } + + }); +} + +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..."; + } + }); +} + +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"); + 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..1c05d8c4 --- /dev/null +++ b/goecharger/goediscovery.h @@ -0,0 +1,91 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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; + 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 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..7d7194fa 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,112 @@ 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()) << "Setting 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()) { + // Handle reconfigure + if (m_monitors.contains(thing)) + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); - // Handle reconfigure - if (m_channels.contains(thing)) { - hardwareManager()->mqttProvider()->releaseChannel(m_channels.take(thing)); - // Continue with normal setup + // 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; + } + + // 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: + 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; + } + }); + } + }); + + // 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()); @@ -161,14 +216,39 @@ 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; + } + } } } void IntegrationPluginGoECharger::thingRemoved(Thing *thing) { // Cleanup mqtt channels if set up - if (m_channels.contains(thing)) { - hardwareManager()->mqttProvider()->releaseChannel(m_channels.take(thing)); + switch (getApiVersion(thing)) { + case ApiVersion1: + if (m_mqttChannelsV1.contains(thing)) { + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV1.take(thing)); + } + break; + case ApiVersion2: + if (m_mqttChannelsV2.contains(thing)) { + hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannelsV2.take(thing)); + } + break; } // Cleanup possible pending replies @@ -187,272 +267,384 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); Action action = info->action(); + ApiVersion apiVersion = getApiVersion(thing); + QHostAddress address = getHostAddress(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, QVariant(power)); + 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, QVariant(maxChargingCurrent)); + return; } else { - thing->setStateMaxValue(goeHomeMaxChargingCurrentStateTypeId, amaLimit); + info->finish(Thing::ThingErrorActionTypeNotFound); } + break; + case ApiVersion2: + 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()->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(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); + return; + } - // 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; + 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; + } - QVariantList measurementList = statusMap.value("nrg").toList(); - if (measurementList.count() >= 1) - voltagePhaseA = measurementList.at(0).toUInt(); + QVariantMap responseCode = jsonDoc.toVariant().toMap(); + if (responseCode.value("frc", false).toBool()) { + qCDebug(dcGoECharger()) << "Execute action finished successfully. Power" << power; + thing->setStateValue("power", power); + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcGoECharger()) << "Action finished with error:" << responseCode.value("frc").toString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); - if (measurementList.count() >= 2) - voltagePhaseB = measurementList.at(1).toUInt(); + return; + } else if (action.actionTypeId() == goeHomeMaxChargingCurrentActionTypeId) { + uint ampere = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + qCDebug(dcGoECharger()) << "Setting max charging current to" << ampere << "A"; + // 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()->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(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The wallbox does not seem to be reachable.")); + return; + } - if (measurementList.count() >= 3) - voltagePhaseC = measurementList.at(2).toUInt(); + 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; + } - 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); + QVariantMap responseCode = jsonDoc.toVariant().toMap(); + if (responseCode.value("amp", false).toBool()) { + qCDebug(dcGoECharger()) << "Execute action finished successfully. Charging current" << ampere; + thing->setStateValue("maxChargingCurrent", ampere); + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcGoECharger()) << "Action finished with error:" << responseCode.value("amp").toString(); + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } else { + info->finish(Thing::ThingErrorActionTypeNotFound); } + break; } } + +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) { + 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()) << "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); + } + break; + } + case ApiVersion2: + // 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; + } + }); +} + 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 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; + default: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Unknown"); + thing->setStateValue(goeHomePluggedInStateTypeId, false); + 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,11 +656,14 @@ 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, 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" - 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(info, &ThingActionInfo::aborted, reply, &QNetworkReply::abort); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [=](){ if (reply->error() != QNetworkReply::NoError) { @@ -486,14 +681,16 @@ void IntegrationPluginGoECharger::sendActionRequest(Thing *thing, ThingActionInf return; } + thing->setStateValue(info->action().actionTypeId(), value); 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 +704,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 +740,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 +770,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 +800,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 +830,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 +863,7 @@ 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); }); }); }); @@ -674,6 +871,553 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q }); } +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); + }); + }); + }); + }); + }); +} + +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 @@ -682,45 +1426,125 @@ 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); + 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); - 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); + // Valid json data received, connected true + thing->setStateValue("connected", true); - qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); - QVariantMap statusMap = jsonDoc.toVariant().toMap(); - update(thing, statusMap); - }); - } + //qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); + QVariantMap statusMap = jsonDoc.toVariant().toMap(); + switch (apiVersion) { + case ApiVersion1: + updateV1(thing, statusMap); + break; + case ApiVersion2: + updateV2(thing, statusMap); + 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)); + } +} + +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", true); +} + +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 dac79095..ef3da050 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,48 @@ 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); + QNetworkRequest buildConfigurationRequestV1(const QHostAddress &address, 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); + + // 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(); - 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); + + // API V2 + void onMqttClientV2Connected(MqttChannel* channel); + void onMqttClientV2Disconnected(MqttChannel* channel); }; diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index 2a322312..97ff8607 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 + }, + { + "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 @@ } ] } - - - - 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.