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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,16 @@
load("//modules:module.bzl", "cc_everest_module")
IMPLS = [
"main",
]
cc_everest_module(
name = "EnergyManager",
impls = IMPLS,
srcs = glob(
[
"*.cpp",
"*.hpp",
],
),
)

View File

@@ -0,0 +1,265 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "Broker.hpp"
#include <everest/logging.hpp>
#include <fmt/core.h>
namespace module {
Broker::Broker(Market& _market, BrokerContext& _context, EnergyManagerConfig _config) :
local_market(_market),
context(_context),
config(_config),
first_trade(globals.schedule_length, true),
slot_type(globals.schedule_length, SlotType::Undecided),
num_phases(globals.schedule_length, {0, "empty"}) {
}
Market& Broker::get_local_market() {
return local_market;
}
bool Broker::trade(Offer& _offer) {
offer = &_offer;
if (globals.debug) {
EVLOG_info << local_market.energy_flow_request.uuid << " Broker: " << *offer;
}
// create a new schedules that contains everything we want to buy
trading = globals.empty_schedule_res;
// buy/sell nothing in the beginning
for (int i = 0; i < globals.schedule_length; i++) {
// make this more readable
auto& max_current = offer->import_offer[i].limits_to_root.ac_max_current_A;
auto& total_power = offer->import_offer[i].limits_to_root.total_power_W;
if (max_current.has_value()) {
trading[i].limits_to_root.ac_max_current_A = {0.};
}
if (total_power.has_value()) {
trading[i].limits_to_root.total_power_W = {0.};
}
}
traded = false;
if (offer->import_offer.size() != offer->export_offer.size()) {
EVLOG_error << "import and export offer do not have the same size!";
return false;
}
// Run the actual broker implementation, depending on strategy. This may change the value of traded.
tradeImpl();
// if we want to buy anything:
if (traded) {
if (globals.debug) {
EVLOG_info << fmt::format("\033[1;33m {}A {}W \033[1;0m",
(trading[0].limits_to_root.ac_max_current_A.has_value()
? std::to_string(trading[0].limits_to_root.ac_max_current_A.value().value)
: " [NOT_SET] "),
(trading[0].limits_to_root.total_power_W.has_value()
? std::to_string(trading[0].limits_to_root.total_power_W.value().value)
: " [NOT_SET] "));
}
// execute the trade on the market
local_market.trade(trading);
return true;
} else {
if (globals.debug) {
EVLOG_info << fmt::format("\033[1;33m NO TRADE \033[1;0m");
}
// execute the zero trade on the market
local_market.trade(trading);
// If no trade happens for the first time after successful tradings, set the source. If trading happens again,
// clear the source again. If this is a second call to no trade, do not update source.
return false;
}
}
date::utc_clock::time_point Broker::to_timestamp(const types::energy::ScheduleReqEntry& entry) {
return Everest::Date::from_rfc3339(entry.timestamp);
}
bool Broker::time_slot_active(const int i, const ScheduleReq& offer) {
const auto& now = globals.start_time;
const auto t_i = to_timestamp(offer[i]);
int active_slot = 0;
// Get active slot:
if (now < to_timestamp(offer[0])) {
// First element already in the future
active_slot = 0;
} else if (now > to_timestamp(offer[offer.size() - 1])) {
// Last element in the past
active_slot = offer.size() - 1;
} else {
// Somewhere in between
for (int n = 0; n < offer.size() - 1; n++) {
if (now > to_timestamp(offer[n]) and now < to_timestamp(offer[n + 1])) {
active_slot = n;
break;
}
}
}
return active_slot == i;
}
bool Broker::buy_ampere_import(int index, float ampere, bool allow_less,
types::energy::IntegerWithSource number_of_phases) {
return buy_ampere(offer->import_offer[index], index, ampere, allow_less, true, number_of_phases);
}
bool Broker::buy_ampere_export(int index, float ampere, bool allow_less,
types::energy::IntegerWithSource number_of_phases) {
return buy_ampere(offer->export_offer[index], index, ampere, allow_less, false, number_of_phases);
}
bool Broker::buy_watt_import(int index, float watt, bool allow_less) {
return buy_watt(offer->import_offer[index], index, watt, allow_less, true);
}
bool Broker::buy_watt_export(int index, float watt, bool allow_less) {
return buy_watt(offer->export_offer[index], index, watt, allow_less, false);
}
bool Broker::buy_ampere(const types::energy::ScheduleReqEntry& _offer, int index, float ampere, bool allow_less,
bool import, types::energy::IntegerWithSource number_of_phases) {
// make this more readable
auto& max_current = _offer.limits_to_root.ac_max_current_A;
auto& total_power = _offer.limits_to_root.total_power_W;
if (!max_current.has_value()) {
// no ampere limit set, cannot do anything here.
EVLOG_error << "[FAIL] called buy_ampere with only watt limit available.";
return false;
}
// enough ampere available?
if (max_current.value().value >= ampere) {
// do we have an additional watt limit?
if (total_power.has_value()) {
// is the watt limit high enough?
if (total_power.value().value >= ampere * number_of_phases.value * local_market.nominal_ac_voltage()) {
// yes, buy both ampere and watt
// EVLOG_info << "[OK] buy amps and total power is big enough for trade of " << a << "A /"
// << a * number_of_phases * local_market.nominal_ac_voltage();
buy_ampere_unchecked(index, {(import ? +1 : -1) * ampere, max_current.value().source},
number_of_phases);
// It was not actually limited by the watt limit, set the source from ampere limit
buy_watt_unchecked(
index, {(import ? +1 : -1) * ampere * number_of_phases.value * local_market.nominal_ac_voltage(),
max_current.value().source});
return true;
}
} else {
// no additional watt limit, let's just buy the ampere value
// EVLOG_info << "[OK] total power is not set, buying amps only " << a;
buy_ampere_unchecked(index, {(import ? +1 : -1) * ampere, max_current.value().source}, number_of_phases);
return true;
}
}
// we are still here, so we were not successfull in buying what we wanted.
// should we try to buy the leftovers?
if (allow_less && max_current.value().value > 0.) {
// we have an additional watt limit
if (total_power.has_value()) {
if (total_power.value().value > 0) {
// is the watt limit high enough?
if (total_power.value().value >=
max_current.value().value * number_of_phases.value * local_market.nominal_ac_voltage()) {
// yes, buy both ampere and watt
// EVLOG_info << "[OK leftovers] total power is big enough for trade of "
// << a * number_of_phases * local_market.nominal_ac_voltage();
buy_ampere_unchecked(index,
{(import ? +1 : -1) * max_current.value().value, max_current.value().source},
number_of_phases);
// It was not actually limited by the watt limit, set the source from ampere limit
buy_watt_unchecked(index, {(import ? +1 : -1) * max_current.value().value * number_of_phases.value *
local_market.nominal_ac_voltage(),
max_current.value().source});
return true;
} else {
// watt limit is lower, try to reduce ampere
float reduced_ampere =
total_power.value().value / number_of_phases.value / local_market.nominal_ac_voltage();
// EVLOG_info << "[OK leftovers] total power is not big enough, buy reduced current " <<
// reduced_ampere
// << reduced_ampere * number_of_phases * local_market.nominal_ac_voltage();
// Actually limited by watt limit, so use the watt source
buy_ampere_unchecked(index, {(import ? +1 : -1) * reduced_ampere, total_power.value().source},
number_of_phases);
buy_watt_unchecked(index, {(import ? +1 : -1) * reduced_ampere * number_of_phases.value *
local_market.nominal_ac_voltage(),
total_power.value().source});
return true;
}
} else {
// Don't buy anything if the total power limit is 0
return false;
}
} else {
buy_ampere_unchecked(index, {(import ? +1 : -1) * max_current.value().value, max_current.value().source},
number_of_phases);
return true;
}
}
return false;
}
bool Broker::buy_watt(const types::energy::ScheduleReqEntry& _offer, int index, float watt, bool allow_less,
bool import) {
// make this more readable
auto& total_power = _offer.limits_to_root.total_power_W;
if (!total_power.has_value()) {
// no watt limit set, cannot do anything here.
EVLOG_error << "[FAIL] called buy watt with no watt limit available.";
return false;
}
// enough watt available?
if (total_power.value().value >= watt) {
// EVLOG_info << "[OK] enough power available, buying " << a;
buy_watt_unchecked(index, {(import ? +1 : -1) * watt, total_power.value().source});
return true;
}
// we are still here, so we were not successfull in buying what we wanted.
// should we try to buy the leftovers?
if (allow_less && total_power.value().value > 0.) {
// EVLOG_info << "[OK] buying leftovers " << total_power.value();
buy_watt_unchecked(index, {(import ? +1 : -1) * total_power.value().value, total_power.value().source});
return true;
}
return false;
}
void Broker::buy_ampere_unchecked(int index, types::energy::NumberWithSource ampere,
types::energy::IntegerWithSource number_of_phases) {
trading[index].limits_to_root.ac_max_current_A = ampere;
trading[index].limits_to_root.ac_max_phase_count = number_of_phases;
traded = true;
first_trade[index] = false;
}
void Broker::buy_watt_unchecked(int index, types::energy::NumberWithSource watt) {
trading[index].limits_to_root.total_power_W = watt;
traded = true;
}
} // namespace module

View File

@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef BROKER_HPP
#define BROKER_HPP
#include "Market.hpp"
#include "Offer.hpp"
namespace module {
enum class SlotType {
Import,
Export,
Undecided
};
// All context data that is stored in between optimization runs
struct BrokerContext {
BrokerContext() {
clear();
};
void clear() {
number_1ph3ph_cycles = 0;
last_ac_number_of_active_phases_import = 0;
ts_1ph_optimal = date::utc_clock::now();
};
int number_1ph3ph_cycles;
int last_ac_number_of_active_phases_import;
std::chrono::time_point<date::utc_clock> ts_1ph_optimal;
};
// base class for different Brokers
class Broker {
public:
// Enums and config for 3ph switching
// Check manifest.yaml of this module for description
enum class Switch1ph3phMode {
Never,
Oneway,
Both,
};
enum class StickyNess {
SinglePhase,
ThreePhase,
DontChange,
};
struct EnergyManagerConfig {
Switch1ph3phMode switch_1ph_3ph_mode{Switch1ph3phMode::Never};
StickyNess stickyness{StickyNess::DontChange};
int max_nr_of_switches_per_session{0};
int power_hysteresis_W{200};
int time_hysteresis_s{600};
};
Broker(Market& market, BrokerContext& context, EnergyManagerConfig config);
virtual ~Broker(){};
// Asks this broker to trade based on the given offer.
// The broker will decide how much / if it wants to trade and
// execute the trade directly on its local market (which was passed in the constructor)
// This function is called from the optimization loop whenever a new offer is available for this
// broker.
bool trade(Offer& offer);
// Actual implementation of the trading algorithm. This function must be overriden by the
// specific implementation class. It will be called from the trade() function of the base class.
virtual void tradeImpl() = 0;
Market& get_local_market();
protected:
void buy_ampere_unchecked(int index, types::energy::NumberWithSource ampere,
types::energy::IntegerWithSource number_of_phases);
void buy_watt_unchecked(int index, types::energy::NumberWithSource watt);
bool buy_ampere_import(int index, float ampere, bool allow_less, types::energy::IntegerWithSource number_of_phases);
bool buy_ampere_export(int index, float ampere, bool allow_less, types::energy::IntegerWithSource number_of_phases);
bool buy_ampere(const types::energy::ScheduleReqEntry& _offer, int index, float ampere, bool allow_less,
bool import, types::energy::IntegerWithSource number_of_phases);
bool buy_watt_import(int index, float watt, bool allow_less);
bool buy_watt_export(int index, float watt, bool allow_less);
bool buy_watt(const types::energy::ScheduleReqEntry& _offer, int index, float watt, bool allow_less, bool import);
date::utc_clock::time_point to_timestamp(const types::energy::ScheduleReqEntry& entry);
bool time_slot_active(const int i, const ScheduleReq& offer);
// reference to local market at the broker's node
Market& local_market;
std::vector<bool> first_trade;
std::vector<SlotType> slot_type;
std::vector<types::energy::IntegerWithSource> num_phases;
Offer* offer{nullptr};
BrokerContext& context;
ScheduleRes trading;
bool traded{false};
EnergyManagerConfig config;
};
} // namespace module
#endif // BROKER_HPP

View File

@@ -0,0 +1,204 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "BrokerFastCharging.hpp"
#include <everest/logging.hpp>
#include <fmt/core.h>
namespace module {
void BrokerFastCharging::tradeImpl() {
// the offer contains all data we need to decide on a trade
// we can now buy from/sell to according to the offer at our local market place for this evse
// our strategy is to charge if we can, and only discharge if charging is not possible. In both
// cases we buy as much as possible (this is the fast charging implementation).
// if we have not bought anything, we first need to buy the minimal limits for ac_amp if any.
for (int i = 0; i < globals.schedule_length; i++) {
bool time_slot_is_active = time_slot_active(i, offer->import_offer);
// make this more readable
auto& max_current_import = offer->import_offer[i].limits_to_root.ac_max_current_A;
const auto& min_current_import = offer->import_offer[i].limits_to_root.ac_min_current_A;
auto& total_power_import = offer->import_offer[i].limits_to_root.total_power_W;
auto& max_current_export = offer->export_offer[i].limits_to_root.ac_max_current_A;
const auto& min_current_export = offer->export_offer[i].limits_to_root.ac_min_current_A;
auto& total_power_export = offer->export_offer[i].limits_to_root.total_power_W;
// If not specified, assume worst case (3ph being active)
const auto ac_number_of_active_phases_import =
offer->import_offer[i].limits_to_root.ac_number_of_active_phases.value_or(3);
const auto ac_number_of_active_phases_export =
offer->export_offer[i].limits_to_root.ac_number_of_active_phases.value_or(3);
const types::energy::IntegerWithSource three = {3, "BrokerFastCharging_Fallback"};
const auto max_phases_import = offer->import_offer[i].limits_to_root.ac_max_phase_count.value_or(three);
const auto min_phases_import = offer->import_offer[i].limits_to_root.ac_min_phase_count.value_or(three);
// in each timeslot: do we want to import or export energy?
if (slot_type[i] == SlotType::Undecided) {
bool can_import = !((total_power_import.has_value() && total_power_import.value().value == 0.) ||
(max_current_import.has_value() && max_current_import.value().value == 0.));
bool can_export = !((total_power_export.has_value() && total_power_export.value().value == 0.) ||
(max_current_export.has_value() && max_current_export.value().value == 0.));
if (can_import) {
slot_type[i] = SlotType::Import;
} else if (can_export) {
slot_type[i] = SlotType::Export;
}
}
if (num_phases[i].value == 0) {
num_phases[i] = {ac_number_of_active_phases_import, "BrokerFastCharging_NumPhasesActive"};
}
if (slot_type[i] == SlotType::Import) {
// EVLOG_info << "We can import.";
if (max_current_import.has_value()) {
// A current limit is set
// If an additional watt limit is set check phases, else it is max_phases (typically 3)
// First decide if we would like to charge 1 phase or 3 phase (if switching is possible at all)
// - Check if we are below e.g. 4.2kW (min_current*voltage*3) -> we have to do single phase
// - Check if we are above e.g. 4.4kW (min_current*voltage*3 + watt_hysteresis) -> we want to go three
// phase
// - If we are in between, use what is currently active (hysteresis)
types::energy::IntegerWithSource number_of_phases = {ac_number_of_active_phases_import,
"BrokerFastCharging_Default"};
float min_power_3ph = 0.;
if (min_current_import.has_value()) {
min_power_3ph =
min_current_import.value().value * max_phases_import.value * local_market.nominal_ac_voltage();
}
bool number_of_switching_cycles_reached = false;
if (first_trade[i]) {
if (config.switch_1ph_3ph_mode not_eq Switch1ph3phMode::Never and total_power_import.has_value() &&
min_power_3ph > 0.) {
if (total_power_import.value().value < min_power_3ph) {
// We have to do single phase, it is impossible with 3ph
number_of_phases = {min_phases_import.value, total_power_import.value().source};
} else if (config.switch_1ph_3ph_mode == Switch1ph3phMode::Both and
total_power_import.value().value > min_power_3ph + config.power_hysteresis_W) {
number_of_phases = max_phases_import;
} else {
// Keep number of phases as they are
number_of_phases = {ac_number_of_active_phases_import, "BrokerFastCharging_KeepNrOfPhases"};
}
// Now we made the decision what the optimal number of phases would be (in variable
// number_of_phases) We also have a time based hysteresis as well as some limits in maximum
// number of switching cycles. This means we maybe cannot use the optimal number of phases just
// now. Check those conditions and adjust number_of_phases accordingly.
if (config.max_nr_of_switches_per_session > 0 and
context.number_1ph3ph_cycles > config.max_nr_of_switches_per_session) {
number_of_switching_cycles_reached = true;
if (config.stickyness == StickyNess::SinglePhase) {
number_of_phases = min_phases_import;
} else if (config.stickyness == StickyNess::ThreePhase) {
number_of_phases = max_phases_import;
} else {
number_of_phases = {ac_number_of_active_phases_import,
"BrokerFastCharging_KeepNrOfPhases"};
}
}
if (number_of_phases.value == min_phases_import.value and time_slot_is_active) {
context.ts_1ph_optimal = date::utc_clock::now();
}
if (config.time_hysteresis_s > 0 and time_slot_is_active) {
// Check time based hysteresis:
// - store timestamp whenever 1ph is optimal (update continously)
// Then now-timestamp is the stable time period for a 3ph condition.
// This should only be done in the currently active time slot. Ignore time hysteresis in
// other slots in the future or past.
// Only allow an actual change to 3ph if the time exceeds the configured hysteresis limit.
const auto stable_3ph = std::chrono::duration_cast<std::chrono::seconds>(
globals.start_time - context.ts_1ph_optimal)
.count();
if (stable_3ph < config.time_hysteresis_s and
number_of_phases.value == max_phases_import.value) {
number_of_phases = min_phases_import;
}
}
} else {
number_of_phases = max_phases_import;
}
}
// store decision in context
if (ac_number_of_active_phases_import not_eq context.last_ac_number_of_active_phases_import) {
context.number_1ph3ph_cycles++;
}
context.last_ac_number_of_active_phases_import = ac_number_of_active_phases_import;
if (first_trade[i] && min_current_import.has_value() && min_current_import.value().value > 0.) {
num_phases[i] = number_of_phases;
// EVLOG_info << "I: first trade: try to buy minimal current_A on AC: " <<
// min_current_import.value();
// try to buy minimal current_A if we are on AC, but don't buy less.
if (not buy_ampere_import(i, min_current_import.value().value, false, number_of_phases) and
config.switch_1ph_3ph_mode not_eq Switch1ph3phMode::Never and
not number_of_switching_cycles_reached) {
// If we cannot buy the minimum amount we need, try again in single phase mode (it may be due to
// a watt limit only)
number_of_phases = {1, "BrokerFastCharging_Buy3phFailed"};
num_phases[i] = number_of_phases;
buy_ampere_import(i, min_current_import.value().value, false, number_of_phases);
}
/*EVLOG_info << "I: " << i << " -- 1ph3ph: " << min_power_3ph << " active_nr_phases "
<< ac_number_of_active_phases_import << " cycles " << context.number_1ph3ph_cycles
<< " number_of_phases " << number_of_phases << " time_slot_active "
<< time_slot_is_active;*/
} else {
// EVLOG_info << "I: Not first trade or nor min current needed.";
// try to buy a slice but allow less to be bought
buy_ampere_import(i, globals.slice_ampere, true, num_phases[i]);
}
} else if (total_power_import.has_value()) {
// only a watt limit is available
// EVLOG_info << "I: Only watt limit is set." << total_power_import.value();
buy_watt_import(i, globals.slice_watt, true);
}
} else if (slot_type[i] == SlotType::Export) {
// EVLOG_info << "We can export.";
// we cannot import, try exporting in this timeslot.
if (max_current_export.has_value()) {
// A current limit is set
if (first_trade[i] && min_current_export.has_value() && min_current_export.value().value > 0.) {
// EVLOG_info << "E: first trade: try to buy minimal current_A on AC: " <<
// min_current_export.value();
// try to buy minimal current_A if we are on AC, but don't buy less.
buy_ampere_export(i, min_current_export.value().value, false, {3, "BrokerFastCharging_FixedValue"});
} else {
// EVLOG_info << "E: Not first trade or nor min current needed.";
// try to buy a slice but allow less to be bought
buy_ampere_export(i, globals.slice_ampere, true, {3, "BrokerFastCharging_FixedValue"});
}
} else if (total_power_export.has_value()) {
// only a watt limit is available
// EVLOG_info << "E: Only watt limit is set." << total_power_export.value();
buy_watt_export(i, globals.slice_watt, true);
}
} else {
// EVLOG_info << "We can neither import nor export.";
}
}
}
} // namespace module

View File

@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef BROKER_FAST_CHARGING_HPP
#define BROKER_FAST_CHARGING_HPP
#include "Broker.hpp"
namespace module {
// This broker tries to charge as fast as possible.
class BrokerFastCharging : public Broker {
public:
explicit BrokerFastCharging(Market& market, BrokerContext& context, EnergyManagerConfig config) :
Broker(market, context, config){};
virtual void tradeImpl() override;
private:
};
} // namespace module
#endif // BROKER_FAST_CHARGING_HPP

View File

@@ -0,0 +1,33 @@
#
# 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_sources(${MODULE_NAME}
PRIVATE
Market.cpp
Broker.cpp
Offer.cpp
BrokerFastCharging.cpp
EnergyManagerImpl.cpp
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/energy_managerImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# insert other things like install cmds etc here
if(EVEREST_CORE_BUILD_TESTING)
add_subdirectory(tests)
endif()
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "EnergyManager.hpp"
#include "Broker.hpp"
#include "BrokerFastCharging.hpp"
#include "Market.hpp"
#include <fmt/core.h>
#include <optional>
namespace module {
void EnergyManager::init() {
EnergyManagerConfig energy_manager_config;
energy_manager_config.nominal_ac_voltage = config.nominal_ac_voltage;
energy_manager_config.update_interval = config.update_interval;
energy_manager_config.schedule_interval_duration = config.schedule_interval_duration;
energy_manager_config.schedule_total_duration = config.schedule_total_duration;
energy_manager_config.slice_ampere = config.slice_ampere;
energy_manager_config.slice_watt = config.slice_watt;
energy_manager_config.debug = config.debug;
energy_manager_config.switch_3ph1ph_while_charging_mode = config.switch_3ph1ph_while_charging_mode;
energy_manager_config.switch_3ph1ph_max_nr_of_switches_per_session =
config.switch_3ph1ph_max_nr_of_switches_per_session;
energy_manager_config.switch_3ph1ph_switch_limit_stickyness = config.switch_3ph1ph_switch_limit_stickyness;
energy_manager_config.switch_3ph1ph_power_hysteresis_W = config.switch_3ph1ph_power_hysteresis_W;
energy_manager_config.switch_3ph1ph_time_hysteresis_s = config.switch_3ph1ph_time_hysteresis_s;
const auto enforce_limits_callback = [this](const std::vector<types::energy::EnforcedLimits>& limits) {
const types::energy::NumberWithSource nonumber = {-9999.0};
const types::energy::IntegerWithSource noint = {-9999};
for (const auto& it : limits) {
if (globals.debug)
EVLOG_info << fmt::format("\033[1;92m{} Enforce limits {}A {}W {} ph\033[1;0m", it.uuid,
it.limits_root_side.ac_max_current_A.value_or(nonumber).value,
it.limits_root_side.total_power_W.value_or(nonumber).value,
it.limits_root_side.ac_max_phase_count.value_or(noint).value);
r_energy_trunk->call_enforce_limits(it);
}
};
this->impl = std::make_unique<EnergyManagerImpl>(energy_manager_config, enforce_limits_callback);
r_energy_trunk->subscribe_energy_flow_request(
[this](types::energy::EnergyFlowRequest const& e) { this->impl->on_energy_flow_request(e); });
invoke_init(*p_main);
}
void EnergyManager::ready() {
invoke_ready(*p_main);
this->impl->start();
}
} // namespace module

View File

@@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef ENERGY_MANAGER_HPP
#define ENERGY_MANAGER_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/energy_manager/Implementation.hpp>
// headers for required interface implementations
#include <generated/interfaces/energy/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
#include "EnergyManagerImpl.hpp"
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
double nominal_ac_voltage;
int update_interval;
int schedule_interval_duration;
int schedule_total_duration;
double slice_ampere;
double slice_watt;
bool debug;
std::string switch_3ph1ph_while_charging_mode;
int switch_3ph1ph_max_nr_of_switches_per_session;
std::string switch_3ph1ph_switch_limit_stickyness;
int switch_3ph1ph_power_hysteresis_W;
int switch_3ph1ph_time_hysteresis_s;
};
class EnergyManager : public Everest::ModuleBase {
public:
EnergyManager() = delete;
EnergyManager(const ModuleInfo& info, std::unique_ptr<energy_managerImplBase> p_main,
std::unique_ptr<energyIntf> r_energy_trunk, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), r_energy_trunk(std::move(r_energy_trunk)), config(config){};
const std::unique_ptr<energy_managerImplBase> p_main;
const std::unique_ptr<energyIntf> r_energy_trunk;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// 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
std::unique_ptr<EnergyManagerImpl> impl;
// 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_MANAGER_HPP

View File

@@ -0,0 +1,228 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <EnergyManagerImpl.hpp>
#include <chrono>
#include <fstream>
#include "Broker.hpp"
#include "BrokerFastCharging.hpp"
#include "Market.hpp"
namespace module {
static BrokerFastCharging::Switch1ph3phMode to_switch_1ph3ph_mode(const std::string& m) {
if (m == "Both") {
return BrokerFastCharging::Switch1ph3phMode::Both;
} else if (m == "Oneway") {
return BrokerFastCharging::Switch1ph3phMode::Oneway;
} else {
return BrokerFastCharging::Switch1ph3phMode::Never;
}
}
static BrokerFastCharging::StickyNess to_stickyness(const std::string& m) {
if (m == "DontChange") {
return BrokerFastCharging::StickyNess::DontChange;
} else if (m == "SinglePhase") {
return BrokerFastCharging::StickyNess::SinglePhase;
} else {
return BrokerFastCharging::StickyNess::ThreePhase;
}
}
static BrokerFastCharging::EnergyManagerConfig to_broker_fast_charging_config(const EnergyManagerConfig& config) {
BrokerFastCharging::EnergyManagerConfig broker_conf;
broker_conf.max_nr_of_switches_per_session = config.switch_3ph1ph_max_nr_of_switches_per_session;
broker_conf.power_hysteresis_W = config.switch_3ph1ph_power_hysteresis_W;
broker_conf.switch_1ph_3ph_mode = to_switch_1ph3ph_mode(config.switch_3ph1ph_while_charging_mode);
broker_conf.time_hysteresis_s = config.switch_3ph1ph_time_hysteresis_s;
broker_conf.stickyness = to_stickyness(config.switch_3ph1ph_switch_limit_stickyness);
return broker_conf;
}
// Check if any node set the priority request flag
bool 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;
}
EnergyManagerImpl::EnergyManagerImpl(
const EnergyManagerConfig& config,
const std::function<void(const std::vector<types::energy::EnforcedLimits>& limits)>& enforced_limits_callback) :
config(config), enforced_limits_callback(enforced_limits_callback) {
this->energy_flow_request.node_type = types::energy::NodeType::Undefined;
}
void EnergyManagerImpl::start() {
// start thread to update energy optimization
std::thread([this] {
while (true) {
auto optimized_values = this->run_optimizer(energy_flow_request, date::utc_clock::now());
enforced_limits_callback(optimized_values);
{
std::unique_lock<std::mutex> lock(mainloop_sleep_mutex);
mainloop_sleep_condvar.wait_for(lock, std::chrono::seconds(config.update_interval));
}
}
}).detach();
}
void EnergyManagerImpl::on_energy_flow_request(const types::energy::EnergyFlowRequest& e) {
// 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();
}
}
std::vector<types::energy::EnforcedLimits>
EnergyManagerImpl::run_optimizer(const types::energy::EnergyFlowRequest& request,
date::utc_clock::time_point start_time, const std::string& test_name) {
std::scoped_lock lock(energy_mutex);
globals.init(start_time, config.schedule_interval_duration, config.schedule_total_duration, config.slice_ampere,
config.slice_watt, config.debug, request);
time_probe optimizer_start;
optimizer_start.start();
if (globals.debug)
EVLOG_info << "\033[1;44m---------------- Run energy optimizer ---------------- \033[1;0m";
time_probe market_tp;
// create market for trading energy based on the request tree
market_tp.start();
Market market(request, config.nominal_ac_voltage);
market_tp.pause();
// create brokers for all evses (they buy/sell energy on behalf of EvseManagers)
std::vector<std::shared_ptr<Broker>> brokers;
auto evse_markets = market.get_list_of_evses();
for (auto m : evse_markets) {
// Check if we need to clear the context
// Note that context is created here if it does not exist implicitly by operator[] of the map
if (m->energy_flow_request.evse_state == types::energy::EvseState::Unplugged or
m->energy_flow_request.evse_state == types::energy::EvseState::Finished) {
contexts[m->energy_flow_request.uuid].clear();
contexts[m->energy_flow_request.uuid].ts_1ph_optimal =
globals.start_time - std::chrono::seconds(config.switch_3ph1ph_time_hysteresis_s);
}
// FIXME: check for actual optimizer_targets and create correct broker for this evse
// For now always create simple FastCharging broker
brokers.push_back(std::make_shared<BrokerFastCharging>(*m, contexts[m->energy_flow_request.uuid],
to_broker_fast_charging_config(config)));
// EVLOG_info << fmt::format("Created broker for {}", m->energy_flow_request.uuid);
}
// for each evse: create a custom offer at their local market place and ask the broker to buy a slice.
// continue until no one wants to buy/sell anything anymore.
int max_number_of_trading_rounds = 100;
time_probe offer_tp;
time_probe broker_tp;
while (max_number_of_trading_rounds-- > 0) {
bool trade_happend_in_this_round = false;
for (auto const& broker : brokers) {
// EVLOG_info << broker->get_local_market().energy_flow_request;
// create local offer at evse's marketplace
offer_tp.start();
Offer local_offer(broker->get_local_market());
offer_tp.pause();
// ask broker to trade
broker_tp.start();
if (broker->trade(local_offer))
trade_happend_in_this_round = true;
broker_tp.pause();
}
if (!trade_happend_in_this_round)
break;
}
if (max_number_of_trading_rounds <= 0) {
EVLOG_error << "Trading: Maximum number of trading rounds reached.";
}
if (globals.debug) {
EVLOG_info << fmt::format("\033[1;44m---------------- End energy optimizer ({} rounds, offer {}ms market {}ms "
"broker {}ms total {}ms) ---------------- \033[1;0m",
100 - max_number_of_trading_rounds, offer_tp.stop(), market_tp.stop(),
broker_tp.stop(), optimizer_start.stop());
}
std::vector<types::energy::EnforcedLimits> optimized_values;
optimized_values.reserve(brokers.size());
for (auto& broker : brokers) {
auto& local_market = broker->get_local_market();
const auto sold_energy = local_market.get_sold_energy();
if (sold_energy.size() > 0) {
types::energy::EnforcedLimits l;
l.uuid = local_market.energy_flow_request.uuid;
l.valid_for = config.update_interval * 10;
l.schedule = sold_energy;
// select root limit from schedule based on globals.start_time
l.limits_root_side = sold_energy[0].limits_to_root;
for (const auto& s : sold_energy) {
const auto schedule_time = Everest::Date::from_rfc3339(s.timestamp);
if (globals.start_time < schedule_time) {
// all further schedules will be further into the future
break;
} else {
// use this schedule as the starting point
l.limits_root_side = s.limits_to_root;
}
}
optimized_values.push_back(l);
if (globals.debug) {
EVLOG_info << "Sending enforced limits (import) to :" << l.uuid << " " << l.limits_root_side;
}
}
}
// Print out test case file
if (not test_name.empty()) {
json test_case;
test_case["start_time"] = Everest::Date::to_rfc3339(start_time);
test_case["request"] = json(request);
test_case["expected_result"] = json(optimized_values);
std::ofstream out(test_name.c_str());
out << test_case;
out.close();
}
return optimized_values;
}
} // namespace module

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
// headers for provided interface implementations
#include <generated/interfaces/energy_manager/Implementation.hpp>
// headers for required interface implementations
#include <generated/interfaces/energy/Interface.hpp>
#include <mutex>
#include <Broker.hpp>
namespace module {
struct EnergyManagerConfig {
double nominal_ac_voltage;
int update_interval;
int schedule_interval_duration;
int schedule_total_duration;
double slice_ampere;
double slice_watt;
bool debug;
std::string switch_3ph1ph_while_charging_mode;
int switch_3ph1ph_max_nr_of_switches_per_session;
std::string switch_3ph1ph_switch_limit_stickyness;
int switch_3ph1ph_power_hysteresis_W;
int switch_3ph1ph_time_hysteresis_s;
};
class EnergyManagerImpl {
public:
EnergyManagerImpl(
const EnergyManagerConfig& config,
const std::function<void(const std::vector<types::energy::EnforcedLimits>& limits)>& enforced_limits_callback);
/// \brief Starts and detaches worker thread that runs run_optimizer periodically or when energy flow request is
/// updated
void start();
/// \brief Updates the energy_flow_request and notifies the worker thread
/// \param e
void on_energy_flow_request(const types::energy::EnergyFlowRequest& e);
/// \brief Runs optimization on the given \p request
/// \param request
/// \param start_time
/// \return a vector of limits to enforce at the individual nodes of the \p request
std::vector<types::energy::EnforcedLimits> run_optimizer(const types::energy::EnergyFlowRequest& request,
date::utc_clock::time_point start_time,
const std::string& test_name = "");
private:
EnergyManagerConfig config;
std::function<void(const std::vector<types::energy::EnforcedLimits>& limits)> enforced_limits_callback;
std::mutex energy_mutex;
std::condition_variable mainloop_sleep_condvar;
std::mutex mainloop_sleep_mutex;
// complete energy tree request
types::energy::EnergyFlowRequest energy_flow_request;
std::map<std::string, BrokerContext> contexts;
};
} // namespace module

View File

@@ -0,0 +1,532 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "Market.hpp"
#include <everest/logging.hpp>
#include <fmt/core.h>
namespace module {
globals_t globals;
void globals_t::init(date::utc_clock::time_point _start_time, int _interval_duration, int _schedule_duration,
float _slice_ampere, float _slice_watt, bool _debug,
const types::energy::EnergyFlowRequest& energy_flow_request) {
start_time = _start_time;
interval_duration = std::chrono::minutes(_interval_duration);
schedule_length = std::chrono::hours(_schedule_duration) / interval_duration;
slice_ampere = _slice_ampere;
slice_watt = _slice_watt;
debug = _debug;
create_timestamps(energy_flow_request);
create_empty_schedule(zero_schedule_req);
for (auto& a : zero_schedule_req) {
a.limits_to_root.ac_max_current_A = {0.};
a.limits_to_root.total_power_W = {0.};
}
create_empty_schedule(empty_schedule_req);
create_empty_schedule(zero_schedule_res);
for (auto& a : zero_schedule_res) {
a.limits_to_root.ac_max_current_A = {0.};
a.limits_to_root.total_power_W = {0.};
}
create_empty_schedule(empty_schedule_res);
create_empty_schedule(empty_schedule_setpoints);
}
void globals_t::create_timestamps(const types::energy::EnergyFlowRequest& energy_flow_request) {
timestamps.clear();
timestamps.reserve(schedule_length);
auto minutes_overflow = start_time.time_since_epoch() % interval_duration;
auto start = start_time - minutes_overflow;
// Add leap seconds
date::get_leap_second_info(start_time);
auto timepoint = start + date::get_leap_second_info(start_time).elapsed;
// Insert all our pre defined time slots
for (int i = 0; i < schedule_length; i++) {
timestamps.push_back(timepoint);
timepoint += interval_duration;
}
// Insert timestamps of all requests
add_timestamps(energy_flow_request);
// sort
std::sort(timestamps.begin(), timestamps.end());
// remove duplicates
timestamps.erase(unique(timestamps.begin(), timestamps.end()), timestamps.end());
schedule_length = timestamps.size();
}
void globals_t::add_timestamps(const types::energy::EnergyFlowRequest& energy_flow_request) {
// add local timestamps
for (auto t : energy_flow_request.schedule_import) {
// insert current timestamp
timestamps.push_back(Everest::Date::from_rfc3339(t.timestamp));
}
for (auto t : energy_flow_request.schedule_export) {
// insert current timestamp
timestamps.push_back(Everest::Date::from_rfc3339(t.timestamp));
}
for (auto t : energy_flow_request.schedule_setpoints) {
// insert current timestamp
timestamps.push_back(Everest::Date::from_rfc3339(t.timestamp));
}
// recurse to all children
for (auto& c : energy_flow_request.children)
add_timestamps(c);
}
template <typename T> void globals_t::create_empty_schedule(T& s) {
// initialize schedule with correct size
typename T::value_type e;
s = T(schedule_length, e);
for (int i = 0; i < schedule_length; i++) {
s[i].timestamp = Everest::Date::to_rfc3339(timestamps[i]);
}
}
int time_probe::stop() {
if (running) {
pause();
}
return std::chrono::duration_cast<std::chrono::milliseconds>(total_duration).count();
}
void time_probe::start() {
timepoint_start = std::chrono::high_resolution_clock::now();
running = true;
}
void time_probe::pause() {
if (running) {
total_duration += std::chrono::high_resolution_clock::now() - timepoint_start;
running = false;
}
}
// returns the smaller of two optionals. Note that comparison operators on optionals are a little weird if not both
// sides have a value, we explicitly want:
// - If both are not set, it should return an empty optional
// - If either a or b is set but not both, return the one set.
// - If both have a value, return the smaller one.
template <typename T> std::optional<T> min_optional(std::optional<T> a, std::optional<T> b) {
if (a.has_value() and b.has_value()) {
if (a.value().value < b.value().value) {
return a;
} else {
return b;
}
}
if (a.has_value()) {
return a;
}
return b;
}
template <typename T> std::optional<T> max_optional(std::optional<T> a, std::optional<T> b) {
if (a.has_value() and b.has_value()) {
if (a.value().value > b.value().value) {
return a;
} else {
return b;
}
}
if (a.has_value()) {
return a;
}
return b;
}
ScheduleSetpoints Market::resample(const ScheduleSetpoints& request) {
ScheduleSetpoints sp = globals.empty_schedule_setpoints;
// First resample request to the timestamps in available and merge all limits on root sides
for (auto& s : sp) {
// find corresponding entry in request
auto r = request.begin();
auto tp_a = Everest::Date::from_rfc3339(s.timestamp);
for (auto ir = request.begin(); ir != request.end(); ir++) {
auto tp_r_1 = Everest::Date::from_rfc3339((*ir).timestamp);
if ((ir + 1 == request.end())) {
r = ir;
break;
}
auto tp_r_2 = Everest::Date::from_rfc3339((*(ir + 1)).timestamp);
if ((tp_a >= tp_r_1 && tp_a < tp_r_2) || (ir == request.begin() && tp_a < tp_r_1)) {
r = ir;
break;
}
}
if (r != request.end()) {
// copy setpoint if any
s.setpoint = (*r).setpoint;
}
}
return sp;
}
ScheduleReq Market::get_max_available_energy(const ScheduleReq& request) {
ScheduleReq available = globals.empty_schedule_req;
// First resample request to the timestamps in available and merge all limits on root sides
for (auto& a : available) {
// find corresponding entry in request
auto r = request.begin();
auto tp_a = Everest::Date::from_rfc3339(a.timestamp);
for (auto ir = request.begin(); ir != request.end(); ir++) {
auto tp_r_1 = Everest::Date::from_rfc3339((*ir).timestamp);
if ((ir + 1 == request.end())) {
r = ir;
break;
}
auto tp_r_2 = Everest::Date::from_rfc3339((*(ir + 1)).timestamp);
if ((tp_a >= tp_r_1 && tp_a < tp_r_2) || (ir == request.begin() && tp_a < tp_r_1)) {
r = ir;
break;
}
}
if (r != request.end()) {
{
auto leaves_power_W = (*r).limits_to_leaves.total_power_W;
if (leaves_power_W.has_value()) {
leaves_power_W.value().value =
leaves_power_W.value().value / (*r).conversion_efficiency.value_or(1.);
}
a.limits_to_root.total_power_W = min_optional(leaves_power_W, (*r).limits_to_root.total_power_W);
}
a.limits_to_root.ac_max_current_A =
min_optional((*r).limits_to_leaves.ac_max_current_A, (*r).limits_to_root.ac_max_current_A);
a.limits_to_root.ac_min_phase_count =
max_optional((*r).limits_to_root.ac_min_phase_count, (*r).limits_to_leaves.ac_min_phase_count);
a.limits_to_root.ac_max_phase_count =
min_optional((*r).limits_to_root.ac_max_phase_count, (*r).limits_to_leaves.ac_max_phase_count);
a.limits_to_root.ac_min_current_A =
max_optional((*r).limits_to_root.ac_min_current_A, (*r).limits_to_leaves.ac_min_current_A);
// all request limits have been merged on root side in available.
// copy other information if any
a.price_per_kwh = (*r).price_per_kwh;
a.limits_to_root.ac_number_of_active_phases = (*r).limits_to_root.ac_number_of_active_phases;
}
}
return available;
}
ScheduleReq Market::get_available_energy(const ScheduleReq& max_available, bool add_sold) {
ScheduleReq available = max_available;
for (ScheduleReq::size_type i = 0; i < available.size(); i++) {
// FIXME: sold_root is the sum of all energy sold, but we need to limit indivdual paths as well
// add config option for pure star type of cabling here as well.
float sold_current = 0;
if (sold_root[i].limits_to_root.ac_max_current_A.has_value()) {
sold_current = (add_sold ? 1 : -1) * sold_root[i].limits_to_root.ac_max_current_A.value().value;
}
if (sold_current > 0)
sold_current = 0;
float sold_watt = 0;
if (sold_root[i].limits_to_root.total_power_W.has_value()) {
sold_watt = (add_sold ? 1 : -1) * sold_root[i].limits_to_root.total_power_W.value().value;
}
if (sold_watt > 0)
sold_watt = 0;
if (available[i].limits_to_root.ac_max_current_A.has_value())
available[i].limits_to_root.ac_max_current_A.value().value += sold_current;
if (available[i].limits_to_root.total_power_W.has_value())
available[i].limits_to_root.total_power_W.value().value += sold_watt;
}
return available;
}
ScheduleReq Market::get_available_energy_import() {
return get_available_energy(import_max_available, false);
}
ScheduleReq Market::get_available_energy_export() {
return get_available_energy(export_max_available, true);
}
float get_watt_from_freq_table(const std::vector<types::energy::FrequencyWattPoint>& table, float freq) {
// the table has to be sorted by freqency
if (table.size() == 0) {
return 0.;
}
if (table.size() == 1) {
return table[0].total_power_W;
}
float watt1 = table[0].total_power_W;
float watt2 = 0.;
float freq1 = 0.;
for (const auto e : table) {
watt2 = e.total_power_W;
if (e.frequency_Hz > freq) {
break;
}
watt1 = e.total_power_W;
freq1 = e.frequency_Hz;
}
return watt1 + (freq - freq1) * (watt2 - watt1);
}
void apply_limit_if_smaller(std::optional<types::energy::NumberWithSource>& base, float limit,
const std::string& source) {
if (not base.has_value() or (base.has_value() and base.value().value > limit)) {
base = {limit, source};
}
}
void apply_setpoints(ScheduleReq& imp, ScheduleReq& exp, const ScheduleSetpoints& setpoints,
std::optional<float> freq) {
if (setpoints.size() != imp.size()) {
EVLOG_error << fmt::format("apply_setpoints: setpoints({}) and import({}) do not have the same size.",
setpoints.size(), imp.size());
return;
}
if (setpoints.size() != exp.size()) {
EVLOG_error << fmt::format("apply_setpoints: setpoints({}) and export({}) do not have the same size.",
setpoints.size(), exp.size());
return;
}
for (ScheduleReq::size_type i = 0; i < setpoints.size(); i++) {
// apply setpoints as limits
if (setpoints[i].setpoint.has_value()) {
const auto& sp = setpoints[i].setpoint.value();
auto& imp_limits = imp[i].limits_to_root;
auto& exp_limits = exp[i].limits_to_root;
// Allow only one actual setpoint value to be set, in this priority order
if (sp.ac_current_A.has_value()) {
if (sp.ac_current_A.value() >= 0.) {
// Charging setpoint
apply_limit_if_smaller(imp_limits.ac_max_current_A, sp.ac_current_A.value(), sp.source);
exp_limits.ac_max_current_A = {0., sp.source};
} else {
// Discharging setpoint
apply_limit_if_smaller(exp_limits.ac_max_current_A, -sp.ac_current_A.value(), sp.source);
imp_limits.ac_max_current_A = {0., sp.source};
}
} else if (sp.total_power_W.has_value()) {
if (sp.total_power_W.value() >= 0.) {
// Charging setpoint
apply_limit_if_smaller(imp_limits.total_power_W, sp.total_power_W.value(), sp.source);
exp_limits.total_power_W = {0., sp.source};
} else {
// Discharging setpoint
apply_limit_if_smaller(exp_limits.total_power_W, -sp.total_power_W.value(), sp.source);
imp_limits.total_power_W = {0., sp.source};
}
} else if (sp.frequency_table.has_value() and freq.has_value()) {
// get actual watt limit from table and current frequency from meter
float watt_limit = get_watt_from_freq_table(sp.frequency_table.value(), freq.value());
if (watt_limit >= 0.) {
// Charging setpoint
apply_limit_if_smaller(imp_limits.total_power_W, watt_limit, sp.source);
exp_limits.total_power_W = {0., sp.source};
} else {
// Discharging setpoint
apply_limit_if_smaller(exp_limits.total_power_W, -watt_limit, sp.source);
imp_limits.total_power_W = {0., sp.source};
}
}
}
}
}
Market::Market(const types::energy::EnergyFlowRequest& _energy_flow_request, const float __nominal_ac_voltage,
Market* __parent) :
energy_flow_request(_energy_flow_request), _parent(__parent), _nominal_ac_voltage(__nominal_ac_voltage) {
// EVLOG_info << "Create market for " << _energy_flow_request.uuid;
sold_root = globals.empty_schedule_res;
if (not energy_flow_request.schedule_import.empty()) {
import_max_available = get_max_available_energy(energy_flow_request.schedule_import);
} else {
// nothing is available as nothing was requested
import_max_available = globals.zero_schedule_req;
}
if (not energy_flow_request.schedule_export.empty()) {
export_max_available = get_max_available_energy(energy_flow_request.schedule_export);
} else {
// nothing is available as nothing was requested
export_max_available = globals.zero_schedule_req;
}
if (not energy_flow_request.schedule_setpoints.empty()) {
setpoints = resample(energy_flow_request.schedule_setpoints);
} else {
// create an empty setpoint schedule
setpoints = globals.empty_schedule_setpoints;
}
// Try to find a frequency measurement
std::optional<float> freq;
if (energy_flow_request.energy_usage_root.has_value() and
energy_flow_request.energy_usage_root.value().frequency_Hz.has_value()) {
freq = energy_flow_request.energy_usage_root.value().frequency_Hz.value().L1;
} else if (energy_flow_request.energy_usage_leaves.has_value() and
energy_flow_request.energy_usage_leaves.value().frequency_Hz.has_value()) {
freq = energy_flow_request.energy_usage_leaves.value().frequency_Hz.value().L1;
}
// Apply setpoints as limit to both import and export schedules
apply_setpoints(import_max_available, export_max_available, setpoints, freq);
// Recursion: create one Market for each child
for (auto& flow_child : _energy_flow_request.children) {
_children.emplace_back(flow_child, _nominal_ac_voltage, this);
}
}
ScheduleRes Market::get_sold_energy() {
return sold_root;
}
Market* Market::parent() {
return _parent;
}
bool Market::is_root() {
return _parent == nullptr;
}
void Market::get_list_of_evses(std::vector<Market*>& list) {
if (energy_flow_request.node_type == types::energy::NodeType::Evse) {
list.push_back(this);
}
for (auto& child : _children) {
child.get_list_of_evses(list);
}
}
std::vector<Market*> Market::get_list_of_evses() {
std::vector<Market*> list;
if (energy_flow_request.node_type == types::energy::NodeType::Evse) {
list.push_back(this);
}
for (auto& child : _children) {
child.get_list_of_evses(list);
}
return list;
}
static void schedule_add(ScheduleRes& a, const ScheduleRes& b) {
if (a.size() != b.size()) {
EVLOG_critical << "schedule_add: Schedules are not of the same size: a: " << a.size() << " b: " << b.size();
return;
}
const types::energy::NumberWithSource NUMZERO = {0};
for (ScheduleRes::size_type i = 0; i < a.size(); i++) {
if (b[i].limits_to_root.ac_max_current_A.has_value()) {
std::string source;
if (b[i].limits_to_root.ac_max_current_A.value().value not_eq 0.) {
source = b[i].limits_to_root.ac_max_current_A.value().source;
} else if (a[i].limits_to_root.ac_max_current_A.has_value()) {
source = a[i].limits_to_root.ac_max_current_A.value().source;
}
a[i].limits_to_root.ac_max_current_A = {b[i].limits_to_root.ac_max_current_A.value().value +
a[i].limits_to_root.ac_max_current_A.value_or(NUMZERO).value,
source};
}
if (b[i].limits_to_root.total_power_W.has_value()) {
std::string source;
if (b[i].limits_to_root.total_power_W.value().value not_eq 0.) {
source = b[i].limits_to_root.total_power_W.value().source;
} else if (a[i].limits_to_root.total_power_W.has_value()) {
source = a[i].limits_to_root.total_power_W.value().source;
}
a[i].limits_to_root.total_power_W = {b[i].limits_to_root.total_power_W.value().value +
a[i].limits_to_root.total_power_W.value_or(NUMZERO).value,
source};
}
if (b[i].limits_to_root.ac_max_phase_count.has_value()) {
if (a[i].limits_to_root.ac_max_phase_count.has_value()) {
if (b[i].limits_to_root.ac_max_phase_count.value().value >
a[i].limits_to_root.ac_max_phase_count.value().value) {
a[i].limits_to_root.ac_max_phase_count = b[i].limits_to_root.ac_max_phase_count;
}
} else {
a[i].limits_to_root.ac_max_phase_count = b[i].limits_to_root.ac_max_phase_count;
}
}
}
}
void Market::trade(const ScheduleRes& traded) {
schedule_add(sold_root, traded);
// propagate to root
if (!is_root()) {
parent()->trade(traded);
}
}
float Market::nominal_ac_voltage() {
return _nominal_ac_voltage;
}
} // namespace module

View File

@@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MARKET_HPP
#define MARKET_HPP
// headers for required interface implementations
#include <generated/interfaces/energy/Interface.hpp>
#include <utils/date.hpp>
#include <vector>
using namespace std::chrono_literals;
namespace module {
typedef std::vector<types::energy::ScheduleReqEntry> ScheduleReq;
typedef std::vector<types::energy::ScheduleResEntry> ScheduleRes;
typedef std::vector<types::energy::ScheduleSetpointEntry> ScheduleSetpoints;
class globals_t {
public:
void init(date::utc_clock::time_point _start_time, int _interval_duration, int _schedule_duration,
float _slice_ampere, float _slice_watt, bool _debug,
const types::energy::EnergyFlowRequest& energy_flow_request);
date::utc_clock::time_point start_time; // common start point
std::chrono::minutes interval_duration; // interval duration
int schedule_length; // total forcast length (in counts of (non-regular) intervals)
float slice_ampere; // ampere_slices for trades
float slice_watt; // ampere_slices for trades
bool debug{false};
ScheduleReq zero_schedule_req, empty_schedule_req;
ScheduleRes zero_schedule_res, empty_schedule_res;
ScheduleSetpoints empty_schedule_setpoints;
private:
void create_timestamps(const types::energy::EnergyFlowRequest& energy_flow_request);
void add_timestamps(const types::energy::EnergyFlowRequest& energy_flow_request);
template <typename T> void create_empty_schedule(T& s);
std::vector<date::utc_clock::time_point> timestamps;
};
extern globals_t globals;
class time_probe {
public:
void start();
void pause();
int stop();
private:
std::chrono::high_resolution_clock::time_point timepoint_start;
std::chrono::nanoseconds total_duration{0};
bool running{false};
};
class Market {
public:
Market(const types::energy::EnergyFlowRequest& _energy_flow_request, const float __nominal_ac_voltage,
Market* __parent = nullptr);
void trade(const ScheduleRes& s);
bool is_root();
void get_list_of_evses(std::vector<Market*>& list);
std::vector<Market*> get_list_of_evses();
ScheduleReq get_available_energy_import();
ScheduleReq get_available_energy_export();
ScheduleSetpoints get_setpoints() {
return setpoints;
};
ScheduleRes get_sold_energy();
Market* parent();
float nominal_ac_voltage();
// local request only for this node
const types::energy::EnergyFlowRequest& energy_flow_request;
private:
Market* _parent;
std::list<Market> _children;
float _nominal_ac_voltage;
// main data structures
ScheduleReq import_max_available, export_max_available;
ScheduleSetpoints setpoints;
ScheduleRes sold_root;
std::vector<ScheduleRes> sold_leaves;
ScheduleReq get_max_available_energy(const ScheduleReq& request);
ScheduleReq get_available_energy(const ScheduleReq& available, bool add_sold);
ScheduleSetpoints resample(const ScheduleSetpoints& request);
};
float get_watt_from_freq_table(const std::vector<types::energy::FrequencyWattPoint>& table, float freq);
void apply_limit_if_smaller(std::optional<types::energy::NumberWithSource>& base, float limit,
const std::string& source);
void apply_setpoints(ScheduleReq& imp, ScheduleReq& exp, const ScheduleSetpoints& setpoints, std::optional<float> freq);
} // namespace module
#endif // MARKET_HPP

View File

@@ -0,0 +1,107 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "Offer.hpp"
#include <everest/logging.hpp>
#include <fmt/core.h>
namespace module {
std::ostream& operator<<(std::ostream& out, const Offer& self) {
/*out << "\nOffer:\n\nImport:\n------\n";
for (auto e : self.import_offer) {
out << e;
}
out << "\n\nExport:\n------\n";
for (auto e : self.export_offer) {
out << e;
}*/
const types::energy::NumberWithSource nonumber = {-9999.0, "Not set"};
out << fmt::format("\033[1;34mOffer[0]: Import {}A ({}) {}W ({}) Export {}A ({}) {}W ({})\033[1;0m",
self.import_offer[0].limits_to_root.ac_max_current_A.value_or(nonumber).value,
self.import_offer[0].limits_to_root.ac_max_current_A.value_or(nonumber).source,
self.import_offer[0].limits_to_root.total_power_W.value_or(nonumber).value,
self.import_offer[0].limits_to_root.total_power_W.value_or(nonumber).source,
self.export_offer[0].limits_to_root.ac_max_current_A.value_or(nonumber).value,
self.export_offer[0].limits_to_root.ac_max_current_A.value_or(nonumber).source,
self.export_offer[0].limits_to_root.total_power_W.value_or(nonumber).value,
self.export_offer[0].limits_to_root.total_power_W.value_or(nonumber).source);
return out;
}
template <class T> void apply_one_limit_if_smaller(std::optional<T>& a, const std::optional<T>& b) {
if (b.has_value()) {
if (a.has_value()) {
if (a.value().value > b.value().value) {
a = b.value();
} else if (a.value().value == b.value().value) {
a.value().source += "," + b.value().source;
}
} else {
a = b.value();
}
}
}
template <class T> void apply_one_limit_if_greater(std::optional<T>& a, const std::optional<T>& b) {
if (b.has_value()) {
if (a.has_value()) {
if (a.value().value < b.value().value) {
a = b.value();
} else if (a.value().value == b.value().value) {
a.value().source += "," + b.value().source;
}
} else {
a = b.value();
}
}
}
static void apply_limits(ScheduleReq& a, const ScheduleReq& b) {
if (a.size() != b.size()) {
EVLOG_error << fmt::format("apply_limits: a({}) and b({}) do not have the same size.", a.size(), b.size());
return;
}
for (ScheduleReq::size_type i = 0; i < a.size(); i++) {
// limits to leave are already merged to the root side, so we dont use them here
apply_one_limit_if_smaller(a[i].limits_to_root.ac_max_current_A, b[i].limits_to_root.ac_max_current_A);
apply_one_limit_if_smaller(a[i].limits_to_root.ac_max_phase_count, b[i].limits_to_root.ac_max_phase_count);
apply_one_limit_if_smaller(a[i].limits_to_root.total_power_W, b[i].limits_to_root.total_power_W);
apply_one_limit_if_greater(a[i].limits_to_root.ac_min_phase_count, b[i].limits_to_root.ac_min_phase_count);
apply_one_limit_if_greater(a[i].limits_to_root.ac_min_current_A, b[i].limits_to_root.ac_min_current_A);
// copy other information if any
a[i].price_per_kwh = b[i].price_per_kwh;
a[i].limits_to_root.ac_number_of_active_phases = b[i].limits_to_root.ac_number_of_active_phases;
}
}
Offer::Offer(Market& market) {
// create maximum offer for this market place
create_offer_for_local_market(market);
}
// Recursive: start at leaf, walk to root and create empty root offer. On the way back, apply all limits of local
// marketplaces until we are at the leaf again.
void Offer::create_offer_for_local_market(Market& market) {
if (!market.is_root()) {
create_offer_for_local_market(*market.parent());
} else {
// initialize time slots
import_offer = globals.empty_schedule_req;
export_offer = globals.empty_schedule_req;
}
// limit offer with limits at this market place
apply_limits(import_offer, market.get_available_energy_import());
// limit offer with limits at this market place
apply_limits(export_offer, market.get_available_energy_export());
optimizer_target = market.energy_flow_request.optimizer_target;
}
} // namespace module

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef OFFER_HPP
#define OFFER_HPP
#include <optional>
#include <vector>
#include "Market.hpp"
#include <generated/interfaces/energy/Interface.hpp>
namespace module {
class Offer {
public:
Offer(Market& market);
std::optional<types::energy::OptimizerTarget> optimizer_target;
ScheduleReq import_offer, export_offer;
private:
void create_offer_for_local_market(Market& market);
};
std::ostream& operator<<(std::ostream& out, const Offer& self);
} // namespace module
#endif // OFFER_HPP

View File

@@ -0,0 +1,15 @@
.. _everest_modules_handwritten_EnergyManager:
.. *************
.. EnergyManager
.. *************
This module implements logic to distribute power to energy nodes based on
energy requests.
One of its central ideas is to represent the energy system for which power is
distributed as an energy tree containing energy nodes.
This enables the representation of arbitrarily complex configurations of
physical and logical components within the targeted energy system.
Please see :doc:`Energy Management in EVerest </explanation/energymanagement/index>`
for a detailed explanation of the concepts behind this module.

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "energy_managerImpl.hpp"
namespace module {
namespace main {
void energy_managerImpl::init() {
}
void energy_managerImpl::ready() {
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_ENERGY_MANAGER_IMPL_HPP
#define MAIN_ENERGY_MANAGER_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/energy_manager/Implementation.hpp>
#include "../EnergyManager.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class energy_managerImpl : public energy_managerImplBase {
public:
energy_managerImpl() = delete;
energy_managerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EnergyManager>& mod, Conf& config) :
energy_managerImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// no commands defined for this interface
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<EnergyManager>& 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 main
} // namespace module
#endif // MAIN_ENERGY_MANAGER_IMPL_HPP

View File

@@ -0,0 +1,92 @@
description: >-
This module is the global Energy Manager for all EVSE/Charging stations in this building
config:
nominal_ac_voltage:
description: Nominal AC voltage to use to convert Ampere to Watt on AC
type: number
default: 230.0
update_interval:
description: Update interval for energy distribution [s]
type: integer
default: 1
schedule_interval_duration:
description: Duration of the schedule interval for forecast [min]
type: integer
default: 60
schedule_total_duration:
description: Total duration of schedule forcast [h]
type: integer
default: 1
slice_ampere:
description: Ampere slice for trading. Lower values will give more even distribution but increase processing time [A].
type: number
default: 0.5
slice_watt:
description: Watt slice for trading. Lower values will give more even distribution but increase processing time [W].
type: number
default: 500
debug:
description: Show debug output on command line.
type: boolean
default: false
switch_3ph1ph_while_charging_mode:
description: >-
If supported by BSP in capabilities to switch between three phases and one phase and config option three_phases is set to true,
this controls the algorithm:
- Never: Do not use 1ph/3ph switching even if supported by the BSP
- Oneway: Only switch from 3ph to 1ph if power is not enough, but never switch back to 3ph for a session.
- Both: Switch in both directions, i.e. from 3ph to 1ph and back to 3ph if available power changes
type: string
enum:
- Never
- Oneway
- Both
default: Never
switch_3ph1ph_max_nr_of_switches_per_session:
description: >-
Limit the maximum number of switches between 1ph and 3ph per charging session.
Set to 0 for no limit.
type: integer
default: 0
switch_3ph1ph_switch_limit_stickyness:
description: >-
If the maximum number of switches between 1ph and 3ph is reached, select what should happen:
- SinglePhase: Switch to 1ph mode
- ThreePhase: Switch to 3ph mode
- DontChange: Stay in the mode it is currently in
type: string
enum:
- SinglePhase
- ThreePhase
- DontChange
default: DontChange
switch_3ph1ph_power_hysteresis_W:
description: >-
Power based hysteresis in Watt. If set to 200W for example,
the hysteresis for PWM based charging will be 4.2kW to 4.4kW.
Actual values will depend on configured nominal AC voltage, and they may be different for
PWM vs ISO based charging in the future.
type: integer
default: 200
switch_3ph1ph_time_hysteresis_s:
description: >-
Time based hysteresis. It will only switch to 3 phases if the condition to select 3 phases
is stable for the configured number of seconds. It will always switch to 1ph mode without
waiting for this delay.
Set to 0 to disable time based hysteresis.
type: integer
default: 600
provides:
main:
description: Main interface of the energy manager
interface: energy_manager
requires:
energy_trunk:
interface: energy
min_connections: 1
max_connections: 1
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Cornelius Claussen
- Lars Dieckmann

View File

@@ -0,0 +1,64 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_EnergyManager_tests)
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)
target_include_directories(${TEST_TARGET_NAME} PRIVATE
..
${GENERATED_INCLUDE_DIR}
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_sources(${TEST_TARGET_NAME} PRIVATE
energy_manager_tests.cpp
JsonDefinedEnergyManagerTest.cpp
../Broker.cpp
../BrokerFastCharging.cpp
../EnergyManagerImpl.cpp
../Market.cpp
../Offer.cpp
)
set(JSON_TESTS_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/json_tests")
target_compile_definitions(${TEST_TARGET_NAME} PRIVATE
BUILD_TESTING_MODULE_ENERGY_MANAGER
JSON_TESTS_LOCATION="${JSON_TESTS_LOCATION}"
)
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
GTest::gmock
GTest::gtest_main
everest::log
everest::framework
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})
# Copy the json files used for testing to the destination directory.
# Uses a stamp file so the copy only runs when source fixtures change —
# plain add_custom_target always runs, which invalidates dependents on every build.
# NOTE: glob is intentionally NOT CONFIGURE_DEPENDS — that flag triggers a cmake
# reconfigure on any fixture mtime change, which cascades into ev-cli regeneration
# and rebuilds hundreds of unrelated targets. If you add/remove a fixture file,
# re-run cmake manually.
file(GLOB JSON_TEST_FILES "${CMAKE_CURRENT_SOURCE_DIR}/json_tests/*")
set(JSON_TESTS_STAMP "${CMAKE_CURRENT_BINARY_DIR}/json_tests.stamp")
add_custom_command(
OUTPUT ${JSON_TESTS_STAMP}
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/json_tests
${CMAKE_CURRENT_BINARY_DIR}/json_tests
COMMAND ${CMAKE_COMMAND} -E touch ${JSON_TESTS_STAMP}
DEPENDS ${JSON_TEST_FILES}
VERBATIM
)
add_custom_target(copy_json_tests DEPENDS ${JSON_TESTS_STAMP})
add_dependencies(${TEST_TARGET_NAME} copy_json_tests)

View File

@@ -0,0 +1,97 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "JsonDefinedEnergyManagerTest.hpp"
#include "EnergyManagerConfigJson.hpp"
#include "everest/logging.hpp"
#include <fstream>
namespace module {
JsonDefinedEnergyManagerTest::JsonDefinedEnergyManagerTest() = default;
JsonDefinedEnergyManagerTest::JsonDefinedEnergyManagerTest(const std::filesystem::path& path) {
load_test(path);
}
void JsonDefinedEnergyManagerTest::TestBody() {
run_test(start_times);
}
void JsonDefinedEnergyManagerTest::load_test(const std::filesystem::path& path) {
std::ifstream f(path.c_str());
json data;
try {
data = json::parse(f);
} catch (...) {
EVLOG_error << "Cannot parse JSON file " << path;
}
if (data.contains("basefile")) {
// Load base file first
std::filesystem::path basefile = std::filesystem::path(path).parent_path() / std::string(data.at("basefile"));
std::ifstream bf(basefile.c_str());
json databf = json::parse(bf);
// Apply patches
data = databf.patch(data.at("patches"));
}
this->request = data.at("request");
for (auto results : data.at("expected_results")) {
std::vector<types::energy::EnforcedLimits> l;
for (auto limit : results) {
types::energy::EnforcedLimits e;
from_json(limit, e);
l.push_back(e);
}
this->expected_results.push_back(l);
}
// Recreate the EnergyManagerImpl with the config from the test
this->config = data.at("config");
this->impl.reset(
new EnergyManagerImpl(this->config, [](const std::vector<types::energy::EnforcedLimits>& limits) { return; }));
this->comment = path;
for (auto start_time : data.at("start_times")) {
this->start_times.push_back(Everest::Date::from_rfc3339(start_time));
}
}
void JsonDefinedEnergyManagerTest::run_test(std::vector<date::utc_clock::time_point> _start_times) {
assert(_start_times.size() == expected_results.size());
for (int i = 0; i < _start_times.size(); i++) {
const auto enforced_limits = this->impl->run_optimizer(request, _start_times[i]);
json diff = json::diff(json(expected_results[i]), json(enforced_limits));
ASSERT_EQ(diff.size(), 0) << "At start time " << _start_times[i] << ": Diff to expected output:" << std::endl
<< diff.dump(2) << std::endl
<< "----------------------------------------" << std::endl
<< "Comment: " << std::endl
<< comment << std::endl
<< "----------------------------------------" << std::endl
<< "Full Request: " << std::endl
<< request << "----------------------------------------" << std::endl
<< "Full Enforced Limits: " << std::endl
<< json(enforced_limits).dump(4) << "----------------------------------------"
<< std::endl;
}
}
// Example to modify the test after loading
// TEST_F(JsonDefinedEnergyManagerTest, json_based_test_01) {
// load_test(std::string(JSON_TESTS_LOCATION) + "/1_0_two_evse_load_balancing.json");
// // Do here any modifications to the test
// this->request;
// this->expected_result;
// run_test();
// }
} // namespace module

View File

@@ -0,0 +1,216 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "JsonDefinedEnergyManagerTest.hpp"
static const std::string source1 = "SOURCE1";
static const std::string source2 = "SOURCE2";
namespace module {
// Register all json tests in the JSON_TESTS_LOCATION directory
void register_json_tests() {
const std::filesystem::path json_tests{std::string(JSON_TESTS_LOCATION)};
for (auto const& test_file : std::filesystem::directory_iterator{json_tests}) {
if (test_file.is_regular_file()) {
::testing::RegisterTest("JsonDefinedEnergyManagerTest", test_file.path().stem().string().c_str(), nullptr,
nullptr, __FILE__, __LINE__, [=]() -> JsonDefinedEnergyManagerTest* {
return new JsonDefinedEnergyManagerTest(test_file.path());
});
}
}
}
} // namespace module
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
// Add the JSON tests programmatically
module::register_json_tests();
return RUN_ALL_TESTS();
}
TEST(FreeFunctionTests, GetWattFreqTable) {
std::vector<types::energy::FrequencyWattPoint> table{{49., -7000}, {50., 0}, {51., 7000}};
EXPECT_EQ(module::get_watt_from_freq_table(table, 48.), -7000.);
EXPECT_EQ(module::get_watt_from_freq_table(table, 49.), -7000.);
EXPECT_EQ(module::get_watt_from_freq_table(table, 49.5), -3500.);
EXPECT_EQ(module::get_watt_from_freq_table(table, 50.), 0.);
EXPECT_EQ(module::get_watt_from_freq_table(table, 50.5), 3500.);
EXPECT_EQ(module::get_watt_from_freq_table(table, 51.), 7000.);
EXPECT_EQ(module::get_watt_from_freq_table(table, 52.), 7000.);
}
TEST(FreeFunctionTests, ApplyLimitIfSmaller) {
std::optional<types::energy::NumberWithSource> base;
types::energy::NumberWithSource result;
const std::string source = "SOURCE";
// Base has no value yet
module::apply_limit_if_smaller(base, 20., source);
EXPECT_TRUE(base.has_value());
EXPECT_EQ(base.value().value, 20.);
EXPECT_EQ(base.value().source, source);
// Now base has a value, test if a bigger limit does not apply
module::apply_limit_if_smaller(base, 21., source);
EXPECT_TRUE(base.has_value());
EXPECT_EQ(base.value().value, 20.);
EXPECT_EQ(base.value().source, source);
// Now base has a value, test if a smaller limit does apply
module::apply_limit_if_smaller(base, 19., source);
EXPECT_TRUE(base.has_value());
EXPECT_EQ(base.value().value, 19.);
EXPECT_EQ(base.value().source, source);
}
TEST(FreeFunctionTests, ApplySetpoints) {
module::ScheduleReq imp;
module::ScheduleReq::value_type v;
// At first it is mostly empty
v.timestamp = "2024-12-17T13:08:46.479Z";
imp.push_back(v);
// Add another entry with an ampere limit
v.timestamp = "2024-12-17T13:08:47.479Z";
v.limits_to_root.ac_max_current_A = {13.0, source1};
imp.push_back(v);
// Add another entry with an additional watt limit
v.timestamp = "2024-12-17T13:08:48.479Z";
v.limits_to_root.total_power_W = {2200.0, source2};
imp.push_back(v);
module::ScheduleReq exp{imp};
module::ScheduleReq imp_orig{imp};
module::ScheduleReq exp_orig{imp};
module::ScheduleSetpoints setpoints;
module::ScheduleSetpoints::value_type s;
// At first no setpoint is actually set
s.timestamp = "2024-12-17T13:08:46.479Z";
setpoints.push_back(s);
s.timestamp = "2024-12-17T13:08:47.479Z";
setpoints.push_back(s);
s.timestamp = "2024-12-17T13:08:48.479Z";
setpoints.push_back(s);
module::apply_setpoints(imp, exp, setpoints, {});
EXPECT_FALSE(imp[0].limits_to_root.ac_max_current_A.has_value());
EXPECT_FALSE(imp[0].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(imp[1].limits_to_root.ac_max_current_A.has_value());
EXPECT_FALSE(imp[1].limits_to_root.total_power_W.has_value());
EXPECT_EQ(imp[1].limits_to_root.ac_max_current_A.value().value, 13.0);
EXPECT_EQ(imp[1].limits_to_root.ac_max_current_A.value().source, source1);
EXPECT_TRUE(imp[2].limits_to_root.ac_max_current_A.has_value());
EXPECT_TRUE(imp[2].limits_to_root.total_power_W.has_value());
EXPECT_EQ(imp[2].limits_to_root.ac_max_current_A.value().value, 13.0);
EXPECT_EQ(imp[2].limits_to_root.ac_max_current_A.value().source, source1);
EXPECT_EQ(imp[2].limits_to_root.total_power_W.value().value, 2200.);
EXPECT_EQ(imp[2].limits_to_root.total_power_W.value().source, source2);
setpoints.clear();
types::energy::SetpointType sp;
sp.ac_current_A = 8.0;
sp.source = "SOURCESETPOINT";
s.setpoint = sp;
s.timestamp = "2024-12-17T13:08:46.479Z";
setpoints.push_back(s);
s.timestamp = "2024-12-17T13:08:47.479Z";
setpoints.push_back(s);
s.timestamp = "2024-12-17T13:08:48.479Z";
setpoints.push_back(s);
module::apply_setpoints(imp, exp, setpoints, {});
EXPECT_TRUE(imp[0].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(imp[0].limits_to_root.ac_max_current_A.value().value, 8.0);
EXPECT_EQ(imp[0].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(imp[0].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(imp[1].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(imp[1].limits_to_root.ac_max_current_A.value().value, 8.0);
EXPECT_EQ(imp[1].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(imp[1].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(imp[2].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(imp[2].limits_to_root.ac_max_current_A.value().value, 8.0);
EXPECT_EQ(imp[2].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_TRUE(imp[2].limits_to_root.total_power_W.has_value());
EXPECT_EQ(imp[2].limits_to_root.total_power_W.value().value, 2200);
EXPECT_EQ(imp[2].limits_to_root.total_power_W.value().source, source2);
EXPECT_TRUE(exp[0].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(exp[0].limits_to_root.ac_max_current_A.value().value, 0.0);
EXPECT_EQ(exp[0].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(exp[0].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(exp[1].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(exp[1].limits_to_root.ac_max_current_A.value().value, 0.0);
EXPECT_EQ(exp[1].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(exp[1].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(exp[2].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(exp[2].limits_to_root.ac_max_current_A.value().value, 0.0);
EXPECT_EQ(exp[2].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_TRUE(exp[2].limits_to_root.total_power_W.has_value());
EXPECT_EQ(exp[2].limits_to_root.total_power_W.value().value, 2200);
EXPECT_EQ(exp[2].limits_to_root.total_power_W.value().source, source2);
imp = imp_orig;
exp = exp_orig;
setpoints.clear();
sp.ac_current_A = 14.0;
sp.source = "SOURCESETPOINT";
s.setpoint = sp;
s.timestamp = "2024-12-17T13:08:46.479Z";
setpoints.push_back(s);
s.timestamp = "2024-12-17T13:08:47.479Z";
setpoints.push_back(s);
s.timestamp = "2024-12-17T13:08:48.479Z";
setpoints.push_back(s);
module::apply_setpoints(imp, exp, setpoints, {});
EXPECT_TRUE(imp[0].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(imp[0].limits_to_root.ac_max_current_A.value().value, 14.0);
EXPECT_EQ(imp[0].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(imp[0].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(imp[1].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(imp[1].limits_to_root.ac_max_current_A.value().value, 13.0);
EXPECT_EQ(imp[1].limits_to_root.ac_max_current_A.value().source, source1);
EXPECT_FALSE(imp[1].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(imp[2].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(imp[2].limits_to_root.ac_max_current_A.value().value, 13.0);
EXPECT_EQ(imp[2].limits_to_root.ac_max_current_A.value().source, source1);
EXPECT_TRUE(imp[2].limits_to_root.total_power_W.has_value());
EXPECT_EQ(imp[2].limits_to_root.total_power_W.value().value, 2200);
EXPECT_EQ(imp[2].limits_to_root.total_power_W.value().source, source2);
EXPECT_TRUE(exp[0].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(exp[0].limits_to_root.ac_max_current_A.value().value, 0.0);
EXPECT_EQ(exp[0].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(exp[0].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(exp[1].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(exp[1].limits_to_root.ac_max_current_A.value().value, 0.0);
EXPECT_EQ(exp[1].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_FALSE(exp[1].limits_to_root.total_power_W.has_value());
EXPECT_TRUE(exp[2].limits_to_root.ac_max_current_A.has_value());
EXPECT_EQ(exp[2].limits_to_root.ac_max_current_A.value().value, 0.0);
EXPECT_EQ(exp[2].limits_to_root.ac_max_current_A.value().source, "SOURCESETPOINT");
EXPECT_TRUE(exp[2].limits_to_root.total_power_W.has_value());
EXPECT_EQ(exp[2].limits_to_root.total_power_W.value().value, 2200);
EXPECT_EQ(exp[2].limits_to_root.total_power_W.value().source, source2);
}

View File

@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef TESTS_ENERGY_MANAGER_CONFIG_JSON_HPP
#define TESTS_ENERGY_MANAGER_CONFIG_JSON_HPP
#include "EnergyManagerImpl.hpp"
#include <nlohmann/json.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
template <> struct adl_serializer<module::EnergyManagerConfig> {
static void to_json(json& j, const module::EnergyManagerConfig& config) {
j = {
{"nominal_ac_voltage", config.nominal_ac_voltage},
{"update_interval", config.update_interval},
{"schedule_interval_duration", config.schedule_interval_duration},
{"schedule_total_duration", config.schedule_total_duration},
{"slice_ampere", config.slice_ampere},
{"slice_watt", config.slice_watt},
{"debug", config.debug},
{"switch_3ph1ph_while_charging_mode", config.switch_3ph1ph_while_charging_mode},
{"switch_3ph1ph_max_nr_of_switches_per_session", config.switch_3ph1ph_max_nr_of_switches_per_session},
{"switch_3ph1ph_switch_limit_stickyness", config.switch_3ph1ph_switch_limit_stickyness},
{"switch_3ph1ph_power_hysteresis_W", config.switch_3ph1ph_power_hysteresis_W},
{"switch_3ph1ph_time_hysteresis_s", config.switch_3ph1ph_time_hysteresis_s},
};
}
static module::EnergyManagerConfig from_json(const json& j) {
return {
j.at("nominal_ac_voltage"),
j.at("update_interval"),
j.at("schedule_interval_duration"),
j.at("schedule_total_duration"),
j.at("slice_ampere"),
j.at("slice_watt"),
j.at("debug"),
j.at("switch_3ph1ph_while_charging_mode"),
j.at("switch_3ph1ph_max_nr_of_switches_per_session"),
j.at("switch_3ph1ph_switch_limit_stickyness"),
j.at("switch_3ph1ph_power_hysteresis_W"),
j.at("switch_3ph1ph_time_hysteresis_s"),
};
}
};
NLOHMANN_JSON_NAMESPACE_END
#endif // TESTS_ENERGY_MANAGER_CONFIG_JSON_HPP

View File

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef JSON_DEFINED_ENERGY_MANAGER_TEST_HPP
#define JSON_DEFINED_ENERGY_MANAGER_TEST_HPP
#include <chrono>
#include <date/date.h>
#include <date/tz.h>
#include <filesystem>
#include <gtest/gtest.h>
#include <memory>
#include <generated/types/energy.hpp>
#include "EnergyManagerImpl.hpp"
namespace module {
// This test runs a test from a single json file
// and asserts that the result matches the expected result defined in the same file
class JsonDefinedEnergyManagerTest : public ::testing::Test {
public:
JsonDefinedEnergyManagerTest();
explicit JsonDefinedEnergyManagerTest(const std::filesystem::path& path);
void TestBody() override;
protected:
void load_test(const std::filesystem::path& path);
void run_test(std::vector<date::utc_clock::time_point> _start_times);
std::unique_ptr<EnergyManagerImpl> impl;
std::vector<date::utc_clock::time_point> start_times;
EnergyManagerConfig config;
types::energy::EnergyFlowRequest request;
std::vector<std::vector<types::energy::EnforcedLimits>> expected_results;
private:
std::string comment;
};
} // namespace module
#endif // JSON_DEFINED_ENERGY_MANAGER_TEST_HPP

View File

@@ -0,0 +1,335 @@
{
"description": "Tests one external watt setpoint for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 10.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"energy_usage_root": {
"current_A": {
"L1": 0.029999999329447746,
"L2": 0.0,
"L3": 0.0,
"N": 0.0
},
"energy_Wh_import": {
"L1": 1.7999999523162842,
"L2": 0.0,
"L3": 0.0,
"total": 1.7999999523162842
},
"power_W": {
"L1": 2.0,
"L2": 0.0,
"L3": 0.0,
"total": 2.0
},
"timestamp": "2024-12-17T13:08:46.479Z",
"voltage_V": {
"DC": 248.10000610351563,
"L1": 0.0,
"L2": 0.0
},
"frequency_Hz": {
"L1": 47.0,
"L2": 47.0,
"L3": 47.0
}
},
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 9.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:46.479Z"
}
],
"schedule_setpoints": [
{
"setpoint": {
"priority": 42,
"source": "external_limit_1_setpoint",
"frequency_table": [
{
"frequency_Hz": 49.0,
"total_power_W": -7000.0
},
{
"frequency_Hz": 50.0,
"total_power_W": 0
},
{
"frequency_Hz": 51.0,
"total_power_W": 7000.0
}
]
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "EVSE1_leave",
"value": -10.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "EVSE1_leave",
"value": -6900.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE1_leave",
"value": -10.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "EVSE1_leave",
"value": -6900.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE1_leave",
"value": -10.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "EVSE1_leave",
"value": -6900.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": -9.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_leave",
"value": -6210.0
}
},
"timestamp": "2024-12-17T13:08:46.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,323 @@
{
"description": "Tests loadbalancing between two DC EVSE nodes (50/50)",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 2,
"slice_watt": 1000,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"total_power_W": {
"value": 10000.0,
"source": "EVSE1_leaves"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"total_power_W": {
"value": 50000.0,
"source": "EVSE1_leaves"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 60.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
},
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"total_power_W": {
"value": 12000.0,
"source": "EVSE2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE2_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE2_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE2_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"total_power_W": {
"value": 60000.0,
"source": "EVSE2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 80.0,
"source": "EVSE2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE2_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE2_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE2_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse2"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 125.0,
"source": "GridConnection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 120.0,
"source": "GridConnection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "GridConnection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 125.0,
"source": "GridConnection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 120.0,
"source": "GridConnection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "GridConnection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "EVSE1_root",
"value": 60.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_NumPhasesActive",
"value": 3
},
"total_power_W": {
"source": "EVSE1_root",
"value": 41400.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE1_root",
"value": 60.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_NumPhasesActive",
"value": 3
},
"total_power_W": {
"source": "EVSE1_root",
"value": 41400.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE1_root",
"value": 60.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_NumPhasesActive",
"value": 3
},
"total_power_W": {
"source": "EVSE1_root",
"value": 41400.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1",
"valid_for": 10
},
{
"limits_root_side": {
"ac_max_current_A": {
"source": "GridConnection_root",
"value": 60.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_NumPhasesActive",
"value": 3
},
"total_power_W": {
"source": "GridConnection_root",
"value": 41400.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "GridConnection_root",
"value": 60.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_NumPhasesActive",
"value": 3
},
"total_power_W": {
"source": "GridConnection_root",
"value": 41400.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "GridConnection_root",
"value": 60.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_NumPhasesActive",
"value": 3
},
"total_power_W": {
"source": "GridConnection_root",
"value": 41400.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse2",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,299 @@
{
"description": "Tests loadbalancing between two EVSE nodes (50/50)",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
},
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE2_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE2_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE2_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 8.0,
"source": "EVSE2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE2_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE2_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE2_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse2"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "GridConnection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "GridConnection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "GridConnection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "GridConnection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "GridConnection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "GridConnection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "GridConnection_root",
"value": 24.0
},
"ac_max_phase_count": {
"source": "GridConnection_phase,EVSE1_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "GridConnection_root",
"value": 24.0
},
"ac_max_phase_count": {
"source": "GridConnection_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "GridConnection_root",
"value": 24.0
},
"ac_max_phase_count": {
"source": "GridConnection_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1",
"valid_for": 10
},
{
"limits_root_side": {
"ac_max_current_A": {
"source": "EVSE2_leave",
"value": 8.0
},
"ac_max_phase_count": {
"source": "GridConnection_phase,EVSE2_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE2_leave",
"value": 8.0
},
"ac_max_phase_count": {
"source": "GridConnection_phase,EVSE2_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE2_leave",
"value": 8.0
},
"ac_max_phase_count": {
"source": "GridConnection_phase,EVSE2_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse2",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,88 @@
{
"basefile": "1_0_two_ac_evse_load_balancing.json",
"description": "Tests load balancing between two EVSE nodes, one is charging and one is unplugged with 0A in idle",
"patches": [
{
"op": "replace",
"path": "/request/children/1/schedule_import/0/limits_to_root/ac_max_current_A/value",
"value": 0.0
},
{
"op": "replace",
"path": "/request/children/1/evse_state",
"value": "Unplugged"
},
{
"op": "replace",
"path": "/expected_results/0/0/limits_root_side/ac_max_current_A/source",
"value": "GridConnection_root,EVSE1_root"
},
{
"op": "replace",
"path": "/expected_results/0/0/limits_root_side/ac_max_current_A/value",
"value": 32.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/0/limits_to_root/ac_max_current_A/source",
"value": "GridConnection_root,EVSE1_root"
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/0/limits_to_root/ac_max_current_A/value",
"value": 32.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/1/limits_to_root/ac_max_current_A/source",
"value": "GridConnection_root,EVSE1_root"
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/1/limits_to_root/ac_max_current_A/value",
"value": 32.0
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/source",
"value": ""
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/value",
"value": 0.0
},
{
"op": "remove",
"path": "/expected_results/0/1/limits_root_side/ac_max_phase_count"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/source",
"value": ""
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/value",
"value": 0.0
},
{
"op": "remove",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_phase_count"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/source",
"value": ""
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/value",
"value": 0.0
},
{
"op": "remove",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_phase_count"
}
]
}

View File

@@ -0,0 +1,61 @@
{
"basefile": "1_0_two_ac_evse_load_balancing.json",
"description": "Tests load balancing between two EVSE nodes, one is charging and one is unplugged with 6A in idle",
"patches": [
{
"op": "replace",
"path": "/request/children/1/schedule_import/0/limits_to_root/ac_max_current_A/value",
"value": 6.0
},
{
"op": "replace",
"path": "/request/children/1/evse_state",
"value": "Unplugged"
},
{
"op": "replace",
"path": "/expected_results/0/0/limits_root_side/ac_max_current_A/value",
"value": 26.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/0/limits_to_root/ac_max_current_A/value",
"value": 26.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/1/limits_to_root/ac_max_current_A/value",
"value": 26.0
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/source",
"value": "EVSE2_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/value",
"value": 6.0
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/source",
"value": "EVSE2_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/value",
"value": 6.0
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/source",
"value": "EVSE2_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/value",
"value": 6.0
}
]
}

View File

@@ -0,0 +1,90 @@
{
"basefile": "1_0_two_ac_evse_load_balancing.json",
"description": "Tests load balancing between two EVSE nodes, one is charging and one is unplugged with 6A in idle",
"patches": [
{
"op": "replace",
"path": "/request/children/0/schedule_import/0/limits_to_root/ac_max_current_A/value",
"value": 45.0
},
{
"op": "replace",
"path": "/request/children/0/schedule_import/0/limits_to_leaves/ac_max_current_A/value",
"value": 44.0
},
{
"op": "replace",
"path": "/request/children/1/schedule_import/0/limits_to_root/ac_max_current_A/value",
"value": 0.0
},
{
"op": "replace",
"path": "/request/children/1/schedule_export/0/limits_to_root/ac_max_current_A/value",
"value": 8.0
},
{
"op": "remove",
"path": "/request/children/1/schedule_export/0/limits_to_leaves/ac_max_current_A"
},
{
"op": "replace",
"path": "/expected_results/0/0/limits_root_side/ac_max_current_A/value",
"value": 40.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/0/limits_to_root/ac_max_current_A/value",
"value": 40.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/1/limits_to_root/ac_max_current_A/value",
"value": 40.0
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/source",
"value": "EVSE2_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/source",
"value": "EVSE2_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/source",
"value": "EVSE2_root"
}
]
}

View File

@@ -0,0 +1,109 @@
{
"basefile": "1_0_two_ac_evse_load_balancing.json",
"description": "Tests load balancing between two EVSE nodes, both are discharging",
"patches": [
{
"op": "replace",
"path": "/request/children/0/schedule_import/0/limits_to_root/ac_max_current_A/value",
"value": 0.0
},
{
"op": "replace",
"path": "/request/children/0/schedule_export/0/limits_to_root/ac_max_current_A/value",
"value": 12.0
},
{
"op": "replace",
"path": "/request/children/1/schedule_import/0/limits_to_root/ac_max_current_A/value",
"value": 0.0
},
{
"op": "replace",
"path": "/request/children/1/schedule_export/0/limits_to_root/ac_max_current_A/value",
"value": 11.0
},
{
"op": "remove",
"path": "/request/children/0/schedule_export/0/limits_to_leaves/ac_max_current_A"
},
{
"op": "remove",
"path": "/request/children/1/schedule_export/0/limits_to_leaves/ac_max_current_A"
},
{
"op": "replace",
"path": "/expected_results/0/0/limits_root_side/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/0/limits_root_side/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/0/limits_to_root/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/0/limits_to_root/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/1/limits_to_root/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/0/schedule/1/limits_to_root/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/source",
"value": "GridConnection_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/1/limits_root_side/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/source",
"value": "GridConnection_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/0/limits_to_root/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/source",
"value": "GridConnection_root"
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_current_A/value",
"value": -8.0
},
{
"op": "replace",
"path": "/expected_results/0/1/schedule/1/limits_to_root/ac_max_phase_count/source",
"value": "BrokerFastCharging_FixedValue"
}
]
}

View File

@@ -0,0 +1,235 @@
{
"description": "Tests one external limit for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "grid_connection_root,external_limit_1_root,EVSE1_root",
"value": 32.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "grid_connection_root,external_limit_1_root,EVSE1_root",
"value": 32.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "grid_connection_root,external_limit_1_root,EVSE1_root",
"value": 32.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,235 @@
{
"description": "Tests one external limit for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 13.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": 13.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": 13.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": 13.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,283 @@
{
"description": "Tests one external limit for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_2_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 31.0,
"source": "external_limit_2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_2_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,518 @@
{
"description": "Tests one external limit for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:40.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 5.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:37.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 6.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:38.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:39.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:40.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:41.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:42.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:43.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_2_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 31.0,
"source": "external_limit_2_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_2_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_2_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:08:37.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": 6.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:38.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:39.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:40.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:08:41.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:42.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_2_leave",
"value": 31.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_2_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:43.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,400 @@
{
"description": "Tests one external limit for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z",
"2024-12-17T13:01:00.000Z",
"2024-12-17T13:02:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Both",
"switch_3ph1ph_max_nr_of_switches_per_session": 3,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 0.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {},
"limits_to_root": {
"total_power_W": {
"value": 11000.0,
"source": "external_limit_1_root"
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
},
{
"limits_to_leaves": {},
"limits_to_root": {
"total_power_W": {
"value": 2000.0,
"source": "external_limit_1_root"
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
},
{
"limits_to_leaves": {},
"limits_to_root": {
"total_power_W": {
"value": 5000.0,
"source": "external_limit_1_root"
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
},
{
"limits_to_leaves": {},
"limits_to_root": {
"total_power_W": {
"value": 0.0,
"source": "external_limit_1_root"
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
}
],
"schedule_setpoints": [],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "grid_connection_root,EVSE1_root",
"value": 32.0
},
"ac_max_phase_count": {
"source": "EVSE1_minphase",
"value": 1
},
"total_power_W": {
"source": "grid_connection_root,EVSE1_root",
"value": 7360.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "grid_connection_root,EVSE1_root",
"value": 32.0
},
"ac_max_phase_count": {
"source": "EVSE1_minphase",
"value": 1
},
"total_power_W": {
"source": "grid_connection_root,EVSE1_root",
"value": 7360.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
],
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_root",
"value": 15.942028999328613
},
"ac_max_phase_count": {
"source": "grid_connection_phase,EVSE1_phase",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_root",
"value": 11000.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
],
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_root",
"value": 15.942028999328613
},
"ac_max_phase_count": {
"source": "grid_connection_phase,EVSE1_phase",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_root",
"value": 11000.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:00:01.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,30 @@
{
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"expected_results": [
[]
],
"request": {
"children": [],
"node_type": "Undefined",
"schedule_setpoints": [],
"uuid": "",
"schedule_import": [],
"schedule_export": []
},
"start_times": [
"2025-01-16T15:42:56.390Z"
]
}

View File

@@ -0,0 +1,88 @@
{
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "",
"value": 0.0
},
"total_power_W": {
"source": "",
"value": 0.0
}
},
"timestamp": "2024-01-01T12:00:00.000Z"
}
],
"schedule_setpoints": [],
"uuid": "evse_manager",
"valid_for": 10
}
]
],
"request": {
"children": [],
"energy_usage_root": {
"current_A": {
"L1": 0.029999999329447746,
"L2": 0.0,
"L3": 0.0,
"N": 0.0
},
"energy_Wh_import": {
"L1": 1.7999999523162842,
"L2": 0.0,
"L3": 0.0,
"total": 1.7999999523162842
},
"power_W": {
"L1": 2.0,
"L2": 0.0,
"L3": 0.0,
"total": 2.0
},
"timestamp": "2024-03-27T12:41:16.864Z",
"voltage_V": {
"DC": 248.10000610351563,
"L1": 0.0,
"L2": 0.0
}
},
"node_type": "Evse",
"priority_request": false,
"schedule_import": [],
"schedule_export": [],
"schedule_setpoints": [],
"uuid": "evse_manager"
},
"start_times": [
"2024-01-01T12:00:00.000Z"
]
}

View File

@@ -0,0 +1,338 @@
{
"description": "Tests one external ampere setpoint for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 10.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 9.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:46.479Z"
}
],
"schedule_setpoints": [
{
"setpoint": {
"priority": 42,
"source": "external_limit_1_setpoint",
"ac_current_A": 7.5
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"setpoint": {
"priority": 41,
"source": "external_limit_1_setpoint",
"ac_current_A": 13.0
},
"timestamp": "2024-12-17T13:08:37.479Z"
},
{
"setpoint": {
"priority": 41,
"source": "external_limit_1_setpoint",
"ac_current_A": -8.0
},
"timestamp": "2024-12-17T13:08:40.479Z"
},
{
"setpoint": {
"priority": 41,
"source": "external_limit_1_setpoint",
"ac_current_A": -12.0
},
"timestamp": "2024-12-17T13:08:44.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 7.5
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 7.5
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 7.5
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 13.0
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:37.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": -8.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:40.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE1_leave",
"value": -10.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:44.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": -9.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
}
},
"timestamp": "2024-12-17T13:08:46.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
]
]
}

View File

@@ -0,0 +1,366 @@
{
"description": "Tests one external watt setpoint for one EVSE",
"start_times": [
"2024-12-17T13:00:00.000Z"
],
"config": {
"nominal_ac_voltage": 230.0,
"update_interval": 1,
"schedule_interval_duration": 60,
"schedule_total_duration": 1,
"slice_ampere": 0.5,
"slice_watt": 500,
"debug": false,
"switch_3ph1ph_while_charging_mode": "Never",
"switch_3ph1ph_max_nr_of_switches_per_session": 10,
"switch_3ph1ph_switch_limit_stickyness": "DontChange",
"switch_3ph1ph_power_hysteresis_W": 500,
"switch_3ph1ph_time_hysteresis_s": 30
},
"request": {
"children": [
{
"children": [
{
"children": [],
"evse_state": "Charging",
"node_type": "Evse",
"priority_request": false,
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 10.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 0.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "EVSE1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "EVSE1_phase"
},
"ac_min_current_A": {
"value": 6.0,
"source": "EVSE1_mincurrent"
},
"ac_min_phase_count": {
"value": 1,
"source": "EVSE1_minphase"
},
"ac_number_of_active_phases": 3,
"ac_supports_changing_phases_during_charging": true
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "evse1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 9.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:46.479Z"
}
],
"schedule_setpoints": [
{
"setpoint": {
"priority": 42,
"source": "external_limit_1_setpoint",
"total_power_W": 4242.0
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"setpoint": {
"priority": 41,
"source": "external_limit_1_setpoint",
"total_power_W": 8242.0
},
"timestamp": "2024-12-17T13:08:37.479Z"
},
{
"setpoint": {
"priority": 41,
"source": "external_limit_1_setpoint",
"total_power_W": -4242.0
},
"timestamp": "2024-12-17T13:08:40.479Z"
},
{
"setpoint": {
"priority": 41,
"source": "external_limit_1_setpoint",
"total_power_W": -14242.0
},
"timestamp": "2024-12-17T13:08:44.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "external_limit_1_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "external_limit_1_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"uuid": "external_limit_1"
}
],
"node_type": "Generic",
"schedule_export": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 16.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_import": [
{
"limits_to_leaves": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_leave"
}
},
"limits_to_root": {
"ac_max_current_A": {
"value": 32.0,
"source": "grid_connection_root"
},
"ac_max_phase_count": {
"value": 3,
"source": "grid_connection_phase"
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
}
],
"schedule_setpoints": [],
"uuid": "grid_connection"
},
"expected_results": [
[
{
"limits_root_side": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 6.147826194763184
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_setpoint",
"value": 4242.0
}
},
"schedule": [
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 6.147826194763184
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_setpoint",
"value": 4242.0
}
},
"timestamp": "2024-12-17T13:00:00.000Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 6.147826194763184
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_setpoint",
"value": 4242.0
}
},
"timestamp": "2024-12-17T13:08:36.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": 11.944927215576172
},
"ac_max_phase_count": {
"source": "grid_connection_phase,external_limit_1_phase,EVSE1_phase",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_setpoint",
"value": 8242.0
}
},
"timestamp": "2024-12-17T13:08:37.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_setpoint",
"value": -6.147826194763184
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_setpoint",
"value": -4242.0
}
},
"timestamp": "2024-12-17T13:08:40.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "EVSE1_leave",
"value": -10.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "EVSE1_leave",
"value": -6900.0
}
},
"timestamp": "2024-12-17T13:08:44.479Z"
},
{
"limits_to_root": {
"ac_max_current_A": {
"source": "external_limit_1_leave",
"value": -9.0
},
"ac_max_phase_count": {
"source": "BrokerFastCharging_FixedValue",
"value": 3
},
"total_power_W": {
"source": "external_limit_1_leave",
"value": -6210.0
}
},
"timestamp": "2024-12-17T13:08:46.479Z"
}
],
"uuid": "evse1",
"valid_for": 10
}
]
]
}