From 52e481bfaa3c7be99e5edcc6528d1330c9b11fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 5 Jun 2025 16:26:49 +0200 Subject: [PATCH] Implement discovery and reply error handling --- everest/everest.pro | 2 + everest/integrationplugineverest.cpp | 222 ++++++++++++++------ everest/integrationplugineverest.json | 183 +++++++++++++++- everest/jsonrpc/everestjsonrpcclient.cpp | 174 +++++++++++++-- everest/jsonrpc/everestjsonrpcclient.h | 27 ++- everest/jsonrpc/everestjsonrpcdiscovery.cpp | 142 +++++++++++++ everest/jsonrpc/everestjsonrpcdiscovery.h | 76 +++++++ everest/jsonrpc/everestjsonrpcreply.cpp | 23 +- everest/jsonrpc/everestjsonrpcreply.h | 27 ++- everest/mqtt/everestmqttdiscovery.cpp | 45 ++-- 10 files changed, 793 insertions(+), 128 deletions(-) create mode 100644 everest/jsonrpc/everestjsonrpcdiscovery.cpp create mode 100644 everest/jsonrpc/everestjsonrpcdiscovery.h diff --git a/everest/everest.pro b/everest/everest.pro index 8f2f4885..75fd54d1 100644 --- a/everest/everest.pro +++ b/everest/everest.pro @@ -5,6 +5,7 @@ PKGCONFIG += nymea-mqtt SOURCES += \ jsonrpc/everestjsonrpcclient.cpp \ + jsonrpc/everestjsonrpcdiscovery.cpp \ jsonrpc/everestjsonrpcinterface.cpp \ jsonrpc/everestjsonrpcreply.cpp \ mqtt/everestmqtt.cpp \ @@ -14,6 +15,7 @@ SOURCES += \ HEADERS += \ jsonrpc/everestjsonrpcclient.h \ + jsonrpc/everestjsonrpcdiscovery.h \ jsonrpc/everestjsonrpcinterface.h \ jsonrpc/everestjsonrpcreply.h \ mqtt/everestmqtt.h \ diff --git a/everest/integrationplugineverest.cpp b/everest/integrationplugineverest.cpp index 1a6e33c8..63cd1b39 100644 --- a/everest/integrationplugineverest.cpp +++ b/everest/integrationplugineverest.cpp @@ -31,6 +31,7 @@ #include "integrationplugineverest.h" #include "plugininfo.h" #include "mqtt/everestmqttdiscovery.h" +#include "jsonrpc/everestjsonrpcdiscovery.h" #include @@ -41,8 +42,8 @@ IntegrationPluginEverest::IntegrationPluginEverest() void IntegrationPluginEverest::init() { - EverestJsonRpcClient *client = new EverestJsonRpcClient(this); - client->setSeverUrl(QUrl("ws://10.10.10.165:8080")); + // EverestJsonRpcClient *client = new EverestJsonRpcClient(this); + // client->connectToServer(QUrl("ws://10.10.10.165:8080")); } void IntegrationPluginEverest::startMonitoringAutoThings() @@ -51,13 +52,13 @@ void IntegrationPluginEverest::startMonitoringAutoThings() // Since this integration plugin is most liekly running on an EV charger running EVerest, the local instance should // be set up automatically. Additional instances in the network can still be added by running a normal network discovery - EverestMqttDiscovery *discovery = new EverestMqttDiscovery(nullptr, this); - connect(discovery, &EverestMqttDiscovery::finished, discovery, &EverestMqttDiscovery::deleteLater); - connect(discovery, &EverestMqttDiscovery::finished, this, [this, discovery](){ + EverestMqttDiscovery *mqttDiscovery = new EverestMqttDiscovery(nullptr, this); + connect(mqttDiscovery, &EverestMqttDiscovery::finished, mqttDiscovery, &EverestMqttDiscovery::deleteLater); + connect(mqttDiscovery, &EverestMqttDiscovery::finished, this, [this, mqttDiscovery](){ ThingDescriptors descriptors; - foreach (const EverestMqttDiscovery::Result &result, discovery->results()) { + foreach (const EverestMqttDiscovery::Result &result, mqttDiscovery->results()) { // Create one EV charger foreach available connector on that host foreach(const QString &connectorName, result.connectors) { @@ -106,7 +107,7 @@ void IntegrationPluginEverest::startMonitoringAutoThings() } }); - discovery->startLocalhost(); + mqttDiscovery->startLocalhost(); } void IntegrationPluginEverest::discoverThings(ThingDiscoveryInfo *info) @@ -118,80 +119,163 @@ void IntegrationPluginEverest::discoverThings(ThingDiscoveryInfo *info) return; } - EverestMqttDiscovery *discovery = new EverestMqttDiscovery(hardwareManager()->networkDeviceDiscovery(), this); - connect(discovery, &EverestMqttDiscovery::finished, discovery, &EverestMqttDiscovery::deleteLater); - connect(discovery, &EverestMqttDiscovery::finished, info, [this, info, discovery](){ - foreach (const EverestMqttDiscovery::Result &result, discovery->results()) { + if (info->thingClassId() == everestMqttThingClassId) { + EverestMqttDiscovery *mqttDiscovery = new EverestMqttDiscovery(hardwareManager()->networkDeviceDiscovery(), this); + connect(mqttDiscovery, &EverestMqttDiscovery::finished, mqttDiscovery, &EverestMqttDiscovery::deleteLater); + connect(mqttDiscovery, &EverestMqttDiscovery::finished, info, [this, info, mqttDiscovery](){ - // Create one EV charger foreach available connector on that host - foreach(const QString &connectorName, result.connectors) { + foreach (const EverestMqttDiscovery::Result &result, mqttDiscovery->results()) { - QString title = QString("Everest (%1)").arg(connectorName); - QString description; - MacAddressInfo macInfo; + // Create one EV charger foreach available connector on that host + foreach(const QString &connectorName, result.connectors) { - switch (result.networkDeviceInfo.monitorMode()) { - case NetworkDeviceInfo::MonitorModeMac: - macInfo = result.networkDeviceInfo.macAddressInfos().constFirst(); - description = result.networkDeviceInfo.address().toString(); - if (!macInfo.vendorName().isEmpty()) - description += " - " + result.networkDeviceInfo.macAddressInfos().constFirst().vendorName(); + QString title = QString("Everest (%1)").arg(connectorName); + QString description; + MacAddressInfo macInfo; - break; - case NetworkDeviceInfo::MonitorModeHostName: - description = result.networkDeviceInfo.address().toString(); - break; - case NetworkDeviceInfo::MonitorModeIp: - description = "Interface: " + result.networkDeviceInfo.networkInterface().name(); - break; - } + switch (result.networkDeviceInfo.monitorMode()) { + case NetworkDeviceInfo::MonitorModeMac: + macInfo = result.networkDeviceInfo.macAddressInfos().constFirst(); + description = result.networkDeviceInfo.address().toString(); + if (!macInfo.vendorName().isEmpty()) + description += " - " + result.networkDeviceInfo.macAddressInfos().constFirst().vendorName(); - ThingDescriptor descriptor(everestMqttThingClassId, title, description); - qCInfo(dcEverest()) << "Discovered -->" << title << description; - - // Note: the network device info already provides the correct set of parameters in order to be used by the monitor - // depending on the possibilities within this network. It is not recommended to fill in all information available. - // Only the information available depending on the monitor mode are relevant for the monitor. - ParamList params; - params.append(Param(everestMqttThingConnectorParamTypeId, connectorName)); - params.append(Param(everestMqttThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress())); - params.append(Param(everestMqttThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName())); - params.append(Param(everestMqttThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress())); - descriptor.setParams(params); - - // Let's check if we aleardy have a thing with those params - bool thingExists = true; - Thing *existingThing = nullptr; - foreach (Thing *thing, myThings()) { - if (thing->thingClassId() != info->thingClassId()) - continue; - - foreach(const Param ¶m, params) { - if (param.value() != thing->paramValue(param.paramTypeId())) { - thingExists = false; - break; - } + break; + case NetworkDeviceInfo::MonitorModeHostName: + description = result.networkDeviceInfo.address().toString(); + break; + case NetworkDeviceInfo::MonitorModeIp: + description = "Interface: " + result.networkDeviceInfo.networkInterface().name(); + break; } - // The params are equal, we already know this thing - if (thingExists) - existingThing = thing; + ThingDescriptor descriptor(everestMqttThingClassId, title, description); + qCInfo(dcEverest()) << "Discovered -->" << title << description; + + // Note: the network device info already provides the correct set of parameters in order to be used by the monitor + // depending on the possibilities within this network. It is not recommended to fill in all information available. + // Only the information available depending on the monitor mode are relevant for the monitor. + ParamList params; + params.append(Param(everestMqttThingConnectorParamTypeId, connectorName)); + params.append(Param(everestMqttThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress())); + params.append(Param(everestMqttThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName())); + params.append(Param(everestMqttThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress())); + descriptor.setParams(params); + + // Let's check if we aleardy have a thing with those params + bool thingExists = true; + Thing *existingThing = nullptr; + foreach (Thing *thing, myThings()) { + if (thing->thingClassId() != info->thingClassId()) + continue; + + foreach(const Param ¶m, params) { + if (param.value() != thing->paramValue(param.paramTypeId())) { + thingExists = false; + break; + } + } + + // The params are equal, we already know this thing + if (thingExists) + existingThing = thing; + } + + // Set the thing ID id we already have this device (for reconfiguration) + if (existingThing) + descriptor.setThingId(existingThing->id()); + + info->addThingDescriptor(descriptor); } - - // Set the thing ID id we already have this device (for reconfiguration) - if (existingThing) - descriptor.setThingId(existingThing->id()); - - info->addThingDescriptor(descriptor); } - } - // All discovery results processed, we are done - info->finish(Thing::ThingErrorNoError); - }); + // All discovery results processed, we are done + info->finish(Thing::ThingErrorNoError); + }); - discovery->start(); + mqttDiscovery->start(); + return; + } + + if (info->thingClassId() == everestJsonRpcThingClassId) { + quint16 port = info->params().paramValue(everestJsonRpcDiscoveryPortParamTypeId).toUInt(); + EverestJsonRpcDiscovery *jsonRpcDiscovery = new EverestJsonRpcDiscovery(hardwareManager()->networkDeviceDiscovery(), port, this); + connect(jsonRpcDiscovery, &EverestJsonRpcDiscovery::finished, jsonRpcDiscovery, &EverestJsonRpcDiscovery::deleteLater); + connect(jsonRpcDiscovery, &EverestJsonRpcDiscovery::finished, info, [this, info, jsonRpcDiscovery, port](){ + + foreach (const EverestJsonRpcDiscovery::Result &result, jsonRpcDiscovery->results()) { + + // Create one EV charger foreach available connector on that host + // foreach(const QString &connectorName, result.connectors) { + + QString title = QString("Everest"); + QString description; + MacAddressInfo macInfo; + + switch (result.networkDeviceInfo.monitorMode()) { + case NetworkDeviceInfo::MonitorModeMac: + macInfo = result.networkDeviceInfo.macAddressInfos().constFirst(); + description = result.networkDeviceInfo.address().toString(); + if (!macInfo.vendorName().isEmpty()) + description += " - " + result.networkDeviceInfo.macAddressInfos().constFirst().vendorName(); + + break; + case NetworkDeviceInfo::MonitorModeHostName: + description = result.networkDeviceInfo.address().toString(); + break; + case NetworkDeviceInfo::MonitorModeIp: + description = "Interface: " + result.networkDeviceInfo.networkInterface().name(); + break; + } + + ThingDescriptor descriptor(everestJsonRpcThingClassId, title, description); + qCInfo(dcEverest()) << "Discovered -->" << title << description; + + // Note: the network device info already provides the correct set of parameters in order to be used by the monitor + // depending on the possibilities within this network. It is not recommended to fill in all information available. + // Only the information available depending on the monitor mode are relevant for the monitor. + ParamList params; + params.append(Param(everestJsonRpcThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress())); + params.append(Param(everestJsonRpcThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName())); + params.append(Param(everestJsonRpcThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress())); + params.append(Param(everestJsonRpcThingPortParamTypeId, port)); + descriptor.setParams(params); + + // Let's check if we aleardy have a thing with those params + bool thingExists = true; + Thing *existingThing = nullptr; + foreach (Thing *thing, myThings()) { + if (thing->thingClassId() != info->thingClassId()) + continue; + + foreach(const Param ¶m, params) { + if (param.value() != thing->paramValue(param.paramTypeId())) { + thingExists = false; + break; + } + } + + // The params are equal, we already know this thing + if (thingExists) + existingThing = thing; + } + + // Set the thing ID id we already have this device (for reconfiguration) + if (existingThing) + descriptor.setThingId(existingThing->id()); + + info->addThingDescriptor(descriptor); + } + // } + + // All discovery results processed, we are done + info->finish(Thing::ThingErrorNoError); + }); + + jsonRpcDiscovery->start(); + return; + } } void IntegrationPluginEverest::setupThing(ThingSetupInfo *info) diff --git a/everest/integrationplugineverest.json b/everest/integrationplugineverest.json index 2fb7a6af..d52a68e9 100644 --- a/everest/integrationplugineverest.json +++ b/everest/integrationplugineverest.json @@ -10,7 +10,7 @@ "thingClasses": [ { "name": "everestMqtt", - "displayName": "Everest", + "displayName": "Everest (MQTT)", "id": "965cbe0d-088c-42a2-965d-ceafbb8b01e9", "setupMethod": "JustAdd", "createMethods": ["discovery", "user"], @@ -171,6 +171,187 @@ ], "eventTypes": [ + ] + }, + { + "name": "everestJsonRpc", + "displayName": "Everest (JSON RPC)", + "id": "7f0387b9-670a-4e63-b100-ead9f6b8e46c", + "setupMethod": "JustAdd", + "createMethods": ["discovery", "user"], + "interfaces": [ "evcharger", "smartmeterconsumer", "networkdevice", "connectable" ], + "discoveryParamTypes": [ + { + "id": "ce21618e-1a2a-4dbc-8843-98eb443ca6c3", + "name": "port", + "displayName": "Port", + "type": "uint", + "defaultValue": 8080 + } + ], + "paramTypes": [ + { + "id": "c54926e2-7b88-4400-b1ca-d01a370919e7", + "name": "hostName", + "displayName": "Host name", + "type": "QString" + }, + { + "id": "b0b52731-56be-4b9e-931a-512b1f9dfe28", + "name": "address", + "displayName": "IP address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "839ece70-472c-4c99-97ae-476be75ba996", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "defaultValue": "00:00:00:00:00:00", + "readOnly": true + }, + { + "id": "e0b6b5d8-78db-4e47-83d2-f8a82cd94648", + "name": "connector", + "displayName": "Connector name", + "type": "QString", + "defaultValue": "" + }, + { + "id": "3c16a1fa-39a8-4ed0-ab00-14b71882726d", + "name": "port", + "displayName": "Port", + "type": "uint", + "defaultValue": 8080 + } + ], + "settingsTypes": [], + "stateTypes": [ + { + "id": "2cbe44b1-5b34-43d6-89d8-bece220270d3", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Enable or disable charging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "b4d4e47a-d229-409b-b6d4-d5c13783f445", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "unit": "Ampere", + "defaultValue": 6, + "minValue": 6, + "maxValue": 32, + "writable": true + }, + { + "id": "85184e0e-e17e-4f1d-8708-9e8b2f95df74", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false + }, + { + "id": "8a39135e-7149-4ccb-9e0a-1fb499107965", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false + }, + { + "id": "d95c8330-b219-4c70-837d-a7ab52c1f02a", + "name": "phaseCount", + "displayName": "Active phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "7dbdd0f3-79f3-404f-8aad-6319eb4a8385", + "name": "desiredPhaseCount", + "displayName": "Desired phase count", + "displayNameAction": "Set desired phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "possibleValues": [1, 3], + "writable": true, + "defaultValue": 3 + }, + { + "id": "1cd82dab-82d8-4dbb-852e-4567284d20e9", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "4c0a0f46-1f31-4d19-93c5-140dceb364ef", + "name": "totalEnergyConsumed", + "displayName": "Total energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0, + "cached": true + }, + { + "id": "ad0ab5f1-8803-426a-9110-29c9d788eedc", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "c20d0f42-afe4-47da-9c84-97fba99053b5", + "name": "currentPower", + "displayName": "Current power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "126a5d55-ee47-4859-8ef2-bc8e79d1372a", + "name": "state", + "displayName": "State", + "type": "QString", + "defaultValue": "", + "cached": false + }, + { + "id": "9df97785-91cd-482f-99f1-c910275bb630", + "name": "temperature", + "displayName": "Temperature", + "type": "double", + "unit": "DegreeCelsius", + "defaultValue": 0.0, + "cached": false + }, + { + "id": "f460d54d-8935-4f79-b79a-405de8f2b3ec", + "name": "fanSpeed", + "displayName": "Fan speed", + "type": "double", + "unit": "Rpm", + "defaultValue": 0, + "cached": false + } + ], + "actionTypes": [ + + ], + "eventTypes": [ + ] } ] diff --git a/everest/jsonrpc/everestjsonrpcclient.cpp b/everest/jsonrpc/everestjsonrpcclient.cpp index 6e8d6cec..5191208a 100644 --- a/everest/jsonrpc/everestjsonrpcclient.cpp +++ b/everest/jsonrpc/everestjsonrpcclient.cpp @@ -35,21 +35,89 @@ #include EverestJsonRpcClient::EverestJsonRpcClient(QObject *parent) - : QObject{parent} + : QObject{parent}, + m_interface{new EverestJsonRpcInterface(this)} { - m_interface = new EverestJsonRpcInterface(this); + connect(m_interface, &EverestJsonRpcInterface::dataReceived, this, &EverestJsonRpcClient::processDataPacket); connect(m_interface, &EverestJsonRpcInterface::connectedChanged, this, [this](bool connected){ + + qCDebug(dcEverest()) << "Interface is" << (connected ? "now connected" : "not connected any more"); + if (connected) { - EverestJsonRpcReply *reply = hello(); + + // The interface is connected, fetch initial data and mark the client as available if successfull, + // otherwise emit connection error signal and close the connection + + EverestJsonRpcReply *reply = apiHello(); + connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ qCDebug(dcEverest()) << "Reply finished" << m_interface->serverUrl().toString() << reply->method(); + if (reply->error()) { + qCWarning(dcEverest()) << "JsonRpc reply finished with error" << reply->method() << reply->error(); + disconnectFromServer(); + emit connectionErrorOccurred(); + return; + } + + // Verify data format and API version + QVariantMap result = reply->response().value("result").toMap(); + if (!result.contains("api_version") || !result.contains("everest_version") || !result.contains("charger_info")) { + qCWarning(dcEverest()) << "Missing expected properties in JsonRpc response" << reply->method(); + disconnectFromServer(); + emit connectionErrorOccurred(); + return; + } + + //D | Everest: <-- {"id":0,"jsonrpc":"2.0","result":{"api_version":"0.0.1","authentication_required":false,"charger_info":{"firmware_version":"unknown","model":"unknown","serial":"unknown","vendor":"unknown"},"everest_version":""} + m_apiVersion = result.value("api_version").toString(); + + EverestJsonRpcReply *reply = chargePointGetEVSEInfos(); + connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); + connect(reply, &EverestJsonRpcReply::finished, this, [this, reply](){ + qCDebug(dcEverest()) << "Reply finished" << m_interface->serverUrl().toString() << reply->method() + << qUtf8Printable(QJsonDocument::fromVariant(reply->response()).toJson(QJsonDocument::Indented)); + + if (reply->error()) { + qCWarning(dcEverest()) << "JsonRpc reply finished with error" << reply->method() << reply->error(); + disconnectFromServer(); + emit connectionErrorOccurred(); + return; + } + + QVariantMap result = reply->response().value("result").toMap(); + QString errorString = result.value("error").toString(); + if (errorString != "NoError") { + qCWarning(dcEverest()) << "Error requesting" << reply->method() << errorString; + disconnectFromServer(); + emit connectionErrorOccurred(); + return; + } + + // TODO: init infos + + // Init data, we are done and connected. + + if (!m_available) { + m_available = true; + emit availableChanged(m_available); + } + + }); }); } else { - // Client not available any more + // Client disconnected. Clean up any pending replies + qCDebug(dcEverest()) << "Lost connection to the server. Finish any pending replies ..."; + foreach (EverestJsonRpcReply *reply, m_replies) { + reply->finishReply(EverestJsonRpcReply::ErrorConnectionError); + } + + if (m_available) { + m_available = false; + emit availableChanged(m_available); + } } }); - connect(m_interface, &EverestJsonRpcInterface::dataReceived, this, &EverestJsonRpcClient::processDataPacket); } QUrl EverestJsonRpcClient::serverUrl() @@ -57,26 +125,79 @@ QUrl EverestJsonRpcClient::serverUrl() return m_interface->serverUrl(); } -void EverestJsonRpcClient::setSeverUrl(const QUrl &serverUrl) +bool EverestJsonRpcClient::available() const +{ + return m_available; +} + +EverestJsonRpcReply *EverestJsonRpcClient::apiHello() +{ + EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "API.Hello", QVariantMap(), this); + qCDebug(dcEverest()) << "Calling" << reply->method(); + sendRequest(reply); + return reply; +} + +EverestJsonRpcReply *EverestJsonRpcClient::chargePointGetEVSEInfos() +{ + EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "ChargePoint.GetEVSEInfos", QVariantMap(), this); + qCDebug(dcEverest()) << "Calling" << reply->method(); + sendRequest(reply); + return reply; +} + +EverestJsonRpcReply *EverestJsonRpcClient::evseGetInfo() +{ + EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "EVSE.GetInfo", QVariantMap(), this); + qCDebug(dcEverest()) << "Calling" << reply->method(); + sendRequest(reply); + return reply; +} + +EverestJsonRpcReply *EverestJsonRpcClient::evseGetStatus(int evseIndex) +{ + QVariantMap params; + params.insert("evse_index", evseIndex); + + EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "EVSE.GetStatus", params, this); + qCDebug(dcEverest()) << "Calling" << reply->method(); + sendRequest(reply); + return reply; +} + +EverestJsonRpcReply *EverestJsonRpcClient::evseGetHardwareCapabilities(int evseIndex) +{ + QVariantMap params; + params.insert("evse_index", evseIndex); + + EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "EVSE.GetStatus", params, this); + qCDebug(dcEverest()) << "Calling" << reply->method(); + sendRequest(reply); + return reply; +} + +void EverestJsonRpcClient::connectToServer(const QUrl &serverUrl) { m_interface->connectServer(serverUrl); } -EverestJsonRpcReply *EverestJsonRpcClient::hello() +void EverestJsonRpcClient::disconnectFromServer() { - EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "API.Hello", QVariantMap(), this); - qCDebug(dcEverest()) << "Calling" << reply->method(); - sendRequest(reply->requestMap()); - m_replies.insert(m_commandId, reply); - m_commandId++; - return reply; + m_interface->disconnectServer(); } -void EverestJsonRpcClient::sendRequest(const QVariantMap &request) +void EverestJsonRpcClient::sendRequest(EverestJsonRpcReply *reply) { - QByteArray data = QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact) + '\n'; + QVariantMap requestMap = reply->requestMap(); + QByteArray data = QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Compact) + '\n'; + qCDebug(dcEverest()) << "-->" << m_interface->serverUrl().toString() << qUtf8Printable(data); m_interface->sendData(data); + + m_replies.insert(m_commandId, reply); + m_commandId++; + + reply->startWaiting(); } void EverestJsonRpcClient::processDataPacket(const QByteArray &data) @@ -91,19 +212,26 @@ void EverestJsonRpcClient::processDataPacket(const QByteArray &data) } QVariantMap dataMap = jsonDoc.toVariant().toMap(); + if (!dataMap.contains("id") || dataMap.value("jsonrpc").toString() != "2.0") { + qCWarning(dcEverest()) << "Received valid JSON data but does not seem to be a JSON RPC 2.0 format" << m_interface->serverUrl().toString() << qUtf8Printable(data); + return; + } int commandId = dataMap.value("id").toInt(); EverestJsonRpcReply *reply = m_replies.take(commandId); if (reply) { - qCDebug(dcEverest()) << QString("Got response for %1: %2").arg(reply->method(), QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Indented))); - - // if (dataMap.value("status").toString() == "error") { - // qCWarning(dcEverest()) << "Api error happend" << dataMap.value("error").toString(); - // // FIMXME: handle json layer errors - // } - reply->setResponse(dataMap); - emit reply->finished(); + + // Verify if we received a json rpc error + if (dataMap.contains("error")) { + reply->finishReply(EverestJsonRpcReply::ErrorJsonRpcError); + } else { + reply->finishReply(); + } + return; + } else { + // Data without reply, check if this is a notification + } } diff --git a/everest/jsonrpc/everestjsonrpcclient.h b/everest/jsonrpc/everestjsonrpcclient.h index 6ab0fe2b..46fb40f5 100644 --- a/everest/jsonrpc/everestjsonrpcclient.h +++ b/everest/jsonrpc/everestjsonrpcclient.h @@ -47,24 +47,35 @@ public: bool available() const; - EverestJsonRpcReply *hello(); + QString apiVersion() const; + + // API calls + EverestJsonRpcReply *apiHello(); + EverestJsonRpcReply *chargePointGetEVSEInfos(); + + EverestJsonRpcReply *evseGetInfo(); + EverestJsonRpcReply *evseGetStatus(int evseIndex); + EverestJsonRpcReply *evseGetHardwareCapabilities(int evseIndex); + +public slots: + void connectToServer(const QUrl &serverUrl); + void disconnectFromServer(); signals: + void connectionErrorOccurred(); void availableChanged(bool available); +private slots: + void sendRequest(EverestJsonRpcReply *reply); + void processDataPacket(const QByteArray &data); + private: bool m_available = false; - int m_commandId = 0; EverestJsonRpcInterface *m_interface = nullptr; - - QHash m_replies; - void sendRequest(const QVariantMap &request); - -private slots: - void processDataPacket(const QByteArray &data); + QString m_apiVersion; }; diff --git a/everest/jsonrpc/everestjsonrpcdiscovery.cpp b/everest/jsonrpc/everestjsonrpcdiscovery.cpp new file mode 100644 index 00000000..6825695a --- /dev/null +++ b/everest/jsonrpc/everestjsonrpcdiscovery.cpp @@ -0,0 +1,142 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, 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 "everestjsonrpcdiscovery.h" +#include "extern-plugininfo.h" + +EverestJsonRpcDiscovery::EverestJsonRpcDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port, QObject *parent) + : QObject{parent}, + m_networkDeviceDiscovery{networkDeviceDiscovery}, + m_port{port} +{ + +} + +void EverestJsonRpcDiscovery::start() +{ + qCInfo(dcEverest()) << "Discovery: Start discovering Everest JsonRpc instances in the network..."; + m_startDateTime = QDateTime::currentDateTime(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &EverestJsonRpcDiscovery::checkHostAddress); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [discoveryReply, this](){ + qCDebug(dcEverest()) << "Discovery: Network device discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + + // Give the last connections added right before the network discovery finished a chance to check the device... + QTimer::singleShot(3000, this, [this](){ + qCDebug(dcEverest()) << "Discovery: Grace period timer triggered."; + finishDiscovery(); + }); + }); + + NetworkDeviceInfo localHostInfo; + checkHostAddress(QHostAddress::LocalHost); +} + +void EverestJsonRpcDiscovery::startLocalhost() +{ + qCInfo(dcEverest()) << "Discovery: Start discovering EVerest on localhost ..."; + m_startDateTime = QDateTime::currentDateTime(); + m_localhostDiscovery = true; + + // For development, check local host + NetworkDeviceInfo localHostInfo; + checkHostAddress(QHostAddress::LocalHost); +} + +QList EverestJsonRpcDiscovery::results() const +{ + return m_results; +} + +void EverestJsonRpcDiscovery::checkHostAddress(const QHostAddress &address) +{ + QUrl url; + url.setScheme("ws"); + url.setHost(address.toString()); + url.setPort(m_port); + + EverestJsonRpcClient *client = new EverestJsonRpcClient(this); + connect(client, &EverestJsonRpcClient::availableChanged, this, [this, client, address](bool available){ + if (available) { + qCDebug(dcEverest()) << "Discovery: Found JsonRpc interface on" << client->serverUrl().toString(); + + Result result; + result.address = address; + m_results.append(result); + + cleanupClient(client); + } + }); + + connect(client, &EverestJsonRpcClient::connectionErrorOccurred, this, [this, client](){ + qCDebug(dcEverest()) << "Discovery: The connection to" << client->serverUrl().toString() << "failed. Skipping host"; + cleanupClient(client); + }); + + client->connectToServer(url); +} + +void EverestJsonRpcDiscovery::cleanupClient(EverestJsonRpcClient *client) +{ + if (!m_clients.contains(client)) + return; + + m_clients.removeAll(client); + + client->disconnectFromServer(); + client->deleteLater(); + + if (m_localhostDiscovery) + finishDiscovery(); + +} + +void EverestJsonRpcDiscovery::finishDiscovery() +{ + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + + // Cleanup any leftovers + foreach (EverestJsonRpcClient *client, m_clients) + cleanupClient(client); + + // Update results with final network device infos + for (int i = 0; i < m_results.count(); i++) + m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address); + + qCInfo(dcEverest()) << "Discovery: Finished the discovery process. Found" + << m_results.count() << "Everest JsonRpc instances in" + << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + + emit finished(); +} diff --git a/everest/jsonrpc/everestjsonrpcdiscovery.h b/everest/jsonrpc/everestjsonrpcdiscovery.h new file mode 100644 index 00000000..9434f3af --- /dev/null +++ b/everest/jsonrpc/everestjsonrpcdiscovery.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, 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 EVERESTJSONRPCDISCOVERY_H +#define EVERESTJSONRPCDISCOVERY_H + +#include + +#include + +#include "jsonrpc/everestjsonrpcclient.h" + +class EverestJsonRpcDiscovery : public QObject +{ + Q_OBJECT +public: + typedef struct Result { + QHostAddress address; + NetworkDeviceInfo networkDeviceInfo; + } Result; + + explicit EverestJsonRpcDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, quint16 port = 8080, QObject *parent = nullptr); + + void start(); + void startLocalhost(); + + QList results() const; + +signals: + void finished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + quint16 m_port = 8080; + + QDateTime m_startDateTime; + NetworkDeviceInfos m_networkDeviceInfos; + QList m_clients; + QList m_results; + + bool m_localhostDiscovery = false; + + void checkHostAddress(const QHostAddress &address); + void cleanupClient(EverestJsonRpcClient *client); + void finishDiscovery(); + +}; + +#endif // EVERESTJSONRPCDISCOVERY_H diff --git a/everest/jsonrpc/everestjsonrpcreply.cpp b/everest/jsonrpc/everestjsonrpcreply.cpp index 8efb863c..63374f9d 100644 --- a/everest/jsonrpc/everestjsonrpcreply.cpp +++ b/everest/jsonrpc/everestjsonrpcreply.cpp @@ -36,7 +36,17 @@ EverestJsonRpcReply::EverestJsonRpcReply(int commandId, QString method, QVariant m_method{method}, m_params{params} { + m_timer.setInterval(2000); + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, this, [this](){ + m_error = ErrorTimeout; + emit finished(); + }); +} +EverestJsonRpcReply::Error EverestJsonRpcReply::error() const +{ + return m_error; } int EverestJsonRpcReply::commandId() const @@ -44,7 +54,6 @@ int EverestJsonRpcReply::commandId() const return m_commandId; } - QString EverestJsonRpcReply::method() const { return m_method; @@ -77,3 +86,15 @@ void EverestJsonRpcReply::setResponse(const QVariantMap &response) { m_response = response; } + +void EverestJsonRpcReply::startWaiting() +{ + m_timer.start(); +} + +void EverestJsonRpcReply::finishReply(Error error) +{ + m_timer.stop(); + m_error = error; + emit finished(); +} diff --git a/everest/jsonrpc/everestjsonrpcreply.h b/everest/jsonrpc/everestjsonrpcreply.h index 150069a6..da702b56 100644 --- a/everest/jsonrpc/everestjsonrpcreply.h +++ b/everest/jsonrpc/everestjsonrpcreply.h @@ -31,32 +31,53 @@ #ifndef EVERESTJSONRPCREPLY_H #define EVERESTJSONRPCREPLY_H +#include #include #include class EverestJsonRpcReply : public QObject { Q_OBJECT -public: - explicit EverestJsonRpcReply(int commandId, QString method, QVariantMap params = QVariantMap(), QObject *parent = nullptr); + friend class EverestJsonRpcClient; + +public: + enum Error { + ErrorNoError = 0, + ErrorTimeout, + ErrorConnectionError, + ErrorJsonRpcError + }; + Q_ENUM(Error) + + Error error() const; + + // Request int commandId() const; QString method() const; QVariantMap params() const; QVariantMap requestMap(); + // Response QVariantMap response() const; - void setResponse(const QVariantMap &response); signals: void finished(); private: + explicit EverestJsonRpcReply(int commandId, QString method, QVariantMap params = QVariantMap(), QObject *parent = nullptr); + int m_commandId; QString m_method; QVariantMap m_params; QVariantMap m_response; + QTimer m_timer; + Error m_error = ErrorNoError; + + void setResponse(const QVariantMap &response); + void startWaiting(); + void finishReply(Error error = ErrorNoError); }; #endif // EVERESTJSONRPCREPLY_H diff --git a/everest/mqtt/everestmqttdiscovery.cpp b/everest/mqtt/everestmqttdiscovery.cpp index c556730f..af3ce7f2 100644 --- a/everest/mqtt/everestmqttdiscovery.cpp +++ b/everest/mqtt/everestmqttdiscovery.cpp @@ -111,32 +111,31 @@ void EverestMqttDiscovery::checkHostAddress(const QHostAddress &address) // We found a mqtt server, let's check if we find everest_api module on it... qCDebug(dcEverest()) << "Discovery: Successfully connected to host" << address.toString(); - connect(client, &MqttClient::publishReceived, client, [this, client, address] - (const QString &topic, const QByteArray &payload, bool retained) { + connect(client, &MqttClient::publishReceived, client, [this, client, address] (const QString &topic, const QByteArray &payload, bool retained) { - qCDebug(dcEverest()) << "Discovery: Received publish on" << topic - << "retained:" << retained << qUtf8Printable(payload); + qCDebug(dcEverest()) << "Discovery: Received publish on" << topic + << "retained:" << retained << qUtf8Printable(payload); - if (topic == m_everestApiModuleTopicConnectors) { - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &jsonError); - if (jsonError.error) { - qCDebug(dcEverest()) << "Discovery: Received payload on topic" << topic - << "with JSON error:" << jsonError.errorString(); - cleanupClient(client); - return; - } + if (topic == m_everestApiModuleTopicConnectors) { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &jsonError); + if (jsonError.error) { + qCDebug(dcEverest()) << "Discovery: Received payload on topic" << topic + << "with JSON error:" << jsonError.errorString(); + cleanupClient(client); + return; + } - QStringList connectors = jsonDoc.toVariant().toStringList(); - qCInfo(dcEverest()) << "Discovery: Found Everest on" << address.toString() << connectors; - Result result; - result.address = address; - result.connectors = connectors; - m_results.append(result); + QStringList connectors = jsonDoc.toVariant().toStringList(); + qCInfo(dcEverest()) << "Discovery: Found Everest on" << address.toString() << connectors; + Result result; + result.address = address; + result.connectors = connectors; + m_results.append(result); - cleanupClient(client); - } - }); + cleanupClient(client); + } + }); connect(client, &MqttClient::subscribeResult, client, [this, client] (quint16 packetId, const Mqtt::SubscribeReturnCodes &subscribeReturnCodes) { @@ -189,7 +188,7 @@ void EverestMqttDiscovery::finishDiscovery() m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address); qCInfo(dcEverest()) << "Discovery: Finished the discovery process. Found" - << m_results.count() << "Everest instances in" + << m_results.count() << "Everest mqtt instances in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); emit finished();