diff --git a/packages/control/consumer/consumer.py b/packages/control/consumer/consumer.py index 540ceb30d8..03499cc25f 100644 --- a/packages/control/consumer/consumer.py +++ b/packages/control/consumer/consumer.py @@ -1,27 +1,21 @@ -from enum import Enum import logging from typing import Callable, Optional, Tuple from control import data from control.algorithm.utils import get_medium_charging_current from control.chargemode import Chargemode from control.chargepoint.chargepoint_state import CHARGING_STATES -from control.consumer.consumer_data import ConsumerData, ConsumerUsage, MeterOnlyConfig +from control.consumer.consumer_data import ConsumerData, ConsumerUsage, MeterOnlyConfig, ResetModes, WaitForStartStates from control.load_protocol import Load from helpermodules import timecheck -from helpermodules.phase_handling import convert_single_evu_phase_to_cp_phase +from helpermodules.phase_handling import convert_single_evu_phase_to_cp_phase, voltages_mean from modules.common.configurable_consumer import ConfigurableConsumer log = logging.getLogger(__name__) -class WaitForStartStates(Enum): - START_TEST_RUN = "start_test_run" - TEST_RUNNIG = "test_running" - START_SIGNAL_RECEIVED = "start_singal_received" - WAIT_FOR_NEXT_TEST_RUN = "wait_for_next_test_run" - - class Consumer(Load): + PAUSE_BETWEEN_WAIT_FOR_START_TEST_RUNS = 3600 + def __init__(self, index: int): self.num = index self.data: ConsumerData = ConsumerData() @@ -37,7 +31,11 @@ def set_state_and_log(self, message: str) -> None: elif message not in self.data.get.state_str: self.data.get.state_str += f" {message}" + def setup_values_at_start(self): + self.data.get.state_str = None + def update(self): + self.setup_values_at_start() if isinstance(self.data.usage, MeterOnlyConfig): return else: @@ -53,9 +51,9 @@ def update(self): self.data.set.phases_to_use = self.data.config.connected_phases self.data.get.charge_state = True if self.data.get.power > 0 else False min_current, required_current, message, mode, submode = self.get_parameter() + self.set_mode_changed(submode, mode) self.set_control_parameter(min_current, required_current, self.data.config.connected_phases, submode, mode) self.set_state_and_log(message) - self.set_mode_changed(submode, mode) log.debug( f"Verbraucher {self.num}: Sollstrom {required_current}, min. Ist-Strom {max(self.data.get.currents)}," f" Modus {mode}, Submodus {submode}, {message}") @@ -67,12 +65,9 @@ def update(self): BELOW_PRICE_LIMIT = "Preislimit für Verbraucher aktiv, aktueller Preis in Ordnung." SURPLUS_CONTINOUS_STILL_RUNNING = ("Verbraucher läuft ggf auch ohne ausreichend Überschuss weiter, da der " "Verbraucher nicht abgeschaltet werden darf.") - WAIT_FOR_START_SIGNAL = "Warte auf Startsignal für kontinuierlichen Verbraucher. Nächster Testlauf in {}" - WAIT_FOR_START_SIGNAL_TEST_RUN = "Verbraucher eingeschaltet, um zu testen, ob ein Startsignal empfangen wird." - WAIT_FOR_START_SIGNAL_RECEIVED = "Startsignal für kontinuierlichen Verbraucher empfangen." def get_parameter(self) -> Tuple[int, int, Optional[str], Chargemode, Chargemode]: - if timecheck.create_timestamp() < self.data.set.timestamp_last_current_set + self.data.usage.min_intervall: + if timecheck.create_timestamp() < self.data.set.timestamp_last_current_set + self.data.config.min_intervall: log.debug("Intervall für neuen Schaltbefehl nicht abgelaufen.") return (self.data.control_parameter.min_current, self.data.control_parameter.required_current, @@ -80,7 +75,7 @@ def get_parameter(self) -> Tuple[int, int, Optional[str], Chargemode, Chargemode self.data.control_parameter.chargemode, self.data.control_parameter.submode) - min_current = self.data.usage.min_current + min_current = self.data.config.min_current if self.data.usage.chargemode == Chargemode.SCHEDULED_CHARGING: required_current, message, mode, submode = self.wait_for_start_handler(self.scheduled_charging) elif self.data.usage.chargemode == Chargemode.TIME_CHARGING: @@ -103,16 +98,183 @@ def get_parameter(self) -> Tuple[int, int, Optional[str], Chargemode, Chargemode def _parse_required_current_by_usage(self, required_current: float) -> float: if self.data.usage.type in [ConsumerUsage.CONTINUOUS, ConsumerUsage.SUSPENDABLE_ONOFF]: return get_medium_charging_current( - self.data.get.currents) if self.data.get.charge_state else self.data.usage.min_current + self.data.get.currents) if self.data.get.charge_state else self.data.config.min_current else: return required_current - def scheduled_charging(self) -> Tuple[int, str, Optional[str], int]: - required_current = 0 - message = None - mode = None - submode = None - return required_current, message, mode, submode + def _convert_power_to_current(self, power: float) -> float: + phases = self.data.config.connected_phases or 1 + voltage = voltages_mean(self.data.get.voltages) or 230.0 + return power / phases / voltage + + BUFFER_AFTER_END_TIME = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen + BUFFER_START_EARLIER = 600 # 10 Min vor dem geplanten Start kann begonnen werden + + def scheduled_charging(self) -> Tuple[Chargemode, str]: + return self.data.config.min_current, "Zielladen noch nicht implementiert", Chargemode.STOP, Chargemode.STOP + plan_data, remaining_time, duration = self._find_recent_plan(self.data.usage.scheduled_charging.plans) + if plan_data: + self.data.config.control_parameter.current_plan = plan_data.id + else: + self.data.config.control_parameter.current_plan = None + return self.scheduled_charging_calc_current( + plan_data, + remaining_time, + duration) + + # def _find_recent_plan(self, + # plans: List[Union[ContinuousScheduledPlanConsumer, SuspendableScheduledPlanConsumer]]): + # plans_diff_end_date = [] + # for p in plans: + # try: + # plans_diff_end_date.append( + # {p.id: timecheck.check_end_time(p, self.BUFFER_AFTER_END_TIME)}) + # log.debug(f"Verbleibende Zeit bis zum Zieltermin [s]: {plans_diff_end_date}") + # except Exception: + # log.exception("Fehler im ev-Modul "+str(self.data.id)) + # if plans_diff_end_date: + # # ermittle den Key vom kleinsten value in plans_diff_end_date + # filtered_plans = [d for d in plans_diff_end_date if list(d.values())[0] is not None] + # if filtered_plans: + # sorted_plans = sorted(filtered_plans, key=lambda x: list(x.values())[0]) + # for plan in sorted_plans: + # if self.BUFFER_AFTER_END_TIME < list(plan.values())[0]: + # plan_dict = plan + # break + # else: + # return None + # plan_id = list(plan_dict.keys())[0] + # diff_end_date = list(plan_dict.values())[0] + + # for p in plans: + # if p.id == plan_id: + # plan = p + + # remaining_time, duration = self._calc_remaining_time(plan, diff_end_date) + + # return plan, remaining_time, duration + # else: + # return None + + # def _calc_remaining_time(self, + # plan: Union[ContinuousScheduledPlanConsumer, SuspendableScheduledPlanConsumer], + # diff_end_date: float) -> Tuple[float, float]: + # duration = plan.duration - self.data.set.log.on_time + # remaining_time = diff_end_date - duration + + # log.debug(f"Verbleibende Zeit bis zum Ladestart [s]:{remaining_time}, Dauer [h]: {duration/3600}") + # return remaining_time, duration + + # SCHEDULED_REACHED_MAX_ON_TIME = ("Zielladen abgeschlossen, da die geplante Ladedauer bereits überschritten " + # "wurde.") + # SCHEDULED_CHARGING_REACHED_MAX_AND_LIMIT_SOC = ( + # "Zielladen abgeschlossen, da das Limit für Fahrzeug Laden mit Überschuss (SoC-Limit)" + # " sowie der Fahrzeug-SoC (Ziel-SoC) bereits erreicht wurde. ") + # SCHEDULED_CHARGING_REACHED_AMOUNT = "Zielladen abgeschlossen, da die Energiemenge bereits erreicht wurde. " + # SCHEDULED_CHARGING_REACHED_SCHEDULED_SOC = ("Falls vorhanden wird mit EVU-Überschuss geladen, da der Ziel-Soc " + # "für Zielladen bereits erreicht wurde. ") + # SCHEDULED_CHARGING_BIDI = ("Der Ziel-Soc für Zielladen wurde bereits erreicht. Das Auto wird " + # "bidirektional ge-/entladen, sodass möglichst weder Bezug noch " + # "Einspeisung erfolgt. ") + # SCHEDULED_CHARGING_NO_PLANS_CONFIGURED = "Kein Zielladen, da keine Ziel-Termine konfiguriert sind." + # SCHEDULED_CHARGING_NO_DATE_PENDING = "Kein Zielladen, da kein Ziel-Termin ansteht. " + # SCHEDULED_CHARGING_USE_PV = "Zielladen startet {}. Falls vorhanden, wird mit Überschuss geladen. " + # SCHEDULED_CHARGING_MAX_CURRENT = "Zielladen mit {}A. Der Ladestrom wurde erhöht, um das Ziel zu erreichen. " + # SCHEDULED_CHARGING_LIMITED_BY_SOC = 'einen SoC von {}%' + # SCHEDULED_CHARGING_LIMITED_BY_AMOUNT = '{}kWh geladene Energie' + # SCHEDULED_CHARGING_IN_TIME = ('Zielladen mit mindestens {}A, um {} um {} zu erreichen. Falls vorhanden wird ' + # 'zusätzlich EVU-Überschuss geladen. ') + # SCHEDULED_CHARGING_CHEAP_HOUR = "Zielladen, da ein günstiger Zeitpunkt zum preisbasierten Laden ist. {}" + # SCHEDULED_CHARGING_EXPENSIVE_HOUR = ( + # "Zielladen ausstehend, da jetzt kein günstiger Zeitpunkt zum preisbasierten " + # "Laden ist. {} Falls vorhanden, wird mit Überschuss geladen. ") + # SCHEDULED_CHARGING_EXPENSIVE_HOUR_REACHED_MAX_SOC = ( + # "Zielladen ausstehend, da jetzt kein günstiger Zeitpunkt zum preisbasierten " + # "Laden ist. {} " + + # "Kein Zielladen mit Überschuss, da das SoC-Limit für Überschuss-Laden erreicht wurde.") + + # def scheduled_charging_calc_current(self, + # plan: Optional[Union[ContinuousScheduledPlanConsumer, + # SuspendableScheduledPlanConsumer]], + # remaining_time: float, + # duration: float) -> Tuple[str, str]: + # submode = "stop" + # if plan is None: + # if len(self.data.usage.scheduled_charging.plans) == 0: + # return submode, self.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED + # else: + # return submode, self.SCHEDULED_CHARGING_NO_DATE_PENDING + # if self.data.set.log.on_time >= duration: + # message = self.SCHEDULED_REACHED_MAX_ON_TIME + # elif (0 < remaining_time < self.BUFFER_START_EARLIER): + # submode = "instant_charging" + # # weniger als die berechnete Zeit verfügbar + # elif remaining_time <= 0: + # submode = "instant_charging" + # else: + # # Wenn dynamische Tarife aktiv sind, prüfen, ob jetzt ein günstiger Zeitpunkt zum Laden + # # ist. + # if plan.et_active: + # def get_hours_message() -> str: + # def end_of_today_timestamp() -> int: + # return datetime.datetime.now().replace( + # hour=23, minute=59, second=59, microsecond=999000).timestamp() + + # def is_loading_hour(hour: int) -> bool: + # return data.data.optional_data.ep_is_charging_allowed_hours_list(hour) + + # def convert_loading_hours_to_string(hour_list: List[int]) -> str: + # if 1 < len(hour_list): + # times_string = ", ".join(hour.strftime('%-H:%M') for hour in hour_list[:-1]) + # return times_string + " und " + hour_list[-1].strftime('%-H:%M') + # else: + # return ", ".join(hour.strftime('%-H:%M') for hour in hour_list) + # midnight = end_of_today_timestamp() + # loading_times_today = [datetime.datetime.fromtimestamp(hour) + # for hour in sorted(hour_list) if hour <= midnight] + # loading_times_today = (loading_times_today[1:] + # if is_loading_hour(hour_list) else loading_times_today) + # loading_times_tomorrow = [datetime.datetime.fromtimestamp(hour) + # for hour in sorted(hour_list) if hour > midnight] + + # parts = [] + + # if is_loading_hour(hour_list): + # parts.append("jetzt") + + # if 0 < len(loading_times_today): + # if parts: + # parts.append(" und ") + # parts.append(f"heute {convert_loading_hours_to_string(loading_times_today)}") + + # if 0 < len(loading_times_tomorrow): + # if parts: + # parts.append(" sowie ") + # parts.append(f"morgen {convert_loading_hours_to_string(loading_times_tomorrow)}") + + # loading_message = "Geladen wird " + "".join(parts) + # return loading_message + '.' + + # hour_list = data.data.optional_data.ep_get_loading_hours(duration, duration + remaining_time) + + # log.debug(f"Günstige Ladezeiten: {hour_list}") + # if data.data.optional_data.ep_is_charging_allowed_hours_list(hour_list): + # message = self.SCHEDULED_CHARGING_CHEAP_HOUR.format(get_hours_message()) + # submode = "instant_charging" + # else: + # message = self.SCHEDULED_CHARGING_EXPENSIVE_HOUR.format(get_hours_message()) + # submode = "pv_charging" + # else: + # now = datetime.datetime.today() + # start_time = now + datetime.timedelta(seconds=remaining_time) + # if start_time.year == now.year and start_time.month == now.month and start_time.day == now.day: + # message = self.SCHEDULED_CHARGING_USE_PV.format( + # f"um {start_time.strftime('%-H:%M')} Uhr") + # else: + # message = self.SCHEDULED_CHARGING_USE_PV.format( + # f"am {start_time.strftime('%d.%m')} um {start_time.strftime('%-H:%M')} Uhr") + # submode = "pv_charging" + # return submode, message TIME_CHARGING_MIN_BAT_SOC_REACHED = ("Betrieb mit Zeitladen nach Speicher-SoC nicht möglich, da der SoC des" " Speichers unter dem minimalen SoC liegt.") @@ -135,12 +297,14 @@ def time_charging(self) -> Tuple[int, str, Optional[str], int]: else: log.debug( "Zeitladen: minimaler Speicher-SoC überschritten, Laden mit Zeitladen möglich.") - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) submode = Chargemode.TIME_CHARGING else: message = self.TIME_CHARGING_CONFLICT_ACTIVE_BAT_CONTROL else: - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) submode = Chargemode.TIME_CHARGING else: message = self.TIME_CHARGING_NO_PLAN_ACTIVE @@ -149,7 +313,8 @@ def time_charging(self) -> Tuple[int, str, Optional[str], int]: return required_current, message, None, submode def instant_charging(self) -> Tuple[int, str, Optional[str], int]: - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) message = None mode = Chargemode.INSTANT_CHARGING submode = Chargemode.INSTANT_CHARGING @@ -162,70 +327,94 @@ def eco_charging(self) -> Tuple[int, str, Optional[str], int]: mode = Chargemode.ECO_CHARGING if data.data.optional_data.data.electricity_pricing.configured: if data.data.optional_data.ep_is_charging_allowed_price_threshold(self.data.usage.eco_charging.price_limit): - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) message = self.CHARGING_PRICE_LOW submode = Chargemode.INSTANT_CHARGING else: - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage(self.data.config.min_current) message = self.CHARGING_PRICE_EXCEEDED if self.data.control_parameter.state in CHARGING_STATES: message += "Lädt mit Überschuss. " submode = Chargemode.PV_CHARGING else: - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage(self.data.config.min_current) submode = Chargemode.PV_CHARGING return required_current, message, mode, submode def pv_charging(self) -> Tuple[int, str, Optional[str], int]: - required_current = self._parse_required_current_by_usage(self.data.usage.min_current) + required_current = self._parse_required_current_by_usage(self.data.config.min_current) message = None mode = Chargemode.PV_CHARGING submode = Chargemode.PV_CHARGING return required_current, message, mode, submode + WAIT_FOR_DEVICE_START = "Warte auf Gerätestart, Gerät eingeschaltet lassen." + WAIT_FOR_STOPPED_DEVICE = "Gerätestart erkannt, warte auf gestopptes Gerät." + DEVICE_WAITING_FOR_START = "Gerätestart erkannt. Warte auf ausreichend Überschuss, um das Gerät zu starten." + def wait_for_start_handler( - self, func: Callable[[], Tuple[int, str, Optional[str], int]]) -> Tuple[int, str, Optional[str], int]: + self, func: Callable[[], Tuple[int, str, Optional[str], int]] + ) -> Tuple[float, str, Optional[Chargemode], Chargemode]: if self.data.usage.wait_for_start_active: - timestamp_next_test = self.data.set.wait_for_start_last_test_timestamp + 60*60 - wait_for_start_state = self._wait_for_start_singal(timestamp_next_test) - if wait_for_start_state == WaitForStartStates.START_SIGNAL_RECEIVED: + if self.data.set.wait_for_start_state == WaitForStartStates.WAIT_FOR_DEVICE_START: + if self.data.get.charge_state: + self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_STOPPED_DEVICE + required_current = 0 + message = self.WAIT_FOR_STOPPED_DEVICE + mode = Chargemode.STOP + submode = Chargemode.STOP + else: + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) + message = self.WAIT_FOR_DEVICE_START + mode = Chargemode.INSTANT_CHARGING + submode = Chargemode.INSTANT_CHARGING + elif self.data.set.wait_for_start_state == WaitForStartStates.WAIT_FOR_STOPPED_DEVICE: + if self.data.get.charge_state is False: + self.data.set.wait_for_start_state = WaitForStartStates.DEVICE_WAITING_FOR_START + required_current, message, mode, submode = func() + message = self.DEVICE_WAITING_FOR_START + " " + (message if message else "") + else: + self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_STOPPED_DEVICE + required_current = 0 + message = self.WAIT_FOR_STOPPED_DEVICE + mode = Chargemode.STOP + submode = Chargemode.STOP + elif self.data.set.wait_for_start_state == WaitForStartStates.DEVICE_WAITING_FOR_START: + if self.data.get.charge_state: + self.data.set.wait_for_start_state = WaitForStartStates.START_SIGNAL_RECEIVED + required_current, message, mode, submode = func() + else: + required_current, message, mode, submode = func() + message = self.DEVICE_WAITING_FOR_START + " " + (message if message else "") + elif self.data.set.wait_for_start_state == WaitForStartStates.START_SIGNAL_RECEIVED: required_current, message, mode, submode = func() - elif (wait_for_start_state == WaitForStartStates.START_TEST_RUN or - wait_for_start_state == WaitForStartStates.TEST_RUNNIG): - required_current = self.data.usage.min_current - message = self.WAIT_FOR_START_SIGNAL_TEST_RUN - mode = Chargemode.INSTANT_CHARGING - submode = Chargemode.INSTANT_CHARGING - elif wait_for_start_state == WaitForStartStates.WAIT_FOR_NEXT_TEST_RUN: - required_current = 0 - message = self.WAIT_FOR_START_SIGNAL.format( - timecheck.convert_timestamp_delta_to_time_string(timestamp_next_test, 0)) - mode = Chargemode.STOP - submode = Chargemode.STOP + else: + raise ValueError( + f"Ungültiger wait_for_start_state {self.data.set.wait_for_start_state} für Verbraucher {self.num}") else: required_current, message, mode, submode = func() return required_current, message, mode, submode - def _wait_for_start_singal(self, timestamp_next_test: float) -> WaitForStartStates: - if self.data.set.wait_for_start_signal_received: - return WaitForStartStates.START_SIGNAL_RECEIVED - else: - if self.data.set.wait_for_start_test_running: - if self.data.get.charge_state: - self.data.set.wait_for_start_signal_received = True - self.data.set.wait_for_start_test_running = False - return WaitForStartStates.START_SIGNAL_RECEIVED - elif timecheck.create_timestamp() < self.data.set.wait_for_start_last_test_timestamp + 60: - return WaitForStartStates.START_TEST_RUN - else: - self.data.set.wait_for_start_test_running = False - log.debug("Testlauf für kontinuierlichen Verbraucher beendet, kein Startsignal empfangen.") - return WaitForStartStates.WAIT_FOR_NEXT_TEST_RUN - elif timecheck.create_timestamp() > timestamp_next_test: - self.data.set.wait_for_start_last_test_timestamp = timecheck.create_timestamp() - return WaitForStartStates.TEST_RUNNIG - else: - return WaitForStartStates.WAIT_FOR_NEXT_TEST_RUN + def reset_wait_for_start(self): + self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_DEVICE_START + + 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 timecheck.create_timestamp() > self.data.usage.reset_chargemode.time: + 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 def set_control_parameter(self, min_current, required_current, phases, submode, mode): self.data.control_parameter.min_current = min_current @@ -251,4 +440,12 @@ def is_charging_stop_allowed(self) -> bool: def set_mode_changed(self, submode: Chargemode, mode: Chargemode) -> None: self.submode_changed = (submode != self.data.control_parameter.submode) - self.chargemode_changed = (mode != self.data.control_parameter.chargemode) + if ((submode == Chargemode.TIME_CHARGING and + self.data.control_parameter.chargemode != Chargemode.TIME_CHARGING) or + (submode != Chargemode.TIME_CHARGING and + self.data.control_parameter.chargemode != mode)): + self.chargemode_changed = True + log.debug("Änderung des Lademodus") + self.data.control_parameter.timestamp_chargemode_changed = timecheck.create_timestamp() + else: + self.chargemode_changed = False diff --git a/packages/control/consumer/consumer_data.py b/packages/control/consumer/consumer_data.py index aa389e8a32..fb765d43a8 100644 --- a/packages/control/consumer/consumer_data.py +++ b/packages/control/consumer/consumer_data.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from enum import Enum from typing import Callable, Dict, List, Optional, Union from control.chargemode import Chargemode from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory @@ -31,6 +32,31 @@ class TimeCharging: plans: List[TimeChargingPlanConsumer] = field(default_factory=empty_list_factory) +class WaitForStartStates(Enum): + WAIT_FOR_DEVICE_START = "wait_for_device_start" + WAIT_FOR_STOPPED_DEVICE = "wait_for_stopped_device" + DEVICE_WAITING_FOR_START = "device_waiting_for_start" + 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 + chargemode: Chargemode = Chargemode.INSTANT_CHARGING + + +@dataclass +class Log: + on_time: float = 0 + + @dataclass class MeterOnlyConfig: type: ConsumerUsage = ConsumerUsage.METER_ONLY @@ -39,37 +65,34 @@ class MeterOnlyConfig: @dataclass class SuspendableTunableDeviceConfig: chargemode: Chargemode = Chargemode.INSTANT_CHARGING - min_current: float = 0.5 - min_intervall: int = 60 eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging()) scheduled_charging: ScheduledSuspendableCharging = field(default_factory=lambda: ScheduledSuspendableCharging()) time_charging: TimeCharging = field(default_factory=lambda: TimeCharging()) type: ConsumerUsage = ConsumerUsage.SUSPENDABLE_TUNABLE + reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode()) wait_for_start_active: bool = True @dataclass class SuspendableOnOffDeviceConfig: chargemode: Chargemode = Chargemode.INSTANT_CHARGING - min_current: float = 0.5 - min_intervall: int = 60 eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging()) scheduled_charging: ScheduledSuspendableCharging = field(default_factory=lambda: ScheduledSuspendableCharging()) time_charging: TimeCharging = field(default_factory=lambda: TimeCharging()) type: ConsumerUsage = ConsumerUsage.SUSPENDABLE_ONOFF + reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode()) wait_for_start_active: bool = True @dataclass class ContinuousDeviceConfig: chargemode: Chargemode = Chargemode.INSTANT_CHARGING - min_current: float = 0.5 - min_intervall: int = 2*60*60 eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging()) scheduled_charging: ScheduledContinuousCharging = field(default_factory=lambda: ScheduledContinuousCharging()) time_charging: TimeCharging = field(default_factory=lambda: TimeCharging()) type: ConsumerUsage = ConsumerUsage.CONTINUOUS wait_for_start_active: bool = True + reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode()) GET_DEFAULTS_BY_USAGE: Dict[ConsumerUsage, Union[MeterOnlyConfig, @@ -89,7 +112,7 @@ class ContinuousDeviceConfig: ConsumerUsage.CONTINUOUS: ContinuousDeviceConfig, } -get_plan_class_for_usage: Dict[ConsumerUsage, Callable] = { +GET_PLAN_CLASS_FOR_USAGE: Dict[ConsumerUsage, Callable] = { ConsumerUsage.SUSPENDABLE_TUNABLE: SuspendableScheduledPlanConsumer, ConsumerUsage.SUSPENDABLE_ONOFF: SuspendableScheduledPlanConsumer, ConsumerUsage.CONTINUOUS: ContinuousScheduledPlanConsumer, @@ -101,6 +124,8 @@ class ConsumerConfig: connected_phases: int = 1 phase_1: int = 1 max_power: float = 5000 + min_current: float = 0.5 + min_intervall: int = 60 @dataclass @@ -124,21 +149,19 @@ class Get: @dataclass class Set: - current: float = 0 + current: float = field(default=0, metadata={"topic": "set/current"}) loadmanagement_available: bool = False - phases_to_use: int = 0 - plug_time: Optional[float] = 0 + phases_to_use: int = field(default=1, metadata={"topic": "set/phases_to_use"}) + plug_time: Optional[float] = field(default=None, metadata={"topic": "set/plug_time"}) required_power: float = 0 current_prev: float = 0 target_current: float = 0 charge_state_prev: bool = False + log: Log = field(default_factory=lambda: Log()) power: Optional[float] = None timestamp_last_current_set: float = field(default=0, metadata={"topic": "set/timestamp_last_current_set"}) - wait_for_start_signal_received: bool = field( - default=False, metadata={"topic": "set/wait_for_start_signal_received"}) - wait_for_start_test_running: bool = field(default=False, metadata={"topic": "set/wait_for_start_test_running"}) - wait_for_start_last_test_timestamp: float = field( - default=0, metadata={"topic": "set/wait_for_start_last_test_timestamp"}) + wait_for_start_state: WaitForStartStates = field( + default=WaitForStartStates.WAIT_FOR_DEVICE_START, metadata={"topic": "set/wait_for_start_state"}) @dataclass diff --git a/packages/control/consumer/consumer_test.py b/packages/control/consumer/consumer_test.py new file mode 100644 index 0000000000..6e6228f585 --- /dev/null +++ b/packages/control/consumer/consumer_test.py @@ -0,0 +1,121 @@ +from typing import Optional, Tuple +from unittest.mock import Mock + +import pytest + +from control import data +from control.chargemode import Chargemode +from control.consumer.consumer import Consumer +from control.consumer.consumer_data import ContinuousDeviceConfig, WaitForStartStates + + +@pytest.fixture(autouse=True) +def mock_data() -> None: + data.data_init(Mock()) + + +@pytest.fixture +def consumer() -> Consumer: + load = Consumer(1) + load.data.usage = ContinuousDeviceConfig() + load.data.config.min_current = 6 + load.data.config.max_power = 2300 + load.data.config.connected_phases = 1 + load.data.get.voltages = [230] + return load + + +@pytest.mark.parametrize( + ("wait_for_start_active", + "state", "charge_state", + "func_result", + "expected_result", + "expected_state", + "func_calls"), + [ + pytest.param( + False, + WaitForStartStates.WAIT_FOR_DEVICE_START, + False, + (11, "ok", Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING), + (11, "ok", Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING), + WaitForStartStates.WAIT_FOR_DEVICE_START, + 1, + id="wait-for-start-disabled-passthrough", + ), + pytest.param( + True, + WaitForStartStates.WAIT_FOR_DEVICE_START, + True, + (9, "running", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), + (0, Consumer.WAIT_FOR_STOPPED_DEVICE, Chargemode.STOP, Chargemode.STOP), + WaitForStartStates.WAIT_FOR_STOPPED_DEVICE, + 0, + id="wait-for-device-start-charge-detected", + ), + pytest.param( + True, + WaitForStartStates.WAIT_FOR_DEVICE_START, + False, + (9, "running", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), + (6, Consumer.WAIT_FOR_DEVICE_START, Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING), + WaitForStartStates.WAIT_FOR_DEVICE_START, + 0, + id="wait-for-device-start-no-charge", + ), + pytest.param( + True, + WaitForStartStates.WAIT_FOR_STOPPED_DEVICE, + False, + (8, "pv ok", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), + (8, f"{Consumer.DEVICE_WAITING_FOR_START} pv ok", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), + WaitForStartStates.DEVICE_WAITING_FOR_START, + 1, + id="wait-for-stopped-device-started-waiting", + ), + pytest.param( + True, + WaitForStartStates.DEVICE_WAITING_FOR_START, + True, + (7, "started", Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING), + (7, "started", Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING), + WaitForStartStates.START_SIGNAL_RECEIVED, + 1, + id="device-waiting-start-signal-received", + ), + pytest.param( + True, + WaitForStartStates.START_SIGNAL_RECEIVED, + True, + (10, "continue", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), + (10, "continue", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), + WaitForStartStates.START_SIGNAL_RECEIVED, + 1, + id="start-signal-received-passthrough", + ), + ], +) +def test_wait_for_start_handler( + consumer: Consumer, + wait_for_start_active: bool, + state: WaitForStartStates, + charge_state: bool, + func_result: Tuple[int, str, Chargemode, Optional[Chargemode]], + expected_result: Tuple[int, str, Chargemode, Optional[Chargemode]], + expected_state: WaitForStartStates, + func_calls: int, +): + # setup + assert isinstance(consumer.data.usage, ContinuousDeviceConfig) + consumer.data.usage.wait_for_start_active = wait_for_start_active + consumer.data.set.wait_for_start_state = state + consumer.data.get.charge_state = charge_state + charging_func = Mock(return_value=func_result) + + # execution + result = consumer.wait_for_start_handler(charging_func) + + # evaluation + assert result == expected_result + assert consumer.data.set.wait_for_start_state == expected_state + assert charging_func.call_count == func_calls diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 8e2c2b7dd1..6e89a39879 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -112,17 +112,17 @@ class AutolockPlan(TimeframePlan): class TimeChargingPlanConsumer(TimeframePlan): id: Optional[int] = None min_bat_soc: Optional[int] = None - name: str = "neuer Verbraucher-Plan" + name: str = "neuer Zeit-Plan" @dataclass class SuspendableScheduledPlanConsumer(TimeframePlan): id: Optional[int] = None - name: str = "neuer Verbraucher-Plan" + name: str = "neuer Ziel-Plan" duration: int = 60 @dataclass class ContinuousScheduledPlanConsumer(TimeframePlan): id: Optional[int] = None - name: str = "neuer Verbraucher-Plan" + name: str = "neuer Ziel-Plan" diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index ce91698197..0095c6d60f 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -17,14 +17,14 @@ from control.chargepoint import chargepoint from control.chargepoint.chargepoint_template import get_chargepoint_template_default -from control.consumer.consumer_data import GET_DEFAULTS_BY_USAGE, get_plan_class_for_usage +from control.consumer.consumer_data import GET_DEFAULTS_BY_USAGE, GET_PLAN_CLASS_FOR_USAGE from control.consumer.usage import ConsumerUsage from control.counter_all import counter_all from control.ev.charge_template import ChargeTemplate, get_new_charge_template from control.ev.ev_template import EvTemplateData from control.consumer.consumer_data import ConsumerConfig from helpermodules import pub -from helpermodules.abstract_plans import AutolockPlan, ScheduledChargingPlan, TimeChargingPlan +from helpermodules.abstract_plans import AutolockPlan, ScheduledChargingPlan, TimeChargingPlan, TimeChargingPlanConsumer from helpermodules.utils.run_command import run_command # ToDo: move to module commands if implemented from modules.backup_clouds.onedrive.api import generateMSALAuthCode, retrieveMSALTokens @@ -64,7 +64,8 @@ class Command: [("autolock_plan", "openWB/chargepoint/template/[0-9]+$", -1), ("charge_template_scheduled_plan", "openWB/vehicle/template/charge_template/[0-9]+$", -1), ("charge_template_time_charging_plan", "openWB/vehicle/template/charge_template/[0-9]+$", -1), - ("usage_plan", "openWB/consumer/[0-9]+/usage$", -1)], + ("consumer_scheduled_plan", "openWB/consumer/[0-9]+/usage$", -1), + ("consumer_time_plan", "openWB/consumer/[0-9]+/usage$", -1)], "topic": [("mqtt_bridge", "openWB/system/mqtt/bridge", -1), ("vehicle", "openWB/vehicle/[0-9]+/name$", 0)], @@ -92,7 +93,8 @@ def _get_max_ids(self) -> None: "charge_template_scheduled_plan": lambda p: p.get("chargemode", {}).get("scheduled_charging", {}).get("plans", []), "charge_template_time_charging_plan": lambda p: p.get("time_charging", {}).get("plans", []), - "usage_plan": lambda p: p.get("plans", []) + "consumer_scheduled_plan": lambda p: p.get("scheduled_charging", {}).get("plans", []), + "consumer_time_plan": lambda p: p.get("time_charging", {}).get("plans", []), } try: received_topics = ProcessBrokerBranch("").get_max_id() @@ -1207,41 +1209,94 @@ def selectUsage(self, connection_id: str, payload: dict) -> None: f'\'{payload["data"]["usage"]}\' zugewiesen.', MessageType.SUCCESS) - def addUsagePlan(self, connection_id: str, payload: dict) -> None: + def addConsumerSchedulePlan(self, connection_id: str, payload: dict) -> None: """ sendet das Topic, zu dem ein neuer Zielladen-Plan erstellt werden soll. """ usage = SubData.consumer_data[f'consumer{payload["data"]["consumer_id"]}'].data.usage - plan = get_plan_class_for_usage(usage.type)() - new_id = self.max_id_usage_plan + 1 - plan.id = new_id - usage.plans.append(plan) + # check if "payload" contains "data.copy" + if "data" in payload and "copy" in payload["data"] and usage.scheduled_charging.plans is not None: + for plan in usage.scheduled_charging.plans: + if plan.id == payload["data"]["copy"]: + new_consumer_schedule_plan = copy.deepcopy(plan) + break + new_consumer_schedule_plan.name = f'Kopie von {new_consumer_schedule_plan.name}' + else: + new_consumer_schedule_plan = GET_PLAN_CLASS_FOR_USAGE[usage.type]() + new_id = self.max_id_consumer_scheduled_plan + 1 + new_consumer_schedule_plan.id = new_id + usage.scheduled_charging.plans.append(new_consumer_schedule_plan) + self.max_id_consumer_scheduled_plan = new_id + Pub().pub("openWB/set/command/max_id/consumer_scheduled_plan", new_id) Pub().pub(f'openWB/set/consumer/{payload["data"]["consumer_id"]}/usage', dataclass_utils.asdict(usage)) - self.max_id_usage_plan = new_id - Pub().pub("openWB/set/command/max_id/usage_plan", new_id) pub_user_message( payload, connection_id, - f'Neuer Zielladen-Plan mit ID \'{new_id}\' zu Profil ' - f'\'{payload["data"]["template"]}\' hinzugefügt.', + f'Neuer Zielladen-Plan mit ID \'{new_id}\' zu Verbraucher ' + f'\'{payload["data"]["consumer_id"]}\' hinzugefügt.', MessageType.SUCCESS) - def removeUsagePlan(self, connection_id: str, payload: dict) -> None: + def removeConsumerSchedulePlan(self, connection_id: str, payload: dict) -> None: """ löscht einen Zielladen-Plan. """ - if self.max_id_usage_plan < payload["data"]["plan"]: + usage = SubData.consumer_data[f'consumer{payload["data"]["consumer_id"]}'].data.usage + if self.max_id_consumer_scheduled_plan < payload["data"]["plan"]: log.error( payload, connection_id, f'Die ID \'{payload["data"]["plan"]}\' ist größer als die maximal vergebene ' - f'ID \'{self.max_id_usage_plan}\'.', MessageType.ERROR) + f'ID \'{self.max_id_consumer_scheduled_plan}\'.', MessageType.ERROR) + return + for plan in usage.scheduled_charging.plans: + if plan.id == payload["data"]["plan"]: + usage.scheduled_charging.plans.remove(plan) + break + Pub().pub(f'openWB/set/consumer/{payload["data"]["consumer_id"]}/usage', dataclass_utils.asdict(usage)) + pub_user_message( + payload, connection_id, + (f'Zielladen-Plan mit ID \'{payload["data"]["plan"]}\' von Verbraucher ' + f'\'{payload["data"]["consumer_id"]}\' gelöscht.'), + MessageType.SUCCESS) + + def addConsumerTimePlan(self, connection_id: str, payload: dict) -> None: + """ sendet das Topic, zu dem ein neuer Zeitladen-Plan erstellt werden soll. + """ + usage = SubData.consumer_data[f'consumer{payload["data"]["consumer_id"]}'].data.usage + # check if "payload" contains "data.copy" + if "data" in payload and "copy" in payload["data"]: + for plan in usage.time_charging.plans: + if plan.id == payload["data"]["copy"]: + new_time_charging_plan = copy.deepcopy(plan) + break + new_time_charging_plan.name = f'Kopie von {new_time_charging_plan.name}' + else: + new_time_charging_plan = TimeChargingPlanConsumer() + new_id = self.max_id_consumer_time_plan + 1 + new_time_charging_plan.id = new_id + usage.time_charging.plans.append(new_time_charging_plan) + Pub().pub(f'openWB/set/consumer/{payload["data"]["consumer_id"]}/usage', dataclass_utils.asdict(usage)) + self.max_id_consumer_time_plan = new_id + Pub().pub( + "openWB/set/command/max_id/consumer_time_plan", new_id) + pub_user_message( + payload, connection_id, + f'Neuer Zeitladen-Plan mit ID \'{new_id}\' zu Verbraucher ' + f'\'{payload["data"]["consumer_id"]}\' hinzugefügt.', MessageType.SUCCESS) + + def removeConsumerTimePlan(self, connection_id: str, payload: dict) -> None: + """ löscht einen Zeitladen-Plan. + """ usage = SubData.consumer_data[f'consumer{payload["data"]["consumer_id"]}'].data.usage - for plan in usage.plans: + if self.max_id_consumer_time_plan < payload["data"]["plan"]: + log.error(payload, connection_id, "Die ID ist größer als die maximal vergebene ID.", + MessageType.ERROR) + return + for plan in usage.time_charging.plans: if plan.id == payload["data"]["plan"]: - usage.plans.remove(plan) + usage.time_charging.plans.remove(plan) break Pub().pub(f'openWB/set/consumer/{payload["data"]["consumer_id"]}/usage', dataclass_utils.asdict(usage)) pub_user_message( payload, connection_id, - f'Zielladen-Plan mit ID \'{payload["data"]["plan"]}\' von Profil ' - f'{payload["data"]["template"]}\' gelöscht.', + (f'Zeitladen-Plan mit ID \'{payload["data"]["plan"]}\' zu Verbraucher ' + f'\'{payload["data"]["consumer_id"]}\' gelöscht.'), MessageType.SUCCESS) diff --git a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py index b06822ab14..b1c74c086e 100644 --- a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py +++ b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py @@ -24,6 +24,8 @@ def _get_data_path() -> Path: 'openWB/command/max_id/charge_template', 'openWB/command/max_id/charge_template_scheduled_plan', 'openWB/command/max_id/charge_template_time_charging_plan', + 'openWB/command/max_id/consumer_scheduled_plan', + 'openWB/command/max_id/consumer_time_plan', 'openWB/command/max_id/chargepoint_template', 'openWB/command/max_id/device', 'openWB/command/max_id/ev_template', diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 7e8cf263d8..87630f128d 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -1249,6 +1249,8 @@ def process_consumer_topic(self, msg): elif (re.search("consumer/[0-9]+/get/power$", msg.topic) is not None or re.search("consumer/[0-9]+/set/power$", msg.topic) is not None or re.search("consumer/[0-9]+/set/current$", msg.topic) is not None or + re.search("consumer/[0-9]+/set/plug_time$", msg.topic) is not None or + re.search("consumer/[0-9]+/set/timestamp_last_current_set$", msg.topic) is not None or re.search("consumer/[0-9]+/get/set_power$", msg.topic) is not None): self._validate_value(msg, float) elif (re.search("consumer/[0-9]+/get/currents$", msg.topic) is not None or @@ -1263,10 +1265,13 @@ def process_consumer_topic(self, msg): elif re.search("consumer/[0-9]+/get/fault_state$", msg.topic) is not None: self._validate_value(msg, int, [(0, 2)]) elif (re.search("consumer/[0-9]+/get/fault_str$", msg.topic) is not None or - re.search("consumer/[0-9]+/get/state_str$", msg.topic) is not None): + re.search("consumer/[0-9]+/get/state_str$", msg.topic) is not None or + re.search("consumer/[0-9]+/set/wait_for_start_state$", msg.topic) is not None): self._validate_value(msg, str) elif re.search("consumer/[0-9]+/get/state$", msg.topic) is not None: self._validate_value(msg, bool) + elif re.search("consumer/[0-9]+/set/phases_to_use$", msg.topic) is not None: + self._validate_value(msg, int, [(0, 3)]) elif re.search("consumer/[0-9]+/control_parameter/", msg.topic) is not None: self.process_control_parameter_topics(msg) else: diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index a53f736d4f..251de5419e 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -1,5 +1,6 @@ """ Modul, um die Daten vom Broker zu erhalten. """ +from enum import Enum import importlib import logging from pathlib import Path @@ -253,6 +254,8 @@ def set_json_payload_class(self, class_obj: Dict, msg: mqtt.MQTTMessage): if isinstance(payload, Dict): for key, value in payload.items(): setattr(class_obj, key, value) + elif isinstance(getattr(class_obj, key, None), Enum): + setattr(class_obj, key, type(getattr(class_obj, key))(payload)) else: setattr(class_obj, key, decode_payload(msg.payload)) else: @@ -1187,6 +1190,8 @@ def process_consumer_topic(self, client: mqtt.Client, var: Dict[str, Consumer], self.set_json_payload_class(var["consumer"+index].data.config, msg) elif re.search("openWB/consumer/[0-9]+/get", msg.topic) is not None: self.set_json_payload_class(var["consumer"+index].data.get, msg) + elif re.search("openWB/consumer/[0-9]+/set", msg.topic) is not None: + self.set_json_payload_class(var["consumer"+index].data.set, msg) elif re.search("openWB/consumer/[0-9]+/extra_meter", msg.topic) is not None: self.set_json_payload_class(var[f"consumer{index}"].data, msg) elif re.search("openWB/consumer/[0-9]+/usage$", msg.topic) is not None: diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index e0ac8bc70e..ccc37b5606 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -9,7 +9,8 @@ with ImportErrorContext(): from dateutil.relativedelta import relativedelta -from helpermodules.abstract_plans import AutolockPlan, ScheduledChargingPlan, TimeChargingPlan, TimeChargingPlanConsumer +from helpermodules.abstract_plans import (AutolockPlan, ContinuousScheduledPlanConsumer, ScheduledChargingPlan, + SuspendableScheduledPlanConsumer, TimeChargingPlan, TimeChargingPlanConsumer) log = logging.getLogger(__name__) @@ -113,7 +114,9 @@ def is_timeframe_valid(now: datetime.datetime, begin: datetime.datetime, end: da return state -def check_end_time(plan: ScheduledChargingPlan, +def check_end_time(plan: Union[ContinuousScheduledPlanConsumer, + ScheduledChargingPlan, + SuspendableScheduledPlanConsumer], buffer: Optional[float]) -> Optional[float]: """ gibt die verbleibende Zeit in Sekunden zurück. diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 4c2bf7d754..5c0a568fb7 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -173,6 +173,8 @@ class UpdateConfig: "^openWB/command/max_id/charge_template$", "^openWB/command/max_id/charge_template_scheduled_plan$", "^openWB/command/max_id/charge_template_time_charging_plan$", + "^openWB/command/max_id/consumer_scheduled_plan$", + "^openWB/command/max_id/consumer_time_plan$", "^openWB/command/max_id/chargepoint_template$", "^openWB/command/max_id/device$", "^openWB/command/max_id/ev_template$", @@ -201,6 +203,11 @@ class UpdateConfig: "^openWB/consumer/[0-9]+/get/state$", "^openWB/consumer/[0-9]+/get/state_str$", "^openWB/consumer/[0-9]+/get/voltages$", + "^openWB/consumer/[0-9]+/set/current$", + "^openWB/consumer/[0-9]+/set/phases_to_use$", + "^openWB/consumer/[0-9]+/set/plug_time$", + "^openWB/consumer/[0-9]+/set/timestamp_last_current_set$", + "^openWB/consumer/[0-9]+/set/wait_for_start_state$", "^openWB/counter/config/consider_less_charging$", "^openWB/counter/config/home_consumption_source_id$", diff --git a/packages/main.py b/packages/main.py index 3385139ba9..1e9570706d 100755 --- a/packages/main.py +++ b/packages/main.py @@ -234,6 +234,10 @@ def handler_midnight(self): thread_errors_path = Path(Path(__file__).resolve().parents[1]/"ramdisk"/"thread_errors.log") with thread_errors_path.open("w") as f: f.write("") + with ChangedValuesContext(loadvars_.event_module_update_completed): + for consumer in data.data.consumer_data.values(): + consumer.reset_wait_for_start() + consumer.reset_chargemode_at_midnight() except Exception: log.exception("Fehler im Main-Modul")