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.
97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
"""Pattern C subdevice discovery: AWM-WW-AID-26-ONEBODY washer+dryer combo
|
|
(issue #241).
|
|
|
|
The fixture is a live capture from the reporting board (see its
|
|
`seeds_note`): the master tree is the dryer (`oic.d.dryer`,
|
|
`..._DV80H27H`), and a complete washer tree (`..._WF80H`) answers at
|
|
`/<uuid>/device/0`, where the UUID appears *only* as the path prefix of the
|
|
`x.com.samsung.da.multidevice` link in `/oic/res` -- no `subdeviceIdList`
|
|
(Pattern B's signal), no `/device/<n>` sibling (Pattern A's).
|
|
"""
|
|
|
|
from tests.conftest import FakeCoapSession, _discover_full, _load_device_full
|
|
|
|
FIXTURE = "washer_dryer_onebody_awm"
|
|
WASHER_UUID = "58b7d338-15c5-97d3-b562-000000000001"
|
|
|
|
|
|
def _discover():
|
|
resources, oic_res, seeds = _load_device_full(FIXTURE)
|
|
return _discover_full(resources, oic_res, seeds)
|
|
|
|
|
|
def test_washer_subdevice_materializes_from_oic_res_uuid_link():
|
|
_bound, materialized, _skipped, _full_resources, _device_type_name = _discover()
|
|
|
|
assert [s.key for s in materialized] == [WASHER_UUID]
|
|
sub = materialized[0]
|
|
assert sub.kind == "prefixed"
|
|
assert sub.seed_path == (WASHER_UUID, "device", "0")
|
|
# Collection mode, not the issue-#205 flat fallback.
|
|
assert sub.flat_hrefs == ()
|
|
|
|
|
|
def test_washer_subdevice_probe_only_fires_for_the_advertised_uuid():
|
|
"""Pattern C must not invent candidates: exactly one prefixed seed is
|
|
probed (the UUID from the /oic/res multidevice link), alongside the
|
|
bounded speculative Pattern A probes this board 4.04s."""
|
|
from custom_components.localthings.registry.subdevices import (
|
|
enumerate_subdevices,
|
|
)
|
|
|
|
resources, oic_res, seeds = _load_device_full(FIXTURE)
|
|
probes: dict[str, bool] = {}
|
|
sess = FakeCoapSession(seeds)
|
|
candidates, _extra = enumerate_subdevices(
|
|
sess,
|
|
resources,
|
|
oic_res,
|
|
probe_log=probes.__setitem__,
|
|
)
|
|
|
|
assert probes[f"/{WASHER_UUID}/device/0"] is True
|
|
assert probes["/device/1"] is False
|
|
assert probes["/device/2"] is False
|
|
# No flat-fallback flood: the seed Collection answered, so no per-href
|
|
# probes under the prefix beyond the seed itself.
|
|
prefixed_probes = [h for h in probes if h.startswith(f"/{WASHER_UUID}/")]
|
|
assert prefixed_probes == [f"/{WASHER_UUID}/device/0"]
|
|
assert [c.key for c in candidates] == [WASHER_UUID]
|
|
|
|
|
|
def test_washer_subdevice_binds_its_own_primary_entities():
|
|
"""The washer tree carries its own /operational/state and /power -- the
|
|
liveness gate must see live primary entities, and the bound set must
|
|
carry washer-side keys under the subdevice key prefix."""
|
|
bound, materialized, _skipped, _full, _name = _discover()
|
|
sub = materialized[0]
|
|
|
|
sub_keys = {b.desc.key for b in bound if b.subdevice.key == sub.key}
|
|
assert sub_keys, "washer subdevice bound no entities at all"
|
|
# Its own operational state and its own power switch/sensor -- the two
|
|
# things SmartThings cloud splits this unit over.
|
|
assert any("machine_state" in k for k in sub_keys), sub_keys
|
|
assert any("power" in k for k in sub_keys), sub_keys
|
|
|
|
|
|
def test_master_dryer_entities_unchanged_by_washer_discovery():
|
|
"""Additive only: the master (dryer) side must keep binding the same
|
|
entity keys with and without the washer candidate materializing."""
|
|
resources, _oic_res, _seeds = _load_device_full(FIXTURE)
|
|
|
|
# Without any subdevices (oic_res withheld, seeds empty -> washer never
|
|
# discovered).
|
|
bound_solo, materialized_solo, _s, _f, _n = _discover_full(
|
|
resources,
|
|
[],
|
|
{},
|
|
)
|
|
assert materialized_solo == []
|
|
solo_keys = {(b.href, b.desc.key) for b in bound_solo}
|
|
|
|
bound, materialized, _s2, _f2, _n2 = _discover()
|
|
sub = materialized[0]
|
|
main_keys = {(b.href, b.desc.key) for b in bound if b.subdevice.key != sub.key}
|
|
|
|
assert main_keys == solo_keys
|