Files
cariflex/tools/EVerest-main/modules/EVSE/EvseManager/energy_grid/energyImpl.cpp
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

677 lines
34 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "energyImpl.hpp"
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <date/date.h>
#include <date/tz.h>
#include <fmt/core.h>
#include <string>
#include <utils/date.hpp>
#include <utils/formatter.hpp>
namespace module {
namespace energy_grid {
// helper to find out if voltage changed (more then noise)
static bool voltage_changed(float a, float b) {
constexpr float noise_voltage = 1;
return (fabs(a - b) > noise_voltage);
}
void energyImpl::init() {
charger_state = Charger::EvseState::Disabled;
source_base = mod->info.id;
source_bsp_caps = source_base + "/evse_board_support_caps";
source_psu_caps = source_base + "/powersupply_dc_caps";
std::srand((unsigned)time(0));
// UUID must be unique also beyond this charging station -> will be handled on framework level and above later
energy_flow_request.uuid = mod->info.id;
energy_flow_request.node_type = types::energy::NodeType::Evse;
if (mod->r_powermeter_car_side.size()) {
mod->r_powermeter_car_side[0]->subscribe_powermeter([this](types::powermeter::Powermeter p) {
// Received new power meter values, update our energy object.
std::lock_guard<std::mutex> lock(this->energy_mutex);
energy_flow_request.energy_usage_leaves = p;
});
}
if (mod->r_powermeter_grid_side.size()) {
mod->r_powermeter_grid_side[0]->subscribe_powermeter([this](types::powermeter::Powermeter p) {
// Received new power meter values, update our energy object.
std::lock_guard<std::mutex> lock(this->energy_mutex);
energy_flow_request.energy_usage_root = p;
});
}
}
void energyImpl::clear_import_request_schedule() {
types::energy::ScheduleReqEntry entry_import;
const auto tpnow = date::utc_clock::now();
const auto tp =
Everest::Date::to_rfc3339(date::floor<std::chrono::hours>(tpnow) + date::get_leap_second_info(tpnow).elapsed);
entry_import.timestamp = tp;
entry_import.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_import};
entry_import.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_import};
entry_import.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_import};
entry_import.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_import};
entry_import.limits_to_root.ac_supports_changing_phases_during_charging =
hw_caps.supports_changing_phases_during_charging;
entry_import.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;
if (mod->config.charge_mode == "DC") {
// For DC, apply our power supply capabilities as limit on leaves side
const auto caps = mod->get_powersupply_capabilities();
entry_import.limits_to_leaves.total_power_W = {caps.max_export_power_W,
source_base + "/clear_import_request_schedule"};
// Note both sides are optionals
entry_import.conversion_efficiency = caps.conversion_efficiency_export;
}
energy_flow_request.schedule_import = std::vector<types::energy::ScheduleReqEntry>({entry_import});
}
void energyImpl::clear_export_request_schedule() {
types::energy::ScheduleReqEntry entry_export;
const auto tpnow = date::utc_clock::now();
const auto tp =
Everest::Date::to_rfc3339(date::floor<std::chrono::hours>(tpnow) + date::get_leap_second_info(tpnow).elapsed);
entry_export.timestamp = tp;
entry_export.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_export, source_bsp_caps};
entry_export.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_export, source_bsp_caps};
entry_export.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_export, source_bsp_caps};
entry_export.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_export, source_bsp_caps};
entry_export.limits_to_root.ac_supports_changing_phases_during_charging =
hw_caps.supports_changing_phases_during_charging;
entry_export.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;
if (mod->config.charge_mode == "DC") {
// For DC, apply our power supply capabilities as limit on leaves side
const auto caps = mod->get_powersupply_capabilities();
entry_export.limits_to_leaves.total_power_W = {caps.max_import_power_W.value_or(0), source_psu_caps};
// Note that both sides are optionals
entry_export.conversion_efficiency = caps.conversion_efficiency_import;
}
energy_flow_request.schedule_export = std::vector<types::energy::ScheduleReqEntry>({entry_export});
}
void energyImpl::clear_request_schedules() {
this->clear_import_request_schedule();
this->clear_export_request_schedule();
}
void energyImpl::ready() {
hw_caps = mod->get_hw_capabilities();
last_powersupply_capabilities = mod->get_powersupply_capabilities();
clear_request_schedules();
// request energy now
request_energy_from_energy_manager(true);
// request energy every second
std::thread([this] {
while (true) {
hw_caps = mod->get_hw_capabilities();
request_energy_from_energy_manager(false);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}).detach();
// request energy at the start and end of a charging session
mod->charger->signal_state.connect([this](Charger::EvseState s) {
charger_state = s;
if (s == Charger::EvseState::WaitingForAuthentication || s == Charger::EvseState::Finished) {
std::thread request_energy_thread([this]() { request_energy_from_energy_manager(true); });
request_energy_thread.detach();
}
});
}
types::energy::EvseState to_energy_evse_state(const Charger::EvseState charger_state) {
switch (charger_state) {
case Charger::EvseState::Disabled:
return types::energy::EvseState::Disabled;
break;
case Charger::EvseState::Idle:
return types::energy::EvseState::Unplugged;
break;
case Charger::EvseState::WaitingForAuthentication:
return types::energy::EvseState::WaitForAuth;
break;
case Charger::EvseState::PrepareCharging:
return types::energy::EvseState::PrepareCharging;
break;
case Charger::EvseState::Charging:
return types::energy::EvseState::Charging;
break;
case Charger::EvseState::ChargingPausedEV:
return types::energy::EvseState::PausedEV;
break;
case Charger::EvseState::ChargingPausedEVSE:
return types::energy::EvseState::PausedEVSE;
break;
case Charger::EvseState::StoppingCharging:
return types::energy::EvseState::Finished;
break;
case Charger::EvseState::Finished:
return types::energy::EvseState::Finished;
break;
case Charger::EvseState::T_step_EF:
return types::energy::EvseState::PrepareCharging;
break;
case Charger::EvseState::T_step_X1:
return types::energy::EvseState::PrepareCharging;
break;
case Charger::EvseState::SwitchPhases:
return types::energy::EvseState::Charging;
break;
}
return types::energy::EvseState::Disabled;
}
void energyImpl::request_energy_from_energy_manager(bool priority_request) {
std::lock_guard<std::mutex> lock(this->energy_mutex);
clear_import_request_schedule();
clear_export_request_schedule();
// If we need energy, copy local limit schedules to energy_flow_request.
if (charger_state == Charger::EvseState::Charging || charger_state == Charger::EvseState::PrepareCharging ||
charger_state == Charger::EvseState::WaitingForAuthentication ||
charger_state == Charger::EvseState::ChargingPausedEV || !mod->config.request_zero_power_in_idle) {
// copy complete external limit schedules for import
if (not mod->get_local_energy_limits().schedule_import.empty()) {
energy_flow_request.schedule_import = mod->get_local_energy_limits().schedule_import;
if (mod->config.charge_mode == "DC") {
// For DC, apply our power supply capabilities as an additional limit on leaves side
const auto caps = mod->get_powersupply_capabilities();
for (auto& entry : energy_flow_request.schedule_import) {
// Apply caps limit if we request a leaves side watt value and it is larger than the capabilities
if (entry.limits_to_leaves.total_power_W.has_value() and
entry.limits_to_leaves.total_power_W.value().value > caps.max_export_power_W) {
entry.limits_to_leaves.total_power_W = {caps.max_export_power_W, source_psu_caps};
}
}
}
}
// apply our local hardware limits on root side
for (auto& e : energy_flow_request.schedule_import) {
if (!e.limits_to_root.ac_max_current_A.has_value() ||
e.limits_to_root.ac_max_current_A.value().value > hw_caps.max_current_A_import) {
e.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_import, source_bsp_caps};
// are we in EV pause mode? -> Reduce requested current to minimum just to see when car
// wants to start charging again. The energy manager may pause us externally to reduce to
// zero
if (charger_state == Charger::EvseState::ChargingPausedEV && mod->config.request_zero_power_in_idle) {
e.limits_to_root.ac_max_current_A = {hw_caps.min_current_A_import, source_bsp_caps};
}
}
if (!e.limits_to_root.ac_max_phase_count.has_value() ||
e.limits_to_root.ac_max_phase_count.value().value > hw_caps.max_phase_count_import)
e.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_import, source_bsp_caps};
// copy remaining hw limits on root side
e.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_import, source_bsp_caps};
e.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_import, source_bsp_caps};
e.limits_to_root.ac_supports_changing_phases_during_charging =
hw_caps.supports_changing_phases_during_charging;
e.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;
}
// copy complete external limit schedules for export
if (not mod->get_local_energy_limits().schedule_export.empty()) {
energy_flow_request.schedule_export = mod->get_local_energy_limits().schedule_export;
if (mod->config.charge_mode == "DC") {
// For DC, apply our power supply capabilities as an additional limit on leaves side
const auto caps = mod->get_powersupply_capabilities();
for (auto& entry : energy_flow_request.schedule_export) {
if (entry.limits_to_leaves.total_power_W.has_value() and caps.max_import_power_W.has_value() and
entry.limits_to_leaves.total_power_W.value().value > caps.max_import_power_W.value()) {
entry.limits_to_leaves.total_power_W = {caps.max_import_power_W.value(), source_bsp_caps};
}
}
}
}
// apply our local hardware limits on root side
for (auto& e : energy_flow_request.schedule_export) {
if (!e.limits_to_root.ac_max_current_A.has_value() ||
e.limits_to_root.ac_max_current_A.value().value > hw_caps.max_current_A_export) {
e.limits_to_root.ac_max_current_A = {hw_caps.max_current_A_export, source_bsp_caps};
// are we in EV pause mode? -> Reduce requested current to minimum just to see when car
// wants to start discharging again. The energy manager may pause us externally to reduce to
// zero
if (charger_state == Charger::EvseState::ChargingPausedEV) {
e.limits_to_root.ac_max_current_A = {hw_caps.min_current_A_export, source_bsp_caps + "_pause"};
}
}
if (!e.limits_to_root.ac_max_phase_count.has_value() ||
e.limits_to_root.ac_max_phase_count.value().value > hw_caps.max_phase_count_export)
e.limits_to_root.ac_max_phase_count = {hw_caps.max_phase_count_export, source_bsp_caps};
// copy remaining hw limits on root side
e.limits_to_root.ac_min_phase_count = {hw_caps.min_phase_count_export, source_bsp_caps};
e.limits_to_root.ac_min_current_A = {hw_caps.min_current_A_export, source_bsp_caps};
e.limits_to_root.ac_supports_changing_phases_during_charging =
hw_caps.supports_changing_phases_during_charging;
e.limits_to_root.ac_number_of_active_phases = mod->ac_nr_phases_active;
}
// Cap by PP cable rating so the EnergyManager cannot allocate more than the cable allows
if (mod->config.charge_mode == "AC") {
const auto pp_rating = mod->bsp->read_pp_ampacity();
if (pp_rating) {
const std::string source_pp = source_base + "/pp_ampacity";
for (auto& e : energy_flow_request.schedule_import) {
if (e.limits_to_root.ac_max_current_A.has_value() &&
e.limits_to_root.ac_max_current_A.value().value > pp_rating.value()) {
e.limits_to_root.ac_max_current_A = {static_cast<float>(pp_rating.value()), source_pp};
}
}
}
}
if (mod->config.charge_mode == "DC") {
// For DC mode remove amp limit on leave side if any
for (auto& e : energy_flow_request.schedule_import) {
e.limits_to_leaves.ac_max_current_A.reset();
}
for (auto& e : energy_flow_request.schedule_export) {
e.limits_to_leaves.ac_max_current_A.reset();
}
}
} else {
if (mod->config.charge_mode == "DC") {
// we dont need power at the moment
energy_flow_request.schedule_import[0].limits_to_leaves.total_power_W = {0., "Idle"};
energy_flow_request.schedule_export[0].limits_to_leaves.total_power_W = {0., "Idle"};
} else {
energy_flow_request.schedule_import[0].limits_to_leaves.ac_max_current_A = {0., "Idle"};
energy_flow_request.schedule_export[0].limits_to_leaves.ac_max_current_A = {0., "Idle"};
}
}
if (priority_request) {
energy_flow_request.priority_request = true;
} else {
energy_flow_request.priority_request = false;
}
// Attach our state
energy_flow_request.evse_state = to_energy_evse_state(charger_state);
publish_energy_flow_request(energy_flow_request);
// EVLOG_info << "Outgoing request " << energy_flow_request;
}
static bool almost_eq(float a, float b) {
return a > b - 0.1 and a < b + 0.1;
}
static bool almost_eq(std::optional<float> const& a, std::optional<float> const& b) {
if (a.has_value() and b.has_value()) {
return almost_eq(a.value(), b.value());
}
if (not a.has_value() and not b.has_value()) {
return true;
}
return false;
}
static bool almost_eq(types::power_supply_DC::Capabilities const& lhs,
types::power_supply_DC::Capabilities const& rhs) {
bool result = lhs.bidirectional == rhs.bidirectional and
almost_eq(lhs.current_regulation_tolerance_A, rhs.current_regulation_tolerance_A) and
almost_eq(lhs.peak_current_ripple_A, rhs.peak_current_ripple_A) and
almost_eq(lhs.max_export_voltage_V, rhs.max_export_voltage_V) and
almost_eq(lhs.min_export_voltage_V, rhs.min_export_voltage_V) and
almost_eq(lhs.max_export_current_A, rhs.max_export_current_A) and
almost_eq(lhs.min_export_current_A, rhs.min_export_current_A) and
almost_eq(lhs.max_export_power_W, rhs.max_export_power_W) and
almost_eq(lhs.max_import_voltage_V, rhs.max_import_voltage_V) and
almost_eq(lhs.min_import_voltage_V, rhs.min_import_voltage_V) and
almost_eq(lhs.max_import_current_A, rhs.max_import_current_A) and
almost_eq(lhs.min_import_current_A, rhs.min_import_current_A) and
almost_eq(lhs.max_import_power_W, rhs.max_import_power_W) and
almost_eq(lhs.conversion_efficiency_import, rhs.conversion_efficiency_import) and
almost_eq(lhs.conversion_efficiency_export, rhs.conversion_efficiency_export);
return result;
}
// This is the decision logic when limits are changing.
bool energyImpl::random_delay_needed(float last_limit, float limit) {
if (mod->config.uk_smartcharging_random_delay_at_any_change) {
if (not almost_eq(last_limit, limit)) {
return true;
}
} else {
if (almost_eq(last_limit, 0.) and limit > 0.1) {
return true;
} else if (last_limit > 0.1 and almost_eq(limit, 0.)) {
return true;
}
}
// Are we starting up with a car attached? This will need a random delay as well
if ((charger_state == Charger::EvseState::PrepareCharging or charger_state == Charger::EvseState::Charging or
charger_state == Charger::EvseState::WaitingForAuthentication) and
std::chrono::steady_clock::now() - mod->timepoint_ready_for_charging.load() <
detect_startup_with_ev_attached_duration) {
last_enforced_limit = 0.;
return true;
}
return false;
}
void energyImpl::handle_enforce_limits(types::energy::EnforcedLimits& value) {
if (value.uuid == energy_flow_request.uuid) {
// EVLOG_info << "Incoming enforce limits" << value;
// set hardware limit
float limit = 0.;
int active_phasecount = mod->ac_nr_phases_active;
// apply enforced limits
// set enforced AC current limit
if (value.limits_root_side.ac_max_current_A.has_value()) {
limit = value.limits_root_side.ac_max_current_A.value().value;
}
// apply number of phase limit
if (value.limits_root_side.ac_max_phase_count.has_value() &&
value.limits_root_side.ac_max_phase_count.value().value not_eq active_phasecount) {
if (mod->get_hw_capabilities().supports_changing_phases_during_charging) {
if (mod->charger->switch_three_phases_while_charging(
value.limits_root_side.ac_max_phase_count.value().value == 3)) {
mod->ac_nr_phases_active = value.limits_root_side.ac_max_phase_count.value().value;
EVLOG_info << fmt::format("3ph/1ph: Switching #ph from {} to {}", active_phasecount,
value.limits_root_side.ac_max_phase_count.value().value);
} else {
EVLOG_warning << fmt::format(
"3ph/1ph: Energymanager requests switching #ph from {} to {}, ignored.", active_phasecount,
value.limits_root_side.ac_max_phase_count.value().value);
}
} else {
EVLOG_error << fmt::format(
"Energy manager requests switching #ph from {} to {}, but switching phases during "
"charging is not supported by HW.",
active_phasecount, value.limits_root_side.ac_max_phase_count.value().value);
}
}
// apply watt limit
if (value.limits_root_side.total_power_W.has_value()) {
mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/max_watt", mod->config.connector_id),
value.limits_root_side.total_power_W.value().value);
// watt limit converted to current limit
const float current_limit_power = value.limits_root_side.total_power_W.value().value /
mod->config.ac_nominal_voltage / mod->ac_nr_phases_active;
if ((limit >= 0 and limit > current_limit_power) or (limit < 0 and limit < current_limit_power)) {
limit = current_limit_power;
}
}
const auto enforced_limit = limit;
// check if we need to add a random delay for UK smart charging regs
if (mod->random_delay_enabled) {
// Are we in a state where a random delay makes sense?
if (not(charger_state == Charger::EvseState::PrepareCharging or
charger_state == Charger::EvseState::Charging or
charger_state == Charger::EvseState::WaitingForAuthentication)) {
mod->random_delay_running = false;
}
// Do we need to start a new random delay?
// Ignore changes of less then 0.1 amps
if (not mod->random_delay_running and random_delay_needed(last_enforced_limit, limit)) {
mod->random_delay_running = true;
mod->random_delay_start_time = date::utc_clock::now();
auto random_delay_s = std::rand() % mod->random_delay_max_duration.load().count();
mod->random_delay_end_time = std::chrono::steady_clock::now() + std::chrono::seconds(random_delay_s);
EVLOG_info << "UK Smart Charging regulations: Starting random delay of " << random_delay_s << "s";
limit_when_random_delay_started = last_enforced_limit;
}
// If a delay is running, replace the current limit with the stored value
if (mod->random_delay_running) {
// use limit from the time point when the random delay started
limit = limit_when_random_delay_started;
// publish the current random delay timer
auto seconds_left = std::chrono::duration_cast<std::chrono::seconds>(mod->random_delay_end_time -
std::chrono::steady_clock::now())
.count();
types::uk_random_delay::CountDown c;
c.current_limit_after_delay_A = enforced_limit;
c.current_limit_during_delay_A = limit_when_random_delay_started;
if (seconds_left <= 0) {
EVLOG_info << "UK Smart Charging regulations: Random delay elapsed.";
c.countdown_s = 0;
mod->random_delay_running = false;
} else {
EVLOG_debug << "Random delay running, " << seconds_left
<< "s left. Applying the limit before the random delay ("
<< limit_when_random_delay_started << "A) instead of requested limit ("
<< enforced_limit << "A)";
c.countdown_s = seconds_left;
c.start_time = Everest::Date::to_rfc3339(mod->random_delay_start_time);
}
mod->p_random_delay->publish_countdown(c);
} else {
types::uk_random_delay::CountDown c;
c.countdown_s = 0;
c.current_limit_after_delay_A = enforced_limit;
c.current_limit_during_delay_A = limit_when_random_delay_started;
mod->p_random_delay->publish_countdown(c);
}
}
last_enforced_limit = enforced_limit;
// publish for e.g. OCPP module with the updated limit
if (value.limits_root_side.ac_max_current_A) {
// update based on the PP cable rating
const auto pp_rating = mod->bsp->read_pp_ampacity();
if (pp_rating) {
types::energy::NumberWithSource nws_updated{std::min(limit, static_cast<float>(pp_rating.value())),
value.limits_root_side.ac_max_current_A.value().source};
value.limits_root_side.ac_max_current_A = std::move(nws_updated);
}
}
mod->p_evse->publish_enforced_limits(value);
// update limit at the charger
const auto valid_until = steady_clock::now() + seconds(value.valid_for);
if (limit >= 0) {
// import
mod->charger->set_max_current(limit, valid_until);
} else {
// export
if (mod->session_is_iso_d20_ac_bpt()) {
mod->charger->set_max_current(limit, valid_until);
} else {
// FIXME: we cannot discharge on PWM charging or with -2, so we fake a charging current here.
mod->charger->set_max_current(0, valid_until);
}
}
mod->signalNrOfPhasesAvailable(mod->ac_nr_phases_active);
if (mod->config.charge_mode == "DC") {
// DC mode apply limit at the leave side, we get root side limits here from EnergyManager on ACDC!
// FIXME: multiply by conversion_efficiency here!
if (value.limits_root_side.total_power_W.has_value() and
value.limits_root_side.ac_max_current_A.has_value()) {
float watt_leave_side = value.limits_root_side.total_power_W.value().value;
float ampere_root_side = value.limits_root_side.ac_max_current_A.value().value;
auto ev_info = mod->get_ev_info();
float target_voltage = ev_info.target_voltage.value_or(0.);
float actual_voltage = ev_info.present_voltage.value_or(0.);
bool values_changed = true;
auto powersupply_capabilities = mod->get_powersupply_capabilities();
// did the values change since the last call?
if (almost_eq(last_enforced_limits_watt, watt_leave_side) and
almost_eq(last_enforced_limits_ampere, ampere_root_side) and
almost_eq(target_voltage, last_target_voltage) and
not voltage_changed(actual_voltage, last_actual_voltage) and
almost_eq(powersupply_capabilities, last_powersupply_capabilities)) {
values_changed = false;
}
if (values_changed) {
last_enforced_limits_ampere = ampere_root_side;
last_enforced_limits_watt = watt_leave_side;
last_target_voltage = target_voltage;
last_actual_voltage = actual_voltage;
last_powersupply_capabilities = powersupply_capabilities;
// tell car our new limits
types::iso15118::DcEvseMaximumLimits evse_max_limits;
types::iso15118::DcEvseMinimumLimits evse_min_limits;
// Current Limits (min & max)
evse_max_limits.evse_maximum_current_limit = powersupply_capabilities.max_export_current_A;
evse_max_limits.evse_maximum_discharge_current_limit =
powersupply_capabilities.max_import_current_A;
evse_min_limits.evse_minimum_current_limit = powersupply_capabilities.min_export_current_A;
evse_min_limits.evse_minimum_discharge_current_limit =
powersupply_capabilities.min_import_current_A;
float total_current{0.0};
if (target_voltage > 10) {
// we use target_voltage here to calculate current limit.
// If target_voltage is a lot higher then the actual voltage the
// current limit is too low, i.e. charging will not reach the actual watt value.
// FIXME: we could use some magic here that involves actual measured voltage as well.
if (actual_voltage > 10) {
total_current = watt_leave_side / actual_voltage;
} else {
total_current = watt_leave_side / target_voltage;
}
} else {
total_current = powersupply_capabilities.max_export_current_A;
}
if (total_current >= 0.0) {
evse_max_limits.evse_maximum_current_limit =
std::min(total_current, powersupply_capabilities.max_export_current_A);
} else {
evse_max_limits.evse_maximum_discharge_current_limit.emplace(std::fabs(total_current));
if (powersupply_capabilities.max_import_current_A.has_value() and
std::fabs(total_current) > powersupply_capabilities.max_import_current_A.value()) {
evse_max_limits.evse_maximum_discharge_current_limit =
powersupply_capabilities.max_import_current_A;
}
}
// Power limits (min & max)
evse_max_limits.evse_maximum_power_limit = powersupply_capabilities.max_export_power_W;
evse_max_limits.evse_maximum_discharge_power_limit = powersupply_capabilities.max_import_power_W;
evse_min_limits.evse_minimum_power_limit =
powersupply_capabilities.min_export_voltage_V * powersupply_capabilities.min_export_current_A;
evse_min_limits.evse_minimum_discharge_power_limit =
powersupply_capabilities.min_import_voltage_V.value_or(0.0) *
powersupply_capabilities.min_import_current_A.value_or(0.0);
if (watt_leave_side >= 0) {
evse_max_limits.evse_maximum_power_limit =
std::min(watt_leave_side, powersupply_capabilities.max_export_power_W);
} else {
evse_max_limits.evse_maximum_discharge_power_limit.emplace(std::fabs(watt_leave_side));
if (powersupply_capabilities.max_import_power_W.has_value() and
std::fabs(watt_leave_side) > powersupply_capabilities.max_import_power_W.value()) {
evse_max_limits.evse_maximum_discharge_power_limit =
powersupply_capabilities.max_import_power_W;
}
}
// Voltage limits (min & max)
evse_max_limits.evse_maximum_voltage_limit = powersupply_capabilities.max_export_voltage_V;
evse_min_limits.evse_minimum_voltage_limit = powersupply_capabilities.min_export_voltage_V;
// FIXME: we tell the ISO stack positive numbers for DIN spec and ISO-2 here in case of exporting to
// grid. This needs to be fixed in the transition to -20 for BPT.
mod->is_actually_exporting_to_grid = false;
if (watt_leave_side < 0 and total_current < 0 and
evse_max_limits.evse_maximum_discharge_power_limit.has_value() and
evse_max_limits.evse_maximum_discharge_current_limit.has_value()) {
// we are exporting power back to the grid
if (mod->config.hack_allow_bpt_with_iso2) {
evse_max_limits.evse_maximum_power_limit =
evse_max_limits.evse_maximum_discharge_power_limit.value();
evse_max_limits.evse_maximum_current_limit =
evse_max_limits.evse_maximum_discharge_current_limit.value();
mod->is_actually_exporting_to_grid = true;
} else if (mod->sae_bidi_active) {
evse_max_limits.evse_maximum_power_limit =
-evse_max_limits.evse_maximum_discharge_power_limit.value();
evse_max_limits.evse_maximum_current_limit =
-evse_max_limits.evse_maximum_discharge_current_limit.value();
mod->is_actually_exporting_to_grid = true;
} else if (mod->session_is_iso_d20_dc_bpt()) {
mod->is_actually_exporting_to_grid = true;
} else {
EVLOG_error << "Bidirectional export back to grid requested, but not supported.";
evse_max_limits.evse_maximum_power_limit = 0.;
evse_max_limits.evse_maximum_current_limit = 0.;
evse_max_limits.evse_maximum_discharge_power_limit = 0.;
evse_max_limits.evse_maximum_discharge_current_limit = 0.;
}
}
session_log.evse(
true, fmt::format(
"Change HLC Limits: {}W/{}A, target_voltage {}, actual_voltage {}, bpt_active {}",
evse_max_limits.evse_maximum_power_limit, evse_max_limits.evse_maximum_current_limit,
target_voltage, actual_voltage, mod->is_actually_exporting_to_grid));
mod->r_hlc[0]->call_update_dc_maximum_limits(evse_max_limits);
mod->r_hlc[0]->call_update_dc_minimum_limits(evse_min_limits);
mod->charger->inform_new_evse_max_hlc_limits(evse_max_limits);
mod->charger->inform_new_evse_min_hlc_limits(evse_min_limits);
// This is just neccessary to switch between charging and discharging
if (target_voltage > 0) {
mod->apply_new_target_voltage_current();
}
// Note: If the limits are lower then before, we could tell the DC power supply to
// ramp down already here instead of waiting for the car to request less power.
// Some cars may not like it, so we wait for the car to request less for now.
}
}
}
}
}
} // namespace energy_grid
} // namespace module