Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending 1ph/3ph feature #807

Merged
merged 4 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/config-sil.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ active_modules:
energy_manager:
config_module:
switch_3ph1ph_while_charging_mode: Both
switch_3ph1ph_max_nr_of_switches_per_session: 5
switch_3ph1ph_time_hysteresis_s: 20
switch_3ph1ph_power_hysteresis_W: 1000
switch_3ph1ph_switch_limit_stickyness: SinglePhase
schedule_interval_duration: 60
schedule_total_duration: 10
debug: false
connections:
energy_trunk:
Expand Down
3 changes: 2 additions & 1 deletion modules/EnergyManager/Broker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

namespace module {

Broker::Broker(Market& _market) :
Broker::Broker(Market& _market, BrokerContext& _context) :
local_market(_market),
context(_context),
first_trade(globals.schedule_length, true),
slot_type(globals.schedule_length, SlotType::Undecided),
num_phases(globals.schedule_length, 0) {
Expand Down
21 changes: 20 additions & 1 deletion modules/EnergyManager/Broker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,27 @@ enum class SlotType {
Undecided
};

// All context data that is stored in between optimization runs
struct BrokerContext {
BrokerContext() {
clear();
};

void clear() {
number_1ph3ph_cycles = 0;
last_ac_number_of_active_phases_import = 0;
ts_1ph_optimal = date::utc_clock::now();
};

int number_1ph3ph_cycles;
int last_ac_number_of_active_phases_import;
std::chrono::time_point<date::utc_clock> ts_1ph_optimal;
};

// base class for different Brokers
class Broker {
public:
Broker(Market& market);
Broker(Market& market, BrokerContext& context);
virtual ~Broker(){};
virtual bool trade(Offer& offer) = 0;
Market& get_local_market();
Expand All @@ -28,6 +45,8 @@ class Broker {
std::vector<bool> first_trade;
std::vector<SlotType> slot_type;
std::vector<int> num_phases;

BrokerContext& context;
};

} // namespace module
Expand Down
131 changes: 108 additions & 23 deletions modules/EnergyManager/BrokerFastCharging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,37 @@

namespace module {

BrokerFastCharging::BrokerFastCharging(Market& _market, Switch1ph3phMode mode) :
Broker(_market), switch_1ph3ph_mode(mode) {
BrokerFastCharging::BrokerFastCharging(Market& _market, BrokerContext& _context, Config _config) :
Broker(_market, _context), config(_config) {
}

static auto to_timestamp(const types::energy::ScheduleReqEntry& entry) {
return Everest::Date::from_rfc3339(entry.timestamp);
}

static bool 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 BrokerFastCharging::trade(Offer& _offer) {
Expand Down Expand Up @@ -46,6 +75,8 @@ bool BrokerFastCharging::trade(Offer& _offer) {
// if we have not bought anything, we first need to buy the minimal limits for ac_amp if any.
for (int i = 0; i < globals.schedule_length; i++) {

bool time_slot_is_active = time_slot_active(i, offer->import_offer);

// make this more readable
auto& max_current_import = offer->import_offer[i].limits_to_root.ac_max_current_A;
const auto& min_current_import = offer->import_offer[i].limits_to_root.ac_min_current_A;
Expand Down Expand Up @@ -89,52 +120,106 @@ bool BrokerFastCharging::trade(Offer& _offer) {
// A current limit is set

// If an additional watt limit is set check phases, else it is max_phases (typically 3)
// First decide if we charge 1 phase or 3 phase (if switching is possible at all)
// First decide if we would like to charge 1 phase or 3 phase (if switching is possible at all)
// - Check if we are below e.g. 4.2kW (min_current*voltage*3) -> we have to do single phase
// - Check if we are above e.g. 7.4kW (max_current*voltage*1) -> we have to go three phase
// - Check if we are above e.g. 4.4kW (min_current*voltage*3 + watt_hysteresis) -> we want to go three
// phase
// - If we are in between, use what is currently active (hysteresis)
// One problem is that we do not know the EV's limit, so the hysteresis does not work properly
// when the EV supports 16A and the EVSE supports 32A:
// Then in single phase, it will increase until 1ph/32A before it switches to 3ph, but the EV gets
// stuck at 3.6kW/1ph/16A because that is its limit.

int number_of_phases = ac_number_of_active_phases_import;
if (switch_1ph3ph_mode not_eq Switch1ph3phMode::Never and total_power_import.has_value() &&
min_current_import.value() && min_current_import.value() > 0.) {
if (total_power_import.value() <
min_current_import.value() * max_phases_import * local_market.nominal_ac_voltage()) {
// We have to do single phase, it is impossible with 3ph
number_of_phases = min_phases_import;
} else if (switch_1ph3ph_mode == Switch1ph3phMode::Both and
total_power_import.value() >
max_current_import.value() * min_phases_import * local_market.nominal_ac_voltage()) {
number_of_phases = max_phases_import;

const auto min_power_3ph =
min_current_import.value_or(0) * max_phases_import * local_market.nominal_ac_voltage();

bool number_of_switching_cycles_reached = false;

if (first_trade[i]) {
if (config.switch_1ph_3ph_mode not_eq Switch1ph3phMode::Never and total_power_import.has_value() &&
min_power_3ph > 0.) {

if (total_power_import.value() < min_power_3ph) {
// We have to do single phase, it is impossible with 3ph
number_of_phases = min_phases_import;
} else if (config.switch_1ph_3ph_mode == Switch1ph3phMode::Both and
total_power_import.value() > min_power_3ph + config.power_hysteresis_W) {
number_of_phases = max_phases_import;
} else {
// Keep number of phases as they are
number_of_phases = ac_number_of_active_phases_import;
}

// Now we made the decision what the optimal number of phases would be (in variable
// number_of_phases) We also have a time based hysteresis as well as some limits in maximum
// number of switching cycles. This means we maybe cannot use the optimal number of phases just
// now. Check those conditions and adjust number_of_phases accordingly.

if (config.max_nr_of_switches_per_session > 0 and
context.number_1ph3ph_cycles > config.max_nr_of_switches_per_session) {
number_of_switching_cycles_reached = true;
if (config.stickyness == StickyNess::SinglePhase) {
number_of_phases = min_phases_import;
} else if (config.stickyness == StickyNess::ThreePhase) {
number_of_phases = max_phases_import;
} else {
number_of_phases = ac_number_of_active_phases_import;
}
}

if (number_of_phases == min_phases_import) {
context.ts_1ph_optimal = date::utc_clock::now();
}

if (config.time_hysteresis_s > 0 and time_slot_is_active) {
// Check time based hysteresis:
// - store timestamp whenever 1ph is optimal (update continously)
// Then now-timestamp is the stable time period for a 3ph condition.
// This should only be done in the currently active time slot. Ignore time hysteresis in
// other slots in the future or past.
// Only allow an actual change to 3ph if the time exceeds the configured hysteresis limit.
const auto stable_3ph = std::chrono::duration_cast<std::chrono::seconds>(
globals.start_time - context.ts_1ph_optimal)
.count();

if (stable_3ph < config.time_hysteresis_s and number_of_phases == max_phases_import) {
number_of_phases = min_phases_import;
}
}
} else {
// Keep number of phases as they are
number_of_phases = ac_number_of_active_phases_import;
number_of_phases = max_phases_import;
}
} else {
number_of_phases = max_phases_import;
}

// store decision in context
if (ac_number_of_active_phases_import not_eq context.last_ac_number_of_active_phases_import) {
context.number_1ph3ph_cycles++;
}
context.last_ac_number_of_active_phases_import = ac_number_of_active_phases_import;

if (first_trade[i] && min_current_import.has_value() && min_current_import.value() > 0.) {
num_phases[i] = number_of_phases;
// EVLOG_info << "I: first trade: try to buy minimal current_A on AC: " <<
// min_current_import.value();
// try to buy minimal current_A if we are on AC, but don't buy less.
if (not buy_ampere_import(i, min_current_import.value(), false, number_of_phases) and
switch_1ph3ph_mode not_eq Switch1ph3phMode::Never) {
config.switch_1ph_3ph_mode not_eq Switch1ph3phMode::Never and
not number_of_switching_cycles_reached) {
// If we cannot buy the minimum amount we need, try again in single phase mode (it may be due to
// a watt limit only)
number_of_phases = 1;
num_phases[i] = number_of_phases;
buy_ampere_import(i, min_current_import.value(), false, number_of_phases);
}

/*EVLOG_info << "I: " << i << " -- 1ph3ph: " << min_power_3ph << " active_nr_phases "
<< ac_number_of_active_phases_import << " cycles " << context.number_1ph3ph_cycles
<< " number_of_phases " << number_of_phases << " time_slot_active "
<< time_slot_is_active;*/
} else {
// EVLOG_info << "I: Not first trade or nor min current needed.";
// try to buy a slice but allow less to be bought
buy_ampere_import(i, globals.slice_ampere, true, num_phases[i]);
}

} else if (total_power_import.has_value()) {
// only a watt limit is available
// EVLOG_info << "I: Only watt limit is set." << total_power_import.value();
Expand Down
19 changes: 17 additions & 2 deletions modules/EnergyManager/BrokerFastCharging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,22 @@
Oneway,
Both,
};
explicit BrokerFastCharging(Market& market, Switch1ph3phMode mode);

enum class StickyNess {
SinglePhase,
ThreePhase,
DontChange,
};

struct Config {
Switch1ph3phMode switch_1ph_3ph_mode{Switch1ph3phMode::Never};

Check notice on line 26 in modules/EnergyManager/BrokerFastCharging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/BrokerFastCharging.hpp#L26

struct member 'Config::switch_1ph_3ph_mode' is never used.
StickyNess stickyness{StickyNess::DontChange};

Check notice on line 27 in modules/EnergyManager/BrokerFastCharging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/BrokerFastCharging.hpp#L27

struct member 'Config::stickyness' is never used.
int max_nr_of_switches_per_session{0};

Check notice on line 28 in modules/EnergyManager/BrokerFastCharging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/BrokerFastCharging.hpp#L28

struct member 'Config::max_nr_of_switches_per_session' is never used.
int power_hysteresis_W{200};

Check notice on line 29 in modules/EnergyManager/BrokerFastCharging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/BrokerFastCharging.hpp#L29

struct member 'Config::power_hysteresis_W' is never used.
int time_hysteresis_s{600};

Check notice on line 30 in modules/EnergyManager/BrokerFastCharging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/BrokerFastCharging.hpp#L30

struct member 'Config::time_hysteresis_s' is never used.
};

explicit BrokerFastCharging(Market& market, BrokerContext& context, Config config);
virtual bool trade(Offer& offer) override;

private:
Expand All @@ -35,7 +50,7 @@
Offer* offer{nullptr};
bool traded{false};

Switch1ph3phMode switch_1ph3ph_mode;
Config config;

Check notice on line 53 in modules/EnergyManager/BrokerFastCharging.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/BrokerFastCharging.hpp#L53

class member 'BrokerFastCharging::config' is never used.
};

} // namespace module
Expand Down
35 changes: 33 additions & 2 deletions modules/EnergyManager/EnergyManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ static BrokerFastCharging::Switch1ph3phMode to_switch_1ph3ph_mode(const std::str
}
}

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::Config to_broker_fast_charging_config(const Conf& module_config) {
BrokerFastCharging::Config broker_conf;

broker_conf.max_nr_of_switches_per_session = module_config.switch_3ph1ph_max_nr_of_switches_per_session;
broker_conf.power_hysteresis_W = module_config.switch_3ph1ph_power_hysteresis_W;
broker_conf.switch_1ph_3ph_mode = to_switch_1ph3ph_mode(module_config.switch_3ph1ph_while_charging_mode);
broker_conf.time_hysteresis_s = module_config.switch_3ph1ph_time_hysteresis_s;
broker_conf.stickyness = to_stickyness(module_config.switch_3ph1ph_switch_limit_stickyness);

return broker_conf;
}

std::vector<types::energy::EnforcedLimits> EnergyManager::run_optimizer(types::energy::EnergyFlowRequest request) {

std::scoped_lock lock(energy_mutex);
Expand All @@ -106,10 +128,19 @@ std::vector<types::energy::EnforcedLimits> EnergyManager::run_optimizer(types::e
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, to_switch_1ph3ph_mode(config.switch_3ph1ph_while_charging_mode)));
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);
}

Expand Down
8 changes: 8 additions & 0 deletions modules/EnergyManager/EnergyManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

#include <mutex>

#include "Broker.hpp"

#ifdef BUILD_TESTING_MODULE_ENERGY_MANAGER
#include <gtest/gtest_prod.h>
namespace module::test {
Expand All @@ -46,6 +48,10 @@
double slice_watt;
bool debug;
std::string switch_3ph1ph_while_charging_mode;
int switch_3ph1ph_max_nr_of_switches_per_session;

Check notice on line 51 in modules/EnergyManager/EnergyManager.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/EnergyManager.hpp#L51

struct member 'Conf::switch_3ph1ph_max_nr_of_switches_per_session' is never used.
std::string switch_3ph1ph_switch_limit_stickyness;

Check notice on line 52 in modules/EnergyManager/EnergyManager.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/EnergyManager.hpp#L52

struct member 'Conf::switch_3ph1ph_switch_limit_stickyness' is never used.
int switch_3ph1ph_power_hysteresis_W;

Check notice on line 53 in modules/EnergyManager/EnergyManager.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/EnergyManager.hpp#L53

struct member 'Conf::switch_3ph1ph_power_hysteresis_W' is never used.
int switch_3ph1ph_time_hysteresis_s;

Check notice on line 54 in modules/EnergyManager/EnergyManager.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/EnergyManager.hpp#L54

struct member 'Conf::switch_3ph1ph_time_hysteresis_s' is never used.
};

class EnergyManager : public Everest::ModuleBase {
Expand Down Expand Up @@ -87,6 +93,8 @@
std::condition_variable mainloop_sleep_condvar;
std::mutex mainloop_sleep_mutex;

std::map<std::string, BrokerContext> contexts;

Check notice on line 96 in modules/EnergyManager/EnergyManager.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/EnergyManager/EnergyManager.hpp#L96

class member 'EnergyManager::contexts' is never used.

#ifdef BUILD_TESTING_MODULE_ENERGY_MANAGER
FRIEND_TEST(EnergyManagerTest, empty);
FRIEND_TEST(EnergyManagerTest, noSchedules);
Expand Down
34 changes: 34 additions & 0 deletions modules/EnergyManager/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ config:
- Oneway
- Both
default: Never
switch_3ph1ph_max_nr_of_switches_per_session:
description: >-
Limit the maximum number of switches between 1ph and 3ph per charging session.
Set to 0 for no limit.
type: integer
default: 0
switch_3ph1ph_switch_limit_stickyness:
description: >-
If the maximum number of switches between 1ph and 3ph is reached, select what should happen:
- SinglePhase: Switch to 1ph mode
- ThreePhase: Switch to 3ph mode
- DontChange: Stay in the mode it is currently in
type: string
enum:
- SinglePhase
- ThreePhase
- DontChange
default: DontChange
switch_3ph1ph_power_hysteresis_W:
description: >-
Power based hysteresis in Watt. If set to 200W for example,
the hysteresis for PWM based charging will be 4.2kW to 4.4kW.
Actual values will depend on configured nominal AC voltage, and they may be different for
PWM vs ISO based charging in the future.
type: integer
default: 200
switch_3ph1ph_time_hysteresis_s:
description: >-
Time based hysteresis. It will only switch to 3 phases if the condition to select 3 phases
is stable for the configured number of seconds. It will always switch to 1ph mode without
waiting for this delay.
Set to 0 to disable time based hysteresis.
type: integer
default: 600
provides:
main:
description: Main interface of the energy manager
Expand Down
Loading
Loading