diff --git "a/docs/IO-Ger\303\244te & -Aktionen.md" "b/docs/IO-Ger\303\244te & -Aktionen.md" index bac6bd4be8..07f34179a8 100644 --- "a/docs/IO-Ger\303\244te & -Aktionen.md" +++ "b/docs/IO-Ger\303\244te & -Aktionen.md" @@ -27,7 +27,11 @@ 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. +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 diff --git "a/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" "b/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" index d4cbb44c5b..0b5b54a611 100644 --- "a/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" +++ "b/docs/Steuerbare Verbrauchseinrichtungen nach \302\24714a.md" @@ -1,3 +1,4 @@ +## Stuerbare 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 @@ -13,3 +14,15 @@ 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. + +## Steuerbare Erzeugungsanlagen (EZA) nach § 9 EEG + +Bitte beachten: Die openWB steuert keinen Wechselrichter an. Sie zeigt lediglich den aktuellen Zustand der Beschränkung an. + +Die Einspeiseleistung des Wechselrichters wird über drei Signalkontakte der FNN-Steuerbox geregelt. Die openWB übernimmt dabei keine direkte Steuerung des Wechselrichters, sondern visualisiert lediglich den aktuellen Steuerzustand. Das Signalkabel der FNN-Steuerbox muss daher beispielsweise über eine Doppelklemme mit dem I/O-Modul der openWB verbunden und anschließend zum Wechselrichter weitergeführt (durchgeschliffen) 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 +Sollten mehrere Kontakte geschlossen sein, so wird die geringste Leistungsstufe ausgewählt (z. B. S2 und W3 -> 0%). \ No newline at end of file diff --git a/packages/control/io_device.py b/packages/control/io_device.py index e64c5aba8e..387d9004bf 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.production_plants.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[str]: + for action in self.actions.values(): + if isinstance(action, StepwiseControl): + if device_id == action.config.configuration.pv_id: + self._check_fault_state_io_device(action.config.configuration.io_device) + return action.control_stepwise() + else: + return None diff --git a/packages/control/pv_all.py b/packages/control/pv_all.py index abe2f50225..01f7833411 100644 --- a/packages/control/pv_all.py +++ b/packages/control/pv_all.py @@ -68,6 +68,10 @@ def calc_power_for_all_components(self) -> None: else: if fault_state < module_data.get.fault_state: fault_state = module_data.get.fault_state + msg = data.data.io_actions.stepwise_control(data.data.pv_data[module].num) + if msg is not None and data.data.pv_data[module].data.get.fault_state == 0: + 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/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/groups.py b/packages/modules/io_actions/groups.py index 538025f563..1015e67a3e 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" + PRODUCTION_PLANTS = "production_plants" READABLE_GROUP_NAME = { ActionGroup.CONTROLLABLE_CONSUMERS: "Steuerbare Verbrauchseinrichtungen (§14a)", + ActionGroup.PRODUCTION_PLANTS: "Erzeugungsanlagen (§9)", } diff --git a/packages/modules/io_actions/production_plants/stepwise_control/__init__.py b/packages/modules/io_actions/production_plants/stepwise_control/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/io_actions/production_plants/stepwise_control/api.py b/packages/modules/io_actions/production_plants/stepwise_control/api.py new file mode 100644 index 0000000000..8dfa0b4110 --- /dev/null +++ b/packages/modules/io_actions/production_plants/stepwise_control/api.py @@ -0,0 +1,73 @@ +import logging +from control import data +from typing import Optional +from helpermodules.logger import ModifyLoglevelContext +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.production_plants.stepwise_control.config import StepwiseControlSetup + +control_command_log = logging.getLogger("steuve_control_command") + + +class StepwiseControl(AbstractIoAction): + def __init__(self, config: StepwiseControlSetup): + self.config = config + control_command_log.info(f"Stufenweise Steuerung einer EZA: Eingang {self.config.configuration.s1} für S1, " + f"Eingang {self.config.configuration.s2} für S2, und Eingang " + f"{self.config.configuration.w3} für W3 wird überwacht. Die Beschränkung musss in " + "der EZA vorgenommen werden.") + super().__init__() + + def setup(self) -> None: + pass + + def control_stepwise(self) -> Optional[str]: + text = (f"Die Einspeiseleistung von {get_component_name_by_id(self.config.configuration.pv_id)} ist auf " + "{} % beschränkt. Die Beschränkung musss in der EZA vorgenommen werden.") + msg = None + 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 + + active_inputs = [ + digital_input[self.config.configuration.s1], + digital_input[self.config.configuration.s2], + digital_input[self.config.configuration.w3] + ] + num_active = sum(1 for v in active_inputs if v) + + if num_active > 1: + error_msg = (f"Fehler: Mehr als ein Eingang ist aktiv für die stufenweise Steuerung der EZA! " + f"S1: {digital_input[self.config.configuration.s1]}, " + f"S2: {digital_input[self.config.configuration.s2]}, " + f"W3: {digital_input[self.config.configuration.w3]}") + with ModifyLoglevelContext(control_command_log, logging.ERROR): + control_command_log.error(error_msg) + raise ValueError(error_msg) + + if digital_input[self.config.configuration.s1]: + msg = text.format(60) + elif digital_input[self.config.configuration.s2]: + msg = text.format(30) + elif digital_input[self.config.configuration.w3]: + msg = text.format(0) + else: + # Keine Beschränkung soll nicht dauerhaft im WR angezeigt werden. + msg = (f"Die Einspeiseleistung von {get_component_name_by_id(self.config.configuration.pv_id)} ist " + "nicht beschränkt. Die Beschränkung musss in der EZA vorgenommen werden.") + + if not (digital_input[self.config.configuration.s1] == digital_input_prev[self.config.configuration.s1] and + digital_input[self.config.configuration.s2] == digital_input_prev[self.config.configuration.s2] and + digital_input[self.config.configuration.w3] == digital_input_prev[self.config.configuration.w3]): + # Wenn sich was geändet hat, loggen + with ModifyLoglevelContext(control_command_log, logging.DEBUG): + control_command_log.info(msg) + return msg + + +def create_action(config: StepwiseControlSetup): + return StepwiseControl(config=config) + + +device_descriptor = DeviceDescriptor(configuration_factory=StepwiseControlSetup) diff --git a/packages/modules/io_actions/production_plants/stepwise_control/config.py b/packages/modules/io_actions/production_plants/stepwise_control/config.py new file mode 100644 index 0000000000..3ac772e1f3 --- /dev/null +++ b/packages/modules/io_actions/production_plants/stepwise_control/config.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import Optional +from modules.io_actions.groups import ActionGroup + + +@dataclass +class StepwiseControlConfig: + io_device: Optional[int] = None + s1: str = None + s2: str = None + w3: str = None + pv_id: int = None + + +class StepwiseControlSetup: + def __init__(self, + name: str = "Stufenweise Steuerung einer 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.PRODUCTION_PLANTS.value