Files
Eric F d398a6ced2 Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
2026-06-08 00:38:27 -04:00

2708 lines
124 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "EvseManager.hpp"
#include <algorithm>
#include <chrono>
#include <fmt/color.h>
#include <fmt/core.h>
#include "IECStateMachine.hpp"
#include "SessionLog.hpp"
#include "Timeout.hpp"
#include "scoped_lock_timeout.hpp"
#include "utils.hpp"
using namespace std::literals::chrono_literals;
namespace module {
namespace {
const std::vector<std::unique_ptr<powermeterIntf>> EMPTY_POWERMETER_VECTOR;
types::power_supply_DC::Capabilities get_sane_default_power_supply_capabilities() {
// Init power supply capabilities with safe defaults
types::power_supply_DC::Capabilities psu_caps;
psu_caps.bidirectional = false;
psu_caps.peak_current_ripple_A = 0.5;
psu_caps.current_regulation_tolerance_A = 0.5;
psu_caps.max_export_current_A = 0;
psu_caps.max_export_power_W = 0;
psu_caps.max_export_voltage_V = 60;
psu_caps.min_export_voltage_V = 0;
psu_caps.min_export_current_A = 0;
return psu_caps;
}
void trim_colons_from_string(std::string& text) {
text.erase(remove(text.begin(), text.end(), ':'), text.end());
}
types::authorization::ProvidedIdToken create_autocharge_token(std::string token, int connector_id) {
types::authorization::ProvidedIdToken autocharge_token;
autocharge_token.authorization_type = types::authorization::AuthorizationType::Autocharge;
trim_colons_from_string(token);
autocharge_token.id_token = {"VID:" + token, types::authorization::IdTokenType::MacAddress};
autocharge_token.connectors.emplace({connector_id});
return autocharge_token;
}
bool almost_eq(double a, double b) {
constexpr auto eps = 1e-6;
return std::fabs(a - b) <= eps;
}
std::optional<float> min_optional(std::optional<float> const& a, std::optional<float> const& b) {
// if both a and b have values, return the smaller one.
if (a.has_value() and b.has_value()) {
return (b.value() < a.value() ? b.value() : a.value());
}
// if a has a value, return that one.
if (a.has_value()) {
return a;
}
// else return b. It is either the only value or empty.
return b;
}
float min_optional(float a, std::optional<float> b) {
// if both a and b have values, return the smaller one.
if (b.has_value()) {
return (b.value() < a ? b.value() : a);
}
// else return a
return a;
}
types::dc_external_derate::ExternalDerating
get_dc_external_derate(std::optional<float> present_voltage,
types::dc_external_derate::ExternalDerating dc_external_derate) {
if (!present_voltage.has_value()) {
return dc_external_derate;
}
const float voltage = present_voltage.value();
if (voltage <= 0.0f) {
return dc_external_derate;
}
types::dc_external_derate::ExternalDerating d = dc_external_derate;
// Derive power limits from current limits (only if power limit is not set)
if (!d.max_export_power_W.has_value() && d.max_export_current_A.has_value()) {
d.max_export_power_W = d.max_export_current_A.value() * voltage;
}
if (!d.max_import_power_W.has_value() && d.max_import_current_A.has_value()) {
d.max_import_power_W = d.max_import_current_A.value() * voltage;
}
// Derive current limits from power limits (only if current limit is not set)
if (!d.max_export_current_A.has_value() && d.max_export_power_W.has_value()) {
d.max_export_current_A = d.max_export_power_W.value() / voltage;
}
if (!d.max_import_current_A.has_value() && d.max_import_power_W.has_value()) {
d.max_import_current_A = d.max_import_power_W.value() / voltage;
}
return d;
}
std::vector<types::iso15118::EnergyTransferMode>
get_supported_ac_energy_transfers(const Conf& config, const types::evse_board_support::HardwareCapabilities& caps) {
std::vector<types::iso15118::EnergyTransferMode> energy_transfers;
const auto min_phases = std::clamp(caps.min_phase_count_import, 1, 3);
const auto max_phases = std::clamp(caps.max_phase_count_import, min_phases, 3);
for (const auto& [count, mode] : {
std::pair{1, types::iso15118::EnergyTransferMode::AC_single_phase_core},
std::pair{2, types::iso15118::EnergyTransferMode::AC_two_phase},
std::pair{3, types::iso15118::EnergyTransferMode::AC_three_phase_core},
}) {
if (count >= min_phases and count <= max_phases) {
energy_transfers.push_back(mode);
}
}
if (config.supported_iso_ac_bpt and caps.max_current_A_export > 0 and caps.max_phase_count_export >= 1) {
energy_transfers.push_back(types::iso15118::EnergyTransferMode::AC_BPT);
}
return energy_transfers;
}
} // namespace
void EvseManager::init() {
if (!config.connector_type.empty()) {
try {
connector_type = types::evse_manager::string_to_connector_type_enum(config.connector_type);
} catch (const std::out_of_range& e) {
EVLOG_warning << "Unknown/invalid connector type: " << config.connector_type;
}
}
latest_target_current_low_pass_last_update = std::chrono::steady_clock::now();
store = std::unique_ptr<PersistentStore>(new PersistentStore(r_store, info.id));
random_delay_enabled = config.uk_smartcharging_random_delay_enable;
random_delay_max_duration = std::chrono::seconds(config.uk_smartcharging_random_delay_max_duration);
if (random_delay_enabled) {
EVLOG_info << "UK Smart Charging regulations: enabled random delay with a default of "
<< random_delay_max_duration.load().count() << "s.";
}
session_log.setPath(config.session_logging_path);
session_log.setMqtt([this](json const& data) {
std::string hlc_log_topic = "everest_api/" + this->info.id + "/var/hlc_log";
mqtt.publish(hlc_log_topic, data.dump());
});
if (config.session_logging) {
session_log.enable();
}
session_log.xmlOutput(config.session_logging_xml);
// default initialize hardware capabilities with safe values until actual capabilities are received.
// This is important to have a defined behavior of the charger even if the bsp does not provide
// capabilities or is slow in doing so.
{
auto hw_capabilities_handle = hw_capabilities.handle();
hw_capabilities_handle->max_current_A_import = 0;
hw_capabilities_handle->min_current_A_import = 0;
hw_capabilities_handle->max_phase_count_import = 1;
hw_capabilities_handle->min_phase_count_import = 0;
hw_capabilities_handle->max_current_A_export = 0;
hw_capabilities_handle->min_current_A_export = 0;
hw_capabilities_handle->max_phase_count_export = 0;
hw_capabilities_handle->min_phase_count_export = 0;
hw_capabilities_handle->supports_changing_phases_during_charging = false;
hw_capabilities_handle->supports_cp_state_E = false;
hw_capabilities_handle->connector_type = types::evse_board_support::Connector_type::IEC62196Type2Cable;
}
invoke_init(*p_evse);
invoke_init(*p_energy_grid);
invoke_init(*p_token_provider);
invoke_init(*p_random_delay);
// check if a slac module is connected to the optional requirement
slac_enabled = not r_slac.empty();
// if hlc is disabled in config, disable slac even if requirement is connected
if (not(config.ac_hlc_enabled or config.ac_with_soc or config.charge_mode == "DC")) {
slac_enabled = false;
}
// Use SLAC MAC address for Autocharge if configured.
if (config.autocharge_use_slac_instead_of_hlc and slac_enabled and config.enable_autocharge) {
r_slac[0]->subscribe_ev_mac_address([this](const std::string& token) {
p_token_provider->publish_provided_token(create_autocharge_token(token, config.connector_id));
});
}
hlc_enabled = not r_hlc.empty();
if (not slac_enabled)
hlc_enabled = false;
if (config.charge_mode == "DC" and (not hlc_enabled or not slac_enabled or r_powersupply_DC.empty())) {
EVLOG_error << "DC mode requires slac, HLC and powersupply DCDC to be connected";
exit(255);
}
if (config.charge_mode == "DC" and r_imd.empty()) {
EVLOG_warning << "DC mode without isolation monitoring configured, please check your national regulations.";
}
pnc_enabled = config.payment_enable_contract;
central_contract_validation_allowed = config.central_contract_validation_allowed;
contract_certificate_installation_enabled = config.contract_certificate_installation_enabled;
reserved = false;
reservation_id = -1;
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = false;
latest_target_voltage = 0;
latest_target_current = 0;
// apply sane defaults capabilities settings once on boot
powersupply_capabilities = get_sane_default_power_supply_capabilities();
if (hlc_enabled) {
if (config.charge_mode == "DC") {
// subscribe to run time updates for real initial values (and changes e.g. due to de-rating)
r_powersupply_DC[0]->subscribe_capabilities([this](const auto& caps) {
update_powersupply_capabilities(caps);
auto mode = types::iso15118::EnergyTransferMode::DC_extended;
auto bpt_mode = types::iso15118::EnergyTransferMode::DC_BPT;
if (connector_type.has_value() and
connector_type.value() == types::evse_manager::ConnectorTypeEnum::cMCS) {
mode = types::iso15118::EnergyTransferMode::MCS;
bpt_mode = types::iso15118::EnergyTransferMode::MCS_BPT;
}
std::vector<types::iso15118::EnergyTransferMode> energy_transfers{mode};
if (caps.bidirectional) {
energy_transfers.push_back(bpt_mode);
}
const bool was_updated = update_supported_energy_transfers(energy_transfers);
if (was_updated) {
this->publish_and_update_supported_energy_transfers();
}
});
}
}
r_bsp->subscribe_request_stop_transaction(
[this](types::evse_manager::StopTransactionRequest r) { charger->cancel_transaction(r); });
r_bsp->subscribe_capabilities([this](types::evse_board_support::HardwareCapabilities const& c) {
{
auto hw_caps_handle = hw_capabilities.handle();
hw_caps_handle.wait([this]() { return ready_for_capabilities.load(); });
*hw_caps_handle = c;
}
if (ac_nr_phases_active == 0) {
ac_nr_phases_active = c.max_phase_count_import;
}
if (ac_nr_phases_active > c.max_phase_count_import) {
ac_nr_phases_active = c.min_phase_count_import;
}
signalNrOfPhasesAvailable(ac_nr_phases_active);
const auto get_max_phases = [](int max_phase_count) -> AcPhases {
switch (max_phase_count) {
case 1:
return AcPhases::SinglePhase;
case 2:
return AcPhases::TwoPhases;
case 3:
return AcPhases::ThreePhases;
default:
EVLOG_warning << "Invalid max_phase_count of " << max_phase_count << ". Falling back to single phase";
}
return AcPhases::SinglePhase;
};
bsp->set_max_phases(get_max_phases(c.max_phase_count_import));
charger->set_connector_type(c.connector_type);
p_evse->publish_hw_capabilities(c);
if (config.charge_mode == "AC" and hlc_enabled) {
EVLOG_debug << fmt::format("Max AC hardware capabilities: {}A/{}ph", c.max_current_A_import,
c.max_phase_count_import);
const auto energy_transfers = get_supported_ac_energy_transfers(config, c);
if (update_supported_energy_transfers(energy_transfers)) {
this->publish_and_update_supported_energy_transfers();
}
update_hlc_ac_parameters();
}
});
}
void EvseManager::ready() {
bsp = std::make_unique<IECStateMachine>(r_bsp, config.lock_connector_in_state_b);
if (config.hack_simplified_mode_limit_10A) {
bsp->set_ev_simplified_mode_evse_limit(true);
}
// we provide the powermeter interface to the ErrorHandling only if we need to react to powermeter errors
// otherwise we provide an empty vector of pointers to the powermeter interface
error_handling = std::unique_ptr<ErrorHandling>(
new ErrorHandling(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse, r_imd, r_powersupply_DC,
config.fail_on_powermeter_errors ? r_powermeter_billing() : EMPTY_POWERMETER_VECTOR,
r_over_voltage_monitor, config.inoperative_error_use_vendor_id));
internal_over_voltage_monitor = std::make_unique<OverVoltageMonitor>(
[this](OverVoltageMonitor::FaultType type, const std::string& description) {
if (this->error_handling) {
const auto severity = type == OverVoltageMonitor::FaultType::Emergency
? Everest::error::Severity::High
: Everest::error::Severity::Medium;
this->error_handling->raise_over_voltage_error(severity, description);
}
},
std::chrono::milliseconds(config.internal_over_voltage_duration_ms));
if (not config.lock_connector_in_state_b) {
EVLOG_warning << "Unlock connector in CP state B. This violates IEC61851-1:2019 D.6.5 Table D.9 line 4 and "
"should not be used in public environments!";
}
const auto hw_caps = *hw_capabilities.handle();
charger = std::make_unique<Charger>(bsp, error_handling, r_powermeter_billing(), store, hw_caps.connector_type,
config.evse_id);
// Now incoming hardware capabilities can be processed
ready_for_capabilities.store(true);
hw_capabilities.notify_all();
if (r_connector_lock.size() > 0) {
bsp->signal_lock.connect([this]() { r_connector_lock[0]->call_lock(); });
bsp->signal_unlock.connect([this]() { r_connector_lock[0]->call_unlock(); });
}
if (hlc_enabled) {
// Set up EVSE ID
types::iso15118::EVSEID evseid = {config.evse_id, config.evse_id_din};
// Set up auth options for HLC
std::vector<types::iso15118::PaymentOption> payment_options;
// if pnc is disabled, disable contract installation and central contract validation
bool _contract_certificate_installation_enabled =
pnc_enabled ? contract_certificate_installation_enabled.load() : false;
bool _central_contract_validation_allowed = pnc_enabled ? central_contract_validation_allowed.load() : false;
if (config.payment_enable_eim) {
payment_options.push_back(types::iso15118::PaymentOption::ExternalPayment);
}
if (pnc_enabled) {
payment_options.push_back(types::iso15118::PaymentOption::Contract);
}
if (!config.payment_enable_eim and !pnc_enabled) {
EVLOG_warning << "Both payment options are disabled! ExternalPayment is nevertheless enabled in this case.";
payment_options.push_back(types::iso15118::PaymentOption::ExternalPayment);
}
r_hlc[0]->call_session_setup(payment_options, _contract_certificate_installation_enabled,
_central_contract_validation_allowed);
r_hlc[0]->subscribe_hlc_session_failed([this](types::evse_manager::HlcSessionFailedReasonEnum reason) {
types::evse_manager::HlcSessionFailedEvent ev;
ev.uuid = charger->get_session_id();
ev.reason = reason;
p_evse->publish_hlc_session_failed(ev);
});
r_hlc[0]->subscribe_dlink_error([this] {
session_log.evse(true, "D-LINK_ERROR.req");
// Inform charger
charger->dlink_error();
// Inform SLAC layer, it will leave the logical network
r_slac[0]->call_dlink_error();
});
r_hlc[0]->subscribe_dlink_pause([this] {
// tell charger (it will disable PWM)
session_log.evse(true, "D-LINK_PAUSE.req");
charger->dlink_pause();
r_slac[0]->call_dlink_pause();
});
r_hlc[0]->subscribe_dlink_terminate([this] {
selected_d20_energy_service.reset();
session_log.evse(true, "D-LINK_TERMINATE.req");
charger->dlink_terminate();
r_slac[0]->call_dlink_terminate();
});
r_hlc[0]->subscribe_v2g_setup_finished([this] { charger->set_hlc_charging_active(); });
r_hlc[0]->subscribe_ac_close_contactor([this] {
session_log.car(true, "AC HLC Close contactor");
charger->set_hlc_allow_close_contactor(true);
});
r_hlc[0]->subscribe_ac_open_contactor([this] {
session_log.car(true, "AC HLC Open contactor");
charger->set_hlc_allow_close_contactor(false);
});
// Trigger SLAC restart
charger->signal_slac_start.connect([this] { r_slac[0]->call_enter_bcd(); });
// Trigger SLAC reset
charger->signal_slac_reset.connect([this] { r_slac[0]->call_reset(false); });
// Ask HLC to stop charging session
charger->signal_hlc_stop_charging.connect([this] { r_hlc[0]->call_stop_charging(true); });
charger->signal_hlc_pause_charging.connect([this] { r_hlc[0]->call_pause_charging(true); });
charger->signal_hlc_plug_in_timeout.connect([this] {
r_hlc[0]->call_authorization_response(types::authorization::AuthorizationStatus::Unknown,
types::authorization::CertificateStatus::NoCertificateAvailable);
});
// Charger needs to inform ISO stack about emergency stop
charger->signal_hlc_error.connect([this](types::iso15118::EvseError error) {
if (r_hlc.empty()) {
EVLOG_warning << "HLC module not connected, cannot send error!";
return;
}
r_hlc[0]->call_send_error(error);
});
// Right now only no energy pause after cable check and pre charge is supported
// For DIN it is always stop before cablecheck
charger->signal_hlc_no_energy_available.connect([this] {
if (config.zero_power_ignore_pause) {
EVLOG_info << "HLC module wont perform a pause. Even when there is no energy available.";
return;
}
const auto no_energy_pause_mode = config.zero_power_allow_ev_to_ignore_pause
? types::iso15118::NoEnergyPauseMode::AllowEvToIgnorePause
: types::iso15118::NoEnergyPauseMode::PauseBeforeCableCheck;
r_hlc[0]->call_no_energy_pause_charging(no_energy_pause_mode);
});
auto sae_mode = types::iso15118::SaeJ2847BidiMode::None;
std::vector<types::iso15118::EnergyTransferMode> initial_energy_transfers;
if (config.charge_mode == "AC") {
types::iso15118::SetupPhysicalValues setup_physical_values;
setup_physical_values.ac_nominal_voltage = config.ac_nominal_voltage;
r_hlc[0]->call_set_charging_parameters(setup_physical_values);
const auto hw_caps = *hw_capabilities.handle();
initial_energy_transfers = get_supported_ac_energy_transfers(config, hw_caps);
r_hlc[0]->subscribe_ac_eamount([this](double e) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex, Everest::MutexDescription::EVSE_subscribe_ac_eamount);
ev_info.remaining_energy_needed = e;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_ac_ev_max_voltage([this](double v) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_ac_ev_max_voltage);
ev_info.maximum_voltage_limit = v;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_ac_ev_max_current([this](double c) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_ac_ev_max_current);
ev_info.maximum_current_limit = c;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_ac_ev_min_current([this](double c) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_ac_ev_min_current);
ev_info.minimum_current_limit = c;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_ac_ev_power_limits([this](types::iso15118::AcEvPowerLimits const& l) {
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_ac_ev_power_limits);
if (l.max_charge_power.has_value()) {
ev_info.ac_max_charge_power = l.max_charge_power.value().total;
}
if (l.min_charge_power.has_value()) {
ev_info.ac_min_charge_power = l.min_charge_power.value().total;
}
if (l.max_discharge_power.has_value()) {
ev_info.ac_max_discharge_power = l.max_discharge_power.value().total;
}
if (l.min_discharge_power.has_value()) {
ev_info.ac_min_discharge_power = l.min_discharge_power.value().total;
}
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_ac_ev_present_powers([this](types::iso15118::AcEvPresentPowerValues const& values) {
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_ac_ev_present_powers);
ev_info.ac_present_active_power = values.present_active_power.total;
if (values.present_reactive_power.has_value()) {
ev_info.ac_present_reactive_power = values.present_reactive_power.value().total;
}
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_ac_ev_dynamic_control_mode(
[this](types::iso15118::AcEvDynamicModeValues const& values) {
Everest::scoped_lock_timeout lock(
ev_info_mutex, Everest::MutexDescription::EVSE_subscribe_ac_ev_dynamic_control_mode);
ev_info.target_energy_request = values.target_energy_request;
ev_info.max_energy_request = values.max_energy_request;
ev_info.min_energy_request = values.min_energy_request;
if (values.departure_time.has_value()) {
// TODO(SL): Convert departure_time to rfc3339
// ev_info.departure_time = values.departure_time.value();
}
// TODO(SL): Missing max_v2x_energy_request & min_v2x_energy_request
p_evse->publish_ev_info(ev_info);
});
} else if (config.charge_mode == "DC") {
// Create voltage plausibility monitor for DC charging
voltage_plausibility_monitor = std::make_unique<VoltagePlausibilityMonitor>(
[this](const std::string& description) {
if (this->error_handling) {
this->error_handling->raise_voltage_plausibility_fault(description);
}
},
config.voltage_plausibility_max_spread_threshold_V,
std::chrono::milliseconds(config.voltage_plausibility_fault_duration_ms));
if (connector_type.has_value() and connector_type.value() == types::evse_manager::ConnectorTypeEnum::cMCS) {
initial_energy_transfers.push_back(types::iso15118::EnergyTransferMode::MCS);
} else {
initial_energy_transfers.push_back(types::iso15118::EnergyTransferMode::DC_extended);
}
const auto caps = get_powersupply_capabilities();
update_powersupply_capabilities(caps);
if (caps.bidirectional) {
if (connector_type.has_value() and
connector_type.value() == types::evse_manager::ConnectorTypeEnum::cMCS) {
initial_energy_transfers.push_back(types::iso15118::EnergyTransferMode::MCS_BPT);
} else {
initial_energy_transfers.push_back(types::iso15118::EnergyTransferMode::DC_BPT);
}
}
// Set present measurements on HLC to sane defaults
types::iso15118::DcEvsePresentVoltageCurrent present_values;
present_values.evse_present_voltage = 0;
present_values.evse_present_current = 0;
r_hlc[0]->call_update_dc_present_values(present_values);
// Cable check for DC charging
r_hlc[0]->subscribe_start_cable_check([this] {
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::CableCheck;
cable_check();
});
// Cable check for DC charging
r_hlc[0]->subscribe_start_pre_charge(
[this] { power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::PreCharge; });
// Notification that current demand has started
r_hlc[0]->subscribe_current_demand_started([this] {
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Charging;
current_demand_active = true;
apply_new_target_voltage_current();
charger->notify_currentdemand_started();
if (not r_over_voltage_monitor.empty()) {
r_over_voltage_monitor[0]->call_start();
}
if (internal_over_voltage_monitor) {
internal_over_voltage_monitor->reset();
internal_over_voltage_monitor->start_monitor();
}
if (voltage_plausibility_monitor) {
voltage_plausibility_monitor->reset();
voltage_plausibility_monitor->start_monitor();
}
});
r_hlc[0]->subscribe_current_demand_finished([this] {
current_demand_active = false;
sae_bidi_active = false;
if (not r_over_voltage_monitor.empty()) {
r_over_voltage_monitor[0]->call_stop();
}
if (voltage_plausibility_monitor) {
voltage_plausibility_monitor->stop_monitor();
}
if (internal_over_voltage_monitor) {
internal_over_voltage_monitor->stop_monitor();
}
});
// Subscribe to voltage measurements from over_voltage_monitor interface
// The internal monitor acts as a software watchdog following the hardware OVM values
if (not r_over_voltage_monitor.empty()) {
r_over_voltage_monitor[0]->subscribe_voltage_measurement_V([this](float voltage_V) {
if (internal_over_voltage_monitor) {
internal_over_voltage_monitor->update_voltage(voltage_V);
}
});
}
// Isolation monitoring for DC charging handler
if (not r_imd.empty()) {
imd_stop();
r_imd[0]->subscribe_isolation_measurement([this](
types::isolation_monitor::IsolationMeasurement const& m) {
// new DC isolation monitoring measurement received
if (voltage_plausibility_monitor && m.voltage_V.has_value()) {
voltage_plausibility_monitor->update_isolation_monitor_voltage(m.voltage_V.value());
}
// Check for isolation errors
if (charger->get_current_state() == Charger::EvseState::Charging and
not check_isolation_resistance_in_range(m.resistance_F_Ohm)) {
error_handling->raise_isolation_resistance_fault(
fmt::format("Isolation resistance too low during charging: {} Ohm", m.resistance_F_Ohm),
"Resistance");
}
// Check L1e and L2e to PE as defined by IEC 61851-23:2023, $6.3.1.112.2
if (charger->get_current_state() == Charger::EvseState::Charging) {
bool is_in_range = check_voltage_to_protective_earth_in_range(m);
auto now = std::chrono::steady_clock::now();
if (not is_in_range) {
// Voltage to earth is out of range
if (voltage_to_earth_failure_count == 0) {
// First failure - record the time
first_voltage_to_earth_failure_time = now;
voltage_to_earth_failure_count = 1;
EVLOG_debug << "Voltage to earth out of range (first failure)";
} else {
// Subsequent failure - increment count
voltage_to_earth_failure_count++;
auto time_since_first_failure = std::chrono::duration_cast<std::chrono::seconds>(
now - first_voltage_to_earth_failure_time);
EVLOG_debug << "Voltage to earth out of range. Failure count: "
<< voltage_to_earth_failure_count
<< ", time since first: " << time_since_first_failure.count() << "s";
// Only raise error if we have at least 2 failures AND at least 2 seconds between first
// and last
if (voltage_to_earth_failure_count >= REQUIRED_CONSECUTIVE_FAILURES and
time_since_first_failure >= MIN_TIME_BETWEEN_FIRST_AND_LAST_FAILURE) {
error_handling->raise_isolation_resistance_fault(
fmt::format("Voltage to earth too high during charging L1e: {} V, L2e: {} V",
m.voltage_to_earth_l1e_V.value_or(NAN),
m.voltage_to_earth_l2e_V.value_or(NAN)),
"VoltageToEarth");
}
}
} else {
// Voltage to earth is in range - reset failure tracking
if (voltage_to_earth_failure_count > 0) {
EVLOG_debug << "Voltage to earth back in range, resetting failure counter";
voltage_to_earth_failure_count = 0;
}
}
}
isolation_measurement = m;
});
r_imd[0]->subscribe_self_test_result([this](bool result) {
session_log.evse(false, fmt::format("Isolation monitor self test result: {}", result));
selftest_result = result;
});
}
// On start up, stop ovm in case it is still enabled from a previous run
if (not r_over_voltage_monitor.empty()) {
r_over_voltage_monitor[0]->call_stop();
}
// Get voltage/current from DC power supply
if (not r_powersupply_DC.empty()) {
r_powersupply_DC[0]->subscribe_voltage_current([this](types::power_supply_DC::VoltageCurrent const& m) {
powersupply_measurement = m;
if (voltage_plausibility_monitor) {
voltage_plausibility_monitor->update_power_supply_voltage(m.voltage_V);
}
types::iso15118::DcEvsePresentVoltageCurrent present_values;
present_values.evse_present_voltage = (m.voltage_V > 0 ? m.voltage_V : 0.0);
present_values.evse_present_current = m.current_A;
if (config.hack_present_current_offset > 0) {
const auto current_offset = std::fabs(present_values.evse_present_current.value()) +
static_cast<float>(config.hack_present_current_offset);
present_values.evse_present_current = (m.current_A >= 0) ? current_offset : -current_offset;
}
if (config.hack_pause_imd_during_precharge and m.voltage_V * std::fabs(m.current_A) > 1000) {
// Start IMD again as it was stopped after CableCheck
imd_start();
EVLOG_info << "Hack: Restarting Isolation Measurement at " << m.voltage_V << " " << m.current_A;
}
r_hlc[0]->call_update_dc_present_values(present_values);
{
// dont publish ev_info here, it will be published when other values change.
// otherwise we will create too much traffic on mqtt
Everest::scoped_lock_timeout lock(ev_info_mutex, Everest::MutexDescription::EVSE_set_ev_info);
ev_info.present_voltage = present_values.evse_present_voltage;
ev_info.present_current = present_values.evse_present_current;
// p_evse->publish_ev_info(ev_info);
}
});
}
// Car requests a target voltage and current limit.
// Store raw EV values and immediately clamp against EVSE limits.
// Clamping is also re-applied by the Charger state machine
// (signal_dc_enforce_target_limits) in case the EV doesnt respect the
// limits or does not change the target values for some time.
r_hlc[0]->subscribe_dc_ev_target_voltage_current([this](types::iso15118::DcEvTargetValues const& v) {
raw_ev_target_voltage = v.dc_ev_target_voltage;
raw_ev_target_current = v.dc_ev_target_current;
process_dc_ev_target_voltage_current(charger->get_evse_max_hlc_limits());
});
r_hlc[0]->subscribe_d20_dc_dynamic_charge_mode(
[this](types::iso15118::DcChargeDynamicModeValues const& values) {
static bool last_is_actually_exporting_to_grid{false};
const auto energy_flow_changed =
is_actually_exporting_to_grid != last_is_actually_exporting_to_grid;
last_is_actually_exporting_to_grid = is_actually_exporting_to_grid;
bool target_changed{false};
double min_charge_power{0.0};
double max_charge_power{0.0};
double max_charge_current{0.0};
double target_current{0.0};
double target_voltage{0.0};
const auto max_hlc_limits = charger->get_evse_max_hlc_limits();
const auto min_hlc_limits = charger->get_evse_min_hlc_limits();
// TODO(SL): How to handle 0kW step between switching from uni to bidi
if (is_actually_exporting_to_grid and current_demand_active) {
if (values.max_discharge_power.has_value() and
max_hlc_limits.evse_maximum_discharge_power_limit.has_value()) {
// fabs every discharge limit
const auto ev_max_discharge_power = std::fabs(values.max_discharge_power.value());
const auto evse_max_discharge_power =
std::fabs(max_hlc_limits.evse_maximum_discharge_power_limit.value());
max_charge_power = (ev_max_discharge_power > evse_max_discharge_power)
? evse_max_discharge_power
: ev_max_discharge_power;
}
if (values.min_discharge_power.has_value() and
min_hlc_limits.evse_minimum_discharge_power_limit.has_value()) {
const auto ev_min_discharge_power = std::fabs(values.min_discharge_power.value());
const auto evse_min_discharge_power =
std::fabs(min_hlc_limits.evse_minimum_discharge_power_limit.value());
min_charge_power = (ev_min_discharge_power < evse_min_discharge_power)
? evse_min_discharge_power
: ev_min_discharge_power;
}
if (values.max_discharge_current.has_value() and
max_hlc_limits.evse_maximum_discharge_current_limit.has_value()) {
const auto ev_max_discharge_current = std::fabs(values.max_discharge_current.value());
const auto evse_max_discharge_current =
std::fabs(max_hlc_limits.evse_maximum_discharge_current_limit.value());
max_charge_current = (ev_max_discharge_current > evse_max_discharge_current)
? evse_max_discharge_current
: ev_max_discharge_current;
}
} else {
// Charging
max_charge_power = (values.max_charge_power > max_hlc_limits.evse_maximum_power_limit)
? max_hlc_limits.evse_maximum_power_limit
: values.max_charge_power;
min_charge_power = (values.min_charge_power > min_hlc_limits.evse_minimum_power_limit)
? values.min_charge_power
: min_hlc_limits.evse_minimum_power_limit;
max_charge_current = (values.max_charge_current > max_hlc_limits.evse_maximum_current_limit)
? max_hlc_limits.evse_maximum_current_limit
: values.max_charge_current;
}
if (min_charge_power > max_charge_power) {
EVLOG_error << "Minimum charge power limit is greater then the maximum charge power limit";
return;
}
// Setting voltage. charging: EvMaxVoltage, discharging: EvMinVoltage
target_voltage = (is_actually_exporting_to_grid)
? std::max(values.min_voltage, min_hlc_limits.evse_minimum_voltage_limit)
: values.max_voltage;
// Setting power to actual voltage on the powermeter
const auto actual_voltage = ev_info.present_voltage.value_or(latest_target_voltage);
target_current = (actual_voltage <= 0.0)
? max_charge_current
: std::min((max_charge_power / actual_voltage), max_charge_current);
raw_ev_target_voltage = target_voltage;
raw_ev_target_current = target_current;
process_dc_ev_target_voltage_current(charger->get_evse_max_hlc_limits());
});
// Car requests DC contactor open. We don't actually open but switch off DC supply.
// opening will be done by Charger on C->B CP event.
r_hlc[0]->subscribe_dc_open_contactor([this] {
powersupply_DC_off();
imd_stop();
});
// Back up switch off - charger signalled that it needs to switch off now.
// During normal operation this should be done earlier before switching off relais by HLC protocol.
charger->signal_dc_supply_off.connect([this] {
powersupply_DC_off();
imd_stop();
});
// Re-evaluate DC target limits so that updated EVSE
// limits are enforced even when the EV does not send new target values
charger->signal_dc_enforce_target_limits.connect(
[this](types::iso15118::DcEvseMaximumLimits const& limits) {
process_dc_ev_target_voltage_current(limits);
});
// Current demand has finished - switch off DC supply
r_hlc[0]->subscribe_current_demand_finished([this] { powersupply_DC_off(); });
r_hlc[0]->subscribe_dc_ev_maximum_limits([this](types::iso15118::DcEvMaximumLimits const& l) {
EVLOG_info << "Received EV maximum limits: " << l;
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_dc_ev_maximum_limits);
ev_info.maximum_current_limit = l.dc_ev_maximum_current_limit;
ev_info.maximum_power_limit = l.dc_ev_maximum_power_limit;
ev_info.maximum_voltage_limit = l.dc_ev_maximum_voltage_limit;
p_evse->publish_ev_info(ev_info);
// Update limits for over voltage monitoring
if (not r_over_voltage_monitor.empty()) {
r_over_voltage_monitor[0]->call_set_limits(get_emergency_over_voltage_threshold(),
get_error_over_voltage_threshold());
// Subscribe to voltage measurements from over_voltage_monitor for plausibility check
r_over_voltage_monitor[0]->subscribe_voltage_measurement_V([this](float voltage_V) {
if (voltage_plausibility_monitor) {
voltage_plausibility_monitor->update_over_voltage_monitor_voltage(voltage_V);
}
});
}
if (internal_over_voltage_monitor) {
internal_over_voltage_monitor->set_limits(get_emergency_over_voltage_threshold(),
get_error_over_voltage_threshold());
}
});
r_hlc[0]->subscribe_departure_time([this](const std::string& t) {
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_departure_time);
ev_info.departure_time = t;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_dc_ev_energy_capacity([this](double c) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_dc_ev_energy_capacity);
ev_info.battery_capacity = c;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_dc_ev_energy_request([this](double c) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_dc_ev_energy_request);
ev_info.remaining_energy_needed = c;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_dc_full_soc([this](double c) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex, Everest::MutexDescription::EVSE_subscribe_dc_full_soc);
ev_info.battery_full_soc = c;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_dc_bulk_soc([this](double c) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex, Everest::MutexDescription::EVSE_subscribe_dc_bulk_soc);
ev_info.battery_bulk_soc = c;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_dc_ev_remaining_time([this](types::iso15118::DcEvRemainingTime const& t) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_dc_ev_remaining_time);
ev_info.estimated_time_full = t.ev_remaining_time_to_full_soc;
ev_info.estimated_time_bulk = t.ev_remaining_time_to_full_bulk_soc;
p_evse->publish_ev_info(ev_info);
});
r_hlc[0]->subscribe_dc_ev_status([this](types::iso15118::DcEvStatus const& s) {
// FIXME send only on change / throttle messages
Everest::scoped_lock_timeout lock(ev_info_mutex,
Everest::MutexDescription::EVSE_subscribe_dc_ev_status);
ev_info.soc = s.dc_ev_ress_soc;
p_evse->publish_ev_info(ev_info);
});
// SAE J2847/2 Bidi
if (config.sae_j2847_2_bpt_enabled == true) {
sae_mode = types::iso15118::string_to_sae_j2847bidi_mode(config.sae_j2847_2_bpt_mode);
r_hlc[0]->subscribe_sae_bidi_mode_active([this] {
sae_bidi_active = true;
if (config.sae_j2847_2_bpt_mode == "V2H") {
setup_v2h_mode();
session_log.evse(true, "SAE J2847 V2H mode is active");
} else if (config.sae_j2847_2_bpt_mode == "V2G") {
session_log.evse(true, "SAE J2847 V2G mode is active");
} else {
EVLOG_error << "Unknown mode discovered. Please select V2G or V2H!";
}
});
}
// unused vars of HLC for now:
// ac_close_contactor
// ac_open_contactor
// selected_payment_option
// requested_energy_transfer_mode
// EV_ChargingSession
// dc_bulk_charging_complete
// dc_charging_complete
} else {
EVLOG_error << "Unsupported charging mode.";
exit(255);
}
r_hlc[0]->subscribe_selected_service_parameters(
[this](types::iso15118::SelectedServiceParameters const& parameters) {
selected_d20_energy_service.emplace(parameters.energy_transfer);
charger->set_hlc_d20_active();
session_log.car(true,
fmt::format("EV selected service: {}",
types::iso15118::service_category_to_string(parameters.energy_transfer)));
});
r_hlc[0]->call_receipt_is_required(config.ev_receipt_required);
this->update_supported_energy_transfers(initial_energy_transfers);
r_hlc[0]->call_setup(evseid, sae_mode, config.session_logging);
this->publish_and_update_supported_energy_transfers();
if ((config.bpt_channel == "Unified" or config.bpt_channel == "Separated") and
(config.bpt_generator_mode == "GridFollowing" or config.bpt_generator_mode == "GridForming")) {
types::iso15118::BptSetup bpt_setup_config{};
bpt_setup_config.bpt_channel = config.bpt_channel == "Unified" ? types::iso15118::BptChannel::Unified
: types::iso15118::BptChannel::Separated;
bpt_setup_config.generator_mode = config.bpt_generator_mode == "GridFollowing"
? types::iso15118::GeneratorMode::GridFollowing
: types::iso15118::GeneratorMode::GridForming;
if (config.bpt_grid_code_island_method == "Active" or config.bpt_grid_code_island_method == "Passive") {
bpt_setup_config.grid_code_detection = config.bpt_grid_code_island_method == "Active"
? types::iso15118::GridCodeIslandingDetectionMethod::Active
: types::iso15118::GridCodeIslandingDetectionMethod::Passive;
}
r_hlc[0]->call_bpt_setup(bpt_setup_config);
}
// reset error flags
r_hlc[0]->call_reset_error();
// implement Auth handlers
r_hlc[0]->subscribe_require_auth_eim([this]() {
// Do we have auth already (i.e. delayed HLC after charging already running)?
if ((config.dbg_hlc_auth_after_tstep and charger->get_authorized_eim_ready_for_hlc()) or
(not config.dbg_hlc_auth_after_tstep and charger->get_authorized_eim())) {
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = false;
r_hlc[0]->call_authorization_response(types::authorization::AuthorizationStatus::Accepted,
types::authorization::CertificateStatus::NoCertificateAvailable);
charger->get_stopwatch().mark("Auth EIM Done");
} else {
if (config.enable_autocharge) {
p_token_provider->publish_provided_token(autocharge_token);
}
hlc_waiting_for_auth_eim = true;
hlc_waiting_for_auth_pnc = false;
}
});
if (not config.autocharge_use_slac_instead_of_hlc) {
r_hlc[0]->subscribe_evcc_id([this](const std::string& token) {
autocharge_token = create_autocharge_token(token, config.connector_id);
car_manufacturer = get_manufacturer_from_mac(token);
p_evse->publish_car_manufacturer(car_manufacturer);
{
Everest::scoped_lock_timeout lock(ev_info_mutex, Everest::MutexDescription::EVSE_subscribe_evcc_id);
ev_info.evcc_id = token;
p_evse->publish_ev_info(ev_info);
}
});
}
r_hlc[0]->subscribe_require_auth_pnc([this](types::authorization::ProvidedIdToken const& _token) {
// Do we have auth already (i.e. delayed HLC after charging already running)?
auto copied_token = _token;
std::vector<int> referenced_connectors = {this->config.connector_id};
copied_token.connectors.emplace(referenced_connectors);
if (charger->get_authorized_pnc()) {
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = false;
} else {
// only publish token if we are not authorized yet. This enables pause/resume with PnC.
p_token_provider->publish_provided_token(copied_token);
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = true;
}
});
// Install debug V2G Messages handler if session logging is enabled
if (config.session_logging) {
r_hlc[0]->subscribe_v2g_messages(
[this](types::iso15118::V2gMessages const& v2g_messages) { log_v2g_message(v2g_messages); });
r_hlc[0]->subscribe_selected_protocol(
[this](std::string const& selected_protocol) { this->selected_protocol = selected_protocol; });
}
// switch to DC mode for first session for AC with SoC
if (config.ac_with_soc) {
bsp->signal_event.connect([this](const CPEvent event) {
if (event == CPEvent::CarUnplugged) {
// configure for DC again for next session. Will reset to AC when SoC is received
switch_DC_mode();
}
});
charger->signal_ac_with_soc_timeout.connect([this]() { switch_DC_mode(); });
r_hlc[0]->subscribe_dc_ev_status([this](types::iso15118::DcEvStatus const& status) {
EVLOG_info << fmt::format("SoC received: {}.", status.dc_ev_ress_soc);
switch_AC_mode();
});
}
}
bsp->signal_event.connect([this](const CPEvent event) {
// Forward events from BSP to SLAC module before we process the events in the charger
if (slac_enabled) {
if (event == CPEvent::EFtoBCD) {
// this means entering BCD from E|F
r_slac[0]->call_enter_bcd();
} else if (event == CPEvent::BCDtoEF) {
r_slac[0]->call_leave_bcd();
} else if (event == CPEvent::CarPluggedIn) {
// CC: right now we dont support energy saving mode, so no need to reset slac here.
// It is more important to start slac as early as possible to avoid unneccesary retries
// e.g. by Tesla cars which send the first SLAC_PARM_REQ directly after plugin.
// If we start slac too late, Tesla will do a B->C->DF->B sequence for each retry which
// may confuse the PWM state machine in some implementations.
// r_slac[0]->call_reset(true);
// This is entering BCD from state A
car_manufacturer = types::evse_manager::CarManufacturer::Unknown;
r_slac[0]->call_enter_bcd();
} else if (event == CPEvent::CarUnplugged) {
// Make a local copy as leave_bcd() will overwrite the slac_unmatched flag
bool unmatched_on_unplug = not slac_unmatched;
r_slac[0]->call_leave_bcd();
if (unmatched_on_unplug) {
r_slac[0]->call_reset(false);
}
hlc_waiting_for_auth_pnc = false;
hlc_waiting_for_auth_eim = false;
}
}
if (not r_over_voltage_monitor.empty() and event == CPEvent::CarUnplugged) {
r_over_voltage_monitor[0]->call_reset_over_voltage_error();
}
if (not r_over_voltage_monitor.empty() and (event == CPEvent::CarUnplugged or event == CPEvent::PowerOff)) {
r_over_voltage_monitor[0]->call_stop();
}
if (internal_over_voltage_monitor and (event == CPEvent::CarUnplugged or event == CPEvent::PowerOff)) {
internal_over_voltage_monitor->stop_monitor();
internal_over_voltage_monitor->reset();
}
if (voltage_plausibility_monitor and (event == CPEvent::CarUnplugged or event == CPEvent::PowerOff)) {
voltage_plausibility_monitor->stop_monitor();
}
charger->bsp_event_queue.push(event);
// Forward some events to HLC
if (hlc_enabled) {
// Reset HLC auth waiting flags on new session
if (event == CPEvent::CarPluggedIn) {
r_hlc[0]->call_reset_error();
r_hlc[0]->call_ac_contactor_closed(false);
r_hlc[0]->call_stop_charging(false);
latest_target_voltage = 0;
latest_target_current = 0;
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = false;
}
if (event == CPEvent::PowerOn) {
contactor_open = false;
r_hlc[0]->call_ac_contactor_closed(true);
}
if (event == CPEvent::PowerOff) {
contactor_open = true;
latest_target_voltage = 0;
latest_target_current = 0;
r_hlc[0]->call_ac_contactor_closed(false);
}
}
});
r_bsp->subscribe_ac_nr_of_phases_available([this](int n) { signalNrOfPhasesAvailable(n); });
r_bsp->subscribe_ac_pp_ampacity(
[this](types::board_support_common::ProximityPilot const& pp) { bsp->set_pp_ampacity(pp); });
if (r_powermeter_billing().size() > 0) {
r_powermeter_billing()[0]->subscribe_powermeter([this](types::powermeter::Powermeter const& p) {
// Update voltage plausibility monitor with powermeter voltage
if (voltage_plausibility_monitor && p.voltage_V.has_value() && p.voltage_V.value().DC.has_value()) {
voltage_plausibility_monitor->update_powermeter_voltage(p.voltage_V.value().DC.value());
}
// Inform charger about current charging current. This is used for slow OC detection.
if (p.current_A and p.current_A.value().L1 and p.current_A.value().L2 and p.current_A.value().L3) {
charger->set_current_drawn_by_vehicle(p.current_A.value().L1.value(), p.current_A.value().L2.value(),
p.current_A.value().L3.value());
}
// Inform HLC about the power meter data
if (hlc_enabled) {
r_hlc[0]->call_update_meter_info(p);
if (p.power_W and selected_d20_energy_service.has_value()) {
r_hlc[0]->call_update_ac_present_power(p.power_W.value());
}
}
// Store local cache
{
Everest::scoped_lock_timeout lock(power_mutex, Everest::MutexDescription::EVSE_subscribe_powermeter);
latest_powermeter_data_billing = p;
}
{
std::scoped_lock<std::mutex> lk(powermeter_mutex);
initial_powermeter_value_received = true;
}
powermeter_cv.notify_one();
// External Nodered interface
if (p.phase_seq_error) {
mqtt.publish(fmt::format("everest_external/nodered/{}/powermeter/phaseSeqError", config.connector_id),
p.phase_seq_error.value());
}
mqtt.publish(fmt::format("everest_external/nodered/{}/powermeter/time_stamp", config.connector_id),
p.timestamp);
if (p.power_W) {
mqtt.publish(fmt::format("everest_external/nodered/{}/powermeter/totalKw", config.connector_id),
p.power_W.value().total / 1000., 1);
}
mqtt.publish(fmt::format("everest_external/nodered/{}/powermeter/totalKWattHr", config.connector_id),
p.energy_Wh_import.total / 1000.);
if (p.energy_Wh_export.has_value()) {
mqtt.publish(
fmt::format("everest_external/nodered/{}/powermeter/totalExportKWattHr", config.connector_id),
p.energy_Wh_export.value().total / 1000.);
}
json j;
to_json(j, p);
mqtt.publish(fmt::format("everest_external/nodered/{}/powermeter_json", config.connector_id), j.dump());
// /External Nodered interface
});
}
if (slac_enabled) {
r_slac[0]->subscribe_state([this](const types::slac::State s) {
session_log.evse(true, fmt::format("SLAC {}", types::slac::state_to_string(s)));
// Notify charger whether matching was started (or is done) or not
if (s == types::slac::State::UNMATCHED) {
charger->set_matching_started(false);
slac_unmatched = true;
} else {
charger->set_matching_started(true);
slac_unmatched = false;
}
});
r_slac[0]->subscribe_request_error_routine([this]() {
EVLOG_info << "Received request error routine from SLAC in evsemanager\n";
charger->request_error_sequence();
});
r_slac[0]->subscribe_dlink_ready([this](const bool value) {
session_log.evse(true, fmt::format("D-LINK_READY ({})", value));
if (hlc_enabled) {
r_hlc[0]->call_dlink_ready(value);
charger->get_stopwatch().mark("D-LINK_READY");
}
});
}
charger->signal_max_current.connect([this](float ampere) {
// The charger changed the max current setting. Forward to HLC
if (hlc_enabled) {
if (not selected_d20_energy_service.has_value() and ampere >= 0.0) {
r_hlc[0]->call_update_ac_max_current(ampere); // ISO-2
return;
}
if (selected_d20_energy_service.value() == types::iso15118::ServiceCategory::AC or
selected_d20_energy_service.value() == types::iso15118::ServiceCategory::AC_BPT) {
types::units::Power target_power = {ampere * static_cast<float>(config.ac_nominal_voltage) *
hw_capabilities.handle()->max_phase_count_import};
// TODO(SL): Adding target frequency
// TODO(SL): Adding reactive power
r_hlc[0]->call_update_ac_target_values({target_power});
}
}
});
// Cancel reservations if charger is faulted
error_handling->signal_error.connect([this](ErrorHandlingEvents event) {
if (event == ErrorHandlingEvents::ForceEmergencyShutdown or event == ErrorHandlingEvents::ForceErrorShutdown) {
cancel_reservation(true);
}
});
charger->signal_simple_event.connect([this](types::evse_manager::SessionEventEnum s) {
if (s == types::evse_manager::SessionEventEnum::SessionFinished) {
// Reset EV information on Session start and end
ev_info = types::evse_manager::EVInfo();
p_evse->publish_ev_info(ev_info);
}
if (not hlc_enabled) {
return;
}
if (s != types::evse_manager::SessionEventEnum::Authorized and
s != types::evse_manager::SessionEventEnum::SessionFinished) {
return;
}
std::vector<types::iso15118::PaymentOption> payment_options;
// if pnc is disabled, disable contract installation and central contract validation
bool _contract_certificate_installation_enabled =
pnc_enabled ? contract_certificate_installation_enabled.load() : false;
bool _central_contract_validation_allowed = pnc_enabled ? central_contract_validation_allowed.load() : false;
if (config.payment_enable_eim) {
payment_options.push_back(types::iso15118::PaymentOption::ExternalPayment);
}
if (pnc_enabled and s == types::evse_manager::SessionEventEnum::SessionFinished) {
// PnC is enabled and this is a SessionFinished event -> enable Contract payment option
payment_options.push_back(types::iso15118::PaymentOption::Contract);
} else {
// We dont add contract if this is an Authorized event, as in this case the ISO15118 stack
// should not offer the contract option and certifiate installation service.
_contract_certificate_installation_enabled = false;
}
if (config.payment_enable_eim == false and pnc_enabled == false) {
EVLOG_warning << "Both payment options are disabled! ExternalPayment is nevertheless enabled in this case.";
payment_options.push_back(types::iso15118::PaymentOption::ExternalPayment);
}
r_hlc[0]->call_session_setup(payment_options, _contract_certificate_installation_enabled,
_central_contract_validation_allowed);
});
charger->signal_session_started_event.connect(
[this](types::evse_manager::StartSessionReason start_reason,
const std::optional<types::authorization::ProvidedIdToken>& provided_id_token) {
// Reset EV information on Session start and end
ev_info = types::evse_manager::EVInfo();
p_evse->publish_ev_info(ev_info);
if (not hlc_enabled) {
return;
}
std::vector<types::iso15118::PaymentOption> payment_options;
// if pnc is disabled, disable contract installation and central contract validation
bool _contract_certificate_installation_enabled =
pnc_enabled ? contract_certificate_installation_enabled.load() : false;
bool _central_contract_validation_allowed =
pnc_enabled ? central_contract_validation_allowed.load() : false;
if (start_reason == types::evse_manager::StartSessionReason::Authorized) {
// Session is already authorized, only use ExternalPayment in PaymentOptions
payment_options.push_back(types::iso15118::PaymentOption::ExternalPayment);
_contract_certificate_installation_enabled = false;
_central_contract_validation_allowed = false;
} else {
// Set payment options according to configuration
if (config.payment_enable_eim) {
payment_options.push_back(types::iso15118::PaymentOption::ExternalPayment);
}
if (pnc_enabled) {
payment_options.push_back(types::iso15118::PaymentOption::Contract);
}
}
r_hlc[0]->call_session_setup(payment_options, _contract_certificate_installation_enabled,
_central_contract_validation_allowed);
});
invoke_ready(*p_evse);
invoke_ready(*p_energy_grid);
invoke_ready(*p_token_provider);
invoke_ready(*p_random_delay);
if (config.ac_with_soc) {
setup_fake_DC_mode();
} else {
charger->setup(config.has_ventilation,
(config.charge_mode == "DC" ? Charger::ChargeMode::DC : Charger::ChargeMode::AC), hlc_enabled,
config.ac_hlc_use_5percent, config.ac_enforce_hlc, false,
config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A,
config.switch_3ph1ph_delay_s, config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms,
config.state_F_after_fault_ms, config.fail_on_powermeter_errors, config.raise_mrec9,
config.sleep_before_enabling_pwm_hlc_mode_ms,
utils::get_session_id_type_from_string(config.session_id_type),
config.hlc_charge_loop_without_energy_timeout_s);
}
telemetryThreadHandle = std::thread([this]() {
while (not telemetryThreadHandle.shouldExit()) {
sleep(10);
auto p = get_latest_powermeter_data_billing();
Everest::TelemetryMap telemetry_data{{"timestamp", p.timestamp},
{"type", "power_meter"},
{"meter_id", p.meter_id.value_or("N/A")},
{"energy_import_total_Wh", p.energy_Wh_import.total}};
if (p.energy_Wh_import.L1) {
telemetry_data["energy_import_L1_Wh"] = p.energy_Wh_import.L1.value();
}
if (p.energy_Wh_import.L2) {
telemetry_data["energy_import_L2_Wh"] = p.energy_Wh_import.L2.value();
}
if (p.energy_Wh_import.L3) {
telemetry_data["energy_import_L3_Wh"] = p.energy_Wh_import.L3.value();
}
if (p.energy_Wh_export) {
telemetry_data["energy_export_total_Wh"] = p.energy_Wh_export.value().total;
}
if (p.energy_Wh_export and p.energy_Wh_export.value().L1) {
telemetry_data["energy_export_L1_Wh"] = p.energy_Wh_export.value().L1.value();
}
if (p.energy_Wh_export and p.energy_Wh_export.value().L2) {
telemetry_data["energy_export_L2_Wh"] = p.energy_Wh_export.value().L2.value();
}
if (p.energy_Wh_export and p.energy_Wh_export.value().L3) {
telemetry_data["energy_export_L3_Wh"] = p.energy_Wh_export.value().L3.value();
}
if (p.power_W) {
telemetry_data["power_total_W"] = p.power_W.value().total;
}
if (p.power_W and p.power_W.value().L1) {
telemetry_data["power_L1_W"] = p.power_W.value().L1.value();
}
if (p.power_W and p.power_W.value().L2) {
telemetry_data["power_L3_W"] = p.power_W.value().L2.value();
}
if (p.power_W and p.power_W.value().L3) {
telemetry_data["power_L3_W"] = p.power_W.value().L3.value();
}
if (p.VAR) {
telemetry_data["var_total"] = p.VAR.value().total;
}
if (p.VAR and p.VAR.value().L1) {
telemetry_data["var_L1"] = p.VAR.value().L1.value();
}
if (p.VAR and p.VAR.value().L2) {
telemetry_data["var_L1"] = p.VAR.value().L2.value();
}
if (p.VAR and p.VAR.value().L3) {
telemetry_data["var_L1"] = p.VAR.value().L3.value();
}
if (p.voltage_V and p.voltage_V.value().L1) {
telemetry_data["voltage_L1_V"] = p.voltage_V.value().L1.value();
}
if (p.voltage_V and p.voltage_V.value().L2) {
telemetry_data["voltage_L2_V"] = p.voltage_V.value().L2.value();
}
if (p.voltage_V and p.voltage_V.value().L3) {
telemetry_data["voltage_L3_V"] = p.voltage_V.value().L3.value();
}
if (p.voltage_V and p.voltage_V.value().DC) {
telemetry_data["voltage_DC_V"] = p.voltage_V.value().DC.value();
}
if (p.current_A and p.current_A.value().L1) {
telemetry_data["current_L1_A"] = p.current_A.value().L1.value();
}
if (p.current_A and p.current_A.value().L2) {
telemetry_data["current_L2_A"] = p.current_A.value().L2.value();
}
if (p.current_A and p.current_A.value().L3) {
telemetry_data["current_L3_A"] = p.current_A.value().L3.value();
}
if (p.current_A and p.current_A.value().DC) {
telemetry_data["current_DC_A"] = p.current_A.value().DC.value();
}
if (p.frequency_Hz) {
telemetry_data["frequency_L1_Hz"] = p.frequency_Hz.value().L1;
}
if (p.frequency_Hz and p.frequency_Hz.value().L2) {
telemetry_data["frequency_L2_Hz"] = p.frequency_Hz.value().L2.value();
}
if (p.frequency_Hz and p.frequency_Hz.value().L3) {
telemetry_data["frequency_L3_Hz"] = p.frequency_Hz.value().L3.value();
}
if (p.phase_seq_error) {
telemetry_data["phase_seq_error"] = p.phase_seq_error.value();
}
// Publish as external telemetry data
telemetry.publish("livedata", "power_meter", telemetry_data);
}
});
{
// wait for first powermeter value
std::unique_lock<std::mutex> lk(powermeter_mutex);
this->powermeter_cv.wait_for(lk, std::chrono::milliseconds(this->config.initial_meter_value_timeout_ms),
[this] { return initial_powermeter_value_received; });
}
// Resuming left-over transaction from e.g. powerloss. This information allows other modules like to OCPP to be
// informed that the EvseManager is aware of previous sessions so that no individual cleanup is required
const auto session_id = store->get_session();
if (!session_id.empty()) {
charger->signal_session_resumed_event(session_id);
}
// By default cleanup left-over transaction from e.g. power loss
// TOOD: Add resume handling
charger->cleanup_transactions_on_startup();
// start with a limit of 0 amps. We will get a budget from EnergyManager that is locally limited by hw
// caps.
charger->set_max_current(0.0F, steady_clock::now() + std::chrono::seconds(120));
this->p_evse->publish_waiting_for_external_ready(config.external_ready_to_start_charging);
if (not config.external_ready_to_start_charging) {
// immediately ready, otherwise delay until we get the external signal
this->ready_to_start_charging();
}
}
void EvseManager::ready_to_start_charging() {
Everest::scoped_lock_timeout lock(charger_ready_mutex, Everest::MutexDescription::EVSE_charger_ready);
if (charger_ready) {
EVLOG_warning << "Already ready to start charging!";
return;
}
charger_ready = true;
p_evse->publish_supported_energy_transfer_modes(*this->supported_energy_transfers.handle());
timepoint_ready_for_charging = std::chrono::steady_clock::now();
charger->run();
// this will publish a session event Enabled or Disabled that allows other modules the retrieve this state on
// startup
charger->enable_disable_initial_state_publish();
this->p_evse->publish_ready(true);
EVLOG_info << fmt::format(fmt::emphasis::bold | fg(fmt::terminal_color::green), "🌀🌀🌀 Ready to start charging 🌀🌀🌀");
if (!initial_powermeter_value_received) {
EVLOG_warning << "No powermeter value received yet!";
}
}
types::powermeter::Powermeter EvseManager::get_latest_powermeter_data_billing() {
Everest::scoped_lock_timeout lock(power_mutex, Everest::MutexDescription::EVSE_get_latest_powermeter_data_billing);
return latest_powermeter_data_billing;
}
types::evse_board_support::HardwareCapabilities EvseManager::get_hw_capabilities() {
return *hw_capabilities.handle();
}
int32_t EvseManager::get_reservation_id() {
Everest::scoped_lock_timeout lock(reservation_mutex, Everest::MutexDescription::EVSE_get_reservation_id);
return reservation_id;
}
void EvseManager::switch_DC_mode() {
setup_fake_DC_mode();
}
void EvseManager::switch_AC_mode() {
setup_AC_mode();
}
// This sets up a fake DC mode that is just supposed to work until we get the SoC.
// It is only used for AC<>DC<>AC<>DC mode to get AC charging with SoC.
void EvseManager::setup_fake_DC_mode() {
charger->setup(config.has_ventilation, Charger::ChargeMode::DC, hlc_enabled, config.ac_hlc_use_5percent,
config.ac_enforce_hlc, false, config.soft_over_current_tolerance_percent,
config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s,
config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms,
config.fail_on_powermeter_errors, config.raise_mrec9, config.sleep_before_enabling_pwm_hlc_mode_ms,
utils::get_session_id_type_from_string(config.session_id_type),
config.hlc_charge_loop_without_energy_timeout_s);
types::iso15118::EVSEID evseid = {config.evse_id, config.evse_id_din};
// Set up energy transfer modes for HLC. For now we only support either DC or AC, not both at the same time.
std::vector<types::iso15118::EnergyTransferMode> transfer_modes;
transfer_modes.push_back(types::iso15118::EnergyTransferMode::DC_core);
transfer_modes.push_back(types::iso15118::EnergyTransferMode::DC_extended);
transfer_modes.push_back(types::iso15118::EnergyTransferMode::DC_combo_core);
transfer_modes.push_back(types::iso15118::EnergyTransferMode::DC_unique);
types::iso15118::DcEvsePresentVoltageCurrent present_values;
present_values.evse_present_voltage = 400; // FIXME: set a correct values
present_values.evse_present_current = 0;
r_hlc[0]->call_update_dc_present_values(present_values);
types::iso15118::DcEvseMaximumLimits evse_max_limits;
evse_max_limits.evse_maximum_current_limit = 400;
evse_max_limits.evse_maximum_power_limit = 200000;
evse_max_limits.evse_maximum_voltage_limit = 1000;
r_hlc[0]->call_update_dc_maximum_limits(evse_max_limits);
types::iso15118::DcEvseMinimumLimits evse_min_limits;
evse_min_limits.evse_minimum_current_limit = 0;
evse_min_limits.evse_minimum_voltage_limit = 0;
r_hlc[0]->call_update_dc_minimum_limits(evse_min_limits);
constexpr auto sae_mode = types::iso15118::SaeJ2847BidiMode::None;
r_hlc[0]->call_setup(evseid, sae_mode, config.session_logging);
this->update_supported_energy_transfers(transfer_modes);
this->publish_and_update_supported_energy_transfers();
}
void EvseManager::setup_AC_mode() {
charger->setup(config.has_ventilation, Charger::ChargeMode::AC, hlc_enabled, config.ac_hlc_use_5percent,
config.ac_enforce_hlc, true, config.soft_over_current_tolerance_percent,
config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s,
config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms,
config.fail_on_powermeter_errors, config.raise_mrec9, config.sleep_before_enabling_pwm_hlc_mode_ms,
utils::get_session_id_type_from_string(config.session_id_type),
config.hlc_charge_loop_without_energy_timeout_s);
types::iso15118::EVSEID evseid = {config.evse_id, config.evse_id_din};
// Set up energy transfer modes for HLC. For now we only support either DC or AC, not both at the same time.
std::vector<types::iso15118::EnergyTransferMode> transfer_modes;
transfer_modes.push_back(types::iso15118::EnergyTransferMode::AC_single_phase_core);
if (hw_capabilities.handle()->max_phase_count_import == 3) {
transfer_modes.push_back(types::iso15118::EnergyTransferMode::AC_three_phase_core);
}
types::iso15118::SetupPhysicalValues setup_physical_values;
constexpr auto sae_mode = types::iso15118::SaeJ2847BidiMode::None;
if (hlc_enabled) {
r_hlc[0]->call_setup(evseid, sae_mode, config.session_logging);
this->update_supported_energy_transfers(transfer_modes);
this->publish_and_update_supported_energy_transfers();
}
}
void EvseManager::setup_v2h_mode() {
const auto timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
types::energy::ExternalLimits external_limits;
types::energy::ScheduleReqEntry target_entry;
target_entry.timestamp = timestamp;
target_entry.limits_to_leaves.total_power_W = {powersupply_capabilities.max_import_power_W.value_or(0.),
info.id + "/setup_v2h_mode"};
types::energy::ScheduleReqEntry zero_entry;
zero_entry.timestamp = timestamp;
zero_entry.limits_to_leaves.total_power_W = {0};
external_limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, target_entry);
external_limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, zero_entry);
update_local_energy_limit(external_limits);
}
bool EvseManager::update_local_energy_limit(types::energy::ExternalLimits l) {
std::scoped_lock lock(external_local_limits_mutex);
external_local_energy_limits = l;
// wait for EnergyManager to assign optimized current on next opimizer run
return true;
}
// Helper function to set a watt limit in an ExternalLimits type
bool EvseManager::update_max_watt_limit(types::energy::ExternalLimits& limits, float max_watt_export,
std::optional<float> max_watt_import) {
types::energy::ScheduleReqEntry e;
e.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
e.limits_to_leaves.total_power_W = {max_watt_export, info.id + " update_max_watt_limit"};
limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, e);
e.limits_to_leaves.total_power_W = {max_watt_import.value_or(0.0f), info.id + " update_max_watt_limit"};
limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, e);
return true;
}
void EvseManager::update_to_zero_discharge_limit(types::energy::ExternalLimits& limits) {
types::energy::ScheduleReqEntry e;
e.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
e.limits_to_leaves.ac_max_current_A = {0., info.id + " set_zero_discharge_limit"};
e.limits_to_leaves.total_power_W = {0., info.id + " set_zero_discharge_limit"};
limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, e);
}
// Helper function to set a current limit in an ExternalLimits type
bool EvseManager::update_max_current_limit(types::energy::ExternalLimits& limits, float max_current_import,
float max_current_export) {
if (config.charge_mode == "DC") {
return false;
}
types::energy::ScheduleReqEntry e;
e.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
e.limits_to_leaves.ac_max_current_A = {max_current_import, info.id + " update_max_current_limit"};
limits.schedule_import = std::vector<types::energy::ScheduleReqEntry>(1, e);
e.limits_to_leaves.ac_max_current_A = {max_current_export, info.id + " update_max_current_limit"};
limits.schedule_export = std::vector<types::energy::ScheduleReqEntry>(1, e);
return true;
}
bool EvseManager::reserve(int32_t id, const bool signal_reservation_event) {
EVLOG_debug << "Reserve called for reservation id " << id
<< ", signal reservation event: " << signal_reservation_event;
// is the evse Unavailable?
if (charger->get_current_state() == Charger::EvseState::Disabled) {
EVLOG_info << "Rejecting reservation because charger is disabled.";
return false;
}
// is the evse faulted?
if (charger->stop_charging_on_fatal_error()) {
EVLOG_info << "Rejecting reservation because of a fatal error.";
return false;
}
// is the connector currently ready to accept a new car?
if (charger->get_current_state() not_eq Charger::EvseState::Idle) {
EVLOG_info << "Rejecting reservation because evse is not idle";
return false;
}
Everest::scoped_lock_timeout lock(reservation_mutex, Everest::MutexDescription::EVSE_reserve);
const bool overwrite_reservation = (this->reservation_id == id);
if (reserved && this->reservation_id != -1) {
EVLOG_info << "Rejecting reservation because evse is already reserved for reservation id "
<< this->reservation_id;
}
// Check if this evse is not already reserved, or overwrite reservation if it is for the same reservation id.
if (not reserved || this->reservation_id == -1 || overwrite_reservation) {
EVLOG_debug << "Make the reservation with id " << id;
reserved = true;
if (id >= 0) {
this->reservation_id = id;
}
// When overwriting the reservation, don't signal.
if ((not overwrite_reservation || this->reservation_id == -1) && signal_reservation_event) {
// publish event to other modules
types::evse_manager::SessionEvent se;
se.event = types::evse_manager::SessionEventEnum::ReservationStart;
// Normally we should signal for each connector when an evse is reserved, but since in this implementation
// each evse only has one connector, this is sufficient for now.
signalReservationEvent(se);
}
return true;
}
return false;
}
void EvseManager::cancel_reservation(bool signal_event) {
Everest::scoped_lock_timeout lock(reservation_mutex, Everest::MutexDescription::EVSE_cancel_reservation);
if (reserved) {
EVLOG_debug << "Reservation cancelled";
reserved = false;
this->reservation_id = -1;
// publish event to other modules
if (signal_event) {
types::evse_manager::SessionEvent se;
se.event = types::evse_manager::SessionEventEnum::ReservationEnd;
signalReservationEvent(se);
}
}
}
bool EvseManager::is_reserved() {
Everest::scoped_lock_timeout lock(reservation_mutex, Everest::MutexDescription::EVSE_is_reserved);
return reserved;
}
bool EvseManager::get_hlc_waiting_for_auth_pnc() {
return hlc_waiting_for_auth_pnc;
}
void EvseManager::set_pnc_enabled(const bool value) {
pnc_enabled = value;
}
void EvseManager::set_central_contract_validation_allowed(const bool value) {
central_contract_validation_allowed = value;
}
void EvseManager::set_contract_certificate_installation_enabled(const bool value) {
contract_certificate_installation_enabled = value;
}
void EvseManager::publish_and_update_supported_energy_transfers() {
std::vector<types::iso15118::EnergyTransferMode> transfer_modes;
{
auto transfer_modes_handle = this->supported_energy_transfers.handle();
transfer_modes = *transfer_modes_handle;
}
p_evse->publish_supported_energy_transfer_modes(transfer_modes);
if (hlc_enabled and not r_hlc.empty()) {
r_hlc[0]->call_update_energy_transfer_modes(transfer_modes);
}
}
bool EvseManager::update_supported_energy_transfers(
const std::vector<types::iso15118::EnergyTransferMode>& energy_transfers) {
auto handle = this->supported_energy_transfers.handle();
if (*handle == energy_transfers) {
return false;
}
*handle = energy_transfers;
return true;
}
bool EvseManager::update_supported_energy_transfers(const types::iso15118::EnergyTransferMode& energy_transfer) {
return update_supported_energy_transfers(std::vector<types::iso15118::EnergyTransferMode>{energy_transfer});
}
void EvseManager::update_hlc_ac_parameters() {
// Copy hw_caps before acquiring hlc_ac_parameters_mutex to avoid holding two locks simultaneously
const auto hw_caps = *hw_capabilities.handle();
std::scoped_lock lock(hlc_ac_parameters_mutex);
types::iso15118::AcEvseMaximumPower ac_maximum_power;
const float max_charge_power_per_phase = config.ac_nominal_voltage * hw_caps.max_current_A_import;
const float max_discharge_power_per_phase = config.ac_nominal_voltage * hw_caps.max_current_A_export;
ac_maximum_power.charge_power = {
max_charge_power_per_phase * hw_caps.max_phase_count_import,
max_charge_power_per_phase,
hw_caps.max_phase_count_import >= 2 ? std::make_optional(max_charge_power_per_phase) : std::nullopt,
hw_caps.max_phase_count_import == 3 ? std::make_optional(max_charge_power_per_phase) : std::nullopt,
};
ac_maximum_power.discharge_power.emplace(types::units::Power{
max_discharge_power_per_phase * hw_caps.max_phase_count_export,
max_discharge_power_per_phase,
hw_caps.max_phase_count_export >= 2 ? std::make_optional(max_discharge_power_per_phase) : std::nullopt,
hw_caps.max_phase_count_export == 3 ? std::make_optional(max_discharge_power_per_phase) : std::nullopt,
});
r_hlc[0]->call_update_ac_maximum_limits(ac_maximum_power);
types::iso15118::AcEvseMinimumPower ac_minimum_power;
const float min_charge_power_per_phase = config.ac_nominal_voltage * hw_caps.min_current_A_import;
const float min_discharge_power_per_phase = config.ac_nominal_voltage * hw_caps.min_current_A_export;
ac_minimum_power.charge_power = {
min_charge_power_per_phase * hw_caps.max_phase_count_import,
min_charge_power_per_phase,
hw_caps.max_phase_count_import >= 2 ? std::make_optional(min_charge_power_per_phase) : std::nullopt,
hw_caps.max_phase_count_import == 3 ? std::make_optional(min_charge_power_per_phase) : std::nullopt,
};
ac_minimum_power.discharge_power.emplace(types::units::Power{
min_discharge_power_per_phase * hw_caps.max_phase_count_export,
min_discharge_power_per_phase,
hw_caps.max_phase_count_export >= 2 ? std::make_optional(min_discharge_power_per_phase) : std::nullopt,
hw_caps.max_phase_count_export == 3 ? std::make_optional(min_discharge_power_per_phase) : std::nullopt,
});
r_hlc[0]->call_update_ac_minimum_limits(ac_minimum_power);
std::vector<types::iso15118::Connector> ac_connectors{types::iso15118::Connector::SinglePhase};
if (hw_caps.max_phase_count_import == 3) {
ac_connectors.push_back(types::iso15118::Connector::ThreePhase);
}
r_hlc[0]->call_update_ac_parameters({50, static_cast<float>(config.ac_nominal_voltage), ac_connectors, std::nullopt,
std::nullopt}); // TODO(sl): Getting nominal frequency
}
void EvseManager::log_v2g_message(types::iso15118::V2gMessages const& v2g_messages) {
if (not config.session_logging) {
return;
}
const std::string msg = types::iso15118::v2g_message_id_to_string(v2g_messages.id);
const std::string xml = v2g_messages.xml.value_or("");
const std::string json_str = v2g_messages.v2g_json.value_or("");
const std::string exi_hex = v2g_messages.exi.value_or("");
const std::string exi_base64 = v2g_messages.exi_base64.value_or("");
// All messages from EVSE contain Req and all originating from Car contain Res
if (msg.find("Res") == std::string::npos) {
session_log.car(true, fmt::format("V2G {}", msg), xml, exi_hex, exi_base64, json_str);
} else {
session_log.evse(true, fmt::format("V2G {}", msg), xml, exi_hex, exi_base64, json_str);
}
}
void EvseManager::charger_was_authorized() {
if (hlc_waiting_for_auth_pnc and charger->get_authorized_pnc()) {
r_hlc[0]->call_authorization_response(types::authorization::AuthorizationStatus::Accepted,
types::authorization::CertificateStatus::Accepted);
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = false;
charger->get_stopwatch().mark("Auth PnC Done");
}
if (hlc_waiting_for_auth_eim and charger->get_authorized_eim()) {
r_hlc[0]->call_authorization_response(types::authorization::AuthorizationStatus::Accepted,
types::authorization::CertificateStatus::NoCertificateAvailable);
hlc_waiting_for_auth_eim = false;
hlc_waiting_for_auth_pnc = false;
charger->get_stopwatch().mark("Auth EIM Done");
}
}
static double get_cable_check_voltage(double ev_max_cpd, double evse_max_cpd) {
double cable_check_voltage = 500;
// IEC 61851-23 (2023) CC.4.1.2 / Formular CC.1
if (ev_max_cpd <= 500) {
if ((ev_max_cpd + 50) < cable_check_voltage) {
cable_check_voltage = (ev_max_cpd + 50);
}
if (evse_max_cpd < cable_check_voltage) {
cable_check_voltage = evse_max_cpd;
}
} else {
cable_check_voltage = evse_max_cpd;
if (1.1 * ev_max_cpd < cable_check_voltage) {
cable_check_voltage = 1.1 * ev_max_cpd;
}
}
return cable_check_voltage;
}
double EvseManager::get_emergency_over_voltage_threshold() {
float ev_max_voltage = 500.;
if (ev_info.maximum_voltage_limit.has_value()) {
ev_max_voltage = ev_info.maximum_voltage_limit.value();
} else {
EVLOG_error << "OverVoltageThreshold: Did not receive EV maximum voltage, falling back to 500V";
}
auto evse_caps = get_powersupply_capabilities();
double negotiated_max_voltage = std::min(ev_max_voltage, evse_caps.max_export_voltage_V);
double ovp = 550.;
if (negotiated_max_voltage > 1000) {
// IEC 61851-23-3 (DRAFT 2025) Table 202
ovp = 1375.;
} else if (negotiated_max_voltage > 850) {
// IEC 61851-23 (2023) 6.3.1.106.2 Table 103
ovp = 1100.;
} else if (negotiated_max_voltage > 750) {
// IEC 61851-23 (2023) 6.3.1.106.2 Table 103
ovp = 935.;
} else if (negotiated_max_voltage > 500) {
// IEC 61851-23 (2023) 6.3.1.106.2 Table 103
ovp = 825.;
}
return ovp;
}
double EvseManager::get_error_over_voltage_threshold() {
double ev_max_voltage = 500;
if (ev_info.maximum_voltage_limit.has_value()) {
ev_max_voltage = ev_info.maximum_voltage_limit.value();
} else {
EVLOG_error << "OverVoltageThreshold: Did not receive EV maximum voltage, falling back to 500V";
}
return ev_max_voltage;
}
bool EvseManager::cable_check_should_exit() {
return charger->get_current_state() not_eq Charger::EvseState::PrepareCharging;
}
bool EvseManager::check_voltage_to_protective_earth_in_range(types::isolation_monitor::IsolationMeasurement m) {
static constexpr double MAX_VOLTAGE_STATIC = 550.0; // defined by IEC 61851-23:2023, $6.3.1.112.2
if (m.voltage_V.has_value() and m.voltage_to_earth_l1e_V.has_value() and m.voltage_to_earth_l2e_V.has_value()) {
// This may trigger while ramping down voltage so we should try to check against the minium valid voltage the
// the power supply can deliver. If the UL1e/2e is poorly synchronized to the actual voltage measurement it may
// trigger false positives thus limiting the checks to min_export_voltage
if (m.voltage_V.value() > powersupply_capabilities.min_export_voltage_V) {
// implemented according to the standard see IEC 61851-23:2023, $6.3.1.112.2
// if the charger rating is above 500, use as maximum value 110% of actual voltage
// if the charger rating is below 500, use as maxiumum value 550V
if (powersupply_capabilities.max_export_voltage_V > 500) {
double max_allowed_value = m.voltage_V.value() * 1.1;
return (std::fabs(m.voltage_to_earth_l1e_V.value()) < max_allowed_value and
std::fabs(m.voltage_to_earth_l2e_V.value()) < max_allowed_value);
} else {
return (std::fabs(m.voltage_to_earth_l1e_V.value()) < MAX_VOLTAGE_STATIC and
std::fabs(m.voltage_to_earth_l2e_V.value()) < MAX_VOLTAGE_STATIC);
}
}
}
// if we can't check, assume all good
return true;
}
bool EvseManager::check_isolation_resistance_in_range(double resistance) {
if (resistance < CABLECHECK_INSULATION_FAULT_RESISTANCE_OHM) {
session_log.evse(false, fmt::format("Isolation measurement FAULT R_F {}.", resistance));
r_hlc[0]->call_update_isolation_status(types::iso15118::IsolationStatus::Fault);
return false;
} else {
session_log.evse(false, fmt::format("Isolation measurement Ok R_F {}.", resistance));
r_hlc[0]->call_update_isolation_status(types::iso15118::IsolationStatus::Valid);
}
return true;
}
void EvseManager::cable_check() {
if (r_imd.empty()) {
// If no IMD is connected, we skip isolation checking.
EVLOG_info << "No IMD: skipping cable check.";
r_hlc[0]->call_update_isolation_status(types::iso15118::IsolationStatus::No_IMD);
r_hlc[0]->call_cable_check_finished(true);
return;
}
// start cable check in a seperate thread.
std::thread t([this]() {
session_log.evse(true, "Start cable check...");
charger->get_stopwatch().report_phase();
charger->get_stopwatch().mark_phase("CableCheck");
// Verify output is below 60V initially
if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) {
std::ostringstream oss;
oss << "Voltage did not drop below " << CABLECHECK_SAFE_VOLTAGE << "V within timeout.";
fail_cable_check(oss.str());
return;
}
charger->get_stopwatch().mark("<60V");
// Allow closing from HLC perspective, it will wait for CP state C in Charger IEC state machine as well.
session_log.car(true, "DC HLC Close contactor (in CableCheck)");
charger->set_hlc_allow_close_contactor(true);
// Some HW platforms require the self test to be performed at the beginning of cablecheck phase.
// They will close the relays only after the self test was successful.
// This is done at a configurable voltage. As only the self test is done and no isolation resistance
// measurement, this voltage may be different from the voltage used later to measure the resistance
// (which is derived from the ev_max_voltage instead)
if (config.cable_check_enable_imd_self_test_relays_open) {
session_log.evse(true, "IMD Early self test in cablecheck");
// Set power supply to configured voltage for the self test
if (not powersupply_DC_set(config.cable_check_relays_open_voltage_V, CABLECHECK_CURRENT_LIMIT)) {
fail_cable_check(
"CableCheck: Could not set DC power supply voltage and current for early IMD self test.");
return;
}
EVLOG_info << "CableCheck early IMD self test: Using " << config.cable_check_relays_open_voltage_V << " V";
powersupply_DC_on();
if (not wait_powersupply_DC_voltage_reached(config.cable_check_relays_open_voltage_V)) {
std::ostringstream oss;
oss << "CableCheck: Voltage did not rise to " << config.cable_check_relays_open_voltage_V
<< " V within timeout";
fail_cable_check(oss.str());
return;
}
selftest_result.clear();
r_imd[0]->call_start_self_test(config.cable_check_relays_open_voltage_V);
EVLOG_info << "CableCheck: Early IMD self test started.";
// Wait for the result of the self test
bool result{false};
bool result_received{false};
for (int wait_seconds = 0; wait_seconds < CABLECHECK_SELFTEST_TIMEOUT; wait_seconds++) {
if (cable_check_should_exit()) {
fail_cable_check("Cancel cable check");
return;
}
if (selftest_result.wait_for(result, 1s)) {
result_received = true;
break;
}
}
if (not result_received) {
fail_cable_check("CableCheck: Did not get a early self test result from IMD within timeout");
return;
}
if (not result) {
EVLOG_error << "CableCheck: Early IMD Self test failed";
fail_cable_check("Early IMD self test failed during cable check");
return;
}
powersupply_DC_off();
charger->get_stopwatch().mark("Early IMD self test");
}
// normally contactors should be closed before entering cable check routine.
// On some hardware implementation it may take some time until the confirmation arrives though,
// so we wait with a timeout here until the contactors are confirmed to be closed.
Timeout timeout;
timeout.start(std::chrono::seconds(config.cable_check_relays_closed_timeout_s));
while (not timeout.reached() and not cable_check_should_exit()) {
if (not contactor_open) {
break;
}
std::this_thread::sleep_for(100ms);
}
// If relais are still open after timeout, give up
if (contactor_open) {
fail_cable_check("CableCheck: Contactors are still open after timeout, giving up.");
return;
}
charger->get_stopwatch().mark("Relay On");
// Get correct voltage used to test the isolation
for (int retry_ev_info = 0; retry_ev_info < 10; retry_ev_info++) {
auto ev_info = get_ev_info();
if (ev_info.maximum_voltage_limit.has_value()) {
break;
}
std::this_thread::sleep_for(100ms);
}
float ev_max_voltage = 500.;
if (ev_info.maximum_voltage_limit.has_value()) {
EVLOG_info << "EV reports " << ev_info.maximum_voltage_limit.value() << " V as maximum voltage";
ev_max_voltage = ev_info.maximum_voltage_limit.value();
} else {
EVLOG_error << "CableCheck: Did not receive EV maximum voltage, falling back to 500V";
}
auto evse_caps = get_powersupply_capabilities();
double cable_check_voltage = get_cable_check_voltage(ev_max_voltage, evse_caps.max_export_voltage_V);
// Allow overriding the cable check voltage from a configuration value
if (config.dc_isolation_voltage_V > 0) {
cable_check_voltage = config.dc_isolation_voltage_V;
}
charger->get_stopwatch().mark("EVInfo");
// Set the DC ouput voltage for testing
if (not powersupply_DC_set(cable_check_voltage, CABLECHECK_CURRENT_LIMIT)) {
fail_cable_check("CableCheck: Could not set DC power supply voltage and current.");
return;
} else {
EVLOG_info << "CableCheck: Using " << cable_check_voltage << " V";
}
// Switch on output voltage
powersupply_DC_on();
// Wait until the voltage has rised to the target value.
// This also handles the short circuit test according to IEC 61851-23 (2023) 6.3.1.109:
// CC.7.6.20.3: the maximum R for the short circuit test is 110 Ohms.
// CC.7.6.20.7: maximum current should be reduced to <5A within 1s. We set a current limit below 5A, so the
// power supply should always achieve that.
// Within 2.5s present voltage at side B must be below 60V. As the power supply ramp up speed varies greatly,
// we can only achieve this by limiting the current to I < cable_check_voltage/110 Ohm. The hard coded limit
// above fulfills that for all voltage ranges.
if (not wait_powersupply_DC_voltage_reached(cable_check_voltage)) {
std::ostringstream oss;
oss << "CableCheck: Voltage did not rise to " << cable_check_voltage << " V within timeout";
fail_cable_check(oss.str());
return;
}
charger->get_stopwatch().mark("VRampUp");
// CC 4.1.3: Now relais are closed, voltage is up. We need to perform a self test of the IMD device
if (config.cable_check_enable_imd_self_test) {
selftest_result.clear();
r_imd[0]->call_start_self_test(cable_check_voltage);
EVLOG_info << "CableCheck: IMD self test started.";
// Wait for the result of the self test
bool result{false};
bool result_received{false};
for (int wait_seconds = 0; wait_seconds < CABLECHECK_SELFTEST_TIMEOUT; wait_seconds++) {
if (cable_check_should_exit()) {
fail_cable_check("Cancel cable check");
return;
}
if (selftest_result.wait_for(result, 1s)) {
result_received = true;
break;
}
}
if (not result_received) {
fail_cable_check("CableCheck: Did not get a self test result from IMD within timeout");
return;
}
if (not result) {
EVLOG_error << "CableCheck: IMD Self test failed";
fail_cable_check("IMD self test failed during cable check");
return;
}
}
charger->get_stopwatch().mark("Self test");
// CC.4.1.4: Perform the insulation resistance check
imd_start();
if (config.cable_check_wait_number_of_imd_measurements > 0) {
// read out new isolation resistance value
isolation_measurement.clear();
types::isolation_monitor::IsolationMeasurement m;
EVLOG_info << "CableCheck: Waiting for " << config.cable_check_wait_number_of_imd_measurements
<< " isolation measurement sample(s)";
// Wait for N isolation measurement values
for (int i = 0; i < config.cable_check_wait_number_of_imd_measurements; i++) {
if (not isolation_measurement.wait_for(m, 5s) or cable_check_should_exit()) {
imd_stop();
fail_cable_check("CableCheck: Did not receive isolation measurement from IMD within 5 seconds.");
return;
}
}
charger->get_stopwatch().mark("Measure");
// Now the value is valid and can be trusted.
// Verify it is within ranges. Fault is <100 kOhm
// Note that 2023 edition removed the warning level which was included in the 2014 edition.
// Refer to IEC 61851-23 (2023) 6.3.1.105 and CC.4.1.2 / CC.4.1.4
if (not check_isolation_resistance_in_range(m.resistance_F_Ohm)) {
imd_stop();
std::ostringstream oss;
oss << "Isolation resistance too low: " << m.resistance_F_Ohm << " Ohm";
error_handling->raise_isolation_resistance_fault(oss.str(), "Resistance");
fail_cable_check(oss.str());
return;
}
} else {
// If no measurements are needed after self test, report valid isolation status to ISO stack
r_hlc[0]->call_update_isolation_status(types::iso15118::IsolationStatus::Valid);
}
// We are done with the isolation measurement and can now report success to the EV,
// but before we do so we need to do a few things for cleanup
if (config.hack_pause_imd_during_precharge) {
imd_stop();
}
if (config.cable_check_wait_below_60V_before_finish) {
// CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV
powersupply_DC_off();
if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) {
std::ostringstream oss;
oss << "Voltage did not drop below " << CABLECHECK_SAFE_VOLTAGE << "V within timeout.";
imd_stop();
fail_cable_check(oss.str());
return;
}
charger->get_stopwatch().mark("VRampDown");
EVLOG_info << "CableCheck done, output is below " << CABLECHECK_SAFE_VOLTAGE << "V";
}
// Report CableCheck Finished with success to EV
r_hlc[0]->call_cable_check_finished(true);
charger->get_stopwatch().report_phase();
charger->get_stopwatch().mark_phase("PrepareCharging");
});
// Detach thread and exit command handler right away
t.detach();
}
void EvseManager::powersupply_DC_on() {
if (not powersupply_dc_is_on) {
session_log.evse(false, "DC power supply: switch ON called, ChargingPhase: " +
types::power_supply_DC::charging_phase_to_string(power_supply_DC_charging_phase));
r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Export, power_supply_DC_charging_phase);
powersupply_dc_is_on = true;
}
}
// input voltage/current is what the evse/car would like to set.
// if it is more then what the energymanager gave us, we can limit it here.
bool EvseManager::powersupply_DC_set(double _voltage, double _current) {
if (last_power_supply_voltage == _voltage and last_power_supply_current == _current) {
return true;
}
double voltage = _voltage;
double current = _current;
static bool last_is_actually_exporting_to_grid{false};
const bool charging_phase_changed = last_power_supply_DC_charging_phase not_eq power_supply_DC_charging_phase;
last_power_supply_DC_charging_phase = power_supply_DC_charging_phase;
// Some cars always request integer ampere values, so if we offer 14.34A they will request 14.0A.
// On low power DC charging this makes quite a difference
// this option will deliver the offered ampere value in those cases
if (config.hack_fix_hlc_integer_current_requests) {
auto hlc_limits = charger->get_evse_max_hlc_limits();
if (hlc_limits.evse_maximum_current_limit - (int)current < 1.)
current = hlc_limits.evse_maximum_current_limit;
}
if (config.sae_j2847_2_bpt_enabled) {
current = std::abs(current);
}
auto caps = get_powersupply_capabilities();
if (((config.hack_allow_bpt_with_iso2 or sae_bidi_active or session_is_iso_d20_dc_bpt()) and
current_demand_active) and
is_actually_exporting_to_grid) {
if (not last_is_actually_exporting_to_grid and powersupply_dc_is_on) {
// switching from import from grid to export to grid
session_log.evse(false, "DC power supply: switch ON in import mode");
r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Import, power_supply_DC_charging_phase);
}
last_is_actually_exporting_to_grid = is_actually_exporting_to_grid;
// Hack: we are exporting to grid but are in ISO-2 mode
// check limits of supply
if (caps.min_import_voltage_V.has_value() and caps.max_import_voltage_V.has_value() and
voltage >= caps.min_import_voltage_V.value() and voltage <= caps.max_import_voltage_V.value()) {
if (caps.max_import_current_A.has_value() and current > caps.max_import_current_A.value()) {
current = caps.max_import_current_A.value();
}
if (caps.min_import_current_A.has_value() and current < caps.min_import_current_A.value()) {
current = caps.min_import_current_A.value();
}
// Now it is within limits of DC power supply.
// now also limit with the limits given by the energymanager.
// FIXME: dont do this for now, see if the car reduces if we supply new limits.
session_log.evse(false, fmt::format("DC power supply set: {:.2f}V/{:.2f}A, requested was {:.2f}V/{:.2f}A.",
voltage, current, _voltage, _current));
// set the new limits for the DC output
r_powersupply_DC[0]->call_setImportVoltageCurrent(voltage, current);
last_power_supply_voltage = voltage;
last_power_supply_current = current;
return true;
}
EVLOG_critical << fmt::format("DC voltage/current out of limits requested: Voltage {:.2f} Current {:.2f}.",
voltage, current);
return false;
}
if (powersupply_dc_is_on and (charging_phase_changed or (((config.hack_allow_bpt_with_iso2 or sae_bidi_active or
session_is_iso_d20_dc_bpt()) and
last_is_actually_exporting_to_grid) and
current_demand_active))) {
// switching from export to grid to import from grid
session_log.evse(false, "DC power supply: switch ON in export mode");
r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Export, power_supply_DC_charging_phase);
last_is_actually_exporting_to_grid = is_actually_exporting_to_grid;
}
// check limits of supply
if (voltage >= caps.min_export_voltage_V and voltage <= caps.max_export_voltage_V) {
if (current > caps.max_export_current_A)
current = caps.max_export_current_A;
if (current < caps.min_export_current_A)
current = caps.min_export_current_A;
// Now it is within limits of DC power supply.
// now also limit with the limits given by the energymanager.
// FIXME: dont do this for now, see if the car reduces if we supply new limits.
session_log.evse(false, fmt::format("DC power supply set: {:.2f}V/{:.2f}A, requested was {:.2f}V/{:.2f}A.",
voltage, current, _voltage, _current));
// set the new limits for the DC output
r_powersupply_DC[0]->call_setExportVoltageCurrent(voltage, current);
last_power_supply_voltage = voltage;
last_power_supply_current = current;
return true;
}
EVLOG_critical << fmt::format("DC voltage/current out of limits requested: Voltage {:.2f} Current {:.2f}.", voltage,
current);
return false;
}
void EvseManager::powersupply_DC_off() {
if (powersupply_dc_is_on) {
session_log.evse(false, "DC power supply OFF");
r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Off, power_supply_DC_charging_phase);
powersupply_dc_is_on = false;
}
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other;
}
bool EvseManager::wait_powersupply_DC_voltage_reached(double target_voltage) {
// wait until the voltage has rised to the target value
Timeout timeout;
timeout.start(10s);
bool voltage_ok = false;
while (not timeout.reached()) {
if (cable_check_should_exit()) {
EVLOG_warning << "Cancel cable check wait voltage reached";
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other;
powersupply_DC_off();
r_hlc[0]->call_cable_check_finished(false);
break;
}
types::power_supply_DC::VoltageCurrent m;
if (powersupply_measurement.wait_for(m, 2000ms)) {
if (fabs(m.voltage_V - target_voltage) < 10) {
voltage_ok = true;
break;
}
} else {
EVLOG_info << "Did not receive voltage measurement from power supply within 2 seconds.";
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other;
powersupply_DC_off();
break;
}
}
return voltage_ok;
}
bool EvseManager::wait_powersupply_DC_below_voltage(double target_voltage) {
// wait until the voltage is below the target voltage
Timeout timeout;
timeout.start(10s);
bool voltage_ok = false;
while (not timeout.reached()) {
if (cable_check_should_exit()) {
EVLOG_warning << "Cancel cable check wait below voltage";
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other;
powersupply_DC_off();
r_hlc[0]->call_cable_check_finished(false);
break;
}
types::power_supply_DC::VoltageCurrent m;
if (powersupply_measurement.wait_for(m, 2000ms)) {
if (m.voltage_V < target_voltage) {
voltage_ok = true;
break;
}
} else {
EVLOG_info << "Did not receive voltage measurement from power supply within 2 seconds.";
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other;
powersupply_DC_off();
break;
}
}
return voltage_ok;
}
const std::vector<std::unique_ptr<powermeterIntf>>& EvseManager::r_powermeter_billing() {
if (r_powermeter_car_side.size() > 0) {
return r_powermeter_car_side;
} else {
return r_powermeter_grid_side;
}
}
void EvseManager::imd_stop() {
if (not r_imd.empty()) {
r_imd[0]->call_stop();
}
}
void EvseManager::imd_start() {
if (not r_imd.empty()) {
r_imd[0]->call_start();
}
}
// This returns our active local limits, which is either externally set limits
// or hardware capabilties
types::energy::ExternalLimits EvseManager::get_local_energy_limits() {
types::energy::ExternalLimits active_local_limits;
std::scoped_lock lock(external_local_limits_mutex);
// external limits are empty
if (external_local_energy_limits.schedule_import.empty() and external_local_energy_limits.schedule_export.empty()) {
if (config.charge_mode == "AC") {
auto handle = hw_capabilities.handle();
update_max_current_limit(active_local_limits, handle->max_current_A_import, handle->max_current_A_export);
} else {
update_max_watt_limit(active_local_limits, get_powersupply_capabilities().max_export_power_W,
get_powersupply_capabilities().max_import_power_W);
}
} else {
// apply external limits if they are lower
active_local_limits = external_local_energy_limits;
}
// Bidi: set local limits to 0A/0W for exporting to grid, except if bidi is actually active at the moment
if (not(config.hack_allow_bpt_with_iso2 or sae_bidi_active or session_is_iso_d20_dc_bpt() or
session_is_iso_d20_ac_bpt())) {
update_to_zero_discharge_limit(active_local_limits);
}
return active_local_limits;
}
void EvseManager::fail_cable_check(const std::string& reason) {
if (config.charge_mode == "DC") {
power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other;
powersupply_DC_off();
// CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV
if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) {
EVLOG_error << "Voltage did not drop below 60V within timeout, sending CableCheck Finished(false) anyway";
}
r_hlc[0]->call_cable_check_finished(false);
}
// Raising a cable check fault should not happen if:
// - a cancel_transaction (DeAuthorized) is triggered during cable check
// - the car has already been unplugged (Idle/Finished), which prevents a race condition
// where the detached cable check thread raises an error after clear_errors_on_unplug()
// has already run, leaving the charger permanently inoperative (see GitHub issue #1392)
const auto current_state = charger->get_current_state();
const auto last_stop_transaction_reason = charger->get_last_stop_transaction_reason();
if (current_state == Charger::EvseState::Idle || current_state == Charger::EvseState::Finished) {
EVLOG_info << "Cable check failed due to: " << reason
<< ", but session already ended (car unplugged). Not raising cable check fault error.";
} else if (not last_stop_transaction_reason.has_value() or
last_stop_transaction_reason.value() != types::evse_manager::StopTransactionReason::DeAuthorized) {
// Raising the cable check error also causes the HLC stack to get notified
this->error_handling->raise_cable_check_fault(reason);
}
}
types::evse_manager::EVInfo EvseManager::get_ev_info() {
Everest::scoped_lock_timeout l(ev_info_mutex, Everest::MutexDescription::EVSE_get_ev_info);
return ev_info;
}
void EvseManager::process_dc_ev_target_voltage_current(const types::iso15118::DcEvseMaximumLimits& hlc_limits) {
double clamped_voltage = raw_ev_target_voltage.load();
double clamped_current = raw_ev_target_current.load();
// Hack for Skoda Enyaq that should be fixed in a different way
if (config.hack_skoda_enyaq and (clamped_voltage < 300 or clamped_current < 0)) {
return;
}
// Limit voltage/current for broken EV implementations
const auto ev_info_snapshot = get_ev_info();
if (ev_info_snapshot.maximum_current_limit.has_value() and
clamped_current > ev_info_snapshot.maximum_current_limit.value()) {
clamped_current = ev_info_snapshot.maximum_current_limit.value();
}
if (ev_info_snapshot.maximum_voltage_limit.has_value() and
clamped_voltage > ev_info_snapshot.maximum_voltage_limit.value()) {
clamped_voltage = ev_info_snapshot.maximum_voltage_limit.value();
}
bool car_breaks_limit{false};
if (clamped_current > hlc_limits.evse_maximum_current_limit) {
clamped_current = hlc_limits.evse_maximum_current_limit;
car_breaks_limit = true;
}
// ISO15118-20 [V2G20-2183] allows EVs to only provide voltage or current,
// i.e. some EV implementations send target voltage as zero later.
// While this requirement is going to be dropped from the standard, we
// have to support older implementations by using a "cached" latest non-zero
// voltage the EV told us.
// EVs sending a current of zero has not yet been seen, so we don't care
// about this particular case here at the moment.
bool car_sent_zero_voltage{false};
if (almost_eq(clamped_voltage, 0.0) and ev_info.target_voltage.value_or(0.f) > 0.f) {
clamped_voltage = ev_info.target_voltage.value();
car_sent_zero_voltage = true;
}
const auto actual_voltage =
ev_info_snapshot.present_voltage.has_value() ? ev_info_snapshot.present_voltage.value() : clamped_voltage;
const auto target_power = clamped_current * actual_voltage;
if (target_power > hlc_limits.evse_maximum_power_limit) {
clamped_current = hlc_limits.evse_maximum_power_limit / actual_voltage;
car_breaks_limit = true;
}
bool target_changed = false;
if (clamped_voltage not_eq latest_target_voltage or clamped_current not_eq latest_target_current) {
latest_target_voltage = clamped_voltage;
latest_target_current = clamped_current;
target_changed = true;
}
apply_new_target_voltage_current();
if (target_changed) {
if (not contactor_open) {
powersupply_DC_on();
}
if (car_breaks_limit) {
EVLOG_warning << "EV ignores new EVSE max limits. Setting target current to new EVSE max limits";
}
if (car_sent_zero_voltage) {
EVLOG_warning << "EV did not provide a recent voltage. Re-using the previously communicated value again.";
}
{
Everest::scoped_lock_timeout lock(ev_info_mutex, Everest::MutexDescription::EVSE_publish_ev_info);
ev_info.target_voltage = latest_target_voltage;
ev_info.target_current = latest_target_current;
p_evse->publish_ev_info(ev_info);
}
}
}
void EvseManager::apply_new_target_voltage_current() {
if (latest_target_voltage > 0) {
const double time_since_last_update =
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() -
latest_target_current_low_pass_last_update.load())
.count();
if (time_since_last_update > 0) {
double diff_amp = (latest_target_current - latest_target_current_low_pass);
if (diff_amp > time_since_last_update / 1000. * config.dc_ramp_ampere_per_second) {
diff_amp = time_since_last_update / 1000. * config.dc_ramp_ampere_per_second;
latest_target_current_low_pass = latest_target_current_low_pass + diff_amp;
} else {
latest_target_current_low_pass.store(latest_target_current);
}
} else {
latest_target_current_low_pass.store(latest_target_current);
}
latest_target_current_low_pass_last_update.store(std::chrono::steady_clock::now());
powersupply_DC_set(latest_target_voltage, latest_target_current_low_pass);
}
// We allow the EV to adjust the voltage/current for a few seconds
// so we reset the timer on every new target value received.
charger->reset_dc_enforce_target_limits_timer();
}
bool EvseManager::session_is_iso_d20_ac_bpt() {
return selected_d20_energy_service.has_value() &&
selected_d20_energy_service.value() == types::iso15118::ServiceCategory::AC_BPT;
}
bool EvseManager::session_is_iso_d20_dc_bpt() {
return selected_d20_energy_service.has_value() and
selected_d20_energy_service.value() == types::iso15118::ServiceCategory::DC_BPT;
}
types::power_supply_DC::Capabilities EvseManager::get_powersupply_capabilities() {
types::power_supply_DC::Capabilities caps;
types::dc_external_derate::ExternalDerating derate;
{
std::scoped_lock lock(powersupply_capabilities_mutex);
caps = powersupply_capabilities;
}
{
std::scoped_lock lock(dc_external_derate_mutex);
derate = get_dc_external_derate(this->ev_info.present_voltage, dc_external_derate);
}
// Apply external derating if set
caps.max_export_current_A = min_optional(caps.max_export_current_A, derate.max_export_current_A);
caps.max_import_current_A = min_optional(caps.max_import_current_A, derate.max_import_current_A);
caps.max_export_power_W = min_optional(caps.max_export_power_W, derate.max_export_power_W);
caps.max_import_power_W = min_optional(caps.max_import_power_W, derate.max_import_power_W);
return caps;
}
void EvseManager::set_external_derating(types::dc_external_derate::ExternalDerating d) {
std::scoped_lock lock(dc_external_derate_mutex);
dc_external_derate = d;
}
} // namespace module