// SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include #include #include #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& 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 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 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> 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(*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 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