This board (NE63T8751SG/AA-class) reports no /information/vs/0 at all -- the modelNum-based routing fallback has nothing to read -- so it fell back to 'unknown' and lost the whole range registry (oven mode/setpoint/ door/connected, cooktop monitoring). /oic/d does carry oic.d.range, though, so this is a routing fix, not a new capability: adds 'oic.d.range' to _OIC_TYPE_TO_KEY. The second oven cavity is a genuine Pattern A indexed subdevice at /device/1 (issue #177's mechanism) -- once routing resolves the master to the range registry, the same registry already applies to the subdevice's canonical view and every href on both binds with zero gaps. _discover_full gains an optional device_types param (default (), every other fixture unaffected) so a fixture that can only route via /oic/d can exercise the same subdevice-aware pipeline the other composite fixtures already do.
93 lines
3.7 KiB
Python
93 lines
3.7 KiB
Python
"""Dual-cavity range (TP1X_DA-KS-RANGE-0101X, NE63T8751SG/AA-class, issue
|
|
#324): no /information/vs/0 at all, so the modelNum-based fallback has
|
|
nothing to read -- routing depends entirely on /oic/d's own device type
|
|
(`oic.d.range`), read separately from the /device/0 batch (see
|
|
registry/identity.py and by_type/__init__.py's `_OIC_TYPE_TO_KEY`).
|
|
|
|
The second oven cavity answers as an indexed sibling at /device/1 (Pattern
|
|
A, same mechanism as the AC family's issue #177 fixtures) -- the master's
|
|
/mode/vs/0 (defaultMode 'UpperConvectionBake') is the upper cavity, the
|
|
subdevice's canonical /mode/vs/0 (defaultMode 'LowerConvectionBake') is the
|
|
lower one.
|
|
"""
|
|
|
|
from custom_components.localthings.registry.adapter import flatten
|
|
from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve
|
|
from tests.conftest import _discover_full, _load_device_full
|
|
|
|
FIXTURE = "range_tp1x_da_ks_range_0101x"
|
|
DEVICE_TYPES = ("oic.wk.d", "oic.d.range")
|
|
|
|
|
|
def _discover():
|
|
resources, oic_res, seeds = _load_device_full(FIXTURE)
|
|
return _discover_full(resources, oic_res, seeds, DEVICE_TYPES)
|
|
|
|
|
|
def test_resolves_via_oic_type():
|
|
reg = for_device_by_oic_type(DEVICE_TYPES)
|
|
assert reg is not None and reg.name == "range"
|
|
|
|
|
|
def test_resolves_via_the_full_resolve_entrypoint():
|
|
resources, _oic_res, _seeds = _load_device_full(FIXTURE)
|
|
reg = resolve(resources, device_types=DEVICE_TYPES)
|
|
assert reg is not None and reg.name == "range"
|
|
|
|
|
|
def test_no_unbound_hrefs():
|
|
"""Every resource in the issue #324 dump binds or is ignored, on both
|
|
the master (upper cavity) and the materialized second cavity -- clears
|
|
the coverage-gap repair."""
|
|
resources, oic_res, seeds = _load_device_full(FIXTURE)
|
|
unbound = []
|
|
from custom_components.localthings.registry.registry import CAPABILITIES
|
|
from custom_components.localthings.registry.subdevices import (
|
|
discover_partitioned,
|
|
enumerate_subdevices,
|
|
)
|
|
from tests.conftest import FakeCoapSession
|
|
|
|
sess = FakeCoapSession(seeds)
|
|
candidates, extra = enumerate_subdevices(sess, resources, oic_res)
|
|
full_resources = {**resources, **extra}
|
|
discover_partitioned(
|
|
full_resources,
|
|
candidates,
|
|
resolve,
|
|
CAPABILITIES,
|
|
log=unbound.append,
|
|
oic_device_types=DEVICE_TYPES,
|
|
)
|
|
assert unbound == []
|
|
|
|
|
|
def test_second_cavity_materializes_as_an_indexed_subdevice():
|
|
_bound, materialized, skipped, _full_resources, device_type_name = _discover()
|
|
assert device_type_name == "range"
|
|
assert skipped == []
|
|
assert [(s.kind, s.key) for s in materialized] == [("indexed", "1")]
|
|
assert materialized[0].seed_path == ("device", "1")
|
|
|
|
|
|
def test_both_cavities_expose_distinct_oven_state():
|
|
bound, _materialized, _skipped, full_resources, _name = _discover()
|
|
state = flatten(bound, full_resources)
|
|
for key in ("oven_mode", "oven_setpoint", "oven_state", "machine_state", "door_open"):
|
|
assert key in state, key
|
|
# Second cavity's entities carry the subdevice1_ prefix (adapter._key) --
|
|
# the master's own keys stay unprefixed, so both cavities get distinct
|
|
# unique_ids rather than colliding on the same entity key.
|
|
assert "subdevice1_oven_mode" in state
|
|
assert "subdevice1_oven_setpoint" in state
|
|
|
|
|
|
def test_cooktop_monitoring_present_on_the_master_only():
|
|
"""The cooktop half belongs to the appliance as a whole, not either oven
|
|
cavity -- /cooktopmonitoring/vs/0 has no per-cavity index."""
|
|
bound, _materialized, _skipped, full_resources, _name = _discover()
|
|
state = flatten(bound, full_resources)
|
|
assert "cooktop_running_state" in state
|
|
assert "warming_center_state" in state
|
|
assert "subdevice1_cooktop_running_state" not in state
|