Skip to content

Commit cb7426c

Browse files
authored
feat(EnergyNode): Enhancement to set per phase A limit when only W is set (EVerest#1942)
We can now configure the energy node to convert the total W power to a current per phase limit. This is only useful for top level energy nodes. Furthermore, we've replaced the use of the mutex for the monitor. Signed-off-by: Martin Litre <mnlitre@gmail.com>
1 parent 61e2554 commit cb7426c

File tree

9 files changed

+442
-62
lines changed

9 files changed

+442
-62
lines changed

modules/EnergyManagement/EnergyNode/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ target_link_libraries(${MODULE_NAME}
1818
target_sources(${MODULE_NAME}
1919
PRIVATE
2020
"energy_grid/energyImpl.cpp"
21+
"energy_grid/energy_schedule_utils.cpp"
2122
"external_limits/external_energy_limitsImpl.cpp"
2223
)
2324

2425
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
2526
# insert other things like install cmds etc here
27+
28+
# Add tests subdirectory
29+
if(BUILD_TESTING)
30+
add_subdirectory(tests)
31+
endif()
2632
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

modules/EnergyManagement/EnergyNode/EnergyNode.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ namespace module {
2929
struct Conf {
3030
double fuse_limit_A;
3131
int phase_count;
32+
bool enhance_external_schedule;
33+
double nominal_voltage_V;
3234
};
3335

3436
class EnergyNode : public Everest::ModuleBase {

modules/EnergyManagement/EnergyNode/energy_grid/energyImpl.cpp

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// SPDX-License-Identifier: Apache-2.0
2-
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
2+
// Copyright Pionix GmbH and Contributors to EVerest
33

44
#include "energyImpl.hpp"
5+
#include "energy_schedule_utils.hpp"
56
#include <chrono>
67
#include <date/date.h>
78
#include <date/tz.h>
@@ -11,60 +12,59 @@ namespace module {
1112
namespace energy_grid {
1213

1314
void energyImpl::init() {
15+
auto energy_state_handle = energy_state.handle();
1416

15-
// UUID must be unique also beyond this charging station -> will be handled on framework level and above later
16-
energy_flow_request.uuid = mod->info.id;
17-
energy_flow_request.node_type = types::energy::NodeType::Generic;
17+
energy_state_handle->energy_flow_request.uuid = mod->info.id;
18+
energy_state_handle->energy_flow_request.node_type = types::energy::NodeType::Generic;
1819

1920
source_cfg = mod->info.id + "/module_config";
2021

2122
// Initialize with sane defaults
22-
energy_flow_request.schedule_import = get_local_schedule();
23-
energy_flow_request.schedule_export = get_local_schedule();
23+
energy_state_handle->energy_flow_request.schedule_import = get_local_schedule();
24+
energy_state_handle->energy_flow_request.schedule_export = get_local_schedule();
2425

2526
for (auto& entry : mod->r_energy_consumer) {
2627
entry->subscribe_energy_flow_request([this](types::energy::EnergyFlowRequest e) {
2728
// Received new energy_flow_request object from a child. Update in the cached object and republish.
28-
std::scoped_lock lock(energy_mutex);
29+
auto energy_state_handle = energy_state.handle();
2930

3031
bool child_found = false;
31-
for (auto& child : energy_flow_request.children) {
32+
for (auto& child : energy_state_handle->energy_flow_request.children) {
3233
if (child.uuid == e.uuid) {
3334
child = e;
3435
child_found = true;
3536
}
3637
}
3738

3839
if (!child_found) {
39-
energy_flow_request.children.push_back(e);
40+
energy_state_handle->energy_flow_request.children.push_back(e);
4041
}
4142

42-
publish_complete_energy_object();
43+
publish_complete_energy_object(*energy_state_handle);
4344
});
4445
}
4546

4647
if (!mod->r_powermeter.empty()) {
4748
mod->r_powermeter[0]->subscribe_powermeter([this](types::powermeter::Powermeter p) {
4849
EVLOG_debug << "Incoming powermeter readings: " << p;
49-
std::scoped_lock lock(energy_mutex);
50-
energy_flow_request.energy_usage_root = p;
51-
publish_complete_energy_object();
50+
auto energy_state_handle = energy_state.handle();
51+
energy_state_handle->energy_flow_request.energy_usage_root = p;
52+
publish_complete_energy_object(*energy_state_handle);
5253
});
5354
}
5455

5556
if (!mod->r_price_information.empty()) {
5657
mod->r_price_information[0]->subscribe_energy_pricing(
5758
[this](types::energy_price_information::EnergyPriceSchedule p) {
5859
EVLOG_debug << "Incoming price schedule: " << p;
59-
std::scoped_lock lock(energy_mutex);
60-
energy_pricing = p;
61-
publish_complete_energy_object();
60+
auto energy_state_handle = energy_state.handle();
61+
energy_state_handle->energy_pricing = p;
62+
publish_complete_energy_object(*energy_state_handle);
6263
});
6364
}
6465
}
6566

6667
types::energy::ScheduleReqEntry energyImpl::get_local_schedule_req_entry() {
67-
// local schedule of this module
6868
types::energy::ScheduleReqEntry local_schedule;
6969
auto tp = date::utc_clock::now();
7070

@@ -84,52 +84,39 @@ std::vector<types::energy::ScheduleReqEntry> energyImpl::get_local_schedule() {
8484
}
8585

8686
void energyImpl::set_external_limits(types::energy::ExternalLimits& l) {
87-
std::scoped_lock lock(energy_mutex);
88-
89-
energy_flow_request.schedule_import = l.schedule_import;
90-
if (not energy_flow_request.schedule_import.empty()) {
91-
// add limits from our own fuse settings
92-
for (auto& e : energy_flow_request.schedule_import) {
93-
if (!e.limits_to_root.ac_max_current_A.has_value() ||
94-
e.limits_to_root.ac_max_current_A.value().value > mod->config.fuse_limit_A)
95-
e.limits_to_root.ac_max_current_A = {static_cast<float>(mod->config.fuse_limit_A), source_cfg};
96-
97-
if (!e.limits_to_root.ac_max_phase_count.has_value() ||
98-
e.limits_to_root.ac_max_phase_count.value().value > mod->config.phase_count)
99-
e.limits_to_root.ac_max_phase_count = {mod->config.phase_count, source_cfg};
100-
}
87+
auto energy_state_handle = energy_state.handle();
88+
89+
// Process import schedule
90+
energy_state_handle->energy_flow_request.schedule_import = l.schedule_import;
91+
if (not energy_state_handle->energy_flow_request.schedule_import.empty()) {
92+
module::energy_grid::process_schedule_with_limits(
93+
energy_state_handle->energy_flow_request.schedule_import, source_cfg, mod->config.fuse_limit_A,
94+
mod->config.phase_count, mod->config.nominal_voltage_V, mod->config.enhance_external_schedule);
10195
} else {
10296
// At least add our local config limit even if the external limit did not set an import schedule
103-
energy_flow_request.schedule_import = get_local_schedule();
97+
energy_state_handle->energy_flow_request.schedule_import = get_local_schedule();
10498
}
10599

106-
energy_flow_request.schedule_export = l.schedule_export;
107-
108-
if (not energy_flow_request.schedule_export.empty()) {
109-
// add limits from our own fuse settings
110-
for (auto& e : energy_flow_request.schedule_export) {
111-
if (!e.limits_to_root.ac_max_current_A.has_value() ||
112-
e.limits_to_root.ac_max_current_A.value().value > mod->config.fuse_limit_A)
113-
e.limits_to_root.ac_max_current_A = {static_cast<float>(mod->config.fuse_limit_A), source_cfg};
114-
115-
if (!e.limits_to_root.ac_max_phase_count.has_value() ||
116-
e.limits_to_root.ac_max_phase_count.value().value > mod->config.phase_count)
117-
e.limits_to_root.ac_max_phase_count = {mod->config.phase_count, source_cfg};
118-
}
100+
// Process export schedule
101+
energy_state_handle->energy_flow_request.schedule_export = l.schedule_export;
102+
if (not energy_state_handle->energy_flow_request.schedule_export.empty()) {
103+
module::energy_grid::process_schedule_with_limits(
104+
energy_state_handle->energy_flow_request.schedule_export, source_cfg, mod->config.fuse_limit_A,
105+
mod->config.phase_count, mod->config.nominal_voltage_V, mod->config.enhance_external_schedule);
119106
} else {
120107
// At least add our local config limit even if the external limit did not set an export schedule
121-
energy_flow_request.schedule_export = get_local_schedule();
108+
energy_state_handle->energy_flow_request.schedule_export = get_local_schedule();
122109
}
123110

124-
energy_flow_request.schedule_setpoints = l.schedule_setpoints;
111+
energy_state_handle->energy_flow_request.schedule_setpoints = l.schedule_setpoints;
125112
}
126113

127-
void energyImpl::publish_complete_energy_object() {
128-
// join the different schedules to the complete array (with resampling)
129-
types::energy::EnergyFlowRequest energy_complete = energy_flow_request;
114+
void energyImpl::publish_complete_energy_object(const EnergyState& state) {
115+
// This method is always called from contexts that already hold the energy_state lock
116+
types::energy::EnergyFlowRequest energy_complete = state.energy_flow_request;
130117

131-
if (not energy_flow_request.schedule_export.empty() and not energy_pricing.schedule_export.empty()) {
132-
merge_price_into_schedule(energy_complete.schedule_export, energy_pricing.schedule_export);
118+
if (not state.energy_flow_request.schedule_export.empty() and not state.energy_pricing.schedule_export.empty()) {
119+
merge_price_into_schedule(energy_complete.schedule_export, state.energy_pricing.schedule_export);
133120
}
134121

135122
publish_energy_flow_request(energy_complete);
@@ -181,16 +168,18 @@ void energyImpl::merge_price_into_schedule(std::vector<types::energy::ScheduleRe
181168
}
182169

183170
void energyImpl::ready() {
171+
auto energy_state_handle = energy_state.handle();
184172
// publish own limits at least once
185-
publish_energy_flow_request(energy_flow_request);
173+
publish_energy_flow_request(energy_state_handle->energy_flow_request);
186174
mod->signalExternalLimit.connect([this](types::energy::ExternalLimits& l) { set_external_limits(l); });
187175
}
188176

189177
void energyImpl::handle_enforce_limits(types::energy::EnforcedLimits& value) {
178+
auto energy_state_handle = energy_state.handle();
190179

191180
// route to children if it is not for me
192181
// FIXME: this sends it to all children, we could do a lookup on which branch it actually is
193-
if (value.uuid != energy_flow_request.uuid) {
182+
if (value.uuid != energy_state_handle->energy_flow_request.uuid) {
194183
for (auto& entry : mod->r_energy_consumer) {
195184
entry->call_enforce_limits(value);
196185
}

modules/EnergyManagement/EnergyNode/energy_grid/energyImpl.hpp

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
1616
// insert your custom include headers here
17-
#include <mutex>
17+
#include <everest/util/async/monitor.hpp>
1818
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
1919

2020
namespace module {
@@ -48,27 +48,34 @@ class energyImpl : public energyImplBase {
4848
virtual void ready() override;
4949

5050
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
51-
std::mutex energy_mutex;
52-
// subtree including children
53-
types::energy::EnergyFlowRequest energy_flow_request;
51+
// Energy state protected by monitor for thread-safe access
52+
struct EnergyState {
53+
// subtree including children
54+
types::energy::EnergyFlowRequest energy_flow_request;
5455

55-
// contains only the pricing informations last update
56-
types::energy_price_information::EnergyPriceSchedule energy_pricing;
56+
// contains only the pricing informations last update
57+
types::energy_price_information::EnergyPriceSchedule energy_pricing;
58+
};
59+
60+
everest::lib::util::monitor<EnergyState> energy_state;
5761

5862
types::energy::ScheduleReqEntry get_local_schedule_req_entry();
5963
std::vector<types::energy::ScheduleReqEntry> get_local_schedule();
6064

61-
void publish_complete_energy_object();
65+
void publish_complete_energy_object(const EnergyState& state);
6266
void set_external_limits(types::energy::ExternalLimits& l);
6367
void merge_price_into_schedule(std::vector<types::energy::ScheduleReqEntry>& schedule,
6468
const std::vector<types::energy_price_information::PricePerkWh>& price);
65-
6669
std::string source_cfg;
6770
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
6871
};
6972

7073
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
7174
// insert other definitions here
75+
// Standalone function for schedule processing (used by tests)
76+
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule, const std::string& source,
77+
double fuse_limit_A, int phase_count, double nominal_voltage_V,
78+
bool enhance_with_current_limits);
7279
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
7380

7481
} // namespace energy_grid
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Pionix GmbH and Contributors to EVerest
3+
4+
#include "energy_schedule_utils.hpp"
5+
6+
namespace module {
7+
namespace energy_grid {
8+
9+
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule, const std::string& source,
10+
double fuse_limit_A, int phase_count, double nominal_voltage_V,
11+
bool enhance_with_current_limits) {
12+
13+
for (auto& entry : schedule) {
14+
// Enhance with current limits from power if enabled
15+
if (enhance_with_current_limits) {
16+
// Determine phase count to use (schedule entry takes priority over module config)
17+
int effective_phase_count = phase_count;
18+
19+
if (entry.limits_to_root.ac_max_phase_count.has_value() &&
20+
entry.limits_to_root.ac_max_phase_count.value().value > 0) {
21+
effective_phase_count = entry.limits_to_root.ac_max_phase_count.value().value;
22+
} else if (entry.limits_to_leaves.ac_max_phase_count.has_value() &&
23+
entry.limits_to_leaves.ac_max_phase_count.value().value > 0) {
24+
effective_phase_count = entry.limits_to_leaves.ac_max_phase_count.value().value;
25+
}
26+
27+
// Default to 1 phase if no valid phase count is available
28+
if (effective_phase_count <= 0) {
29+
effective_phase_count = 1;
30+
}
31+
32+
// Calculate current from power for limits_to_root (only if not already set)
33+
if (entry.limits_to_root.total_power_W.has_value() &&
34+
entry.limits_to_root.total_power_W.value().value > 0 && nominal_voltage_V > 0 &&
35+
!entry.limits_to_root.ac_max_current_A.has_value()) {
36+
37+
float calculated_current = static_cast<float>(entry.limits_to_root.total_power_W.value().value /
38+
(nominal_voltage_V * effective_phase_count));
39+
entry.limits_to_root.ac_max_current_A = {calculated_current, source};
40+
}
41+
42+
// Note: limits_to_leaves current limits are not modified to match fuse limit behavior
43+
}
44+
45+
// Apply fuse limit to limits_to_root (as a safety constraint)
46+
if (!entry.limits_to_root.ac_max_current_A.has_value() ||
47+
entry.limits_to_root.ac_max_current_A->value > fuse_limit_A) {
48+
entry.limits_to_root.ac_max_current_A = {static_cast<float>(fuse_limit_A), source};
49+
}
50+
51+
// Apply phase count limit to limits_to_root (as a safety constraint, same model as fuse limit)
52+
if (phase_count > 0 && (!entry.limits_to_root.ac_max_phase_count.has_value() ||
53+
entry.limits_to_root.ac_max_phase_count->value > phase_count)) {
54+
entry.limits_to_root.ac_max_phase_count = {phase_count, source};
55+
}
56+
}
57+
}
58+
59+
} // namespace energy_grid
60+
} // namespace module
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Pionix GmbH and Contributors to EVerest
3+
4+
#ifndef ENERGY_SCHEDULE_UTILS_HPP
5+
#define ENERGY_SCHEDULE_UTILS_HPP
6+
7+
#include <generated/types/energy.hpp>
8+
#include <string>
9+
#include <vector>
10+
11+
namespace module {
12+
namespace energy_grid {
13+
14+
/**
15+
* @brief Processes energy schedule entries with limits and fuse constraints
16+
*
17+
* This function applies fuse limits to schedule entries and optionally enhances
18+
* them with current limits calculated from power values.
19+
*
20+
* @param schedule The schedule entries to process
21+
* @param source The source identifier for any modifications made
22+
* @param fuse_limit_A The fuse limit in amperes to apply as a safety constraint
23+
* @param phase_count The default phase count to use for calculations
24+
* @param nominal_voltage_V The nominal voltage to use for power-to-current calculations
25+
* @param enhance_with_current_limits If true, calculates current limits from power values
26+
*/
27+
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule, const std::string& source,
28+
double fuse_limit_A, int phase_count, double nominal_voltage_V,
29+
bool enhance_with_current_limits);
30+
31+
} // namespace energy_grid
32+
} // namespace module
33+
34+
#endif // ENERGY_SCHEDULE_UTILS_HPP

modules/EnergyManagement/EnergyNode/manifest.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ config:
1212
type: integer
1313
minimum: 0
1414
maximum: 3
15+
enhance_external_schedule:
16+
description: >-
17+
When enabled, calculates per-phase current limits from total_power_W
18+
and adds them to ac_max_current_A when processing external schedules.
19+
ac_max_current_A is only enhanced in case it was not specified as part of
20+
the external schedules. Uses nominal_voltage_V for calculations.
21+
type: boolean
22+
default: false
23+
nominal_voltage_V:
24+
description: >-
25+
Nominal voltage in volts used for power to current calculations when
26+
enhance_external_schedule is enabled.
27+
This allows configuration for different regions (e.g., 120V, 230V, 400V).
28+
type: number
29+
minimum: 1.0
30+
maximum: 1000.0
31+
default: 230.0
1532
provides:
1633
energy_grid:
1734
description: This is the chain interface to build the energy supply tree

0 commit comments

Comments
 (0)