AWM-WW-AID-26-ONEBODY (washer+dryer combo) reports numofsubdevice='2' on /multidevice/vs/0 but carries no /subdevices/vs/0 (no subdeviceIdList -- Pattern B's signal) and 4.04s /device/1 and /device/2 (Pattern A's). The washer subdevice's UUID appears only as the path prefix of the x.com.samsung.da.multidevice link in /oic/res; GET /<uuid>/device/0 answers the washer's own full Collection batch (model ..._WF80H vs the master's ..._DV80H27H). Treat every UUID path prefix seen in /oic/res as a prefixed-subdevice candidate (minus ones subdeviceIdList already named), probed with the same tolerated-404 seed RETRIEVE as Pattern B -- the shared body is factored into _probe_prefixed. discover_partitioned's entity-level liveness gate still decides materialization, so a UUID link with no live sibling behind it contributes nothing. Fixture is a live capture from the reporting board (serials/MACs/di scrubbed); tests cover discovery, probe hygiene, washer-side entity binding, and that the master's own entity set is unchanged.
93 lines
3.7 KiB
Python
93 lines
3.7 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
|