From 6519dbab17946b71da7bd2165963c4c6c5e40fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 8 Sep 2026 12:12:33 +0200 Subject: [PATCH] EVerest: Add HLC DC charger and BiDi charging support --- .gitignore | 1 + everest/integrationplugineverest.cpp | 217 +++++++++++++++++++++++ everest/integrationplugineverest.h | 8 + everest/integrationplugineverest.json | 115 +++++++++++- everest/jsonrpc/everestevse.cpp | 15 ++ everest/jsonrpc/everestevse.h | 4 + everest/jsonrpc/everestjsonrpcclient.cpp | 33 ++-- 7 files changed, 379 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 440b85e9..f466bcb3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ doc/html .DS_Store */modules/ *__pycache__* +.build-env diff --git a/everest/integrationplugineverest.cpp b/everest/integrationplugineverest.cpp index 9805deb4..e6d886ed 100644 --- a/everest/integrationplugineverest.cpp +++ b/everest/integrationplugineverest.cpp @@ -30,15 +30,209 @@ #include "jsonrpc/everestjsonrpcdiscovery.h" #include +#include #include #include +namespace { + +static const char *connectedVehiclePropertyName = "connectedVehicleThingId"; +static const char *pendingHlcVehicleCreationPropertyName = "pendingHlcVehicleCreation"; +static const char *latestHlcBatteryLevelPropertyName = "latestHlcBatteryLevel"; +static const char *latestHlcCapacityPropertyName = "latestHlcCapacity"; +static const char *latestHlcPowerPropertyName = "latestHlcPower"; + +bool isHlcChargeProtocol(EverestJsonRpcClient::ChargeProtocol protocol) +{ + return protocol == EverestJsonRpcClient::ChargeProtocolDIN70121 + || protocol == EverestJsonRpcClient::ChargeProtocolISO15118 + || protocol == EverestJsonRpcClient::ChargeProtocolISO15118_20; +} + +bool hasVehicleDisplayData(const EverestJsonRpcClient::DisplayParameters &displayParameters) +{ + return displayParameters.presentSoc >= 0 + || displayParameters.startSoc >= 0 + || displayParameters.minimumSoc >= 0 + || displayParameters.targetSoc >= 0 + || displayParameters.maximumSoc >= 0 + || displayParameters.batteryEnergyCapacity >= 0; +} + +int displaySoc(const EverestJsonRpcClient::DisplayParameters &displayParameters) +{ + if (displayParameters.presentSoc >= 0) + return displayParameters.presentSoc; + if (displayParameters.startSoc >= 0) + return displayParameters.startSoc; + if (displayParameters.targetSoc >= 0) + return displayParameters.targetSoc; + if (displayParameters.maximumSoc >= 0) + return displayParameters.maximumSoc; + if (displayParameters.minimumSoc >= 0) + return displayParameters.minimumSoc; + + return -1; +} + +double displayCapacityKWh(double batteryEnergyCapacity) +{ + if (batteryEnergyCapacity <= 0) + return -1; + + return batteryEnergyCapacity > 1000 ? batteryEnergyCapacity / 1000.0 : batteryEnergyCapacity; +} + +QString chargingStateFromPower(double power) +{ + if (power > 0.01) + return "charging"; + if (power < -0.01) + return "discharging"; + + return "idle"; +} + +} + IntegrationPluginEverest::IntegrationPluginEverest() { } +Thing *IntegrationPluginEverest::findHlcVehicleForCharger(Thing *charger) const +{ + if (!charger) + return nullptr; + + foreach (Thing *childThing, myThings().filterByParentId(charger->id())) { + if (childThing->thingClassId() == everestHlcVehicleThingClassId) + return childThing; + } + + return nullptr; +} + +void IntegrationPluginEverest::createHlcVehicleForCharger(Thing *charger) +{ + if (!charger || findHlcVehicleForCharger(charger) || charger->property(pendingHlcVehicleCreationPropertyName).toBool()) + return; + + ThingDescriptor descriptor(everestHlcVehicleThingClassId, QString("EVerest HLC vehicle %1").arg(charger->name()), QString(), charger->id()); + descriptor.setParams(ParamList() << Param(everestHlcVehicleThingChargerIndexParamTypeId, charger->paramValue("index").toUInt())); + + charger->setProperty(pendingHlcVehicleCreationPropertyName, true); + emit autoThingsAppeared({descriptor}); +} + +void IntegrationPluginEverest::assignHlcVehicleToCharger(Thing *charger, Thing *vehicle) +{ + if (!charger || !vehicle) + return; + + charger->setProperty(connectedVehiclePropertyName, vehicle->id().toString()); + charger->setProperty(pendingHlcVehicleCreationPropertyName, false); + charger->setStateValue("connectedVehicleThingId", vehicle->id().toString()); + charger->setStateValue("vehicleIdentified", true); + + vehicle->setStateValue("pluggedIn", true); + vehicle->setStateValue("connectedChargerThingId", charger->id().toString()); + vehicle->setStateValue("hlcSessionActive", charger->stateValue("hlcSessionActive").toBool()); + vehicle->setStateValue("chargingInterfaces", "dc"); + vehicle->setStateValue("dcMaxChargingPower", charger->stateValue("maxChargingPower")); + + if (charger->property(latestHlcBatteryLevelPropertyName).isValid()) { + const int batteryLevel = qBound(0, charger->property(latestHlcBatteryLevelPropertyName).toInt(), 100); + vehicle->setStateValue("batteryLevel", batteryLevel); + vehicle->setStateValue("batteryCritical", batteryLevel < 10); + } + if (charger->property(latestHlcCapacityPropertyName).isValid()) + vehicle->setStateValue("capacity", charger->property(latestHlcCapacityPropertyName)); + if (charger->property(latestHlcPowerPropertyName).isValid()) + vehicle->setStateValue("chargingState", chargingStateFromPower(charger->property(latestHlcPowerPropertyName).toDouble())); +} + +void IntegrationPluginEverest::unassignHlcVehicleFromCharger(Thing *charger) +{ + if (!charger) + return; + + Thing *vehicle = findHlcVehicleForCharger(charger); + + charger->setProperty(connectedVehiclePropertyName, QString()); + charger->setProperty(pendingHlcVehicleCreationPropertyName, false); + charger->setStateValue("connectedVehicleThingId", QString()); + charger->setStateValue("vehicleIdentified", false); + charger->setStateValue("hlcSessionActive", false); + + if (!vehicle) + return; + + vehicle->setStateValue("pluggedIn", false); + vehicle->setStateValue("connectedChargerThingId", QString()); + vehicle->setStateValue("hlcSessionActive", false); + vehicle->setStateValue("chargingState", "idle"); +} + +void IntegrationPluginEverest::updateHlcVehicleFromEvseStatus(Thing *charger, const EverestJsonRpcClient::EVSEStatus &status) +{ + if (!charger || charger->thingClassId() != everestChargerDcThingClassId) + return; + + const bool pluggedIn = status.evseState != EverestJsonRpcClient::EvseStateUnplugged; + const bool hlcSessionActive = pluggedIn && isHlcChargeProtocol(status.chargeProtocol); + const bool vehicleIdentified = hlcSessionActive || hasVehicleDisplayData(status.displayParameters); + + charger->setStateValue("hlcSessionActive", hlcSessionActive); + charger->setStateValue("vehicleIdentified", vehicleIdentified); + + const int soc = displaySoc(status.displayParameters); + if (soc >= 0) + charger->setProperty(latestHlcBatteryLevelPropertyName, qBound(0, soc, 100)); + + const double capacity = displayCapacityKWh(status.displayParameters.batteryEnergyCapacity); + if (capacity > 0) + charger->setProperty(latestHlcCapacityPropertyName, capacity); + + if (!pluggedIn) { + unassignHlcVehicleFromCharger(charger); + return; + } + + Thing *vehicle = findHlcVehicleForCharger(charger); + if (!vehicle) { + createHlcVehicleForCharger(charger); + return; + } + + assignHlcVehicleToCharger(charger, vehicle); + + if (soc >= 0) { + const int batteryLevel = charger->property(latestHlcBatteryLevelPropertyName).toInt(); + vehicle->setStateValue("batteryLevel", batteryLevel); + vehicle->setStateValue("batteryCritical", batteryLevel < 10); + } + + if (capacity > 0) + vehicle->setStateValue("capacity", capacity); +} + +void IntegrationPluginEverest::updateHlcVehiclePowerFlow(Thing *charger, double power) +{ + if (!charger || charger->thingClassId() != everestChargerDcThingClassId) + return; + + charger->setProperty(latestHlcPowerPropertyName, power); + + Thing *vehicle = findHlcVehicleForCharger(charger); + if (!vehicle) + return; + + const QString chargingState = chargingStateFromPower(power); + vehicle->setStateValue("chargingState", chargingState); +} + void IntegrationPluginEverest::init() { QFileInfo enableMqttFileInfo(NymeaSettings::settingsPath() + "/everest-mqtt"); @@ -474,6 +668,13 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info) connection->start(); return; + } else if (thing->thingClassId() == everestHlcVehicleThingClassId) { + Thing *chargerThing = myThings().findById(thing->parentId()); + if (chargerThing && chargerThing->thingClassId() == everestChargerDcThingClassId && chargerThing->stateValue("pluggedIn").toBool()) + assignHlcVehicleToCharger(chargerThing, thing); + + info->finish(Thing::ThingErrorNoError); + return; } else if (thing->thingClassId() == everestChargerAcThingClassId || thing->thingClassId() == everestChargerDcThingClassId) { Thing *parentThing = myThings().findById(thing->parentId()); @@ -486,6 +687,10 @@ void IntegrationPluginEverest::setupThing(ThingSetupInfo *info) info->finish(Thing::ThingErrorNoError); connection->addThing(thing); + if (EverestEvse *evse = connection->getEvse(thing)) { + connect(evse, &EverestEvse::dcHlcStatusChanged, this, &IntegrationPluginEverest::updateHlcVehicleFromEvseStatus); + connect(evse, &EverestEvse::dcPowerFlowChanged, this, &IntegrationPluginEverest::updateHlcVehiclePowerFlow); + } return; } } @@ -731,7 +936,19 @@ void IntegrationPluginEverest::thingRemoved(Thing *thing) } } else if (thing->thingClassId() == everestConnectionThingClassId) { m_everstConnections.take(thing)->deleteLater(); + } else if (thing->thingClassId() == everestHlcVehicleThingClassId) { + Thing *chargerThing = myThings().findById(thing->parentId()); + if (chargerThing && chargerThing->thingClassId() == everestChargerDcThingClassId) { + chargerThing->setProperty(connectedVehiclePropertyName, QString()); + chargerThing->setProperty(pendingHlcVehicleCreationPropertyName, false); + chargerThing->setStateValue("connectedVehicleThingId", QString()); + } } else if (thing->thingClassId() == everestChargerAcThingClassId || thing->thingClassId() == everestChargerDcThingClassId) { + foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { + if (childThing->thingClassId() == everestHlcVehicleThingClassId) + emit autoThingDisappeared(childThing->id()); + } + Thing *parentThing = myThings().findById(thing->parentId()); EverestConnection *connection = m_everstConnections.value(parentThing); if (!connection) diff --git a/everest/integrationplugineverest.h b/everest/integrationplugineverest.h index d45a7d5b..6dc3742e 100644 --- a/everest/integrationplugineverest.h +++ b/everest/integrationplugineverest.h @@ -30,6 +30,7 @@ #include "mqtt/everestmqttclient.h" #include "jsonrpc/everestconnection.h" +#include "jsonrpc/everestjsonrpcclient.h" #include @@ -53,6 +54,13 @@ public: void executeAction(ThingActionInfo *info) override; private: + Thing *findHlcVehicleForCharger(Thing *charger) const; + void createHlcVehicleForCharger(Thing *charger); + void assignHlcVehicleToCharger(Thing *charger, Thing *vehicle); + void unassignHlcVehicleFromCharger(Thing *charger); + void updateHlcVehicleFromEvseStatus(Thing *charger, const EverestJsonRpcClient::EVSEStatus &status); + void updateHlcVehiclePowerFlow(Thing *charger, double power); + int m_autodetectCounter = 0; int m_autodetectCounterLimit = 4; bool m_useMqtt = false; diff --git a/everest/integrationplugineverest.json b/everest/integrationplugineverest.json index 913b921d..e831a424 100644 --- a/everest/integrationplugineverest.json +++ b/everest/integrationplugineverest.json @@ -486,7 +486,7 @@ "displayName": "EVerest charger DC", "id": "b31e14dd-43e0-4278-b517-d50afd6806ec", "createMethods": ["auto"], - "interfaces": [ "evchargerdc", "connectable"], + "interfaces": [ "evchargerdc", "hlccharger", "connectable"], "paramTypes": [ { "id": "4a3e03ed-44ea-4a32-9f7f-b4e6b1647921", @@ -718,6 +718,27 @@ "unit": "KiloWattHour", "defaultValue": 0 }, + { + "id": "8fc87cd3-2989-421d-9d6c-d1364bf4ca39", + "name": "hlcSessionActive", + "displayName": "HLC session active", + "type": "bool", + "defaultValue": false + }, + { + "id": "d9713e6b-563c-466e-9689-bf25bfcf490c", + "name": "vehicleIdentified", + "displayName": "Vehicle identified", + "type": "bool", + "defaultValue": false + }, + { + "id": "2a4fb1ce-4112-452b-b217-d70a78410aa3", + "name": "connectedVehicleThingId", + "displayName": "Connected vehicle thing ID", + "type": "QString", + "defaultValue": "" + }, { "id": "d526c751-51e0-4c69-a88e-ae33db38b182", "name": "temperature", @@ -738,6 +759,97 @@ } ], "actionTypes": [ ] + }, + { + "name": "everestHlcVehicle", + "displayName": "EVerest HLC vehicle", + "id": "d9423da9-e18a-41f3-8775-f376b727ffe3", + "setupMethod": "JustAdd", + "createMethods": ["auto"], + "interfaces": [ "electricvehiclehlc" ], + "paramTypes": [ + { + "id": "29adfdc5-d5ee-42c8-a813-ebf184ab9357", + "name": "chargerIndex", + "displayName": "Charger index", + "type": "uint", + "defaultValue": 0 + } + ], + "settingsTypes": [], + "stateTypes": [ + { + "id": "c88bba3c-843f-4c46-a698-b9f46047214b", + "name": "batteryLevel", + "displayName": "Battery level", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "dd1c1a0d-4c84-4b44-a6a8-8c1ba1811486", + "name": "batteryCritical", + "displayName": "Battery level critical", + "type": "bool", + "defaultValue": false + }, + { + "id": "4cbbb4f8-7f17-49bc-b848-5f8e75e52630", + "name": "capacity", + "displayName": "Battery capacity", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "39ce5deb-d253-43c2-8ade-cfd04b605d60", + "name": "dcMaxChargingPower", + "displayName": "Maximum charging power", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "a8a68754-4092-4997-bebf-00e3a350b685", + "name": "chargingState", + "displayName": "Charging state", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "70fe1623-5db7-4cdf-b0fc-0ddf8f319f73", + "name": "chargingInterfaces", + "displayName": "Charging interfaces", + "type": "QString", + "possibleValues": ["ac", "dc", "acdc"], + "defaultValue": "dc" + }, + { + "id": "0b830a50-059a-45f6-b65a-3acc22977509", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false + }, + { + "id": "8bd9eeb0-7f50-457f-a541-0fe211e4b1f1", + "name": "connectedChargerThingId", + "displayName": "Connected charger thing ID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "98245a35-6ff6-4cde-9ace-724056873d66", + "name": "hlcSessionActive", + "displayName": "HLC session active", + "type": "bool", + "defaultValue": false + } + ], + "actionTypes": [ ] } ] @@ -745,4 +857,3 @@ ] } - diff --git a/everest/jsonrpc/everestevse.cpp b/everest/jsonrpc/everestevse.cpp index 24e87162..64c5c2da 100644 --- a/everest/jsonrpc/everestevse.cpp +++ b/everest/jsonrpc/everestevse.cpp @@ -243,6 +243,20 @@ void EverestEvse::processEvseStatus() m_thing->setStateValue(everestChargerDcChargingStateStateTypeId, "idle"); } + m_thing->setStateValue(everestChargerDcHlcSessionActiveStateTypeId, m_evseStatus.chargeProtocol == EverestJsonRpcClient::ChargeProtocolDIN70121 + || m_evseStatus.chargeProtocol == EverestJsonRpcClient::ChargeProtocolISO15118 + || m_evseStatus.chargeProtocol == EverestJsonRpcClient::ChargeProtocolISO15118_20); + m_thing->setStateValue(everestChargerDcVehicleIdentifiedStateTypeId, m_evseStatus.displayParameters.presentSoc >= 0 + || m_evseStatus.displayParameters.startSoc >= 0 + || m_evseStatus.displayParameters.minimumSoc >= 0 + || m_evseStatus.displayParameters.targetSoc >= 0 + || m_evseStatus.displayParameters.maximumSoc >= 0 + || m_evseStatus.displayParameters.batteryEnergyCapacity >= 0); + m_thing->setStateValue(everestChargerDcChargingCapabilitiesStateTypeId, + m_evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_BPT) + || m_evseInfo.supportedEnergyTransferModes.contains(EverestJsonRpcClient::EnergyTransferModeDC_ACDP_BPT) + ? "bidirectional" : "charging"); + emit dcHlcStatusChanged(m_thing, m_evseStatus); } } @@ -300,5 +314,6 @@ void EverestEvse::processMeterData() m_thing->setStateValue(everestChargerDcTotalEnergyConsumedStateTypeId, m_meterData.energyImportedTotal / 1000.0); m_thing->setStateValue(everestChargerDcTotalEnergyProducedStateTypeId, m_meterData.energyExportedTotal/ 1000.0); + emit dcPowerFlowChanged(m_thing, m_meterData.powerTotal); } } diff --git a/everest/jsonrpc/everestevse.h b/everest/jsonrpc/everestevse.h index 6899803b..12b65f51 100644 --- a/everest/jsonrpc/everestevse.h +++ b/everest/jsonrpc/everestevse.h @@ -42,6 +42,10 @@ public: EverestJsonRpcReply *setACChargingPhaseCount(int phaseCount); EverestJsonRpcReply *setDCChargingPower(double chargingPower); +signals: + void dcHlcStatusChanged(Thing *charger, const EverestJsonRpcClient::EVSEStatus &status); + void dcPowerFlowChanged(Thing *charger, double power); + private: EverestJsonRpcClient *m_client = nullptr; Thing *m_thing = nullptr; diff --git a/everest/jsonrpc/everestjsonrpcclient.cpp b/everest/jsonrpc/everestjsonrpcclient.cpp index e205ad85..46ecdc34 100644 --- a/everest/jsonrpc/everestjsonrpcclient.cpp +++ b/everest/jsonrpc/everestjsonrpcclient.cpp @@ -434,29 +434,38 @@ EverestJsonRpcClient::DisplayParameters EverestJsonRpcClient::parseDisplayParame // Treat all properties as optional until the object is specified upstream. DisplayParameters displayParameters; - if (displayParametersMap.contains("start_soc")) { + if (displayParametersMap.contains("start_soc")) displayParameters.startSoc = displayParametersMap.value("start_soc").toInt() ; - } else if (displayParametersMap.contains("present_soc")) { + + if (displayParametersMap.contains("present_soc")) displayParameters.presentSoc = displayParametersMap.value("present_soc").toInt(); - } else if (displayParametersMap.contains("minimum_soc")) { + + if (displayParametersMap.contains("minimum_soc")) displayParameters.minimumSoc = displayParametersMap.value("minimum_soc").toInt(); - } else if (displayParametersMap.contains("target_soc")) { + + if (displayParametersMap.contains("target_soc")) displayParameters.targetSoc = displayParametersMap.value("target_soc").toInt(); - } else if (displayParametersMap.contains("maximum_soc")) { + + if (displayParametersMap.contains("maximum_soc")) displayParameters.maximumSoc = displayParametersMap.value("maximum_soc").toInt(); - } else if (displayParametersMap.contains("remaining_time_to_minimum_soc")) { + + if (displayParametersMap.contains("remaining_time_to_minimum_soc")) displayParameters.remainingTimeToMinimumSoc = displayParametersMap.value("remaining_time_to_minimum_soc").toInt(); - } else if (displayParametersMap.contains("remaining_time_to_target_soc")) { + + if (displayParametersMap.contains("remaining_time_to_target_soc")) displayParameters.remainingTimeToTargetSoc = displayParametersMap.value("remaining_time_to_target_soc").toInt(); - } else if (displayParametersMap.contains("remaining_time_to_maximum_soc")) { + + if (displayParametersMap.contains("remaining_time_to_maximum_soc")) displayParameters.remainingTimeToMaximumSoc = displayParametersMap.value("remaining_time_to_maximum_soc").toInt(); - } else if (displayParametersMap.contains("charging_complete")) { + + if (displayParametersMap.contains("charging_complete")) displayParameters.chargingComplete = displayParametersMap.value("charging_complete").toBool(); - } else if (displayParametersMap.contains("battery_energy_capacity")) { + + if (displayParametersMap.contains("battery_energy_capacity")) displayParameters.batteryEnergyCapacity = displayParametersMap.value("battery_energy_capacity").toDouble(); - } else if (displayParametersMap.contains("inlet_hot")) { + + if (displayParametersMap.contains("inlet_hot")) displayParameters.inletHot = displayParametersMap.value("inlet_hot").toBool(); - } return displayParameters; }