From 0be35bcfff167dadb59558daef84a0ac0b02619a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 7 Jul 2026 14:37:56 +0200 Subject: [PATCH 01/11] Add LoadManager --- packages/control/io_device.py | 15 ++++- packages/control/loadmanagement.py | 15 +++++ packages/helpermodules/setdata.py | 18 ++++++ .../load_manager/api.py | 57 ++++++++++++++++++ .../load_manager/config.py | 28 +++++++++ .../io_devices/load_manager/__init__.py | 0 .../modules/io_devices/load_manager/api.py | 59 +++++++++++++++++++ .../modules/io_devices/load_manager/config.py | 41 +++++++++++++ 8 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 packages/modules/io_actions/controllable_consumers/load_manager/api.py create mode 100644 packages/modules/io_actions/controllable_consumers/load_manager/config.py create mode 100644 packages/modules/io_devices/load_manager/__init__.py create mode 100644 packages/modules/io_devices/load_manager/api.py create mode 100644 packages/modules/io_devices/load_manager/config.py diff --git a/packages/control/io_device.py b/packages/control/io_device.py index b24138a75b..d84130011a 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -4,6 +4,9 @@ from helpermodules.constants import NO_ERROR from modules.io_actions.controllable_consumers.dimming.api_eebus import DimmingEebus from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo + +from modules.io_actions.controllable_consumers.load_manager.api import DimmingLoadManager + from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl from modules.io_actions.controllable_consumers.ripple_control_receiver.api import RippleControlReceiver from modules.io_actions.generator_systems.stepwise_control.api_eebus import StepwiseControlEebus @@ -55,7 +58,8 @@ def __init__(self, num: Union[int, str]): class IoActions: def __init__(self): self.actions: Dict[int, Union[DimmingIo, DimmingEebus, DimmingDirectControl, - RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo]] = {} + RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo, + DimmingLoadManager]] = {} def setup(self): for action in self.actions.values(): @@ -102,3 +106,12 @@ def stepwise_control(self, device_id: int) -> Tuple[Optional[float], Loadmanagem return action.control_stepwise() else: return None, LoadmanagementLimit(None, None) + + def dimming_load_manager(self, device: Dict) -> Optional[float]: + for action in self.actions.values(): + if isinstance(action, (DimmingLoadManager)): + for d in action.config.configuration.devices: + if d == device: + return action.dimming_get_import_power_left() + else: + return None, LoadmanagementLimit(None, None) diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index ae12e06f3c..06fef83109 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -26,6 +26,9 @@ def get_available_currents(self, available_currents, new_limit = self._limit_by_dimming(available_currents, cp) limit = new_limit if new_limit.limiting_value is not None else limit + available_currents, new_limit = self._limit_loadmanager(available_currents, cp) + limit = new_limit if new_limit.limiting_value is not None else limit + available_currents, new_limit = self._limit_by_ripple_control_receiver(available_currents, cp) limit = new_limit if new_limit.limiting_value is not None else limit except ValueError as e: @@ -181,3 +184,15 @@ def _limit_by_ripple_control_receiver(self, if c > 0 else 0 for c in available_currents] log.debug(f"Reduzierung durch RSE-Kontakt auf {value*100}%, maximal {max_current*value}A") return available_currents, limit + + def _limit_loadmanager(self, + available_currents: List[float], + cp: Chargepoint) -> Tuple[List[float], LoadmanagementLimit]: + dimming_power_left, limit = data.data.io_actions.dimming_load_manager({"type": "cp", "id": cp.num}) + if dimming_power_left is not None: + if sum(available_currents)*230 > dimming_power_left: + phases = 3-available_currents.count(0) + overload_per_phase = (sum(available_currents) - dimming_power_left/230)/phases + available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents] + log.debug(f"Reduzierung der Ströme durch die Dimmung: {available_currents}A") + return available_currents, limit diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 94e877a19b..2ba46096ed 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -851,6 +851,8 @@ def process_mqtt_topic(self, msg: mqtt.MQTTMessage): self.process_pv_topic(msg) elif "openWB/set/mqtt/vehicle/" in msg.topic: self.process_vehicle_topic(msg) + elif "openWB/set/mqtt/loadmanager/" in msg.topic: + self.loadmanager_topic(msg) def process_optional_topic(self, msg: mqtt.MQTTMessage): """ Handler für die Optionalen-Topics @@ -1214,3 +1216,19 @@ def process_legacy_smart_home_topic(self, msg): def _get_ramdisk_path(self) -> Path: return Path(__file__).resolve().parents[2]/"ramdisk" + + def loadmanager_topic(self, msg: mqtt.MQTTMessage): + """ Handler für die LoadMananger-Topics + + Parameters + ---------- + msg: + enthält Topic und Payload + """ + try: + if "openWB/set/mqtt/loadmanager/set/loadmanager" in msg.topic: + self._validate_value(msg, "json") + else: + self.__unknown_topic(msg) + except Exception: + log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/api.py b/packages/modules/io_actions/controllable_consumers/load_manager/api.py new file mode 100644 index 0000000000..0984583baa --- /dev/null +++ b/packages/modules/io_actions/controllable_consumers/load_manager/api.py @@ -0,0 +1,57 @@ +import logging +from typing import Optional, Tuple +from control import data +from control.limiting_value import LimitingValue, LoadmanagementLimit +from helpermodules.pub import Pub +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.controllable_consumers.load_manager.config import LoadManagerSetup +from modules.io_devices.load_manager.config import AnalogInputMapping + +from modules.common.abstract_device import DeviceDescriptor + +log = logging.getLogger(__name__) +control_command_log = logging.getLogger("steuve_control_command") + + +class DimmingLoadManager(AbstractIoAction): + def __init__(self, config: LoadManagerSetup): + self.config = config + self.import_power_left = None + control_command_log.info("Dimmen per LoadManager.") + + fixed_import_power = 0 + for device in self.config.configuration.devices: + if device["type"] != "cp": + fixed_import_power += 4200 + log.debug(f"Dimmen per LoadManager: Fest vergebene Mindestleistung: {fixed_import_power}W") + if fixed_import_power != self.config.configuration.fixed_import_power: + self.config.configuration.fixed_import_power = fixed_import_power + Pub().pub(f"openWB/set/io/action/{self.config.id}/config", asdict(self.config)) + + super().__init__() + + def setup(self) -> None: + if check_fault_state_io_device(self.config.configuration.io_device): + self.import_power_left = self.config.configuration.fixed_import_power + else: + max_power = data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.analog_input[AnalogInputMapping.MAX_POWER.name] + self.import_power_left = max_power + + def dimming_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: + if check_fault_state_io_device(self.config.configuration.io_device): + return (self.import_power_left, LoadmanagementLimit( + LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( + self.config.configuration.io_device)), + LimitingValue.CONTROLLABLE_CONSUMERS_ERROR)) + return self.import_power_left, LoadmanagementLimit(LimitingValue.DIMMING.value, LimitingValue.DIMMING) + + +def create_action(config: LoadManagerSetup, parent_device_type: str): + return DimmingLoadManager(config=config) + + +device_descriptor = DeviceDescriptor(configuration_factory=LoadManagerSetup) diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/config.py b/packages/modules/io_actions/controllable_consumers/load_manager/config.py new file mode 100644 index 0000000000..b4f24f4f3e --- /dev/null +++ b/packages/modules/io_actions/controllable_consumers/load_manager/config.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass, field +from typing import Dict, List, Optional +from dataclass_utils.factories import empty_io_pattern_boolean_factory, empty_list_factory +from modules.io_actions.groups import ActionGroup + + +@dataclass +class LoadManagerConfig: + io_device: Optional[int] = None + input_pattern: List[Dict] = field(default_factory=empty_io_pattern_boolean_factory) + devices: List[Dict] = field(default_factory=empty_list_factory) + # [{"type": "cp", "id": 0}, + # {"type": "io", "id": 1, "digital_output": "SofortLa"}] + max_import_power: int = 0 + fixed_import_power: float = 0 # don't show in UI + + +class LoadManagerSetup: + def __init__(self, + name: str = "Dimmen per Loadmanager", + type: str = "load_manager", + id: int = 0, + configuration: LoadManagerConfig = None): + self.name = name + self.id = id + self.configuration = configuration or LoadManagerConfig() + self.type = type + self.group = ActionGroup.CONTROLLABLE_CONSUMERS.value diff --git a/packages/modules/io_devices/load_manager/__init__.py b/packages/modules/io_devices/load_manager/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py new file mode 100644 index 0000000000..55861e9c57 --- /dev/null +++ b/packages/modules/io_devices/load_manager/api.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import logging + +from helpermodules.broker import BrokerClient +from helpermodules.utils.topic_parser import decode_payload +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import IoState +from modules.common.configurable_io import ConfigurableIo +from modules.io_devices.load_manager.config import AnalogInputMapping +from modules.io_devices.load_manager.config import LoadManager + +log = logging.getLogger(__name__) +control_command_log = logging.getLogger("steuve_control_command") + + +def create_io(config: LoadManager): + received_topics = {} + broker = None + thread_exception = None # Shared state für Thread-Exceptions + + def read(): + nonlocal broker + nonlocal received_topics + nonlocal thread_exception + + broker.start_finite_loop() + log.debug(f"Empfange MQTT Daten für LoadManager {config.id}: {received_topics}") + io_state = IoState() + io_state.analog_input = getattr(io_state, "analog_input", None) or {} + io_state.analog_output = getattr(io_state, "analog_output", None) or {} + io_state.digital_input = getattr(io_state, "digital_input", None) or {} + io_state.digital_output = getattr(io_state, "digital_output", None) or {} + + if received_topics.get("openWB/mqtt/loadmanager/set/loadmanager"): + payload = received_topics["openWB/mqtt/loadmanager/set/loadmanager"] + io_state.analog_input.update({AnalogInputMapping.MAX_POWER.name: payload["max_power"]}) + io_state.analog_input.update({AnalogInputMapping.MAX_CURRENT.name: payload["max_current"]}) + io_state.analog_input.update({AnalogInputMapping.TIMESTAMP.name: payload["timestamp"]}) + return io_state + + def initializer(): + nonlocal broker + nonlocal received_topics + + def on_connect(client, userdata, flags, rc): + client.subscribe("openWB/mqtt/loadmanager/#") + + def on_message(client, userdata, message): + received_topics.update({message.topic: decode_payload(message.payload)}) + + received_topics = {} + broker = BrokerClient("subscribeMqttLoadmanager", + on_connect, on_message) + + return ConfigurableIo(config=config, component_reader=read, component_writer=lambda: None, initializer=initializer) + + +device_descriptor = DeviceDescriptor(configuration_factory=LoadManager) diff --git a/packages/modules/io_devices/load_manager/config.py b/packages/modules/io_devices/load_manager/config.py new file mode 100644 index 0000000000..2830223fcf --- /dev/null +++ b/packages/modules/io_devices/load_manager/config.py @@ -0,0 +1,41 @@ +from enum import Enum +from typing import Dict, Union +from modules.common.io_setup import IoDeviceSetup + + +class AnalogInputMapping(Enum): + MAX_POWER = "max_power" + MAX_CURRENT = "max_current" + TIMESTAMP = "timestamp" + + +class LoadManagerConfiguration: + def __init__(self) -> None: + pass + + +def init_input(): + return {"analog": {pin.name: None for pin in AnalogInputMapping}} + + +def init_output(): + return {"analog": {}} + + +class LoadManager(IoDeviceSetup[LoadManagerConfiguration]): + def __init__(self, + name: str = "OpenWB LoadManager", + type: str = "load_manager", + id: Union[int, str] = 0, + configuration: LoadManagerConfiguration = None, + input: Dict[str, Dict[int, float]] = None, + output: Dict[str, Dict[int, float]] = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or LoadManagerConfiguration() + if input is None: + input = init_input() + if output is None: + output = init_output() + super().__init__(name, type, id, configuration or LoadManagerConfiguration(), input=input, output=output) From 94a8aa7c14815b453e535298938a4d23f615b630 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 7 Jul 2026 14:49:03 +0200 Subject: [PATCH 02/11] Remove unused thread-variable --- packages/modules/io_devices/load_manager/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py index 55861e9c57..fb7abb666f 100644 --- a/packages/modules/io_devices/load_manager/api.py +++ b/packages/modules/io_devices/load_manager/api.py @@ -20,10 +20,6 @@ def create_io(config: LoadManager): thread_exception = None # Shared state für Thread-Exceptions def read(): - nonlocal broker - nonlocal received_topics - nonlocal thread_exception - broker.start_finite_loop() log.debug(f"Empfange MQTT Daten für LoadManager {config.id}: {received_topics}") io_state = IoState() From 3e42a9a37d8a1e24b9f0a6c4b9cbfb8b193ef555 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 7 Jul 2026 14:52:01 +0200 Subject: [PATCH 03/11] Remove unused thread-variable --- packages/modules/io_devices/load_manager/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py index fb7abb666f..7c4104b216 100644 --- a/packages/modules/io_devices/load_manager/api.py +++ b/packages/modules/io_devices/load_manager/api.py @@ -17,7 +17,6 @@ def create_io(config: LoadManager): received_topics = {} broker = None - thread_exception = None # Shared state für Thread-Exceptions def read(): broker.start_finite_loop() From a44eebe13ccfca508c54072a3743cbbbd55267ee Mon Sep 17 00:00:00 2001 From: AlexanderHa98 Date: Tue, 7 Jul 2026 15:22:21 +0200 Subject: [PATCH 04/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/control/io_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/control/io_device.py b/packages/control/io_device.py index d84130011a..a789380e57 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -107,9 +107,9 @@ def stepwise_control(self, device_id: int) -> Tuple[Optional[float], Loadmanagem else: return None, LoadmanagementLimit(None, None) - def dimming_load_manager(self, device: Dict) -> Optional[float]: + def dimming_load_manager(self, device: Dict) -> Tuple[Optional[float], LoadmanagementLimit]: for action in self.actions.values(): - if isinstance(action, (DimmingLoadManager)): + if isinstance(action, DimmingLoadManager): for d in action.config.configuration.devices: if d == device: return action.dimming_get_import_power_left() From 89c6be7ae809913a01a32939552421c7fe8519ac Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 7 Jul 2026 15:41:32 +0200 Subject: [PATCH 05/11] Add Copilot suggestion --- packages/control/loadmanagement.py | 11 ++++++----- packages/helpermodules/setdata.py | 2 +- .../controllable_consumers/load_manager/api.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index 06fef83109..77197d570e 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -188,11 +188,12 @@ def _limit_by_ripple_control_receiver(self, def _limit_loadmanager(self, available_currents: List[float], cp: Chargepoint) -> Tuple[List[float], LoadmanagementLimit]: - dimming_power_left, limit = data.data.io_actions.dimming_load_manager({"type": "cp", "id": cp.num}) - if dimming_power_left is not None: - if sum(available_currents)*230 > dimming_power_left: + + loadmanager_power_left, limit = data.data.io_actions.dimming_load_manager({"type": "cp", "id": cp.num}) + if loadmanager_power_left is not None: + if sum(available_currents)*230 > loadmanager_power_left: phases = 3-available_currents.count(0) - overload_per_phase = (sum(available_currents) - dimming_power_left/230)/phases + overload_per_phase = (sum(available_currents) - loadmanager_power_left/230)/phases available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents] - log.debug(f"Reduzierung der Ströme durch die Dimmung: {available_currents}A") + log.debug(f"Reduzierung der Ströme durch den LoadManager: {available_currents}A") return available_currents, limit diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 2ba46096ed..4d9d05db22 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -1218,7 +1218,7 @@ def _get_ramdisk_path(self) -> Path: return Path(__file__).resolve().parents[2]/"ramdisk" def loadmanager_topic(self, msg: mqtt.MQTTMessage): - """ Handler für die LoadMananger-Topics + """ Handler für die LoadManager-Topics Parameters ---------- diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/api.py b/packages/modules/io_actions/controllable_consumers/load_manager/api.py index 0984583baa..863637ee27 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/api.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/api.py @@ -35,11 +35,11 @@ def __init__(self, config: LoadManagerSetup): def setup(self) -> None: if check_fault_state_io_device(self.config.configuration.io_device): - self.import_power_left = self.config.configuration.fixed_import_power + max_power = self.config.configuration.fixed_import_power else: max_power = data.data.io_states[f"io_states{self.config.configuration.io_device}" ].data.get.analog_input[AnalogInputMapping.MAX_POWER.name] - self.import_power_left = max_power + self.import_power_left = max_power def dimming_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): From 2ef39eda6b31fa219445e4260188e51362ab8ad5 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 9 Jul 2026 12:31:34 +0200 Subject: [PATCH 06/11] Add requested changes --- packages/control/io_device.py | 4 ++-- packages/control/loadmanagement.py | 4 ++-- .../controllable_consumers/load_manager/api.py | 18 +++--------------- .../load_manager/config.py | 4 ++-- .../modules/io_devices/load_manager/api.py | 2 +- .../modules/io_devices/load_manager/config.py | 2 +- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/control/io_device.py b/packages/control/io_device.py index a789380e57..c699cef08a 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -107,11 +107,11 @@ def stepwise_control(self, device_id: int) -> Tuple[Optional[float], Loadmanagem else: return None, LoadmanagementLimit(None, None) - def dimming_load_manager(self, device: Dict) -> Tuple[Optional[float], LoadmanagementLimit]: + def limit_loadmanager(self, device: Dict) -> Tuple[Optional[float], LoadmanagementLimit]: for action in self.actions.values(): if isinstance(action, DimmingLoadManager): for d in action.config.configuration.devices: if d == device: - return action.dimming_get_import_power_left() + return action.loadmanager_get_import_power_left() else: return None, LoadmanagementLimit(None, None) diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index 77197d570e..ec5dba62c9 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -189,11 +189,11 @@ def _limit_loadmanager(self, available_currents: List[float], cp: Chargepoint) -> Tuple[List[float], LoadmanagementLimit]: - loadmanager_power_left, limit = data.data.io_actions.dimming_load_manager({"type": "cp", "id": cp.num}) + loadmanager_power_left, limit = data.data.io_actions.limit_loadmanager({"type": "cp", "id": cp.num}) if loadmanager_power_left is not None: if sum(available_currents)*230 > loadmanager_power_left: phases = 3-available_currents.count(0) overload_per_phase = (sum(available_currents) - loadmanager_power_left/230)/phases available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents] - log.debug(f"Reduzierung der Ströme durch den LoadManager: {available_currents}A") + log.debug(f"Reduzierung der Ströme durch den Lastmanager: {available_currents}A") return available_currents, limit diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/api.py b/packages/modules/io_actions/controllable_consumers/load_manager/api.py index 863637ee27..cc0cd51acc 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/api.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/api.py @@ -2,8 +2,6 @@ from typing import Optional, Tuple from control import data from control.limiting_value import LimitingValue, LoadmanagementLimit -from helpermodules.pub import Pub -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 @@ -20,28 +18,18 @@ class DimmingLoadManager(AbstractIoAction): def __init__(self, config: LoadManagerSetup): self.config = config self.import_power_left = None - control_command_log.info("Dimmen per LoadManager.") - - fixed_import_power = 0 - for device in self.config.configuration.devices: - if device["type"] != "cp": - fixed_import_power += 4200 - log.debug(f"Dimmen per LoadManager: Fest vergebene Mindestleistung: {fixed_import_power}W") - if fixed_import_power != self.config.configuration.fixed_import_power: - self.config.configuration.fixed_import_power = fixed_import_power - Pub().pub(f"openWB/set/io/action/{self.config.id}/config", asdict(self.config)) - + control_command_log.info("Begrenzung per Lastmanager.") super().__init__() def setup(self) -> None: if check_fault_state_io_device(self.config.configuration.io_device): - max_power = self.config.configuration.fixed_import_power + max_power = self.config.configuration.max_power_on_failure else: max_power = data.data.io_states[f"io_states{self.config.configuration.io_device}" ].data.get.analog_input[AnalogInputMapping.MAX_POWER.name] self.import_power_left = max_power - def dimming_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: + def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): return (self.import_power_left, LoadmanagementLimit( LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/config.py b/packages/modules/io_actions/controllable_consumers/load_manager/config.py index b4f24f4f3e..ff01a4a63b 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/config.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/config.py @@ -12,12 +12,12 @@ class LoadManagerConfig: # [{"type": "cp", "id": 0}, # {"type": "io", "id": 1, "digital_output": "SofortLa"}] max_import_power: int = 0 - fixed_import_power: float = 0 # don't show in UI + max_power_on_failure: float = 0 class LoadManagerSetup: def __init__(self, - name: str = "Dimmen per Loadmanager", + name: str = "Begrenzung per Lastmanager", type: str = "load_manager", id: int = 0, configuration: LoadManagerConfig = None): diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py index 7c4104b216..72d0209e87 100644 --- a/packages/modules/io_devices/load_manager/api.py +++ b/packages/modules/io_devices/load_manager/api.py @@ -20,7 +20,7 @@ def create_io(config: LoadManager): def read(): broker.start_finite_loop() - log.debug(f"Empfange MQTT Daten für LoadManager {config.id}: {received_topics}") + log.debug(f"Empfange MQTT Daten für Lastmanager {config.id}: {received_topics}") io_state = IoState() io_state.analog_input = getattr(io_state, "analog_input", None) or {} io_state.analog_output = getattr(io_state, "analog_output", None) or {} diff --git a/packages/modules/io_devices/load_manager/config.py b/packages/modules/io_devices/load_manager/config.py index 2830223fcf..810ee573b8 100644 --- a/packages/modules/io_devices/load_manager/config.py +++ b/packages/modules/io_devices/load_manager/config.py @@ -24,7 +24,7 @@ def init_output(): class LoadManager(IoDeviceSetup[LoadManagerConfiguration]): def __init__(self, - name: str = "OpenWB LoadManager", + name: str = "openWB Lastmanager", type: str = "load_manager", id: Union[int, str] = 0, configuration: LoadManagerConfiguration = None, From 498fb9b92a7538c9bed6ed0c5519e491c8f062ad Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 9 Jul 2026 13:47:21 +0200 Subject: [PATCH 07/11] Add max_current und Timestamp check --- .../controllable_consumers/load_manager/api.py | 7 +++++-- .../controllable_consumers/load_manager/config.py | 1 + packages/modules/io_devices/load_manager/api.py | 10 +++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/api.py b/packages/modules/io_actions/controllable_consumers/load_manager/api.py index cc0cd51acc..f4847033b2 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/api.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/api.py @@ -11,24 +11,27 @@ from modules.common.abstract_device import DeviceDescriptor log = logging.getLogger(__name__) -control_command_log = logging.getLogger("steuve_control_command") class DimmingLoadManager(AbstractIoAction): def __init__(self, config: LoadManagerSetup): self.config = config self.import_power_left = None - control_command_log.info("Begrenzung per Lastmanager.") super().__init__() def setup(self) -> None: if check_fault_state_io_device(self.config.configuration.io_device): max_power = self.config.configuration.max_power_on_failure + max_current = self.config.configuration.max_current_on_failure else: max_power = data.data.io_states[f"io_states{self.config.configuration.io_device}" ].data.get.analog_input[AnalogInputMapping.MAX_POWER.name] + max_current = data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.analog_input[AnalogInputMapping.MAX_CURRENT.name] self.import_power_left = max_power + # Wie verrechne ich power und current? + def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): return (self.import_power_left, LoadmanagementLimit( diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/config.py b/packages/modules/io_actions/controllable_consumers/load_manager/config.py index ff01a4a63b..8c066b1e75 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/config.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/config.py @@ -13,6 +13,7 @@ class LoadManagerConfig: # {"type": "io", "id": 1, "digital_output": "SofortLa"}] max_import_power: int = 0 max_power_on_failure: float = 0 + max_current_on_failure: float = 0 class LoadManagerSetup: diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py index 72d0209e87..fc7e99af41 100644 --- a/packages/modules/io_devices/load_manager/api.py +++ b/packages/modules/io_devices/load_manager/api.py @@ -2,6 +2,7 @@ import logging +from helpermodules import timecheck from helpermodules.broker import BrokerClient from helpermodules.utils.topic_parser import decode_payload from modules.common.abstract_device import DeviceDescriptor @@ -29,9 +30,16 @@ def read(): if received_topics.get("openWB/mqtt/loadmanager/set/loadmanager"): payload = received_topics["openWB/mqtt/loadmanager/set/loadmanager"] + + timestamp = float(payload["timestamp"]) + if timestamp > 1e10: + timestamp /= 1000 + if timecheck.create_timestamp() - timestamp > 60: + raise Exception(f"Lastmanager-Daten sind veraltet (Timestamp: {timestamp}).") + io_state.analog_input.update({AnalogInputMapping.MAX_POWER.name: payload["max_power"]}) io_state.analog_input.update({AnalogInputMapping.MAX_CURRENT.name: payload["max_current"]}) - io_state.analog_input.update({AnalogInputMapping.TIMESTAMP.name: payload["timestamp"]}) + io_state.analog_input.update({AnalogInputMapping.TIMESTAMP.name: timestamp}) return io_state def initializer(): From a2eb9418f48e062504cadddd85022631bd69590b Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 10 Jul 2026 15:18:14 +0200 Subject: [PATCH 08/11] Update Loadmanager --- packages/control/algorithm/common.py | 2 ++ packages/control/io_device.py | 17 +++++++++---- packages/control/limiting_value.py | 1 + packages/control/loadmanagement.py | 25 ++++++++++++++++--- .../load_manager/api.py | 24 ++++++++++++++---- .../modules/io_devices/load_manager/api.py | 7 ++++-- 6 files changed, 61 insertions(+), 15 deletions(-) diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 60f81c7fe5..68d1ebc86b 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -82,6 +82,7 @@ def set_current_counterdiff(diff_current: float, diffs, voltages_mean(chargepoint.data.get.voltages)) data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) + data.data.io_actions.set_limit_loadmanager({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) chargepoint.data.set.current = current log.info(f"LP{chargepoint.num}: Stromstärke {current}A") @@ -156,6 +157,7 @@ def update_raw_data(preferenced_chargepoints: List[Chargepoint], else: data.data.counter_data[counter].update_values_left(diffs, voltages_mean(chargepoint.data.get.voltages)) data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) + data.data.io_actions.set_limit_loadmanager({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) def consider_less_charging_chargepoint_in_loadmanagement(cp: Chargepoint, set_current: float) -> bool: diff --git a/packages/control/io_device.py b/packages/control/io_device.py index c699cef08a..678ff6f6af 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -5,7 +5,7 @@ from modules.io_actions.controllable_consumers.dimming.api_eebus import DimmingEebus from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo -from modules.io_actions.controllable_consumers.load_manager.api import DimmingLoadManager +from modules.io_actions.controllable_consumers.load_manager.api import LoadManager from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl from modules.io_actions.controllable_consumers.ripple_control_receiver.api import RippleControlReceiver @@ -59,7 +59,7 @@ class IoActions: def __init__(self): self.actions: Dict[int, Union[DimmingIo, DimmingEebus, DimmingDirectControl, RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo, - DimmingLoadManager]] = {} + LoadManager]] = {} def setup(self): for action in self.actions.values(): @@ -107,11 +107,18 @@ def stepwise_control(self, device_id: int) -> Tuple[Optional[float], Loadmanagem else: return None, LoadmanagementLimit(None, None) - def limit_loadmanager(self, device: Dict) -> Tuple[Optional[float], LoadmanagementLimit]: + def get_limit_loadmanager(self, device: Dict) -> Tuple[Optional[float], Optional[float], LoadmanagementLimit]: for action in self.actions.values(): - if isinstance(action, DimmingLoadManager): + if isinstance(action, LoadManager): for d in action.config.configuration.devices: if d == device: return action.loadmanager_get_import_power_left() else: - return None, LoadmanagementLimit(None, None) + return None, None, LoadmanagementLimit(None, None) + + def set_limit_loadmanager(self, device: Dict, used_power: float) -> Optional[float]: + for action in self.actions.values(): + if isinstance(action, LoadManager): + for d in action.config.configuration.devices: + if d == device: + return action.loadmanager_set_import_power_left(used_power, used_power/230) diff --git a/packages/control/limiting_value.py b/packages/control/limiting_value.py index e4b32a16cf..b023f4f546 100644 --- a/packages/control/limiting_value.py +++ b/packages/control/limiting_value.py @@ -7,6 +7,7 @@ class LimitingValue(Enum): CURRENT = ", da der Maximal-Strom an Zähler {} erreicht ist." POWER = ", da die maximale Leistung an Zähler {} erreicht ist." UNBALANCED_LOAD = ", da die maximale Schieflast an Zähler {} erreicht ist." + LOADMANAGER = ", da der Lastmanager die Ladeleistung begrenzt." DIMMING = ", da die Dimmung die Ladeleistung begrenzt." DIMMING_VIA_DIRECT_CONTROL = ", da die Dimmung per Direkt-Steuerung die Ladeleistung auf 4,2 kW begrenzt." RIPPLE_CONTROL_RECEIVER = (", da der Ladepunkt durch den RSE-Kontakt auf {}% der konfigurierten Anschlussleistung " diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index ec5dba62c9..af9049029d 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -189,11 +189,30 @@ def _limit_loadmanager(self, available_currents: List[float], cp: Chargepoint) -> Tuple[List[float], LoadmanagementLimit]: - loadmanager_power_left, limit = data.data.io_actions.limit_loadmanager({"type": "cp", "id": cp.num}) - if loadmanager_power_left is not None: + loadmanager_power_left, loadmanager_current_left, limit = data.data.io_actions.get_limit_loadmanager({ + "type": "cp", "id": cp.num}) + + if loadmanager_power_left is not None and loadmanager_current_left is not None: if sum(available_currents)*230 > loadmanager_power_left: + # --REDU + # wie viel phasen sind im Einsatz? phases = 3-available_currents.count(0) + # Wie viel ist jede Phase zu hoch overload_per_phase = (sum(available_currents) - loadmanager_power_left/230)/phases + # Ziehe das für jede Phase ab, die im Einsatz ist. available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents] - log.debug(f"Reduzierung der Ströme durch den Lastmanager: {available_currents}A") + log.debug( + f"#######_Reduzierung der Ströme auf {sum(available_currents)*230}W durch den Lastmanager. " + f"(Leistungsgrenze: {loadmanager_power_left}W)") + return available_currents, limit + elif sum(available_currents) > loadmanager_current_left: + # --REDU + phases = 3 - available_currents.count(0) + overload_per_phase = (sum(available_currents) - loadmanager_current_left) / phases + available_currents = [max(c - overload_per_phase, 0) if c > 0 else 0 for c in available_currents] + log.debug( + f"#######_Reduzierung der Ströme auf {available_currents}A durch den Lastmanager. " + f"(Stromgrenze: {loadmanager_current_left}A)") + return available_currents, limit + # muss nix reduzieren. Passt alles so return available_currents, limit diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/api.py b/packages/modules/io_actions/controllable_consumers/load_manager/api.py index f4847033b2..a626c5ce1a 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/api.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/api.py @@ -13,14 +13,16 @@ log = logging.getLogger(__name__) -class DimmingLoadManager(AbstractIoAction): +class LoadManager(AbstractIoAction): def __init__(self, config: LoadManagerSetup): self.config = config self.import_power_left = None + self.import_current_left = None super().__init__() def setup(self) -> None: if check_fault_state_io_device(self.config.configuration.io_device): + log.warning("Fehler des IO-Geräts: Lastmanager aktiviert für Failsafe-Modus.") max_power = self.config.configuration.max_power_on_failure max_current = self.config.configuration.max_current_on_failure else: @@ -29,8 +31,7 @@ def setup(self) -> None: max_current = data.data.io_states[f"io_states{self.config.configuration.io_device}" ].data.get.analog_input[AnalogInputMapping.MAX_CURRENT.name] self.import_power_left = max_power - - # Wie verrechne ich power und current? + self.import_current_left = max_current def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): @@ -38,11 +39,24 @@ def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], Loadmanage LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( self.config.configuration.io_device)), LimitingValue.CONTROLLABLE_CONSUMERS_ERROR)) - return self.import_power_left, LoadmanagementLimit(LimitingValue.DIMMING.value, LimitingValue.DIMMING) + return self.import_power_left, self.import_current_left, LoadmanagementLimit(LimitingValue.LOADMANAGER.value, + LimitingValue.LOADMANAGER) + + def loadmanager_set_import_power_left(self, used_power: float, used_current: float) -> None: + if check_fault_state_io_device(self.config.configuration.io_device): + return (self.import_power_left, LoadmanagementLimit( + LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( + self.config.configuration.io_device)), + LimitingValue.CONTROLLABLE_CONSUMERS_ERROR)) + self.import_power_left -= used_power + self.import_current_left -= used_current + log.debug( + f"verbleibende Dimm-Leistung: {self.import_power_left}W, verbleibender Strom: {self.import_current_left}A") + return self.import_power_left def create_action(config: LoadManagerSetup, parent_device_type: str): - return DimmingLoadManager(config=config) + return LoadManager(config=config) device_descriptor = DeviceDescriptor(configuration_factory=LoadManagerSetup) diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py index fc7e99af41..73e5447924 100644 --- a/packages/modules/io_devices/load_manager/api.py +++ b/packages/modules/io_devices/load_manager/api.py @@ -34,8 +34,11 @@ def read(): timestamp = float(payload["timestamp"]) if timestamp > 1e10: timestamp /= 1000 - if timecheck.create_timestamp() - timestamp > 60: - raise Exception(f"Lastmanager-Daten sind veraltet (Timestamp: {timestamp}).") + age_s = timecheck.create_timestamp() - timestamp + # < = deaktiviert + if age_s < 60: + log.warning("Lastmanager-Daten veraltet: age=%.1fs, timestamp=%s", age_s, timestamp) + raise RuntimeError(f"Lastmanager-Daten sind veraltet: age={age_s:.1f}s, timestamp={timestamp}.") io_state.analog_input.update({AnalogInputMapping.MAX_POWER.name: payload["max_power"]}) io_state.analog_input.update({AnalogInputMapping.MAX_CURRENT.name: payload["max_current"]}) From c1cc1b5e996aa821304a908c86e5af004de7dc9a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 13 Jul 2026 10:45:20 +0200 Subject: [PATCH 09/11] Fix timestamp error-handling --- packages/control/limiting_value.py | 1 + packages/control/loadmanagement.py | 12 +++-------- .../load_manager/api.py | 20 ++++++------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/control/limiting_value.py b/packages/control/limiting_value.py index b023f4f546..0ccd5fff38 100644 --- a/packages/control/limiting_value.py +++ b/packages/control/limiting_value.py @@ -8,6 +8,7 @@ class LimitingValue(Enum): POWER = ", da die maximale Leistung an Zähler {} erreicht ist." UNBALANCED_LOAD = ", da die maximale Schieflast an Zähler {} erreicht ist." LOADMANAGER = ", da der Lastmanager die Ladeleistung begrenzt." + LOADMANAGER_ERROR = ", da aufgrund eines Fehlers im Lastmanager die Ladeleistung auf die maximale Leistung im Fehlerfall begrenzt wird." DIMMING = ", da die Dimmung die Ladeleistung begrenzt." DIMMING_VIA_DIRECT_CONTROL = ", da die Dimmung per Direkt-Steuerung die Ladeleistung auf 4,2 kW begrenzt." RIPPLE_CONTROL_RECEIVER = (", da der Ladepunkt durch den RSE-Kontakt auf {}% der konfigurierten Anschlussleistung " diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index af9049029d..261de5e61c 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -194,25 +194,19 @@ def _limit_loadmanager(self, if loadmanager_power_left is not None and loadmanager_current_left is not None: if sum(available_currents)*230 > loadmanager_power_left: - # --REDU - # wie viel phasen sind im Einsatz? phases = 3-available_currents.count(0) - # Wie viel ist jede Phase zu hoch overload_per_phase = (sum(available_currents) - loadmanager_power_left/230)/phases - # Ziehe das für jede Phase ab, die im Einsatz ist. available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents] log.debug( - f"#######_Reduzierung der Ströme auf {sum(available_currents)*230}W durch den Lastmanager. " + f"Reduzierung der Leistung auf {sum(available_currents)*230}W durch den Lastmanager. " f"(Leistungsgrenze: {loadmanager_power_left}W)") return available_currents, limit - elif sum(available_currents) > loadmanager_current_left: - # --REDU + if sum(available_currents) > loadmanager_current_left: phases = 3 - available_currents.count(0) overload_per_phase = (sum(available_currents) - loadmanager_current_left) / phases available_currents = [max(c - overload_per_phase, 0) if c > 0 else 0 for c in available_currents] log.debug( - f"#######_Reduzierung der Ströme auf {available_currents}A durch den Lastmanager. " + f"Reduzierung der Ströme auf {available_currents}A durch den Lastmanager. " f"(Stromgrenze: {loadmanager_current_left}A)") return available_currents, limit - # muss nix reduzieren. Passt alles so return available_currents, limit diff --git a/packages/modules/io_actions/controllable_consumers/load_manager/api.py b/packages/modules/io_actions/controllable_consumers/load_manager/api.py index a626c5ce1a..b419b04624 100644 --- a/packages/modules/io_actions/controllable_consumers/load_manager/api.py +++ b/packages/modules/io_actions/controllable_consumers/load_manager/api.py @@ -3,7 +3,6 @@ from control import data from control.limiting_value import LimitingValue, LoadmanagementLimit 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.controllable_consumers.load_manager.config import LoadManagerSetup from modules.io_devices.load_manager.config import AnalogInputMapping @@ -28,31 +27,24 @@ def setup(self) -> None: else: max_power = data.data.io_states[f"io_states{self.config.configuration.io_device}" ].data.get.analog_input[AnalogInputMapping.MAX_POWER.name] - max_current = data.data.io_states[f"io_states{self.config.configuration.io_device}" - ].data.get.analog_input[AnalogInputMapping.MAX_CURRENT.name] + max_current = sum(data.data.io_states[f"io_states{self.config.configuration.io_device}" + ].data.get.analog_input[AnalogInputMapping.MAX_CURRENT.name]) self.import_power_left = max_power self.import_current_left = max_current - def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], LoadmanagementLimit]: + def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], Optional[float], LoadmanagementLimit]: if check_fault_state_io_device(self.config.configuration.io_device): - return (self.import_power_left, LoadmanagementLimit( - LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( - self.config.configuration.io_device)), - LimitingValue.CONTROLLABLE_CONSUMERS_ERROR)) + return (self.import_power_left, self.import_current_left, LoadmanagementLimit( + LimitingValue.LOADMANAGER_ERROR.value, + LimitingValue.LOADMANAGER_ERROR)) return self.import_power_left, self.import_current_left, LoadmanagementLimit(LimitingValue.LOADMANAGER.value, LimitingValue.LOADMANAGER) def loadmanager_set_import_power_left(self, used_power: float, used_current: float) -> None: - if check_fault_state_io_device(self.config.configuration.io_device): - return (self.import_power_left, LoadmanagementLimit( - LimitingValue.CONTROLLABLE_CONSUMERS_ERROR.value.format(get_io_name_by_id( - self.config.configuration.io_device)), - LimitingValue.CONTROLLABLE_CONSUMERS_ERROR)) self.import_power_left -= used_power self.import_current_left -= used_current log.debug( f"verbleibende Dimm-Leistung: {self.import_power_left}W, verbleibender Strom: {self.import_current_left}A") - return self.import_power_left def create_action(config: LoadManagerSetup, parent_device_type: str): From e990e5df86cd435d88ef9cba9339ce75b05b5cd4 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 13 Jul 2026 10:46:14 +0200 Subject: [PATCH 10/11] Fix row-length --- packages/control/limiting_value.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/control/limiting_value.py b/packages/control/limiting_value.py index 0ccd5fff38..c93d11772c 100644 --- a/packages/control/limiting_value.py +++ b/packages/control/limiting_value.py @@ -8,7 +8,8 @@ class LimitingValue(Enum): POWER = ", da die maximale Leistung an Zähler {} erreicht ist." UNBALANCED_LOAD = ", da die maximale Schieflast an Zähler {} erreicht ist." LOADMANAGER = ", da der Lastmanager die Ladeleistung begrenzt." - LOADMANAGER_ERROR = ", da aufgrund eines Fehlers im Lastmanager die Ladeleistung auf die maximale Leistung im Fehlerfall begrenzt wird." + LOADMANAGER_ERROR = (", da aufgrund eines Fehlers im Lastmanager die Ladeleistung auf die maximale Leistung " + "im Fehlerfall begrenzt wird.") DIMMING = ", da die Dimmung die Ladeleistung begrenzt." DIMMING_VIA_DIRECT_CONTROL = ", da die Dimmung per Direkt-Steuerung die Ladeleistung auf 4,2 kW begrenzt." RIPPLE_CONTROL_RECEIVER = (", da der Ladepunkt durch den RSE-Kontakt auf {}% der konfigurierten Anschlussleistung " From 7e32982b84d98c2dd143051ca50a0d0055d20935 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 13 Jul 2026 15:54:03 +0200 Subject: [PATCH 11/11] Add id to MQTT-msg --- packages/control/loadmanagement.py | 9 +++++---- packages/helpermodules/setdata.py | 2 +- packages/modules/io_devices/load_manager/api.py | 11 ++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index 261de5e61c..dbfb3c67b6 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -193,13 +193,14 @@ def _limit_loadmanager(self, "type": "cp", "id": cp.num}) if loadmanager_power_left is not None and loadmanager_current_left is not None: - if sum(available_currents)*230 > loadmanager_power_left: + if sum(available_currents)*voltages_mean(cp.data.get.voltages) > loadmanager_power_left: phases = 3-available_currents.count(0) - overload_per_phase = (sum(available_currents) - loadmanager_power_left/230)/phases + overload_per_phase = (sum(available_currents) - + loadmanager_power_left/voltages_mean(cp.data.get.voltages))/phases available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents] log.debug( - f"Reduzierung der Leistung auf {sum(available_currents)*230}W durch den Lastmanager. " - f"(Leistungsgrenze: {loadmanager_power_left}W)") + f"Reduzierung der Leistung auf {sum(available_currents)*voltages_mean(cp.data.get.voltages)}W " + f"durch den Lastmanager. (Leistungsgrenze: {loadmanager_power_left}W)") return available_currents, limit if sum(available_currents) > loadmanager_current_left: phases = 3 - available_currents.count(0) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 4d9d05db22..5108ef7530 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -1226,7 +1226,7 @@ def loadmanager_topic(self, msg: mqtt.MQTTMessage): enthält Topic und Payload """ try: - if "openWB/set/mqtt/loadmanager/set/loadmanager" in msg.topic: + if re.search("^openWB/set/mqtt/loadmanager/[0-9]+/set/loadmanager$", msg.topic) is not None: self._validate_value(msg, "json") else: self.__unknown_topic(msg) diff --git a/packages/modules/io_devices/load_manager/api.py b/packages/modules/io_devices/load_manager/api.py index 73e5447924..67ab50e734 100644 --- a/packages/modules/io_devices/load_manager/api.py +++ b/packages/modules/io_devices/load_manager/api.py @@ -28,16 +28,13 @@ def read(): io_state.digital_input = getattr(io_state, "digital_input", None) or {} io_state.digital_output = getattr(io_state, "digital_output", None) or {} - if received_topics.get("openWB/mqtt/loadmanager/set/loadmanager"): - payload = received_topics["openWB/mqtt/loadmanager/set/loadmanager"] + if received_topics.get(f"openWB/mqtt/loadmanager/{config.id}/set/loadmanager"): + payload = received_topics[f"openWB/mqtt/loadmanager/{config.id}/set/loadmanager"] timestamp = float(payload["timestamp"]) - if timestamp > 1e10: - timestamp /= 1000 age_s = timecheck.create_timestamp() - timestamp # < = deaktiviert - if age_s < 60: - log.warning("Lastmanager-Daten veraltet: age=%.1fs, timestamp=%s", age_s, timestamp) + if age_s > 60: raise RuntimeError(f"Lastmanager-Daten sind veraltet: age={age_s:.1f}s, timestamp={timestamp}.") io_state.analog_input.update({AnalogInputMapping.MAX_POWER.name: payload["max_power"]}) @@ -50,7 +47,7 @@ def initializer(): nonlocal received_topics def on_connect(client, userdata, flags, rc): - client.subscribe("openWB/mqtt/loadmanager/#") + client.subscribe(f"openWB/mqtt/loadmanager/{config.id}/#") def on_message(client, userdata, message): received_topics.update({message.topic: decode_payload(message.payload)})