424 lines
21 KiB
C++
424 lines
21 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*
|
|
* 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 <QUrlQuery>
|
|
#include <QHostAddress>
|
|
#include <QDataStream>
|
|
#include <QJsonDocument>
|
|
#include <QJsonParseError>
|
|
|
|
// 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 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)
|
|
{
|
|
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<CarState>(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<Access>(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);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
|