diff --git a/haco/capabilities.py b/haco/capabilities.py index df3daa4..73eb7f6 100644 --- a/haco/capabilities.py +++ b/haco/capabilities.py @@ -1,11 +1,12 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Type from fmtr.tools.iterator_tools import strip_none from haco.base import Base from haco.topics import AnnounceTopicState, AnnounceTopicCommand +from haco.utils import Converters if TYPE_CHECKING: from haco.control import Control @@ -15,6 +16,8 @@ if TYPE_CHECKING: class Capability(Base): name: str | None = None + converters: Type[Converters] | None = field(default=None, metadata=dict(exclude=True)) + state: AnnounceTopicState | None = field(default_factory=AnnounceTopicState) command: AnnounceTopicCommand | None = field(default_factory=AnnounceTopicCommand) subscriptions: dict | None = field(default=None, metadata=dict(exclude=True), init=False) @@ -27,6 +30,7 @@ class Capability(Base): for topic in self.topics: topic.set_parent(self) + self.converters = self.converters or self.control.converters self.subscriptions = self.get_subscriptions() self.announce = self.get_announce() diff --git a/haco/control.py b/haco/control.py index 9e2895b..380f968 100644 --- a/haco/control.py +++ b/haco/control.py @@ -1,24 +1,25 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar, Generic, Type, ClassVar from fmtr.tools.json_tools import to_json from haco.base import Base from haco.capabilities import Capability from haco.obs import logger -from haco.utils import sanitize_name +from haco.utils import sanitize_name, Converters, ConvertersBool if TYPE_CHECKING: from haco.device import Device +DeviceT = TypeVar("DeviceT", bound="Device") @dataclass(kw_only=True) -class Control(Base): +class Control(Base, Generic[DeviceT]): + converters: ClassVar[Type[Converters]] = ConvertersBool + name: str - - - device: Device = field(default=None, init=False) + device: DeviceT = field(default=None, init=False) capabilities: None | list[Capability] = field(default=None, metadata=dict(exclude=True)) unique_id: str | None = field(default=None, init=False) availability_topic: str | None = field(default=None, init=False) diff --git a/haco/select.py b/haco/select.py index c49b058..716bd7c 100644 --- a/haco/select.py +++ b/haco/select.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from haco.control import Control +from haco.utils import ConvertersString @dataclass(kw_only=True) @@ -8,6 +9,7 @@ class Select(Control): DATA = dict( platform='select' ) + converters = ConvertersString options: list[str] diff --git a/haco/topics.py b/haco/topics.py index aadfe3f..6ceb323 100644 --- a/haco/topics.py +++ b/haco/topics.py @@ -84,14 +84,15 @@ class AnnounceTopicState(AnnounceTopic): try: if is_async: - value_raw = await method(value) + value = await method(value) else: - value_raw = method(value) + value = method(value) except NotImplementedError: logger.warning(f'No method implemented for {self.capability.control.__class__.__name__}.{self.callback_name}: {self.topic}') return + value_raw = self.capability.converters.state(value) await self.capability.control.device.client.publish(self.topic, value_raw) return value_raw @@ -107,13 +108,11 @@ class AnnounceTopicCommand(AnnounceTopic): method = getattr(self.capability.control, self.callback_name, None) is_async = aio.is_async(method) - - #test + value = message.payload.decode('utf-8') + value = self.capability.converters.command(value) try: - value = message.payload.decode('utf-8') - if is_async: value_raw = await method(value) else: diff --git a/haco/utils.py b/haco/utils.py index 4ce9777..45a1002 100644 --- a/haco/utils.py +++ b/haco/utils.py @@ -1,5 +1,8 @@ import string from dataclasses import dataclass +from typing import Callable + +from fmtr.tools.datatype_tools import to_bool CHARS_ALLOWED = string.ascii_lowercase + string.digits SEPS = '_- /' @@ -10,6 +13,9 @@ RECONNECT_DELAY_SEC = 5 ON = 'ON' OFF = 'OFF' +BOOL_MAP = {True: ON, False: OFF} + +to_bool_ha = BOOL_MAP.get def sanitize_name(s, sep='-'): chars = [] @@ -28,6 +34,20 @@ def sanitize_name(s, sep='-'): return sanitized +class Converters: + command: Callable = None + state: Callable = None + + +class ConvertersBool(Converters): + command: Callable = to_bool + state: Callable = to_bool_ha + + +class ConvertersString(Converters): + command: Callable = str + state: Callable = str + @dataclass class Metadata: exclude: bool = False