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
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
load("//modules:module.bzl", "cc_everest_module")
|
||||
|
||||
IMPLS = [
|
||||
"energy_grid",
|
||||
"external_limits",
|
||||
]
|
||||
|
||||
cc_everest_module(
|
||||
name = "EnergyNode",
|
||||
impls = IMPLS,
|
||||
deps = [
|
||||
"@sigslot//:sigslot",
|
||||
],
|
||||
srcs = glob(
|
||||
[
|
||||
"*.cpp",
|
||||
"*.hpp",
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
# template version 3
|
||||
#
|
||||
|
||||
# module setup:
|
||||
# - ${MODULE_NAME}: module name
|
||||
ev_setup_cpp_module()
|
||||
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
# insert your custom targets and additional config variables here
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
Pal::Sigslot
|
||||
)
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"energy_grid/energyImpl.cpp"
|
||||
"external_limits/external_energy_limitsImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# insert other things like install cmds etc here
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"energy_grid/energy_schedule_utils.cpp"
|
||||
)
|
||||
|
||||
# Add tests subdirectory
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
#include "EnergyNode.hpp"
|
||||
|
||||
namespace module {
|
||||
|
||||
void EnergyNode::init() {
|
||||
invoke_init(*p_energy_grid);
|
||||
}
|
||||
|
||||
void EnergyNode::ready() {
|
||||
invoke_ready(*p_energy_grid);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef ENERGY_NODE_HPP
|
||||
#define ENERGY_NODE_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/energy/Implementation.hpp>
|
||||
#include <generated/interfaces/external_energy_limits/Implementation.hpp>
|
||||
|
||||
// headers for required interface implementations
|
||||
#include <generated/interfaces/energy/Interface.hpp>
|
||||
#include <generated/interfaces/energy_price_information/Interface.hpp>
|
||||
#include <generated/interfaces/powermeter/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
#include <sigslot/signal.hpp>
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
double fuse_limit_A;
|
||||
int phase_count;
|
||||
double nominal_voltage_V;
|
||||
bool enhance_external_schedule;
|
||||
};
|
||||
|
||||
class EnergyNode : public Everest::ModuleBase {
|
||||
public:
|
||||
EnergyNode() = delete;
|
||||
EnergyNode(const ModuleInfo& info, std::unique_ptr<energyImplBase> p_energy_grid,
|
||||
std::unique_ptr<external_energy_limitsImplBase> p_external_limits,
|
||||
std::vector<std::unique_ptr<energyIntf>> r_energy_consumer,
|
||||
std::vector<std::unique_ptr<powermeterIntf>> r_powermeter,
|
||||
std::vector<std::unique_ptr<energy_price_informationIntf>> r_price_information, Conf& config) :
|
||||
ModuleBase(info),
|
||||
p_energy_grid(std::move(p_energy_grid)),
|
||||
p_external_limits(std::move(p_external_limits)),
|
||||
r_energy_consumer(std::move(r_energy_consumer)),
|
||||
r_powermeter(std::move(r_powermeter)),
|
||||
r_price_information(std::move(r_price_information)),
|
||||
config(config){};
|
||||
|
||||
const std::unique_ptr<energyImplBase> p_energy_grid;
|
||||
const std::unique_ptr<external_energy_limitsImplBase> p_external_limits;
|
||||
const std::vector<std::unique_ptr<energyIntf>> r_energy_consumer;
|
||||
const std::vector<std::unique_ptr<powermeterIntf>> r_powermeter;
|
||||
const std::vector<std::unique_ptr<energy_price_informationIntf>> r_price_information;
|
||||
const Conf& config;
|
||||
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
// insert your public definitions here
|
||||
sigslot::signal<types::energy::ExternalLimits&> signalExternalLimit;
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
|
||||
protected:
|
||||
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
|
||||
// insert your protected definitions here
|
||||
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
|
||||
|
||||
private:
|
||||
friend class LdEverest;
|
||||
void init();
|
||||
void ready();
|
||||
|
||||
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
|
||||
// insert your private definitions here
|
||||
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
|
||||
};
|
||||
|
||||
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
|
||||
// insert other definitions here
|
||||
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // ENERGY_NODE_HPP
|
||||
@@ -0,0 +1,8 @@
|
||||
.. _everest_modules_handwritten_EnergyNode:
|
||||
|
||||
.. ===================
|
||||
.. EnergyNode
|
||||
.. ===================
|
||||
|
||||
The EnergyNode module is usually used in conjunction with the **EnergyManager** module.
|
||||
See the :ref:`documentation <everest_modules_EnergyManager>` of the latter for a detailed explanation of energy management.
|
||||
@@ -0,0 +1,193 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "energyImpl.hpp"
|
||||
#include "energy_schedule_utils.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <date/date.h>
|
||||
#include <date/tz.h>
|
||||
#include <string_view>
|
||||
#include <utils/date.hpp>
|
||||
|
||||
namespace module {
|
||||
namespace energy_grid {
|
||||
|
||||
void energyImpl::init() {
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
|
||||
energy_state_handle->energy_flow_request.uuid = mod->info.id;
|
||||
energy_state_handle->energy_flow_request.node_type = types::energy::NodeType::Generic;
|
||||
|
||||
source_cfg = mod->info.id + "/module_config";
|
||||
|
||||
// Initialize with sane defaults
|
||||
energy_state_handle->energy_flow_request.schedule_import = get_local_schedule();
|
||||
energy_state_handle->energy_flow_request.schedule_export = get_local_schedule();
|
||||
|
||||
for (auto& entry : mod->r_energy_consumer) {
|
||||
entry->subscribe_energy_flow_request([this](types::energy::EnergyFlowRequest const& e) {
|
||||
// Received new energy_flow_request object from a child. Update in the cached object and republish.
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
|
||||
auto& children = energy_state_handle->energy_flow_request.children;
|
||||
auto children_it = std::find_if(children.begin(), children.end(), [&e](const auto& child) {
|
||||
return std::string_view{child.uuid} == std::string_view{e.uuid};
|
||||
});
|
||||
if (children_it != children.end()) {
|
||||
*children_it = e;
|
||||
} else {
|
||||
children.push_back(std::move(e));
|
||||
}
|
||||
|
||||
publish_complete_energy_object(*energy_state_handle);
|
||||
});
|
||||
}
|
||||
|
||||
if (!mod->r_powermeter.empty()) {
|
||||
mod->r_powermeter[0]->subscribe_powermeter([this](types::powermeter::Powermeter const& p) {
|
||||
EVLOG_debug << "Incoming powermeter readings: " << p;
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
energy_state_handle->energy_flow_request.energy_usage_root = p;
|
||||
publish_complete_energy_object(*energy_state_handle);
|
||||
});
|
||||
}
|
||||
|
||||
if (!mod->r_price_information.empty()) {
|
||||
mod->r_price_information[0]->subscribe_energy_pricing(
|
||||
[this](types::energy_price_information::EnergyPriceSchedule p) {
|
||||
EVLOG_debug << "Incoming price schedule: " << p;
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
energy_state_handle->energy_pricing = p;
|
||||
publish_complete_energy_object(*energy_state_handle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
types::energy::ScheduleReqEntry energyImpl::get_local_schedule_req_entry() {
|
||||
types::energy::ScheduleReqEntry local_schedule;
|
||||
auto tp = date::utc_clock::now();
|
||||
|
||||
local_schedule.timestamp =
|
||||
Everest::Date::to_rfc3339(date::floor<std::chrono::hours>(tp) + date::get_leap_second_info(tp).elapsed);
|
||||
local_schedule.limits_to_root.ac_max_phase_count = {mod->config.phase_count, source_cfg};
|
||||
local_schedule.limits_to_root.ac_max_current_A = {static_cast<float>(mod->config.fuse_limit_A), source_cfg};
|
||||
local_schedule.limits_to_leaves.ac_max_phase_count = {mod->config.phase_count, source_cfg};
|
||||
local_schedule.limits_to_leaves.ac_max_current_A = {static_cast<float>(mod->config.fuse_limit_A), source_cfg};
|
||||
|
||||
return local_schedule;
|
||||
}
|
||||
|
||||
std::vector<types::energy::ScheduleReqEntry> energyImpl::get_local_schedule() {
|
||||
const auto local_schedule = get_local_schedule_req_entry();
|
||||
return std::vector<types::energy::ScheduleReqEntry>({local_schedule});
|
||||
}
|
||||
|
||||
void energyImpl::set_external_limits(types::energy::ExternalLimits& l) {
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
|
||||
// Process import schedule
|
||||
energy_state_handle->energy_flow_request.schedule_import = l.schedule_import;
|
||||
if (not energy_state_handle->energy_flow_request.schedule_import.empty()) {
|
||||
module::energy_grid::process_schedule_with_limits(
|
||||
energy_state_handle->energy_flow_request.schedule_import, source_cfg, mod->config.fuse_limit_A,
|
||||
mod->config.phase_count, mod->config.nominal_voltage_V, mod->config.enhance_external_schedule);
|
||||
} else {
|
||||
// At least add our local config limit even if the external limit did not set an import schedule
|
||||
energy_state_handle->energy_flow_request.schedule_import = get_local_schedule();
|
||||
}
|
||||
|
||||
// Process export schedule
|
||||
energy_state_handle->energy_flow_request.schedule_export = l.schedule_export;
|
||||
if (not energy_state_handle->energy_flow_request.schedule_export.empty()) {
|
||||
module::energy_grid::process_schedule_with_limits(
|
||||
energy_state_handle->energy_flow_request.schedule_export, source_cfg, mod->config.fuse_limit_A,
|
||||
mod->config.phase_count, mod->config.nominal_voltage_V, mod->config.enhance_external_schedule);
|
||||
} else {
|
||||
// At least add our local config limit even if the external limit did not set an export schedule
|
||||
energy_state_handle->energy_flow_request.schedule_export = get_local_schedule();
|
||||
}
|
||||
|
||||
energy_state_handle->energy_flow_request.schedule_setpoints = l.schedule_setpoints;
|
||||
}
|
||||
|
||||
void energyImpl::publish_complete_energy_object(const EnergyState& state) {
|
||||
// This method is always called from contexts that already hold the energy_state lock
|
||||
const auto& energy_flow_request = state.energy_flow_request;
|
||||
const auto& energy_pricing_schedule_export = state.energy_pricing.schedule_export;
|
||||
|
||||
if (not energy_flow_request.schedule_export.empty() and not energy_pricing_schedule_export.empty()) {
|
||||
types::energy::EnergyFlowRequest energy_complete = energy_flow_request;
|
||||
merge_price_into_schedule(energy_complete.schedule_export, energy_pricing_schedule_export);
|
||||
publish_energy_flow_request(energy_complete);
|
||||
} else {
|
||||
publish_energy_flow_request(energy_flow_request);
|
||||
}
|
||||
}
|
||||
|
||||
void energyImpl::merge_price_into_schedule(std::vector<types::energy::ScheduleReqEntry>& schedule,
|
||||
const std::vector<types::energy_price_information::PricePerkWh>& price) {
|
||||
auto it_schedule = schedule.begin();
|
||||
auto it_price = price.begin();
|
||||
|
||||
std::vector<types::energy::ScheduleReqEntry> joined_schedule;
|
||||
|
||||
// The first element is already valid now even if the timestamp is in the future (per agreement)
|
||||
auto next_entry_schedule = *it_schedule;
|
||||
auto next_entry_price = *it_price;
|
||||
auto currently_valid_entry_schedule = next_entry_schedule;
|
||||
auto currently_valid_entry_price = next_entry_price;
|
||||
|
||||
while (it_schedule != schedule.end() && it_price != price.end()) {
|
||||
auto tp_schedule = Everest::Date::from_rfc3339(next_entry_schedule.timestamp);
|
||||
auto tp_price = Everest::Date::from_rfc3339(next_entry_price.timestamp);
|
||||
|
||||
if ((tp_schedule < tp_price && it_schedule != schedule.end()) || it_price == price.end()) {
|
||||
currently_valid_entry_schedule = next_entry_schedule;
|
||||
auto joined_entry = currently_valid_entry_schedule;
|
||||
|
||||
joined_entry.price_per_kwh = currently_valid_entry_price;
|
||||
joined_schedule.push_back(joined_entry);
|
||||
it_schedule++;
|
||||
if (it_schedule != schedule.end()) {
|
||||
next_entry_schedule = *it_schedule;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tp_price < tp_schedule && it_price != price.end()) || it_schedule == schedule.end()) {
|
||||
currently_valid_entry_price = next_entry_price;
|
||||
auto joined_entry = currently_valid_entry_schedule;
|
||||
joined_entry.price_per_kwh = currently_valid_entry_price;
|
||||
joined_entry.timestamp = currently_valid_entry_price.timestamp;
|
||||
joined_schedule.push_back(joined_entry);
|
||||
it_price++;
|
||||
if (it_price != price.end()) {
|
||||
next_entry_price = *it_price;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void energyImpl::ready() {
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
// publish own limits at least once
|
||||
publish_energy_flow_request(energy_state_handle->energy_flow_request);
|
||||
mod->signalExternalLimit.connect([this](types::energy::ExternalLimits& l) { set_external_limits(l); });
|
||||
}
|
||||
|
||||
void energyImpl::handle_enforce_limits(types::energy::EnforcedLimits& value) {
|
||||
auto energy_state_handle = energy_state.handle();
|
||||
|
||||
// route to children if it is not for me
|
||||
// FIXME: this sends it to all children, we could do a lookup on which branch it actually is
|
||||
if (value.uuid != energy_state_handle->energy_flow_request.uuid) {
|
||||
for (auto& entry : mod->r_energy_consumer) {
|
||||
entry->call_enforce_limits(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace energy_grid
|
||||
} // namespace module
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef ENERGY_GRID_ENERGY_IMPL_HPP
|
||||
#define ENERGY_GRID_ENERGY_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/energy/Implementation.hpp>
|
||||
|
||||
#include "../EnergyNode.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
#include <everest/util/async/monitor.hpp>
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace energy_grid {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class energyImpl : public energyImplBase {
|
||||
public:
|
||||
energyImpl() = delete;
|
||||
energyImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EnergyNode>& mod, Conf& config) :
|
||||
energyImplBase(ev, "energy_grid"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_enforce_limits(types::energy::EnforcedLimits& value) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<EnergyNode>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// Energy state protected by monitor for thread-safe access
|
||||
struct EnergyState {
|
||||
// subtree including children
|
||||
types::energy::EnergyFlowRequest energy_flow_request;
|
||||
|
||||
// contains only the pricing informations last update
|
||||
types::energy_price_information::EnergyPriceSchedule energy_pricing;
|
||||
};
|
||||
|
||||
everest::lib::util::monitor<EnergyState> energy_state;
|
||||
|
||||
types::energy::ScheduleReqEntry get_local_schedule_req_entry();
|
||||
std::vector<types::energy::ScheduleReqEntry> get_local_schedule();
|
||||
|
||||
void publish_complete_energy_object(const EnergyState& state);
|
||||
void set_external_limits(types::energy::ExternalLimits& l);
|
||||
void merge_price_into_schedule(std::vector<types::energy::ScheduleReqEntry>& schedule,
|
||||
const std::vector<types::energy_price_information::PricePerkWh>& price);
|
||||
std::string source_cfg;
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// Standalone function for schedule processing (used by tests)
|
||||
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule, const std::string& source,
|
||||
double fuse_limit_A, int phase_count, double nominal_voltage_V,
|
||||
bool enhance_with_current_limits);
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace energy_grid
|
||||
} // namespace module
|
||||
|
||||
#endif // ENERGY_GRID_ENERGY_IMPL_HPP
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "energy_schedule_utils.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace energy_grid {
|
||||
|
||||
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule, const std::string& source,
|
||||
double fuse_limit_A, int phase_count, double nominal_voltage_V,
|
||||
bool enhance_with_current_limits) {
|
||||
|
||||
for (auto& entry : schedule) {
|
||||
// Enhance with current limits from power if enabled
|
||||
if (enhance_with_current_limits) {
|
||||
// Determine phase count to use (schedule entry takes priority over module config)
|
||||
int effective_phase_count = phase_count;
|
||||
|
||||
if (entry.limits_to_root.ac_max_phase_count.has_value() &&
|
||||
entry.limits_to_root.ac_max_phase_count.value().value > 0) {
|
||||
effective_phase_count = entry.limits_to_root.ac_max_phase_count.value().value;
|
||||
} else if (entry.limits_to_leaves.ac_max_phase_count.has_value() &&
|
||||
entry.limits_to_leaves.ac_max_phase_count.value().value > 0) {
|
||||
effective_phase_count = entry.limits_to_leaves.ac_max_phase_count.value().value;
|
||||
}
|
||||
|
||||
// Default to 1 phase if no valid phase count is available
|
||||
if (effective_phase_count <= 0) {
|
||||
effective_phase_count = 1;
|
||||
}
|
||||
|
||||
// Calculate current from power for limits_to_root (only if not already set)
|
||||
if (entry.limits_to_root.total_power_W.has_value() &&
|
||||
entry.limits_to_root.total_power_W.value().value > 0 && nominal_voltage_V > 0 &&
|
||||
!entry.limits_to_root.ac_max_current_A.has_value()) {
|
||||
|
||||
float calculated_current = static_cast<float>(entry.limits_to_root.total_power_W.value().value /
|
||||
(nominal_voltage_V * effective_phase_count));
|
||||
entry.limits_to_root.ac_max_current_A = {calculated_current, source};
|
||||
}
|
||||
|
||||
// Note: limits_to_leaves current limits are not modified to match fuse limit behavior
|
||||
}
|
||||
|
||||
// Apply fuse limit to limits_to_root (as a safety constraint)
|
||||
if (!entry.limits_to_root.ac_max_current_A.has_value() ||
|
||||
entry.limits_to_root.ac_max_current_A->value > fuse_limit_A) {
|
||||
entry.limits_to_root.ac_max_current_A = {static_cast<float>(fuse_limit_A), source};
|
||||
}
|
||||
|
||||
// Apply phase count limit to limits_to_root (as a safety constraint, same model as fuse limit)
|
||||
if (phase_count > 0 && (!entry.limits_to_root.ac_max_phase_count.has_value() ||
|
||||
entry.limits_to_root.ac_max_phase_count->value > phase_count)) {
|
||||
entry.limits_to_root.ac_max_phase_count = {phase_count, source};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace energy_grid
|
||||
} // namespace module
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef ENERGY_SCHEDULE_UTILS_HPP
|
||||
#define ENERGY_SCHEDULE_UTILS_HPP
|
||||
|
||||
#include <generated/types/energy.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace module {
|
||||
namespace energy_grid {
|
||||
|
||||
/**
|
||||
* @brief Processes energy schedule entries with limits and fuse constraints
|
||||
*
|
||||
* This function applies fuse limits to schedule entries and optionally enhances
|
||||
* them with current limits calculated from power values.
|
||||
*
|
||||
* @param schedule The schedule entries to process
|
||||
* @param source The source identifier for any modifications made
|
||||
* @param fuse_limit_A The fuse limit in amperes to apply as a safety constraint
|
||||
* @param phase_count The default phase count to use for calculations
|
||||
* @param nominal_voltage_V The nominal voltage to use for power-to-current calculations
|
||||
* @param enhance_with_current_limits If true, calculates current limits from power values
|
||||
*/
|
||||
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule, const std::string& source,
|
||||
double fuse_limit_A, int phase_count, double nominal_voltage_V,
|
||||
bool enhance_with_current_limits);
|
||||
|
||||
} // namespace energy_grid
|
||||
} // namespace module
|
||||
|
||||
#endif // ENERGY_SCHEDULE_UTILS_HPP
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "external_energy_limitsImpl.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace external_limits {
|
||||
|
||||
void external_energy_limitsImpl::init() {
|
||||
}
|
||||
|
||||
void external_energy_limitsImpl::ready() {
|
||||
// special case: phase_count could be configured as zero for historic reason,
|
||||
// so let's just use voltage and current for power calculation
|
||||
types::energy::CapabilityLimits capabilities{
|
||||
static_cast<float>(mod->config.fuse_limit_A), mod->config.phase_count,
|
||||
static_cast<float>(mod->config.nominal_voltage_V),
|
||||
static_cast<float>(mod->config.nominal_voltage_V * mod->config.fuse_limit_A *
|
||||
(mod->config.phase_count ? mod->config.phase_count : 1))};
|
||||
|
||||
this->publish_capabilities(capabilities);
|
||||
}
|
||||
|
||||
void external_energy_limitsImpl::handle_set_external_limits(types::energy::ExternalLimits& value) {
|
||||
// your code for cmd set_external_limits goes here
|
||||
mod->signalExternalLimit(value);
|
||||
};
|
||||
|
||||
} // namespace external_limits
|
||||
} // namespace module
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef EXTERNAL_LIMITS_EXTERNAL_ENERGY_LIMITS_IMPL_HPP
|
||||
#define EXTERNAL_LIMITS_EXTERNAL_ENERGY_LIMITS_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/external_energy_limits/Implementation.hpp>
|
||||
|
||||
#include "../EnergyNode.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
// insert your custom include headers here
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace external_limits {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class external_energy_limitsImpl : public external_energy_limitsImplBase {
|
||||
public:
|
||||
external_energy_limitsImpl() = delete;
|
||||
external_energy_limitsImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EnergyNode>& mod, Conf& config) :
|
||||
external_energy_limitsImplBase(ev, "external_limits"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_set_external_limits(types::energy::ExternalLimits& value) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<EnergyNode>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace external_limits
|
||||
} // namespace module
|
||||
|
||||
#endif // EXTERNAL_LIMITS_EXTERNAL_ENERGY_LIMITS_IMPL_HPP
|
||||
@@ -0,0 +1,57 @@
|
||||
description: >-
|
||||
This module is part of the Energy Tree and represents a simple current fuse on AC side.
|
||||
config:
|
||||
fuse_limit_A:
|
||||
description: >-
|
||||
Fuse limit in ampere for all phases. Note: this always applies
|
||||
in addition to limits set by external_limits interface.
|
||||
type: number
|
||||
minimum: 0.0
|
||||
phase_count:
|
||||
description: phase count of this fuse
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 3
|
||||
default: 3
|
||||
nominal_voltage_V:
|
||||
description: >-
|
||||
Nominal AC voltage between a single phase and neutral in volt.
|
||||
Used for e.g. power to current calculations when
|
||||
enhance_external_schedule is enabled.
|
||||
This allows configuration for different regions (e.g., 120V, 230V, 400V).
|
||||
type: number
|
||||
minimum: 1.0
|
||||
maximum: 1000.0
|
||||
default: 230.0
|
||||
enhance_external_schedule:
|
||||
description: >-
|
||||
When enabled, calculates per-phase current limits from total_power_W
|
||||
and adds them to ac_max_current_A when processing external schedules.
|
||||
ac_max_current_A is only enhanced in case it was not specified as part of
|
||||
the external schedules. Uses nominal_voltage_V for calculations.
|
||||
type: boolean
|
||||
default: false
|
||||
provides:
|
||||
energy_grid:
|
||||
description: This is the chain interface to build the energy supply tree
|
||||
interface: energy
|
||||
external_limits:
|
||||
description: Additional external limits can be set via this interface.
|
||||
interface: external_energy_limits
|
||||
requires:
|
||||
energy_consumer:
|
||||
interface: energy
|
||||
min_connections: 1
|
||||
max_connections: 128
|
||||
powermeter:
|
||||
interface: powermeter
|
||||
min_connections: 0
|
||||
max_connections: 1
|
||||
price_information:
|
||||
interface: energy_price_information
|
||||
min_connections: 0
|
||||
max_connections: 1
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Cornelius Claussen
|
||||
@@ -0,0 +1,37 @@
|
||||
set(TEST_TARGET_NAME ${PROJECT_NAME}_energy_node_tests)
|
||||
# Compile the test file and the utility implementation
|
||||
add_executable(${TEST_TARGET_NAME})
|
||||
|
||||
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
|
||||
|
||||
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
|
||||
|
||||
set(INCLUDE_DIR
|
||||
"${MODULE_DIR}/include"
|
||||
"${MODULE_DIR}/tests"
|
||||
..
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_include_directories(${TEST_TARGET_NAME} PUBLIC
|
||||
${INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_sources(${TEST_TARGET_NAME} PRIVATE
|
||||
energy_node_tests.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../energy_grid/energy_schedule_utils.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
|
||||
GTest::gmock
|
||||
GTest::gtest_main
|
||||
everest::log
|
||||
everest::framework
|
||||
everest::util
|
||||
)
|
||||
|
||||
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
|
||||
ev_register_test_target(${TEST_TARGET_NAME})
|
||||
@@ -0,0 +1,228 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <generated/types/energy.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// Include the utility header
|
||||
#include "../energy_grid/energy_schedule_utils.hpp"
|
||||
|
||||
// Helper function to create test schedule entries
|
||||
static types::energy::ScheduleReqEntry create_test_entry(double total_power = 0.0,
|
||||
std::optional<int> root_phase_count = std::nullopt,
|
||||
std::optional<int> leaves_phase_count = std::nullopt) {
|
||||
types::energy::ScheduleReqEntry entry;
|
||||
entry.timestamp = "2024-01-01T00:00:00Z"; // Required field
|
||||
|
||||
if (total_power > 0) {
|
||||
entry.limits_to_root.total_power_W =
|
||||
types::energy::NumberWithSource{static_cast<float>(total_power), "test_source"};
|
||||
entry.limits_to_leaves.total_power_W =
|
||||
types::energy::NumberWithSource{static_cast<float>(total_power), "test_source"};
|
||||
}
|
||||
|
||||
if (root_phase_count.has_value()) {
|
||||
entry.limits_to_root.ac_max_phase_count =
|
||||
types::energy::IntegerWithSource{root_phase_count.value(), "test_source"};
|
||||
}
|
||||
|
||||
if (leaves_phase_count.has_value()) {
|
||||
entry.limits_to_leaves.ac_max_phase_count =
|
||||
types::energy::IntegerWithSource{leaves_phase_count.value(), "test_source"};
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
class EnergyNodeTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
}
|
||||
void TearDown() override {
|
||||
}
|
||||
|
||||
void process_schedule_with_limits(std::vector<types::energy::ScheduleReqEntry>& schedule,
|
||||
double fuse_limit_A = 32.0, int phase_count = 3, double nominal_voltage_V = 230.0,
|
||||
bool enhance_with_current_limits = false) {
|
||||
// Call the actual implementation function
|
||||
module::energy_grid::process_schedule_with_limits(schedule, "test_source", fuse_limit_A, phase_count,
|
||||
nominal_voltage_V, enhance_with_current_limits);
|
||||
}
|
||||
};
|
||||
|
||||
/// Test enhancement feature disabled by default
|
||||
TEST_F(EnergyNodeTest, TestEnhancementDisabledByDefault) {
|
||||
auto entry = create_test_entry(7000.0); // 7kW total power
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule);
|
||||
|
||||
// Enhancement should not be applied when disabled (no current limits from power calculation)
|
||||
EXPECT_FALSE(schedule[0].limits_to_leaves.ac_max_current_A.has_value());
|
||||
|
||||
// But fuse limits should still be applied to limits_to_root
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_EQ(schedule[0].limits_to_root.ac_max_current_A->value, 32.0f);
|
||||
}
|
||||
|
||||
/// Test enhancement feature when enabled
|
||||
TEST_F(EnergyNodeTest, TestEnhancementEnabled) {
|
||||
auto entry = create_test_entry(7000.0); // 7kW total power
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 3, 230.0, true);
|
||||
|
||||
// Calculate expected current: 7000W / (230V * 3 phases) = 10.25A
|
||||
float expected_current = 7000.0f / (230.0f * 3.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, expected_current, 0.01f);
|
||||
|
||||
// limits_to_leaves should not be modified (matching fuse limit behavior)
|
||||
EXPECT_FALSE(schedule[0].limits_to_leaves.ac_max_current_A.has_value());
|
||||
}
|
||||
|
||||
/// Test phase count priority: schedule entry over module config
|
||||
TEST_F(EnergyNodeTest, TestPhaseCountPriority) {
|
||||
// Create entry with 1 phase specified
|
||||
auto entry = create_test_entry(7000.0, 1, 1); // 1 phase in schedule entry
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 3, 230.0, true);
|
||||
|
||||
// Should use 1 phase from schedule entry, not 3 from module config
|
||||
// 7000W / (230V * 1 phase) = 30.43A
|
||||
float expected_current = 7000.0f / (230.0f * 1.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, expected_current, 0.01f);
|
||||
}
|
||||
|
||||
/// Test fallback to module config when schedule has no phase count
|
||||
TEST_F(EnergyNodeTest, TestPhaseCountFallback) {
|
||||
// Create entry without phase count
|
||||
auto entry = create_test_entry(7000.0); // No phase count specified
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 2, 230.0, true);
|
||||
|
||||
// Should use 2 phases from module config
|
||||
// 7000W / (230V * 2 phases) = 15.22A
|
||||
float expected_current = 7000.0f / (230.0f * 2.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, expected_current, 0.01f);
|
||||
}
|
||||
|
||||
/// Test default to 1 phase when no phase count available
|
||||
TEST_F(EnergyNodeTest, TestPhaseCountDefault) {
|
||||
// Create entry without phase count
|
||||
auto entry = create_test_entry(7000.0); // No phase count specified
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 0, 230.0, true);
|
||||
|
||||
// Should default to 1 phase
|
||||
// 7000W / (230V * 1 phase) = 30.43A
|
||||
float expected_current = 7000.0f / (230.0f * 1.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, expected_current, 0.01f);
|
||||
}
|
||||
|
||||
/// Test that existing current limits are not overwritten
|
||||
TEST_F(EnergyNodeTest, TestPreserveExistingCurrentLimits) {
|
||||
auto entry = create_test_entry(7000.0); // 7kW total power
|
||||
entry.limits_to_root.ac_max_current_A =
|
||||
types::energy::NumberWithSource{25.0f, "existing_source"}; // Pre-existing current limit
|
||||
entry.limits_to_leaves.ac_max_current_A = types::energy::NumberWithSource{20.0f, "existing_source"};
|
||||
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 3, 230.0, true);
|
||||
|
||||
// Existing current limits should be preserved
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_EQ(schedule[0].limits_to_root.ac_max_current_A->value, 25.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_leaves.ac_max_current_A.has_value());
|
||||
EXPECT_EQ(schedule[0].limits_to_leaves.ac_max_current_A->value, 20.0f);
|
||||
}
|
||||
|
||||
/// Test fuse limit still applies when enhancement is enabled
|
||||
TEST_F(EnergyNodeTest, TestFuseLimitWithEnhancement) {
|
||||
auto entry = create_test_entry(10000.0); // 10kW would calculate to ~14.5A with 3 phases
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 10.0, 3, 230.0, true);
|
||||
|
||||
// Calculated current would be ~14.5A, but fuse limit of 10A should be applied
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_EQ(schedule[0].limits_to_root.ac_max_current_A->value, 10.0f);
|
||||
}
|
||||
|
||||
/// Test multiple schedule entries
|
||||
TEST_F(EnergyNodeTest, TestMultipleEntries) {
|
||||
auto entry1 = create_test_entry(3000.0, 1); // 3kW, 1 phase
|
||||
auto entry2 = create_test_entry(6000.0, 2); // 6kW, 2 phases
|
||||
auto entry3 = create_test_entry(9000.0); // 9kW, use module config (3 phases)
|
||||
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry1, entry2, entry3};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 3, 230.0, true);
|
||||
|
||||
// Entry 1: 3000W / (230V * 1) = 13.04A
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, 3000.0f / 230.0f, 0.01f);
|
||||
|
||||
// Entry 2: 6000W / (230V * 2) = 13.04A
|
||||
EXPECT_TRUE(schedule[1].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[1].limits_to_root.ac_max_current_A->value, 6000.0f / (230.0f * 2.0f), 0.01f);
|
||||
|
||||
// Entry 3: 9000W / (230V * 3) = 13.04A
|
||||
EXPECT_TRUE(schedule[2].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[2].limits_to_root.ac_max_current_A->value, 9000.0f / (230.0f * 3.0f), 0.01f);
|
||||
}
|
||||
|
||||
/// Test configurable voltage - 120V (US standard)
|
||||
TEST_F(EnergyNodeTest, TestConfigurableVoltage120V) {
|
||||
auto entry = create_test_entry(7000.0); // 7kW total power
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 3, 120.0, true);
|
||||
|
||||
// Calculate expected current: 7000W / (120V * 3 phases) = 19.44A
|
||||
float expected_current = 7000.0f / (120.0f * 3.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, expected_current, 0.01f);
|
||||
}
|
||||
|
||||
/// Test configurable voltage - 400V (3-phase industrial)
|
||||
TEST_F(EnergyNodeTest, TestConfigurableVoltage400V) {
|
||||
auto entry = create_test_entry(22000.0); // 22kW total power
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 32.0, 3, 400.0, true);
|
||||
|
||||
// Calculate expected current: 22000W / (400V * 3 phases) = 18.33A
|
||||
float expected_current = 22000.0f / (400.0f * 3.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_NEAR(schedule[0].limits_to_root.ac_max_current_A->value, expected_current, 0.01f);
|
||||
}
|
||||
|
||||
/// Test configurable voltage - 240V (US split-phase)
|
||||
TEST_F(EnergyNodeTest, TestConfigurableVoltage240V) {
|
||||
auto entry = create_test_entry(19200.0); // 19.2kW total power
|
||||
std::vector<types::energy::ScheduleReqEntry> schedule = {entry};
|
||||
|
||||
process_schedule_with_limits(schedule, 40.0, 2, 240.0, true);
|
||||
|
||||
// Calculate expected current: 19200W / (240V * 2 phases) = 40A
|
||||
float expected_current = 19200.0f / (240.0f * 2.0f);
|
||||
|
||||
EXPECT_TRUE(schedule[0].limits_to_root.ac_max_current_A.has_value());
|
||||
EXPECT_EQ(schedule[0].limits_to_root.ac_max_current_A->value, expected_current);
|
||||
}
|
||||
Reference in New Issue
Block a user