From d0808006ab71b340e73a31c8acb989bb2e59a195 Mon Sep 17 00:00:00 2001 From: Edward Brown Date: Wed, 26 Nov 2025 09:31:53 +0000 Subject: [PATCH] Move control state/command method wrapping to topic classes --- haco/capabilities.py | 9 +++------ haco/control.py | 12 +++--------- haco/topics.py | 44 ++++++++++++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/haco/capabilities.py b/haco/capabilities.py index 73eb7f6..fa9e1ca 100644 --- a/haco/capabilities.py +++ b/haco/capabilities.py @@ -20,7 +20,7 @@ class Capability(Base): 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) + parent: Control | None = None announce = None @@ -31,20 +31,17 @@ class Capability(Base): topic.set_parent(self) self.converters = self.converters or self.control.converters - self.subscriptions = self.get_subscriptions() - - self.announce = self.get_announce() def get_announce(self) -> dict: data = {} for topic in self.topics: - data |= topic.announce + data |= topic.get_announce() return data def get_subscriptions(self) -> dict: data = {} for topic in self.topics: - data |= topic.subscriptions + data |= topic.get_subscriptions() return data @property diff --git a/haco/control.py b/haco/control.py index 4386c7a..5b27953 100644 --- a/haco/control.py +++ b/haco/control.py @@ -41,13 +41,7 @@ class Control(Base, Generic[DeviceT]): self.subscriptions = self.get_subscriptions() #self.announce = self.get_announce() - for capability in self.capabilities: - for topic in capability.topics: - method_name = topic.callback_name - method = getattr(self, method_name, None) - if not method: continue - setattr(self, f'_{method_name}', method) - setattr(self, f'{method_name}', topic.wrap_back) + @property def parent(self): @@ -56,7 +50,7 @@ class Control(Base, Generic[DeviceT]): def get_announce(self): data = self.model_dump() for cap in self.capabilities: - data |= cap.announce + data |= cap.get_announce() return data @@ -89,7 +83,7 @@ class Control(Base, Generic[DeviceT]): data = {} for capability in self.capabilities: - data |= capability.subscriptions + data |= capability.get_subscriptions() return data diff --git a/haco/topics.py b/haco/topics.py index f8ea0e7..4dee0d2 100644 --- a/haco/topics.py +++ b/haco/topics.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field from functools import cached_property -from typing import ClassVar, TYPE_CHECKING +from typing import ClassVar, TYPE_CHECKING, Callable from fmtr.tools import aio from haco.obs import logger @@ -24,12 +24,17 @@ class AnnounceTopic: announce: dict | None = field(default=None, metadata=dict(exclude=True)) subscriptions: dict | None = field(default=None, metadata=dict(exclude=True), init=False) + control_method: Callable | None = field(default=None, metadata=dict(exclude=True)) + def set_parent(self, capability: Capability): self.parent = capability if self.key is None: self.key = self.KEY_DEFAULTS[bool(self.capability.name)] - self.announce = self.get_announce() - self.subscriptions = self.get_subscriptions() + + method_name = self.callback_name + method = getattr(self.capability.control, method_name, None) + self.control_method = method + setattr(self.capability.control, method_name, self.wrap_back) def fill(self, mask): return mask.format(**self.fills) @@ -60,6 +65,10 @@ class AnnounceTopic: return f'{self.capability.name}_{self.IO}' + @property + def callback_class_method_name(self): + return f'{self.capability.control.__class__.__name__}.{self.callback_name}' + def get_subscriptions(self): return {} @@ -67,6 +76,9 @@ class AnnounceTopic: def capability(self): return self.parent + def wrap_back(self, value): + raise NotImplementedError() + @dataclass(kw_only=True) class AnnounceTopicState(AnnounceTopic): @@ -76,20 +88,21 @@ class AnnounceTopicState(AnnounceTopic): return {} async def wrap_back(self, value): - method = getattr(self.capability.control, f'_{self.callback_name}', None) - is_async = aio.is_async(method) - # test + if not self.control_method: + logger.error(f'Incomplete base class: {self.callback_class_method_name}') + return + is_async = aio.is_async(self.control_method) try: if is_async: - value = await method(value) + value = await self.control_method(value) else: - value = method(value) + value = self.control_method(value) except NotImplementedError: - logger.warning(f'No method implemented for {self.capability.control.__class__.__name__}.{self.callback_name}: {self.topic}') + logger.warning(f'Subclass has not implemented method: {self.callback_class_method_name} {self.topic} {value}') return value_raw = self.capability.converters.state(value) @@ -105,21 +118,24 @@ class AnnounceTopicCommand(AnnounceTopic): return self.capability.state async def wrap_back(self, message): - method = getattr(self.capability.control, f'_{self.callback_name}', None) - is_async = aio.is_async(method) + if not self.control_method: + logger.error(f'Incomplete base class: {self.callback_class_method_name}') + return + + is_async = aio.is_async(self.control_method) value = message.payload.decode('utf-8') value = self.capability.converters.command(value) try: if is_async: - value_raw = await method(value) + value_raw = await self.control_method(value) else: - value_raw = method(value) + value_raw = self.control_method(value) except NotImplementedError: - logger.warning(f'{self.capability.control.__class__.__name__}.{self.callback_name}: {self.topic}') + logger.warning(f'Subclass has not implemented method: {self.callback_class_method_name} {self.topic} {value}') return # Echo back as new state