Files
Eric F d398a6ced2 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
2026-06-08 00:38:27 -04:00

229 lines
8.4 KiB
C++

// 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