Add LoadManager#3636
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “LoadManager” integration that ingests import-limit data via MQTT as an IO device and applies it as an additional limiting step in the charging loadmanagement pipeline.
Changes:
- Introduces a new MQTT-based IO device module for LoadManager analog inputs (max power/current/timestamp).
- Adds a new controllable-consumers action (
DimmingLoadManager) and wires it into loadmanagement limiting. - Extends MQTT set-topic validation to include the new loadmanager topic.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/modules/io_devices/load_manager/config.py | Defines LoadManager IO device config and analog input mapping. |
| packages/modules/io_devices/load_manager/api.py | Subscribes to internal MQTT topics and populates IoState analog inputs for the LoadManager. |
| packages/modules/io_devices/load_manager/init.py | Module package initializer for the new IO device. |
| packages/modules/io_actions/controllable_consumers/load_manager/config.py | Adds configuration/setup types for the new controllable-consumers LoadManager action. |
| packages/modules/io_actions/controllable_consumers/load_manager/api.py | Implements DimmingLoadManager action that provides remaining import power for loadmanagement. |
| packages/helpermodules/setdata.py | Validates the new openWB/set/mqtt/loadmanager/... topic so it is forwarded to internal MQTT. |
| packages/control/loadmanagement.py | Adds a new _limit_loadmanager limiting step in get_available_currents. |
| packages/control/io_device.py | Registers the new IO action type and exposes it via IoActions.dimming_load_manager(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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 |
| 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 |
| return Path(__file__).resolve().parents[2]/"ramdisk" | ||
|
|
||
| def loadmanager_topic(self, msg: mqtt.MQTTMessage): | ||
| """ Handler für die LoadMananger-Topics |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| 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) |
| broker = BrokerClient("subscribeMqttLoadmanager", | ||
| on_connect, on_message) | ||
|
|
||
| return ConfigurableIo(config=config, component_reader=read, component_writer=lambda: None, initializer=initializer) |
| return Path(__file__).resolve().parents[2]/"ramdisk" | ||
|
|
||
| def loadmanager_topic(self, msg: mqtt.MQTTMessage): | ||
| """ Handler für die LoadMananger-Topics |
| 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 |
| 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 |
…8/core into feature_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)) |
There was a problem hiding this comment.
fixed_import_power gibt es beim Lastmanager nicht
|
|
||
| def setup(self) -> None: | ||
| if check_fault_state_io_device(self.config.configuration.io_device): | ||
| max_power = self.config.configuration.fixed_import_power |
There was a problem hiding this comment.
fixed_import_power gibt es beim Lastmanager nicht, sondern "maximale Leistung im Fehlerfall"
LKuemmel
left a comment
There was a problem hiding this comment.
Es kommen auch max_currents rein, die müssen auch berücksichtigt werden ;)
Und bitte noch eine Prüfung des Timestamps im IO-Device einbauen. Wenn der älter als 60s ist, eine Exception werfen.
| 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.") |
There was a problem hiding this comment.
Einträge im control_command_log braucht man nicht. Bei §14a muss für den Netzbetreiber dokumentiert werden, das und wie viel gedimmt wurde, dafür ist dieses Log da.
| "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: |
There was a problem hiding this comment.
Es gab irgendwann mal den Wunsch von der Community, dass das Lastmanagement genauer rechnen soll. Also mit der tatsächlichen Spannung voltages_mean(cp.data.get.voltages) statt 230V
| if timestamp > 1e10: | ||
| timestamp /= 1000 |
There was a problem hiding this comment.
| if timestamp > 1e10: | |
| timestamp /= 1000 |
Der Timestamp sollte als Unix-Timestamp kommen.
| age_s = timecheck.create_timestamp() - timestamp | ||
| # < = deaktiviert | ||
| if age_s < 60: | ||
| log.warning("Lastmanager-Daten veraltet: age=%.1fs, timestamp=%s", age_s, timestamp) |
There was a problem hiding this comment.
| log.warning("Lastmanager-Daten veraltet: age=%.1fs, timestamp=%s", age_s, timestamp) |
Die Exception sollte ohnehin geloggt werden. Dann kann die Warnung weg, sonst ist es doppelt.
| 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"): |
There was a problem hiding this comment.
Für die eindeutige Zuordnung wäre es gut, wenn noch die ID vom IO-Gerät im Topic ist. Dann ist es auch kein Problem, wenn mehrere Lastmanager konfiguriert werden -> openWB/mqtt/loadmanager/1/set/loadmanager
Das müsste dann auch noch in der setdata angepasst werden.
openWB/openwb-ui-settings#1007
Aufbau
GUI
MQTT-Topic
openWB/set/mqtt/loadmanager/set/{id}/loadmanagerBeispiel-Payload
{ "max_power": 1000, "max_current": [6, 6, 6], "timestamp": 1783596593.635847 }Funktionsweise
Lastmanagement
Der Lastmanager berechnet die verbleibend verfügbare Leistung und den maximal zulässigen Strom.
Dabei erfolgt die Begrenzung in folgender Reihenfolge:
Prüfung der Leistungsgrenze (über MQTT vorgegeben
max_power).Prüfung der Stromgrenze (über MQTT vorgegeben
max_current).Sind weder Leistungs- noch Stromgrenze erreicht, wird keine Reduzierung durchgeführt.
Fehlerfall
Wenn der
timestampälter als 60 Sekunden ist:RuntimeErrorausgelöst.