from custom_components.localthings.registry.capability import Capability from custom_components.localthings.registry.discovery import ( discover, instance_suffix, ) from custom_components.localthings.registry.entities import BinarySensorDesc LOCK = Capability( href="/kidslock/vs/0", entities=(BinarySensorDesc(key="child_lock", field="x.com.samsung.da.kidsLock"),), ) assert LOCK.href is not None REG = {LOCK.href: [LOCK]} def test_instance_suffix(): assert instance_suffix("/kidslock/vs/0") == "" assert instance_suffix("/door/vs/1") == "_1" assert instance_suffix("/icemaker/vs/2") == "_2" def test_discover_binds_present_capability(): resources = {"/kidslock/vs/0": {"x.com.samsung.da.kidsLock": "On"}} bound = discover(resources, REG) assert len(bound) == 1 assert bound[0].href == "/kidslock/vs/0" assert bound[0].desc.key == "child_lock" assert bound[0].instance == "" def test_discover_skips_unknown_href(): seen = [] resources = {"/mystery/vs/0": {"x.com.samsung.da.mystery": "x"}} bound = discover(resources, REG, log=seen.append) assert bound == [] assert any("mystery" in m for m in seen) def test_discover_multi_instance_suffixes(): cap = Capability( href="/door/vs/0", entities=(BinarySensorDesc(key="door", field="x.com.samsung.da.doorState"),), ) resources = { "/door/vs/0": {"x.com.samsung.da.doorState": "Open"}, "/door/vs/1": {"x.com.samsung.da.doorState": "Closed"}, } reg = {"/door/vs/0": [cap], "/door/vs/1": [cap]} bound = discover(resources, reg) insts = sorted(b.instance for b in bound) assert insts == ["", "_1"] def test_discover_logs_unregistered_href(): seen = [] cap = Capability( href="/kidslock/vs/0", entities=(BinarySensorDesc(key="child_lock", field="x.com.samsung.da.kidsLock"),), ) resources = { "/kidslock/vs/0": {"x.com.samsung.da.kidsLock": "On"}, "/mystery/vs/0": {"x.com.samsung.da.mystery": "x"}, } bound = discover(resources, {"/kidslock/vs/0": [cap]}, log=seen.append) assert len(bound) == 1 assert any("mystery" in m for m in seen) # --------------------------------------------------------------------------- # New tests for Task 2: pattern caps, rt_filter, match_fn # --------------------------------------------------------------------------- def test_discover_pattern_cap_binds_unmatched_href(): """Pattern cap with rt_filter binds unmatched hrefs; exact-href cap does not steal unmatched hrefs.""" cooler_cap = Capability( href="/door/cooler/0", entities=(BinarySensorDesc(key="cooler_door", field="x.com.samsung.da.doorState"),), ) door_pattern = Capability( href=None, rt_filter="oic.r.door", entities=(BinarySensorDesc(key="door", field="x.com.samsung.da.doorState"),), ) resources = { "/door/cooler/0": {"x.com.samsung.da.doorState": "Closed", "rt": ["oic.r.door"]}, "/door/wine/0": {"x.com.samsung.da.doorState": "Open", "rt": ["oic.r.door"]}, } reg = {"/door/cooler/0": [cooler_cap]} bound = discover(resources, reg, pattern_caps=[door_pattern]) hrefs = [b.href for b in bound] # exact cap claims /door/cooler/0 assert "/door/cooler/0" in hrefs # pattern cap claims /door/wine/0 (unmatched by registry) assert "/door/wine/0" in hrefs # /door/cooler/0 is not also claimed by the pattern cap cooler_bindings = [b for b in bound if b.href == "/door/cooler/0"] assert all(b.capability is cooler_cap for b in cooler_bindings) def test_discover_pattern_cap_skips_already_bound_href(): """Pattern cap must not bind an href already claimed by an exact-href cap.""" exact_cap = Capability( href="/door/cooler/0", entities=(BinarySensorDesc(key="cooler_door", field="x.com.samsung.da.doorState"),), ) pattern = Capability( href=None, rt_filter="oic.r.door", entities=(BinarySensorDesc(key="generic_door", field="x.com.samsung.da.doorState"),), ) resources = {"/door/cooler/0": {"x.com.samsung.da.doorState": "Closed", "rt": ["oic.r.door"]}} reg = {"/door/cooler/0": [exact_cap]} bound = discover(resources, reg, pattern_caps=[pattern]) # exactly one binding for /door/cooler/0 — the exact cap, not the pattern assert len(bound) == 1 assert bound[0].capability is exact_cap def test_discover_match_fn_filters_wrong_device(): """A cap with match_fn must not bind when its condition is not met.""" # Without /oven/vs/0 present as a key in resources, match_fn should be False resources_no_oven = {"/other/vs/0": {"x.com.samsung.da.state": "Ready"}} # Put oven cap on a different href to test match_fn rejection oven_cap2 = Capability( href="/other/vs/0", match_fn=lambda r, rs: "/oven/vs/0" in rs, entities=(BinarySensorDesc(key="oven_status", field="x.com.samsung.da.state"),), ) bound = discover(resources_no_oven, {"/other/vs/0": [oven_cap2]}) # match_fn returns False → no bindings assert bound == [] def test_discover_match_fn_decline_is_not_logged_as_gap(): """A registered href whose match_fn declines is known, not a gap. Distinguishes "no capability registered for this href" (a real coverage gap) from "a capability is registered but chose not to bind for this device" (e.g. a filter capability on hardware that doesn't have that filter) — only the former should be reported via `log`. """ seen = [] cap = Capability( href="/filter/vs/0", match_fn=lambda rep, resources: False, entities=(BinarySensorDesc(key="filter", field="x.com.samsung.da.filterStatus"),), ) resources = {"/filter/vs/0": {"x.com.samsung.da.filterStatus": "notused"}} bound = discover(resources, {"/filter/vs/0": [cap]}, log=seen.append) assert bound == [] assert seen == [] def test_discover_pattern_cap_reads_name_field(): """A pattern cap's name_field normalizes a device-given instance name (e.g. an ice maker's "CUBED_ICE") for use in the entity's display name, independent of the href-derived key (issue #27).""" cap = Capability( href=None, href_prefix="/icemaker/", name_field="x.com.samsung.da.iceMaker.name", entities=(BinarySensorDesc(key="enabled", field="x.com.samsung.da.iceMaker.state"),), ) resources = { "/icemaker/one/vs/0": { "x.com.samsung.da.iceMaker.state": "On", "x.com.samsung.da.iceMaker.name": "CUBED_ICE", }, "/icemaker/two/vs/0": { "x.com.samsung.da.iceMaker.state": "On", "x.com.samsung.da.iceMaker.name": "ICE_BITES", }, } bound = discover(resources, {}, pattern_caps=[cap]) names = {b.href: b.instance_name for b in bound} assert names == { "/icemaker/one/vs/0": "Cubed Ice", "/icemaker/two/vs/0": "Ice Bites", } def test_discover_pattern_cap_name_field_absent_leaves_instance_name_none(): cap = Capability( href=None, href_prefix="/icemaker/", name_field="x.com.samsung.da.iceMaker.name", entities=(BinarySensorDesc(key="enabled", field="x.com.samsung.da.iceMaker.state"),), ) resources = {"/icemaker/one/vs/0": {"x.com.samsung.da.iceMaker.state": "On"}} bound = discover(resources, {}, pattern_caps=[cap]) assert bound[0].instance_name is None def test_discover_rt_filter_gates_binding(): """Cap with rt_filter must not bind a rep whose rt list does not match.""" oven_mode_cap = Capability( href="/mode/vs/0", rt_filter="x.com.samsung.da.ovenMode", entities=(BinarySensorDesc(key="oven_mode", field="x.com.samsung.da.mode"),), ) # rep has a different rt → should not bind resources = {"/mode/vs/0": {"rt": ["x.com.samsung.da.mode"], "x.com.samsung.da.mode": "Bake"}} bound = discover(resources, {"/mode/vs/0": [oven_mode_cap]}) assert bound == [] def test_discover_tier_log_fires_for_no_entity_coverage_cap(): """A coverage-only Capability (entities=(), used to mark a href as handled by e.g. a composite climate entity rather than its own HA entity) produces zero BoundEntity rows -- `bound` alone can't tell a caller its poll_tier. tier_log must still report it, or a consumer (the coordinator's hot/warm href lists) silently treats it as 'cold' regardless of what poll_tier the capability actually declares.""" coverage_cap = Capability(href="/wind/strength/vs/0", poll_tier="warm") resources = {"/wind/strength/vs/0": {"x.com.samsung.da.modes": "0"}} seen = [] assert coverage_cap.href is not None bound = discover( resources, {coverage_cap.href: [coverage_cap]}, tier_log=lambda href, tier: seen.append((href, tier)), ) assert bound == [] assert seen == [("/wind/strength/vs/0", "warm")]