EVerest: Add HLC DC charger and BiDi charging support

This commit is contained in:
Simon Stürz 2026-09-08 12:12:33 +02:00
parent 262597d106
commit 6519dbab17
7 changed files with 379 additions and 14 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ doc/html
.DS_Store
*/modules/
*__pycache__*
.build-env

View File

@ -30,15 +30,209 @@
#include "jsonrpc/everestjsonrpcdiscovery.h"
#include <QFileInfo>
#include <QtMath>
#include <nymeasettings.h>
#include <network/networkdevicediscovery.h>
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)

View File

@ -30,6 +30,7 @@
#include "mqtt/everestmqttclient.h"
#include "jsonrpc/everestconnection.h"
#include "jsonrpc/everestjsonrpcclient.h"
#include <mqttclient.h>
@ -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;

View File

@ -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 @@
]
}

View File

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

View File

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

View File

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