diff --git a/debian/control b/debian/control index dfc04795..26aa8f15 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..dabce807 --- /dev/null +++ b/goecharger/README.md @@ -0,0 +1,25 @@ +# go-eCharger + +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 other 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 00000000..1a9f7f3f Binary files /dev/null and b/goecharger/go-e-logo.png differ diff --git a/goecharger/goecharger.pro b/goecharger/goecharger.pro new file mode 100644 index 00000000..a8122ee7 --- /dev/null +++ b/goecharger/goecharger.pro @@ -0,0 +1,11 @@ +include(../plugins.pri) + +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..9a76348e --- /dev/null +++ b/goecharger/integrationplugingoecharger.cpp @@ -0,0 +1,534 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "network/networkdevicediscovery.h" + +#include +#include +#include +#include +#include + +// API documentation: https://github.com/goecharger/go-eCharger-API-v1 + +IntegrationPluginGoECharger::IntegrationPluginGoECharger() +{ + +} + +void IntegrationPluginGoECharger::discoverThings(ThingDiscoveryInfo *info) +{ + 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 NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + + qCDebug(dcGoECharger()) << "Checking discovered" << networkDeviceInfo; + // Filter by hostname + if (!networkDeviceInfo.hostName().contains("go-eCharger")) + continue; + + // We need also the mac address + if (networkDeviceInfo.macAddress().isEmpty()) + continue; + + QString title; + if (networkDeviceInfo.hostName().isEmpty()) { + title = networkDeviceInfo.address().toString(); + } else { + title = "go-eCharger (" + networkDeviceInfo.address().toString() + ")"; + } + + QString description; + if (networkDeviceInfo.macAddressManufacturer().isEmpty()) { + description = networkDeviceInfo.macAddress(); + } else { + description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")"; + } + + ThingDescriptor descriptor(goeHomeThingClassId, title, description); + ParamList params; + 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, networkDeviceInfo.macAddress()); + if (existingThings.count() == 1) { + qCDebug(dcGoECharger()) << "This go-eCharger already exists in the system" << networkDeviceInfo; + descriptor.setThingId(existingThings.first()->id()); + } + + info->addThingDescriptor(descriptor); + } + + 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, 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; + } + + 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(); + + if (thing->thingClassId() != goeHomeThingClassId) { + info->finish(Thing::ThingErrorThingClassNotFound); + return; + } + + if (!thing->stateValue(goeHomeConnectedStateTypeId).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) { + 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) +{ + 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"); + 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; + } + + 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()); + } +} + +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::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, 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; + } + + 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; + } + + info->finish(Thing::ThingErrorNoError); + update(thing, jsonDoc.toVariant().toMap()); + }); +} + +void IntegrationPluginGoECharger::setupMqttChannel(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); + 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; + 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, 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; + } + + 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, 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; + } + + 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, 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; + } + + 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, 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; + } + + 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, 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; + } + + 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..f5cc8dff --- /dev/null +++ b/goecharger/integrationplugingoecharger.h @@ -0,0 +1,104 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 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); + 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..d27255dc --- /dev/null +++ b/goecharger/integrationplugingoecharger.json @@ -0,0 +1,230 @@ +{ + "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": ["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" + } + ], + "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", + "suggestLogging": true + }, + { + "id": "d80e1ed8-c3ae-4b68-bf86-21b4d7b2b201", + "name": "access", + "displayName": "Access", + "displayNameEvent": "Access changed", + "type": "QString", + "possibleValues": [ + "Open", + "RFID", + "Automatic" + ], + "defaultValue": "Open", + "suggestLogging": true + }, + { + "id": "8a7ab9f1-0143-494c-98ee-69f94125fe42", + "name": "power", + "displayName": "Allow charging", + "type": "bool", + "displayNameAction": "Allow charging", + "displayNameEvent": "Allow charging 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, + "suggestLogging": true + }, + { + "id": "08b107bc-1284-455d-9e5a-6a1c3adc389f", + "name": "updateAvailable", + "displayName": "Update available", + "displayNameEvent": "Update available changed", + "type": "bool", + "defaultValue": false, + "suggestLogging": true + }, + { + "id": "d557e59e-ca22-4aff-bf80-dfee44db0f69", + "name": "adapterConnected", + "displayName": "Adapter connected", + "displayNameEvent": "Adapter connected changed", + "type": "bool", + "defaultValue": false, + "suggestLogging": true + }, + { + "id": "d8f5abb6-5db3-4040-8829-553b1d881ce4", + "name": "totalEnergyConsumed", + "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, + "suggestLogging": true + }, + { + "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", + "displayName": "Temperature 1", + "displayNameEvent": "Temperature 1 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0, + "suggestLogging": true + }, + { + "id": "558e273a-4028-495a-902a-e4e932a0ae24", + "name": "temperatureSensor2", + "displayName": "Temperature 2", + "displayNameEvent": "Temperature 2 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0, + "suggestLogging": true + }, + { + "id": "dbf8a5dc-b8f5-437a-ac0c-c4cf8a09aacb", + "name": "temperatureSensor3", + "displayName": "Temperature 3", + "displayNameEvent": "Temperature 3 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0, + "suggestLogging": true + }, + { + "id": "1953e29f-fe28-4016-9b05-f4baf4c311ff", + "name": "temperatureSensor4", + "displayName": "Temperature 4", + "displayNameEvent": "Temperature 4 changed", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0, + "suggestLogging": true + }, + { + "id": "5d18b48d-b886-409e-ab2e-336d9c94a55c", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "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 + } + ] + } + ] + } + ] +} + + + + 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 e86833fb..226fd80f 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -23,6 +23,7 @@ PLUGIN_DIRS = \ fronius \ genericelements \ genericthings \ + goecharger \ gpio \ i2cdevices \ httpcommander \