Skip to content

Commit fdffd64

Browse files
committed
EEBus
1 parent 7da0665 commit fdffd64

18 files changed

Lines changed: 725 additions & 208 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
.vscode/*
1111
__pycache__/
1212
node_modules/
13+
data/config/eebus/certs/*
1314
data/log/*
1415
data/charge_log/*
1516
data/daily_log/*

packages/control/io_device.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from control.limiting_value import LimitingValue
55
from helpermodules.constants import NO_ERROR
66
from modules.common.utils.component_parser import get_io_name_by_id
7-
from modules.io_actions.controllable_consumers.dimming.api import Dimming
7+
from modules.io_actions.controllable_consumers.dimming.api_eebus import DimmingEebus
8+
from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo
89
from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl
910
from modules.io_actions.controllable_consumers.ripple_control_receiver.api import RippleControlReceiver
10-
from modules.io_actions.generator_systems.stepwise_control.api import StepwiseControl
11+
from modules.io_actions.generator_systems.stepwise_control.api_eebus import StepwiseControlEebus
12+
from modules.io_actions.generator_systems.stepwise_control.api_io import StepwiseControlIo
1113

1214

1315
@dataclass
@@ -54,7 +56,8 @@ def __init__(self, num: Union[int, str]):
5456

5557
class IoActions:
5658
def __init__(self):
57-
self.actions: Dict[int, Union[Dimming, DimmingDirectControl, RippleControlReceiver, StepwiseControl]] = {}
59+
self.actions: Dict[int, Union[DimmingIo, DimmingEebus, DimmingDirectControl,
60+
RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo]] = {}
5861

5962
def setup(self):
6063
for action in self.actions.values():
@@ -66,7 +69,7 @@ def _check_fault_state_io_device(self, io_device: int) -> None:
6669

6770
def dimming_get_import_power_left(self, device: Dict) -> Optional[float]:
6871
for action in self.actions.values():
69-
if isinstance(action, Dimming):
72+
if isinstance(action, (DimmingIo, DimmingEebus)):
7073
for d in action.config.configuration.devices:
7174
if device == d:
7275
self._check_fault_state_io_device(action.config.configuration.io_device)
@@ -76,7 +79,7 @@ def dimming_get_import_power_left(self, device: Dict) -> Optional[float]:
7679

7780
def dimming_set_import_power_left(self, device: Dict, used_power: float) -> Optional[float]:
7881
for action in self.actions.values():
79-
if isinstance(action, Dimming):
82+
if isinstance(action, (DimmingIo, DimmingEebus)):
8083
for d in action.config.configuration.devices:
8184
if d == device:
8285
return action.dimming_set_import_power_left(used_power)
@@ -103,7 +106,7 @@ def ripple_control_receiver(self, device: Dict) -> float:
103106

104107
def stepwise_control(self, device_id: int) -> Optional[float]:
105108
for action in self.actions.values():
106-
if isinstance(action, StepwiseControl):
109+
if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)):
107110
if device_id in [component["id"] for component in action.config.configuration.devices]:
108111
self._check_fault_state_io_device(action.config.configuration.io_device)
109112
return action.control_stepwise()

packages/control/process.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
from helpermodules.utils._thread_handler import joined_thread_handler
1414
from modules.common.abstract_io import AbstractIoDevice
1515
from modules.common.fault_state_level import FaultStateLevel
16-
from modules.io_actions.controllable_consumers.dimming.api import Dimming
16+
from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo
1717
from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl
18-
from modules.io_actions.generator_systems.stepwise_control.api import StepwiseControl
18+
from modules.io_actions.generator_systems.stepwise_control.api_io import StepwiseControlIo
1919

2020
log = logging.getLogger(__name__)
2121

@@ -77,13 +77,13 @@ def process_algorithm_results(self) -> None:
7777
data.data.io_states[f"io_states{d['id']}"].data.set.digital_output[d["digital_output"]] = (
7878
action.dimming_via_direct_control() is None # active output (True) if no dimming
7979
)
80-
if isinstance(action, Dimming):
80+
if isinstance(action, DimmingIo):
8181
for d in action.config.configuration.devices:
8282
if d["type"] == "io":
8383
data.data.io_states[f"io_states{d['id']}"].data.set.digital_output[d["digital_output"]] = (
8484
not action.dimming_active() # active output (True) if no dimming
8585
)
86-
if isinstance(action, StepwiseControl):
86+
if isinstance(action, StepwiseControlIo):
8787
# check if passthrough is enabled
8888
if action.config.configuration.passthrough_enabled:
8989
# find output pattern by value
@@ -99,7 +99,8 @@ def process_algorithm_results(self) -> None:
9999
modules_threads.append(
100100
Thread(
101101
target=io.write,
102-
args=(None, data.data.io_states[f"io_states{io.config.id}"].data.set.digital_output,),
102+
args=(data.data.io_states[f"io_states{io.config.id}"].data.set.analog_output,
103+
data.data.io_states[f"io_states{io.config.id}"].data.set.digital_output,),
103104
name=f"set output io{io.config.id}"))
104105
if modules_threads:
105106
joined_thread_handler(modules_threads, 3)

packages/helpermodules/command.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from helpermodules.utils.run_command import run_command
2424
# ToDo: move to module commands if implemented
2525
from modules.backup_clouds.onedrive.api import generateMSALAuthCode, retrieveMSALTokens
26+
from modules.io_devices.eebus.api import create_pub_cert_ski
2627

2728
from helpermodules.broker import BrokerClient
2829
from helpermodules.data_migration.data_migration import MigrateData
@@ -924,6 +925,9 @@ def retrieveMSALTokens(self, connection_id: str, payload: dict) -> None:
924925
result = retrieveMSALTokens(cloud_backup_config.config)
925926
pub_user_message(payload, connection_id, result["message"], result["MessageType"])
926927

928+
def createEebusCert(self, connection_id: str, payload: dict) -> None:
929+
create_pub_cert_ski(payload["data"]["io_device"])
930+
927931
def factoryReset(self, connection_id: str, payload: dict) -> None:
928932
Path(Path(__file__).resolve().parents[2] / 'data' / 'restore' / 'factory_reset').touch()
929933
pub_user_message(payload, connection_id,

packages/helpermodules/subdata.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ def on_connect(self, client: mqtt.Client, userdata, flags: dict, rc: int):
132132
("openWB/general/#", 2),
133133
("openWB/graph/#", 2),
134134
("openWB/internal_io/#", 2),
135-
("openWB/io/#", 2),
136135
("openWB/optional/#", 2),
137136
("openWB/counter/#", 2),
138137
("openWB/command/command_completed", 2),
@@ -148,6 +147,7 @@ def on_connect(self, client: mqtt.Client, userdata, flags: dict, rc: int):
148147
("openWB/system/device/+/config", 2),
149148
("openWB/system/io/#", 2),
150149
("openWB/LegacySmartHome/Status/wattnichtHaus", 2),
150+
("openWB/io/#", 2),
151151
])
152152
self.processing_counter.add_task()
153153
Pub().pub("openWB/system/subdata_initialized", True)
@@ -677,7 +677,8 @@ def process_io_topic(self, var: Dict[str, Union[io_device.IoActions, io_device.I
677677
mod = importlib.import_module(
678678
f".io_actions.{payload['group']}.{payload['type']}.api", "modules")
679679
config = dataclass_from_dict(mod.device_descriptor.configuration_factory, payload)
680-
var.actions[f"io_action{index}"] = mod.create_action(config)
680+
var.actions[f"io_action{index}"] = mod.create_action(
681+
config, self.system_data[f"io{config.configuration.io_device}"].config.type)
681682
elif re.search("/io/action/[0-9]+/timestamp", msg.topic) is not None:
682683
index = get_index(msg.topic)
683684
self.set_json_payload_class(var.actions[f"io_action{index}"], msg)

packages/helpermodules/timecheck.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
import logging
44
import datetime
5+
import re
56
from typing import List, Optional, Tuple, TypeVar, Union
67

78
from helpermodules.utils.error_handling import ImportErrorContext
@@ -332,3 +333,27 @@ def convert_timestamp_delta_to_time_string(timestamp: int, delta: int) -> str:
332333

333334
def convert_to_timestamp(timestring: str) -> int:
334335
return int(datetime.datetime.fromisoformat(timestring).timestamp())
336+
337+
338+
def parse_iso8601_duration(duration: str) -> float:
339+
"""
340+
Parst eine ISO-8601 Duration wie 'PT3723S', 'P1DT2H30M', etc.
341+
Gibt ein timedelta zurück.
342+
"""
343+
pattern = re.compile(
344+
r'P' # beginnt immer mit P
345+
r'(?:(?P<days>\d+)D)?' # Tage
346+
r'(?:T' # Zeit-Teil beginnt mit T
347+
r'(?:(?P<hours>\d+)H)?' # Stunden
348+
r'(?:(?P<minutes>\d+)M)?' # Minuten
349+
r'(?:(?P<seconds>\d+)S)?' # Sekunden
350+
r')?$'
351+
)
352+
353+
match = pattern.fullmatch(duration)
354+
if not match:
355+
raise ValueError(f"Ungültiges ISO-8601 Duration Format: {duration}")
356+
357+
parts = {name: int(val) if val else 0 for name, val in match.groupdict().items()}
358+
return datetime.timedelta(days=parts["days"], hours=parts["hours"],
359+
minutes=parts["minutes"], seconds=parts["seconds"]).total_seconds()

packages/helpermodules/update_config.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,6 @@ class UpdateConfig:
282282
"^openWB/internal_chargepoint/[0-1]/get/rfid$",
283283
"^openWB/internal_chargepoint/[0-1]/get/rfid_timestamp$",
284284

285-
"^openWB/io/states/[0-9]+/get/digital_input$",
286-
"^openWB/io/states/[0-9]+/get/analog_input$",
287-
"^openWB/io/states/[0-9]+/set/digital_output$",
288-
"^openWB/io/states/[0-9]+/set/analog_output$",
289285
"^openWB/io/action/[0-9]+/config$",
290286
"^openWB/io/action/[0-9]+/timestamp$",
291287

packages/modules/common/configurable_io.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from typing import Dict, Optional, TypeVar, Generic, Callable, Union
33

4+
from helpermodules import timecheck
45
from helpermodules.pub import Pub
56
from modules.common import store
67
from modules.common.abstract_io import AbstractIoDevice
@@ -19,23 +20,47 @@ def __init__(self,
1920
config: T_IO_CONFIG,
2021
component_reader: Callable[[], IoState],
2122
component_writer: Callable[[Dict[int, Union[float, int]]], Optional[IoState]],
22-
initializer: Callable = lambda: None) -> None:
23+
initializer: Callable = lambda: None,
24+
error_handler: Callable = lambda: None) -> None:
2325
self.config = config
26+
self.__error_handler = error_handler
2427
self.fault_state = FaultState(ComponentInfo(self.config.id, self.config.name,
2528
ComponentType.IO.value))
2629
self.store = store.get_io_value_store(self.config.id)
2730
self.set_manual: Dict = {"analog_output": {}, "digital_output": {}}
31+
self.error_timestamp = None
2832
with SingleComponentUpdateContext(self.fault_state):
2933
self.component_reader = component_reader
3034
self.component_writer = component_writer
3135
initializer()
3236

37+
def error_handler(self) -> None:
38+
error_timestamp_topic = f"openWB/set/system/device/{self.config.id}/error_timestamp"
39+
if self.error_timestamp is None:
40+
self.error_timestamp = timecheck.create_timestamp()
41+
Pub().pub(error_timestamp_topic, self.error_timestamp)
42+
log.debug(
43+
f"Fehler bei Gerät {self.config.name} aufgetreten, Fehlerzeitstempel: {self.error_timestamp}")
44+
if timecheck.check_timestamp(self.error_timestamp, 60) is False:
45+
try:
46+
self.__error_handler()
47+
except Exception:
48+
log.exception(f"Fehlerbehandlung für Gerät {self.config.name} fehlgeschlagen")
49+
else:
50+
log.debug(f"Fehlerbehandlung für Gerät {self.config.name} wurde durchgeführt.")
51+
52+
self.error_timestamp = None
53+
Pub().pub(error_timestamp_topic, self.error_timestamp)
54+
3355
def read(self):
3456
if hasattr(self, "component_reader"):
3557
# Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten
36-
with SingleComponentUpdateContext(self.fault_state):
37-
io_state = self.component_reader()
38-
self.store.set(io_state)
58+
try:
59+
with SingleComponentUpdateContext(self.fault_state, reraise=True):
60+
io_state = self.component_reader()
61+
self.store.set(io_state)
62+
except Exception:
63+
self.error_handler()
3964

4065
def update_manual_output(self, manual: Dict[str, bool], output: Dict[str, bool], string: str, topic_suffix: str):
4166
if len(manual) > 0:
@@ -48,12 +73,16 @@ def update_manual_output(self, manual: Dict[str, bool], output: Dict[str, bool],
4873
def write(self, analog_output, digital_output):
4974
if hasattr(self, "component_writer"):
5075
# Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten
51-
with SingleComponentUpdateContext(self.fault_state):
52-
self.update_manual_output(self.set_manual["analog_output"], analog_output, "analoge", "analog_output")
53-
self.update_manual_output(self.set_manual["digital_output"],
54-
digital_output, "digitale", "digital_output")
55-
if ((analog_output and self.store.delegate.state.analog_output != analog_output) or
56-
(digital_output and self.store.delegate.state.digital_output != digital_output)):
57-
io_state = self.component_writer(analog_output, digital_output)
58-
if io_state is not None:
59-
self.store.set(io_state)
76+
try:
77+
with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
78+
self.update_manual_output(self.set_manual["analog_output"],
79+
analog_output, "analoge", "analog_output")
80+
self.update_manual_output(self.set_manual["digital_output"],
81+
digital_output, "digitale", "digital_output")
82+
if ((analog_output and self.store.delegate.state.analog_output != analog_output) or
83+
(digital_output and self.store.delegate.state.digital_output != digital_output)):
84+
io_state = self.component_writer(analog_output, digital_output)
85+
if io_state is not None:
86+
self.store.set(io_state)
87+
except Exception:
88+
self.error_handler()

0 commit comments

Comments
 (0)