diff --git a/packages/control/algorithm/filter_chargepoints.py b/packages/control/algorithm/filter_chargepoints.py index 944af89944..603dee204f 100644 --- a/packages/control/algorithm/filter_chargepoints.py +++ b/packages/control/algorithm/filter_chargepoints.py @@ -70,8 +70,7 @@ def _group_loads_by_chargemode(chargemodes: Tuple[Tuple[Optional[str], str]], def get_loads_by_chargemodes(chargemodes: Tuple[Tuple[Optional[Chargemode], Chargemode]]) -> List[Load]: def _is_valid_for_chargemode(entity, chargemode, valid): """Helper function to validate entity against chargemode conditions.""" - return (entity.data.control_parameter.required_current != 0 and - (entity.data.control_parameter.chargemode == chargemode[0] or chargemode[0] is None) and + return ((entity.data.control_parameter.chargemode == chargemode[0] or chargemode[0] is None) and entity.data.control_parameter.submode == chargemode[1] and entity not in valid) diff --git a/packages/control/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index 112ba4d043..6620eb31d0 100644 --- a/packages/control/chargepoint/control_parameter.py +++ b/packages/control/chargepoint/control_parameter.py @@ -11,7 +11,7 @@ class ControlParameter: chargemode: Chargemode_enum = field(default=Chargemode_enum.STOP, metadata={ "topic": "control_parameter/chargemode"}) - current_plan: Optional[str] = field(default=None, metadata={"topic": "control_parameter/current_plan"}) + current_plan: Optional[int] = field(default=None, metadata={"topic": "control_parameter/current_plan"}) failed_phase_switches: int = field(default=0, metadata={"topic": "control_parameter/failed_phase_switches"}) limit: Optional[LoadmanagementLimit] = field(default_factory=loadmanagement_limit_factory, metadata={ "topic": "control_parameter/limit"}) diff --git a/packages/control/consumer/consumer.py b/packages/control/consumer/consumer.py index a7819344f6..84a6ce6d3c 100644 --- a/packages/control/consumer/consumer.py +++ b/packages/control/consumer/consumer.py @@ -1,14 +1,15 @@ import datetime import logging -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, 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, ResetModes, WaitForStartStates +from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState +from control.consumer.consumer_data import ConsumerData, ConsumerUsage, ResetModes, WaitForStartStates from control.load_protocol import Load from helpermodules import timecheck -from helpermodules.abstract_plans import ContinuousScheduledPlanConsumer, SuspendableScheduledPlanConsumer +from helpermodules.abstract_plans import ScheduledPlanConsumer from helpermodules.phase_handling import convert_single_evu_phase_to_cp_phase, voltages_mean from modules.common.configurable_consumer import ConfigurableConsumer @@ -25,7 +26,7 @@ def __init__(self, index: int): self.chargemode_changed: bool = False self.submode_changed: bool = False - def set_state_and_log(self, message: str) -> None: + def set_state_and_log(self, message: Optional[str]) -> None: if message: log.info(f"Verbraucher {self.num}: {message}") if self.data.get.state_str is None: @@ -34,45 +35,66 @@ def set_state_and_log(self, message: str) -> None: self.data.get.state_str += f" {message}" def setup_values_at_start(self): + self.data.set.state_str_prev = self.data.get.state_str self.data.get.state_str = None + self.data.set.current_prev = self.data.set.current def update(self): - self.setup_values_at_start() - if isinstance(self.data.usage, MeterOnlyConfig): - return - else: - if self.data.get.voltages is None: - self.data.get.voltages = [230 for i in range(0, self.data.config.connected_phases)] - if self.data.get.currents is None: - self.data.get.currents = [0]*3 - for i in range(0, self.data.config.connected_phases): - self.data.get.currents[i] = (self.data.get.power / - self.data.config.connected_phases / - self.data.get.voltages[i]) - self.data.get.phases_in_use = self.data.config.connected_phases - 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.process_on_time() - log.debug( - f"Verbraucher {self.num}: Sollstrom {required_current}, min. Ist-Strom {max(self.data.get.currents)}," - f" Modus {mode}, Submodus {submode}, {message}") + try: + self.setup_values_at_start() + if self.data.usage.type == ConsumerUsage.METER_ONLY: + return + else: + if self.data.get.voltages is None: + self.data.get.voltages = [230 for i in range(0, self.data.config.connected_phases)] + if self.data.get.currents is None: + self.data.get.currents = [0]*3 + for i in range(0, self.data.config.connected_phases): + self.data.get.currents[i] = (self.data.get.power / + self.data.config.connected_phases / + self.data.get.voltages[i]) + self.data.get.phases_in_use = self.data.config.connected_phases + 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 + self.reset_chargemode_at_time() + self.is_switch_interval_elapsed() + 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.process_on_time() + log.debug(f"Verbraucher {self.num}: Sollstrom {required_current}, " + f"min. Ist-Strom {max(self.data.get.currents)}," + f" Modus {mode}, Submodus {submode}, {message}") + except Exception: + log.exception(f"Fehler bei Verbraucher {self.num}") def process_on_time(self): - if self.data.control_parameter.timestamp_charge_start is None: - if max(self.data.get.currents) > self.data.config.min_current: + if self.data.get.charge_state: + self.data.set.on_time += data.data.general_data.data.control_interval + if self.data.control_parameter.timestamp_charge_start is None: self.data.control_parameter.timestamp_charge_start = timecheck.create_timestamp() - elif max(self.data.get.currents) < self.data.config.min_current: - self.reset_timestamp_start() - else: - self.data.set.on_time = timecheck.create_timestamp() - self.data.control_parameter.timestamp_charge_start + elif self.data.get.charge_state is False: + self.data.control_parameter.timestamp_charge_start = None def reset_timestamp_start(self): self.data.control_parameter.timestamp_charge_start = None + def reset_on_time(self): + self.data.set.on_time = 0 + + def is_switch_interval_elapsed(self): + if (timecheck.create_timestamp() < self.data.set.timestamp_last_current_set + self.data.config.min_interval and + # wenn kein Betrieb, darf eingeschaltet werden, auch wenn das Intervall noch nicht abgelaufen ist + self.data.control_parameter.state != ChargepointState.NO_CHARGING_ALLOWED and + (self.data.usage.wait_for_start_active is False or + (self.data.usage.wait_for_start_active and + self.data.set.wait_for_start_state == WaitForStartStates.START_SIGNAL_RECEIVED))): + self.data.set.switch_interval_elapsed = False + else: + self.data.set.switch_interval_elapsed = True + PRICE_LIMIT_EXCEEDED = "Preislimit für Verbraucher aktiv, aktueller Preis zu hoch." PRICE_LIMIT_EXCEEDED_CONTINOUS_STILL_RUNNING = ("Preislimit für Verbraucher aktiv, aktueller Preis zu hoch. " "Verbraucher läuft weiter, da der Verbraucher nicht abgeschaltet " @@ -81,39 +103,50 @@ def reset_timestamp_start(self): SURPLUS_CONTINOUS_STILL_RUNNING = ("Verbraucher läuft ggf auch ohne ausreichend Überschuss weiter, da der " "Verbraucher nicht abgeschaltet werden darf.") - def get_parameter(self) -> Tuple[int, int, Optional[str], Chargemode, Chargemode]: - if timecheck.create_timestamp() < self.data.set.timestamp_last_current_set + self.data.config.min_interval: + def get_parameter(self) -> Tuple[float, float, Optional[str], Optional[Chargemode], Chargemode]: + if self.data.set.switch_interval_elapsed is False: log.debug("Intervall für neuen Schaltbefehl nicht abgelaufen.") - return (self.data.control_parameter.min_current, - self.data.control_parameter.required_current, - None, + return (0, + 0, + self.data.set.state_str_prev, self.data.control_parameter.chargemode, self.data.control_parameter.submode) - 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: - required_current, message, mode, submode = self.time_charging() - elif self.data.usage.chargemode == Chargemode.INSTANT_CHARGING: - required_current, message, mode, submode = self.instant_charging() - elif self.data.usage.chargemode == Chargemode.ECO_CHARGING: - required_current, message, mode, submode = self.wait_for_start_handler(self.eco_charging) - elif self.data.usage.chargemode == Chargemode.PV_CHARGING: - required_current, message, mode, submode = self.wait_for_start_handler(self.pv_charging) - elif self.data.usage.chargemode == Chargemode.STOP: - required_current = 0 - message = None - mode = Chargemode.STOP - submode = Chargemode.STOP + if self.data.usage.type == ConsumerUsage.SUSPENDABLE_TUNABLE: + min_current = self.data.config.min_current else: - raise ValueError(f"Ungültiger Lademodus {self.data.usage.chargemode} für Verbraucher {self.num}") + min_current = self._convert_power_to_current(self.data.config.max_power) + required_current = 0 + submode = Chargemode.STOP + mode = self.data.usage.chargemode + message = None + if self.data.usage.chargemode == Chargemode.SCHEDULED_CHARGING: + required_current, tmp_message, submode = self.wait_for_start_handler(self.scheduled_charging) + message = f"{tmp_message or ''}".strip() + if submode != Chargemode.INSTANT_CHARGING and self.data.usage.time_charging.active: + required_current, tmp_message, submode = self.time_charging() + # Info vom Zielladen erhalten + message = f"{message or ''} {tmp_message or ''}".strip() + if required_current == 0: + if self.data.usage.chargemode == Chargemode.INSTANT_CHARGING: + required_current, tmp_message, submode = self.instant_charging() + elif self.data.usage.chargemode == Chargemode.ECO_CHARGING: + required_current, tmp_message, submode = self.wait_for_start_handler(self.eco_charging) + elif self.data.usage.chargemode == Chargemode.PV_CHARGING: + required_current, tmp_message, submode = self.wait_for_start_handler(self.pv_charging) + else: + tmp_message = None + message = f"{message or ''} {tmp_message or ''}".strip() + if self.data.usage.chargemode == Chargemode.STOP: + required_current, message, submode = self.stop() return min_current, required_current, message, mode, submode 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.config.min_current + if self.data.get.charge_state: + return get_medium_charging_current(self.data.get.currents) + else: + return self._convert_power_to_current(self.data.config.max_power) else: return required_current @@ -125,7 +158,7 @@ def _convert_power_to_current(self, power: float) -> float: 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]: + def scheduled_charging(self) -> Tuple[float, Optional[str], Chargemode]: plan_data, remaining_time, duration = self._find_recent_plan(self.data.usage.scheduled_charging.plans) if plan_data: self.data.control_parameter.current_plan = plan_data.id @@ -137,7 +170,7 @@ def scheduled_charging(self) -> Tuple[Chargemode, str]: duration) def _find_recent_plan(self, - plans: List[Union[ContinuousScheduledPlanConsumer, SuspendableScheduledPlanConsumer]]): + plans: List[ScheduledPlanConsumer]) -> Tuple[Optional[ScheduledPlanConsumer], float, float]: plans_diff_end_date: List[Dict[int, float]] = [] for p in plans: try: @@ -148,30 +181,32 @@ def _find_recent_plan(self, log.exception("Fehler im ev-Modul "+str(self.num)) 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 + # filtered_plans = [d for d in plans_diff_end_date if list(d.values())[0] is not None] + # if filtered_plans: + sorted_plans = sorted(plans_diff_end_date, key=lambda x: list(x.values())[0]) + for p in sorted_plans: + if self.BUFFER_AFTER_END_TIME < list(p.values())[0]: + plan_dict = p + break + else: + return None, 0, 0 + 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 + break + else: + raise Exception(f"Verbraucher {self.num} konnte plan_id {plan_id} keine Plan zuordnen.") - remaining_time, duration = self._calc_remaining_time(plan, diff_end_date) + remaining_time, duration = self._calc_remaining_time(plan, diff_end_date) - return plan, remaining_time, duration - else: - return None + return plan, remaining_time, duration + return None, 0, 0 def _calc_remaining_time(self, - plan: Union[ContinuousScheduledPlanConsumer, SuspendableScheduledPlanConsumer], + plan: ScheduledPlanConsumer, diff_end_date: float) -> Tuple[float, float]: duration = plan.duration - self.data.set.on_time remaining_time = diff_end_date - duration @@ -179,65 +214,52 @@ def _calc_remaining_time(self, 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_REACHED_MAX_ON_TIME = ("Ziel erreicht, da das Gerät die gewünschte Dauer in Betrieb war.") + SCHEDULED_CHARGING_NO_PLANS_CONFIGURED = "Kein Ziel, da keine Ziel-Termine konfiguriert sind." + SCHEDULED_CHARGING_NO_DATE_PENDING = "Kein Ziel, da kein Ziel-Termin ansteht. " + SCHEDULED_CHARGING_USE_PV = "Betrieb startet {}. Falls vorhanden, wird Überschuss verwendet. " + SCHEDULED_CHARGING_CHEAP_HOUR = "Ziel, da ein günstiger Zeitpunkt für den preisbasierten Betrieb 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" + "Ziel ausstehend, da jetzt kein günstiger Zeitpunkt für den preisbasierten " + "Betrieb ist. {} Falls vorhanden, wird Überschuss verwendet. ") + + def scheduled_charging_calc_current( + self, + plan: Optional[ScheduledPlanConsumer], + remaining_time: float, + duration: float) -> Tuple[float, Optional[str], Chargemode]: + required_current = 0 + message = None if plan is None: if len(self.data.usage.scheduled_charging.plans) == 0: - return submode, self.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED + return 0, self.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, Chargemode.STOP else: - return submode, self.SCHEDULED_CHARGING_NO_DATE_PENDING - if self.data.set.on_time >= duration: + return 0, self.SCHEDULED_CHARGING_NO_DATE_PENDING, Chargemode.STOP + if duration <= 0: message = self.SCHEDULED_REACHED_MAX_ON_TIME + submode = Chargemode.STOP elif (0 < remaining_time < self.BUFFER_START_EARLIER): - submode = "instant_charging" + submode = Chargemode.INSTANT_CHARGING + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) # weniger als die berechnete Zeit verfügbar elif remaining_time <= 0: - submode = "instant_charging" + submode = Chargemode.INSTANT_CHARGING + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) else: # Wenn dynamische Tarife aktiv sind, prüfen, ob jetzt ein günstiger Zeitpunkt zum Laden # ist. if data.data.optional_data.data.electricity_pricing.configured: def get_hours_message() -> str: - def end_of_today_timestamp() -> int: + def end_of_today_timestamp() -> float: 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 is_loading_hour(hour_list: List[int]) -> bool: + return data.data.optional_data.ep_is_charging_allowed_hours_list(hour_list) - def convert_loading_hours_to_string(hour_list: List[int]) -> str: + def convert_loading_hours_to_string(hour_list: List[datetime.datetime]) -> 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') @@ -251,7 +273,7 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str: loading_times_tomorrow = [datetime.datetime.fromtimestamp(hour) for hour in sorted(hour_list) if hour > midnight] - parts = [] + parts: List[str] = [] if is_loading_hour(hour_list): parts.append("jetzt") @@ -266,7 +288,7 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str: parts.append(" sowie ") parts.append(f"morgen {convert_loading_hours_to_string(loading_times_tomorrow)}") - loading_message = "Geladen wird " + "".join(parts) + loading_message = "Eingeschaltet wird " + "".join(parts) return loading_message + '.' hour_list = data.data.optional_data.ep_get_loading_hours(duration, duration + remaining_time) @@ -274,10 +296,13 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str: 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" + submode = Chargemode.INSTANT_CHARGING + required_current = self._parse_required_current_by_usage( + self._convert_power_to_current(self.data.config.max_power)) else: message = self.SCHEDULED_CHARGING_EXPENSIVE_HOUR.format(get_hours_message()) - submode = "pv_charging" + submode = Chargemode.PV_CHARGING + required_current = self._parse_required_current_by_usage(self.data.config.min_current) else: now = datetime.datetime.today() start_time = now + datetime.timedelta(seconds=remaining_time) @@ -287,8 +312,9 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str: 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 + submode = Chargemode.PV_CHARGING + required_current = self._parse_required_current_by_usage(self.data.config.min_current) + return required_current, message, submode 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.") @@ -297,7 +323,7 @@ def convert_loading_hours_to_string(hour_list: List[int]) -> str: TIME_CHARGING_NO_PLAN_ACTIVE = "Betrieb mit Zeitladen nicht möglich, da kein Plan aktiv ist." TIME_CHARGING_NO_PLAN_CONFIGURED = "Betrieb mit Zeitladen nicht möglich, da kein Plan konfiguriert ist." - def time_charging(self) -> Tuple[int, str, Optional[str], int]: + def time_charging(self) -> Tuple[float, Optional[str], Chargemode]: required_current = 0 message = None submode = Chargemode.STOP @@ -324,23 +350,23 @@ def time_charging(self) -> Tuple[int, str, Optional[str], int]: message = self.TIME_CHARGING_NO_PLAN_ACTIVE else: message = self.TIME_CHARGING_NO_PLAN_CONFIGURED - return required_current, message, None, submode + return required_current, message, submode - def instant_charging(self) -> Tuple[int, str, Optional[str], int]: + def instant_charging(self) -> Tuple[float, Optional[str], Chargemode]: 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 - return required_current, message, mode, submode + return required_current, message, submode CHARGING_PRICE_EXCEEDED = "Der aktuelle Strompreis liegt über dem maximalen Strompreis. " CHARGING_PRICE_LOW = "Betrieb, da der aktuelle Strompreis unter dem maximalen Strompreis liegt." - def eco_charging(self) -> Tuple[int, str, Optional[str], int]: - mode = Chargemode.ECO_CHARGING + def eco_charging(self) -> Tuple[float, Optional[str], Chargemode]: + message = None 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): + 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._convert_power_to_current(self.data.config.max_power)) message = self.CHARGING_PRICE_LOW @@ -354,62 +380,65 @@ def eco_charging(self) -> Tuple[int, str, Optional[str], int]: else: required_current = self._parse_required_current_by_usage(self.data.config.min_current) submode = Chargemode.PV_CHARGING - return required_current, message, mode, submode + return required_current, message, submode - def pv_charging(self) -> Tuple[int, str, Optional[str], int]: + def pv_charging(self) -> Tuple[float, Optional[str], Chargemode]: 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 + return required_current, message, submode + + def stop(self) -> Tuple[float, Optional[str], Chargemode]: + required_current = 0 + message = "Verbraucher im Modus Stop. " + submode = Chargemode.STOP + return required_current, message, 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." + DEVICE_WAITING_FOR_START = ("Gerätestart erkannt. " + "Warte auf Erreichen der Einschaltschwelle, um das Gerät zu starten.") def wait_for_start_handler( - self, func: Callable[[], Tuple[int, str, Optional[str], int]] - ) -> Tuple[float, str, Optional[Chargemode], Chargemode]: + self, func: Callable[[], Tuple[float, Optional[str], Chargemode]] + ) -> Tuple[float, Optional[str], Chargemode]: if self.data.usage.wait_for_start_active: 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() + required_current, message, 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() + required_current, message, submode = func() else: - required_current, message, mode, submode = func() + required_current, message, 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() + required_current, message, submode = func() 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 + required_current, message, submode = func() + return required_current, message, submode def reset_wait_for_start(self): self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_DEVICE_START @@ -418,6 +447,7 @@ def midnight_handler(self): self.reset_chargemode_at_midnight() self.reset_wait_for_start() self.reset_timestamp_start() + self.reset_on_time() def reset_chargemode_at_midnight(self): if self.data.usage.reset_chargemode.mode == ResetModes.MIDNIGHT: @@ -429,13 +459,19 @@ def reset_chargemode_at_midnight(self): 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.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 - def set_control_parameter(self, min_current, required_current, phases, submode, mode): + def set_control_parameter(self, + min_current: float, + required_current: float, + phases: int, + submode: Chargemode, + mode: Optional[Chargemode]): self.data.control_parameter.min_current = min_current self.data.control_parameter.required_current = required_current self.data.control_parameter.phases = phases @@ -457,7 +493,7 @@ def set_control_parameter(self, min_current, required_current, phases, submode, def is_charging_stop_allowed(self) -> bool: return self.data.usage.type != ConsumerUsage.CONTINUOUS - def set_mode_changed(self, submode: Chargemode, mode: Chargemode) -> None: + def set_mode_changed(self, submode: Chargemode, mode: Optional[Chargemode]) -> None: self.submode_changed = (submode != self.data.control_parameter.submode) if ((submode == Chargemode.TIME_CHARGING and self.data.control_parameter.chargemode != Chargemode.TIME_CHARGING) or diff --git a/packages/control/consumer/consumer_all.py b/packages/control/consumer/consumer_all.py index d6e5108a58..c33d036a2b 100644 --- a/packages/control/consumer/consumer_all.py +++ b/packages/control/consumer/consumer_all.py @@ -53,7 +53,7 @@ def get_consumer_sum(self): self.data.get.exported = exported self.data.get.fault_state = fault_state self.data.get.fault_str = NO_ERROR if fault_state == 0 else ( - "Bitte die Statusmeldungen der Wechselrichter prüfen. " + "Bitte die Statusmeldungen der Verbraucher prüfen. " "Es haben nicht alle Module aktuelle Zählerstände geliefert.") except Exception: log.exception("Fehler in der allgemeinen Verbaucher-Klasse") diff --git a/packages/control/consumer/consumer_data.py b/packages/control/consumer/consumer_data.py index 3ec679609f..584bd441b0 100644 --- a/packages/control/consumer/consumer_data.py +++ b/packages/control/consumer/consumer_data.py @@ -1,12 +1,11 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Callable, Dict, List, Optional, Union +from typing import List, Optional from control.chargemode import Chargemode from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory from control.consumer.usage import ConsumerUsage from dataclass_utils.factories import empty_list_factory -from helpermodules.abstract_plans import (ContinuousScheduledPlanConsumer, SuspendableScheduledPlanConsumer, - TimeChargingPlanConsumer) +from helpermodules.abstract_plans import ScheduledPlanConsumer, TimeChargingPlanConsumer from helpermodules.constants import NO_ERROR from modules.common.consumer_setup import ConsumerSetup @@ -17,13 +16,8 @@ class EcoCharging: @dataclass -class ScheduledContinuousCharging: - plans: List[ContinuousScheduledPlanConsumer] = field(default_factory=empty_list_factory) - - -@dataclass -class ScheduledSuspendableCharging: - plans: List[SuspendableScheduledPlanConsumer] = field(default_factory=empty_list_factory) +class ScheduledCharging: + plans: List[ScheduledPlanConsumer] = field(default_factory=empty_list_factory) @dataclass @@ -58,67 +52,16 @@ class Log: @dataclass -class MeterOnlyConfig: - type: ConsumerUsage = ConsumerUsage.METER_ONLY - - -@dataclass -class SuspendableTunableDeviceConfig: +class Usage: chargemode: Chargemode = Chargemode.INSTANT_CHARGING eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging()) - scheduled_charging: ScheduledSuspendableCharging = field(default_factory=lambda: ScheduledSuspendableCharging()) + scheduled_charging: ScheduledCharging = field(default_factory=lambda: ScheduledCharging()) 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 - 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 + type: ConsumerUsage = ConsumerUsage.METER_ONLY reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode()) wait_for_start_active: bool = True -@dataclass -class ContinuousDeviceConfig: - chargemode: Chargemode = Chargemode.INSTANT_CHARGING - 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, - SuspendableTunableDeviceConfig, - SuspendableOnOffDeviceConfig, - ContinuousDeviceConfig]] = { - ConsumerUsage.METER_ONLY: MeterOnlyConfig(), - ConsumerUsage.SUSPENDABLE_TUNABLE: SuspendableTunableDeviceConfig(), - ConsumerUsage.SUSPENDABLE_ONOFF: SuspendableOnOffDeviceConfig(), - ConsumerUsage.CONTINUOUS: ContinuousDeviceConfig(), -} - -GET_CLASS_BY_USAGE: Dict[ConsumerUsage, Callable] = { - ConsumerUsage.METER_ONLY: MeterOnlyConfig, - ConsumerUsage.SUSPENDABLE_TUNABLE: SuspendableTunableDeviceConfig, - ConsumerUsage.SUSPENDABLE_ONOFF: SuspendableOnOffDeviceConfig, - ConsumerUsage.CONTINUOUS: ContinuousDeviceConfig, -} - -GET_PLAN_CLASS_FOR_USAGE: Dict[ConsumerUsage, Callable] = { - ConsumerUsage.SUSPENDABLE_TUNABLE: SuspendableScheduledPlanConsumer, - ConsumerUsage.SUSPENDABLE_ONOFF: SuspendableScheduledPlanConsumer, - ConsumerUsage.CONTINUOUS: ContinuousScheduledPlanConsumer, -} - - @dataclass class ConsumerConfig: connected_phases: int = 1 @@ -155,11 +98,13 @@ class Set: plug_time: Optional[float] = field(default=None, metadata={"topic": "set/plug_time"}) required_power: float = 0 current_prev: float = 0 + state_str_prev: str = "" target_current: float = 0 charge_state_prev: bool = False log: Log = field(default_factory=lambda: Log()) on_time: float = field(default=0, metadata={"topic": "set/on_time"}) power: Optional[float] = None + switch_interval_elapsed: bool = False timestamp_last_current_set: float = field(default=0, metadata={"topic": "set/timestamp_last_current_set"}) wait_for_start_state: WaitForStartStates = field( default=WaitForStartStates.WAIT_FOR_DEVICE_START, metadata={"topic": "set/wait_for_start_state"}) @@ -167,13 +112,10 @@ class Set: @dataclass class ConsumerData: - module: ConsumerSetup = None - config: ConsumerConfig = field(default_factory=lambda: ConsumerConfig()) - extra_meter: Optional[int] = None - usage: Union[MeterOnlyConfig, - SuspendableTunableDeviceConfig, - SuspendableOnOffDeviceConfig, - ContinuousDeviceConfig] = field(default_factory=lambda: MeterOnlyConfig()) control_parameter: ControlParameter = field(default_factory=control_parameter_factory) get: Get = field(default_factory=lambda: Get()) set: Set = field(default_factory=lambda: Set()) + module: ConsumerSetup = None + config: ConsumerConfig = field(default_factory=lambda: ConsumerConfig()) + extra_meter: Optional[int] = None + usage: Usage = field(default_factory=lambda: Usage(), metadata={"topic": "usage"}) diff --git a/packages/control/consumer/consumer_test.py b/packages/control/consumer/consumer_test.py index 6e6228f585..ebd457f3b0 100644 --- a/packages/control/consumer/consumer_test.py +++ b/packages/control/consumer/consumer_test.py @@ -6,7 +6,8 @@ from control import data from control.chargemode import Chargemode from control.consumer.consumer import Consumer -from control.consumer.consumer_data import ContinuousDeviceConfig, WaitForStartStates +from control.consumer.consumer_data import WaitForStartStates +from control.consumer.usage import ConsumerUsage @pytest.fixture(autouse=True) @@ -17,7 +18,7 @@ def mock_data() -> None: @pytest.fixture def consumer() -> Consumer: load = Consumer(1) - load.data.usage = ContinuousDeviceConfig() + load.data.usage.type = ConsumerUsage.CONTINUOUS load.data.config.min_current = 6 load.data.config.max_power = 2300 load.data.config.connected_phases = 1 @@ -106,7 +107,6 @@ def test_wait_for_start_handler( 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 diff --git a/packages/control/counter.py b/packages/control/counter.py index 103c61546c..17e6bf8af3 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -96,6 +96,26 @@ class CounterData: set: Set = field(default_factory=set_factory) +@dataclass(frozen=True) +class SwitchOffTexts: + stop: str + waiting: str + waiting_min_interval: str + no_stop: str + exceeded: str + not_charging: str + + +@dataclass(frozen=True) +class SwitchOnTexts: + fallen_below: str + waiting: str + not_exceeded: str + expired: str + max_phases: str = "" + feed_in_limit_note: str = "" + + class Counter: MAX_EVU_ERROR_DURATION = 60 @@ -153,6 +173,12 @@ def _set_current_left(self, loadmanagement_available: bool) -> None: load = data.data.cp_data[load_key] else: load = data.data.consumer_data[load_key] + # Wenn der Verbraucher nicht angesteuert werden darf, im LM als nicht veränderbaren Verbrauch + # berücksichtigen. + if load.data.set.switch_interval_elapsed is False: + log.debug(f"Verbraucher {load.num} als unveränderlichen Verbrauch im LM " + f"mit {load.data.get.power}W berücksichtigt.") + continue try: element_current = convert_cp_currents_to_evu_currents( load.data.config.phase_1, @@ -191,6 +217,14 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: power_raw = self.data.get.power for cp in data.data.cp_data.values(): power_raw -= cp.data.get.power + for consumer in data.data.consumer_data.values(): + # Wenn der Verbraucher nicht angesteuert werden darf, im LM als nicht veränderbaren Verbrauch + # berücksichtigen. + if consumer.data.set.switch_interval_elapsed: + power_raw -= consumer.data.get.power + else: + log.debug(f"Verbraucher {consumer.num} als unveränderlichen Verbrauch im LM " + f"mit {consumer.data.get.power}W berücksichtigt.") self.data.set.raw_power_left = self.data.config.max_total_power - power_raw log.info(f'Verbleibende Leistung an Zähler {self.num}: {self.data.set.raw_power_left}W') else: @@ -267,12 +301,27 @@ def get_usable_surplus(self, feed_in_yield: float) -> float: return (-self.calc_surplus() + self.data.set.released_surplus - self.data.set.reserved_surplus - feed_in_yield) - SWITCH_ON_FALLEN_BELOW = "Einschaltschwelle während der Wartezeit unterschritten." - SWITCH_ON_WAITING = "Die Ladung wird gestartet, sobald in {} die Wartezeit abgelaufen ist." - SWITCH_ON_NOT_EXCEEDED = ("Die Ladung kann nicht gestartet werden, da die Einschaltschwelle nicht erreicht " - "wird.") - SWITCH_ON_EXPIRED = "Einschaltschwelle für die Dauer der Wartezeit überschritten." - SWITCH_ON_MAX_PHASES = "Der Überschuss ist ausreichend, um direkt mit {} Phasen zu laden." + SWITCH_ON_TEXTS_CP = SwitchOnTexts( + fallen_below="Einschaltschwelle während der Wartezeit unterschritten.", + waiting="Die Ladung wird gestartet, sobald in {} die Wartezeit abgelaufen ist.", + not_exceeded=("Die Ladung kann nicht gestartet werden, da die Einschaltschwelle nicht erreicht " + "wird."), + expired="Einschaltschwelle für die Dauer der Wartezeit überschritten.", + max_phases="Der Überschuss ist ausreichend, um direkt mit {} Phasen zu laden.", + feed_in_limit_note=" Die Einspeisegrenze wird berücksichtigt.", + ) + SWITCH_ON_TEXTS_CONSUMER = SwitchOnTexts( + fallen_below="Einschaltschwelle während der Wartezeit unterschritten.", + waiting="Der Betrieb wird gestartet, sobald in {} die Wartezeit abgelaufen ist.", + not_exceeded=("Der Betrieb kann nicht gestartet werden, da die Einschaltschwelle nicht erreicht " + "wird."), + expired="Einschaltschwelle für die Dauer der Wartezeit überschritten.", + ) + + def _get_switch_on_texts(self, load: Load) -> SwitchOnTexts: + if isinstance(load, Chargepoint): + return self.SWITCH_ON_TEXTS_CP + return self.SWITCH_ON_TEXTS_CONSUMER def calc_switch_on_power(self, load: Load) -> Tuple[float, float]: surplus = self.calc_raw_surplus() - self.data.set.reserved_surplus @@ -300,6 +349,7 @@ def get_pv_config_py_load(self, load: Load) -> Tuple[float, float, bool]: def switch_on_threshold_reached(self, load: Load) -> None: try: message = None + texts = self._get_switch_on_texts(load) control_parameter = load.data.control_parameter timestamp_switch_on_off = control_parameter.timestamp_switch_on_off switch_on_delay, switch_on_threshold, feed_in_limit = self.get_pv_config_py_load(load) @@ -313,7 +363,7 @@ def switch_on_threshold_reached(self, load: Load) -> None: # Einschaltschwelle wurde unterschritten, Timer zurücksetzen timestamp_switch_on_off = None self.data.set.reserved_surplus -= power_to_reserve - message = self.SWITCH_ON_FALLEN_BELOW.format(switch_on_threshold) + message = texts.fallen_below control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED else: # Timer starten @@ -321,14 +371,14 @@ def switch_on_threshold_reached(self, load: Load) -> None: not feed_in_limit): timestamp_switch_on_off = timecheck.create_timestamp() self.data.set.reserved_surplus += power_to_reserve - message = self.SWITCH_ON_WAITING.format(timecheck.convert_timestamp_delta_to_time_string( + message = texts.waiting.format(timecheck.convert_timestamp_delta_to_time_string( timestamp_switch_on_off, switch_on_delay)) if feed_in_limit: - message += " Die Einspeisegrenze wird berücksichtigt." + message += texts.feed_in_limit_note control_parameter.state = ChargepointState.SWITCH_ON_DELAY else: # Einschaltschwelle nicht erreicht - message = self.SWITCH_ON_NOT_EXCEEDED.format(switch_on_threshold) + message = texts.not_exceeded if timestamp_switch_on_off != control_parameter.timestamp_switch_on_off: control_parameter.timestamp_switch_on_off = timestamp_switch_on_off @@ -347,18 +397,19 @@ def switch_on_timer_expired(self, load: Load) -> None: """ try: msg = None + texts = self._get_switch_on_texts(load) switch_on_delay, switch_on_threshold, _ = self.get_pv_config_py_load(load) control_parameter = load.data.control_parameter # Timer ist noch nicht abgelaufen if timecheck.check_timestamp(control_parameter.timestamp_switch_on_off, switch_on_delay): - msg = self.SWITCH_ON_WAITING.format(timecheck.convert_timestamp_delta_to_time_string( + msg = texts.waiting.format(timecheck.convert_timestamp_delta_to_time_string( control_parameter.timestamp_switch_on_off, switch_on_delay)) # Timer abgelaufen else: control_parameter.timestamp_switch_on_off = None self.data.set.reserved_surplus -= switch_on_threshold*control_parameter.phases - msg = self.SWITCH_ON_EXPIRED.format(switch_on_threshold) + msg = texts.expired control_parameter.state = ChargepointState.WAIT_FOR_USING_PHASES if isinstance(load, Chargepoint): @@ -375,18 +426,37 @@ def switch_on_timer_expired(self, load: Load) -> None: load.hw_supports_phase_switch() and self.get_usable_surplus(feed_in_yield) > max_phases_power): control_parameter.phases = ev_template.data.max_phases - msg += self.SWITCH_ON_MAX_PHASES.format(ev_template.data.max_phases) + msg += texts.max_phases.format(ev_template.data.max_phases) + elif isinstance(load, Consumer): + control_parameter.state = ChargepointState.CHARGING_ALLOWED load.set_state_and_log(msg) except Exception: log.exception("Fehler im allgemeinen PV-Modul") - SWITCH_OFF_STOP = "Ladevorgang nach Ablauf der Wartezeit gestoppt." - SWITCH_OFF_WAITING = "Ladevorgang wird nach Ablauf der Wartezeit in {} gestoppt." - SWITCH_OFF_NO_STOP = ("Der Ladevorgang wird trotz fehlenden Überschusses nicht gestoppt, da in dem Fahrzeug-Profil " - "die Einstellung 'Ladung aktiv halten' aktiviert ist.") - SWITCH_OFF_EXCEEDED = "Abschaltschwelle während der Verzögerung überschritten." - SWITCH_OFF_NOT_CHARGING = ("Da das EV nicht lädt und die Abschaltschwelle erreicht wird, " - "wird die Ladefreigabe sofort entzogen.") + SWITCH_OFF_TEXTS_CP = SwitchOffTexts( + stop="Ladevorgang nach Ablauf der Wartezeit gestoppt.", + waiting="Ladevorgang wird nach Ablauf der Wartezeit in {} gestoppt.", + waiting_min_interval="Ladevorgang wird nach Ablauf des minimalen Regelintervalls in {} gestoppt.", + no_stop=("Der Ladevorgang wird trotz fehlenden Überschusses nicht gestoppt, da in dem Fahrzeug-Profil " + "die Einstellung 'Ladung aktiv halten' aktiviert ist."), + exceeded="Abschaltschwelle während der Verzögerung überschritten.", + not_charging=("Da das EV nicht lädt und die Abschaltschwelle erreicht wird, " + "wird die Ladefreigabe sofort entzogen."), + ) + SWITCH_OFF_TEXTS_CONSUMER = SwitchOffTexts( + stop="Verbraucher nach Ablauf der Wartezeit gestoppt.", + waiting="Verbraucher wird nach Ablauf der Wartezeit in {} gestoppt.", + waiting_min_interval="Betrieb wird nach Ablauf des minimalen Regelintervalls in {} gestoppt.", + no_stop="Der Betrieb wird trotz fehlenden Überschusses nicht gestoppt.", + exceeded="Abschaltschwelle während der Verzögerung überschritten.", + not_charging=("Da der Verbraucher nicht aktiv ist und die Abschaltschwelle erreicht wird, " + "wird die Freigabe sofort entzogen."), + ) + + def _get_switch_off_texts(self, load: Load) -> SwitchOffTexts: + if isinstance(load, Chargepoint): + return self.SWITCH_OFF_TEXTS_CP + return self.SWITCH_OFF_TEXTS_CONSUMER def _get_switch_off_delay_by_load(self, load: Load) -> float: if isinstance(load, Chargepoint): @@ -397,19 +467,25 @@ def _get_switch_off_delay_by_load(self, load: Load) -> float: def switch_off_check_timer(self, load: Load) -> None: try: msg = None + texts = self._get_switch_off_texts(load) control_parameter = load.data.control_parameter switch_off_delay = self._get_switch_off_delay_by_load(load) if control_parameter.timestamp_switch_on_off is not None: - if not timecheck.check_timestamp( + if (isinstance(load, Consumer) and + timecheck.create_timestamp() < (load.data.set.timestamp_last_current_set + + load.data.config.min_interval)): + msg = texts.waiting_min_interval.format(timecheck.convert_timestamp_delta_to_time_string( + load.data.set.timestamp_last_current_set, load.data.config.min_interval)) + elif not timecheck.check_timestamp( control_parameter.timestamp_switch_on_off, switch_off_delay): control_parameter.timestamp_switch_on_off = None self.data.set.released_surplus -= load.data.set.required_power - msg = self.SWITCH_OFF_STOP + msg = texts.stop control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED else: - msg = self.SWITCH_OFF_WAITING.format(timecheck.convert_timestamp_delta_to_time_string( + msg = texts.waiting.format(timecheck.convert_timestamp_delta_to_time_string( control_parameter.timestamp_switch_on_off, switch_off_delay)) load.set_state_and_log(msg) except Exception: @@ -442,6 +518,7 @@ def switch_off_check_threshold(self, load: Load) -> bool: """ charge = True msg = None + texts = self._get_switch_off_texts(load) if isinstance(load, Chargepoint): nominal_difference = load.data.set.charging_ev_data.ev_template.data.nominal_difference else: @@ -464,7 +541,7 @@ def switch_off_check_threshold(self, load: Load) -> bool: if power_in_use + load.data.set.required_power < threshold: timestamp_switch_on_off = None self.data.set.released_surplus -= load.data.set.required_power - msg = self.SWITCH_OFF_EXCEEDED + msg = texts.exceeded control_parameter.state = ChargepointState.CHARGING_ALLOWED else: # Wurde die Abschaltschwelle ggf. durch die Verzögerung anderer LP erreicht? @@ -487,19 +564,19 @@ def switch_off_check_threshold(self, load: Load) -> bool: # nicht 0 sein, wenn sich die Ladeleistung zwischendurch verändert hat. if load.data.get.charge_state is False: charge = False - msg = self.SWITCH_OFF_NOT_CHARGING + msg = texts.not_charging control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED else: timestamp_switch_on_off = timecheck.create_timestamp() # merken, dass ein LP verzögert wird, damit nicht zu viele LP verzögert werden. self.data.set.released_surplus += load.data.set.required_power - msg = self.SWITCH_OFF_WAITING.format(timecheck.convert_timestamp_delta_to_time_string( + msg = texts.waiting.format(timecheck.convert_timestamp_delta_to_time_string( timestamp_switch_on_off, switch_off_delay)) control_parameter.state = ChargepointState.SWITCH_OFF_DELAY # Die Abschaltschwelle wird immer noch überschritten und es sollten weitere LP abgeschaltet # werden. else: - msg = self.SWITCH_OFF_NO_STOP + msg = texts.no_stop if timestamp_switch_on_off != control_parameter.timestamp_switch_on_off: control_parameter.timestamp_switch_on_off = timestamp_switch_on_off load.set_state_and_log(msg) diff --git a/packages/control/counter_test.py b/packages/control/counter_test.py index fa671e29b8..ef1eaac040 100644 --- a/packages/control/counter_test.py +++ b/packages/control/counter_test.py @@ -111,20 +111,22 @@ class Params: cases = [ Params("Einschaltschwelle wurde unterschritten, Timer zurücksetzen", False, 1500, -119, 1500, 1652683250.0, ChargepointState.SWITCH_ON_DELAY, - Counter.SWITCH_ON_FALLEN_BELOW.format(1500), None, 0), + Counter.SWITCH_ON_TEXTS_CP.fallen_below.format(1500), None, 0), Params("Timer starten", False, 0, 1501, 1500, None, ChargepointState.NO_CHARGING_ALLOWED, - Counter.SWITCH_ON_WAITING.format("30 Sek."), 1652683252.0, 1500), + Counter.SWITCH_ON_TEXTS_CP.waiting.format("30 Sek."), 1652683252.0, 1500), Params("Einschaltschwelle nicht erreicht", False, 0, 1499, 1500, - None, ChargepointState.NO_CHARGING_ALLOWED, Counter.SWITCH_ON_NOT_EXCEEDED.format(1500), None, 0), + None, ChargepointState.NO_CHARGING_ALLOWED, Counter.SWITCH_ON_TEXTS_CP.not_exceeded, None, 0), Params("Einschaltschwelle läuft", False, 1500, 121, 1500, 1652683250.0, ChargepointState.SWITCH_ON_DELAY, None, 1652683250.0, 1500), Params("Feed_in_limit, Einschaltschwelle wurde unterschritten, Timer zurücksetzen", True, 1500, -681, 15000, 1652683250.0, ChargepointState.SWITCH_ON_DELAY, - Counter.SWITCH_ON_FALLEN_BELOW.format(1500), None, 0), + Counter.SWITCH_ON_TEXTS_CP.fallen_below.format(1500), None, 0), Params("Feed_in_limit, Timer starten", True, 0, 15001, 15000, None, ChargepointState.NO_CHARGING_ALLOWED, - Counter.SWITCH_ON_WAITING.format("30 Sek.")+" Die Einspeisegrenze wird berücksichtigt.", 1652683252.0, 1500), + Counter.SWITCH_ON_TEXTS_CP.waiting.format("30 Sek.")+" Die Einspeisegrenze wird berücksichtigt.", + 1652683252.0, 1500), Params("Feed_in_limit, Einschaltschwelle nicht erreicht", True, 0, 14999, - 15000, None, ChargepointState.NO_CHARGING_ALLOWED, Counter.SWITCH_ON_NOT_EXCEEDED.format(1500), None, 0), + 15000, None, ChargepointState.NO_CHARGING_ALLOWED, + Counter.SWITCH_ON_TEXTS_CP.not_exceeded.format(1500), None, 0), Params("Feed_in_limit, Einschaltschwelle läuft", True, 1500, 15001, 15000, 1652683250.0, ChargepointState.SWITCH_ON_DELAY, None, 1652683250.0, 1500), ] diff --git a/packages/control/data.py b/packages/control/data.py index 2a6f28c933..b5121a65cc 100644 --- a/packages/control/data.py +++ b/packages/control/data.py @@ -287,6 +287,8 @@ def print_all(self): log.info(f"cp_all_data\n{self._cp_all_data.data}") self._print_dictionaries(self._cp_data) self._print_dictionaries(self._cp_template_data) + log.info(f"consumer_all_data\n{self._consumer_all_data.data}") + self._print_dictionaries(self._consumer_data) self._print_dictionaries(self._counter_data) log.info(f"counter_all_data\n{self._counter_all_data.data}") self._print_dictionaries(self._ev_charge_template_data) @@ -295,8 +297,6 @@ def print_all(self): log.info(f"general_data\n{self._general_data.data}") log.info(f"general_data-display\n{self._general_data.data.extern_display_mode}") log.info(f"graph_data\n{self._graph_data.data}") - log.info(f"consumer_all_data\n{self._consumer_all_data.data}") - self._print_dictionaries(self._consumer_data) self._print_io_actions(self._io_actions) self._print_dictionaries(self._io_states) log.info(f"optional_data\n{self._optional_data.data}") diff --git a/packages/control/process.py b/packages/control/process.py index 193b626d4c..8269a9385b 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -8,9 +8,10 @@ from control.chargelog import chargelog from control.chargepoint import chargepoint from control import data -from control.chargepoint.chargepoint_state import ChargepointState +from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState from control.consumer.consumer import Consumer from control.consumer.usage import ConsumerUsage +from helpermodules import timecheck from helpermodules.phase_handling import voltages_mean from helpermodules.pub import Pub from helpermodules.utils._thread_handler import joined_thread_handler @@ -76,18 +77,13 @@ def process_algorithm_results(self) -> None: name=f"set power limit {bat_component.component_config.id}")) for consumer in data.data.consumer_data.values(): try: - control_parameter = consumer.data.control_parameter - if (control_parameter.state != ChargepointState.NO_CHARGING_ALLOWED or - consumer.data.set.current != 0): - control_parameter.state = ChargepointState.CHARGING_ALLOWED - else: - control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED self._update_state_consumer(consumer) if consumer.data.get.state_str is None: if consumer.data.get.charge_state: consumer.data.get.state_str = "Verbraucher läuft." else: consumer.data.get.state_str = "Verbraucher wird gestartet... " + modules_threads.append(self._start_consumer(consumer)) except Exception: log.exception("Fehler im Process-Modul für Verbaucher "+str(consumer)) @@ -171,13 +167,21 @@ def _start_charging(self, chargepoint: chargepoint.Chargepoint) -> Thread: name=f"set current cp{chargepoint.chargepoint_module.config.id}") def _update_state_consumer(self, consumer: Consumer) -> None: - consumer.data.set.current = round(consumer.data.set.current, 2) + if consumer.data.set.switch_interval_elapsed is False: + log.debug("Intervall für neuen Schaltbefehl nicht abgelaufen.") + consumer.data.set.current = consumer.data.set.current_prev + else: + consumer.data.set.current = round(consumer.data.set.current, 2) + if consumer.data.set.current != 0 and consumer.data.control_parameter.state not in CHARGING_STATES: + consumer.data.control_parameter.state = ChargepointState.CHARGING_ALLOWED consumer.data.set.power = consumer.data.set.current * \ voltages_mean(consumer.data.get.voltages) * consumer.data.config.connected_phases log.info(f"Verbraucher{consumer.num}: set current {consumer.data.set.current} A, " f"state {ChargepointState(consumer.data.control_parameter.state).name}") def _start_consumer(self, consumer: Consumer) -> Thread: + if consumer.data.set.current != consumer.data.set.current_prev: + consumer.data.set.timestamp_last_current_set = timecheck.create_timestamp() if consumer.data.usage.type in (ConsumerUsage.CONTINUOUS, ConsumerUsage.SUSPENDABLE_ONOFF): return Thread( diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 6e89a39879..65bec56e84 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -116,13 +116,9 @@ class TimeChargingPlanConsumer(TimeframePlan): @dataclass -class SuspendableScheduledPlanConsumer(TimeframePlan): - id: Optional[int] = None - name: str = "neuer Ziel-Plan" - duration: int = 60 - - -@dataclass -class ContinuousScheduledPlanConsumer(TimeframePlan): +class ScheduledPlanConsumer(PlanBase): + frequency: FrequencyDate = field(default_factory=frequency_date_factory) id: Optional[int] = None name: str = "neuer Ziel-Plan" + time: str = "07:00" + duration: int = 3600 diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index 145ecb0d88..f024910af9 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -17,15 +17,15 @@ 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 as ConsumerGet -from control.consumer.usage import ConsumerUsage +from control.consumer.consumer_data import Usage +from control.consumer.consumer_data import Get as ConsumerGet, Set as ConsumerSet 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, TimeChargingPlanConsumer +from helpermodules.abstract_plans import (AutolockPlan, ScheduledChargingPlan, + ScheduledPlanConsumer, 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 @@ -1156,9 +1156,12 @@ def addConsumer(self, connection_id: str, payload: dict) -> None: consumer_default["id"] = new_id Pub().pub(f'openWB/set/consumer/{new_id}/module', consumer_default) Pub().pub(f"openWB/set/consumer/{new_id}/config", dataclass_utils.asdict(ConsumerConfig())) - Pub().pub(f"openWB/set/consumer/{new_id}/get", dataclass_utils.asdict(ConsumerGet())) + for (k, v) in dataclass_utils.asdict(ConsumerGet()).items(): + Pub().pub(f"openWB/set/consumer/{new_id}/get/"+k, v) + for (k, v) in dataclass_utils.asdict(ConsumerSet()).items(): + Pub().pub(f"openWB/set/consumer/{new_id}/set/"+k, v) Pub().pub(f"openWB/set/consumer/{new_id}/extra_meter", None) - Pub().pub(f"openWB/set/consumer/{new_id}/usage", None) + Pub().pub(f"openWB/set/consumer/{new_id}/usage", dataclass_utils.asdict(Usage())) self.max_id_hierarchy = new_id Pub().pub("openWB/set/command/max_id/hierarchy", new_id) try: @@ -1200,18 +1203,6 @@ def removeConsumer(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, f'Verbraucher mit ID \'{payload["data"]["consumer_id"]}\' gelöscht.', MessageType.SUCCESS) - def selectUsage(self, connection_id: str, payload: dict) -> None: - """ weist einem Verbraucher eine Nutzung zu. - """ - usage = ConsumerUsage(payload["data"]["usage"]) - usage_config = GET_DEFAULTS_BY_USAGE[usage] - Pub().pub(f'openWB/set/consumer/{payload["data"]["consumer_id"]}/usage', dataclass_utils.asdict(usage_config)) - pub_user_message( - payload, connection_id, - f'Dem Verbraucher mit ID \'{payload["data"]["consumer_id"]}\' wurde die Nutzung ' - f'\'{payload["data"]["usage"]}\' zugewiesen.', - MessageType.SUCCESS) - def addConsumerSchedulePlan(self, connection_id: str, payload: dict) -> None: """ sendet das Topic, zu dem ein neuer Zielladen-Plan erstellt werden soll. """ @@ -1224,7 +1215,7 @@ def addConsumerSchedulePlan(self, connection_id: str, payload: dict) -> None: 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_consumer_schedule_plan = ScheduledPlanConsumer() 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) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 84a7a07d11..46cd8daa83 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -509,6 +509,21 @@ def calc_energy_imported_by_source(entry, names, message_key_filter: Optional[st if message_key_filter is None or message_key_filter == cp_key: message += ERROR_STATE_MESSAGE.format(f"Ladepunkt {names.get(cp_key, cp_key)}") + consumer_section = entry.get("consumer") + if isinstance(consumer_section, dict): + for consumer_key, consumer_data in consumer_section.items(): + if isinstance(consumer_data, dict): + if consumer_data.get("fault_state", 0) != 2 and "energy_imported" in consumer_data: + for source in ("grid", "pv", "bat", "cp"): + consumer_data[f"energy_imported_{source}"] = decimal_multiply( + consumer_data["energy_imported"], energy_source[source]) + else: + for source in ("grid", "pv", "bat", "cp"): + consumer_data[f"energy_imported_{source}"] = 0 + if message_key_filter is None or message_key_filter == consumer_key: + message += ERROR_STATE_MESSAGE.format( + f"Verbraucher {names.get(consumer_key, consumer_key)}") + counter_section = entry.get("counter") if isinstance(counter_section, dict): for counter_key, counter_data in counter_section.items(): @@ -530,7 +545,7 @@ def calc_energy_imported_by_source(entry, names, message_key_filter: Optional[st def analyse_percentage_totals(entries, totals): - for section in ("hc", "cp"): + for section in ("consumer", "cp", "hc"): if "all" not in totals[section].keys(): totals[section]["all"] = {} for source in ("grid", "pv", "bat", "cp"): @@ -549,6 +564,14 @@ def analyse_percentage_totals(entries, totals): add_value = entry["cp"][key][f"energy_imported_{source}"] totals["cp"][key][f"energy_imported_{source}"] = decimal_add( current_value, add_value) + for key in entry["consumer"].keys(): + if f"energy_imported_{source}" in entry["consumer"][key].keys(): + if totals["consumer"][key].get(f"energy_imported_{source}") is None: + totals["consumer"][key].update({f"energy_imported_{source}": 0}) + current_value = totals["consumer"][key][f"energy_imported_{source}"] + add_value = entry["consumer"][key][f"energy_imported_{source}"] + totals["consumer"][key][f"energy_imported_{source}"] = decimal_add( + current_value, add_value) for key, counter in entry["counter"].items(): if counter["grid"] is False: if totals["counter"][key].get(f"energy_imported_{source}") is None: diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index 4e1d01e4ef..edb22907f1 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -16,11 +16,10 @@ from control.chargepoint.chargepoint_data import Log from control.chargepoint.chargepoint_state_update import ChargepointStateUpdate from control.chargepoint.chargepoint_template import CpTemplate, CpTemplateData +from control.consumer.consumer_data import Usage from control.counter_all import counter_all from control.consumer.consumer import Consumer from control.consumer.consumer_all import AllConsumers -from control.consumer.consumer_data import GET_CLASS_BY_USAGE -from control.consumer.usage import ConsumerUsage from control.ev.charge_template import ChargeTemplate, ChargeTemplateData from control.ev import ev from control.ev.ev_template import EvTemplate, EvTemplateData @@ -1256,11 +1255,7 @@ def process_consumer_topic(self, client: mqtt.Client, var: Dict[str, Consumer], 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: - payload = decode_payload(msg.payload) - if payload is not None: - var[f"consumer{index}"].data.usage = dataclass_from_dict( - GET_CLASS_BY_USAGE[ConsumerUsage(payload["type"])], - payload) + var[f"consumer{index}"].data.usage = dataclass_from_dict(Usage, decode_payload(msg.payload)) elif re.search("openWB/consumer/[0-9]+/control_parameter/", msg.topic) is not None: if re.search("openWB/consumer/[0-9]+/control_parameter/limit", msg.topic) is not None: payload = decode_payload(msg.payload) diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index 1e8a3d7088..c0a86edea6 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -9,8 +9,8 @@ with ImportErrorContext(): from dateutil.relativedelta import relativedelta -from helpermodules.abstract_plans import (AutolockPlan, ContinuousScheduledPlanConsumer, ScheduledChargingPlan, - SuspendableScheduledPlanConsumer, TimeChargingPlan, TimeChargingPlanConsumer) +from helpermodules.abstract_plans import (AutolockPlan, ScheduledChargingPlan, ScheduledPlanConsumer, + TimeChargingPlan, TimeChargingPlanConsumer) log = logging.getLogger(__name__) @@ -114,9 +114,7 @@ def is_timeframe_valid(now: datetime.datetime, begin: datetime.datetime, end: da return state -def check_end_time(plan: Union[ContinuousScheduledPlanConsumer, - ScheduledChargingPlan, - SuspendableScheduledPlanConsumer], +def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, TimeChargingPlanConsumer], buffer: Optional[float]) -> float: """ gibt die verbleibende Zeit in Sekunden zurück. diff --git a/packages/modules/common/store/_consumer.py b/packages/modules/common/store/_consumer.py index b2120d9f63..e845b76acb 100644 --- a/packages/modules/common/store/_consumer.py +++ b/packages/modules/common/store/_consumer.py @@ -1,6 +1,5 @@ -from typing import Union from control import data -from modules.common.component_state import ConsumerState, CounterState +from modules.common.component_state import ConsumerState from modules.common.store import ValueStore from modules.common.store._api import LoggingValueStore from modules.common.store._broker import pub_to_broker @@ -11,23 +10,21 @@ class ConsumerValueStoreBroker(ValueStore[ConsumerState]): def __init__(self, component_num: int) -> None: self.num = component_num - def set(self, consumer_state: Union[ConsumerState, CounterState]): - self.state = consumer_state + def set(self, state: ConsumerState) -> None: + self.state = state - def update(self): + def update(self) -> None: if self.state.currents is not None: pub_to_broker(f"openWB/set/consumer/{self.num}/get/currents", self.state.currents, 2) if self.state.powers is not None: pub_to_broker(f"openWB/set/consumer/{self.num}/get/powers", self.state.powers, 2) if self.state.power is not None: pub_to_broker(f"openWB/set/consumer/{self.num}/get/power", self.state.power, 2) - if isinstance(self.state, ConsumerState): - if self.state.set_power is not None: - pub_to_broker(f"openWB/set/consumer/{self.num}/get/set_power", self.state.set_power, 2) - if self.state.state is not None: - pub_to_broker(f"openWB/set/consumer/{self.num}/get/state", self.state.state, 2) - if self.state.voltages is not None: - pub_to_broker(f"openWB/set/consumer/{self.num}/get/voltages", self.state.voltages, 2) + if self.state.set_power is not None: + pub_to_broker(f"openWB/set/consumer/{self.num}/get/set_power", self.state.set_power, 2) + if self.state.state is not None: + pub_to_broker(f"openWB/set/consumer/{self.num}/get/state", self.state.state, 2) + pub_to_broker(f"openWB/set/consumer/{self.num}/get/voltages", self.state.voltages, 2) if self.state.imported is not None and self.state.exported is not None: pub_to_broker(f"openWB/set/consumer/{self.num}/get/imported", self.state.imported, 2) pub_to_broker(f"openWB/set/consumer/{self.num}/get/exported", self.state.exported, 2) @@ -35,8 +32,8 @@ def update(self): pub_to_broker(f"openWB/set/consumer/{self.num}/get/temperatures", self.state.temperatures) -class PurgeConsumerState: - def __init__(self, delegate: LoggingValueStore) -> None: +class PurgeConsumerState(ValueStore[ConsumerState]): + def __init__(self, delegate: LoggingValueStore[ConsumerState]) -> None: self.delegate = delegate def set(self, state: ConsumerState) -> None: @@ -45,16 +42,20 @@ def set(self, state: ConsumerState) -> None: def update(self) -> None: extra_meter_id = data.data.consumer_data[f"consumer{self.delegate.delegate.num}"].data.extra_meter if extra_meter_id is not None: - component = get_component_obj_by_id(extra_meter_id) - component_state = component.store.delegate.state - self.set(ConsumerState( - power=component_state.power, - imported=component_state.imported, - exported=component_state.exported, - voltages=component_state.voltages, - currents=component_state.currents, - powers=component_state.powers, - )) + try: + component = get_component_obj_by_id(extra_meter_id) + component_state = component.store.delegate.delegate.state + self.set(ConsumerState( + power=component_state.power, + imported=component_state.imported, + exported=component_state.exported, + voltages=component_state.voltages, + currents=component_state.currents, + powers=component_state.powers, + )) + except Exception: + raise Exception(f"Fehler beim Auslesen des Verbrauchszählers {extra_meter_id} " + f"für Verbraucher {self.delegate.delegate.num}") self.delegate.update() diff --git a/packages/modules/consumers/askoma/askoheat/consumer.py b/packages/modules/consumers/askoma/askoheat/consumer.py index dc906c8a5a..71d9728a2b 100644 --- a/packages/modules/consumers/askoma/askoheat/consumer.py +++ b/packages/modules/consumers/askoma/askoheat/consumer.py @@ -24,7 +24,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter power = client.read_input_registers(110, ModbusDataType.INT_16, unit=config.configuration.modbus_id) imported, exported = sim_counter.sim_count(power) return ConsumerState( @@ -35,8 +34,7 @@ def update() -> ConsumerState: ) def set_limit(power_limit: float) -> None: - nonlocal client - client.write_registers(201, power_limit, unit=config.configuration.modbus_id) + client.write_register(201, power_limit, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, initializer=initializer, diff --git a/packages/modules/consumers/avm/avm/consumer.py b/packages/modules/consumers/avm/avm/consumer.py index 18c2d57823..5b28d073c3 100644 --- a/packages/modules/consumers/avm/avm/consumer.py +++ b/packages/modules/consumers/avm/avm/consumer.py @@ -112,14 +112,12 @@ def get_session_id(): return session_id def switch_on() -> None: - nonlocal ain ensure_valid_session_id() req.get_http_session().get( f"http://{config.configuration.ip_address}/webservices/homeautoswitch.lua?sid=" f"{config.configuration.session_id}&switchcmd=setswitchon&ain={ain}") def switch_off() -> None: - nonlocal ain ensure_valid_session_id() req.get_http_session().get( f"http://{config.configuration.ip_address}/webservices/homeautoswitch.lua?sid=" diff --git a/packages/modules/consumers/generic/dac/consumer.py b/packages/modules/consumers/generic/dac/consumer.py index 88f34476c6..654f2ab1c5 100644 --- a/packages/modules/consumers/generic/dac/consumer.py +++ b/packages/modules/consumers/generic/dac/consumer.py @@ -24,7 +24,6 @@ def error_handler() -> None: initializer() def set_limit(power_limit: float) -> None: - nonlocal client if config.configuration.model == Model.N4Dac02 or config.configuration.model == Model.M120T: # pwoer_limit -= power pass diff --git a/packages/modules/consumers/generic/http/config.py b/packages/modules/consumers/generic/http/config.py index 8c288086c1..a35d62a217 100644 --- a/packages/modules/consumers/generic/http/config.py +++ b/packages/modules/consumers/generic/http/config.py @@ -40,6 +40,9 @@ def __init__(self, type: str = "http", id: int = 0, configuration: HttpConfiguration = None, - usage: List[ConsumerUsage] = [ConsumerUsage.METER_ONLY]) -> None: + usage: List[ConsumerUsage] = [ConsumerUsage.METER_ONLY, + ConsumerUsage.CONTINUOUS, + ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE]) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( ).type, configuration=configuration or HttpConfiguration(), usage=usage) diff --git a/packages/modules/consumers/generic/http/consumer.py b/packages/modules/consumers/generic/http/consumer.py index a7815ef6e3..080e4983ad 100644 --- a/packages/modules/consumers/generic/http/consumer.py +++ b/packages/modules/consumers/generic/http/consumer.py @@ -48,7 +48,6 @@ def initializer(): post_switch_off = create_post_function(config.configuration.url, config.configuration.switch_off_path) def update() -> None: - nonlocal session, sim_counter, get_power, get_imported, get_exported, get_currents, get_temperatures power = get_power(session) exported = get_exported(session) imported = get_imported(session) @@ -65,17 +64,14 @@ def update() -> None: ) def switch_on(): - nonlocal session, post_switch_on # Authorization? post_switch_on(session, params={"state": True}) def switch_off(): - nonlocal session, post_switch_off # Authorization? post_switch_off(session, params={"state": False}) def set_power_limit(power_limit: int): - nonlocal session, post_set_power_limit # Authorization? post_set_power_limit(session, params={"power_limit": power_limit}) diff --git a/packages/modules/consumers/generic/json/config.py b/packages/modules/consumers/generic/json/config.py index 7733bbfa2a..feb0f1ce18 100644 --- a/packages/modules/consumers/generic/json/config.py +++ b/packages/modules/consumers/generic/json/config.py @@ -40,6 +40,9 @@ def __init__(self, type: str = "json", id: int = 0, configuration: JsonConfiguration = None, - usage: List[ConsumerUsage] = [ConsumerUsage.METER_ONLY]) -> None: + usage: List[ConsumerUsage] = [ConsumerUsage.METER_ONLY, + ConsumerUsage.CONTINUOUS, + ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE]) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( ).type, configuration=configuration or JsonConfiguration(), usage=usage) diff --git a/packages/modules/consumers/generic/json/consumer.py b/packages/modules/consumers/generic/json/consumer.py index 93cfe5cc50..5d765dd3eb 100644 --- a/packages/modules/consumers/generic/json/consumer.py +++ b/packages/modules/consumers/generic/json/consumer.py @@ -37,7 +37,6 @@ def _compile_jq_filters() -> None: config.configuration.jq_temperatures) if config.configuration.jq_temperatures else None def create_post_function(path: Optional[str]) -> Callable[[dict], None]: - nonlocal session if path is None: return lambda _: None else: @@ -59,7 +58,6 @@ def initializer(): jq_switch_off = create_post_function(config.configuration.jq_switch_off) def update() -> None: - nonlocal session, sim_counter, jq_power, jq_imported, jq_exported, jq_currents, jq_temperatures response = req.get_http_session().get(config.configuration.url, timeout=5).json() power = float(jq_power.input(response).first()) temperatures = float(jq_temperatures.input(response).first()) if jq_temperatures is not None else None diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index 0ac9fbf973..2c590bb21e 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -25,7 +25,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter if config.configuration.version == 1: power = client.read_holding_registers(4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index 0b3da7745f..83063bbb0b 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -25,7 +25,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter power = client.read_holding_registers(103, ModbusDataType.INT_16, unit=config.configuration.modbus_id) imported, exported = sim_counter.sim_count(power) return ConsumerState( @@ -35,7 +34,6 @@ def update() -> ConsumerState: ) def set_limit(power_limit: float) -> None: - nonlocal client client.write_register(102, max(power_limit * config.configuration.sign, 0), wordorder=Endian.Little, diff --git a/packages/modules/consumers/my_pv/acthor/consumer.py b/packages/modules/consumers/my_pv/acthor/consumer.py index 455f5b0f0b..f35b96d8dc 100644 --- a/packages/modules/consumers/my_pv/acthor/consumer.py +++ b/packages/modules/consumers/my_pv/acthor/consumer.py @@ -50,7 +50,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter resp = client.read_holding_registers_bulk( Register.POWER, 35, mapping=REG_MAPPING, unit=config.configuration.modbus_id) power = resp[Register.POWER] @@ -63,7 +62,6 @@ def update() -> ConsumerState: ) def set_limit(power_limit: float) -> None: - nonlocal client power_limit = power_limit * FACTORS.get(config.configuration.model, 9000)/config.configuration.max_power client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, diff --git a/packages/modules/consumers/my_pv/elwa_e/consumer.py b/packages/modules/consumers/my_pv/elwa_e/consumer.py index ad28612b4d..779fb41c6b 100644 --- a/packages/modules/consumers/my_pv/elwa_e/consumer.py +++ b/packages/modules/consumers/my_pv/elwa_e/consumer.py @@ -55,7 +55,7 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter, status + nonlocal status resp = client.read_holding_registers_bulk( Register.POWER, 4, mapping=REG_MAPPING, unit=config.configuration.modbus_id) power = resp[Register.POWER] @@ -71,7 +71,6 @@ def update() -> ConsumerState: ) def set_limit(power_limit: float) -> None: - nonlocal client, fuse, power, status if status == 4: log.debug("Elwa-E im Boost-Heat Modus, keine Leistungsvorgabe möglich") return diff --git a/packages/modules/consumers/mystrom/mystrom/consumer.py b/packages/modules/consumers/mystrom/mystrom/consumer.py index 11844537f8..82b07b13ad 100644 --- a/packages/modules/consumers/mystrom/mystrom/consumer.py +++ b/packages/modules/consumers/mystrom/mystrom/consumer.py @@ -24,7 +24,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal session, sim_counter resp = session.get(f"http://{config.configuration.ip_address}/report", timeout=3).json() power = resp["power"] relais = resp["relay"] @@ -39,11 +38,9 @@ def update() -> ConsumerState: ) def switch_on() -> None: - nonlocal session session.get(f"http://{config.configuration.ip_address}/relay?state=1", timeout=3) def switch_off() -> None: - nonlocal session session.get(f"http://{config.configuration.ip_address}/relay?state=0", timeout=3) return ConfigurableConsumer(consumer_config=config, diff --git a/packages/modules/consumers/nibe/nibe_s_series/consumer.py b/packages/modules/consumers/nibe/nibe_s_series/consumer.py index 4958de5d59..bf2142caf5 100644 --- a/packages/modules/consumers/nibe/nibe_s_series/consumer.py +++ b/packages/modules/consumers/nibe/nibe_s_series/consumer.py @@ -24,7 +24,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter power = client.read_input_registers(2166, ModbusDataType.UINT_32, unit=config.configuration.modbus_id) # / 10 lt Doku? imported, exported = sim_counter.sim_count(power) diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py index 8b6d0e10a9..cd8cd4f06c 100644 --- a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py +++ b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py @@ -23,7 +23,6 @@ def error_handler() -> None: initializer() def set_limit(power_limit: float) -> None: - nonlocal client # Absturz bei negativen Zahlen client.write_register(100, max(power_limit, 0), ModbusDataType.INT_16, unit=config.configuration.modbus_id) diff --git a/packages/modules/consumers/shelly/shelly_pm/consumer.py b/packages/modules/consumers/shelly/shelly_pm/consumer.py index adb68ea3bb..e188f233a9 100644 --- a/packages/modules/consumers/shelly/shelly_pm/consumer.py +++ b/packages/modules/consumers/shelly/shelly_pm/consumer.py @@ -16,11 +16,9 @@ def error_handler() -> None: initializer() def switch_on() -> None: - nonlocal client client.write_coil(16, True, unit=config.configuration.modbus_id) def switch_off() -> None: - nonlocal client client.write_coil(16, False, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, diff --git a/packages/modules/consumers/solvis/solvis/consumer.py b/packages/modules/consumers/solvis/solvis/consumer.py index d6244a7b8f..3830a9d2f4 100644 --- a/packages/modules/consumers/solvis/solvis/consumer.py +++ b/packages/modules/consumers/solvis/solvis/consumer.py @@ -21,7 +21,6 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal client, sim_counter power = client.read_input_registers(33545, ModbusDataType.INT_16, unit=config.configuration.modbus_id) * 100 imported, exported = sim_counter.sim_count(power) return ConsumerState( diff --git a/packages/modules/consumers/stiebel/stiebel/consumer.py b/packages/modules/consumers/stiebel/stiebel/consumer.py index c3d6d1a0d0..c7d39a3a1e 100644 --- a/packages/modules/consumers/stiebel/stiebel/consumer.py +++ b/packages/modules/consumers/stiebel/stiebel/consumer.py @@ -23,11 +23,9 @@ def error_handler() -> None: initializer() def switch_on() -> None: - nonlocal client client.write_register(4001, 1, ModbusDataType.INT_16, unit=config.configuration.modbus_id) def switch_off() -> None: - nonlocal client client.write_register(4001, 0, ModbusDataType.INT_16, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, diff --git a/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py index e09b65ca5d..cac0876d05 100644 --- a/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py +++ b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py @@ -16,11 +16,9 @@ def error_handler() -> None: initializer() def switch_on() -> None: - nonlocal client client.write_single_coil(16, True, unit=config.configuration.modbus_id) def switch_off() -> None: - nonlocal client client.write_single_coil(16, False, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config,