From cb3076ee6ee8c76495d80fb9d70c3dc0098b6ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 28 May 2021 15:54:24 +0200 Subject: [PATCH 01/10] Add MQTT based go-eCharger plugin --- debian/control | 15 + debian/nymea-plugin-goecharger.install.in | 1 + goecharger/README.md | 25 ++ goecharger/go-e-logo.png | Bin 0 -> 66205 bytes goecharger/goecharger.pro | 13 + goecharger/integrationplugingoecharger.cpp | 381 ++++++++++++++++++ goecharger/integrationplugingoecharger.h | 102 +++++ goecharger/integrationplugingoecharger.json | 156 +++++++ goecharger/meta.json | 13 + ...fca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts | 275 +++++++++++++ nymea-plugins.pro | 1 + 11 files changed, 982 insertions(+) create mode 100644 debian/nymea-plugin-goecharger.install.in create mode 100644 goecharger/README.md create mode 100644 goecharger/go-e-logo.png create mode 100644 goecharger/goecharger.pro create mode 100644 goecharger/integrationplugingoecharger.cpp create mode 100644 goecharger/integrationplugingoecharger.h create mode 100644 goecharger/integrationplugingoecharger.json create mode 100644 goecharger/meta.json create mode 100644 goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts diff --git a/debian/control b/debian/control index ea89a128..ea0402f3 100644 --- a/debian/control +++ b/debian/control @@ -376,6 +376,21 @@ Description: nymea.io plugin for gpio This package will install the nymea.io plugin for gpio +Package: nymea-plugin-goecharger +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for the go-eCharger wallbox + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for the go-eCharger wallbox + + Package: nymea-plugin-homeconnect Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-goecharger.install.in b/debian/nymea-plugin-goecharger.install.in new file mode 100644 index 00000000..d959940d --- /dev/null +++ b/debian/nymea-plugin-goecharger.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugingoecharger.so diff --git a/goecharger/README.md b/goecharger/README.md new file mode 100644 index 00000000..2fc4590b --- /dev/null +++ b/goecharger/README.md @@ -0,0 +1,25 @@ +# go-eCharger + +nymea plug-in for go-eCharger smart wallbox for electic vehicles. + +Once you connect to the go-eCharger, nymea will configure the wallbox to use MQTT and send information to nymea. +Please make sure no ther service is using the custom MQTT server in the local network, otherwise they will exclude each other, depending who comes first. + +## Supported Things + +* go-eCharger Home + +## 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`. + +## Developer documentation + +The documentation of the API can be found [here](https://github.com/goecharger/go-eCharger-API-v1). + +## More + +https://go-e.co/ + diff --git a/goecharger/go-e-logo.png b/goecharger/go-e-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1a9f7f3f43ad0c67458f38fb70a63f9008b28ea1 GIT binary patch literal 66205 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 z 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 literal 0 HcmV?d00001 diff --git a/goecharger/goecharger.pro b/goecharger/goecharger.pro new file mode 100644 index 00000000..a1bad035 --- /dev/null +++ b/goecharger/goecharger.pro @@ -0,0 +1,13 @@ +include(../plugins.pri) + +TARGET = $$qtLibraryTarget(nymea_integrationplugingoecharger) + +QT += network + +PKGCONFIG += nymea-mqtt + +SOURCES += \ + integrationplugingoecharger.cpp \ + +HEADERS += \ + integrationplugingoecharger.h \ diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp new file mode 100644 index 00000000..07a17cdb --- /dev/null +++ b/goecharger/integrationplugingoecharger.cpp @@ -0,0 +1,381 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 "plugininfo.h" +#include "integrationplugingoecharger.h" + +#include +#include +#include +#include +#include + +// Modbus documentation: https://github.com/goecharger/go-eCharger-API-v1 + +IntegrationPluginGoECharger::IntegrationPluginGoECharger() +{ + +} + +void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) +{ + // TODO: perform nmap discovery and filter for "go-eCharger" hosts + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + if (thing->thingClassId() == goeHomeThingClassId) { + QHostAddress address = QHostAddress(thing->paramValue(goeHomeThingIpAddressParamTypeId).toString()); + QUrl requestUrl; + requestUrl.setScheme("http"); + requestUrl.setHost(address.toString()); + requestUrl.setPath("/status"); + + QNetworkRequest request(requestUrl); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, thing, [=](){ + 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; + } + + 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; + } + + qCDebug(dcGoECharger()) << "Received" << qUtf8Printable(jsonDoc.toJson()); + // Verify mqtt client and set it up + setupMqttChannel(info, address, jsonDoc.toVariant().toMap()); + }); + return; + } + + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); +} + + +void IntegrationPluginGoECharger::thingRemoved(Thing *thing) +{ + if (m_channels.contains(thing)) { + hardwareManager()->mqttProvider()->releaseChannel(m_channels.take(thing)); + } +} + +void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) +{ + //Thing *thing = info->thing(); + //Action action = info->action(); + + + info->finish(Thing::ThingErrorNoError); +} + +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 << qUtf8Printable(payload); +} + +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"); + break; + case CarStateCharging: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Vehicle loads"); + break; + case CarStateWaitForCar: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Waiting for vehicle"); + break; + case CarStateChargedCarConnected: + thing->setStateValue(goeHomeCarStatusStateTypeId, "Charging finished and vehicle still connected"); + break; + } + + 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; + } + + thing->setStateValue(goeHomeTotalEnergyStateTypeId, statusMap.value("eto").toUInt() / 10.0); + thing->setStateValue(goeHomeChargeEnergyStateTypeId, 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()); + thing->setStateValue(goeHomeMaxChargingCurrentStateTypeId, statusMap.value("ama").toUInt()); + thing->setStateValue(goeHomeSerialNumberStateTypeId, statusMap.value("sse").toString()); + } +} + +QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequest(const QHostAddress &address, const QString &configuration) +{ + QUrl requestUrl; + requestUrl.setScheme("http"); + requestUrl.setHost(address.toString()); + requestUrl.setPath("/mqtt"); + QUrlQuery query; + query.addQueryItem("payload", configuration); + requestUrl.setQuery(query); + return QNetworkRequest(requestUrl); +} + +void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap) +{ + Thing *thing = info->thing(); + QString statusTopic = QString("go-eCharger/%1/status").arg(statusMap.value("sse").toString()); + qCDebug(dcGoECharger()) << "Setting up mqtt channel for" << thing << address.toString() << statusTopic; + + MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(thing->id().toString(), 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_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); + + // Configure the mqtt server on the go-e + QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ + reply->deleteLater(); + 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; + } + + 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; + } + + // 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(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error while configuring MQTT settings on the wallbox.")); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->serverAddress().toString(); + } + + QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ + reply->deleteLater(); + 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; + } + + 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; + } + + // 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(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error while configuring MQTT settings on the wallbox.")); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->serverPort(); + } + + QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ + reply->deleteLater(); + 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; + } + + 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; + } + + // 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(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error while configuring MQTT settings on the wallbox.")); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->username(); + } + + QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ + reply->deleteLater(); + 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; + } + + 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; + } + + // 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(); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error while configuring MQTT settings on the wallbox.")); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server" << thing << channel->password(); + } + + QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ + reply->deleteLater(); + 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; + } + + 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; + } + + // 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"; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error while configuring MQTT settings on the wallbox.")); + return; + } else { + qCDebug(dcGoECharger()) << "Configured successfully MQTT server enabled" << thing; + } + + info->finish(Thing::ThingErrorNoError); + qCDebug(dcGoECharger()) << "Configuration of MQTT for" << thing << "finished successfully"; + // Update states... + update(thing, statusMap); + }); + }); + }); + }); + }); +} + + diff --git a/goecharger/integrationplugingoecharger.h b/goecharger/integrationplugingoecharger.h new file mode 100644 index 00000000..e0e1c6ca --- /dev/null +++ b/goecharger/integrationplugingoecharger.h @@ -0,0 +1,102 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, 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 INTEGRATIONPLUGINGOECHARGER_H +#define INTEGRATIONPLUGINGOECHARGER_H + +#include + +#include +#include +#include +#include +#include + +class IntegrationPluginGoECharger: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugingoecharger.json") + Q_INTERFACES(IntegrationPlugin) + +public: + enum CarState { + CarStateReadyNoCar = 1, + CarStateCharging = 2, + CarStateWaitForCar = 3, + CarStateChargedCarConnected = 4 + }; + Q_ENUM(CarState) + + enum Access { + AccessOpen = 0, + AccessRfid = 1, + AccessAuto = 2 + }; + Q_ENUM(Access) + + enum ErrorCode { + ErrorCodeResidualCurrentCircuitBreaker = 1, + ErrorCodePhase = 3, + ErrorCodeNoGround = 8, + ErrorCodeInternalError = 10 + }; + Q_ENUM(ErrorCode) + + enum CableLockMode { + CableLockModeLockWhileCareConnected = 0, + CableLockModeUnlockAfterCharging = 1, + CableLockModeAlwaysLock = 2 + }; + Q_ENUM(CableLockMode) + + explicit IntegrationPluginGoECharger(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + QHash m_channels; + + void update(Thing *thing, const QVariantMap &statusMap); + QNetworkRequest buildConfigurationRequest(const QHostAddress &address, const QString &configuration); + void setupMqttChannel(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap); + +private slots: + void onClientConnected(MqttChannel* channel); + void onClientDisconnected(MqttChannel* channel); + void onPublishReceived(MqttChannel* channel, const QString &topic, const QByteArray &payload); + +}; + +#endif // INTEGRATIONPLUGINGOECHARGER_H + diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json new file mode 100644 index 00000000..fb20793e --- /dev/null +++ b/goecharger/integrationplugingoecharger.json @@ -0,0 +1,156 @@ +{ + "name": "GoECharger", + "displayName": "go-eCharger", + "id": "a1dfca21-3f41-4a67-bc8c-c8b333411bd9", + "vendors": [ + { + "name": "goE", + "displayName": "go-e", + "id": "c2cf9998-3584-489f-8d82-68a0baed2064", + "thingClasses": [ + { + "name": "goeHome", + "displayName": "go-eCharger Home", + "id": "3b663d51-fdb5-4944-b409-c07f7933877e", + "createMethods": ["User"], + "interfaces": ["connectable"], + "paramTypes": [ + { + "id": "4342b72c-99d0-41a5-abc6-ea6c1cc1352c", + "name":"ipAddress", + "displayName": "IP address", + "type": "QString" + } + ], + "stateTypes":[ + { + "id": "a5afaad5-78bf-4cac-b98d-7eae31aac518", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "c69053bc-3a53-4e76-868b-ccf0958e9e44", + "name": "carStatus", + "displayName": "Car state", + "displayNameEvent": "Car status changed", + "type": "QString", + "possibleValues": [ + "Ready but no vehicle connected", + "Vehicle loads", + "Waiting for vehicle", + "Charging finished and vehicle still connected" + ], + "defaultValue": "Ready but no vehicle connected" + }, + { + "id": "d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201", + "name": "access", + "displayName": "Access", + "displayNameEvent": "Access changed", + "type": "QString", + "possibleValues": [ + "Open", + "RFID", + "Automatic" + ], + "defaultValue": "Open" + }, + { + "id": "8a7ab9f1-0143-494c-98ee-69f94125fe42", + "name": "power", + "displayName": "Charging", + "type": "bool", + "displayNameAction": "Start charging", + "displayNameEvent": "Charging status changed", + "defaultValue": false, + "writable": true + }, + { + "id": "446fb786-bfbe-4938-963c-73d02184573f", + "name": "maxChargingCurrent", + "displayName": "Charging current", + "displayNameEvent": "Charging current changed", + "displayNameAction": "Set charging current", + "type": "double", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "defaultValue": 16, + "writable": 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": "08b107bc-1284-455d-9e5a-6a1c3adc389f", + "name": "updateAvailable", + "displayName": "Update available", + "displayNameEvent": "Update available changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "d8f5abb6-5db3-4040-8829-553b1d881ce4", + "name": "totalEnergy", + "displayName": "Total energy", + "displayNameEvent": "Total energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0 + }, + { + "id": "e8258831-ad89-4d27-b295-e8c10dd42b76", + "name": "chargeEnergy", + "displayName": "Charge energy", + "displayNameEvent": "Charge energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.0 + }, + { + "id": "b06479d5-7a38-4fbd-867e-e55bdb54651b", + "name": "ledBrightness", + "displayName": "Led brightness", + "displayNameEvent": "Led brightness changed", + "type": "int", + "minValue": 0, + "maxValue": 255, + "defaultValue": 255 + }, + { + "id": "5d18b48d-b886-409e-ab2e-336d9c94a55c", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "8ecdf24b-daca-4b7a-98b5-3236f1e6ad85", + "name": "serialNumber", + "displayName": "Serial number", + "displayNameEvent": "Serial number changed", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} + + + + diff --git a/goecharger/meta.json b/goecharger/meta.json new file mode 100644 index 00000000..376be697 --- /dev/null +++ b/goecharger/meta.json @@ -0,0 +1,13 @@ +{ + "title": "go-eCharger", + "tagline": "Control and monitor the go-eCharger smart wallbox for electric vehicles.", + "icon": "go-e-logo.png", + "stability": "community", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts b/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts new file mode 100644 index 00000000..ffd1a6b3 --- /dev/null +++ b/goecharger/translations/a1dfca21-3f41-4a67-bc8c-c8b333411bd9-en_US.ts @@ -0,0 +1,275 @@ + + + + + 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 + + + + + Access changed + The name of the EventType ({d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201}) 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 + + + + + Car status changed + The name of the EventType ({c69053bc-3a53-4e76-868b-ccf0958e9e44}) of ThingClass goeHome + + + + + + Charge energy + The name of the ParamType (ThingClass: goeHome, EventType: chargeEnergy, ID: {e8258831-ad89-4d27-b295-e8c10dd42b76}) +---------- +The name of the StateType ({e8258831-ad89-4d27-b295-e8c10dd42b76}) of ThingClass goeHome + + + + + Charge energy changed + The name of the EventType ({e8258831-ad89-4d27-b295-e8c10dd42b76}) of ThingClass goeHome + + + + + + + Charging + The name of the ParamType (ThingClass: goeHome, ActionType: power, ID: {8a7ab9f1-0143-494c-98ee-69f94125fe42}) +---------- +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 + + + + + + + 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 + + + + + Charging status changed + The name of the EventType ({8a7ab9f1-0143-494c-98ee-69f94125fe42}) 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 + + + + + Connected changed + The name of the EventType ({a5afaad5-78bf-4cac-b98d-7eae31aac518}) 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 + + + + + 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, 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 + + + + + + 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 + + + + + Serial number changed + The name of the EventType ({8ecdf24b-daca-4b7a-98b5-3236f1e6ad85}) 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 + + + + + Start charging + The name of the ActionType ({8a7ab9f1-0143-494c-98ee-69f94125fe42}) of ThingClass goeHome + + + + + + Total energy + The name of the ParamType (ThingClass: goeHome, EventType: totalEnergy, ID: {d8f5abb6-5db3-4040-8829-553b1d881ce4}) +---------- +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 + + + + + Update available changed + The name of the EventType ({08b107bc-1284-455d-9e5a-6a1c3adc389f}) of ThingClass goeHome + + + + + 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}) + + + + + IntegrationPluginGoECharger + + + + + + + + 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. + + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index e32bd80e..51a727f7 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -23,6 +23,7 @@ PLUGIN_DIRS = \ fronius \ genericelements \ genericthings \ + goecharger \ gpio \ i2cdevices \ httpcommander \ From c9bcbf2717d73c87a2a5f5a1501b2a1b04c8af4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 7 Jun 2021 09:57:14 +0200 Subject: [PATCH 02/10] Add network discovery for go-eCharger --- goecharger/integrationplugingoecharger.cpp | 48 +++++++++++++++++++-- goecharger/integrationplugingoecharger.json | 6 +++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 07a17cdb..bcd41d04 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -30,6 +30,7 @@ #include "plugininfo.h" #include "integrationplugingoecharger.h" +#include "network/networkdevicediscovery.h" #include #include @@ -37,7 +38,7 @@ #include #include -// Modbus documentation: https://github.com/goecharger/go-eCharger-API-v1 +// API documentation: https://github.com/goecharger/go-eCharger-API-v1 IntegrationPluginGoECharger::IntegrationPluginGoECharger() { @@ -46,8 +47,49 @@ IntegrationPluginGoECharger::IntegrationPluginGoECharger() void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) { - // TODO: perform nmap discovery and filter for "go-eCharger" hosts - info->finish(Thing::ThingErrorNoError); + if (!hardwareManager()->networkDeviceDiscovery()->available()) { + qCWarning(dcGoECharger()) << "The network discovery is not available on this platform."; + info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); + return; + } + + // Perform a network device discovery and filter for "go-eCharger" hosts + NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { + qCDebug(dcGoECharger()) << "Found" << networkDevice; + QString title; + if (networkDevice.hostName().isEmpty()) { + title = networkDevice.address().toString(); + } else { + title = networkDevice.hostName() + " (" + networkDevice.address().toString() + ")"; + } + + QString description; + if (networkDevice.macAddressManufacturer().isEmpty()) { + description = networkDevice.macAddress(); + } else { + description = networkDevice.macAddress() + " (" + networkDevice.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(goeHomeThingClassId, title, description); + ParamList params; + params << Param(goeHomeThingIpAddressParamTypeId, networkDevice.address().toString()); + params << Param(goeHomeThingMacAddressParamTypeId, networkDevice.macAddress()); + descriptor.setParams(params); + + // Check if we already have set up this device + Things existingThings = myThings().filterByParam(goeHomeThingMacAddressParamTypeId, networkDevice.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system!" << networkDevice; + descriptor.setThingId(existingThings.first()->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); } void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index fb20793e..000bda8f 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -20,6 +20,12 @@ "name":"ipAddress", "displayName": "IP address", "type": "QString" + }, + { + "id": "0e30e30f-ad96-417e-b739-cac85f75de39", + "name":"macAddress", + "displayName": "MAC address", + "type": "QString" } ], "stateTypes":[ From ac8bbae26d136b7d9326947f8c4a9735f75b3baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 7 Jun 2021 15:03:41 +0200 Subject: [PATCH 03/10] Use proper client id for MQTT and add discovery create method and evcharger interface --- goecharger/integrationplugingoecharger.cpp | 6 ++++-- goecharger/integrationplugingoecharger.json | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index bcd41d04..0a6c5dde 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -242,10 +242,12 @@ QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequest(const QHo void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap) { Thing *thing = info->thing(); - QString statusTopic = QString("go-eCharger/%1/status").arg(statusMap.value("sse").toString()); + 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); qCDebug(dcGoECharger()) << "Setting up mqtt channel for" << thing << address.toString() << statusTopic; - MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(thing->id().toString(), address, {statusTopic}); + MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(clientId, 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.")); diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index 000bda8f..a0f5eee1 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -12,8 +12,8 @@ "name": "goeHome", "displayName": "go-eCharger Home", "id": "3b663d51-fdb5-4944-b409-c07f7933877e", - "createMethods": ["User"], - "interfaces": ["connectable"], + "createMethods": ["Discovery", "User"], + "interfaces": ["evcharger", "connectable"], "paramTypes": [ { "id": "4342b72c-99d0-41a5-abc6-ea6c1cc1352c", From 1a09836dd7c399d68e7b9a1cf4d7cf8619fa9227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 8 Jun 2021 11:07:32 +0200 Subject: [PATCH 04/10] Implement actions and add more states --- goecharger/integrationplugingoecharger.cpp | 123 ++++++++++++++++++-- goecharger/integrationplugingoecharger.h | 2 + goecharger/integrationplugingoecharger.json | 74 ++++++++++-- 3 files changed, 179 insertions(+), 20 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 0a6c5dde..e12c4f70 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -57,12 +57,16 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { + qCDebug(dcGoECharger()) << "Found" << networkDevice; + if (!networkDevice.hostName().toLower().contains("go-echarger")) + continue; + QString title; if (networkDevice.hostName().isEmpty()) { title = networkDevice.address().toString(); } else { - title = networkDevice.hostName() + " (" + networkDevice.address().toString() + ")"; + title = "go-eCharger (" + networkDevice.address().toString() + ")"; } QString description; @@ -112,7 +116,6 @@ void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) } QByteArray data = reply->readAll(); - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { @@ -141,11 +144,63 @@ void IntegrationPluginGoECharger::thingRemoved(Thing *thing) void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) { - //Thing *thing = info->thing(); - //Action action = info->action(); + Thing *thing = info->thing(); + Action action = info->action(); + if (thing->thingClassId() != goeHomeThingClassId) { + info->finish(Thing::ThingErrorThingClassNotFound); + return; + } - info->finish(Thing::ThingErrorNoError); + if (thing->stateValue(goeHomeConnectedStateTypeId).toBool()) { + 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) { + int maxChargingCurrent = action.paramValue(goeHomeMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toUInt(); + qCDebug(dcGoECharger()) << "Setting max charging current to" << maxChargingCurrent << "A"; + // Set the allow value + QString configuration = QString("ama=%1").arg(maxChargingCurrent); + sendActionRequest(thing, info, configuration); + return; + } else if (action.actionTypeId() == goeHomeCloudActionTypeId) { + bool enabled = action.paramValue(goeHomeCloudActionCloudParamTypeId).toBool(); + qCDebug(dcGoECharger()) << "Set cloud" << (enabled ? "enabled" : "disabled"); + // Set the allow value + 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; + // Set the allow value + 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"); + // Set the allow value + QString configuration = QString("lse=%1").arg(enabled ? 1: 0); + sendActionRequest(thing, info, configuration); + return; + } else { + info->finish(Thing::ThingErrorActionTypeNotFound); + } } void IntegrationPluginGoECharger::onClientConnected(MqttChannel *channel) @@ -180,7 +235,18 @@ void IntegrationPluginGoECharger::onPublishReceived(MqttChannel *channel, const return; } - qCDebug(dcGoECharger()) << thing << "publish received" << topic << qUtf8Printable(payload); + 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()); + } } void IntegrationPluginGoECharger::update(Thing *thing, const QVariantMap &statusMap) @@ -216,13 +282,24 @@ void IntegrationPluginGoECharger::update(Thing *thing, const QVariantMap &status break; } - thing->setStateValue(goeHomeTotalEnergyStateTypeId, statusMap.value("eto").toUInt() / 10.0); + QVariantList temperatureSensorList = statusMap.value("tma").toList(); + if (temperatureSensorList.count() == 4) { + thing->setStateValue(goeHomeTemperatureSensor1StateTypeId, temperatureSensorList.at(0).toDouble()); + thing->setStateValue(goeHomeTemperatureSensor2StateTypeId, temperatureSensorList.at(1).toDouble()); + thing->setStateValue(goeHomeTemperatureSensor3StateTypeId, temperatureSensorList.at(2).toDouble()); + thing->setStateValue(goeHomeTemperatureSensor4StateTypeId, temperatureSensorList.at(3).toDouble()); + } + + thing->setStateValue(goeHomeTotalEnergyConsumedStateTypeId, statusMap.value("eto").toUInt() / 10.0); thing->setStateValue(goeHomeChargeEnergyStateTypeId, 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(goeHomeAdapterConnectedStateTypeId, (statusMap.value("adi").toUInt() == 0 ? false : true)); thing->setStateValue(goeHomeCloudStateTypeId, (statusMap.value("cdi").toUInt() == 0 ? false : true)); thing->setStateValue(goeHomeFirmwareVersionStateTypeId, statusMap.value("fwv").toString()); thing->setStateValue(goeHomeMaxChargingCurrentStateTypeId, statusMap.value("ama").toUInt()); + thing->setStateValue(goeHomeLedBrightnessStateTypeId, statusMap.value("lbr").toUInt()); + thing->setStateValue(goeHomeLedEnergySaveStateTypeId, statusMap.value("lse").toBool()); thing->setStateValue(goeHomeSerialNumberStateTypeId, statusMap.value("sse").toString()); } } @@ -239,6 +316,33 @@ QNetworkRequest IntegrationPluginGoECharger::buildConfigurationRequest(const QHo return QNetworkRequest(requestUrl); } +void IntegrationPluginGoECharger::sendActionRequest(Thing *thing, ThingActionInfo *info, const QString &configuration) +{ + // Lets use rest here since we get a reply on the rest request. For using MQTT publish to topic "go-eCharger//cmd/req" + QNetworkRequest request = buildConfigurationRequest(QHostAddress(thing->paramValue(goeHomeThingIpAddressParamTypeId).toString()), configuration); + QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); + connect(reply, &QNetworkReply::finished, thing, [=](){ + reply->deleteLater(); + 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; + } + + 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; + } + + qCDebug(dcGoECharger()) << "Action response" << jsonDoc.toJson(QJsonDocument::Compact); + info->finish(Thing::ThingErrorNoError); + }); +} + void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const QHostAddress &address, const QVariantMap &statusMap) { Thing *thing = info->thing(); @@ -272,7 +376,6 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } QByteArray data = reply->readAll(); - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { @@ -302,7 +405,6 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } QByteArray data = reply->readAll(); - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { @@ -332,7 +434,6 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } QByteArray data = reply->readAll(); - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { @@ -362,7 +463,6 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } QByteArray data = reply->readAll(); - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { @@ -392,7 +492,6 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q } QByteArray data = reply->readAll(); - QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { diff --git a/goecharger/integrationplugingoecharger.h b/goecharger/integrationplugingoecharger.h index e0e1c6ca..f5cc8dff 100644 --- a/goecharger/integrationplugingoecharger.h +++ b/goecharger/integrationplugingoecharger.h @@ -89,8 +89,10 @@ private: void update(Thing *thing, const QVariantMap &statusMap); 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); + private slots: void onClientConnected(MqttChannel* channel); void onClientDisconnected(MqttChannel* channel); diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index a0f5eee1..62f86c99 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -13,7 +13,7 @@ "displayName": "go-eCharger Home", "id": "3b663d51-fdb5-4944-b409-c07f7933877e", "createMethods": ["Discovery", "User"], - "interfaces": ["evcharger", "connectable"], + "interfaces": ["evcharger", "smartmeter", "connectable"], "paramTypes": [ { "id": "4342b72c-99d0-41a5-abc6-ea6c1cc1352c", @@ -68,10 +68,10 @@ { "id": "8a7ab9f1-0143-494c-98ee-69f94125fe42", "name": "power", - "displayName": "Charging", + "displayName": "Allow charging", "type": "bool", - "displayNameAction": "Start charging", - "displayNameEvent": "Charging status changed", + "displayNameAction": "Allow charging", + "displayNameEvent": "Allow charging changed", "defaultValue": false, "writable": true }, @@ -106,9 +106,17 @@ "type": "bool", "defaultValue": false }, + { + "id": "d557e59e-ca22-4aff-bf80-dfee44db0f69", + "name": "adapterConnected", + "displayName": "Adapter connected", + "displayNameEvent": "Adapter connected changed", + "type": "bool", + "defaultValue": false + }, { "id": "d8f5abb6-5db3-4040-8829-553b1d881ce4", - "name": "totalEnergy", + "name": "totalEnergyConsumed", "displayName": "Total energy", "displayNameEvent": "Total energy changed", "type": "double", @@ -128,11 +136,59 @@ "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 + "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", + "displayName": "Temperature 1", + "displayNameEvent": "Temperature 1 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0 + }, + { + "id": "558e273a-4028-495a-902a-e4e932a0ae24", + "name": "temperatureSensor2", + "displayName": "Temperature 2", + "displayNameEvent": "Temperature 2 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0 + }, + { + "id": "dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb", + "name": "temperatureSensor3", + "displayName": "Temperature 3", + "displayNameEvent": "Temperature 3 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0 + }, + { + "id": "1953e29f-fe28-4016-9b05-f4baf4c311ff", + "name": "temperatureSensor4", + "displayName": "Temperature 4", + "displayNameEvent": "Temperature 4 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0 }, { "id": "5d18b48d-b886-409e-ab2e-336d9c94a55c", @@ -140,7 +196,8 @@ "displayName": "Firmware version", "displayNameEvent": "Firmware version changed", "type": "QString", - "defaultValue": "" + "defaultValue": "", + "cached": true }, { "id": "8ecdf24b-daca-4b7a-98b5-3236f1e6ad85", @@ -148,7 +205,8 @@ "displayName": "Serial number", "displayNameEvent": "Serial number changed", "type": "QString", - "defaultValue": "" + "defaultValue": "", + "cached": true } ] } From 24a7ce13be345c1d720705256b0660637f3cff27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 9 Jun 2021 09:34:09 +0200 Subject: [PATCH 05/10] Log interesting go-eCharger states and fix execute action and discovry --- goecharger/integrationplugingoecharger.cpp | 17 +++++++++--- goecharger/integrationplugingoecharger.json | 30 ++++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index e12c4f70..e940d3f9 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -59,9 +59,14 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { qCDebug(dcGoECharger()) << "Found" << networkDevice; + // Filter by hostname if (!networkDevice.hostName().toLower().contains("go-echarger")) continue; + // We need also the mac address + if (!networkDevice.macAddress().isEmpty()) + continue; + QString title; if (networkDevice.hostName().isEmpty()) { title = networkDevice.address().toString(); @@ -152,7 +157,8 @@ void IntegrationPluginGoECharger::executeAction(ThingActionInfo *info) return; } - if (thing->stateValue(goeHomeConnectedStateTypeId).toBool()) { + if (!thing->stateValue(goeHomeConnectedStateTypeId).toBool()) { + qCWarning(dcGoECharger()) << thing << "failed to execute action. The device seems not to be connected."; info->finish(Thing::ThingErrorHardwareNotAvailable); return; } @@ -246,6 +252,8 @@ void IntegrationPluginGoECharger::onPublishReceived(MqttChannel *channel, const 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)); } } @@ -338,8 +346,8 @@ void IntegrationPluginGoECharger::sendActionRequest(Thing *thing, ThingActionInf return; } - qCDebug(dcGoECharger()) << "Action response" << jsonDoc.toJson(QJsonDocument::Compact); info->finish(Thing::ThingErrorNoError); + update(thing, jsonDoc.toVariant().toMap()); }); } @@ -349,9 +357,10 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q 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); - qCDebug(dcGoECharger()) << "Setting up mqtt channel for" << thing << address.toString() << statusTopic; + 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}); + MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(clientId, address, {statusTopic, commandTopic}); 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.")); diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index 62f86c99..44c4384d 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -50,7 +50,8 @@ "Waiting for vehicle", "Charging finished and vehicle still connected" ], - "defaultValue": "Ready but no vehicle connected" + "defaultValue": "Ready but no vehicle connected", + "suggestLogging": true }, { "id": "d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201", @@ -63,7 +64,8 @@ "RFID", "Automatic" ], - "defaultValue": "Open" + "defaultValue": "Open", + "suggestLogging": true }, { "id": "8a7ab9f1-0143-494c-98ee-69f94125fe42", @@ -96,7 +98,8 @@ "displayNameEvent": "Cloud enabled changed", "type": "bool", "defaultValue": true, - "writable": true + "writable": true, + "suggestLogging": true }, { "id": "08b107bc-1284-455d-9e5a-6a1c3adc389f", @@ -104,7 +107,8 @@ "displayName": "Update available", "displayNameEvent": "Update available changed", "type": "bool", - "defaultValue": false + "defaultValue": false, + "suggestLogging": true }, { "id": "d557e59e-ca22-4aff-bf80-dfee44db0f69", @@ -112,7 +116,8 @@ "displayName": "Adapter connected", "displayNameEvent": "Adapter connected changed", "type": "bool", - "defaultValue": false + "defaultValue": false, + "suggestLogging": true }, { "id": "d8f5abb6-5db3-4040-8829-553b1d881ce4", @@ -130,7 +135,8 @@ "displayNameEvent": "Charge energy changed", "type": "double", "unit": "KiloWattHour", - "defaultValue": 0.0 + "defaultValue": 0.0, + "suggestLogging": true }, { "id": "b06479d5-7a38-4fbd-867e-e55bdb54651b", @@ -161,7 +167,8 @@ "displayNameEvent": "Temperature 1 changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 0.0 + "defaultValue": 0.0, + "suggestLogging": true }, { "id": "558e273a-4028-495a-902a-e4e932a0ae24", @@ -170,7 +177,8 @@ "displayNameEvent": "Temperature 2 changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 0.0 + "defaultValue": 0.0, + "suggestLogging": true }, { "id": "dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb", @@ -179,7 +187,8 @@ "displayNameEvent": "Temperature 3 changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 0.0 + "defaultValue": 0.0, + "suggestLogging": true }, { "id": "1953e29f-fe28-4016-9b05-f4baf4c311ff", @@ -188,7 +197,8 @@ "displayNameEvent": "Temperature 4 changed", "type": "double", "unit": "DegreeCelsius", - "defaultValue": 0.0 + "defaultValue": 0.0, + "suggestLogging": true }, { "id": "5d18b48d-b886-409e-ab2e-336d9c94a55c", From a690689ab86f387c05fe8ea58430dc254664ed6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 10 Jun 2021 12:05:31 +0200 Subject: [PATCH 06/10] Fix go-eCharger discovery --- goecharger/integrationplugingoecharger.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index e940d3f9..1f0c6ed4 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -58,13 +58,13 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { - qCDebug(dcGoECharger()) << "Found" << networkDevice; + qCDebug(dcGoECharger()) << "Checking discovered" << networkDevice; // Filter by hostname if (!networkDevice.hostName().toLower().contains("go-echarger")) continue; // We need also the mac address - if (!networkDevice.macAddress().isEmpty()) + if (networkDevice.macAddress().isEmpty()) continue; QString title; @@ -90,7 +90,7 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) // Check if we already have set up this device Things existingThings = myThings().filterByParam(goeHomeThingMacAddressParamTypeId, networkDevice.macAddress()); if (existingThings.count() == 1) { - qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system!" << networkDevice; + qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system" << networkDevice; descriptor.setThingId(existingThings.first()->id()); } From dfcebb0f6b53d787b172b968eecf0ca2a94badf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 11 Jun 2021 11:04:00 +0200 Subject: [PATCH 07/10] Update discovery to renamed network device --- goecharger/integrationplugingoecharger.cpp | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 1f0c6ed4..18b6cfdb 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -56,41 +56,41 @@ void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) // Perform a network device discovery and filter for "go-eCharger" hosts NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover(); connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ - foreach (const NetworkDevice &networkDevice, discoveryReply->networkDevices()) { + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { - qCDebug(dcGoECharger()) << "Checking discovered" << networkDevice; + qCDebug(dcGoECharger()) << "Checking discovered" << networkDeviceInfo; // Filter by hostname - if (!networkDevice.hostName().toLower().contains("go-echarger")) + if (!networkDeviceInfo.hostName().contains("go-eCharger")) continue; // We need also the mac address - if (networkDevice.macAddress().isEmpty()) + if (networkDeviceInfo.macAddress().isEmpty()) continue; QString title; - if (networkDevice.hostName().isEmpty()) { - title = networkDevice.address().toString(); + if (networkDeviceInfo.hostName().isEmpty()) { + title = networkDeviceInfo.address().toString(); } else { - title = "go-eCharger (" + networkDevice.address().toString() + ")"; + title = "go-eCharger (" + networkDeviceInfo.address().toString() + ")"; } QString description; - if (networkDevice.macAddressManufacturer().isEmpty()) { - description = networkDevice.macAddress(); + if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { + description = networkDeviceInfo.macAddress(); } else { - description = networkDevice.macAddress() + " (" + networkDevice.macAddressManufacturer() + ")"; + description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; } ThingDescriptor descriptor(goeHomeThingClassId, title, description); ParamList params; - params << Param(goeHomeThingIpAddressParamTypeId, networkDevice.address().toString()); - params << Param(goeHomeThingMacAddressParamTypeId, networkDevice.macAddress()); + params << Param(goeHomeThingIpAddressParamTypeId, networkDeviceInfo.address().toString()); + params << Param(goeHomeThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); descriptor.setParams(params); // Check if we already have set up this device - Things existingThings = myThings().filterByParam(goeHomeThingMacAddressParamTypeId, networkDevice.macAddress()); + Things existingThings = myThings().filterByParam(goeHomeThingMacAddressParamTypeId, networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { - qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system" << networkDevice; + qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system" << networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); } From 84d2365dbcb73bc531aed32112b73158569d9cbb Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 27 Jun 2021 19:41:38 +0200 Subject: [PATCH 08/10] Fix smartmeter interface --- goecharger/integrationplugingoecharger.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goecharger/integrationplugingoecharger.json b/goecharger/integrationplugingoecharger.json index 44c4384d..d27255dc 100644 --- a/goecharger/integrationplugingoecharger.json +++ b/goecharger/integrationplugingoecharger.json @@ -13,7 +13,7 @@ "displayName": "go-eCharger Home", "id": "3b663d51-fdb5-4944-b409-c07f7933877e", "createMethods": ["Discovery", "User"], - "interfaces": ["evcharger", "smartmeter", "connectable"], + "interfaces": ["evcharger", "smartmeterconsumer", "connectable"], "paramTypes": [ { "id": "4342b72c-99d0-41a5-abc6-ea6c1cc1352c", From 8235cdb8f869140a86b59f734da92f1aa2ea63a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 30 Jun 2021 14:05:35 +0200 Subject: [PATCH 09/10] Fix typos and network reply clean up and connection --- goecharger/README.md | 4 ++-- goecharger/integrationplugingoecharger.cpp | 27 +++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/goecharger/README.md b/goecharger/README.md index 2fc4590b..dabce807 100644 --- a/goecharger/README.md +++ b/goecharger/README.md @@ -1,9 +1,9 @@ # go-eCharger -nymea plug-in for go-eCharger smart wallbox for electic vehicles. +nymea plugin for go-eCharger smart wallbox for electic vehicles. Once you connect to the go-eCharger, nymea will configure the wallbox to use MQTT and send information to nymea. -Please make sure no ther service is using the custom MQTT server in the local network, otherwise they will exclude each other, depending who comes first. +Please make sure no other service is using the custom MQTT server in the local network, otherwise they will exclude each other, depending who comes first. ## Supported Things diff --git a/goecharger/integrationplugingoecharger.cpp b/goecharger/integrationplugingoecharger.cpp index 18b6cfdb..9a76348e 100644 --- a/goecharger/integrationplugingoecharger.cpp +++ b/goecharger/integrationplugingoecharger.cpp @@ -113,7 +113,8 @@ void IntegrationPluginGoECharger::setupThing(ThingSetupInfo *info) QNetworkRequest request(requestUrl); QNetworkReply *reply = hardwareManager()->networkManager()->get(request); - connect(reply, &QNetworkReply::finished, 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.")); @@ -329,8 +330,8 @@ void IntegrationPluginGoECharger::sendActionRequest(Thing *thing, ThingActionInf // 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); QNetworkReply *reply = hardwareManager()->networkManager()->sendCustomRequest(request, "SET"); - connect(reply, &QNetworkReply::finished, thing, [=](){ - reply->deleteLater(); + 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.")); @@ -376,8 +377,8 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ - reply->deleteLater(); + 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.")); @@ -405,8 +406,8 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ - reply->deleteLater(); + 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.")); @@ -434,8 +435,8 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ - reply->deleteLater(); + 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.")); @@ -463,8 +464,8 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ - reply->deleteLater(); + 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.")); @@ -492,8 +493,8 @@ void IntegrationPluginGoECharger::setupMqttChannel(ThingSetupInfo *info, const Q QNetworkRequest request = buildConfigurationRequest(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, thing, [=](){ - reply->deleteLater(); + 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.")); From 0229c00b61c65ff4617e2ca7022bdb03169572ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 1 Jul 2021 07:17:45 +0200 Subject: [PATCH 10/10] Remove target definition and let plugin.pri handle that --- goecharger/goecharger.pro | 2 -- 1 file changed, 2 deletions(-) diff --git a/goecharger/goecharger.pro b/goecharger/goecharger.pro index a1bad035..a8122ee7 100644 --- a/goecharger/goecharger.pro +++ b/goecharger/goecharger.pro @@ -1,7 +1,5 @@ include(../plugins.pri) -TARGET = $$qtLibraryTarget(nymea_integrationplugingoecharger) - QT += network PKGCONFIG += nymea-mqtt