Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/EZA-Beispielkonfiguration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 8 additions & 2 deletions docs/IO-Geräte & -Aktionen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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.
Binary file added docs/RSE-Beispielkonfiguration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 25 additions & 1 deletion docs/Steuerbare Verbrauchseinrichtungen nach §14a.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
18 changes: 17 additions & 1 deletion packages/control/io_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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():
Expand All @@ -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():
Expand Down Expand Up @@ -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
12 changes: 12 additions & 0 deletions packages/control/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions packages/control/pv_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 24 additions & 3 deletions packages/dataclass_utils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
]
8 changes: 4 additions & 4 deletions packages/helpermodules/data_migration/data_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
24 changes: 23 additions & 1 deletion packages/helpermodules/update_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@

class UpdateConfig:

DATASTORE_VERSION = 89
DATASTORE_VERSION = 90

valid_topic = [
"^openWB/bat/config/bat_control_permitted$",
Expand Down Expand Up @@ -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)
13 changes: 13 additions & 0 deletions packages/modules/common/store/_io.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
Loading