Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit b172bb0
Author: corneliusclaussen <[email protected]>
Date:   Thu Mar 14 10:29:51 2024 +0100

    Feature/random delays (#530)

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

commit 8cc7da8
Author: Michael Heimpold <[email protected]>
Date:   Wed Mar 13 19:41:28 2024 +0100

    GenericPowermeter: add new model Klefr 693x-694x (#560)

    We use this model in some of our developer charging stations.

    Signed-off-by: Michael Heimpold <[email protected]>
  • Loading branch information
james-ctc committed Mar 14, 2024
1 parent fa3f464 commit c310bf6
Show file tree
Hide file tree
Showing 21 changed files with 769 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 c310bf6

Please sign in to comment.