- 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
266 lines
11 KiB
C++
266 lines
11 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Pionix GmbH and Contributors to EVerest
|
|
|
|
#include "Broker.hpp"
|
|
#include <everest/logging.hpp>
|
|
#include <fmt/core.h>
|
|
|
|
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
|