From 1611e8a44d2a3f041639df27afe0e2134a1970ec Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 28 Sep 2026 09:50:23 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Verbraucher:=20Refactor=20f=C3=BCr=20ResetC?= =?UTF-8?q?hargemode=20auf=20einmalig,=20t=C3=A4glich,=20w=C3=B6chentlich?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/control/consumer/consumer.py | 27 +++----- packages/control/consumer/consumer_data.py | 12 +--- packages/control/consumer/consumer_test.py | 66 ++++++++++++++++++ packages/helpermodules/abstract_plans.py | 10 ++- packages/helpermodules/timecheck.py | 6 +- packages/helpermodules/update_config.py | 34 ++++++++- packages/helpermodules/update_config_test.py | 72 +++++++++++++++++--- 7 files changed, 181 insertions(+), 46 deletions(-) diff --git a/packages/control/consumer/consumer.py b/packages/control/consumer/consumer.py index 2a2cc2513f..17d1110705 100644 --- a/packages/control/consumer/consumer.py +++ b/packages/control/consumer/consumer.py @@ -7,7 +7,7 @@ from control.algorithm.utils import get_medium_charging_current from control.chargemode import Chargemode from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState -from control.consumer.consumer_data import ConsumerData, ConsumerUsage, ResetModes, WaitForStartStates +from control.consumer.consumer_data import ConsumerData, ConsumerUsage, WaitForStartStates from control.load_protocol import Load from control.text import format_next_time_charging_start from helpermodules import timecheck @@ -465,26 +465,19 @@ def reset_wait_for_start(self): self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_DEVICE_START def midnight_handler(self): - self.reset_chargemode_at_midnight() self.reset_wait_for_start() self.reset_on_time() - def reset_chargemode_at_midnight(self): - if self.data.usage.reset_chargemode.mode == ResetModes.MIDNIGHT: - if self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode: - log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} " - f"für Verbraucher {self.num} um Mitternacht.") - self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode - def reset_chargemode_at_time(self): - if (self.data.usage.reset_chargemode.mode == ResetModes.TIME and - self.data.usage.reset_chargemode.time is not None): - if (self.data.usage.reset_chargemode.time < timecheck.create_timestamp() < - self.data.usage.reset_chargemode.time + data.data.general_data.data.control_interval): - if self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode: - log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} " - f"für Verbraucher {self.num} um definierte Zeit.") - self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode + if self.data.usage.reset_chargemode.active is False: + return + + if ((data.data.general_data.data.control_interval * -1 < timecheck.check_end_time( + self.data.usage.reset_chargemode) <= 0) and + self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode): + log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} " + f"für Verbraucher {self.num} um definierte Zeit.") + self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode def set_control_parameter(self, required_current: float, diff --git a/packages/control/consumer/consumer_data.py b/packages/control/consumer/consumer_data.py index 42d5c75029..e021dee601 100644 --- a/packages/control/consumer/consumer_data.py +++ b/packages/control/consumer/consumer_data.py @@ -5,7 +5,7 @@ from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory from control.consumer.usage import ConsumerUsage from dataclass_utils.factories import empty_list_factory, voltages_list_factory -from helpermodules.abstract_plans import ScheduledPlanConsumer, TimeChargingPlanConsumer +from helpermodules.abstract_plans import ScheduledPlanConsumer, TimeChargingPlanConsumer, TimepointPlan from helpermodules.constants import NO_ERROR from modules.common.consumer_setup import ConsumerSetup @@ -33,16 +33,8 @@ class WaitForStartStates(Enum): START_SIGNAL_RECEIVED = "start_signal_received" -class ResetModes(Enum): - NEVER = "never" - MIDNIGHT = "midnight" - TIME = "time" - - @dataclass -class ResetChargemode: - mode: ResetModes = ResetModes.NEVER - time: Optional[int] = None +class ResetChargemode(TimepointPlan): chargemode: Chargemode = Chargemode.INSTANT_CHARGING diff --git a/packages/control/consumer/consumer_test.py b/packages/control/consumer/consumer_test.py index 11cfbf6d36..a9b1284a9b 100644 --- a/packages/control/consumer/consumer_test.py +++ b/packages/control/consumer/consumer_test.py @@ -497,3 +497,69 @@ def test_scheduled_charging_calc_current_electricity_tariff( # evaluation assert ret == expected + + +@pytest.mark.parametrize( + "is_active,current_mode,target_mode,check_end_time_result,expected_mode,expected_check_end_time_calls", + [ + pytest.param( + False, + Chargemode.STOP, + Chargemode.PV_CHARGING, + 0, + Chargemode.STOP, + 0, + id="inactive-noop", + ), + pytest.param( + True, + Chargemode.STOP, + Chargemode.PV_CHARGING, + 5, + Chargemode.STOP, + 1, + id="active-not-due-noop", + ), + pytest.param( + True, + Chargemode.STOP, + Chargemode.PV_CHARGING, + 0, + Chargemode.PV_CHARGING, + 1, + id="active-due-switch", + ), + pytest.param( + True, + Chargemode.PV_CHARGING, + Chargemode.PV_CHARGING, + 0, + Chargemode.PV_CHARGING, + 1, + id="active-due-already-target", + ), + ], +) +def test_reset_chargemode_at_time( + consumer: Consumer, + monkeypatch: pytest.MonkeyPatch, + is_active: bool, + current_mode: Chargemode, + target_mode: Chargemode, + check_end_time_result: int, + expected_mode: Chargemode, + expected_check_end_time_calls: int, +): + # setup + consumer.data.usage.chargemode = current_mode + consumer.data.usage.reset_chargemode.active = is_active + consumer.data.usage.reset_chargemode.chargemode = target_mode + check_end_time_mock = Mock(return_value=check_end_time_result) + monkeypatch.setattr(timecheck, "check_end_time", check_end_time_mock) + + # execution + consumer.reset_chargemode_at_time() + + # evaluation + assert consumer.data.usage.chargemode == expected_mode + assert check_end_time_mock.call_count == expected_check_end_time_calls diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 65bec56e84..669e7d8078 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -76,19 +76,23 @@ class TimeframePlan(PlanBase): @dataclass -class ScheduledChargingPlan(PlanBase): +class TimepointPlan(PlanBase): + time: str = "07:00" + frequency: FrequencyDate = field(default_factory=frequency_date_factory) + + +@dataclass +class ScheduledChargingPlan(TimepointPlan): bidi_charging_enabled: bool = False bidi_power: int = 10000 current: int = 14 dc_current: float = 145 et_active: bool = False - frequency: FrequencyDate = field(default_factory=frequency_date_factory) id: Optional[int] = None name: str = "neuer Zielladen-Plan" limit: ScheduledLimit = field(default_factory=scheduled_limit_factory) phases_to_use: int = 0 phases_to_use_pv: int = 0 - time: str = "07:00" # ToDo: aktuelle Zeit verwenden @dataclass diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index e4720c4d7f..2ae0a0e476 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -10,7 +10,7 @@ from dateutil.relativedelta import relativedelta from helpermodules.abstract_plans import (AutolockPlan, ScheduledChargingPlan, ScheduledPlanConsumer, - TimeChargingPlan, TimeChargingPlanConsumer) + TimeChargingPlan, TimeChargingPlanConsumer, TimepointPlan) log = logging.getLogger(__name__) @@ -201,8 +201,8 @@ def _get_next_start_for_timeframe_plan(plan: T, now: datetime.datetime) -> Optio return None -def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, TimeChargingPlanConsumer], - buffer: Optional[float]) -> float: +def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, TimeChargingPlanConsumer, TimepointPlan], + buffer: float = 0) -> float: """ gibt die verbleibende Zeit in Sekunden zurück. Return diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 5777eb80e8..c4256495fa 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -14,6 +14,7 @@ from paho.mqtt.client import Client as MqttClient, MQTTMessage from control.chargemode import Chargemode +from control.consumer.consumer_data import ResetChargemode from control.consumer.usage import NOT_CONTROLLED from control.limiting_value import LoadmanagementLimit @@ -22,7 +23,7 @@ from helpermodules import hardware_configuration from helpermodules import pub from helpermodules.broker import BrokerClient -from helpermodules.abstract_plans import Limit +from helpermodules.abstract_plans import FrequencyDate, Limit from helpermodules.constants import DEFAULT_COLORS, NO_ERROR from helpermodules.hardware_configuration import ( get_hardware_configuration_setting, @@ -62,7 +63,7 @@ class UpdateConfig: - DATASTORE_VERSION = 152 + DATASTORE_VERSION = 153 FILE_OPERATION_VERSION = 0 @@ -4070,3 +4071,32 @@ def upgrade_datastore_152(self): else: remove_acl_role("consumer--write-access", consumer_id) self._append_datastore_version(152) + + def upgrade_datastore_153(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search(r"^openWB/consumer/[0-9]+/usage$", topic) is not None: + usage = decode_payload(payload) + reset_chargemode = usage["reset_chargemode"] + if reset_chargemode.get("mode") is not None: + # altes Schema + if reset_chargemode["mode"] == "never": + reset_chargemode = ResetChargemode(active=False) + elif reset_chargemode["mode"] == "midnight": + reset_chargemode = ResetChargemode(active=True, time="00:00", + frequency=FrequencyDate(selected="daily")) + elif reset_chargemode["mode"] == "time": + timestamp = reset_chargemode["time"] + if timestamp is not None: + date = datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d") + time = datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M") + else: + date = datetime.datetime.today().strftime("%Y-%m-%d") + time = "07:00" + reset_chargemode = ResetChargemode( + active=True, time=time, frequency=FrequencyDate(selected="once", once=date)) + + usage["reset_chargemode"] = reset_chargemode + return {topic: asdict(usage)} + return None + self._loop_all_received_topics(upgrade) + self._append_datastore_version(153) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index 1938dcc7ea..fb24f18328 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -1,5 +1,6 @@ -from typing import List +from typing import List, Optional from unittest.mock import Mock, patch, mock_open +import datetime from control.chargemode import Chargemode from control.ev.charge_template import ChargeTemplate import dataclass_utils @@ -487,16 +488,65 @@ def test_upgrade_datastore_151_removes_linked_extra_meter_counters_from_hierarch uc.upgrade_datastore_151() - assert uc.all_received_topics["openWB/counter/get/hierarchy"] == [{ - "id": 0, - "type": "counter", - "children": [ - {"id": 2, "type": "consumer", "children": []}, - {"id": 6, "type": "counter", "children": []}, - ], - }] - assert uc.all_received_topics["openWB/system/datastore_version"] == list(range(152)) - assert mock_pub.pub.call_count == 2 + +@pytest.mark.parametrize( + "legacy_reset, expected_active, expected_time, expected_selected, expected_once, expected_chargemode", + [ + pytest.param( + {"mode": "never", "time": None, "chargemode": "pv_charging"}, + False, + None, + None, + None, + "pv_charging", + id="legacy-never", + ), + pytest.param( + {"mode": "midnight", "time": None, "chargemode": "eco_charging"}, + True, + "00:00", + "daily", + None, + "eco_charging", + id="legacy-midnight", + ), + pytest.param( + {"mode": "time", "time": 1735687800, "chargemode": "stop"}, + True, + datetime.datetime.fromtimestamp(1735687800).strftime("%H:%M"), + "once", + datetime.datetime.fromtimestamp(1735687800).strftime("%Y-%m-%d"), + "stop", + id="legacy-time-with-timestamp", + ), + ], +) +def test_upgrade_datastore_152_migrates_legacy_modes( + legacy_reset: dict, + expected_active: bool, + expected_time: Optional[str], + expected_selected: Optional[str], + expected_once: Optional[str], + expected_chargemode: str, +): + uc = UpdateConfig() + uc.all_received_topics = { + "openWB/consumer/1/usage": {"reset_chargemode": legacy_reset}, + "openWB/system/datastore_version": [151], + } + + uc.upgrade_datastore_152() + + result = uc.all_received_topics["openWB/consumer/1/usage"]["reset_chargemode"] + assert result["active"] is expected_active + if expected_time is not None: + assert result["time"] == expected_time + if expected_selected is not None: + assert result["frequency"]["selected"] == expected_selected + if expected_once is not None: + assert result["frequency"]["once"] == expected_once + assert result["chargemode"] == expected_chargemode + assert uc.all_received_topics["openWB/system/datastore_version"] == [151, 152] @pytest.mark.parametrize("file_operation_version, finished, expected_calls", [ From d809b26594b48704f0e7d2db84fe15d0214677bf Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 28 Sep 2026 09:54:32 +0200 Subject: [PATCH 2/7] flake8 --- packages/helpermodules/update_config_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index fb24f18328..edd02d81ce 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -1,6 +1,5 @@ from typing import List, Optional from unittest.mock import Mock, patch, mock_open -import datetime from control.chargemode import Chargemode from control.ev.charge_template import ChargeTemplate import dataclass_utils From a86a0e92e3e7ae9222171d2b60d4303aca27373f Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 28 Sep 2026 10:29:52 +0200 Subject: [PATCH 3/7] review --- packages/control/consumer/consumer.py | 8 +++-- packages/control/consumer/consumer_data.py | 1 + packages/control/consumer/consumer_test.py | 10 ++++--- packages/helpermodules/timecheck.py | 27 +++++++++++++++++ packages/helpermodules/timecheck_test.py | 34 ++++++++++++++++++++++ packages/helpermodules/update_config.py | 9 ++++-- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/control/consumer/consumer.py b/packages/control/consumer/consumer.py index 17d1110705..def4aafc6c 100644 --- a/packages/control/consumer/consumer.py +++ b/packages/control/consumer/consumer.py @@ -472,8 +472,12 @@ def reset_chargemode_at_time(self): if self.data.usage.reset_chargemode.active is False: return - if ((data.data.general_data.data.control_interval * -1 < timecheck.check_end_time( - self.data.usage.reset_chargemode) <= 0) and + control_interval = data.data.general_data.data.control_interval + reset_window_start = -control_interval + remaining_time = timecheck.check_end_time_current_occurrence(self.data.usage.reset_chargemode) + is_within_reset_window = reset_window_start < remaining_time <= 0 + + if (is_within_reset_window and self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode): log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} " f"für Verbraucher {self.num} um definierte Zeit.") diff --git a/packages/control/consumer/consumer_data.py b/packages/control/consumer/consumer_data.py index e021dee601..f830321cba 100644 --- a/packages/control/consumer/consumer_data.py +++ b/packages/control/consumer/consumer_data.py @@ -35,6 +35,7 @@ class WaitForStartStates(Enum): @dataclass class ResetChargemode(TimepointPlan): + active: bool = False chargemode: Chargemode = Chargemode.INSTANT_CHARGING diff --git a/packages/control/consumer/consumer_test.py b/packages/control/consumer/consumer_test.py index a9b1284a9b..b64e6b6631 100644 --- a/packages/control/consumer/consumer_test.py +++ b/packages/control/consumer/consumer_test.py @@ -500,7 +500,7 @@ def test_scheduled_charging_calc_current_electricity_tariff( @pytest.mark.parametrize( - "is_active,current_mode,target_mode,check_end_time_result,expected_mode,expected_check_end_time_calls", + "is_active,current_mode,target_mode,remaining_time,expected_mode,expected_check_end_time_calls", [ pytest.param( False, @@ -546,7 +546,7 @@ def test_reset_chargemode_at_time( is_active: bool, current_mode: Chargemode, target_mode: Chargemode, - check_end_time_result: int, + remaining_time: int, expected_mode: Chargemode, expected_check_end_time_calls: int, ): @@ -554,8 +554,8 @@ def test_reset_chargemode_at_time( consumer.data.usage.chargemode = current_mode consumer.data.usage.reset_chargemode.active = is_active consumer.data.usage.reset_chargemode.chargemode = target_mode - check_end_time_mock = Mock(return_value=check_end_time_result) - monkeypatch.setattr(timecheck, "check_end_time", check_end_time_mock) + check_end_time_mock = Mock(return_value=remaining_time) + monkeypatch.setattr(timecheck, "check_end_time_current_occurrence", check_end_time_mock) # execution consumer.reset_chargemode_at_time() @@ -563,3 +563,5 @@ def test_reset_chargemode_at_time( # evaluation assert consumer.data.usage.chargemode == expected_mode assert check_end_time_mock.call_count == expected_check_end_time_calls + if expected_check_end_time_calls: + check_end_time_mock.assert_called_once_with(consumer.data.usage.reset_chargemode) diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index 2ae0a0e476..eeee139646 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -238,6 +238,33 @@ def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, Tim return remaining_time.total_seconds() +def check_end_time_current_occurrence(plan: TimepointPlan) -> float: + now = datetime.datetime.today() + end = datetime.datetime.strptime(plan.time, '%H:%M') + + if plan.frequency.selected == "once": + end_date = datetime.datetime.strptime(plan.frequency.once, "%Y-%m-%d") + end = end.replace(end_date.year, end_date.month, end_date.day) + elif plan.frequency.selected == "daily": + end = end.replace(now.year, now.month, now.day) + elif plan.frequency.selected == "weekly": + if not any(plan.frequency.weekly): + raise ValueError("Es muss mindestens ein Tag ausgewählt werden.") + end = end.replace(now.year, now.month, now.day) + today_weekday = now.weekday() + if not plan.frequency.weekly[today_weekday]: + days_to_previous_selected = next( + day_offset + for day_offset in range(1, 7) + if plan.frequency.weekly[(today_weekday - day_offset) % 7] + ) + end = end - datetime.timedelta(days=days_to_previous_selected) + else: + raise TypeError(f'Unbekannte Häufigkeit {plan.frequency.selected}') + + return (end - now).total_seconds() + + def _get_next_charging_day(weekly: List[bool], weekday: int) -> int: count = 0 for i in range(weekday, len(weekly)): diff --git a/packages/helpermodules/timecheck_test.py b/packages/helpermodules/timecheck_test.py index 143d010aac..3aa646e21d 100644 --- a/packages/helpermodules/timecheck_test.py +++ b/packages/helpermodules/timecheck_test.py @@ -49,6 +49,40 @@ def test_check_end_time(time: str, assert remaining_time == expected_remaining_time +@pytest.mark.parametrize( + "time, selected, date,expected_remaining_time", + [ + pytest.param("9:00", "once", "2022-05-16", 1148, id="once-future-not-due"), + pytest.param("12:00", "daily", [], 11948, id="daily-future-not-due"), + pytest.param("8:05", "daily", [], -2152, id="daily-past-due"), + pytest.param("9:00", "weekly", [True, False, False, False, False, False, False], 1148, + id="weekly-selected-today-future"), + pytest.param("8:05", "weekly", [True, False, False, False, False, False, False], -2152, + id="weekly-selected-today-past-due"), + pytest.param("9:00", "weekly", [False, False, False, False, False, False, True], -85252, + id="weekly-not-selected-today-use-previous-selected"), + ], +) +def test_check_end_time_current_occurrence( + time: str, + selected: str, + date: Union[List[bool], str, List], + expected_remaining_time: float, +): + # setup + plan = Mock(spec=ScheduledChargingPlan, time=time, frequency=Mock(spec=FrequencyDate, selected=selected)) + if selected == "weekly": + setattr(plan.frequency, "weekly", date) + elif selected == "once": + setattr(plan.frequency, "once", date) + + # execution + remaining_time = timecheck.check_end_time_current_occurrence(plan) + + # evaluation + assert remaining_time == expected_remaining_time + + @pytest.mark.parametrize("weekday, weekly, expected_days", [pytest.param(0, [True, True, False, False, False, False, False], 0), pytest.param(1, [True, False, False, False, False, False, False], 6), diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index c4256495fa..df9bd4824a 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -4078,12 +4078,14 @@ def upgrade(topic: str, payload) -> Optional[dict]: usage = decode_payload(payload) reset_chargemode = usage["reset_chargemode"] if reset_chargemode.get("mode") is not None: + legacy_target_chargemode = reset_chargemode.get("chargemode", Chargemode.INSTANT_CHARGING) # altes Schema if reset_chargemode["mode"] == "never": - reset_chargemode = ResetChargemode(active=False) + reset_chargemode = ResetChargemode(active=False, chargemode=legacy_target_chargemode) elif reset_chargemode["mode"] == "midnight": reset_chargemode = ResetChargemode(active=True, time="00:00", - frequency=FrequencyDate(selected="daily")) + frequency=FrequencyDate(selected="daily"), + chargemode=legacy_target_chargemode) elif reset_chargemode["mode"] == "time": timestamp = reset_chargemode["time"] if timestamp is not None: @@ -4093,7 +4095,8 @@ def upgrade(topic: str, payload) -> Optional[dict]: date = datetime.datetime.today().strftime("%Y-%m-%d") time = "07:00" reset_chargemode = ResetChargemode( - active=True, time=time, frequency=FrequencyDate(selected="once", once=date)) + active=True, time=time, frequency=FrequencyDate(selected="once", once=date), + chargemode=legacy_target_chargemode) usage["reset_chargemode"] = reset_chargemode return {topic: asdict(usage)} From 7c9b9145a409da259147f6ec19a6f23f9d3849f1 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 28 Sep 2026 12:01:58 +0200 Subject: [PATCH 4/7] pytest --- packages/helpermodules/update_config_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index edd02d81ce..cff4b7039b 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -1,3 +1,4 @@ +import datetime from typing import List, Optional from unittest.mock import Mock, patch, mock_open from control.chargemode import Chargemode From aad293302742c7dbe242b9dfeb632732d43107bc Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 30 Sep 2026 15:00:36 +0200 Subject: [PATCH 5/7] Update koala theme --- .../src/components/ConsumerSettings.vue | 152 +++++++++++------- .../source/src/stores/mqtt-store-model.ts | 2 +- .../koala/source/src/stores/mqtt-store.ts | 106 ++++++++++-- 3 files changed, 193 insertions(+), 67 deletions(-) diff --git a/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue b/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue index b8d9773054..d6c2e69195 100644 --- a/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue +++ b/packages/modules/web_themes/koala/source/src/components/ConsumerSettings.vue @@ -21,35 +21,71 @@
Betriebsmodus umstellen
+ -
- +