diff --git a/packages/control/auto_phase_switch_test.py b/packages/control/auto_phase_switch_test.py index 8e3ff229a0..321c63b4f2 100644 --- a/packages/control/auto_phase_switch_test.py +++ b/packages/control/auto_phase_switch_test.py @@ -33,7 +33,7 @@ class Params: def __init__(self, name: str, max_current_single_phase: int, - timestamp_last_phase_switch: Optional[float], + timestamp_phase_switch_buffer_start: Optional[float], phases_to_use: int, required_current: float, evu_surplus: int, @@ -46,7 +46,7 @@ def __init__(self, expected_message: Optional[str] = None) -> None: self.name = name self.max_current_single_phase = max_current_single_phase - self.timestamp_last_phase_switch = timestamp_last_phase_switch + self.timestamp_phase_switch_buffer_start = timestamp_phase_switch_buffer_start self.phases_to_use = phases_to_use self.required_current = required_current self.available_power = evu_surplus @@ -60,54 +60,55 @@ def __init__(self, cases = [ - Params("1to3, enough power, start timer", max_current_single_phase=16, timestamp_last_phase_switch=1652683202, + Params("1to3, enough power, start timer", max_current_single_phase=16, timestamp_phase_switch_buffer_start=None, phases_to_use=1, required_current=6, evu_surplus=800, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.CHARGING_ALLOWED, expected_phases_to_use=1, expected_current=6, - expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "4 Min. 10 Sek."), + expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "30 Sek."), expected_state=ChargepointState.PHASE_SWITCH_DELAY), - Params("1to3, not enough power, start timer", max_current_single_phase=16, timestamp_last_phase_switch=1652683202, + Params("1to3, not enough power, start timer", max_current_single_phase=16, timestamp_phase_switch_buffer_start=None, phases_to_use=1, required_current=6, evu_surplus=300, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.CHARGING_ALLOWED, expected_phases_to_use=1, expected_current=6, expected_state=ChargepointState.CHARGING_ALLOWED), Params("1to3, enough power, timer not expired", max_current_single_phase=16, - timestamp_last_phase_switch=1652683202.0, phases_to_use=1, required_current=6, + timestamp_phase_switch_buffer_start=1652683232.0, phases_to_use=1, required_current=6, evu_surplus=1460, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=1, expected_current=6, - expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "4 Min. 10 Sek."), + expected_message=Ev.PHASE_SWITCH_DELAY_TEXT.format("Umschaltung von 1 auf 3", "10 Sek."), expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("1to3, not enough power, timer not expired", max_current_single_phase=16, - timestamp_last_phase_switch=1652683202.0, phases_to_use=1, required_current=6, + timestamp_phase_switch_buffer_start=1652683202.0, phases_to_use=1, required_current=6, evu_surplus=460, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=1, expected_current=6, expected_message=f"Verzögerung für die Umschaltung von 1 auf 3 Phasen abgebrochen{Ev.NOT_ENOUGH_POWER}", expected_state=ChargepointState.CHARGING_ALLOWED), Params("1to3, enough power, timer expired", max_current_single_phase=16, - timestamp_last_phase_switch=1652682802.0, phases_to_use=1, required_current=6, + timestamp_phase_switch_buffer_start=1652682802.0, phases_to_use=1, required_current=6, evu_surplus=1640, get_currents=[15.6, 0, 0], get_power=3450, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=3, expected_current=6, expected_state=ChargepointState.PHASE_SWITCH_AWAITED), - Params("3to1, not enough power, start timer", max_current_single_phase=16, timestamp_last_phase_switch=1652683202, + Params("3to1, not enough power, start timer", max_current_single_phase=16, + timestamp_phase_switch_buffer_start=1652683202, phases_to_use=3, required_current=6, evu_surplus=0, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.CHARGING_ALLOWED, expected_phases_to_use=3, expected_current=6, - expected_message="Umschaltung von 3 auf 1 Phasen in 4 Min. 10 Sek..", + expected_message="Umschaltung von 3 auf 1 Phasen in 10 Sek..", expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("3to1, not enough power, timer not expired", max_current_single_phase=16, - timestamp_last_phase_switch=1652683202.0, + timestamp_phase_switch_buffer_start=1652683202.0, phases_to_use=3, required_current=6, evu_surplus=-460, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=3, expected_current=6, - expected_message="Umschaltung von 3 auf 1 Phasen in 4 Min. 10 Sek..", + expected_message="Umschaltung von 3 auf 1 Phasen in 10 Sek..", expected_state=ChargepointState.PHASE_SWITCH_DELAY), Params("3to1, enough power, timer not expired", max_current_single_phase=16, - timestamp_last_phase_switch=1652683202.0, phases_to_use=3, required_current=6, + timestamp_phase_switch_buffer_start=1652683202.0, phases_to_use=3, required_current=6, evu_surplus=860, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=3, expected_current=6, expected_message=f"Verzögerung für die Umschaltung von 3 auf 1 Phasen abgebrochen{Ev.ENOUGH_POWER}", expected_state=ChargepointState.CHARGING_ALLOWED), Params("3to1, not enough power, timer expired", max_current_single_phase=16, - timestamp_last_phase_switch=1652682802.0, phases_to_use=3, required_current=6, + timestamp_phase_switch_buffer_start=1652682802.0, phases_to_use=3, required_current=6, evu_surplus=-460, get_currents=[4.5, 4.4, 5.8], get_power=3381, state=ChargepointState.PHASE_SWITCH_DELAY, expected_phases_to_use=1, expected_current=16, expected_state=ChargepointState.PHASE_SWITCH_AWAITED), @@ -127,7 +128,8 @@ def test_auto_phase_switch(monkeypatch, vehicle: Ev, params: Params): vehicle.ev_template.data.max_current_single_phase = params.max_current_single_phase control_parameter = ControlParameter() - control_parameter.timestamp_last_phase_switch = params.timestamp_last_phase_switch + control_parameter.timestamp_last_phase_switch = 1652682802 + control_parameter.timestamp_phase_switch_buffer_start = params.timestamp_phase_switch_buffer_start control_parameter.phases = params.phases_to_use control_parameter.required_current = params.required_current control_parameter.state = params.state diff --git a/packages/control/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index 0d221308db..e1bbe8c4a5 100644 --- a/packages/control/chargepoint/control_parameter.py +++ b/packages/control/chargepoint/control_parameter.py @@ -29,6 +29,8 @@ class ControlParameter: default=None, metadata={"topic": "control_parameter/timestamp_chargemode_changed"}) timestamp_last_phase_switch: float = field( default=0, metadata={"topic": "control_parameter/timestamp_last_phase_switch"}) + timestamp_phase_switch_buffer_start: Optional[float] = field( + default=None, metadata={"topic": "control_parameter/timestamp_phase_switch_buffer_start"}) timestamp_switch_on_off: Optional[float] = field( default=None, metadata={"topic": "control_parameter/timestamp_switch_on_off"}) diff --git a/packages/control/counter.py b/packages/control/counter.py index 5ede5b128e..20a3da9289 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -253,11 +253,11 @@ def get_usable_surplus(self, feed_in_yield: float) -> float: return (-self.calc_surplus() - self.data.set.released_surplus + self.data.set.reserved_surplus - feed_in_yield) - SWITCH_ON_FALLEN_BELOW = "Einschaltschwelle während der Einschaltverzögerung unterschritten." - SWITCH_ON_WAITING = "Die Ladung wird gestartet, sobald in {} die Einschaltverzögerung abgelaufen ist." + SWITCH_ON_FALLEN_BELOW = "Einschaltschwelle während der Wartezeit unterschritten." + SWITCH_ON_WAITING = "Die Ladung wird gestartet, sobald in {} die Wartezeit abgelaufen ist." SWITCH_ON_NOT_EXCEEDED = ("Die Ladung kann nicht gestartet werden, da die Einschaltschwelle nicht erreicht " "wird.") - SWITCH_ON_EXPIRED = "Einschaltschwelle für die Dauer der Einschaltverzögerung überschritten." + SWITCH_ON_EXPIRED = "Einschaltschwelle für die Dauer der Wartezeit überschritten." SWITCH_ON_MAX_PHASES = "Der Überschuss ist ausreichend, um direkt mit {} Phasen zu laden." def calc_switch_on_power(self, chargepoint: Chargepoint) -> Tuple[float, float]: @@ -354,8 +354,8 @@ def switch_on_timer_expired(self, chargepoint: Chargepoint) -> None: except Exception: log.exception("Fehler im allgemeinen PV-Modul") - SWITCH_OFF_STOP = "Ladevorgang nach Ablauf der Abschaltverzögerung gestoppt." - SWITCH_OFF_WAITING = "Ladevorgang wird nach Ablauf der Abschaltverzögerung in {} gestoppt." + SWITCH_OFF_STOP = "Ladevorgang nach Ablauf der Wartezeit gestoppt." + SWITCH_OFF_WAITING = "Ladevorgang wird nach Ablauf der Wartezeit in {} gestoppt." SWITCH_OFF_NO_STOP = ("Der Ladevorgang wird trotz fehlenden Überschusses nicht gestoppt, da in dem Fahrzeug-Profil " "die Einstellung 'Ladung aktiv halten' aktiviert ist.") SWITCH_OFF_EXCEEDED = "Abschaltschwelle während der Verzögerung überschritten." diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 11bf4a82d4..2bd1a924c4 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -315,7 +315,6 @@ def auto_phase_switch(self, max_phases: int, limit: LoadmanagementLimit) -> Tuple[int, float, Optional[str]]: message = None - timestamp_last_phase_switch = control_parameter.timestamp_last_phase_switch current = control_parameter.required_current phases_to_use = control_parameter.phases phases_in_use = control_parameter.phases @@ -334,12 +333,14 @@ def auto_phase_switch(self, new_phase = max_phases new_current = control_parameter.min_current + waiting_time = pv_config.switch_on_delay else: direction_str = f"Umschaltung von {max_phases} auf 1" # Es kann einphasig mit entsprechend niedriger Leistung gestartet werden. required_reserved_power = 0 new_phase = 1 new_current = self.ev_template.data.max_current_single_phase + waiting_time = pv_config.switch_off_delay log.debug( f'Genutzter Strom: {get_medium_charging_current(get_currents)}A, Überschuss: {all_surplus}W, benötigte ' @@ -360,17 +361,21 @@ def auto_phase_switch(self, ).data.set.reserved_surplus += max(0, required_reserved_power) message = self.PHASE_SWITCH_DELAY_TEXT.format( direction_str, - timecheck.convert_timestamp_delta_to_time_string(timestamp_last_phase_switch, delay)) + self._remaining_phase_switch_time(control_parameter, + waiting_time, + delay)[1]) control_parameter.state = ChargepointState.PHASE_SWITCH_DELAY elif condition_msg: log.debug(f"Keine Phasenumschaltung{condition_msg}") else: if condition: # Timer laufen lassen - if timecheck.check_timestamp(control_parameter.timestamp_last_phase_switch, delay): - message = self.PHASE_SWITCH_DELAY_TEXT.format( - direction_str, - timecheck.convert_timestamp_delta_to_time_string(timestamp_last_phase_switch, delay)) + timestamp_passed, remaining_time = self._remaining_phase_switch_time( + control_parameter, + waiting_time, + delay) + if timestamp_passed is False: + message = self.PHASE_SWITCH_DELAY_TEXT.format(direction_str, remaining_time) else: data.data.counter_all_data.get_evu_counter( ).data.set.reserved_surplus -= max(0, required_reserved_power) @@ -388,6 +393,36 @@ def auto_phase_switch(self, log.info(f"LP {cp_num}: {message}") return phases_to_use, current, message + def _remaining_phase_switch_time(self, control_parameter: ControlParameter, + waiting_time: float, + buffer: float) -> float: + + if control_parameter.timestamp_phase_switch_buffer_start is None: + control_parameter.timestamp_phase_switch_buffer_start = timecheck.create_timestamp() + # Wenn der Puffer seit der letzen Umschaltung abgelaufen ist, warte noch die Umschaltverzögerung ab. ODER + # Wenn der Puffer noch nicht abgelaufen ist und Wartezeit länger als Pufferzeit, dann warte Wartezeit ab + remaining_buffer = (buffer - (control_parameter.timestamp_phase_switch_buffer_start - + control_parameter.timestamp_last_phase_switch)) + if remaining_buffer < 0 or remaining_buffer < waiting_time: + if timecheck.check_timestamp(control_parameter.timestamp_phase_switch_buffer_start, waiting_time): + remaining_time = timecheck.convert_timestamp_delta_to_time_string( + control_parameter.timestamp_phase_switch_buffer_start, waiting_time) + log.debug(f"Warte verbleibende Wartezeit ab: {remaining_time}") + return False, remaining_time + else: + control_parameter.timestamp_phase_switch_buffer_start = None + return True, None + else: + # Puffer noch nicht abgelaufen, verbleibende Pufferzeit ist länger als Wartezeit + if timecheck.check_timestamp(control_parameter.timestamp_last_phase_switch, buffer): + remaining_time = timecheck.convert_timestamp_delta_to_time_string( + control_parameter.timestamp_last_phase_switch, buffer) + log.debug(f"Warte verbleibende Pufferzeit ab: {remaining_time}") + return False, remaining_time + else: + control_parameter.timestamp_phase_switch_buffer_start = None + return True, None + def reset_phase_switch(self, control_parameter: ControlParameter): """ Zurücksetzen der Zeitstempel und reservierten Leistung. diff --git a/packages/control/ev/ev_test.py b/packages/control/ev/ev_test.py index 638d08dec5..5b6649be7a 100644 --- a/packages/control/ev/ev_test.py +++ b/packages/control/ev/ev_test.py @@ -3,6 +3,7 @@ import pytest +from control.chargepoint.control_parameter import ControlParameter from control.ev.ev import Ev from helpermodules import timecheck from modules.common.abstract_vehicle import VehicleUpdateData @@ -35,3 +36,40 @@ def test_soc_interval_expired(check_timestamp: bool, # evaluation assert request_soc == expected_request_soc + + +@pytest.mark.parametrize( + "timestamp_last_phase_switch, timestamp_phase_switch_buffer_start, expected_result", + [ + pytest.param(1652682881, None, (False, "30 Sek."), id="Puffer abgelaufen, Wartezeit noch nicht gestartet"), + pytest.param(1652682881, 1652683232, (False, "10 Sek."), id="Puffer abgelaufen, Wartezeit gestartet"), + pytest.param(1652682881, 1652683212, (True, None), id="Puffer abgelaufen, Wartezeit abgelaufen"), + pytest.param(1652682962, None, (False, "30 Sek."), + id="Puffer noch nicht abgelaufen, Wartezeit länger, Wartezeit noch nicht gestartet"), + pytest.param(1652682962, 1652683237, (False, "15 Sek."), + id="Puffer noch nicht abgelaufen, Wartezeit länger, Wartezeit gestartet"), + pytest.param(1652682932, 1652683220, (True, None), + id="Puffer noch nicht abgelaufen, Wartezeit länger, Wartezeit abgelaufen"), + pytest.param(1652683132, None, (False, "3 Min."), id="Puffer noch nicht abgelaufen, Puffer länger, abwarten"), + pytest.param(1652682950, 1652682972, (True, None), + id="Puffer noch nicht abgelaufen, Puffer länger, abgelaufen"), + ], +) +def test_remaining_phase_switch_time(timestamp_last_phase_switch, + timestamp_phase_switch_buffer_start, + expected_result): + # setup + ev = Ev(0) + control_parameter = ControlParameter() + control_parameter.timestamp_last_phase_switch = timestamp_last_phase_switch + control_parameter.timestamp_phase_switch_buffer_start = timestamp_phase_switch_buffer_start + + # execution + result = ev._remaining_phase_switch_time( + control_parameter=control_parameter, + waiting_time=30, + buffer=300, + ) + + # evaluation + assert result == expected_result diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 77d188332e..eb8809c844 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -502,7 +502,8 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): "/control_parameter/timestamp_switch_on_off" in msg.topic or "/control_parameter/timestamp_charge_start" in msg.topic or "/control_parameter/timestamp_chargemode_changed" in msg.topic or - "/control_parameter/timestamp_last_phase_switch" in msg.topic): + "/control_parameter/timestamp_last_phase_switch" in msg.topic or + "/control_parameter/timestamp_phase_switch_buffer_start" in msg.topic): self._validate_value(msg, float, [(0, float("inf"))]) elif "/control_parameter/state" in msg.topic: self._validate_value(msg, int, [(0, 7)])