Files
localthings/tests/test_diagnostics_subdevices.py
T
Marc Billow e78d941af3 fix(subdevices): fall back to per-href probing when a prefixed subdevice has no /device/0 Collection
Issue #205 shows the UUID-prefixed pattern's own reference device
(TP2X_FAC_BORA_21K) doesn't always answer /<uuid>/device/0, contrary to
what the pattern was built against. When that Collection GET comes back
empty, enumerate_subdevices now probes every href the master itself
answered this cycle individually under the UUID prefix, keeping whichever
ones respond. Subdevice gains a flat_hrefs field for this, and the
coordinator re-polls those hrefs individually each cycle instead of
re-fetching a Collection batch that doesn't exist.

Built a fixture from the reporter's real #205 diagnostics dump: the
fallback finds a candidate through the one href already confirmed live
under this UUID (/information/vs/0, from the #177 thread), and
discover_partitioned's liveness gate correctly holds it back since that
href alone binds no entity -- honest current state, not a guessed
resolution.
2026-07-30 02:25:26 +00:00

130 lines
6.1 KiB
Python

"""Sanity check for diagnostics.py's issue #177 additions (subdevices,
subdevices_skipped, subdevice_probes) against a real composite-device
discovery run -- makes sure the new blocks are actually reachable/shaped
right, not just that the coordinator's own attributes look correct in
isolation (test_subdevice_discovery.py covers that)."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from custom_components.localthings.const import DOMAIN
from custom_components.localthings.diagnostics import (
async_get_config_entry_diagnostics,
)
from tests.test_subdevice_discovery import _coordinator, _discover
async def test_diagnostics_reports_materialized_and_skipped_subdevices(
hass: HomeAssistant, enable_custom_integrations,
) -> None:
coordinator = _coordinator(hass)
await _discover(coordinator, 'airconditioner_artik051_dongle_fac_18k')
hass.data.setdefault(DOMAIN, {})[coordinator._entry.entry_id] = coordinator
diag = await async_get_config_entry_diagnostics(hass, coordinator._entry)
subdevice_keys = {su['key'] for su in diag['subdevices']}
skipped_keys = {su['key'] for su in diag['subdevices_skipped']}
assert subdevice_keys == {'1'}
assert skipped_keys == {'2'}
assert diag['subdevices'][0]['bound_entity_count'] > 0
assert diag['subdevices_skipped'][0]['hrefs']
assert '/multidevice/vs/0' in diag['subdevice_probes']
# The reporter's hand-read value, carried in the fixture's `probes` map
# (it belongs to no batch -- see that fixture's seeds_note). Two real
# subdevices, master + bedroom, independently corroborating the gate's
# decision to skip /device/2. Reported on its own, never as a resource.
assert diag['multidevice'] == {'x.com.samsung.da.numofsubdevice': '2'}
assert '/multidevice/vs/0' not in diag['resources']
async def test_top_level_resources_is_the_master_subdevice_only(
hass: HomeAssistant, enable_custom_integrations,
) -> None:
"""`resources` reports this subdevice's own hrefs and nothing else.
It used to be `last_resources` raw -- the union across every live subdevice,
keyed by real hrefs -- so a sibling's /mode/vs/1 sat in it alongside the
master's /mode/vs/0 with no attribution, contradicting both the module
docstring and the adding-device-support skill ("the parsed /device/0
snapshot"). Each sibling carries its own canonicalized block instead.
"""
coordinator = _coordinator(hass)
await _discover(coordinator, 'airconditioner_artik051_dongle_fac_18k')
hass.data.setdefault(DOMAIN, {})[coordinator._entry.entry_id] = coordinator
diag = await async_get_config_entry_diagnostics(hass, coordinator._entry)
assert not [h for h in diag['resources'] if h.endswith('/1')]
assert '/mode/vs/0' in diag['resources']
# The sibling's own block carries its state, canonicalized -- '/mode/vs/0',
# not the '/mode/vs/1' it actually answers on.
sub1 = diag["subdevices"][0]
assert '/mode/vs/0' in sub1["resources"]
assert not [h for h in sub1['resources'] if h.endswith('/1')]
assert sub1["resources"]["/power/vs/0"]['x.com.samsung.da.power'] == 'On'
async def test_rejected_candidate_reps_reach_diagnostics_but_not_the_cache(
hass: HomeAssistant, enable_custom_integrations,
) -> None:
"""A gate-rejected slot is never polled again, so anything applied to the
state cache for it would sit frozen at its first-discovery value while
looking as live as every other href. It's kept out of the cache entirely
and reported only under its own subdevices_skipped entry -- which is also
the only place a reader can go to second-guess the gate."""
coordinator = _coordinator(hass)
await _discover(coordinator, 'airconditioner_artik051_dongle_fac_18k')
hass.data.setdefault(DOMAIN, {})[coordinator._entry.entry_id] = coordinator
assert not [h for h in coordinator.last_resources if h.endswith('/2')]
diag = await async_get_config_entry_diagnostics(hass, coordinator._entry)
skipped = diag['subdevices_skipped'][0]
# Present, canonicalized, and visibly the empty state the gate rejected.
assert skipped['resources']['/power/vs/0'] == {}
assert skipped['resources']['/mode/vs/0'] == {}
assert skipped['resources']['/information/vs/0']
async def test_diagnostics_reports_prefixed_subdevice(
hass: HomeAssistant, enable_custom_integrations,
) -> None:
coordinator = _coordinator(hass)
await _discover(coordinator, 'airconditioner_fac_bora_2in1')
hass.data.setdefault(DOMAIN, {})[coordinator._entry.entry_id] = coordinator
diag = await async_get_config_entry_diagnostics(hass, coordinator._entry)
assert len(diag['subdevices']) == 1
assert diag['subdevices'][0]['kind'] == 'prefixed'
# diagnostics reports the raw modelNum field verbatim (board revision/
# capability-bitmap suffix included), unlike device_info_for's model
# (split at '|') -- confirms it's still the wall subdevice's own identity,
# not the master's TP2X_FAC_BORA_21K.
assert diag['subdevices'][0]['model'].startswith('TP2X_FAC_BORA_RAC_21K')
assert diag['subdevices_skipped'] == []
async def test_diagnostics_reports_flat_hrefs_for_skipped_prefixed_candidate(
hass: HomeAssistant, enable_custom_integrations,
) -> None:
"""issue #205: a prefixed candidate found through the per-href flat
fallback (no working /<uuid>/device/0 Collection) has no meaningful
seed_path -- diagnostics reports None there instead of the misleading
bare "/" an empty tuple would otherwise join to, and lists the actual
hrefs the fallback confirmed instead."""
coordinator = _coordinator(hass)
await _discover(coordinator, 'airconditioner_fac_bora_205_flat')
hass.data.setdefault(DOMAIN, {})[coordinator._entry.entry_id] = coordinator
diag = await async_get_config_entry_diagnostics(hass, coordinator._entry)
assert diag['subdevices'] == []
assert len(diag['subdevices_skipped']) == 1
skipped = diag['subdevices_skipped'][0]
assert skipped['kind'] == 'prefixed'
assert skipped['seed_path'] is None
assert skipped['flat_hrefs'] == ['/information/vs/0']