- 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
229 lines
8.4 KiB
C++
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
|