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