Skip to content
Merged

EEBus #2962

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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.vscode/*
__pycache__/
node_modules/
data/config/eebus/certs/*
data/log/*
data/charge_log/*
data/daily_log/*
Expand Down
15 changes: 9 additions & 6 deletions packages/control/io_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from control.limiting_value import LimitingValue
from helpermodules.constants import NO_ERROR
from modules.common.utils.component_parser import get_io_name_by_id
from modules.io_actions.controllable_consumers.dimming.api import Dimming
from modules.io_actions.controllable_consumers.dimming.api_eebus import DimmingEebus
from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo
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
from modules.io_actions.generator_systems.stepwise_control.api_eebus import StepwiseControlEebus
from modules.io_actions.generator_systems.stepwise_control.api_io import StepwiseControlIo


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

class IoActions:
def __init__(self):
self.actions: Dict[int, Union[Dimming, DimmingDirectControl, RippleControlReceiver, StepwiseControl]] = {}
self.actions: Dict[int, Union[DimmingIo, DimmingEebus, DimmingDirectControl,
RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo]] = {}

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

def dimming_get_import_power_left(self, device: Dict) -> Optional[float]:
for action in self.actions.values():
if isinstance(action, Dimming):
if isinstance(action, (DimmingIo, DimmingEebus)):
for d in action.config.configuration.devices:
if device == d:
self._check_fault_state_io_device(action.config.configuration.io_device)
Expand All @@ -76,7 +79,7 @@ def dimming_get_import_power_left(self, device: Dict) -> Optional[float]:

def dimming_set_import_power_left(self, device: Dict, used_power: float) -> Optional[float]:
for action in self.actions.values():
if isinstance(action, Dimming):
if isinstance(action, (DimmingIo, DimmingEebus)):
for d in action.config.configuration.devices:
if d == device:
return action.dimming_set_import_power_left(used_power)
Expand All @@ -103,7 +106,7 @@ def ripple_control_receiver(self, device: Dict) -> float:

def stepwise_control(self, device_id: int) -> Optional[float]:
for action in self.actions.values():
if isinstance(action, StepwiseControl):
if isinstance(action, (StepwiseControlEebus, StepwiseControlIo)):
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()
Expand Down
11 changes: 6 additions & 5 deletions packages/control/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
from helpermodules.utils._thread_handler import joined_thread_handler
from modules.common.abstract_io import AbstractIoDevice
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.api_io import DimmingIo
from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl
from modules.io_actions.generator_systems.stepwise_control.api import StepwiseControl
from modules.io_actions.generator_systems.stepwise_control.api_io import StepwiseControlIo

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -77,13 +77,13 @@ def process_algorithm_results(self) -> None:
data.data.io_states[f"io_states{d['id']}"].data.set.digital_output[d["digital_output"]] = (
action.dimming_via_direct_control() is None # active output (True) if no dimming
)
if isinstance(action, Dimming):
if isinstance(action, DimmingIo):
for d in action.config.configuration.devices:
if d["type"] == "io":
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):
if isinstance(action, StepwiseControlIo):
# check if passthrough is enabled
if action.config.configuration.passthrough_enabled:
# find output pattern by value
Expand All @@ -99,7 +99,8 @@ def process_algorithm_results(self) -> None:
modules_threads.append(
Thread(
target=io.write,
args=(None, data.data.io_states[f"io_states{io.config.id}"].data.set.digital_output,),
args=(data.data.io_states[f"io_states{io.config.id}"].data.set.analog_output,
data.data.io_states[f"io_states{io.config.id}"].data.set.digital_output,),
name=f"set output io{io.config.id}"))
if modules_threads:
joined_thread_handler(modules_threads, 3)
Expand Down
4 changes: 4 additions & 0 deletions packages/helpermodules/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from helpermodules.utils.run_command import run_command
# ToDo: move to module commands if implemented
from modules.backup_clouds.onedrive.api import generateMSALAuthCode, retrieveMSALTokens
from modules.io_devices.eebus.api import create_pub_cert_ski

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

def createEebusCert(self, connection_id: str, payload: dict) -> None:
create_pub_cert_ski(payload["data"]["io_device"])

def factoryReset(self, connection_id: str, payload: dict) -> None:
Path(Path(__file__).resolve().parents[2] / 'data' / 'restore' / 'factory_reset').touch()
pub_user_message(payload, connection_id,
Expand Down
5 changes: 3 additions & 2 deletions packages/helpermodules/subdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ def on_connect(self, client: mqtt.Client, userdata, flags: dict, rc: int):
("openWB/general/#", 2),
("openWB/graph/#", 2),
("openWB/internal_io/#", 2),
("openWB/io/#", 2),
("openWB/optional/#", 2),
("openWB/counter/#", 2),
("openWB/command/command_completed", 2),
Expand All @@ -148,6 +147,7 @@ def on_connect(self, client: mqtt.Client, userdata, flags: dict, rc: int):
("openWB/system/device/+/config", 2),
("openWB/system/io/#", 2),
("openWB/LegacySmartHome/Status/wattnichtHaus", 2),
("openWB/io/#", 2),
])
self.processing_counter.add_task()
Pub().pub("openWB/system/subdata_initialized", True)
Expand Down Expand Up @@ -677,7 +677,8 @@ def process_io_topic(self, var: Dict[str, Union[io_device.IoActions, io_device.I
mod = importlib.import_module(
f".io_actions.{payload['group']}.{payload['type']}.api", "modules")
config = dataclass_from_dict(mod.device_descriptor.configuration_factory, payload)
var.actions[f"io_action{index}"] = mod.create_action(config)
var.actions[f"io_action{index}"] = mod.create_action(
config, self.system_data[f"io{config.configuration.io_device}"].config.type)
elif re.search("/io/action/[0-9]+/timestamp", msg.topic) is not None:
index = get_index(msg.topic)
self.set_json_payload_class(var.actions[f"io_action{index}"], msg)
Expand Down
25 changes: 25 additions & 0 deletions packages/helpermodules/timecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
import logging
import datetime
import re
from typing import List, Optional, Tuple, TypeVar, Union

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

def convert_to_timestamp(timestring: str) -> int:
return int(datetime.datetime.fromisoformat(timestring).timestamp())


def parse_iso8601_duration(duration: str) -> float:
"""
Parst eine ISO-8601 Duration wie 'PT3723S', 'P1DT2H30M', etc.
Gibt ein timedelta zurück.
"""
pattern = re.compile(
r'P' # beginnt immer mit P
r'(?:(?P<days>\d+)D)?' # Tage
r'(?:T' # Zeit-Teil beginnt mit T
r'(?:(?P<hours>\d+)H)?' # Stunden
r'(?:(?P<minutes>\d+)M)?' # Minuten
r'(?:(?P<seconds>\d+)S)?' # Sekunden
r')?$'
)

match = pattern.fullmatch(duration)
if not match:
raise ValueError(f"Ungültiges ISO-8601 Duration Format: {duration}")

parts = {name: int(val) if val else 0 for name, val in match.groupdict().items()}
return datetime.timedelta(days=parts["days"], hours=parts["hours"],
minutes=parts["minutes"], seconds=parts["seconds"]).total_seconds()
4 changes: 0 additions & 4 deletions packages/helpermodules/update_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,6 @@ class UpdateConfig:
"^openWB/internal_chargepoint/[0-1]/get/rfid$",
"^openWB/internal_chargepoint/[0-1]/get/rfid_timestamp$",

"^openWB/io/states/[0-9]+/get/digital_input$",
"^openWB/io/states/[0-9]+/get/analog_input$",
"^openWB/io/states/[0-9]+/set/digital_output$",
"^openWB/io/states/[0-9]+/set/analog_output$",
"^openWB/io/action/[0-9]+/config$",
"^openWB/io/action/[0-9]+/timestamp$",

Expand Down
55 changes: 42 additions & 13 deletions packages/modules/common/configurable_io.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from typing import Dict, Optional, TypeVar, Generic, Callable, Union

from helpermodules import timecheck
from helpermodules.pub import Pub
from modules.common import store
from modules.common.abstract_io import AbstractIoDevice
Expand All @@ -19,23 +20,47 @@ def __init__(self,
config: T_IO_CONFIG,
component_reader: Callable[[], IoState],
component_writer: Callable[[Dict[int, Union[float, int]]], Optional[IoState]],
initializer: Callable = lambda: None) -> None:
initializer: Callable = lambda: None,
error_handler: Callable = lambda: None) -> None:
self.config = config
self.__error_handler = error_handler
self.fault_state = FaultState(ComponentInfo(self.config.id, self.config.name,
ComponentType.IO.value))
self.store = store.get_io_value_store(self.config.id)
self.set_manual: Dict = {"analog_output": {}, "digital_output": {}}
self.error_timestamp = None
with SingleComponentUpdateContext(self.fault_state):
self.component_reader = component_reader
self.component_writer = component_writer
initializer()

def error_handler(self) -> None:
error_timestamp_topic = f"openWB/set/system/device/{self.config.id}/error_timestamp"
if self.error_timestamp is None:
self.error_timestamp = timecheck.create_timestamp()
Pub().pub(error_timestamp_topic, self.error_timestamp)
log.debug(
f"Fehler bei Gerät {self.config.name} aufgetreten, Fehlerzeitstempel: {self.error_timestamp}")
if timecheck.check_timestamp(self.error_timestamp, 60) is False:
try:
self.__error_handler()
except Exception:
log.exception(f"Fehlerbehandlung für Gerät {self.config.name} fehlgeschlagen")
else:
log.debug(f"Fehlerbehandlung für Gerät {self.config.name} wurde durchgeführt.")

self.error_timestamp = None
Pub().pub(error_timestamp_topic, self.error_timestamp)

def read(self):
if hasattr(self, "component_reader"):
# Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten
with SingleComponentUpdateContext(self.fault_state):
io_state = self.component_reader()
self.store.set(io_state)
try:
with SingleComponentUpdateContext(self.fault_state, reraise=True):
io_state = self.component_reader()
self.store.set(io_state)
except Exception:
self.error_handler()

def update_manual_output(self, manual: Dict[str, bool], output: Dict[str, bool], string: str, topic_suffix: str):
if len(manual) > 0:
Expand All @@ -48,12 +73,16 @@ def update_manual_output(self, manual: Dict[str, bool], output: Dict[str, bool],
def write(self, analog_output, digital_output):
if hasattr(self, "component_writer"):
# Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten
with SingleComponentUpdateContext(self.fault_state):
self.update_manual_output(self.set_manual["analog_output"], analog_output, "analoge", "analog_output")
self.update_manual_output(self.set_manual["digital_output"],
digital_output, "digitale", "digital_output")
if ((analog_output and self.store.delegate.state.analog_output != analog_output) or
(digital_output and self.store.delegate.state.digital_output != digital_output)):
io_state = self.component_writer(analog_output, digital_output)
if io_state is not None:
self.store.set(io_state)
try:
with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
self.update_manual_output(self.set_manual["analog_output"],
analog_output, "analoge", "analog_output")
self.update_manual_output(self.set_manual["digital_output"],
digital_output, "digitale", "digital_output")
if ((analog_output and self.store.delegate.state.analog_output != analog_output) or
(digital_output and self.store.delegate.state.digital_output != digital_output)):
io_state = self.component_writer(analog_output, digital_output)
if io_state is not None:
self.store.set(io_state)
except Exception:
self.error_handler()
Loading