From 646b95aafd1c1ed26fbc3cfb6ae8732242cd928d Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Apr 2026 14:08:07 +0200 Subject: [PATCH 1/7] time charging: min bat soc --- packages/control/bat_all.py | 20 +++++ packages/control/bat_all_test.py | 88 +++++++++++++++++++++ packages/control/ev/charge_template.py | 30 ++++--- packages/control/ev/charge_template_test.py | 39 +++++++++ packages/helpermodules/abstract_plans.py | 1 + packages/helpermodules/update_config.py | 13 ++- 6 files changed, 178 insertions(+), 13 deletions(-) diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index 31511c9352..f6653a551e 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -654,6 +654,26 @@ def get_power_limit(self): else: self.data.set.current_state = CurrentState.ACTIVE.value + def time_charging_min_bat_soc_allowed(self) -> bool: + if self.data.config.configured and self.data.config.bat_control_permitted and self.data.config.bat_control_activated: + if (self.data.config.power_limit_condition == BatPowerLimitCondition.MANUAL.value or + self.data.config.power_limit_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value): + return False + elif self.data.config.power_limit_condition == BatPowerLimitCondition.PRICE_LIMIT.value: + if self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value: + if ((self.data.config.price_limit_activated and + data.data.optional_data.ep_is_charging_allowed_price_threshold( + self.data.config.price_limit)) + or (self.data.config.price_charge_activated and + data.data.optional_data.ep_is_charging_allowed_price_threshold( + self.data.config.charge_limit))): + return True + else: + return False + else: + return + return True + def get_bat_components_by_controllability() -> Tuple[List, List]: bat_components_controllable, bat_components_not_controllable = [], [] diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 74c3f8dda4..0a3d37e17d 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -344,3 +344,91 @@ def test_control_price_limit(params: BatControlParams, data_, monkeypatch): data.data.bat_all_data._set_bat_power_active_control(data.data.bat_all_data.data.set.power_limit) assert data.data.bat_data["bat2"].data.set.power_limit == params.expected_power_limit_bat + + +@pytest.mark.parametrize( + "control_permitted, control_activated, condition, limit, expected_result", + [ + pytest.param(False, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, + id="Speichersteuerung nicht erlaubt, aber aktiviert -> laden"), + pytest.param(True, False, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, + id="Speichersteuerung erlaubt, aber nicht aktiviert -> laden"), + pytest.param(True, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + id="Manuell, volle Entladesperre -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + id="Manuell, Entladung in Fahrzeuge sperren -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, + id="Manuell, PV-Ertrag speichern -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + id="Fahrzeuge laden, volle Entladesperre -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + id="Fahrzeuge laden, Entladung in Fahrzeuge sperren -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, + id="Fahrzeuge laden, PV-Ertrag speichern -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.PRICE_LIMIT.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + id="Preislimit, volle Entladesperre -> nicht laden"), + pytest.param(True, True, BatPowerLimitCondition.PRICE_LIMIT.value, BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + id="Preislimit, Entladung in Fahrzeuge sperren -> nicht laden"), + + ] +) +def test_time_charging_min_bat_soc_allowed(control_permitted: bool, + control_activated: bool, + condition: BatPowerLimitCondition, + limit: BatPowerLimitMode, + expected_result: bool): + # setup + b = BatAll() + b.data.config.power_limit_condition = condition + b.data.config.power_limit_mode = limit + b.data.config.bat_control_permitted = control_permitted + b.data.config.bat_control_activated = control_activated + + # execution + result = b.time_charging_min_bat_soc_allowed() + + # evaluation + assert result == expected_result + + +@pytest.mark.parametrize( + "ep_configured, price_limit_activated, price_charge_activated, price_threshold_mock, expected_result", + [ + pytest.param(False, True, True, [True, True], True, + id="Preislimit aktiviert, aber kein Preis konfiguriert -> Eigenregelung -> laden"), + pytest.param(True, True, False, [True], True, + id="Strompreis für Regelmodus, Preis unter Limit -> laden"), + pytest.param(True, True, False, [False], False, + id="Strompreis für Regelmodus, Preis über Limit -> nicht laden"), + pytest.param(True, False, True, [True], True, + id="Strompreis für aktives Laden, Preis unter Limit -> laden"), + pytest.param(True, False, True, [False], False, + id="Strompreis für aktives Laden, Preis unter Limit -> nicht laden"), + pytest.param(True, False, False, [], False, + id="beide Strompreise deaktiviert -> nicht laden"), + ] +) +def test_time_charging_min_bat_soc_allowed(ep_configured: bool, + price_limit_activated: bool, + price_charge_activated: bool, + price_threshold_mock: List[bool], + expected_result: bool, + monkeypatch): + # setup + b = BatAll() + b.data.config.power_limit_condition = BatPowerLimitCondition.PRICE_LIMIT.value + b.data.config.power_limit_mode = BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value + b.data.config.price_limit_activated = price_limit_activated + b.data.config.price_charge_activated = price_charge_activated + data.data.optional_data.data.electricity_pricing.configured = ep_configured + b.data.config.bat_control_permitted = True + b.data.config.bat_control_activated = True + + monkeypatch.setattr(data.data.optional_data, "ep_is_charging_allowed_price_threshold", + Mock(side_effect=price_threshold_mock)) + + # execution + result = b.time_charging_min_bat_soc_allowed() + + # evaluation + assert result == expected_result diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index a77aa4d64e..c363e336ce 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -143,6 +143,10 @@ class ChargeTemplate: TIME_CHARGING_NO_PLAN_ACTIVE = "Kein Zeitfenster für Zeitladen aktiv." TIME_CHARGING_SOC_REACHED = "Das Ladeziel für das Zeitladen wurde erreicht." TIME_CHARGING_AMOUNT_REACHED = "Die gewünschte Energiemenge für das Zeitladen wurde geladen." + TIME_CHARGING_CONFLICT_ACTIVE_BAT_CONTROL = ("Laden mit Zeitladen nach Speicher-SoC nicht möglich, da dies im " + "Konflikt mit der aktiven Speichersteuerung steht.") + TIME_CHARGING_MIN_BAT_SOC_REACHED = ("Laden mit Zeitladen nach Speicher-SoC nicht möglich, da der minimale SoC des" + " Speichers erreicht wurde.") def time_charging(self, soc: Optional[float], @@ -151,34 +155,38 @@ def time_charging(self, """ prüft, ob ein Zeitfenster aktiv ist und setzt entsprechend den Ladestrom """ message = None - sub_mode = "time_charging" + sub_mode = "stop" + current = 0 id = None phases = None try: if self.data.time_charging.plans: plan = timecheck.check_plans_timeframe(self.data.time_charging.plans) if plan is not None: - current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current - phases = plan.phases_to_use id = plan.id + phases = plan.phases_to_use if plan.limit.selected == "soc" and soc and soc >= plan.limit.soc: # SoC-Limit erreicht - current = 0 - sub_mode = "stop" message = self.TIME_CHARGING_SOC_REACHED elif plan.limit.selected == "amount" and used_amount_time_charging >= plan.limit.amount: # Energie-Limit erreicht - current = 0 - sub_mode = "stop" message = self.TIME_CHARGING_AMOUNT_REACHED + elif plan.min_bat_soc is not None: + if data.data.bat_all_data.time_charging_min_bat_soc_allowed(): + if data.data.bat_all_data.data.get.soc < plan.min_bat_soc: + message = self.TIME_CHARGING_MIN_BAT_SOC_REACHED + else: + current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current + sub_mode = "time_charging" + else: + message = self.TIME_CHARGING_CONFLICT_ACTIVE_BAT_CONTROL + else: + current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current + sub_mode = "time_charging" else: message = self.TIME_CHARGING_NO_PLAN_ACTIVE - current = 0 - sub_mode = "stop" else: message = self.TIME_CHARGING_NO_PLAN_CONFIGURED - current = 0 - sub_mode = "stop" return current, sub_mode, message, id, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.data.id)) diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 228deb4e5a..9669032c24 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -6,6 +6,7 @@ from control import data from control import optional +from control.bat_all import BatAll from control.chargepoint.control_parameter import ControlParameter from control.ev.charge_template import SelectedPlan from control.chargepoint.charging_type import ChargingType @@ -73,6 +74,44 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou assert ret == expected +@pytest.mark.parametrize( + "min_bat_soc, charging_allowed_mock, soc, expected", + [ + pytest.param(80, False, 81, (0, "stop", ChargeTemplate.TIME_CHARGING_CONFLICT_ACTIVE_BAT_CONTROL, 0, 1), + id="Konflikt mit aktiver Speichersteuerung -> nicht laden"), + pytest.param(80, True, 79, (0, "stop", ChargeTemplate.TIME_CHARGING_MIN_BAT_SOC_REACHED, 0, 1), + id="Mindest-SoC des Speichers unterschritten -> nicht laden"), + pytest.param(80, True, 80, (16, "time_charging", None, 0, 1), id="laden erlaubt"), + pytest.param(None, True, 80, (16, "time_charging", None, 0, 1), + id="Mindest-SoC-Beachtung nicht konfiguriert, laden erlaubt"), + ] +) +def test_time_charging_min_bat_soc(min_bat_soc: Optional[int], + charging_allowed_mock: bool, + soc: float, + expected: Tuple[int, str, Optional[str], Optional[str], int], + monkeypatch): + # setup + ct = ChargeTemplate() + plan = TimeChargingPlan(id=0) + plan.min_bat_soc = min_bat_soc + ct.data.time_charging.plans = [plan] + check_plans_timeframe_mock = Mock(return_value=plan) + monkeypatch.setattr(timecheck, "check_plans_timeframe", check_plans_timeframe_mock) + + data.data.bat_all_data = BatAll() + data.data.bat_all_data.data.config.configured = True + data.data.bat_all_data.data.get.soc = soc + monkeypatch.setattr(data.data.bat_all_data, "time_charging_min_bat_soc_allowed", + Mock(return_value=charging_allowed_mock)) + + # execution + ret = ct.time_charging(soc, 100, ChargingType.AC.value) + + # evaluation + assert ret == expected + + @pytest.mark.parametrize( "selected, current_soc, used_amount, expected", [ diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 6c7e3ff78f..20b3ce6b98 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -97,6 +97,7 @@ class TimeChargingPlan(TimeframePlan): dc_current: float = 145 id: Optional[int] = None limit: Limit = field(default_factory=limit_factory) + min_bat_soc: Optional[int] = None name: str = "neuer Zeitladen-Plan" phases_to_use: int = 1 diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 7a34baf412..2bf25eae64 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 128 + DATASTORE_VERSION = 129 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3265,4 +3265,13 @@ def upgrade_component(topic: str, payload) -> Optional[dict]: MessageType.INFO ) - self._append_datastore_version(128) + def upgrade_datastore_129(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/vehicle/template/charge_template/[0-9]+$", topic) is not None: + payload = decode_payload(payload) + for plan in payload["time_charging"]["plans"]: + if plan.get("min_bat_soc") is None: + plan.update({"min_bat_soc": None}) + return {topic: payload} + self._loop_all_received_topics(upgrade) + self._append_datastore_version(129) From 179826848c6193e427f31b370a5b4b5a61be8f1c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Apr 2026 14:29:11 +0200 Subject: [PATCH 2/7] log --- packages/control/ev/charge_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index c363e336ce..11a7c60cbe 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -176,6 +176,7 @@ def time_charging(self, if data.data.bat_all_data.data.get.soc < plan.min_bat_soc: message = self.TIME_CHARGING_MIN_BAT_SOC_REACHED else: + log.debug("Zeitladen: minimaler Speicher-SoC überschritten, Laden mit Zeitladen möglich.") current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current sub_mode = "time_charging" else: From 81b6339272ed6108842fee5c05b173e48c4a6a34 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Apr 2026 14:41:38 +0200 Subject: [PATCH 3/7] review --- packages/control/bat_all.py | 6 ++-- packages/control/bat_all_test.py | 48 +++++++++++++++++--------- packages/control/ev/charge_template.py | 5 +-- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index f6653a551e..4c6511145b 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -655,7 +655,9 @@ def get_power_limit(self): self.data.set.current_state = CurrentState.ACTIVE.value def time_charging_min_bat_soc_allowed(self) -> bool: - if self.data.config.configured and self.data.config.bat_control_permitted and self.data.config.bat_control_activated: + if (self.data.config.configured and + self.data.config.bat_control_permitted and + self.data.config.bat_control_activated): if (self.data.config.power_limit_condition == BatPowerLimitCondition.MANUAL.value or self.data.config.power_limit_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value): return False @@ -671,7 +673,7 @@ def time_charging_min_bat_soc_allowed(self) -> bool: else: return False else: - return + return False return True diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 0a3d37e17d..da557332a1 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -349,25 +349,39 @@ def test_control_price_limit(params: BatControlParams, data_, monkeypatch): @pytest.mark.parametrize( "control_permitted, control_activated, condition, limit, expected_result", [ - pytest.param(False, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, + pytest.param(False, True, + BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, id="Speichersteuerung nicht erlaubt, aber aktiviert -> laden"), - pytest.param(True, False, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, + pytest.param(True, False, + BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, id="Speichersteuerung erlaubt, aber nicht aktiviert -> laden"), - pytest.param(True, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + pytest.param(True, True, + BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, id="Manuell, volle Entladesperre -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + pytest.param(True, True, + BatPowerLimitCondition.MANUAL.value, + BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, id="Manuell, Entladung in Fahrzeuge sperren -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, + pytest.param(True, True, + BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, id="Manuell, PV-Ertrag speichern -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + pytest.param(True, True, + BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, id="Fahrzeuge laden, volle Entladesperre -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + pytest.param(True, True, + BatPowerLimitCondition.VEHICLE_CHARGING.value, + BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, id="Fahrzeuge laden, Entladung in Fahrzeuge sperren -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, + pytest.param(True, True, + BatPowerLimitCondition.VEHICLE_CHARGING.value, + BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, id="Fahrzeuge laden, PV-Ertrag speichern -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.PRICE_LIMIT.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + pytest.param(True, True, + BatPowerLimitCondition.PRICE_LIMIT.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, id="Preislimit, volle Entladesperre -> nicht laden"), - pytest.param(True, True, BatPowerLimitCondition.PRICE_LIMIT.value, BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + pytest.param(True, True, + BatPowerLimitCondition.PRICE_LIMIT.value, + BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, id="Preislimit, Entladung in Fahrzeuge sperren -> nicht laden"), ] @@ -379,6 +393,7 @@ def test_time_charging_min_bat_soc_allowed(control_permitted: bool, expected_result: bool): # setup b = BatAll() + b.data.config.configured = True b.data.config.power_limit_condition = condition b.data.config.power_limit_mode = limit b.data.config.bat_control_permitted = control_permitted @@ -408,14 +423,15 @@ def test_time_charging_min_bat_soc_allowed(control_permitted: bool, id="beide Strompreise deaktiviert -> nicht laden"), ] ) -def test_time_charging_min_bat_soc_allowed(ep_configured: bool, - price_limit_activated: bool, - price_charge_activated: bool, - price_threshold_mock: List[bool], - expected_result: bool, - monkeypatch): +def test_time_charging_min_bat_soc_allowed_pricing(ep_configured: bool, + price_limit_activated: bool, + price_charge_activated: bool, + price_threshold_mock: List[bool], + expected_result: bool, + monkeypatch): # setup b = BatAll() + b.data.config.configured = True b.data.config.power_limit_condition = BatPowerLimitCondition.PRICE_LIMIT.value b.data.config.power_limit_mode = BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value b.data.config.price_limit_activated = price_limit_activated diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 11a7c60cbe..68e33222bc 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -171,12 +171,13 @@ def time_charging(self, elif plan.limit.selected == "amount" and used_amount_time_charging >= plan.limit.amount: # Energie-Limit erreicht message = self.TIME_CHARGING_AMOUNT_REACHED - elif plan.min_bat_soc is not None: + elif plan.min_bat_soc is not None and data.data.bat_all_data.data.config.configured: if data.data.bat_all_data.time_charging_min_bat_soc_allowed(): if data.data.bat_all_data.data.get.soc < plan.min_bat_soc: message = self.TIME_CHARGING_MIN_BAT_SOC_REACHED else: - log.debug("Zeitladen: minimaler Speicher-SoC überschritten, Laden mit Zeitladen möglich.") + log.debug( + "Zeitladen: minimaler Speicher-SoC überschritten, Laden mit Zeitladen möglich.") current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current sub_mode = "time_charging" else: From 603c5bd7d5298165796ceded96797b0fa8809dcb Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:50:49 +0200 Subject: [PATCH 4/7] Update packages/control/ev/charge_template.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 68e33222bc..9c219247c9 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -145,8 +145,8 @@ class ChargeTemplate: TIME_CHARGING_AMOUNT_REACHED = "Die gewünschte Energiemenge für das Zeitladen wurde geladen." TIME_CHARGING_CONFLICT_ACTIVE_BAT_CONTROL = ("Laden mit Zeitladen nach Speicher-SoC nicht möglich, da dies im " "Konflikt mit der aktiven Speichersteuerung steht.") - TIME_CHARGING_MIN_BAT_SOC_REACHED = ("Laden mit Zeitladen nach Speicher-SoC nicht möglich, da der minimale SoC des" - " Speichers erreicht wurde.") + TIME_CHARGING_MIN_BAT_SOC_REACHED = ("Laden mit Zeitladen nach Speicher-SoC nicht möglich, da der SoC des" + " Speichers unter dem minimalen SoC liegt.") def time_charging(self, soc: Optional[float], From f5b04b73a25041b77f9edf0b309c4187600b67a8 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 10 Jun 2026 14:11:04 +0200 Subject: [PATCH 5/7] fix --- packages/control/bat_all.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index 4c6511145b..2c370bfa23 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -655,24 +655,15 @@ def get_power_limit(self): self.data.set.current_state = CurrentState.ACTIVE.value def time_charging_min_bat_soc_allowed(self) -> bool: - if (self.data.config.configured and - self.data.config.bat_control_permitted and - self.data.config.bat_control_activated): - if (self.data.config.power_limit_condition == BatPowerLimitCondition.MANUAL.value or + if self.data.config.configured and self.data.config.bat_control_activated: + # manueller Modus und keine Eigenregelung oder aktive Speichersteuerung bei Fahrzeugladung + if ((self.data.config.power_limit_condition == BatPowerLimitCondition.MANUAL.value and + self.data.config.manual_mode != ManualMode.MANUAL_DISABLE.value) or self.data.config.power_limit_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value): return False elif self.data.config.power_limit_condition == BatPowerLimitCondition.PRICE_LIMIT.value: - if self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value: - if ((self.data.config.price_limit_activated and - data.data.optional_data.ep_is_charging_allowed_price_threshold( - self.data.config.price_limit)) - or (self.data.config.price_charge_activated and - data.data.optional_data.ep_is_charging_allowed_price_threshold( - self.data.config.charge_limit))): - return True - else: - return False - else: + # Preisgrenze aktiv und Ladeleistung des Speichers vorgegeben + if self.data.set.power_limit is not None: return False return True From 0565cf80dd91d2496019daf6f52614d3734f073c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 10 Jun 2026 14:34:55 +0200 Subject: [PATCH 6/7] review --- packages/control/bat_all.py | 10 ++++++++-- packages/control/bat_all_test.py | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index 2c370bfa23..7f487f7736 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -662,8 +662,14 @@ def time_charging_min_bat_soc_allowed(self) -> bool: self.data.config.power_limit_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value): return False elif self.data.config.power_limit_condition == BatPowerLimitCondition.PRICE_LIMIT.value: - # Preisgrenze aktiv und Ladeleistung des Speichers vorgegeben - if self.data.set.power_limit is not None: + if ((self.data.config.price_limit_activated and + data.data.optional_data.ep_is_charging_allowed_price_threshold( + self.data.config.price_limit)) + or (self.data.config.price_charge_activated and + data.data.optional_data.ep_is_charging_allowed_price_threshold( + self.data.config.charge_limit))): + return True + else: return False return True diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index da557332a1..42bf83f7fa 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -437,7 +437,6 @@ def test_time_charging_min_bat_soc_allowed_pricing(ep_configured: bool, b.data.config.price_limit_activated = price_limit_activated b.data.config.price_charge_activated = price_charge_activated data.data.optional_data.data.electricity_pricing.configured = ep_configured - b.data.config.bat_control_permitted = True b.data.config.bat_control_activated = True monkeypatch.setattr(data.data.optional_data, "ep_is_charging_allowed_price_threshold", From 68983bf52d70b17736a3e44e199325abfd4e9ad5 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 11 Jun 2026 09:00:51 +0200 Subject: [PATCH 7/7] pytest --- packages/control/bat_all_test.py | 71 ++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 42bf83f7fa..c9787d71d8 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -347,57 +347,68 @@ def test_control_price_limit(params: BatControlParams, data_, monkeypatch): @pytest.mark.parametrize( - "control_permitted, control_activated, condition, limit, expected_result", + "control_activated, condition, limit, manual_mode, expected_result", [ - pytest.param(False, True, - BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, - id="Speichersteuerung nicht erlaubt, aber aktiviert -> laden"), - pytest.param(True, False, - BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, True, - id="Speichersteuerung erlaubt, aber nicht aktiviert -> laden"), - pytest.param(True, True, - BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, - id="Manuell, volle Entladesperre -> nicht laden"), - pytest.param(True, True, + pytest.param(False, BatPowerLimitCondition.MANUAL.value, - BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + BatPowerLimitMode.MODE_NO_DISCHARGE.value, + ManualMode.MANUAL_DISABLE.value, True, + id="Speichersteuerung nicht aktiviert, aber aktiviert -> laden"), + pytest.param(True, + BatPowerLimitCondition.MANUAL.value, + BatPowerLimitMode.MODE_NO_DISCHARGE.value, + ManualMode.MANUAL_DISABLE.value, True, + id="Manuell, Eigenregelung, volle Entladesperre -> nicht laden"), + pytest.param(True, + BatPowerLimitCondition.MANUAL.value, + BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, + ManualMode.MANUAL_LIMIT.value, False, id="Manuell, Entladung in Fahrzeuge sperren -> nicht laden"), - pytest.param(True, True, - BatPowerLimitCondition.MANUAL.value, BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, + pytest.param(True, + BatPowerLimitCondition.MANUAL.value, + BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, + ManualMode.MANUAL_CHARGE.value, False, id="Manuell, PV-Ertrag speichern -> nicht laden"), - pytest.param(True, True, - BatPowerLimitCondition.VEHICLE_CHARGING.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + pytest.param(True, + BatPowerLimitCondition.VEHICLE_CHARGING.value, + BatPowerLimitMode.MODE_NO_DISCHARGE.value, + ManualMode.MANUAL_DISABLE.value, False, id="Fahrzeuge laden, volle Entladesperre -> nicht laden"), - pytest.param(True, True, + pytest.param(True, BatPowerLimitCondition.VEHICLE_CHARGING.value, - BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, + ManualMode.MANUAL_DISABLE.value, False, id="Fahrzeuge laden, Entladung in Fahrzeuge sperren -> nicht laden"), - pytest.param(True, True, + pytest.param(True, BatPowerLimitCondition.VEHICLE_CHARGING.value, - BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, False, + BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value, + ManualMode.MANUAL_DISABLE.value, False, id="Fahrzeuge laden, PV-Ertrag speichern -> nicht laden"), - pytest.param(True, True, - BatPowerLimitCondition.PRICE_LIMIT.value, BatPowerLimitMode.MODE_NO_DISCHARGE.value, False, + pytest.param(True, + BatPowerLimitCondition.PRICE_LIMIT.value, + BatPowerLimitMode.MODE_NO_DISCHARGE.value, + ManualMode.MANUAL_DISABLE.value, False, id="Preislimit, volle Entladesperre -> nicht laden"), - pytest.param(True, True, + pytest.param(True, BatPowerLimitCondition.PRICE_LIMIT.value, - BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, False, + BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value, + ManualMode.MANUAL_DISABLE.value, False, id="Preislimit, Entladung in Fahrzeuge sperren -> nicht laden"), ] ) -def test_time_charging_min_bat_soc_allowed(control_permitted: bool, - control_activated: bool, - condition: BatPowerLimitCondition, - limit: BatPowerLimitMode, +def test_time_charging_min_bat_soc_allowed(control_activated: bool, + condition: str, + limit: str, + manual_mode: str, expected_result: bool): # setup b = BatAll() b.data.config.configured = True b.data.config.power_limit_condition = condition b.data.config.power_limit_mode = limit - b.data.config.bat_control_permitted = control_permitted b.data.config.bat_control_activated = control_activated + b.data.config.manual_mode = manual_mode # execution result = b.time_charging_min_bat_soc_allowed() @@ -428,7 +439,7 @@ def test_time_charging_min_bat_soc_allowed_pricing(ep_configured: bool, price_charge_activated: bool, price_threshold_mock: List[bool], expected_result: bool, - monkeypatch): + monkeypatch: pytest.MonkeyPatch): # setup b = BatAll() b.data.config.configured = True