From c4b8a9a77e881c09aac11c804d337e6b67b9b8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 11 Jun 2025 16:47:27 +0200 Subject: [PATCH] Introduce EVerest connection as gateway thing --- everest/everest.pro | 2 + everest/integrationplugineverest.cpp | 278 +++++++++++++++-------- everest/integrationplugineverest.h | 8 +- everest/integrationplugineverest.json | 109 ++++++--- everest/jsonrpc/everestconnection.cpp | 174 ++++++++++++++ everest/jsonrpc/everestconnection.h | 82 +++++++ everest/jsonrpc/everestjsonrpcclient.cpp | 67 +++++- everest/jsonrpc/everestjsonrpcclient.h | 70 +++++- 8 files changed, 651 insertions(+), 139 deletions(-) create mode 100644 everest/jsonrpc/everestconnection.cpp create mode 100644 everest/jsonrpc/everestconnection.h diff --git a/everest/everest.pro b/everest/everest.pro index 75fd54d1..f42fcc13 100644 --- a/everest/everest.pro +++ b/everest/everest.pro @@ -4,6 +4,7 @@ QT += network websockets PKGCONFIG += nymea-mqtt SOURCES += \ + jsonrpc/everestconnection.cpp \ jsonrpc/everestjsonrpcclient.cpp \ jsonrpc/everestjsonrpcdiscovery.cpp \ jsonrpc/everestjsonrpcinterface.cpp \ @@ -14,6 +15,7 @@ SOURCES += \ integrationplugineverest.cpp HEADERS += \ + jsonrpc/everestconnection.h \ jsonrpc/everestjsonrpcclient.h \ jsonrpc/everestjsonrpcdiscovery.h \ jsonrpc/everestjsonrpcinterface.h \ diff --git a/everest/integrationplugineverest.cpp b/everest/integrationplugineverest.cpp index 63cd1b39..2fe8e977 100644 --- a/everest/integrationplugineverest.cpp +++ b/everest/integrationplugineverest.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2024, nymea GmbH +* Copyright 2013 - 2025, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -42,8 +42,7 @@ IntegrationPluginEverest::IntegrationPluginEverest() void IntegrationPluginEverest::init() { - // EverestJsonRpcClient *client = new EverestJsonRpcClient(this); - // client->connectToServer(QUrl("ws://10.10.10.165:8080")); + } void IntegrationPluginEverest::startMonitoringAutoThings() @@ -119,7 +118,6 @@ void IntegrationPluginEverest::discoverThings(ThingDiscoveryInfo *info) return; } - if (info->thingClassId() == everestMqttThingClassId) { EverestMqttDiscovery *mqttDiscovery = new EverestMqttDiscovery(hardwareManager()->networkDeviceDiscovery(), this); connect(mqttDiscovery, &EverestMqttDiscovery::finished, mqttDiscovery, &EverestMqttDiscovery::deleteLater); @@ -198,76 +196,72 @@ void IntegrationPluginEverest::discoverThings(ThingDiscoveryInfo *info) return; } - if (info->thingClassId() == everestJsonRpcThingClassId) { - quint16 port = info->params().paramValue(everestJsonRpcDiscoveryPortParamTypeId).toUInt(); + if (info->thingClassId() == everestConnectionThingClassId) { + quint16 port = info->params().paramValue(everestConnectionDiscoveryPortParamTypeId).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; - 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(); - 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); + break; + case NetworkDeviceInfo::MonitorModeHostName: + description = result.networkDeviceInfo.address().toString(); + break; + case NetworkDeviceInfo::MonitorModeIp: + description = "Interface: " + result.networkDeviceInfo.networkInterface().name(); + break; } - // } + + ThingDescriptor descriptor(everestConnectionThingClassId, 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(everestConnectionThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress())); + params.append(Param(everestConnectionThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName())); + params.append(Param(everestConnectionThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress())); + params.append(Param(everestConnectionThingPortParamTypeId, 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); @@ -282,42 +276,129 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); - QHostAddress address(thing->paramValue(everestMqttThingAddressParamTypeId).toString()); - MacAddress macAddress(thing->paramValue(everestMqttThingMacAddressParamTypeId).toString()); - QString hostName(thing->paramValue(everestMqttThingHostNameParamTypeId).toString()); - QString connector(thing->paramValue(everestMqttThingConnectorParamTypeId).toString()); + qCDebug(dcEverest()) << "Setting up" << thing << thing->params(); - EverestMqttClient *everstClient = nullptr; + if (thing->thingClassId() == everestMqttThingClassId) { - foreach (EverestMqttClient *ec, m_everstClients) { - if (ec->monitor()->macAddress() == macAddress && - ec->monitor()->hostName() == hostName && - ec->monitor()->address() == address) { - // We have already a client for this host - qCDebug(dcEverest()) << "Using existing" << ec; - everstClient = ec; + QHostAddress address(thing->paramValue(everestMqttThingAddressParamTypeId).toString()); + MacAddress macAddress(thing->paramValue(everestMqttThingMacAddressParamTypeId).toString()); + QString hostName(thing->paramValue(everestMqttThingHostNameParamTypeId).toString()); + QString connector(thing->paramValue(everestMqttThingConnectorParamTypeId).toString()); + + EverestMqttClient *everstClient = nullptr; + + foreach (EverestMqttClient *ec, m_everstMqttClients) { + if (ec->monitor()->macAddress() == macAddress && + ec->monitor()->hostName() == hostName && + ec->monitor()->address() == address) { + // We have already a client for this host + qCDebug(dcEverest()) << "Using existing" << ec; + everstClient = ec; + } } - } - if (!everstClient) { - NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); - if (!monitor) { - qCWarning(dcEverest()) << "Incomplete paramerters. Could not register network device monitor with these thing paramaters:" << thing->name() << thing->params(); - info->finish(Thing::ThingErrorMissingParameter); + if (!everstClient) { + everstClient = new EverestMqttClient(this); + everstClient->setMonitor(hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing)); + m_everstMqttClients.append(everstClient); + qCDebug(dcEverest()) << "Created new" << everstClient; + everstClient->start(); + } + + everstClient->addThing(thing); + m_thingClients.insert(thing, everstClient); + info->finish(Thing::ThingErrorNoError); + return; + } else if (thing->thingClassId() == everestConnectionThingClassId) { + + QHostAddress address(thing->paramValue(everestConnectionThingAddressParamTypeId).toString()); + MacAddress macAddress(thing->paramValue(everestConnectionThingMacAddressParamTypeId).toString()); + QString hostName(thing->paramValue(everestConnectionThingHostNameParamTypeId).toString()); + + quint16 port = thing->paramValue(everestConnectionThingPortParamTypeId).toUInt(); + + EverestConnection *connection = nullptr; + + // Handle reconfigure + if (m_everstConnections.contains(thing)) { + connection = m_everstConnections.take(thing); + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(connection->monitor()); + connection->deleteLater(); + connection = nullptr; + } + + connection = new EverestConnection(port, this); + connection->setMonitor(hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing)); + m_everstConnections.insert(thing, connection); + + connect(connection, &EverestConnection::availableChanged, thing, [this, thing, connection](bool available){ + thing->setStateValue(everestConnectionConnectedStateTypeId, available); + // Update connected state of child things + foreach (Thing *child, myThings().filterByParentId(thing->id())) { + child->setStateValue("connected", available); + } + + if (available) { + thing->setStateValue(everestConnectionApiVersionStateTypeId, connection->client()->apiVersion()); + + // Sync things with availabe EvseInfos + + ThingDescriptors descriptors; + qCDebug(dcEverest()) << "The client is now available, synching things..."; + foreach (const EverestJsonRpcClient::EVSEInfo &evseInfo, connection->client()->evseInfos()) { + // FIXME: somehow we need to now if this evse is AC or DC in order to spawn the right child thingclass. + + // Check if we already have a child device for this index + bool alreadyAdded = false; + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + if (childThing->paramValue("index").toInt() == evseInfo.index) { + qCDebug(dcEverest()) << "-> Found already added charger with index" << evseInfo.index; + alreadyAdded = true; + break; + } + } + + if (!alreadyAdded) { + qCDebug(dcEverest()) << "-> Adding new discovered AC charger on" << connection->client()->serverUrl(); + ThingDescriptor descriptor(everestChargerAcThingClassId, evseInfo.id, evseInfo.description, thing->id()); + descriptor.setParams(ParamList() << Param(everestChargerAcThingIndexParamTypeId, evseInfo.index)); + descriptors.append(descriptor); + } + } + + + // TODO: evaluate if any thing dissapeared + + if (!descriptors.isEmpty()) { + emit autoThingsAppeared(descriptors); + } + + } + }); + + info->finish(Thing::ThingErrorNoError); + + connection->start(); + return; + } else if (thing->thingClassId() == everestChargerAcThingClassId) { + + Thing *parentThing = myThings().findById(thing->parentId()); + EverestConnection *connection = m_everstConnections.value(parentThing); + if (!connection) { + info->finish(Thing::ThingErrorHardwareNotAvailable); return; } - everstClient = new EverestMqttClient(this); - everstClient->setMonitor(monitor); - m_everstClients.append(everstClient); - qCDebug(dcEverest()) << "Created new" << everstClient; - everstClient->start(); + info->finish(Thing::ThingErrorNoError); + + thing->setStateValue(everestChargerAcConnectedStateTypeId, connection->available()); } - everstClient->addThing(thing); - m_thingClients.insert(thing, everstClient); - info->finish(Thing::ThingErrorNoError); - return; +} + +void IntegrationPluginEverest::postSetupThing(Thing *thing) +{ + Q_UNUSED(thing) } void IntegrationPluginEverest::executeAction(ThingActionInfo *info) @@ -381,21 +462,30 @@ void IntegrationPluginEverest::executeAction(ThingActionInfo *info) void IntegrationPluginEverest::thingRemoved(Thing *thing) { qCDebug(dcEverest()) << "Remove thing" << thing; + if (thing->thingClassId() == everestMqttThingClassId) { EverestMqttClient *everestClient = m_thingClients.take(thing); everestClient->removeThing(thing); if (everestClient->things().isEmpty()) { qCDebug(dcEverest()) << "Deleting" << everestClient << "since there is no thing left"; // No more things related to this client, we can delete it - m_everstClients.removeAll(everestClient); + m_everstMqttClients.removeAll(everestClient); // Unregister monitor if (everestClient->monitor()) hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(everestClient->monitor()); - everestClient->deleteLater(); } + } else if (thing->thingClassId() == everestConnectionThingClassId) { + m_everstConnections.take(thing)->deleteLater(); + } else if (thing->thingClassId() == everestChargerAcThingClassId) { + Thing *parentThing = myThings().findById(thing->parentId()); + EverestConnection *connection = m_everstConnections.value(parentThing); + if (!connection) + return; + + connection->removeThing(thing); } } diff --git a/everest/integrationplugineverest.h b/everest/integrationplugineverest.h index f4f77a52..cf35f61a 100644 --- a/everest/integrationplugineverest.h +++ b/everest/integrationplugineverest.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2024, nymea GmbH +* Copyright 2013 - 2025, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -35,7 +35,7 @@ #include "extern-plugininfo.h" #include "mqtt/everestmqttclient.h" -#include "jsonrpc/everestjsonrpcclient.h" +#include "jsonrpc/everestconnection.h" #include @@ -54,14 +54,16 @@ public: void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; void thingRemoved(Thing *thing) override; void executeAction(ThingActionInfo *info) override; private: - QList m_everstClients; + QList m_everstMqttClients; QHash m_thingClients; + QHash m_everstConnections; }; #endif // INTEGRATIONPLUGINEVEREST_H diff --git a/everest/integrationplugineverest.json b/everest/integrationplugineverest.json index d52a68e9..f5b64779 100644 --- a/everest/integrationplugineverest.json +++ b/everest/integrationplugineverest.json @@ -174,15 +174,16 @@ ] }, { - "name": "everestJsonRpc", - "displayName": "Everest (JSON RPC)", - "id": "7f0387b9-670a-4e63-b100-ead9f6b8e46c", + "name": "everestConnection", + "displayName": "EVerest connection", + "id": "6a69c792-14e0-40a1-bbb2-94dcb84d82df", "setupMethod": "JustAdd", - "createMethods": ["discovery", "user"], - "interfaces": [ "evcharger", "smartmeterconsumer", "networkdevice", "connectable" ], + "createMethods": [ "Discovery", "User" ], + "interfaces": [ "gateway", "networkdevice" ], + "providedInterfaces": [ "evcharger" ], "discoveryParamTypes": [ { - "id": "ce21618e-1a2a-4dbc-8843-98eb443ca6c3", + "id": "cd1cfca6-22f7-4d31-a95f-f642e0e1470f", "name": "port", "displayName": "Port", "type": "uint", @@ -191,13 +192,13 @@ ], "paramTypes": [ { - "id": "c54926e2-7b88-4400-b1ca-d01a370919e7", + "id": "2af7a13b-f68c-40ab-960f-64f4e4885913", "name": "hostName", "displayName": "Host name", "type": "QString" }, { - "id": "b0b52731-56be-4b9e-931a-512b1f9dfe28", + "id": "3761a8e8-5cff-4285-8dc5-58a5655da603", "name": "address", "displayName": "IP address", "type": "QString", @@ -205,7 +206,7 @@ "defaultValue": "" }, { - "id": "839ece70-472c-4c99-97ae-476be75ba996", + "id": "f9c425f3-fe72-422d-b57c-0f2e7518ac38", "name": "macAddress", "displayName": "MAC address", "type": "QString", @@ -213,24 +214,66 @@ "readOnly": true }, { - "id": "e0b6b5d8-78db-4e47-83d2-f8a82cd94648", - "name": "connector", - "displayName": "Connector name", - "type": "QString", - "defaultValue": "" - }, - { - "id": "3c16a1fa-39a8-4ed0-ab00-14b71882726d", + "id": "d8f64b57-37e7-4b48-857c-a82ac9bfbd28", "name": "port", "displayName": "Port", "type": "uint", "defaultValue": 8080 + }, + { + "id": "088ef8e6-7979-40d0-8429-3a8dec3866fa", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "cf9930ed-077a-4396-826e-23634a01bffb", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "b1695978-8f6e-499d-8b2b-5e9e09ad73ff", + "name": "apiVersion", + "displayName": "API Version", + "type": "QString", + "defaultValue": "" + } + ] + }, + { + "name": "everestChargerAc", + "displayName": "EVerest AC Charger", + "id": "ec47ac6b-e6ec-4a6c-a584-ac6802f79d12", + "setupMethod": "JustAdd", + "createMethods": ["auto"], + "interfaces": [ "evcharger", "smartmeterconsumer", "connectable" ], + "paramTypes": [ + { + "id": "b9773de0-a4a8-457e-b221-448d299b9e38", + "name": "index", + "displayName": "Index", + "type": "uint", + "defaultValue": 0 } ], "settingsTypes": [], "stateTypes": [ { - "id": "2cbe44b1-5b34-43d6-89d8-bece220270d3", + "id": "c117636b-f3c3-4930-8b93-83732627c6a8", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "d683be61-113d-4501-bb42-991117fa7889", "name": "power", "displayName": "Charging enabled", "displayNameAction": "Enable or disable charging", @@ -239,7 +282,7 @@ "writable": true }, { - "id": "b4d4e47a-d229-409b-b6d4-d5c13783f445", + "id": "11d76fab-b48e-443d-9b4f-1dd13a495256", "name": "maxChargingCurrent", "displayName": "Maximum charging current", "displayNameAction": "Set maximum charging current", @@ -251,21 +294,21 @@ "writable": true }, { - "id": "85184e0e-e17e-4f1d-8708-9e8b2f95df74", + "id": "22d66da7-6935-4497-914b-38936267bf25", "name": "pluggedIn", "displayName": "Plugged in", "type": "bool", "defaultValue": false }, { - "id": "8a39135e-7149-4ccb-9e0a-1fb499107965", + "id": "aab3a0d8-f35d-4430-89b5-67a72be1fa50", "name": "charging", "displayName": "Charging", "type": "bool", "defaultValue": false }, { - "id": "d95c8330-b219-4c70-837d-a7ab52c1f02a", + "id": "58756435-11bf-4896-bdff-e221aa8ffbcc", "name": "phaseCount", "displayName": "Active phases", "type": "uint", @@ -274,7 +317,7 @@ "defaultValue": 1 }, { - "id": "7dbdd0f3-79f3-404f-8aad-6319eb4a8385", + "id": "8d3d0334-c410-47ce-ae56-7f8db980a7c7", "name": "desiredPhaseCount", "displayName": "Desired phase count", "displayNameAction": "Set desired phase count", @@ -286,15 +329,7 @@ "defaultValue": 3 }, { - "id": "1cd82dab-82d8-4dbb-852e-4567284d20e9", - "name": "connected", - "displayName": "Connected", - "type": "bool", - "cached": false, - "defaultValue": false - }, - { - "id": "4c0a0f46-1f31-4d19-93c5-140dceb364ef", + "id": "2bb15b1e-43a2-4102-bbc2-04689bb749eb", "name": "totalEnergyConsumed", "displayName": "Total energy", "type": "double", @@ -303,7 +338,7 @@ "cached": true }, { - "id": "ad0ab5f1-8803-426a-9110-29c9d788eedc", + "id": "905a48b1-b0ec-4e14-93c4-1c629e9af790", "name": "sessionEnergy", "displayName": "Session energy", "type": "double", @@ -312,7 +347,7 @@ "suggestLogging": true }, { - "id": "c20d0f42-afe4-47da-9c84-97fba99053b5", + "id": "28219545-18a9-4e5d-8c05-30805c87ac74", "name": "currentPower", "displayName": "Current power", "type": "double", @@ -321,7 +356,7 @@ "cached": false }, { - "id": "126a5d55-ee47-4859-8ef2-bc8e79d1372a", + "id": "9226f350-9ffd-4e0d-808b-67da51496ecb", "name": "state", "displayName": "State", "type": "QString", @@ -329,7 +364,7 @@ "cached": false }, { - "id": "9df97785-91cd-482f-99f1-c910275bb630", + "id": "ee1c2e36-41c2-475e-8850-c9051720061a", "name": "temperature", "displayName": "Temperature", "type": "double", @@ -338,7 +373,7 @@ "cached": false }, { - "id": "f460d54d-8935-4f79-b79a-405de8f2b3ec", + "id": "1b281c9e-4dc1-4dd2-b152-825015fb73e3", "name": "fanSpeed", "displayName": "Fan speed", "type": "double", diff --git a/everest/jsonrpc/everestconnection.cpp b/everest/jsonrpc/everestconnection.cpp new file mode 100644 index 00000000..9d4bd633 --- /dev/null +++ b/everest/jsonrpc/everestconnection.cpp @@ -0,0 +1,174 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "everestconnection.h" +#include "extern-plugininfo.h" +#include "jsonrpc/everestjsonrpcclient.h" + +EverestConnection::EverestConnection(quint16 port, QObject *parent) + : QObject{parent}, + m_port{port} +{ + m_client = new EverestJsonRpcClient(this); + + connect(m_client, &EverestJsonRpcClient::availableChanged, this, &EverestConnection::availableChanged); + connect(m_client, &EverestJsonRpcClient::availableChanged, this, [this](bool available){ + if (available) { + qCDebug(dcEverest()) << "The JsonRpc client is now available" << this; + m_reconnectTimer.stop(); + } else { + qCDebug(dcEverest()) << "The JsonRpc client is not available any more" << this; + if (m_monitor->reachable()) { + // Start the reconnect timer + qCDebug(dcEverest()) << "Starting reconnect timer for JsonRpc client to" << m_client->serverUrl().toString(); + m_reconnectTimer.start(); + } + } + }); + + // Reconnect timer is only required for IP based connections, otherwise we have the NetworkDeviceMonitor + m_reconnectTimer.setInterval(5000); + m_reconnectTimer.setSingleShot(false); + + connect(&m_reconnectTimer, &QTimer::timeout, this, [this](){ + if (m_client->available()) + return; + + if (!m_running) { + qCDebug(dcEverest()) << "The everest client is not running. Ignoring event..."; + return; + } + + m_client->connectToServer(buildUrl()); + }); +} + +bool EverestConnection::available() const +{ + return m_client->available(); +} + +EverestJsonRpcClient *EverestConnection::client() const +{ + return m_client; +} + +Things EverestConnection::things() const +{ + return Things(); +} + +void EverestConnection::addThing(Thing *thing) +{ + Q_UNUSED(thing) +} + +void EverestConnection::removeThing(Thing *thing) +{ + Q_UNUSED(thing) +} + +NetworkDeviceMonitor *EverestConnection::monitor() const +{ + return m_monitor; +} + +void EverestConnection::setMonitor(NetworkDeviceMonitor *monitor) +{ + if (!monitor && m_monitor) + disconnect(m_monitor, &NetworkDeviceMonitor::reachableChanged, this, &EverestConnection::onMonitorReachableChanged); + + m_monitor = monitor; + if (m_monitor) { + connect(m_monitor, &NetworkDeviceMonitor::reachableChanged, this, &EverestConnection::onMonitorReachableChanged); + } +} + +void EverestConnection::start() +{ + qCDebug(dcEverest()) << "Starting" << this; + m_running = true; + + if (m_monitor) { + if (m_monitor->reachable()) { + QUrl url = buildUrl(); + qCDebug(dcEverest()) << "Connecting JsonRpc client to" << url.toString(); + if (m_client->available()) + m_client->disconnectFromServer(); + + m_client->connectToServer(url); + } + } else { + qCDebug(dcEverest()) << "Connecting MQTT client to" << m_monitor->networkDeviceInfo().address().toString(); + m_client->connectToServer(buildUrl()); + + // Note: on connected this will be stopped, otherwise we want the timer running + m_reconnectTimer.start(); + } +} + +void EverestConnection::stop() +{ + qCDebug(dcEverest()) << "Stopping" << this; + m_running = false; + m_reconnectTimer.stop(); + m_client->disconnectFromServer(); +} + +void EverestConnection::onMonitorReachableChanged(bool reachable) +{ + qCDebug(dcEverest()) << "Network monitor for" << m_monitor << (reachable ? " is now reachable" : "is not reachable any more"); + + if (!m_running) { + qCDebug(dcEverest()) << "The everest client is not running. Ignoring event..."; + return; + } + + if (reachable) { + // The monitor is reachable which means we have a verfied IP, (re)connecting the JsonRpc client + QUrl url = buildUrl(); + qCDebug(dcEverest()) << "Connecting JsonRpc client to" << url.toString(); + if (m_client->available()) + m_client->disconnectFromServer(); + + m_client->connectToServer(url); + } else { + m_reconnectTimer.stop(); + } +} + +QUrl EverestConnection::buildUrl() const +{ + QUrl url; + url.setScheme("ws"); + url.setHost(m_monitor->networkDeviceInfo().address().toString()); + url.setPort(m_port); + return url; +} diff --git a/everest/jsonrpc/everestconnection.h b/everest/jsonrpc/everestconnection.h new file mode 100644 index 00000000..eb435217 --- /dev/null +++ b/everest/jsonrpc/everestconnection.h @@ -0,0 +1,82 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 EVERESTCONNECTION_H +#define EVERESTCONNECTION_H + +#include +#include + +#include +#include +#include + +class EverestJsonRpcClient; + +class EverestConnection : public QObject +{ + Q_OBJECT +public: + explicit EverestConnection(quint16 port, QObject *parent = nullptr); + + bool available() const; + + EverestJsonRpcClient *client() const; + + Things things() const; + + void addThing(Thing *thing); + void removeThing(Thing *thing); + + NetworkDeviceMonitor *monitor() const; + void setMonitor(NetworkDeviceMonitor *monitor); + +public slots: + void start(); + void stop(); + +signals: + void availableChanged(bool available); + +private slots: + void onMonitorReachableChanged(bool reachable); + +private: + NetworkDeviceMonitor *m_monitor = nullptr; + QTimer m_reconnectTimer; + quint16 m_port = 8080; + + EverestJsonRpcClient *m_client = nullptr; + bool m_running = false; + + QUrl buildUrl() const; +}; + +#endif // EVERESTCONNECTION_H diff --git a/everest/jsonrpc/everestjsonrpcclient.cpp b/everest/jsonrpc/everestjsonrpcclient.cpp index 5191208a..000a1129 100644 --- a/everest/jsonrpc/everestjsonrpcclient.cpp +++ b/everest/jsonrpc/everestjsonrpcclient.cpp @@ -31,6 +31,7 @@ #include "everestjsonrpcclient.h" #include "extern-plugininfo.h" +#include #include #include @@ -70,6 +71,14 @@ EverestJsonRpcClient::EverestJsonRpcClient(QObject *parent) //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(); + m_everestVersion = result.value("everest_version").toString(); + m_authenticationRequired = result.value("authentication_required").toBool(); + + QVariantMap chargerInfoMap = result.value("charger_info").toMap(); + m_chargerInfo.vendor = chargerInfoMap.value("vendor").toString(); + m_chargerInfo.model = chargerInfoMap.value("model").toString(); + m_chargerInfo.serialNumber = chargerInfoMap.value("serial").toString(); + m_chargerInfo.firmwareVersion = chargerInfoMap.value("firmware_version").toString(); EverestJsonRpcReply *reply = chargePointGetEVSEInfos(); connect(reply, &EverestJsonRpcReply::finished, reply, &EverestJsonRpcReply::deleteLater); @@ -93,10 +102,12 @@ EverestJsonRpcClient::EverestJsonRpcClient(QObject *parent) return; } - // TODO: init infos - - // Init data, we are done and connected. + m_evseInfos.clear(); + foreach (const QVariant &evseInfoVariant, result.value("infos").toList()) { + m_evseInfos.append(parseEvseInfo(evseInfoVariant.toMap())); + } + // We are done with the init and the client is now available if (!m_available) { m_available = true; emit availableChanged(m_available); @@ -130,6 +141,26 @@ bool EverestJsonRpcClient::available() const return m_available; } +QString EverestJsonRpcClient::apiVersion() const +{ + return m_apiVersion; +} + +QString EverestJsonRpcClient::everestVersion() const +{ + return m_everestVersion; +} + +QList EverestJsonRpcClient::evseInfos() const +{ + return m_evseInfos; +} + +EverestJsonRpcClient::ChargerInfo EverestJsonRpcClient::chargerInfo() const +{ + return m_chargerInfo; +} + EverestJsonRpcReply *EverestJsonRpcClient::apiHello() { EverestJsonRpcReply *reply = new EverestJsonRpcReply(m_commandId, "API.Hello", QVariantMap(), this); @@ -232,6 +263,34 @@ void EverestJsonRpcClient::processDataPacket(const QByteArray &data) return; } else { // Data without reply, check if this is a notification - + qCDebug(dcEverest()) << "Received data without reply" << qUtf8Printable(data); } } + +EverestJsonRpcClient::EVSEInfo EverestJsonRpcClient::parseEvseInfo(const QVariantMap &evseInfoMap) +{ + EVSEInfo evseInfo; + evseInfo.index = evseInfoMap.value("index").toInt(); + evseInfo.id = evseInfoMap.value("id").toString(); + evseInfo.bidirectionalCharging = evseInfoMap.value("bidi_charging").toBool(); + foreach (const QVariant &connectorInfoVariant, evseInfoMap.value("available_connectors").toList()) { + evseInfo.availableConnectors.append(parseConnectorInfo(connectorInfoVariant.toMap())); + } + return evseInfo; +} + +EverestJsonRpcClient::ConnectorInfo EverestJsonRpcClient::parseConnectorInfo(const QVariantMap &connectorInfoMap) +{ + ConnectorInfo connectorInfo; + connectorInfo.connectorId = connectorInfoMap.value("id").toInt(); + connectorInfo.type = parseConnectorType(connectorInfoMap.value("type").toString()); + connectorInfo.description = connectorInfoMap.value("description").toString(); + return connectorInfo; +} + +EverestJsonRpcClient::ConnectorType EverestJsonRpcClient::parseConnectorType(const QString &connectorTypeString) +{ + QMetaEnum metaEnum = QMetaEnum::fromType(); + return static_cast(metaEnum.keyToValue(QString("ConnectorType").append(connectorTypeString).toUtf8())); +} + diff --git a/everest/jsonrpc/everestjsonrpcclient.h b/everest/jsonrpc/everestjsonrpcclient.h index 46fb40f5..9e84e425 100644 --- a/everest/jsonrpc/everestjsonrpcclient.h +++ b/everest/jsonrpc/everestjsonrpcclient.h @@ -33,6 +33,10 @@ #include +#include +#include +#include + #include "everestjsonrpcreply.h" #include "everestjsonrpcinterface.h" @@ -40,14 +44,69 @@ class EverestJsonRpcClient : public QObject { Q_OBJECT public: + // API Enums + + enum ConnectorType { + ConnectorTypecCCS1, + ConnectorTypecCCS2, + ConnectorTypecG105, + ConnectorTypecTesla, + ConnectorTypecType1, + ConnectorTypecType2, + ConnectorTypes309_1P_16A, + ConnectorTypes309_1P_32A, + ConnectorTypes309_3P_16A, + ConnectorTypes309_3P_32A, + ConnectorTypesBS1361, + ConnectorTypesCEE_7_7, + ConnectorTypesType2, + ConnectorTypesType3, + ConnectorTypeOther1PhMax16A, + ConnectorTypeOther1PhOver16A, + ConnectorTypeOther3Ph, + ConnectorTypePan, + ConnectorTypewInductive, + ConnectorTypewResonant, + ConnectorTypeUndetermined + }; + Q_ENUM(ConnectorType) + + // API Objects + + typedef struct ChargerInfo { + QString vendor; + QString model; + QString serialNumber; + QString firmwareVersion; + } ChargerInfo; + + typedef struct ConnectorInfo { + int connectorId = -1; + ConnectorType type = ConnectorTypeUndetermined; + QString description; // optional + } ConnectorInfo; + + typedef struct EVSEInfo { + int index = -1; + QString id; + QString description; // optional + bool bidirectionalCharging = false; + QList availableConnectors; + } EVSEInfo; + + explicit EverestJsonRpcClient(QObject *parent = nullptr); QUrl serverUrl(); - void setSeverUrl(const QUrl &serverUrl); bool available() const; + // Once available, following properties are set QString apiVersion() const; + QString everestVersion() const; + ChargerInfo chargerInfo() const; + + QList evseInfos() const; // API calls EverestJsonRpcReply *apiHello(); @@ -75,8 +134,17 @@ private: EverestJsonRpcInterface *m_interface = nullptr; QHash m_replies; + // Init infos QString m_apiVersion; + QString m_everestVersion; + ChargerInfo m_chargerInfo; + bool m_authenticationRequired = false; + QList m_evseInfos; + // API parser methods + EVSEInfo parseEvseInfo(const QVariantMap &evseInfoMap); + ConnectorInfo parseConnectorInfo(const QVariantMap &connectorInfoMap); + ConnectorType parseConnectorType(const QString &connectorTypeString); }; #endif // EVERESTJSONRPCCLIENT_H