diff --git a/docs/EZA-Beispielkonfiguration.png b/docs/EZA-Beispielkonfiguration.png new file mode 100644 index 0000000000..19980e4a5f Binary files /dev/null and b/docs/EZA-Beispielkonfiguration.png differ diff --git "a/docs/IO-Ger\303\244te & -Aktionen.md" "b/docs/IO-Ger\303\244te & -Aktionen.md" index bac6bd4be8..05b90d7ad9 100644 --- "a/docs/IO-Ger\303\244te & -Aktionen.md" +++ "b/docs/IO-Ger\303\244te & -Aktionen.md" @@ -27,7 +27,13 @@ Die AddOn-Platine stellt 7 Eingänge und 3 Ausgänge zur Verfügung. WICHTIG: In ### Steuerbare Verbrauchseinrichtungen: Dimmen per EMS, Dimmung per Direkt-Steuerung, RSE -Ausführliche Informationen findest Du im gesonderten Wiki-Beitrag [Steuerbare Verbrauchseinrichtungen](https://github.com/openWB/core/wiki/Steuerbare-Verbrauchseinrichtungen-nach-§14a) +Ausführliche Informationen findest Du im gesonderten Wiki-Beitrag [Steuerbare Einrichtungen nach § 14a EnGW und § 9 EEG](https://github.com/openWB/core/wiki/Steuerbare-Verbrauchseinrichtungen-nach-§14a) + +### Steuerbare Erzeugungseinrichtungen: Stufenweise Steuerung + +Bitte beachten: Die openWB steuert keinen Wechselrichter an. Sie zeigt lediglich den aktuellen Zustand der Beschränkung an und kann optional das Signal der Eingänge an Ausgänge durchreichen. +Ausführliche Informationen findest Du im gesonderten Wiki-Beitrag [Steuerbare Einrichtungen nach § 14a EnGW und § 9 EEG](https://github.com/openWB/core/wiki/Steuerbare-Verbrauchseinrichtungen-nach-§14a) ## Manuelles Setzen der Ausgänge -Die Ausgänge aller IO-Geräte können per MQTT gesetzt werden. Die Topics findet Ihr in den Einstellungen des jeweiligen Geräts als Copy-to-Clipboard-Link. Das manuelle Setzen des Ausgangs überschreibt den Wert, den zB die openWB bei einer IO-Aktion gesetzt hat. \ No newline at end of file + +Die Ausgänge aller IO-Geräte können per MQTT gesetzt werden. Die Topics findet Ihr in den Einstellungen des jeweiligen Geräts als Copy-to-Clipboard-Link. Das manuelle Setzen des Ausgangs überschreibt den Wert, den zB die openWB bei einer IO-Aktion gesetzt hat. diff --git a/docs/RSE-Beispielkonfiguration.png b/docs/RSE-Beispielkonfiguration.png new file mode 100644 index 0000000000..5d1e14a646 Binary files /dev/null and b/docs/RSE-Beispielkonfiguration.png differ diff --git "a/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" "b/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" index d4cbb44c5b..77a573797b 100644 --- "a/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" +++ "b/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" @@ -1,3 +1,5 @@ +## Steuerbare Verbrauchseinrichtungen (SteuVE) nach § 14a EnGW + Der Gesetzgeber sieht verschiedene Möglichkeiten für steuerbare Verbrauchseinrichtungen vor. Für jede steuerbare Verbrauchseinrichtung kann eine andere Option angemeldet werden. Bei der Konfiguration muss deshalb auch immer der/die Ladepunkte angegeben werden, für die die IO-Aktion angewendet werden soll. ### Dimmen per EMS @@ -12,4 +14,26 @@ Pro steuerbarer Verbrauchseinrichtung muss eine IO-Aktion konfiguriert werden un ### Rundsteuer-Empfänger-Kontakt (RSE) -Für den RSE-Kontakt kann ein Muster aus verschiedenen Eingängen und ein Prozentwert, auf den die Anschlussleistung begrenzt wird, angegeben werden. +Für den RSE-Kontakt können Muster aus verschiedenen Eingängen angegeben werden. Es kann frei festgelegt werden, bei welchem Muster die zugeordneten Ladepunkte Gesperrt oder freigegeben sind. + +In der abgebildeten Konfiguration werden die Ladepunkte nur freigegeben, wenn beide Kontakte DI1 und DI2 geschlossen sind. Ist auch nur einer geöffnet, wird gesperrt. + +![RSE-Beispielkonfiguration](RSE-Beispielkonfiguration.png) + +## Steuerbare Erzeugungsanlagen (EZA) nach § 9 EEG + +Bitte beachten: Die openWB steuert keinen Wechselrichter an. Sie zeigt lediglich den aktuellen Zustand der Beschränkung an und kann optional das Signal der Eingänge an Ausgänge durchreichen. + +Die Einspeise- oder Erzeugungsleistung der EZA (abhängig von der Implementierung in der EZA) wird über drei potentialfreie Signalkontakte der FNN-Steuerbox geregelt. Die openWB übernimmt dabei keine direkte Steuerung des Wechselrichters, sondern visualisiert lediglich und protokolliert den aktuellen Steuerzustand. + +Das Signalkabel der FNN-Steuerbox muss daher beispielsweise über ein Koppelrelais mit zwei separaten Schließer-/Wechselkontakten mit dem I/O-Modul der openWB und der Erzeugungsanlage verbunden werden. Falls dies nicht möglich ist, kann die Steuerbox über einfache Koppelrelais mit dem I/O-Modul der openWB verbunden werden und das empfangene Signal an vorhandene Ausgänge des I/O-Moduls (falls vorhanden) durchgereicht werden. + +Die Signalkontakte bilden folgende Zustände ab: +S1 -> 60% der EZA +S2 -> 30% der EZA +W3 -> 0% der EZA +alle Kontakte offen -> 100% der EZA + +Die Eingangsmuster sind so zu konfigurieren, dass auch bei mehreren geschlossenen Kontakten eine eindeutige Funktion gewährleistet wird. In der abgebildeten Konfiguration hat z.B. der Eingang DI5 für Begrenzung auf 0% Priorität, sodass dieses Muster auch erkannt wird, falls noch einer der Eingänge DI3 oder DI4 geschlossen sind. + +![EZA-Beispielkonfiguration](EZA-Beispielkonfiguration.png) diff --git a/packages/control/io_device.py b/packages/control/io_device.py index e64c5aba8e..c045a80c92 100644 --- a/packages/control/io_device.py +++ b/packages/control/io_device.py @@ -7,6 +7,7 @@ from modules.io_actions.controllable_consumers.dimming.api import Dimming 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 import StepwiseControl @dataclass @@ -15,6 +16,10 @@ class Get: analog_output: Dict[int, float] = None digital_input: Dict[int, bool] = None digital_output: Dict[int, bool] = None + analog_input_prev: Dict[int, float] = None + analog_output_prev: Dict[int, float] = None + digital_input_prev: Dict[int, bool] = None + digital_output_prev: Dict[int, bool] = None fault_str: str = NO_ERROR fault_state: int = 0 @@ -26,7 +31,9 @@ def get_factory(): @dataclass class Set: analog_output: Dict[int, float] = None + analog_output_prev: Dict[int, float] = None digital_output: Dict[int, bool] = None + digital_output_prev: Dict[int, bool] = None def set_factory(): @@ -47,7 +54,7 @@ def __init__(self, num: Union[int, str]): class IoActions: def __init__(self): - self.actions: Dict[int, Union[Dimming, DimmingDirectControl, RippleControlReceiver]] = {} + self.actions: Dict[int, Union[Dimming, DimmingDirectControl, RippleControlReceiver, StepwiseControl]] = {} def setup(self): for action in self.actions.values(): @@ -93,3 +100,12 @@ def ripple_control_receiver(self, device: Dict) -> float: return action.ripple_control_receiver() else: return 1 + + def stepwise_control(self, device_id: int) -> Optional[float]: + for action in self.actions.values(): + if isinstance(action, StepwiseControl): + if device_id in [component["id"] for component in action.config.configuration.devices]: + self._check_fault_state_io_device(action.config.configuration.io_device) + return action.control_stepwise() + else: + return None diff --git a/packages/control/process.py b/packages/control/process.py index abf9090ec8..8dfe32dc19 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -15,6 +15,7 @@ from modules.common.fault_state_level import FaultStateLevel from modules.io_actions.controllable_consumers.dimming.api import Dimming from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl +from modules.io_actions.generator_systems.stepwise_control.api import StepwiseControl log = logging.getLogger(__name__) @@ -85,6 +86,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, StepwiseControl): + # check if passthrough is enabled + if action.config.configuration.passthrough_enabled: + # find output pattern by value + for pattern in action.config.configuration.output_pattern: + if pattern["value"] == action.control_stepwise(): + # 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}" + ].data.set.digital_output[output] = pattern["matrix"][output] for io in data.data.system_data.values(): if isinstance(io, AbstractIoDevice): modules_threads.append( diff --git a/packages/control/pv_all.py b/packages/control/pv_all.py index abe2f50225..7250a32da2 100644 --- a/packages/control/pv_all.py +++ b/packages/control/pv_all.py @@ -68,6 +68,15 @@ def calc_power_for_all_components(self) -> None: else: if fault_state < module_data.get.fault_state: fault_state = module_data.get.fault_state + limit_value = data.data.io_actions.stepwise_control(data.data.pv_data[module].num) + if limit_value is not None and data.data.pv_data[module].data.get.fault_state == 0: + msg = ( + f"Leistung begrenzt auf {int(limit_value * 100)}%" + if limit_value < 1 + else "Keine Leistungsbegrenzung aktiv." + ) + data.data.pv_data[module].data.get.fault_str = msg + Pub().pub(f"openWB/set/pv/{data.data.pv_data[module].num}/get/fault_str", msg) except Exception: log.exception("Fehler im allgemeinen PV-Modul für "+str(module)) if fault_state == 0: diff --git a/packages/dataclass_utils/factories.py b/packages/dataclass_utils/factories.py index 8b58340c6f..acb55a2fa8 100644 --- a/packages/dataclass_utils/factories.py +++ b/packages/dataclass_utils/factories.py @@ -17,14 +17,35 @@ def voltages_list_factory() -> List[float]: return [230.0]*3 -def empty_io_pattern_factory(): +def empty_io_pattern_boolean_factory(): return [ { "value": True, # dimmen - "input_matrix": {} + "matrix": {} }, { "value": False, # unbeschränkt - "input_matrix": {} + "matrix": {} + } + ] + + +def empty_io_pattern_stepwise_factory(): + return [ + { + "value": 1.0, # keine Begrenzung + "matrix": {} + }, + { + "value": 0.6, # Stufe 1: 60% + "matrix": {} + }, + { + "value": 0.3, # Stufe 2: 30% + "matrix": {} + }, + { + "value": 0.0, # Stufe 3: 0% + "matrix": {} } ] diff --git a/packages/helpermodules/data_migration/data_migration.py b/packages/helpermodules/data_migration/data_migration.py index cca56b8285..dc9127a64b 100644 --- a/packages/helpermodules/data_migration/data_migration.py +++ b/packages/helpermodules/data_migration/data_migration.py @@ -581,10 +581,10 @@ def _move_rse(self) -> None: action.configuration.io_device = 0 # Wenn mindestens ein Kontakt geschlossen ist, wird die Ladung gesperrt. Wenn beide Kontakt # offen sind, darf geladen werden. - action.configuration.input_pattern = [{"value": 1, "input_matrix": {"21": False, "24": False}}, - {"value": 0, "input_matrix": {"21": False, "24": True}}, - {"value": 0, "input_matrix": {"21": True, "24": False}}, - {"value": 0, "input_matrix": {"21": True, "24": True}}] + action.configuration.input_pattern = [{"value": 1, "matrix": {"21": False, "24": False}}, + {"value": 0, "matrix": {"21": False, "24": True}}, + {"value": 0, "matrix": {"21": True, "24": False}}, + {"value": 0, "matrix": {"21": True, "24": True}}] Pub().pub('openWB/system/io/0/config', dataclass_utils.asdict(AddOn())) Pub().pub('openWB/io/action/0/config', dataclass_utils.asdict(action)) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 40daa6a85b..39b41716dc 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -56,7 +56,7 @@ class UpdateConfig: - DATASTORE_VERSION = 89 + DATASTORE_VERSION = 90 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2348,3 +2348,25 @@ def upgrade_datastore_88(self) -> None: pub_system_message({}, "Es gibt ein neues Theme: das Koala-Theme! Smarthpone-optimiert und mit " "Energiefluss-Diagramm & Karten-Ansicht der Ladepunkte", MessageType.INFO) self.__update_topic("openWB/system/datastore_version", 89) + + def upgrade_datastore_89(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("^openWB/io/action/[0-9]+/config$", topic) is not None: + payload = decode_payload(payload) + # modify key "input_matrix" to "matrix" for all patterns + if "input_pattern" not in payload["configuration"] and "output_pattern" not in payload["configuration"]: + # No input/output patterns found in IO action configuration + return None + log.debug(f"Updating IO action configuration: {topic}: {payload}") + if "input_pattern" in payload["configuration"]: + for pattern in payload["configuration"]["input_pattern"]: + if "input_matrix" in pattern: + pattern["matrix"] = pattern.pop("input_matrix") + if "output_pattern" in payload["configuration"]: + for pattern in payload["configuration"]["output_pattern"]: + if "input_matrix" in pattern: + pattern["matrix"] = pattern.pop("input_matrix") + log.debug(f"Updated IO action configuration: {topic}: {payload}") + return {topic: payload} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 90) diff --git a/packages/modules/common/store/_io.py b/packages/modules/common/store/_io.py index cba0fcccc9..f083c4d46f 100644 --- a/packages/modules/common/store/_io.py +++ b/packages/modules/common/store/_io.py @@ -1,3 +1,4 @@ +from control import data from modules.common.component_state import IoState from modules.common.fault_state import FaultState from modules.common.store import ValueStore @@ -16,14 +17,26 @@ def set(self, state: IoState) -> None: def update(self): try: if self.state.digital_input: + pub_to_broker(f"openWB/set/io/states/{self.num}/get/digital_input_prev", + data.data.io_states[f"io_states{self.num}"].data.get.digital_input) pub_to_broker(f"openWB/set/io/states/{self.num}/get/digital_input", self.state.digital_input) if self.state.analog_input: + pub_to_broker(f"openWB/set/io/states/{self.num}/get/analog_input_prev", + data.data.io_states[f"io_states{self.num}"].data.get.analog_input) pub_to_broker(f"openWB/set/io/states/{self.num}/get/analog_input", self.state.analog_input) if self.state.digital_output: + pub_to_broker(f"openWB/set/io/states/{self.num}/get/digital_output_prev", + data.data.io_states[f"io_states{self.num}"].data.get.digital_output) pub_to_broker(f"openWB/set/io/states/{self.num}/get/digital_output", self.state.digital_output) + pub_to_broker(f"openWB/set/io/states/{self.num}/set/digital_output_prev", + data.data.io_states[f"io_states{self.num}"].data.set.digital_output) pub_to_broker(f"openWB/set/io/states/{self.num}/set/digital_output", self.state.digital_output) if self.state.analog_output: + pub_to_broker(f"openWB/set/io/states/{self.num}/get/analog_output_prev", + data.data.io_states[f"io_states{self.num}"].data.get.analog_output) pub_to_broker(f"openWB/set/io/states/{self.num}/get/analog_output", self.state.analog_output) + pub_to_broker(f"openWB/set/io/states/{self.num}/set/analog_output_prev", + data.data.io_states[f"io_states{self.num}"].data.set.analog_output) pub_to_broker(f"openWB/set/io/states/{self.num}/set/analog_output", self.state.analog_output) except Exception as e: raise FaultState.from_exception(e) diff --git a/packages/modules/io_actions/controllable_consumers/dimming/api.py b/packages/modules/io_actions/controllable_consumers/dimming/api.py index e38bc2709a..0312bc1945 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/api.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/api.py @@ -17,7 +17,7 @@ def __init__(self, config: DimmingSetup): self.config = config self.import_power_left = None for pattern in self.config.configuration.input_pattern: - input_matrix_list = list(pattern["input_matrix"].items()) + input_matrix_list = list(pattern["matrix"].items()) if len(input_matrix_list): if pattern["value"]: self.dimming_input, self.dimming_value = input_matrix_list[0] diff --git a/packages/modules/io_actions/controllable_consumers/dimming/config.py b/packages/modules/io_actions/controllable_consumers/dimming/config.py index f655093c8b..76197a77bf 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming/config.py +++ b/packages/modules/io_actions/controllable_consumers/dimming/config.py @@ -1,13 +1,13 @@ from dataclasses import dataclass, field from typing import Dict, List, Optional -from dataclass_utils.factories import empty_io_pattern_factory, empty_list_factory +from dataclass_utils.factories import empty_io_pattern_boolean_factory, empty_list_factory from modules.io_actions.groups import ActionGroup @dataclass class DimmingConfig: io_device: Optional[int] = None - input_pattern: List[Dict] = field(default_factory=empty_io_pattern_factory) + 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"}] 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 cbeaea3fde..980e9d2a08 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 @@ -14,7 +14,7 @@ class DimmingDirectControl(AbstractIoAction): def __init__(self, config: DimmingDirectControlSetup): self.config = config for pattern in self.config.configuration.input_pattern: - input_matrix_list = list(pattern["input_matrix"].items()) + input_matrix_list = list(pattern["matrix"].items()) if len(input_matrix_list): if pattern["value"]: self.dimming_input, self.dimming_value = input_matrix_list[0] diff --git a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/config.py b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/config.py index 7ed110d50a..3ad19cd4c7 100644 --- a/packages/modules/io_actions/controllable_consumers/dimming_direct_control/config.py +++ b/packages/modules/io_actions/controllable_consumers/dimming_direct_control/config.py @@ -1,13 +1,13 @@ from dataclasses import dataclass, field from typing import Dict, List, Optional -from dataclass_utils.factories import empty_io_pattern_factory, empty_list_factory +from dataclass_utils.factories import empty_io_pattern_boolean_factory, empty_list_factory from modules.io_actions.groups import ActionGroup @dataclass class DimmingDirectControlConfig: io_device: Optional[int] = None - input_pattern: List[Dict] = field(default_factory=empty_io_pattern_factory) + 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"}] 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 b17191f9a0..886db6e43c 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 @@ -18,7 +18,7 @@ def __init__(self, config: RippleControlReceiverSetup): def setup(self) -> None: with ModifyLoglevelContext(control_command_log, logging.DEBUG): for pattern in self.config.configuration.input_pattern: - for digital_input, value in pattern["input_matrix"].items(): + 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 @@ -51,7 +51,7 @@ def setup(self) -> None: def ripple_control_receiver(self) -> float: for pattern in self.config.configuration.input_pattern: - for digital_input, value in pattern["input_matrix"].items(): + 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 diff --git a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/config.py b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/config.py index 9a866908b5..2daa3e29c3 100644 --- a/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/config.py +++ b/packages/modules/io_actions/controllable_consumers/ripple_control_receiver/config.py @@ -7,8 +7,8 @@ @dataclass class RippleControlReceiverConfig: io_device: Optional[int] = None - # [{"value": 0.5, "input_matrix": {"SofortLa": False, "PV": True}}] input_pattern: List[Dict] = field(default_factory=empty_list_factory) + # [{"value": 0.5, "matrix": {"SofortLa": False, "PV": True}}] devices: List[Dict] = field(default_factory=empty_list_factory) # [{"type": "cp", "id": 0}, # {"type": "io", "id": 1, "digital_output": "SofortLa"}, diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/__init__.py b/packages/modules/io_actions/generator_systems/stepwise_control/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/api.py b/packages/modules/io_actions/generator_systems/stepwise_control/api.py new file mode 100644 index 0000000000..2811ac5b9b --- /dev/null +++ b/packages/modules/io_actions/generator_systems/stepwise_control/api.py @@ -0,0 +1,100 @@ +import logging +from typing import Optional +from control import data +from helpermodules.logger import ModifyLoglevelContext +from helpermodules.pub import Pub +from helpermodules.timecheck import create_timestamp +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 +from modules.io_actions.generator_systems.stepwise_control.config import StepwiseControlSetup + +control_command_log = logging.getLogger("steuve_control_command") + + +class StepwiseControl(AbstractIoAction): + def __init__(self, config: StepwiseControlSetup): + self.config = config + self.__unique_inputs = [] + for pattern in self.config.configuration.input_pattern: + for key in pattern["matrix"].keys(): + if key not in self.__unique_inputs: + self.__unique_inputs.append(key) + assigned_inverters = [ + f"{device['id']}" + 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" + ] + 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( + 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!" + ) + super().__init__() + + def setup(self) -> None: + with ModifyLoglevelContext(control_command_log, logging.DEBUG): + 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: + if device["type"] == "inverter": + control_command_log.info( + f"Erzeugungsanlage {get_component_name_by_id(device['id'])} " + f"auf {int(pattern['value']*100)}% begrenzt." + ) + 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) -> Optional[float]: + 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 + return pattern['value'] + else: + # Zustand entspricht keinem Pattern, Leistungsbegrenzung aufheben + return 1 + + +def create_action(config: StepwiseControlSetup): + return StepwiseControl(config=config) + + +device_descriptor = DeviceDescriptor(configuration_factory=StepwiseControlSetup) diff --git a/packages/modules/io_actions/generator_systems/stepwise_control/config.py b/packages/modules/io_actions/generator_systems/stepwise_control/config.py new file mode 100644 index 0000000000..7cfc0d4802 --- /dev/null +++ b/packages/modules/io_actions/generator_systems/stepwise_control/config.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +from typing import Dict, List, Optional +from dataclass_utils.factories import empty_list_factory, empty_io_pattern_stepwise_factory +from modules.io_actions.groups import ActionGroup + + +@dataclass +class StepwiseControlConfig: + io_device: Optional[int] = None + input_pattern: List[Dict] = field(default_factory=empty_io_pattern_stepwise_factory) + devices: List[Dict] = field(default_factory=empty_list_factory) + # [{"type": "inverter", "id": 1},...] + passthrough_enabled: bool = False + output_pattern: List[Dict] = field(default_factory=empty_io_pattern_stepwise_factory) + + +class StepwiseControlSetup: + def __init__(self, + name: str = "Stufenweise Steuerung von EZA", + type: str = "stepwise_control", + id: int = 0, + configuration: StepwiseControlConfig = None): + self.name = name + self.type = type + self.id = id + self.configuration = configuration or StepwiseControlConfig() + self.group = ActionGroup.GENERATOR_SYSTEMS.value diff --git a/packages/modules/io_actions/groups.py b/packages/modules/io_actions/groups.py index 538025f563..7705fd053b 100644 --- a/packages/modules/io_actions/groups.py +++ b/packages/modules/io_actions/groups.py @@ -3,8 +3,10 @@ class ActionGroup(Enum): CONTROLLABLE_CONSUMERS = "controllable_consumers" + GENERATOR_SYSTEMS = "generator_systems" READABLE_GROUP_NAME = { ActionGroup.CONTROLLABLE_CONSUMERS: "Steuerbare Verbrauchseinrichtungen (§14a)", + ActionGroup.GENERATOR_SYSTEMS: "Erzeugungsanlagen (§9)", }