From 10c8fb690b1828cb35f028dbef8461457adba37d Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 27 Aug 2026 12:48:22 +0200 Subject: [PATCH 1/5] dimm kit: improve error handling, handle deleted output devices --- packages/control/io_device.py | 69 ++++++++++++------- packages/control/process.py | 55 ++++++++------- packages/helpermodules/exceptions/registry.py | 3 +- packages/modules/common/fault_state.py | 30 +++++++- packages/modules/io_actions/common.py | 23 +++++++ .../dimming/api_eebus.py | 11 +-- .../controllable_consumers/dimming/api_io.py | 11 +-- .../dimming_direct_control/api.py | 10 +-- .../ripple_control_receiver/api.py | 11 +-- .../stepwise_control/api_eebus.py | 15 ++-- .../stepwise_control/api_io.py | 16 +++-- 11 files changed, 158 insertions(+), 96 deletions(-) diff --git a/packages/control/io_device.py b/packages/control/io_device.py index 1b2b4839f7..8eacfffc14 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -1,7 +1,10 @@ from dataclasses import dataclass, field from typing import Dict, Optional, Tuple, Union +from control import data from control.limiting_value import LoadmanagementLimit from helpermodules.constants import NO_ERROR +from modules.common.configurable_io import ConfigurableIo +from modules.common.fault_state import FaultStateContext from modules.io_actions.controllable_consumers.dimming.api_eebus import DimmingEebus from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo @@ -63,47 +66,63 @@ def __init__(self): def setup(self): for action in self.actions.values(): - action.setup() + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + action.setup() - def dimming_get_import_power_left(self, device: Dict) -> Tuple[Optional[float], LoadmanagementLimit]: + def dimming_get_import_power_left(self, + device: Dict[str, Union[int, str]] + ) -> Tuple[Optional[float], LoadmanagementLimit]: for action in self.actions.values(): - if isinstance(action, (DimmingIo, DimmingEebus)): - for d in action.config.configuration.devices: - if device == d: - return action.dimming_get_import_power_left() + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + if isinstance(action, (DimmingIo, DimmingEebus)): + for d in action.config.configuration.devices: + if device == d: + return action.dimming_get_import_power_left() else: return None, LoadmanagementLimit(None, None) - def dimming_set_import_power_left(self, device: Dict, used_power: float) -> Optional[float]: + def dimming_set_import_power_left(self, + device: Dict[str, Union[int, str]], used_power: float) -> Optional[float]: for action in self.actions.values(): - if isinstance(action, (DimmingIo, DimmingEebus)): - for d in action.config.configuration.devices: - if d == device: - return action.dimming_set_import_power_left(used_power) - - def dimming_via_direct_control(self, device: Dict) -> Tuple[Optional[float], LoadmanagementLimit]: + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + if isinstance(action, (DimmingIo, DimmingEebus)): + for d in action.config.configuration.devices: + if d == device: + return action.dimming_set_import_power_left(used_power) + + def dimming_via_direct_control(self, + device: Dict[str, Union[int, str]]) -> Tuple[Optional[float], LoadmanagementLimit]: for action in self.actions.values(): - if isinstance(action, DimmingDirectControl): - for d in action.config.configuration.devices: - if device == d: - return action.dimming_via_direct_control() + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + if isinstance(action, DimmingDirectControl): + for d in action.config.configuration.devices: + if device == d: + return action.dimming_via_direct_control() else: return None, LoadmanagementLimit(None, None) - def ripple_control_receiver(self, device: Dict) -> Tuple[float, LoadmanagementLimit]: + def ripple_control_receiver(self, device: Dict[str, Union[int, str]]) -> Tuple[float, LoadmanagementLimit]: for action in self.actions.values(): - if isinstance(action, RippleControlReceiver): - for d in action.config.configuration.devices: - if device == d: - return action.ripple_control_receiver() + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + if isinstance(action, RippleControlReceiver): + for d in action.config.configuration.devices: + if device == d: + return action.ripple_control_receiver() else: return 1, LoadmanagementLimit(None, None) def stepwise_control(self, device_id: int) -> Tuple[Optional[float], LoadmanagementLimit]: for action in self.actions.values(): - if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)): - if device_id in [component["id"] for component in action.config.configuration.devices]: - return action.control_stepwise() + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)): + if device_id in [component["id"] for component in action.config.configuration.devices]: + return action.control_stepwise() else: return None, LoadmanagementLimit(None, None) diff --git a/packages/control/process.py b/packages/control/process.py index 09abe7ab02..0ddc27403b 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -19,6 +19,7 @@ from modules.common.abstract_io import AbstractIoDevice from modules.common.configurable_consumer import SetLimitData from modules.common.configurable_device import set_power_limit_wrapper +from modules.common.fault_state import FaultStateContext from modules.common.fault_state_level import FaultStateLevel from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl @@ -97,30 +98,36 @@ def process_algorithm_results(self) -> None: except Exception: log.exception("Fehler im Process-Modul für Verbaucher "+str(consumer)) for action in data.data.io_actions.actions.values(): - if isinstance(action, DimmingDirectControl): - for d in action.config.configuration.devices: - if d["type"] == "io": - data.data.io_states[f"io_states{d['id']}"].data.set.digital_output[d["digital_output"]] = ( - action.dimming_via_direct_control()[0] is None # active output (True) if no dimming - ) - if isinstance(action, DimmingIo): - for d in action.config.configuration.devices: - if d["type"] == "io": - data.data.io_states[f"io_states{d['id']}"].data.set.digital_output[d["digital_output"]] = ( - not action.dimming_active() # active output (True) if no dimming - ) - if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)): - # check if passthrough is enabled - if (action.config.configuration.passthrough_enabled and - action.config.configuration.io_output_device is not None): - # find output pattern by value - for pattern in action.config.configuration.output_pattern: - if pattern["value"] == action.control_stepwise()[0]: - # set digital outputs according to matching output_pattern - for output in pattern["matrix"].keys(): - data.data.io_states[ - f"io_states{action.config.configuration.io_output_device}" - ].data.set.digital_output[output] = pattern["matrix"][output] + io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + with FaultStateContext(io_device.fault_state, update_always=False): + try: + if isinstance(action, DimmingDirectControl): + for d in action.config.configuration.devices: + if d["type"] == "io": + digital_output = data.data.io_states[f"io_states{d['id']}"].data.set.digital_output + # active output (True) if no dimming + digital_output[d["digital_output"]] = action.dimming_via_direct_control()[0] is None + if isinstance(action, DimmingIo): + for d in action.config.configuration.devices: + if d["type"] == "io": + digital_output = data.data.io_states[f"io_states{d['id']}"].data.set.digital_output + # active output (True) if no dimming + digital_output[d["digital_output"]] = not action.dimming_active() + if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)): + # check if passthrough is enabled + if (action.config.configuration.passthrough_enabled and + action.config.configuration.io_output_device is not None): + # find output pattern by value + for pattern in action.config.configuration.output_pattern: + if pattern["value"] == action.control_stepwise()[0]: + # set digital outputs according to matching output_pattern + for output in pattern["matrix"].keys(): + data.data.io_states[ + f"io_states{action.config.configuration.io_output_device}" + ].data.set.digital_output[output] = pattern["matrix"][output] + except KeyError as e: + raise KeyError(f"Ausgang konnte für die Aktion {action.config.name} nicht zugeordnet werden. " + "Bitte prüfen Sie die Konfiguration.") from e for io in data.data.system_data.values(): if isinstance(io, AbstractIoDevice): modules_threads.append( diff --git a/packages/helpermodules/exceptions/registry.py b/packages/helpermodules/exceptions/registry.py index b83987142c..b849864530 100644 --- a/packages/helpermodules/exceptions/registry.py +++ b/packages/helpermodules/exceptions/registry.py @@ -25,7 +25,8 @@ class ExceptionRegistry: def translate_exception(self, exception: Exception) -> Tuple[str, FaultStateLevel]: entry = self.find_registry_entry(exception) if entry is None: - return str(exception.args[0]), FaultStateLevel.ERROR + message = str(exception.args[0]) if exception.args else str(exception) + return message, FaultStateLevel.ERROR if isinstance(entry.handler, str): return entry.handler, FaultStateLevel.ERROR result = entry.handler(exception) diff --git a/packages/modules/common/fault_state.py b/packages/modules/common/fault_state.py index 854787f104..cc8f1a2139 100644 --- a/packages/modules/common/fault_state.py +++ b/packages/modules/common/fault_state.py @@ -1,6 +1,7 @@ import logging import traceback -from typing import Optional, Callable, TypeVar +from types import TracebackType +from typing import Optional, Callable, Type, TypeVar from helpermodules import exceptions from helpermodules.pub import Pub @@ -87,4 +88,31 @@ def from_exception(self, exception: Optional[Exception] = None) -> None: exception) +class FaultStateContext: + def __init__(self, fault_state: FaultState, update_always: bool = True, reraise: bool = False) -> None: + self.__fault_state = fault_state + self.update_always = update_always + self.reraise = reraise + + def __enter__(self) -> None: + if self.update_always: + self.__fault_state.no_error() + return None + + def __exit__(self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType]) -> bool: + if isinstance(exc_value, Exception): + self.__fault_state.from_exception(exc_value) + elif self.update_always is False and self.__fault_state.fault_state == 0: + # Fehlerstatus nicht überschreiben + return True + self.__fault_state.store_error() + if self.reraise is False or exc_value is None: + return True + else: + return False + + T_C = TypeVar("T_C", bound=Callable) diff --git a/packages/modules/io_actions/common.py b/packages/modules/io_actions/common.py index fc448436a6..c92e38bcd5 100644 --- a/packages/modules/io_actions/common.py +++ b/packages/modules/io_actions/common.py @@ -1,6 +1,29 @@ +import logging +from typing import Dict, Union + from control import data from modules.common.fault_state_level import FaultStateLevel +control_command_log = logging.getLogger("steuve_control_command") + + def check_fault_state_io_device(io_device: int) -> bool: return data.data.io_states[f"io_states{io_device}"].data.get.fault_state == FaultStateLevel.ERROR + + +def get_device_log_message(device: Dict[str, Union[int, str]]) -> str: + try: + if device["type"] == "cp": + cp = f"cp{device['id']}" + return (f"Ladepunkt {data.data.cp_data[cp].data.config.name}: " + f"{data.data.cp_data[cp].data.get.powers}W, ") + if device["type"] == "io": + io = f"io{device['id']}" + return (f"{data.data.system_data[io].config.name}: " + "Leistung unbekannt, ") + except KeyError: + control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") + except Exception: + control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") + return "Unbekanntes Gerät, " diff --git a/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py b/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py index 1115671987..ea67a880aa 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py @@ -9,7 +9,7 @@ from dataclass_utils import asdict from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device +from modules.io_actions.common import check_fault_state_io_device, get_device_log_message from modules.io_actions.controllable_consumers.dimming.config import DimmingSetup from modules.io_actions.controllable_consumers.dimming.utils import calc_dimming_surplus from modules.io_devices.eebus.config import AnalogInputMapping, DigitalInputMapping @@ -64,14 +64,7 @@ def setup(self) -> None: evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" for device in self.config.configuration.devices: - if device["type"] == "cp": - cp = f"cp{device['id']}" - msg += (f", Ladepunkt {data.data.cp_data[cp].data.config.name}: " - f"{data.data.cp_data[cp].data.get.powers}W") - if device["type"] == "io": - io = f"io{device['id']}" - msg += (f", {data.data.system_data[io].config.name}: " - "Leistung unbekannt") + msg += get_device_log_message(device) control_command_log.info(msg) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) diff --git a/packages/modules/io_actions/controllable_consumers/dimming/api_io.py b/packages/modules/io_actions/controllable_consumers/dimming/api_io.py index 1b1aee08f5..69b64903c4 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/api_io.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/api_io.py @@ -9,7 +9,7 @@ from dataclass_utils import asdict from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device +from modules.io_actions.common import check_fault_state_io_device, get_device_log_message from modules.io_actions.controllable_consumers.dimming.config import DimmingSetup from modules.io_actions.controllable_consumers.dimming.utils import calc_dimming_surplus @@ -62,14 +62,7 @@ def setup(self) -> None: evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" for device in self.config.configuration.devices: - if device["type"] == "cp": - cp = f"cp{device['id']}" - msg += (f", Ladepunkt {data.data.cp_data[cp].data.config.name}: " - f"{data.data.cp_data[cp].data.get.powers}W") - if device["type"] == "io": - io = f"io{device['id']}" - msg += (f", {data.data.system_data[io].config.name}: " - "Leistung unbekannt") + msg += get_device_log_message(device) control_command_log.info(msg) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) diff --git a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py index 22a821e819..e869a23ef7 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py +++ b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device +from modules.io_actions.common import check_fault_state_io_device, get_device_log_message from modules.io_actions.controllable_consumers.dimming_direct_control.config import DimmingDirectControlSetup control_command_log = logging.getLogger("steuve_control_command") @@ -51,13 +51,7 @@ def setup(self) -> None: evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" - if device["type"] == "cp": - msg += (f", Ladepunkt {data.data.cp_data[cp].data.config.name}: " - f"{data.data.cp_data[cp].data.get.powers}W") - if device["type"] == "io": - io = f"io{device['id']}" - msg += (f", IO-Gerät {data.data.system_data[io].config.name}: " - "Leistung unbekannt") + msg += get_device_log_message(device) control_command_log.info(msg) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) diff --git a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py index 5fd6b787bb..fc2f4347d1 100644 --- a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py +++ b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device +from modules.io_actions.common import check_fault_state_io_device, get_device_log_message from modules.io_actions.controllable_consumers.ripple_control_receiver.config import RippleControlReceiverSetup control_command_log = logging.getLogger("steuve_control_command") @@ -33,14 +33,7 @@ def log_active_ripple_control_receiver(): evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" for device in self.config.configuration.devices: - if device["type"] == "cp": - cp = f"cp{device['id']}" - msg += (f", Ladepunkt {data.data.cp_data[cp].data.config.name}: " - f"{data.data.cp_data[cp].data.get.powers}W") - if device["type"] == "io": - io = f"io{device['id']}" - msg += (f", IO-Gerät {data.data.io_data[io].data.config.name}: " - "Leistung unbekannt") + msg += get_device_log_message(device) control_command_log.info(msg) with ModifyLoglevelContext(control_command_log, logging.DEBUG): diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py index 03a554c2a3..190e559917 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py @@ -92,11 +92,16 @@ def setup(self) -> None: control_command_log.info( f"EEBus-Steuerung: EZA-Begrenzung mit LPP-Wert {self.lpp_value}W aktiviert.") for device in self.config.configuration.devices: - control_command_log.info( - f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " - f"auf {self.lpp_value}W begrenzt. Gestufte Ansteuerung: " - f"{self.step*100:.0f}% der maximalen Ausgangsleistung." - ) + try: + control_command_log.info( + f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " + f"auf {self.lpp_value}W begrenzt. Gestufte Ansteuerung: " + f"{self.step*100:.0f}% der maximalen Ausgangsleistung." + ) + except ValueError: + control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") + except Exception: + control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") else: self.step = 1 if changed: diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py index 34fc18806f..44da029a89 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py @@ -74,11 +74,17 @@ def setup(self) -> None: control_command_log.info( f"EZA-Begrenzung mit Wert {int(pattern['value']*100)}% aktiviert.") for device in self.config.configuration.devices: - if device["type"] == "inverter": - control_command_log.info( - f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " - f"auf {int(pattern['value']*100)}% begrenzt." - ) + try: + if device["type"] == "inverter": + control_command_log.info( + f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " + f"auf {int(pattern['value']*100)}% begrenzt." + ) + except ValueError: + control_command_log.warning( + f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") + except Exception: + control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") break else: if changed: From 3014a5565f2cfc82c22feafac62734c1f3f928b1 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 17 Sep 2026 12:08:44 +0200 Subject: [PATCH 2/5] review, complete logging --- packages/control/io_device.py | 45 ++++++---- packages/control/process.py | 3 +- packages/modules/common/fault_state.py | 7 +- packages/modules/io_actions/common.py | 22 +++-- .../dimming/api_eebus.py | 8 +- .../controllable_consumers/dimming/api_io.py | 8 +- .../dimming_direct_control/api.py | 29 ++++--- .../ripple_control_receiver/api.py | 8 +- .../stepwise_control/api_eebus.py | 74 ++++++++-------- .../stepwise_control/api_io.py | 87 ++++++++++--------- 10 files changed, 157 insertions(+), 134 deletions(-) diff --git a/packages/control/io_device.py b/packages/control/io_device.py index 8eacfffc14..74399b4e2c 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -1,5 +1,6 @@ from dataclasses import dataclass, field -from typing import Dict, Optional, Tuple, Union +import logging +from typing import Dict, Iterator, Optional, Tuple, Union from control import data from control.limiting_value import LoadmanagementLimit from helpermodules.constants import NO_ERROR @@ -15,6 +16,18 @@ from modules.io_actions.generator_systems.stepwise_control.api_eebus import StepwiseControlEebus from modules.io_actions.generator_systems.stepwise_control.api_io import StepwiseControlIo +log = logging.getLogger(__name__) + +IoAction = Union[ + DimmingIo, + DimmingEebus, + DimmingDirectControl, + RippleControlReceiver, + StepwiseControlEebus, + StepwiseControlIo, + LoadManager, +] + @dataclass class Get: @@ -60,21 +73,25 @@ def __init__(self, num: Union[int, str]): class IoActions: def __init__(self): - self.actions: Dict[int, Union[DimmingIo, DimmingEebus, DimmingDirectControl, - RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo, - LoadManager]] = {} + self.actions: Dict[int, IoAction] = {} - def setup(self): + def iter_actions_with_io_device(self) -> Iterator[Tuple[IoAction, ConfigurableIo]]: for action in self.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + io_device = data.data.system_data.get(f"io{action.config.configuration.io_device}") + if not isinstance(io_device, ConfigurableIo): + log.error(f"I/O device io{action.config.configuration.io_device} not found.") + continue + yield action, io_device + + def setup(self): + for action, io_device in self.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): action.setup() def dimming_get_import_power_left(self, device: Dict[str, Union[int, str]] ) -> Tuple[Optional[float], LoadmanagementLimit]: - for action in self.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + for action, io_device in self.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): if isinstance(action, (DimmingIo, DimmingEebus)): for d in action.config.configuration.devices: @@ -85,8 +102,7 @@ def dimming_get_import_power_left(self, def dimming_set_import_power_left(self, device: Dict[str, Union[int, str]], used_power: float) -> Optional[float]: - for action in self.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + for action, io_device in self.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): if isinstance(action, (DimmingIo, DimmingEebus)): for d in action.config.configuration.devices: @@ -95,8 +111,7 @@ def dimming_set_import_power_left(self, def dimming_via_direct_control(self, device: Dict[str, Union[int, str]]) -> Tuple[Optional[float], LoadmanagementLimit]: - for action in self.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + for action, io_device in self.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): if isinstance(action, DimmingDirectControl): for d in action.config.configuration.devices: @@ -106,8 +121,7 @@ def dimming_via_direct_control(self, return None, LoadmanagementLimit(None, None) def ripple_control_receiver(self, device: Dict[str, Union[int, str]]) -> Tuple[float, LoadmanagementLimit]: - for action in self.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + for action, io_device in self.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): if isinstance(action, RippleControlReceiver): for d in action.config.configuration.devices: @@ -117,8 +131,7 @@ def ripple_control_receiver(self, device: Dict[str, Union[int, str]]) -> Tuple[f return 1, LoadmanagementLimit(None, None) def stepwise_control(self, device_id: int) -> Tuple[Optional[float], LoadmanagementLimit]: - for action in self.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + for action, io_device in self.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)): if device_id in [component["id"] for component in action.config.configuration.devices]: diff --git a/packages/control/process.py b/packages/control/process.py index 0ddc27403b..67528fb0da 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -97,8 +97,7 @@ def process_algorithm_results(self) -> None: modules_threads.append(consumer_thread) except Exception: log.exception("Fehler im Process-Modul für Verbaucher "+str(consumer)) - for action in data.data.io_actions.actions.values(): - io_device = data.data.system_data[f"io{action.config.configuration.io_device}"] + for action, io_device in data.data.io_actions.iter_actions_with_io_device(): with FaultStateContext(io_device.fault_state, update_always=False): try: if isinstance(action, DimmingDirectControl): diff --git a/packages/modules/common/fault_state.py b/packages/modules/common/fault_state.py index cc8f1a2139..499725ac52 100644 --- a/packages/modules/common/fault_state.py +++ b/packages/modules/common/fault_state.py @@ -103,16 +103,15 @@ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> bool: + if exc_value is not None and not isinstance(exc_value, Exception): + return False if isinstance(exc_value, Exception): self.__fault_state.from_exception(exc_value) elif self.update_always is False and self.__fault_state.fault_state == 0: # Fehlerstatus nicht überschreiben return True self.__fault_state.store_error() - if self.reraise is False or exc_value is None: - return True - else: - return False + return self.reraise is False or exc_value is None T_C = TypeVar("T_C", bound=Callable) diff --git a/packages/modules/io_actions/common.py b/packages/modules/io_actions/common.py index c92e38bcd5..75cee92305 100644 --- a/packages/modules/io_actions/common.py +++ b/packages/modules/io_actions/common.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Union +from typing import Dict, List, Union from control import data from modules.common.fault_state_level import FaultStateLevel @@ -16,14 +16,26 @@ def get_device_log_message(device: Dict[str, Union[int, str]]) -> str: try: if device["type"] == "cp": cp = f"cp{device['id']}" - return (f"Ladepunkt {data.data.cp_data[cp].data.config.name}: " - f"{data.data.cp_data[cp].data.get.powers}W, ") + return (f", Ladepunkt {data.data.cp_data[cp].data.config.name}: " + f"{data.data.cp_data[cp].data.get.powers}W") + if device["type"] == "inverter": + inverter = f"pv{device['id']}" + return (f", Erzeugungsanlage {data.data.pv_data[inverter].data.config.name}: " + f"{data.data.pv_data[inverter].data.get.power}W") if device["type"] == "io": io = f"io{device['id']}" - return (f"{data.data.system_data[io].config.name}: " - "Leistung unbekannt, ") + return (f", {data.data.system_data[io].config.name}: " + "Leistung unbekannt") except KeyError: control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") except Exception: control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") return "Unbekanntes Gerät, " + + +def get_power_log_message(devices: List[Dict[str, Union[int, str]]]) -> str: + evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] + msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" + for device in devices: + msg += get_device_log_message(device) + return msg diff --git a/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py b/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py index ea67a880aa..3582ccfba1 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/api_eebus.py @@ -9,7 +9,7 @@ from dataclass_utils import asdict from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device, get_device_log_message +from modules.io_actions.common import check_fault_state_io_device, get_power_log_message from modules.io_actions.controllable_consumers.dimming.config import DimmingSetup from modules.io_actions.controllable_consumers.dimming.utils import calc_dimming_surplus from modules.io_devices.eebus.config import AnalogInputMapping, DigitalInputMapping @@ -61,11 +61,7 @@ def setup(self) -> None: control_command_log.info(f"Dimmen aktiviert. Übermittelter LPC-Wert: {lpc_value/1000}kWh. " "Leistungswerte vor Ausführung des Steuerbefehls:") - evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] - msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" - for device in self.config.configuration.devices: - msg += get_device_log_message(device) - control_command_log.info(msg) + control_command_log.info(get_power_log_message(self.config.configuration.devices)) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) control_command_log.info("Dimmen deaktiviert.") diff --git a/packages/modules/io_actions/controllable_consumers/dimming/api_io.py b/packages/modules/io_actions/controllable_consumers/dimming/api_io.py index 69b64903c4..60378ac59a 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/api_io.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/api_io.py @@ -9,7 +9,7 @@ from dataclass_utils import asdict from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device, get_device_log_message +from modules.io_actions.common import check_fault_state_io_device, get_power_log_message from modules.io_actions.controllable_consumers.dimming.config import DimmingSetup from modules.io_actions.controllable_consumers.dimming.utils import calc_dimming_surplus @@ -59,11 +59,7 @@ def setup(self) -> None: control_command_log.info("Fehler des IO-Geräts: Dimmen aktiviert für Failsafe-Modus.") control_command_log.info("Dimmen aktiviert. Leistungswerte vor Ausführung des Steuerbefehls:") - evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] - msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" - for device in self.config.configuration.devices: - msg += get_device_log_message(device) - control_command_log.info(msg) + control_command_log.info(get_power_log_message(self.config.configuration.devices)) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) control_command_log.info("Dimmen deaktiviert.") diff --git a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py index e869a23ef7..6894e4f20d 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py +++ b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device, get_device_log_message +from modules.io_actions.common import check_fault_state_io_device, get_power_log_message from modules.io_actions.controllable_consumers.dimming_direct_control.config import DimmingDirectControlSetup control_command_log = logging.getLogger("steuve_control_command") @@ -36,23 +36,24 @@ def setup(self) -> None: self.dimming_input] == self.dimming_value or check_fault_state_io_device(self.config.configuration.io_device)): device = self.config.configuration.devices[0] - if device["type"] == "cp": - cp = f"cp{device['id']}" if self.timestamp is None: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) - if device["type"] == "cp": - if check_fault_state_io_device(self.config.configuration.io_device): - control_command_log.info( - "Fehler des IO-Geräts: Direktsteuerung an Ladepunkt aktiviert für Failsafe-Modus.") + if check_fault_state_io_device(self.config.configuration.io_device): control_command_log.info( - f"Direktsteuerung an Ladepunkt " - f"{data.data.cp_data[cp].data.config.name} aktiviert. " - "Leistungswerte vor Ausführung des Steuerbefehls:") + "Fehler des IO-Geräts: Direktsteuerung aktiviert für Failsafe-Modus.") + + if device["type"] == "cp": + cp = f"cp{device['id']}" + device_name = f"an Ladepunkt {data.data.cp_data[cp].data.config.name}" + elif device["type"] == "io": + io = f"io{device['id']}" + device_name = f"an IO-Gerät {data.data.system_data[io].config.name}" + else: + device_name = f"an unbekanntem Gerät {device['id']}" + control_command_log.info( + f"Direktsteuerung {device_name} aktiviert. Leistungswerte vor Ausführung des Steuerbefehls:") - evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] - msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" - msg += get_device_log_message(device) - control_command_log.info(msg) + control_command_log.info(get_power_log_message([device])) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) control_command_log.info("Direktsteuerung deaktiviert.") diff --git a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py index fc2f4347d1..b42772da05 100644 --- a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py +++ b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device, get_device_log_message +from modules.io_actions.common import check_fault_state_io_device, get_power_log_message from modules.io_actions.controllable_consumers.ripple_control_receiver.config import RippleControlReceiverSetup control_command_log = logging.getLogger("steuve_control_command") @@ -30,11 +30,7 @@ def log_active_ripple_control_receiver(): f"RSE-Sperre mit Wert {pattern['value']*100}" "% aktiviert. Leistungswerte vor Ausführung des Steuerbefehls:") - evu_counter = data.data.counter_data[data.data.counter_all_data.get_evu_counter_str()] - msg = f"EVU-Zähler: {evu_counter.data.get.powers}W, {evu_counter.data.get.power}W" - for device in self.config.configuration.devices: - msg += get_device_log_message(device) - control_command_log.info(msg) + control_command_log.info(get_power_log_message(self.config.configuration.devices)) with ModifyLoglevelContext(control_command_log, logging.DEBUG): if check_fault_state_io_device(self.config.configuration.io_device): diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py index 190e559917..798bc9ac10 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_component_name_by_id, get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device +from modules.io_actions.common import check_fault_state_io_device, get_power_log_message from modules.io_actions.generator_systems.stepwise_control.config import StepwiseControlSetup from modules.io_devices.eebus.config import AnalogInputMapping, DigitalInputMapping @@ -73,40 +73,46 @@ def get_step(self) -> float: def setup(self) -> None: with ModifyLoglevelContext(control_command_log, logging.DEBUG): if check_fault_state_io_device(self.config.configuration.io_device): - control_command_log.info("Fehler des IO-Geräts: EZA-Begrenzung kann nicht erfasst werden.") - else: - self.lpp_value = data.data.io_states[f"io_states{self.config.configuration.io_device}" - ].data.get.analog_input[AnalogInputMapping.LPP_VALUE.name] - lpp_value_prev = data.data.io_states[f"io_states{self.config.configuration.io_device}" - ].data.get.analog_input_prev[AnalogInputMapping.LPP_VALUE.name] - self.lpp_active = data.data.io_states[f"io_states{self.config.configuration.io_device}" - ].data.get.digital_input[DigitalInputMapping.LPP_ACTIVE.name] - lpp_active_prev = data.data.io_states[f"io_states{self.config.configuration.io_device}" - ].data.get.digital_input_prev[DigitalInputMapping.LPP_ACTIVE.name] - changed = True if self.lpp_value != lpp_value_prev or self.lpp_active != lpp_active_prev else False + if self.timestamp is None: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) + control_command_log.info("Fehler des IO-Geräts: EZA-Begrenzung aktiviert für Failsafe-Modus.") + control_command_log.info(get_power_log_message(self.config.configuration.devices)) + self.step = 0 + return + + self.lpp_value = data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.analog_input[AnalogInputMapping.LPP_VALUE.name] + lpp_value_prev = data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.analog_input_prev[AnalogInputMapping.LPP_VALUE.name] + self.lpp_active = data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.digital_input[DigitalInputMapping.LPP_ACTIVE.name] + lpp_active_prev = data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.digital_input_prev[DigitalInputMapping.LPP_ACTIVE.name] + changed = True if self.lpp_value != lpp_value_prev or self.lpp_active != lpp_active_prev else False - if self.lpp_active: - self.step = self.get_step() - if changed: - Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) - control_command_log.info( - f"EEBus-Steuerung: EZA-Begrenzung mit LPP-Wert {self.lpp_value}W aktiviert.") - for device in self.config.configuration.devices: - try: - control_command_log.info( - f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " - f"auf {self.lpp_value}W begrenzt. Gestufte Ansteuerung: " - f"{self.step*100:.0f}% der maximalen Ausgangsleistung." - ) - except ValueError: - control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") - except Exception: - control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") - else: - self.step = 1 - if changed: - Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) - control_command_log.info("EZA-Begrenzung aufgehoben.") + if self.lpp_active: + self.step = self.get_step() + if changed: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) + control_command_log.info( + f"EEBus-Steuerung: EZA-Begrenzung mit LPP-Wert {self.lpp_value}W aktiviert.") + for device in self.config.configuration.devices: + try: + control_command_log.info( + f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " + f"auf {self.lpp_value}W begrenzt. Gestufte Ansteuerung: " + f"{self.step*100:.0f}% der maximalen Ausgangsleistung." + ) + except ValueError: + control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") + except Exception: + control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") + control_command_log.info(get_power_log_message(self.config.configuration.devices)) + else: + self.step = 1 + if changed: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) + control_command_log.info("EZA-Begrenzung aufgehoben.") def control_stepwise(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py index 44da029a89..6f055098f5 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.abstract_io import AbstractIoAction from modules.common.utils.component_parser import get_component_name_by_id, get_io_name_by_id -from modules.io_actions.common import check_fault_state_io_device +from modules.io_actions.common import check_fault_state_io_device, get_power_log_message from modules.io_actions.generator_systems.stepwise_control.config import StepwiseControlSetup control_command_log = logging.getLogger("steuve_control_command") @@ -48,48 +48,53 @@ def __init__(self, config: StepwiseControlSetup): def setup(self) -> None: with ModifyLoglevelContext(control_command_log, logging.DEBUG): if check_fault_state_io_device(self.config.configuration.io_device): - control_command_log.info("Fehler des IO-Geräts: EZA-Begrenzung kann nicht erfasst werden.") - else: - digital_input = ( - data.data.io_states[ - f"io_states{self.config.configuration.io_device}" - ].data.get.digital_input - ) - digital_input_prev = data.data.io_states[ - f"io_states{self.config.configuration.io_device}"].data.get.digital_input_prev - changed = len([ - input_name for input_name in self.__unique_inputs - if digital_input[input_name] != digital_input_prev[input_name] - ]) > 0 + if self.timestamp is None: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) + control_command_log.info("Fehler des IO-Geräts: EZA-Begrenzung aktiviert für Failsafe-Modus.") + control_command_log.info(get_power_log_message(self.config.configuration.devices)) + return + + digital_input = ( + data.data.io_states[ + f"io_states{self.config.configuration.io_device}" + ].data.get.digital_input + ) + digital_input_prev = data.data.io_states[ + f"io_states{self.config.configuration.io_device}"].data.get.digital_input_prev + changed = len([ + input_name for input_name in self.__unique_inputs + if digital_input[input_name] != digital_input_prev[input_name] + ]) > 0 - for pattern in self.config.configuration.input_pattern: - for action_input, value in pattern["matrix"].items(): - if digital_input[action_input] != value: - break - else: - # Alle digitalen Eingänge entsprechen dem Pattern - if pattern["value"] != 1: - if changed: - Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) - control_command_log.info( - f"EZA-Begrenzung mit Wert {int(pattern['value']*100)}% aktiviert.") - for device in self.config.configuration.devices: - try: - if device["type"] == "inverter": - control_command_log.info( - f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " - f"auf {int(pattern['value']*100)}% begrenzt." - ) - except ValueError: - control_command_log.warning( - f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") - except Exception: - control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") - break + for pattern in self.config.configuration.input_pattern: + for action_input, value in pattern["matrix"].items(): + if digital_input[action_input] != value: + break else: - if changed: - Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) - control_command_log.info("EZA-Begrenzung aufgehoben.") + # Alle digitalen Eingänge entsprechen dem Pattern + if pattern["value"] != 1: + if changed: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) + control_command_log.info( + f"EZA-Begrenzung mit Wert {int(pattern['value']*100)}% aktiviert.") + for device in self.config.configuration.devices: + try: + if device["type"] == "inverter": + control_command_log.info( + f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " + f"auf {int(pattern['value']*100)}% begrenzt." + ) + except ValueError: + control_command_log.warning( + f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") + except Exception: + control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") + control_command_log.info(get_power_log_message(self.config.configuration.devices)) + break + else: + if changed: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) + control_command_log.info("EZA-Begrenzung aufgehoben.") def control_stepwise(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): From e4eba5d210b212f8b079accc8384764fb5845c9a Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:34:36 +0200 Subject: [PATCH 3/5] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/helpermodules/exceptions/registry.py | 2 +- packages/modules/io_actions/common.py | 2 +- .../io_actions/generator_systems/stepwise_control/api_eebus.py | 2 +- .../io_actions/generator_systems/stepwise_control/api_io.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/helpermodules/exceptions/registry.py b/packages/helpermodules/exceptions/registry.py index b849864530..ed745ecae2 100644 --- a/packages/helpermodules/exceptions/registry.py +++ b/packages/helpermodules/exceptions/registry.py @@ -25,7 +25,7 @@ class ExceptionRegistry: def translate_exception(self, exception: Exception) -> Tuple[str, FaultStateLevel]: entry = self.find_registry_entry(exception) if entry is None: - message = str(exception.args[0]) if exception.args else str(exception) + message = (str(exception.args[0]) if exception.args else str(exception)) or exception.__class__.__name__ return message, FaultStateLevel.ERROR if isinstance(entry.handler, str): return entry.handler, FaultStateLevel.ERROR diff --git a/packages/modules/io_actions/common.py b/packages/modules/io_actions/common.py index 75cee92305..bce16d0cde 100644 --- a/packages/modules/io_actions/common.py +++ b/packages/modules/io_actions/common.py @@ -30,7 +30,7 @@ def get_device_log_message(device: Dict[str, Union[int, str]]) -> str: control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") except Exception: control_command_log.exception(f"Fehler beim Zugriff auf Gerät {device}") - return "Unbekanntes Gerät, " + return f", Unbekanntes Gerät: {device}" def get_power_log_message(devices: List[Dict[str, Union[int, str]]]) -> str: diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py index 798bc9ac10..e5b2081deb 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus.py @@ -110,7 +110,7 @@ def setup(self) -> None: control_command_log.info(get_power_log_message(self.config.configuration.devices)) else: self.step = 1 - if changed: + if changed or self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) control_command_log.info("EZA-Begrenzung aufgehoben.") diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py index 6f055098f5..f742094fca 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py @@ -92,7 +92,7 @@ def setup(self) -> None: control_command_log.info(get_power_log_message(self.config.configuration.devices)) break else: - if changed: + if changed or self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) control_command_log.info("EZA-Begrenzung aufgehoben.") From e2c112ad1405e0baff8b06908dc62dc0aea17c60 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 18 Sep 2026 12:27:44 +0200 Subject: [PATCH 4/5] fixes --- packages/control/process.py | 3 + packages/modules/io_actions/common.py | 5 +- .../controllable_consumers/dimming/api_io.py | 4 +- .../dimming_direct_control/api.py | 22 ++++---- .../ripple_control_receiver/api.py | 56 +++++++++++-------- .../stepwise_control/api_io.py | 22 ++++---- 6 files changed, 65 insertions(+), 47 deletions(-) diff --git a/packages/control/process.py b/packages/control/process.py index 67528fb0da..607d227c4c 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -27,6 +27,7 @@ from modules.io_actions.generator_systems.stepwise_control.api_io import StepwiseControlIo log = logging.getLogger(__name__) +control_command_log = logging.getLogger("steuve_control_command") class Process: @@ -125,6 +126,8 @@ def process_algorithm_results(self) -> None: f"io_states{action.config.configuration.io_output_device}" ].data.set.digital_output[output] = pattern["matrix"][output] except KeyError as e: + control_command_log.error( + f"Ausgang konnte für die Aktion {action.config.name} nicht zugeordnet werden.") raise KeyError(f"Ausgang konnte für die Aktion {action.config.name} nicht zugeordnet werden. " "Bitte prüfen Sie die Konfiguration.") from e for io in data.data.system_data.values(): diff --git a/packages/modules/io_actions/common.py b/packages/modules/io_actions/common.py index bce16d0cde..d78b3cfc2c 100644 --- a/packages/modules/io_actions/common.py +++ b/packages/modules/io_actions/common.py @@ -3,6 +3,7 @@ from control import data from modules.common.fault_state_level import FaultStateLevel +from modules.common.utils.component_parser import get_component_name_by_id control_command_log = logging.getLogger("steuve_control_command") @@ -20,11 +21,11 @@ def get_device_log_message(device: Dict[str, Union[int, str]]) -> str: f"{data.data.cp_data[cp].data.get.powers}W") if device["type"] == "inverter": inverter = f"pv{device['id']}" - return (f", Erzeugungsanlage {data.data.pv_data[inverter].data.config.name}: " + return (f", Erzeugungsanlage {get_component_name_by_id(device['id'])}: " f"{data.data.pv_data[inverter].data.get.power}W") if device["type"] == "io": io = f"io{device['id']}" - return (f", {data.data.system_data[io].config.name}: " + return (f", {data.data.system_data[io].config.name} {device['digital_output']}: " "Leistung unbekannt") except KeyError: control_command_log.warning(f"Zugriff auf gelöschtes Gerät nicht möglich: {device}") diff --git a/packages/modules/io_actions/controllable_consumers/dimming/api_io.py b/packages/modules/io_actions/controllable_consumers/dimming/api_io.py index 60378ac59a..0b50ec0bf6 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/api_io.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/api_io.py @@ -55,9 +55,9 @@ def setup(self) -> None: if check_fault_state_io_device(self.config.configuration.io_device) or self.dimming_active(): if self.timestamp is None: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) - if check_fault_state_io_device(self.config.configuration.io_device): - control_command_log.info("Fehler des IO-Geräts: Dimmen aktiviert für Failsafe-Modus.") control_command_log.info("Dimmen aktiviert. Leistungswerte vor Ausführung des Steuerbefehls:") + if check_fault_state_io_device(self.config.configuration.io_device): + control_command_log.info("Fehler des IO-Geräts: Dimmen aktiviert für Failsafe-Modus.") control_command_log.info(get_power_log_message(self.config.configuration.devices)) elif self.timestamp: diff --git a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py index 6894e4f20d..7dcb1679f7 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py +++ b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/api.py @@ -32,31 +32,31 @@ def __init__(self, config: DimmingDirectControlSetup): def setup(self) -> None: with ModifyLoglevelContext(control_command_log, logging.DEBUG): + device = self.config.configuration.devices[0] + if device["type"] == "cp": + cp = f"cp{device['id']}" + device_name = f"an Ladepunkt {data.data.cp_data[cp].data.config.name}" + elif device["type"] == "io": + io = f"io{device['id']}" + device_name = f"an IO-Gerät {data.data.system_data[io].config.name} {device['digital_output']}" + else: + device_name = f"an unbekanntem Gerät {device['id']}" + if (data.data.io_states[f"io_states{self.config.configuration.io_device}"].data.get.digital_input[ self.dimming_input] == self.dimming_value or check_fault_state_io_device(self.config.configuration.io_device)): - device = self.config.configuration.devices[0] if self.timestamp is None: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) if check_fault_state_io_device(self.config.configuration.io_device): control_command_log.info( "Fehler des IO-Geräts: Direktsteuerung aktiviert für Failsafe-Modus.") - - if device["type"] == "cp": - cp = f"cp{device['id']}" - device_name = f"an Ladepunkt {data.data.cp_data[cp].data.config.name}" - elif device["type"] == "io": - io = f"io{device['id']}" - device_name = f"an IO-Gerät {data.data.system_data[io].config.name}" - else: - device_name = f"an unbekanntem Gerät {device['id']}" control_command_log.info( f"Direktsteuerung {device_name} aktiviert. Leistungswerte vor Ausführung des Steuerbefehls:") control_command_log.info(get_power_log_message([device])) elif self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) - control_command_log.info("Direktsteuerung deaktiviert.") + control_command_log.info(f"Direktsteuerung {device_name} deaktiviert.") def dimming_via_direct_control(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): diff --git a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py index b42772da05..f6980cbf56 100644 --- a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py +++ b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/api.py @@ -17,34 +17,46 @@ class RippleControlReceiver(AbstractIoAction): def __init__(self, config: RippleControlReceiverSetup): self.config = config + for pattern in self.config.configuration.input_pattern: + input_matrix_list = list(pattern["matrix"].items()) + if len(input_matrix_list): + inputs = ", ".join([input_name for input_name, _ in input_matrix_list]) + control_command_log.info(f"RSE-Kontakt: Eingänge {inputs} werden überwacht.") + else: + control_command_log.warning("RSE-Kontakt: Kein Eingang zum Überwachen konfiguriert.") super().__init__() def setup(self) -> None: - def log_active_ripple_control_receiver(): - if self.timestamp is None: - Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) - if check_fault_state_io_device(self.config.configuration.io_device): + with ModifyLoglevelContext(control_command_log, logging.DEBUG): + io_state = data.data.io_states[f"io_states{self.config.configuration.io_device}"].data.get + + # IO-Fehler aktiviert den Failsafe unabhängig von den Eingangsmustern. + if check_fault_state_io_device(self.config.configuration.io_device): + if self.timestamp is None: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) control_command_log.info( - "Fehler des IO-Geräts: RSE aktiviert für Failsafe-Modus.") - control_command_log.info( - f"RSE-Sperre mit Wert {pattern['value']*100}" - "% aktiviert. Leistungswerte vor Ausführung des Steuerbefehls:") + "RSE-Sperre im Failsafe-Modus aktiviert. " + "Leistungswerte vor Ausführung des Steuerbefehls:") + control_command_log.info("Fehler des IO-Geräts: RSE aktiviert für Failsafe-Modus.") + control_command_log.info(get_power_log_message(self.config.configuration.devices)) + return - control_command_log.info(get_power_log_message(self.config.configuration.devices)) + matching_pattern = None + for pattern in self.config.configuration.input_pattern: + for digital_input, value in pattern["matrix"].items(): + if io_state.digital_input[digital_input] != value: + break + else: + matching_pattern = pattern + break - with ModifyLoglevelContext(control_command_log, logging.DEBUG): - if check_fault_state_io_device(self.config.configuration.io_device): - for pattern in self.config.configuration.input_pattern: - for digital_input, value in pattern["matrix"].items(): - if data.data.io_states[ - f"io_states{self.config.configuration.io_device}"].data.get.digital_input[ - digital_input] != value: - break - else: - # Alle digitalen Eingänge entsprechen dem Pattern - if pattern["value"] != 1: - log_active_ripple_control_receiver() - break + if matching_pattern and matching_pattern["value"] not in (1, None): + if self.timestamp is None: + Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) + control_command_log.info( + f"RSE-Sperre mit Wert {matching_pattern['value']*100}% aktiviert. " + "Leistungswerte vor Ausführung des Steuerbefehls:") + control_command_log.info(get_power_log_message(self.config.configuration.devices)) else: if self.timestamp: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", None) diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py index f742094fca..9440335a85 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_io.py @@ -27,22 +27,24 @@ def __init__(self, config: StepwiseControlSetup): for device in self.config.configuration.devices if device["type"] == "inverter" ] - assigned_outputs = [ - f"{device['id']}/{device['digital_output']}" - for device in self.config.configuration.devices - if device["type"] == "io" - ] + self.__unique_outputs = [] + for pattern in self.config.configuration.output_pattern: + for key in pattern["matrix"].keys(): + if key not in self.__unique_outputs: + self.__unique_outputs.append(key) with ModifyLoglevelContext(control_command_log, logging.DEBUG): # Log the configuration details # We cannot use configured names here, as the devices are not yet initialized # and thus the names are not available. - control_command_log.info( + info_message = ( f"Stufenweise Steuerung von EZA: I/O-Gerät: {self.config.configuration.io_device}, " f"Überwachte digitale Eingänge: {self.__unique_inputs}, " - f"zugeordnete Erzeugungsanlagen: {assigned_inverters} " - f"zugeordnete IO-Ausgänge: {assigned_outputs} " - "Die Begrenzung muss in den EZA vorgenommen werden!" + f"zugeordnete Erzeugungsanlagen: {assigned_inverters}, " + f"zugeordnete IO-Ausgänge: {self.__unique_outputs}" ) + if len(self.__unique_outputs) == 0: + info_message += ", Die Begrenzung muss in den EZA vorgenommen werden!" + control_command_log.info(info_message) super().__init__() def setup(self) -> None: @@ -50,7 +52,7 @@ def setup(self) -> None: if check_fault_state_io_device(self.config.configuration.io_device): if self.timestamp is None: Pub().pub(f"openWB/set/io/action/{self.config.id}/timestamp", create_timestamp()) - control_command_log.info("Fehler des IO-Geräts: EZA-Begrenzung aktiviert für Failsafe-Modus.") + control_command_log.info("Fehler des IO-Geräts: EZA-Begrenzung aktiviert für Failsafe-Modus.") control_command_log.info(get_power_log_message(self.config.configuration.devices)) return From d7b68d698eed3291182199cd3d69c2041d9ae65c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 18 Sep 2026 12:38:25 +0200 Subject: [PATCH 5/5] add pytest --- .../helpermodules/exceptions/registry_test.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/helpermodules/exceptions/registry_test.py b/packages/helpermodules/exceptions/registry_test.py index 7d3d90d258..a39897184f 100644 --- a/packages/helpermodules/exceptions/registry_test.py +++ b/packages/helpermodules/exceptions/registry_test.py @@ -31,6 +31,11 @@ class ErrorF(ErrorD): pass +class EmptyStringException(Exception): + def __str__(self): + return "" + + @pytest.mark.parametrize("exception,expected_message", [ [ErrorRoot, "ErrorRoot"], [ErrorB, "B"], @@ -68,3 +73,21 @@ def test_accepts_all_supported_formats(handler): # evaluation assert isinstance(actual[0], str) assert actual[0] == "msg" + + +@pytest.mark.parametrize("exception,expected_message", [ + pytest.param(Exception(), "Exception", id="no args fallback"), + pytest.param(EmptyStringException(), "EmptyStringException", id="empty string fallback"), +]) +def test_translate_exception_falls_back_to_class_name_for_empty_message(exception: Exception, + expected_message: str, + monkeypatch): + # setup + monkeypatch.setattr(ExceptionRegistry, "registry", []) + registry = ExceptionRegistry() + + # execution + actual = registry.translate_exception(exception) + + # evaluation + assert actual[0] == expected_message