diff --git a/samsung_appliance/registry/adapter.py b/samsung_appliance/registry/adapter.py index 34d3dbe..5e9c3fd 100644 --- a/samsung_appliance/registry/adapter.py +++ b/samsung_appliance/registry/adapter.py @@ -37,7 +37,6 @@ def _encode(cfg: dict) -> bytes: class RuntimeDescriptor: name: str seed_path: list[str] - default_observe_port: int active_interval_s: float = 5.0 idle_interval_s: float = 30.0 is_active: Optional[Callable[[dict], bool]] = None @@ -208,7 +207,6 @@ def build_runtime_descriptor(bound, *, topic_prefix, ha_prefix, device_name, return RuntimeDescriptor( name=name, seed_path=['device', '0'], - default_observe_port=default_port or 0, active_interval_s=active_interval_s, idle_interval_s=idle_interval_s, is_active=_make_is_active(bound), diff --git a/samsung_appliance/registry/by_type/__init__.py b/samsung_appliance/registry/by_type/__init__.py index 7735076..97a4e38 100644 --- a/samsung_appliance/registry/by_type/__init__.py +++ b/samsung_appliance/registry/by_type/__init__.py @@ -48,4 +48,10 @@ def for_device(one_ui_version: str) -> Optional[DeviceRegistry]: DeviceRegistry if a matching registry exists, None otherwise. """ key = _type_key(one_ui_version) - return _REGISTRY_BY_KEY.get(key) + if key in _REGISTRY_BY_KEY: + return _REGISTRY_BY_KEY[key] + # Suffix fallback: e.g. "french_door_refrigerator" ends with "_refrigerator" + for rkey, reg in _REGISTRY_BY_KEY.items(): + if key.endswith(f'_{rkey}'): + return reg + return None diff --git a/samsung_appliance/registry/by_type/_base.py b/samsung_appliance/registry/by_type/_base.py index 0ea8c45..5c47962 100644 --- a/samsung_appliance/registry/by_type/_base.py +++ b/samsung_appliance/registry/by_type/_base.py @@ -32,6 +32,10 @@ def _build(caps: list[Capability]) -> dict[str, list[Capability]]: for cap in caps: if cap.href is None: raise ValueError(f"Use pattern_capabilities for href=None caps") + if cap.href_prefix is not None: + raise ValueError( + f"href_prefix is only valid for pattern caps (href=None); " + f"cap with href={cap.href!r} must not set href_prefix") out.setdefault(cap.href, []).append(cap) # Validate that multi-cap hrefs have proper discrimination diff --git a/samsung_appliance/registry/capabilities/fridge.py b/samsung_appliance/registry/capabilities/fridge.py index 6d9ad55..5a6c3ed 100644 --- a/samsung_appliance/registry/capabilities/fridge.py +++ b/samsung_appliance/registry/capabilities/fridge.py @@ -353,7 +353,7 @@ def _door_key(href: str) -> str: DOOR_GENERIC = Capability( href=None, - rt_filter='oic.r.door', + href_prefix='/door/', poll_tier='hot', key_fn=_door_key, entities=( diff --git a/samsung_appliance/registry/capability.py b/samsung_appliance/registry/capability.py index 48b9923..5247c71 100644 --- a/samsung_appliance/registry/capability.py +++ b/samsung_appliance/registry/capability.py @@ -13,6 +13,7 @@ class Capability: entities: tuple[SamsungEntityDescription, ...] = () poll_tier: str = 'warm' # 'hot' | 'warm' | 'cold' rt_filter: Optional[str] = None # bind only if rt_filter in rep.get('rt', ()) + href_prefix: Optional[str] = None # pattern caps only: bind only if href starts with this 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. diff --git a/samsung_appliance/registry/discovery.py b/samsung_appliance/registry/discovery.py index 1b30069..73006c4 100644 --- a/samsung_appliance/registry/discovery.py +++ b/samsung_appliance/registry/discovery.py @@ -37,7 +37,6 @@ def discover( log: Optional[Callable[[str], None]] = None, ) -> list[BoundEntity]: out: list[BoundEntity] = [] - bound_hrefs: set[str] = set() for href, rep in resources.items(): if not isinstance(rep, dict): @@ -58,11 +57,12 @@ def discover( matched = True if matched: - bound_hrefs.add(href) continue # Pattern cap fallback — first matching pattern wins for cap in pattern_caps: + if cap.href_prefix and not href.startswith(cap.href_prefix): + continue if cap.rt_filter is not None and cap.rt_filter not in rts: continue if cap.match_fn is not None and not cap.match_fn(rep, resources): diff --git a/tests/test_by_type.py b/tests/test_by_type.py index 4663b74..c50671d 100644 --- a/tests/test_by_type.py +++ b/tests/test_by_type.py @@ -34,6 +34,13 @@ class TestForDevice: registry = for_device("7.0 Toaster") assert registry is None + def test_for_device_suffix_fallback(self): + """for_device("7.0 French Door Refrigerator") resolves via suffix fallback.""" + registry = for_device("7.0 French Door Refrigerator") + assert registry is not None + assert isinstance(registry, DeviceRegistry) + assert registry.name == 'refrigerator' + class TestDeviceRegistries: """Tests for device registries themselves.""" diff --git a/tests/test_by_type_capabilities.py b/tests/test_by_type_capabilities.py index fc041d7..8dc5614 100644 --- a/tests/test_by_type_capabilities.py +++ b/tests/test_by_type_capabilities.py @@ -55,9 +55,9 @@ def test_refrigerator_registry_discovers_expected_keys(): # --------------------------------------------------------------------------- def test_door_generic_binds_unknown_door_href(): - """/door/wine/0 with oic.r.door rt binds via DOOR_GENERIC with key door_wine_open.""" + """/door/wine/0 binds via DOOR_GENERIC (href_prefix='/door/') with key door_wine_open.""" resources = { - '/door/wine/0': {'rt': ['oic.r.door'], 'openState': 'Open'}, + '/door/wine/0': {'openState': 'Open'}, } bound = discover(resources, {}, [fridge.DOOR_GENERIC]) assert len(bound) == 1 @@ -66,10 +66,9 @@ def test_door_generic_binds_unknown_door_href(): def test_door_generic_does_not_bind_doors_aggregate(): - """/doors/vs/0 with Samsung-proprietary rt is not bound by DOOR_GENERIC.""" + """/doors/vs/0 does not start with '/door/' so DOOR_GENERIC must not bind it.""" resources = { '/doors/vs/0': { - 'rt': ['x.com.samsung.da.doors'], 'x.com.samsung.da.items': [], }, } @@ -80,7 +79,7 @@ def test_door_generic_does_not_bind_doors_aggregate(): def test_door_generic_does_not_bind_known_door(): """/door/cooler/0 is claimed by DOOR_FRIDGE exact cap; DOOR_GENERIC must not also bind it.""" resources = { - '/door/cooler/0': {'rt': ['oic.r.door'], 'openState': 'Closed'}, + '/door/cooler/0': {'openState': 'Closed'}, } reg = refrigerator.REGISTRY bound = discover(resources, reg.capabilities, [fridge.DOOR_GENERIC])