- 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
533 lines
18 KiB
C++
533 lines
18 KiB
C++
// 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
|