From 614d6b997c0eb3512c5d82bbc9efcbd332349c1b Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 20 Mar 2026 14:45:40 +0100 Subject: [PATCH 1/4] generator_systems: configure another output io device as input device --- packages/control/process.py | 2 +- packages/helpermodules/update_config.py | 15 ++++++++++++++- .../generator_systems/stepwise_control/config.py | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/control/process.py b/packages/control/process.py index 2fadf4987a..129f3fa6b9 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -92,7 +92,7 @@ def process_algorithm_results(self) -> None: # 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_device}" + f"io_states{action.config.configuration.io_device_output}" ].data.set.digital_output[output] = pattern["matrix"][output] for io in data.data.system_data.values(): if isinstance(io, AbstractIoDevice): diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index f85dd8c8f0..5c04d07d3d 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -58,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 133 + DATASTORE_VERSION = 134 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3421,3 +3421,16 @@ def upgrade(topic: str, payload) -> None: return {topic: provider} self._loop_all_received_topics(upgrade) self._append_datastore_version(133) + + def upgrade_datastore_134(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("openWB/io/action/[0-9]+/config", topic) is not None: + config = decode_payload(payload) + if (config["configuration"]["passthrough_enabled"] is True and + config["configuration"].get("io_device_output") is None): + config["configuration"]["io_device_output"] = config["configuration"].get("io_device") + else: + config["configuration"]["io_device_output"] = None + return {topic: config} + self._loop_all_received_topics(upgrade) + self._append_datastore_version(134) diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/config.py b/packages/modules/io_actions/generator_systems/stepwise_control/config.py index 7cfc0d4802..9fb3bb23fc 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/config.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/config.py @@ -11,6 +11,7 @@ class StepwiseControlConfig: devices: List[Dict] = field(default_factory=empty_list_factory) # [{"type": "inverter", "id": 1},...] passthrough_enabled: bool = False + io_device_output: Optional[int] = None output_pattern: List[Dict] = field(default_factory=empty_io_pattern_stepwise_factory) From b90b79e9e5e301ebb20e20c9c9e302ed273fc602 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 16 Jul 2026 11:15:52 +0200 Subject: [PATCH 2/4] review --- packages/control/process.py | 3 ++- packages/helpermodules/update_config.py | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/control/process.py b/packages/control/process.py index 129f3fa6b9..1c4dd22c4b 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -85,7 +85,8 @@ def process_algorithm_results(self) -> None: ) if isinstance(action, StepwiseControlIo): # check if passthrough is enabled - if action.config.configuration.passthrough_enabled: + if (action.config.configuration.passthrough_enabled and + action.config.configuration.io_device_output is not None): # find output pattern by value for pattern in action.config.configuration.output_pattern: if pattern["value"] == action.control_stepwise()[0]: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 5c04d07d3d..498f9ddf88 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -3426,11 +3426,12 @@ def upgrade_datastore_134(self) -> None: def upgrade(topic: str, payload) -> Optional[dict]: if re.search("openWB/io/action/[0-9]+/config", topic) is not None: config = decode_payload(payload) - if (config["configuration"]["passthrough_enabled"] is True and - config["configuration"].get("io_device_output") is None): - config["configuration"]["io_device_output"] = config["configuration"].get("io_device") - else: - config["configuration"]["io_device_output"] = None - return {topic: config} + if config.get("type") == "stepwise_control": + if config["configuration"]["passthrough_enabled"] is True: + if config["configuration"].get("io_device_output") is None: + config["configuration"]["io_device_output"] = config["configuration"].get("io_device") + else: + config["configuration"]["io_device_output"] = None + return {topic: config} self._loop_all_received_topics(upgrade) self._append_datastore_version(134) From d4b67bc995f5c32fcf4cea9f33892e6fb6dafda7 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Jul 2026 15:17:34 +0200 Subject: [PATCH 3/4] fix --- packages/control/prepare.py | 3 +- packages/control/process.py | 7 +- packages/helpermodules/update_config.py | 6 +- .../stepwise_control/api_eebus.py | 83 ++++++++++--------- .../stepwise_control/api_eebus_test.py | 46 ++++++++++ .../stepwise_control/config.py | 2 +- 6 files changed, 99 insertions(+), 48 deletions(-) create mode 100644 packages/modules/io_actions/generator_systems/stepwise_control/api_eebus_test.py diff --git a/packages/control/prepare.py b/packages/control/prepare.py index ddb09b471c..e10429952e 100644 --- a/packages/control/prepare.py +++ b/packages/control/prepare.py @@ -18,6 +18,8 @@ def setup_algorithm(self) -> None: """ bereitet die Daten für den Algorithmus vor und startet diesen. """ try: + # vor PV, greift auf IO-Aktion stepwise_control zu + data.data.io_actions.setup() data.data.pv_all_data.calc_power_for_all_components() data.data.bat_all_data.calc_power_for_all_components() for cp in data.data.cp_data.values(): @@ -34,7 +36,6 @@ def setup_algorithm(self) -> None: data.data.cp_all_data.get_cp_sum() data.data.cp_all_data.no_charge() data.data.counter_all_data.set_home_consumption() - data.data.io_actions.setup() except Exception: log.exception("Fehler im Prepare-Modul") data.data.print_all() diff --git a/packages/control/process.py b/packages/control/process.py index 1c4dd22c4b..9fa439b828 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -16,6 +16,7 @@ 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 +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__) @@ -83,17 +84,17 @@ def process_algorithm_results(self) -> None: 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, StepwiseControlIo): + if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)): # check if passthrough is enabled if (action.config.configuration.passthrough_enabled and - action.config.configuration.io_device_output is not None): + 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_device_output}" + f"io_states{action.config.configuration.io_output_device}" ].data.set.digital_output[output] = pattern["matrix"][output] for io in data.data.system_data.values(): if isinstance(io, AbstractIoDevice): diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 498f9ddf88..36735ee5be 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -3428,10 +3428,10 @@ def upgrade(topic: str, payload) -> Optional[dict]: config = decode_payload(payload) if config.get("type") == "stepwise_control": if config["configuration"]["passthrough_enabled"] is True: - if config["configuration"].get("io_device_output") is None: - config["configuration"]["io_device_output"] = config["configuration"].get("io_device") + if config["configuration"].get("io_output_device") is None: + config["configuration"]["io_output_device"] = config["configuration"].get("io_device") else: - config["configuration"]["io_device_output"] = None + config["configuration"]["io_output_device"] = None return {topic: config} self._loop_all_received_topics(upgrade) self._append_datastore_version(134) 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 a2fe36464b..c214a621fe 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 @@ -42,6 +42,34 @@ def __init__(self, config: StepwiseControlSetup): ) super().__init__() + def get_step(self) -> float: + step: float = 0 + try: + max_output_inverter: float = 0 + for inverter in self.config.configuration.devices: + if data.data.pv_data[f"pv{inverter['id']}"].data.config.max_ac_out == 0: + msg = "Maximale Ausgangsleistung des Wechselrichters muss im Lastmanagement konfiguriert werden." + control_command_log.error(msg) + raise ValueError(msg) + max_output_inverter += data.data.pv_data[f"pv{inverter['id']}"].data.config.max_ac_out + percentage = self.lpp_value / max_output_inverter + except ZeroDivisionError: + msg = ("Bitte unter Konfiguration -> Lastmanagement die maximale Ausgangsleistung" + " des Wechselrichters angeben.") + log.exception(msg) + control_command_log.info(msg) + percentage = 0 + except ValueError: + percentage = 0 + except Exception: + log.exception("Fehler beim Berechnen des Steuerungsschritts.") + percentage = 0 + for s in [0, 0.3, 0.6, 1.0]: + if percentage <= s: + step = s + break + return step + def setup(self) -> None: with ModifyLoglevelContext(control_command_log, logging.DEBUG): if check_fault_state_io_device(self.config.configuration.io_device): @@ -57,32 +85,18 @@ def setup(self) -> None: ].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 - max_output_inverter = 0 - for inverter in self.config.configuration.devices: - max_output_inverter += data.data.pv_data[f"pv{inverter['id']}"].data.config.max_ac_out - if self.lpp_active: - try: - self.step = self.lpp_value / max_output_inverter - except ZeroDivisionError: - msg = ("Bitte unter Konfiguration -> Lastmanagement die maximale Ausgangsleistung" - " des Wechselrichters angeben.") - log.exception(msg) - control_command_log.info(msg) - self.step = 0 - for s in [0, 0.25, 0.5, 0.75, 1.0]: - if self.step <= s: - self.step = s - break + 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: LPP-Wert {self.lpp_value} / " - f"max. PV-Leistung {max_output_inverter} = {self.step}") - control_command_log.info(f"EZA-Begrenzung mit Wert {self.step*100}% aktiviert.") + 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.step*100}% begrenzt." + f"auf {self.lpp_value}W begrenzt. Gestufte Ansteuerung: " + f"{self.step*100:.0f}% der maximalen Ausgangsleistung." ) else: if changed: @@ -95,28 +109,17 @@ def control_stepwise(self) -> Tuple[Optional[float], LoadmanagementLimit]: LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( self.config.configuration.io_device)), LimitingValue.CONTROLLABLE_CONSUMERS_ERROR)) - 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 + + if self.lpp_active: + if self.step != 1: + limit = LoadmanagementLimit( + LimitingValue.CONTROL_STEPWISE.value.format(self.step*100), + LimitingValue.CONTROL_STEPWISE) else: - if pattern["value"] is None: - return 0, LoadmanagementLimit(LimitingValue.MISSING_CONFIGURATION.value, - LimitingValue.MISSING_CONFIGURATION) - # Alle digitalen Eingänge entsprechen dem Pattern - elif pattern["value"] != 1: - limit = LoadmanagementLimit( - LimitingValue.CONTROL_STEPWISE.value.format(pattern["value"]*100), - LimitingValue.CONTROL_STEPWISE) - else: - limit = LoadmanagementLimit("Keine Leistungsbegrenzung aktiv.", None) - return pattern["value"], limit + limit = LoadmanagementLimit("Keine Leistungsbegrenzung aktiv.", None) else: - # Zustand entspricht keinem Pattern - return 0, LoadmanagementLimit( - LimitingValue.CONTROL_STEPWISE.value.format(0), - LimitingValue.CONTROL_STEPWISE) + limit = LoadmanagementLimit("Keine Leistungsbegrenzung aktiv.", None) + return self.step, limit def create_action(config: StepwiseControlSetup, parent_device_type: str): diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus_test.py b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus_test.py new file mode 100644 index 0000000000..b58688c59b --- /dev/null +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api_eebus_test.py @@ -0,0 +1,46 @@ +from unittest.mock import Mock + +import pytest + +from control import data +from modules.io_actions.generator_systems.stepwise_control.api_eebus import StepwiseControlEebus +from modules.io_actions.generator_systems.stepwise_control.config import StepwiseControlConfig, StepwiseControlSetup + + +@pytest.fixture() +def mock_data() -> None: + data.data_init(Mock()) + + +def _build_action(lpp_value: float, max_ac_out: float) -> StepwiseControlEebus: + config = StepwiseControlSetup(configuration=StepwiseControlConfig( + io_device=0, + devices=[{"type": "inverter", "id": 1}], + )) + action = StepwiseControlEebus(config) + action.lpp_value = lpp_value + data.data.pv_data["pv1"] = Mock(data=Mock(config=Mock(max_ac_out=max_ac_out))) + return action + + +@pytest.mark.parametrize( + "lpp_value,max_ac_out,expected", + [ + pytest.param(0, 10000, 0, id="0W -> 0%"), + pytest.param(1000, 10000, 0.3, id="10% -> 30%"), + pytest.param(3000, 10000, 0.3, id="30% -> 30%"), + pytest.param(4500, 10000, 0.6, id="45% -> 60%"), + pytest.param(6000, 10000, 0.6, id="60% -> 60%"), + pytest.param(9000, 10000, 1.0, id="90% -> 100%"), + ], +) +def test_get_step(mock_data, lpp_value, max_ac_out, expected): + action = _build_action(lpp_value=lpp_value, max_ac_out=max_ac_out) + + assert action.get_step() == expected + + +def test_get_step_handles_zero_division(mock_data): + action = _build_action(lpp_value=5000, max_ac_out=0) + + assert action.get_step() == 0 diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/config.py b/packages/modules/io_actions/generator_systems/stepwise_control/config.py index 9fb3bb23fc..e8c1df3efd 100644 --- a/packages/modules/io_actions/generator_systems/stepwise_control/config.py +++ b/packages/modules/io_actions/generator_systems/stepwise_control/config.py @@ -11,7 +11,7 @@ class StepwiseControlConfig: devices: List[Dict] = field(default_factory=empty_list_factory) # [{"type": "inverter", "id": 1},...] passthrough_enabled: bool = False - io_device_output: Optional[int] = None + io_output_device: Optional[int] = None output_pattern: List[Dict] = field(default_factory=empty_io_pattern_stepwise_factory) From 0614c4c9d5d7cfe60c30138e00e8606216abd3ba Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 20 Jul 2026 12:03:56 +0200 Subject: [PATCH 4/4] review --- .../io_actions/generator_systems/stepwise_control/api_eebus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c214a621fe..03a554c2a3 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 @@ -87,7 +87,6 @@ def setup(self) -> None: 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( @@ -99,6 +98,7 @@ def setup(self) -> None: f"{self.step*100:.0f}% der maximalen Ausgangsleistung." ) 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.")