Skip to content

Commit

Permalink
Feature/random delays (#530)
Browse files Browse the repository at this point in the history
 Add random delay according to UK smart charging regulations
to EvseManager and API modules. The feature is disabled by default. It can be controlled
at runtime using the random_delay implementation.
Includes changes required by changed type of composite schedules in libocpp from ChargingSchedule
to EnhancedChargingSchedule, which includes stackLevel in EnhancedChargingSchedulePeriods

Signed-off-by: Cornelius Claussen <[email protected]>
Co-authored-by: Kai Hermann <[email protected]>
Co-authored-by: James Chapman <[email protected]>
  • Loading branch information
3 people authored Mar 14, 2024
1 parent 8cc7da8 commit b172bb0
Show file tree
Hide file tree
Showing 20 changed files with 549 additions and 110 deletions.
9 changes: 8 additions & 1 deletion config/config-sil-energy-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ active_modules:
ac_hlc_enabled: true
ac_hlc_use_5percent: false
ac_enforce_hlc: false
request_zero_power_in_idle: false
uk_smartcharging_random_delay_at_any_change: false
uk_smartcharging_random_delay_max_duration: 100
uk_smartcharging_random_delay_enable: true
connections:
bsp:
- module_id: yeti_driver_1
Expand Down Expand Up @@ -148,7 +152,7 @@ active_modules:
config_module:
schedule_total_duration: 2
schedule_interval_duration: 15
debug: true
debug: false
connections:
energy_trunk:
- module_id: grid_connection_point
Expand All @@ -174,4 +178,7 @@ active_modules:
evse_manager:
- module_id: evse_manager_1
implementation_id: evse
random_delay:
- module_id: evse_manager_1
implementation_id: random_delay
x-module-layout: {}
24 changes: 24 additions & 0 deletions interfaces/uk_random_delay.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
description: >-
This interface provides functions for a random delay feature as required by the UK smart charging regulations
The logic whether to use a random delay or not is not included in EvseManager, a different module can use this
interface to enable/disable the feature during runtime and cancel a running random delay.
This always applies to all connectors of this EVSE.
By default, on start up, random delays are disabled.
cmds:
enable:
description: Call to enable the random delay feature
disable:
description: Call to disable the random delay feature
cancel:
description: Cancels a running random delay. The effect is the same as if the time expired just now.
set_duration_s:
description: Set the maximum duration of the random delay. Default is 600 seconds.
arguments:
value:
description: Maximum duration in seconds
type: integer
vars:
countdown:
description: Countdown of the currently running random delay
type: object
$ref: /uk_random_delay#/CountDown
90 changes: 79 additions & 11 deletions modules/API/API.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ SessionInfo::SessionInfo() :
end_energy_export_wh(0) {
this->start_time_point = date::utc_clock::now();
this->end_time_point = this->start_time_point;

uk_random_delay_remaining.countdown_s = 0;
uk_random_delay_remaining.current_limit_after_delay_A = 0.;
uk_random_delay_remaining.current_limit_during_delay_A = 0;
}

bool SessionInfo::is_state_charging(const SessionInfo::State current_state) {
Expand Down Expand Up @@ -190,6 +194,11 @@ void SessionInfo::set_latest_total_w(double latest_total_w) {
this->latest_total_w = latest_total_w;
}

void SessionInfo::set_uk_random_delay_remaining(const types::uk_random_delay::CountDown& cd) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->uk_random_delay_remaining = cd;
}

static void to_json(json& j, const SessionInfo::Error& e) {
j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}};
}
Expand All @@ -215,6 +224,14 @@ SessionInfo::operator std::string() {
{"latest_total_w", this->latest_total_w},
{"charging_duration_s", charging_duration_s.count()},
{"datetime", Everest::Date::to_rfc3339(now)}});
if (uk_random_delay_remaining.countdown_s > 0) {
json random_delay =
json::object({{"remaining_s", uk_random_delay_remaining.countdown_s},
{"current_limit_after_delay_A", uk_random_delay_remaining.current_limit_after_delay_A},
{"current_limit_during_delay_A", uk_random_delay_remaining.current_limit_during_delay_A},
{"start_time", uk_random_delay_remaining.start_time.value_or("")}});
session_info["uk_random_delay"] = random_delay;
}

return session_info.dump();
}
Expand Down Expand Up @@ -423,18 +440,61 @@ void API::init() {
}
evse->call_force_unlock(connector_id); //
});

// Check if a uk_random_delay is connected that matches this evse_manager
for (auto& random_delay : this->r_random_delay) {
if (random_delay->module_id == evse->module_id) {

random_delay->subscribe_countdown([&session_info](const types::uk_random_delay::CountDown& s) {
session_info->set_uk_random_delay_remaining(s);
});

std::string cmd_uk_random_delay = cmd_base + "uk_random_delay";
this->mqtt.subscribe(cmd_uk_random_delay, [&random_delay](const std::string& data) {
if (data == "enable") {
random_delay->call_enable();
} else if (data == "disable") {
random_delay->call_disable();
} else if (data == "cancel") {
random_delay->call_cancel();
}
});

std::string uk_random_delay_set_max_duration_s = cmd_base + "uk_random_delay_set_max_duration_s";
this->mqtt.subscribe(uk_random_delay_set_max_duration_s, [&random_delay](const std::string& data) {
int seconds = 600;
try {
seconds = std::stoi(data);
} catch (const std::exception& e) {
EVLOG_error
<< "Could not parse connector duration value for uk_random_delay_set_max_duration_s: "
<< e.what();
}
random_delay->call_set_duration_s(seconds);
});
}
}
}

std::string var_ocpp_connection_status = api_base + "ocpp/var/connection_status";
std::string var_ocpp_schedule = api_base + "ocpp/var/charging_schedules";

if (this->r_ocpp.size() == 1) {

this->r_ocpp.at(0)->subscribe_is_connected([this](bool is_connected) {
std::scoped_lock lock(ocpp_data_mutex);
if (is_connected) {
this->ocpp_connection_status = "connected";
} else {
this->ocpp_connection_status = "disconnected";
}
});

this->r_ocpp.at(0)->subscribe_charging_schedules([this, &var_ocpp_schedule](json schedule) {
std::scoped_lock lock(ocpp_data_mutex);
this->ocpp_charging_schedule = schedule;
this->ocpp_charging_schedule_updated = true;
});
}

std::string var_info = api_base + "info/var/info";
Expand All @@ -450,18 +510,26 @@ void API::init() {
}
}

this->api_threads.push_back(std::thread([this, var_connectors, connectors, var_info, var_ocpp_connection_status]() {
auto next_tick = std::chrono::steady_clock::now();
while (this->running) {
json connectors_array = connectors;
this->mqtt.publish(var_connectors, connectors_array.dump());
this->mqtt.publish(var_info, this->charger_information.dump());
this->mqtt.publish(var_ocpp_connection_status, this->ocpp_connection_status);
this->api_threads.push_back(
std::thread([this, var_connectors, connectors, var_info, var_ocpp_connection_status, var_ocpp_schedule]() {
auto next_tick = std::chrono::steady_clock::now();
while (this->running) {
json connectors_array = connectors;
this->mqtt.publish(var_connectors, connectors_array.dump());
this->mqtt.publish(var_info, this->charger_information.dump());
{
std::scoped_lock lock(ocpp_data_mutex);
this->mqtt.publish(var_ocpp_connection_status, this->ocpp_connection_status);
if (this->ocpp_charging_schedule_updated) {
this->ocpp_charging_schedule_updated = false;
this->mqtt.publish(var_ocpp_schedule, ocpp_charging_schedule.dump());
}
}

next_tick += NOTIFICATION_PERIOD;
std::this_thread::sleep_until(next_tick);
}
}));
next_tick += NOTIFICATION_PERIOD;
std::this_thread::sleep_until(next_tick);
}
}));
}

void API::ready() {
Expand Down
20 changes: 15 additions & 5 deletions modules/API/API.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// headers for required interface implementations
#include <generated/interfaces/evse_manager/Interface.hpp>
#include <generated/interfaces/ocpp/Interface.hpp>
#include <generated/interfaces/uk_random_delay/Interface.hpp>

// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
Expand Down Expand Up @@ -57,6 +58,7 @@ class SessionInfo {
void set_end_energy_export_wh(int32_t end_energy_export_wh);
void set_latest_energy_export_wh(int32_t latest_export_energy_wh);
void set_latest_total_w(double latest_total_w);
void set_uk_random_delay_remaining(const types::uk_random_delay::CountDown& c);

/// \brief Converts this struct into a serialized json object
operator std::string();
Expand All @@ -70,9 +72,11 @@ class SessionInfo {
int32_t end_energy_import_wh; ///< Energy reading (import) at the end of this charging session in Wh
int32_t start_energy_export_wh; ///< Energy reading (export) at the beginning of this charging session in Wh
int32_t end_energy_export_wh; ///< Energy reading (export) at the end of this charging session in Wh
std::chrono::time_point<date::utc_clock> start_time_point; ///< Start of the charging session
std::chrono::time_point<date::utc_clock> end_time_point; ///< End of the charging session
double latest_total_w; ///< Latest total power reading in W
types::uk_random_delay::CountDown uk_random_delay_remaining; ///< Remaining time of a UK smart charging regs
///< delay. Set to 0 if no delay is active
std::chrono::time_point<date::utc_clock> start_time_point; ///< Start of the charging session
std::chrono::time_point<date::utc_clock> end_time_point; ///< End of the charging session
double latest_total_w; ///< Latest total power reading in W

enum class State {
Unknown,
Expand Down Expand Up @@ -142,18 +146,20 @@ class API : public Everest::ModuleBase {
API() = delete;
API(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr<emptyImplBase> p_main,
std::vector<std::unique_ptr<evse_managerIntf>> r_evse_manager, std::vector<std::unique_ptr<ocppIntf>> r_ocpp,
Conf& config) :
std::vector<std::unique_ptr<uk_random_delayIntf>> r_random_delay, Conf& config) :
ModuleBase(info),
mqtt(mqtt_provider),
p_main(std::move(p_main)),
r_evse_manager(std::move(r_evse_manager)),
r_ocpp(std::move(r_ocpp)),
r_random_delay(std::move(r_random_delay)),
config(config){};

Everest::MqttProvider& mqtt;
const std::unique_ptr<emptyImplBase> p_main;
const std::vector<std::unique_ptr<evse_managerIntf>> r_evse_manager;
const std::vector<std::unique_ptr<ocppIntf>> r_ocpp;
const std::vector<std::unique_ptr<uk_random_delayIntf>> r_random_delay;
const Conf& config;

// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
Expand All @@ -179,8 +185,12 @@ class API : public Everest::ModuleBase {
std::list<std::string> hw_capabilities_str;
std::string selected_protocol;
json charger_information;
std::string ocpp_connection_status = "unknown";
std::unique_ptr<LimitDecimalPlaces> limit_decimal_places;

std::mutex ocpp_data_mutex;
json ocpp_charging_schedule;
bool ocpp_charging_schedule_updated = false;
std::string ocpp_connection_status = "unknown";
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};

Expand Down
15 changes: 14 additions & 1 deletion modules/API/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ This variable is published every second and contains a json object with informat
"latest_total_w": 0.0,
"state": "Unplugged",
"active_permanent_faults": [],
"active_errors": []
"active_errors": [],
"uk_random_delay": {
"remaining_s": 34,
"current_limit_after_delay_A": 16.0,
"current_limit_during_delay_A": 0.0,
"start_time": "2024-02-28T14:11:11.129Z"
}
}
```

Expand Down Expand Up @@ -80,6 +86,7 @@ Example with permanent faults being active:
- **datetime** contains a string representation of the current UTC datetime in RFC3339 format
- **discharged_energy_wh** contains the energy fed into the power grid by the EV in Wh
- **latest_total_w** contains the latest total power reading over all phases in Watt
- **uk_random_delay_remaining_s** Remaining time of a currently active random delay according to UK smart charging regulations. Not set if no delay is active.
- **state** contains the current state of the charging session, from a list of the following possible states:
- Unplugged
- Disabled
Expand Down Expand Up @@ -219,3 +226,9 @@ Command to set a watt limit for this EVSE that will be considered within the Ene

### everest_api/evse_manager/cmd/force_unlock
Command to force unlock a connector on the EVSE. They payload should be a positive integer identifying the connector that should be unlocked. If the payload is empty or cannot be converted to an integer connector 1 is assumed.

### everest_api/evse_manager/cmd/uk_random_delay
Command to control the UK Smart Charging random delay feature. The payload can be the following enum: "enable" and "disable" to enable/disable the feature entirely or "cancel" to cancel an ongoing delay.

### everest_api/evse_manager/cmd/uk_random_delay_set_max_duration_s
Command to set the UK Smart Charging random delay maximum duration. Payload is an integer in seconds.
4 changes: 4 additions & 0 deletions modules/API/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ requires:
interface: ocpp
min_connections: 0
max_connections: 1
random_delay:
interface: uk_random_delay
min_connections: 0
max_connections: 128
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
Expand Down
29 changes: 28 additions & 1 deletion modules/EnergyManager/EnergyManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ void EnergyManager::init() {
// Received new energy object from a child.
std::scoped_lock lock(energy_mutex);
energy_flow_request = e;

if (is_priority_request(e)) {
// trigger optimization now
mainloop_sleep_condvar.notify_all();
}
});

invoke_init(*p_main);
Expand All @@ -30,11 +35,33 @@ void EnergyManager::ready() {
config.slice_ampere, config.slice_watt, config.debug, energy_flow_request);
auto optimized_values = run_optimizer(energy_flow_request);
enforce_limits(optimized_values);
sleep(config.update_interval);
{
std::unique_lock<std::mutex> lock(mainloop_sleep_mutex);
mainloop_sleep_condvar.wait_for(lock, std::chrono::seconds(config.update_interval));
}
}
}).detach();
}

// Check if any node set the priority request flag
bool EnergyManager::is_priority_request(const types::energy::EnergyFlowRequest& e) {
bool prio = e.priority_request.has_value() and e.priority_request.value();

// If this node has priority, no need to travese the tree any longer
if (prio) {
return true;
}

// recurse to all children
for (auto& c : e.children) {
if (is_priority_request(c)) {
return true;
}
}

return false;
}

void EnergyManager::enforce_limits(const std::vector<types::energy::EnforcedLimits>& limits) {
for (const auto& it : limits) {
if (globals.debug)
Expand Down
5 changes: 4 additions & 1 deletion modules/EnergyManager/EnergyManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,17 @@ class EnergyManager : public Everest::ModuleBase {

// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here

bool is_priority_request(const types::energy::EnergyFlowRequest& e);
std::mutex energy_mutex;

// complete energy tree requests
types::energy::EnergyFlowRequest energy_flow_request;

void enforce_limits(const std::vector<types::energy::EnforcedLimits>& limits);
std::vector<types::energy::EnforcedLimits> run_optimizer(types::energy::EnergyFlowRequest request);

std::condition_variable mainloop_sleep_condvar;
std::mutex mainloop_sleep_mutex;
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};

Expand Down
3 changes: 2 additions & 1 deletion modules/EnergyManager/Market.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void globals_t::init(date::utc_clock::time_point start_time, int _interval_durat
void globals_t::create_timestamps(const date::utc_clock::time_point& start_time,
const types::energy::EnergyFlowRequest& energy_flow_request) {

timestamps.clear();
timestamps.reserve(schedule_length);

auto minutes_overflow = start_time.time_since_epoch() % interval_duration;
Expand Down Expand Up @@ -85,7 +86,7 @@ void globals_t::add_timestamps(const types::energy::EnergyFlowRequest& energy_fl
}

// recurse to all children
for (auto c : energy_flow_request.children)
for (auto& c : energy_flow_request.children)
add_timestamps(c);
}

Expand Down
1 change: 1 addition & 0 deletions modules/EvseManager/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ target_sources(${MODULE_NAME}
"evse/evse_managerImpl.cpp"
"energy_grid/energyImpl.cpp"
"token_provider/auth_token_providerImpl.cpp"
"random_delay/uk_random_delayImpl.cpp"
)

# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
Expand Down
Loading

0 comments on commit b172bb0

Please sign in to comment.