// SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include "Broker.hpp" #include #include 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