Implement discovery and reply error handling

master
Simon Stürz 2025-06-05 16:26:49 +02:00
parent 5acce61d6a
commit 52e481bfaa
10 changed files with 793 additions and 128 deletions

View File

@ -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 \

View File

@ -31,6 +31,7 @@
#include "integrationplugineverest.h"
#include "plugininfo.h"
#include "mqtt/everestmqttdiscovery.h"
#include "jsonrpc/everestjsonrpcdiscovery.h"
#include <network/networkdevicediscovery.h>
@ -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 &param, 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 &param, 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 &param, 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)

View File

@ -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": [
]
}
]

View File

@ -35,21 +35,89 @@
#include <QJsonParseError>
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
}
}

View File

@ -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<int, EverestJsonRpcReply *> m_replies;
void sendRequest(const QVariantMap &request);
private slots:
void processDataPacket(const QByteArray &data);
QString m_apiVersion;
};

View File

@ -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 <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 "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::Result> 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();
}

View File

@ -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 <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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef EVERESTJSONRPCDISCOVERY_H
#define EVERESTJSONRPCDISCOVERY_H
#include <QObject>
#include <network/networkdevicediscovery.h>
#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<EverestJsonRpcDiscovery::Result> results() const;
signals:
void finished();
private:
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
quint16 m_port = 8080;
QDateTime m_startDateTime;
NetworkDeviceInfos m_networkDeviceInfos;
QList<EverestJsonRpcClient *> m_clients;
QList<EverestJsonRpcDiscovery::Result> m_results;
bool m_localhostDiscovery = false;
void checkHostAddress(const QHostAddress &address);
void cleanupClient(EverestJsonRpcClient *client);
void finishDiscovery();
};
#endif // EVERESTJSONRPCDISCOVERY_H

View File

@ -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();
}

View File

@ -31,32 +31,53 @@
#ifndef EVERESTJSONRPCREPLY_H
#define EVERESTJSONRPCREPLY_H
#include <QTimer>
#include <QObject>
#include <QVariantMap>
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

View File

@ -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();