Files
Marc Billow daf7e3787f Add ruff (lint + format) and ty (type checking) to the project
Adds [tool.ruff] and [tool.ty] config to pyproject.toml with a curated
ruff rule set (E, F, W, I, UP, B, C4, SIM, RUF, ASYNC, LOG, G, PIE, RET,
PERF, N), pins ruff/ty in requirements-dev.txt, reformats the whole tree
with `ruff format`, and fixes the pre-existing lint and type-check debt
those tools surfaced so both run clean.

Production-code type fixes include: HA's ConfigFlowResult vs. the
generic FlowResult in config_flow.py, narrowing BoundEntity.desc to its
platform-specific subclass (SelectDesc/NumberDesc/SensorDesc/etc.) via
cast() instead of an unchecked annotation, converting HA device_class
strings to their proper enum types, a resolve_registry callback typed
as `object` instead of `DeviceRegistry | None`, and a couple of other
narrow correctness fixes (CA key type validation, an index-out-of-bounds
false positive from an empty-tuple fallback, a bool/dict argument swap).

Test-file fixes are mechanical: narrowing SamsungEntityDescription to
the correct subclass via isinstance()/cast() before accessing
subclass-only fields, and asserting Optional write_fn/unit_fn fields
are set before calling them.
2026-08-02 23:56:38 +00:00

229 lines
8.8 KiB
Python

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")]