From 9ff6849c6ab168c280ffc1e43cbc86e9a934d732 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 24 Feb 2026 13:39:24 +0100 Subject: [PATCH 1/3] scheduled charging: increase buffer time by soc request interval after charge start --- packages/control/chargepoint/chargepoint.py | 3 ++- packages/control/ev/charge_template.py | 13 +++++++++---- packages/control/ev/charge_template_test.py | 6 +++--- packages/control/ev/ev.py | 6 ++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 328cfe899b..c7d8ed6ce1 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -667,7 +667,8 @@ def update(self, ev_list: Dict[str, Ev]) -> None: self.hw_supports_phase_switch(), self.template.data.charging_type, self.data.set.log.imported_since_plugged, - self.hw_bidi_capable()) + self.hw_bidi_capable(), + self.data.get.charge_state) required_phases = self.get_phases_by_selected_chargemode(template_phases) required_phases = self.set_phases(required_phases, template_phases) self._pub_connected_vehicle(charging_ev) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 10c8d0af57..21c2204acb 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -373,7 +373,8 @@ def scheduled_charging(self, charging_type: str, control_parameter: ControlParameter, soc_request_interval_offset: int, - bidi_state: BidiState) -> Optional[SelectedPlan]: + bidi_state: BidiState, + charge_state: bool) -> Optional[SelectedPlan]: if bidi_state == BidiState.BIDI_CAPABLE and soc is None: raise Exception("Für den Lademodis Bidi ist zwingend ein SoC-Modul erforderlich. Soll der " "SoC ausschließlich aus dem Fahrzeug ausgelesen werden, bitte auf " @@ -402,7 +403,8 @@ def scheduled_charging(self, soc_request_interval_offset, charging_type, ev_template, - bidi_state) + bidi_state, + charge_state) def _calc_remaining_time(self, plan: ScheduledChargingPlan, @@ -538,7 +540,8 @@ def scheduled_charging_calc_current(self, soc_request_interval_offset: int, charging_type: str, ev_template: EvTemplate, - bidi_state: BidiState) -> Tuple[float, str, str, int]: + bidi_state: BidiState, + charge_state: bool) -> Tuple[float, str, str, int]: current = 0 submode = "stop" if selected_plan is None: @@ -555,7 +558,9 @@ def scheduled_charging_calc_current(self, else: plan_current = plan.dc_current max_current = ev_template.data.dc_max_current - if plan.limit.selected != "soc": + if plan.limit.selected != "soc" and charge_state is False: + # das Abfrageintervall nur einbeziehen, wenn die Ladung bereits gestartet wurde, sonst wird einfach alles um das + # Abfrageintervall früher gestartet, und nicht die Pufferzeit um das Abfrageintervall erhöht. soc_request_interval_offset = 0 log.debug("Verwendeter Plan: "+str(plan.name)) if (limit.selected == "soc" and diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 97c45b7639..50a5f59c90 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -276,7 +276,7 @@ def test_scheduled_charging_calc_current(plan_data: SelectedPlan, # execution ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 3, 6, - 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE) + 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE, True) # evaluation assert ret == expected @@ -288,7 +288,7 @@ def test_scheduled_charging_calc_current_no_plans(): # execution ret = ct.scheduled_charging_calc_current( - None, 63, 5, 3, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE) + None, 63, 5, 3, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE, True) # evaluation assert ret == (0, "stop", ChargeTemplate.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, 3) @@ -383,7 +383,7 @@ def test_scheduled_charging_calc_current_electricity_tariff( # execution ret = ct.scheduled_charging_calc_current( SelectedPlan(plan=plan, remaining_time=601, phases=3, duration=3600), - current_soc, 0, 3, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE) + current_soc, 0, 3, 3, 6, 0, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE, True) # evaluation assert ret == expected diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index ae2d1aa02b..711caa994b 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -121,7 +121,8 @@ def get_required_current(self, phase_switch_supported: bool, charging_type: str, imported_since_plugged: float, - bidi: BidiState) -> Tuple[bool, Optional[str], str, float, int]: + bidi: BidiState, + charge_state: bool) -> Tuple[bool, Optional[str], str, float, int]: """ ermittelt, ob und mit welchem Strom das EV geladen werden soll (unabhängig vom Lastmanagement) Parameter @@ -166,7 +167,8 @@ def get_required_current(self, charging_type, control_parameter, soc_request_interval_offset, - bidi) + bidi, + charge_state) message = f"{tmp_message or ''}".strip() # Wenn Zielladen auf Überschuss wartet, prüfen, ob Zeitladen aktiv ist. From 397dca7667bf8aa963382546ff7da6c450e6e0f1 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 24 Feb 2026 13:42:16 +0100 Subject: [PATCH 2/3] flake8 --- packages/control/ev/charge_template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 21c2204acb..dcc23948c8 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -559,8 +559,8 @@ def scheduled_charging_calc_current(self, plan_current = plan.dc_current max_current = ev_template.data.dc_max_current if plan.limit.selected != "soc" and charge_state is False: - # das Abfrageintervall nur einbeziehen, wenn die Ladung bereits gestartet wurde, sonst wird einfach alles um das - # Abfrageintervall früher gestartet, und nicht die Pufferzeit um das Abfrageintervall erhöht. + # das Abfrageintervall nur einbeziehen, wenn die Ladung bereits gestartet wurde, sonst wird einfach alles + # um das Abfrageintervall früher gestartet, und nicht die Pufferzeit um das Abfrageintervall erhöht. soc_request_interval_offset = 0 log.debug("Verwendeter Plan: "+str(plan.name)) if (limit.selected == "soc" and From d09934d02734cc2f77ab6f5390ddab39bce1c6e5 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 3 Mar 2026 13:26:41 +0100 Subject: [PATCH 3/3] pytest --- packages/control/ev/charge_template.py | 5 ++-- packages/control/ev/charge_template_test.py | 33 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index dcc23948c8..f96976fd76 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -558,8 +558,9 @@ def scheduled_charging_calc_current(self, else: plan_current = plan.dc_current max_current = ev_template.data.dc_max_current - if plan.limit.selected != "soc" and charge_state is False: - # das Abfrageintervall nur einbeziehen, wenn die Ladung bereits gestartet wurde, sonst wird einfach alles + if plan.limit.selected != "soc" or charge_state is False: + # das Abfrageintervall nur einbeziehen, wenn die Ladung bereits gestartet wurde und + # SoC-basiertes Laden aktiv ist, sonst wird einfach alles # um das Abfrageintervall früher gestartet, und nicht die Pufferzeit um das Abfrageintervall erhöht. soc_request_interval_offset = 0 log.debug("Verwendeter Plan: "+str(plan.name)) diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 50a5f59c90..68fe7c4d83 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -294,6 +294,39 @@ def test_scheduled_charging_calc_current_no_plans(): assert ret == (0, "stop", ChargeTemplate.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, 3) +@pytest.mark.parametrize( + "selected_limit, charge_state, expected", + [ + pytest.param("soc", False, + (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:50 Uhr"), 0), + id="soc request interval not considered, use pv"), + pytest.param("soc", True, + (14, "instant_charging", + ChargeTemplate.SCHEDULED_CHARGING_IN_TIME.format(14, 'einen SoC von 80%', "07:00"), 3), + id="soc request interval considered"), + pytest.param("amount", True, + (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:50 Uhr"), 0), + id="amount, charging, soc request interval not considered, no soc limit configured"), + pytest.param("amount", False, + (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:50 Uhr"), 0), + id="amount, not charging, soc request interval not considered, no soc limit configured"), + ]) +def test_scheduled_charging_calc_current_consider_soc_request_interval_offset( + selected_limit: str, charge_state: bool, expected: Tuple[float, str, str, int]): + # setup + ct = ChargeTemplate() + plan = ScheduledChargingPlan() + plan.limit.selected = selected_limit + + # execution + ret = ct.scheduled_charging_calc_current( + SelectedPlan(plan=plan, remaining_time=601, phases=3, duration=3600), + 79, 0, 3, 3, 6, 800, ChargingType.AC.value, EvTemplate(), BidiState.BIDI_CAPABLE, charge_state) + + # evaluation + assert ret == expected + + LOADING_HOURS_TODAY = [datetime.datetime( year=2022, month=5, day=16, hour=8, minute=0).timestamp()]