From 81d97e2fb5b5d857283bf039e51f0ccde0cfe6f6 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Sun, 28 Jun 2026 22:20:58 -0500 Subject: [PATCH] Task 1: Simplify Capability; extend BoundEntity - Make href Optional[str] = None for pattern capabilities - Remove observe field (v2 architecture observes all hrefs) - Add rt_filter, match_fn, key_fn fields to Capability - Add entities default value tuple[()] to Capability - Add key_override field to BoundEntity - Update _key() to use key_override when present - Update _make_observe_paths() to observe all unique hrefs (OBSERVE as accelerator) - Guard _build() to skip pattern capabilities (href is None) - Add test_bound_entity_key_override_takes_precedence test - Update test_capability_defaults to remove observe assertion All 61 tests passing (60 existing + 1 new). --- samsung_appliance/registry/adapter.py | 4 ++-- samsung_appliance/registry/capability.py | 8 ++++--- samsung_appliance/registry/discovery.py | 1 + samsung_appliance/registry/registry.py | 7 +++++- tests/test_adapter.py | 28 ++++++++++++++++++++++++ tests/test_capability.py | 1 - 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/samsung_appliance/registry/adapter.py b/samsung_appliance/registry/adapter.py index 3892810..73ef6d5 100644 --- a/samsung_appliance/registry/adapter.py +++ b/samsung_appliance/registry/adapter.py @@ -27,7 +27,7 @@ def _segs(href: str) -> list[str]: def _key(b: BoundEntity) -> str: - return f"{b.desc.key}{b.instance}" + return f"{b.key_override or b.desc.key}{b.instance}" def _encode(cfg: dict) -> bytes: @@ -70,7 +70,7 @@ def _make_flatten(bound: list[BoundEntity]) -> Callable[[dict], dict]: def _make_observe_paths(bound: list[BoundEntity]) -> list[list[str]]: seen, out = set(), [] for b in bound: - if not b.capability.observe or b.href in seen: + if b.href in seen: continue seen.add(b.href) out.append(_segs(b.href)) diff --git a/samsung_appliance/registry/capability.py b/samsung_appliance/registry/capability.py index e7e4f32..48b9923 100644 --- a/samsung_appliance/registry/capability.py +++ b/samsung_appliance/registry/capability.py @@ -9,10 +9,12 @@ from .entities import SamsungEntityDescription @dataclass(frozen=True, kw_only=True) class Capability: - href: str - entities: tuple[SamsungEntityDescription, ...] + href: Optional[str] = None + entities: tuple[SamsungEntityDescription, ...] = () poll_tier: str = 'warm' # 'hot' | 'warm' | 'cold' - observe: bool = True + rt_filter: Optional[str] = None # bind only if rt_filter in rep.get('rt', ()) + match_fn: Optional[Callable[[dict, dict], bool]] = None # match_fn(rep, resources) -> bool + key_fn: Optional[Callable[[str], str]] = None # href -> entity key (pattern caps) # Rare optional hooks — only operational-state-style resources use these. active_when: Optional[Callable[[dict], bool]] = None on_observation: Optional[Callable[[dict, dict], None]] = None diff --git a/samsung_appliance/registry/discovery.py b/samsung_appliance/registry/discovery.py index 72b2ba2..c3bdc7c 100644 --- a/samsung_appliance/registry/discovery.py +++ b/samsung_appliance/registry/discovery.py @@ -19,6 +19,7 @@ class BoundEntity: capability: Capability desc: SamsungEntityDescription instance: str = '' + key_override: Optional[str] = None def instance_suffix(href: str) -> str: diff --git a/samsung_appliance/registry/registry.py b/samsung_appliance/registry/registry.py index 5310c54..b79b0a4 100644 --- a/samsung_appliance/registry/registry.py +++ b/samsung_appliance/registry/registry.py @@ -10,9 +10,14 @@ from .capability import Capability def _build() -> dict[str, Capability]: - """Build the registry, raising ValueError if any href is duplicated.""" + """Build the registry, raising ValueError if any href is duplicated. + + Skips capabilities with href=None (pattern capabilities, handled elsewhere). + """ out: dict[str, Capability] = {} for cap in ALL: + if cap.href is None: + continue if cap.href in out: raise ValueError(f"duplicate capability href: {cap.href}") out[cap.href] = cap diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 0785322..050c09b 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -57,3 +57,31 @@ def test_switch_discovery_config_has_correct_payloads(dishwasher_resources): assert cfg['state_off'] == 'Off' assert 'On' in cfg['value_template'] assert 'Off' in cfg['value_template'] + + +def test_bound_entity_key_override_takes_precedence(): + from samsung_appliance.registry.adapter import _key + from samsung_appliance.registry.discovery import BoundEntity + from samsung_appliance.registry.entities import SensorDesc + + cap = common.ENERGY_METER + desc = SensorDesc( + key='power_watts', + field='x.com.samsung.da.power', + unit='W', + device_class='power', + state_class='measurement', + value_fn=lambda x: x, + ) + + # Without key_override, should use desc.key + b1 = BoundEntity(href='/power/vs/0', capability=cap, desc=desc, instance='') + assert _key(b1) == 'power_watts' + + # With key_override, should use key_override + b2 = BoundEntity(href='/power/vs/0', capability=cap, desc=desc, instance='', key_override='foo') + assert _key(b2) == 'foo' + + # key_override with instance suffix should preserve instance suffix + b3 = BoundEntity(href='/power/vs/0', capability=cap, desc=desc, instance='_1', key_override='bar') + assert _key(b3) == 'bar_1' diff --git a/tests/test_capability.py b/tests/test_capability.py index e97f7a4..70bcab4 100644 --- a/tests/test_capability.py +++ b/tests/test_capability.py @@ -6,7 +6,6 @@ def test_capability_defaults(): c = Capability(href='/kidslock/vs/0', entities=(BinarySensorDesc(key='child_lock', field='x.com.samsung.da.kidsLock'),)) assert c.poll_tier == 'warm' - assert c.observe is True assert c.active_when is None assert len(c.entities) == 1