A DV6800N (DA_WM_A51_20_COMMON) reports the same /st/dryercourse/vs/0 courseTable 'Table_00' as issue #357's DVE45R6300W/A3 -- also DA_WM_A51_20_COMMON -- and its reporter listed 14 selectable courses read straight off the appliance's own menu, in the same order /course/vs/0's supportedOptions enumerates them. Initially treated this as a second, incompatible Table_00 code family and added a device-model-keyed disambiguation layer to laundry.cycle_select. That was an unproven assumption: the two reporters' confirmed sets are mostly non-overlapping subsets (11 vs 14 codes), which is exactly what you'd expect from two models on a shared board exposing different subsets of one course table via their own /course/vs/0 supportedOptions -- not evidence of two different code dictionaries. The one code both reporters confirmed, 'a5', means Bedding on both, which is corroborating, not neutral. No confirmed code conflicts between the two sets, so this folds #394's 14 codes straight into the existing dryer_cycle_table_00 catalog entry (mirrored to all shipped languages), same shape as #357's original translations-only change. Adds a scrubbed fixture + golden for the DV6800N dump and tests confirming zero unbound hrefs and that its course codes resolve through the shared, now-larger dryer_cycle_table_00 catalog.
1561 lines
77 KiB
Python
1561 lines
77 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
GOLDEN = Path(__file__).parent / "fixtures" / "golden"
|
|
|
|
|
|
def _new_state_keys(name, resources, device_types=()):
|
|
from custom_components.localthings.registry.adapter import flatten
|
|
from custom_components.localthings.registry.by_type import resolve
|
|
from custom_components.localthings.registry.discovery import discover
|
|
|
|
reg = resolve(resources, device_types=device_types)
|
|
if reg is None:
|
|
from custom_components.localthings.registry.registry import CAPABILITIES
|
|
|
|
caps, pats = CAPABILITIES, []
|
|
else:
|
|
caps, pats = reg.capabilities, reg.pattern_capabilities
|
|
bound = discover(resources, caps, pats)
|
|
state = flatten(bound, resources)
|
|
return sorted(state.keys())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name,ip",
|
|
[
|
|
("dishwasher", "10.0.0.129"),
|
|
("refrigerator", "10.0.0.254"),
|
|
],
|
|
)
|
|
def test_registry_reproduces_golden_state_keys(name, ip, request):
|
|
from tests.conftest import _load_resources
|
|
|
|
resources = _load_resources(ip)
|
|
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
|
state_keys = _new_state_keys(name, resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_washer():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("washer")
|
|
golden = json.loads((GOLDEN / "washer.json").read_text())
|
|
state_keys = _new_state_keys("washer", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_ehs():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("ehs")
|
|
golden = json.loads((GOLDEN / "ehs.json").read_text())
|
|
state_keys = _new_state_keys("ehs", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_washer_wa8000t():
|
|
"""Top-load washer (WA8000T, issue #106) reports no oneUiVersion and
|
|
used the 'WA' consumer-model prefix, previously unmapped in
|
|
_CONSUMER_PREFIX_TO_KEY -- fell back to 'unknown'.
|
|
|
|
Also gains drum_clean_last_cleaned (issue #258): this fixture's
|
|
DrumCleanLog_ is a '|'-joined multi-entry history, which always failed
|
|
datetime.fromisoformat and silently returned None before
|
|
laundry.drum_clean_last_cleaned learned to take the last entry."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("washer_wa8000t")
|
|
golden = json.loads((GOLDEN / "washer_wa8000t.json").read_text())
|
|
state_keys = _new_state_keys("washer_wa8000t", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dryer():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dryer")
|
|
golden = json.loads((GOLDEN / "dryer.json").read_text())
|
|
state_keys = _new_state_keys("dryer", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dryer_dve50a8600():
|
|
"""DVE50A8600V/A3 (issue #79) -- description pairs two model numbers
|
|
('..._DVE50A8800_8600/...'), so the true 'DV' consumer-model token sits
|
|
one segment before the actual last segment ('8600'). The old
|
|
last-segment-only check missed it and fell back to 'unknown'; resolved
|
|
via _consumer_model_key scanning segments from the end."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dryer_dve50a8600")
|
|
golden = json.loads((GOLDEN / "dryer_dve50a8600.json").read_text())
|
|
state_keys = _new_state_keys("dryer_dve50a8600", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dryer_dv6800n():
|
|
"""DA_WM_A51_20_COMMON/DV6800N (issue #394) resolves via /oic/d's
|
|
'oic.d.dryer' device type, not board-token guessing -- its modelNum's
|
|
board tokens ('DA', 'WM', 'COMMON') are all deliberately excluded from
|
|
_BOARD_TOKEN_TO_KEY (see registry/by_type's module docstring)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dryer_dv6800n")
|
|
golden = json.loads((GOLDEN / "dryer_dv6800n.json").read_text())
|
|
state_keys = _new_state_keys(
|
|
"dryer_dv6800n", resources, device_types=("oic.wk.d", "oic.d.dryer")
|
|
)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner")
|
|
golden = json.loads((GOLDEN / "airconditioner.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_ailp_fac():
|
|
"""AILP_DA-AC-FAC-02011_0000 (issue #319) resolves both ways --
|
|
modelNum's 'FAC' board token and /oic/d's oic.d.airconditioner type
|
|
independently agree on the airconditioner registry. Passes
|
|
device_types through resolve() to exercise that agreement, not because
|
|
the board token is absent."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_ailp_fac")
|
|
golden = json.loads((GOLDEN / "airconditioner_ailp_fac.json").read_text())
|
|
state_keys = _new_state_keys(
|
|
"airconditioner_ailp_fac", resources, device_types=("oic.wk.d", "oic.d.airconditioner")
|
|
)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_oven_tp2x_ks_walloven():
|
|
"""TP2X_DA-KS-WALLOVEN-000002 (issue #300), a steam-oven-class board
|
|
(water tank, descale, pyro-free) quite unlike the NV7000BS-class board
|
|
oven.py's write comments were written against. /diagnosis/vs/0 was the
|
|
dump's only unbound href, now covered via dishwasher.DIAGNOSIS. The
|
|
board's options[] also has no UpperLamp_ token at all -- confirms
|
|
LAMP's new exists_fn keeps it from registering a phantom switch here,
|
|
same fastpreheat/NaturalSteam-class gap issue #183 fixed on its
|
|
siblings."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("oven_tp2x_ks_walloven")
|
|
golden = json.loads((GOLDEN / "oven_tp2x_ks_walloven.json").read_text())
|
|
state_keys = _new_state_keys(
|
|
"oven_tp2x_ks_walloven", resources, device_types=("oic.wk.d", "oic.d.oven")
|
|
)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dehumidifier():
|
|
"""TP1X_DA_AC_DHM_01001_0000 (issue #88, AY18CG7500GED) shares the DA_AC_
|
|
board family with the room-AC models but carries the '_DHM_' token;
|
|
resolved via the '_DHM_' modelNum fallback in for_device_by_model into a
|
|
dedicated dehumidifier registry (target humidity, operating mode, reused
|
|
AC filter/auto-clean/mute-once capabilities)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dehumidifier")
|
|
golden = json.loads((GOLDEN / "dehumidifier.json").read_text())
|
|
state_keys = _new_state_keys("dehumidifier", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_water_purifier():
|
|
"""TP2X_WATERPURIFIER_20K (issue #90) reports no oneUiVersion; resolved
|
|
via the 'WATERPURIFIER' modelNum/description fallback into a dedicated
|
|
water_purifier registry (dispense settings, sterilize/filter status,
|
|
favorite capacity, and the three lock switches)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("water_purifier")
|
|
golden = json.loads((GOLDEN / "water_purifier.json").read_text())
|
|
state_keys = _new_state_keys("water_purifier", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_water_purifier_ailite_25k():
|
|
"""AILITE_WATERPURIFIER_25K (issue #196, RWP70F15ANW) -- a coffee-capable
|
|
water purifier on an AILITE_DA-REF-WATERPURIFIER board, whose modelNum's
|
|
'REF' token would otherwise misroute it to the refrigerator registry (see
|
|
TestBoardTokenAmbiguity's carve-out). Also the first dump to expose
|
|
/cup/state/vs/0, /statistic/pour/vs/0, and the settings/sound/* trio on
|
|
this device type, and a hot_water_temperature select that must gate off
|
|
(no supportedHotTemperatures reported) rather than surface as 'unknown'."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("water_purifier_ailite_25k")
|
|
golden = json.loads((GOLDEN / "water_purifier_ailite_25k.json").read_text())
|
|
state_keys = _new_state_keys("water_purifier_ailite_25k", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_water_purifier_coffee():
|
|
"""TP2X_WATERPURIFIER_20K coffee-capable variant (issue #107) adds
|
|
/favorite/coffee/vs/0, /favorite/hotwater/vs/0, and three static
|
|
coffee-recipe resources not present in issue #90's original dump.
|
|
|
|
This fixture's /setting/waterpurifier/vs/0 turns out to report no
|
|
supportedHotTemperatures either (only hotwaterLevel/hotwaterRange, the
|
|
same shape issue #196 surfaced) -- so hot_water_temperature dropped out
|
|
of this golden when the #196 fix (see water_purifier.DISPENSE's
|
|
exists_fn) landed. This fixture was quietly hitting the same 'unknown'
|
|
bug all along; the registry-level golden just had no way to show it
|
|
since flatten()'s state dict doesn't model select option membership."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("water_purifier_coffee")
|
|
golden = json.loads((GOLDEN / "water_purifier_coffee.json").read_text())
|
|
state_keys = _new_state_keys("water_purifier_coffee", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_gas_cooktop_tp2x_ks():
|
|
"""TP2X_DA-KS-COOKTOP-000001 (issue #314) -- routes via
|
|
for_device_by_resources' DeviceType_/OperationState signature, same as
|
|
the original cooktop fixture. /alarms/vs/0 and /kidslock/vs/0 were the
|
|
dump's only unbound hrefs; both are common.UNIVERSAL shapes this
|
|
registry now picks up individually (common.ALARMS,
|
|
common.KIDS_LOCK_VS_FALLBACK)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("gas_cooktop_tp2x_ks")
|
|
golden = json.loads((GOLDEN / "gas_cooktop_tp2x_ks.json").read_text())
|
|
state_keys = _new_state_keys("gas_cooktop_tp2x_ks", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_cooktop():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("cooktop")
|
|
golden = json.loads((GOLDEN / "cooktop.json").read_text())
|
|
state_keys = _new_state_keys("cooktop", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_us():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp1x_ref_21k_us")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_us.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_us", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_eu():
|
|
"""TP1X_REF_21K, EU region variant (issue #165) -- self-reports
|
|
oneUiVersion "7.0 Refrigerator" like the US variant, but additionally
|
|
carries /rm/control/vs/0 (a bare resource-monitoring poll-interval
|
|
config, added to the global ignore list) that the US dump doesn't
|
|
report at all. Door sensors (the reporter's actual ask) were already
|
|
covered by fridge.DOOR_GENERIC/DOORS_FALLBACK -- this href was the only
|
|
gap keeping the coverage repair open."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp1x_ref_21k_eu")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_eu.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_eu", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_airfilter():
|
|
"""TP1X_REF_21K, air-filter-equipped variant (issue #318) -- reports
|
|
/filter/airdustfilter/vs/0 (internal deodorizing filter), the one
|
|
unbound href on this dump. Unlike airconditioner.AIR_FILTER's
|
|
filterUsage/filterCapacity pair, this board's filterUsage is already a
|
|
0-100 percentage with no filterCapacity to divide by."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp1x_ref_21k_airfilter")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_airfilter.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_airfilter", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range_hood():
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("range_hood")
|
|
golden = json.loads((GOLDEN / "range_hood.json").read_text())
|
|
state_keys = _new_state_keys("range_hood", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_washer_flexwash():
|
|
"""FlexWash twin washers (WV-prefix consumer model, e.g. WV55M9600AW)
|
|
report no oneUiVersion and previously fell through for_device_by_model's
|
|
consumer-prefix map entirely -- issue #19.
|
|
|
|
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
|
|
above for why."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("washer_flexwash")
|
|
golden = json.loads((GOLDEN / "washer_flexwash.json").read_text())
|
|
state_keys = _new_state_keys("washer_flexwash", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_washer_dryer_combo():
|
|
"""Washer/dryer combo units carry a writable dryLevel field on
|
|
/washer/vs/0 itself, with no separate dryer resource -- issue #22.
|
|
|
|
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
|
|
above for why."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("washer_dryer_combo")
|
|
golden = json.loads((GOLDEN / "washer_dryer_combo.json").read_text())
|
|
state_keys = _new_state_keys("washer_dryer_combo", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
|
"""ARTIK051_REF_17K's Cool Select Zone pantry compartment
|
|
(/status/pantry/one/vs/0) -- issue #20."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_artik051_ref_17k")
|
|
golden = json.loads((GOLDEN / "refrigerator_artik051_ref_17k.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_artik051_ref_17k", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_artik051_dongle_ref_cooler():
|
|
"""RR40M7165WW (issue #78) -- the same ARTIK051_DONGLE_REF household
|
|
dongle family as issue #77's freezer, but the fridge half: reports
|
|
/door/cooler/0 *and* /door/onedoorfreezer/vs/0 (the latter apparently
|
|
shared firmware naming, not an actual second freezer compartment) plus
|
|
/temperature/{current,desired}/cooler/0. Same pipe-delimited modelNum
|
|
detection gap and DOOR_GENERIC field-name gap as #77, same fix."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_artik051_dongle_ref_cooler")
|
|
golden = json.loads((GOLDEN / "refrigerator_artik051_dongle_ref_cooler.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_artik051_dongle_ref_cooler", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_artik051_dongle_ref():
|
|
"""ARTIK051_DONGLE_REF standalone freezer (issues #77/#83) -- reports no
|
|
oneUiVersion and a pipe-delimited modelNum ('..._DONGLE_REF|<rest>')
|
|
that the old '_REF_' substring check missed entirely; resolved via the
|
|
segment-based check in for_device_by_model. Its door
|
|
(/door/onedoorfreezer/vs/0) and temperature
|
|
(/temperature/{current,desired}/freezer/0) hrefs only bind through
|
|
fridge.py's pattern capabilities, which the 'unknown' fallback never
|
|
tries -- so this also regression-tests that those resources bind at
|
|
all once routed to the right registry."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_artik051_dongle_ref")
|
|
golden = json.loads((GOLDEN / "refrigerator_artik051_dongle_ref.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_artik051_dongle_ref", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k():
|
|
"""TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra
|
|
energy fields (cumulativeConsumption/monthlyConsumption/
|
|
thismonthlyConsumption) surfaced by issue #26."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp2x_ref_20k")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp2x_ref_20k.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp2x_ref_20k", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k_kimchi():
|
|
"""A different physical unit reporting the same "TP2X_REF_20K" modelNum
|
|
string as the fixture above (issue #26's second reporter) -- a
|
|
3-compartment kimchi refrigerator with no flex zone, doors/icemaker, or
|
|
freezer/cooler split, but its own /status/kimchi/<slot>/vs/0 and
|
|
/kimchidoors/top/vs/0 resources (fridge.KIMCHI_ZONE/KIMCHI_DOOR_GENERIC)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp2x_ref_20k_kimchi")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp2x_ref_20k_kimchi.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp2x_ref_20k_kimchi", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_ac_tp1x_da_ac_rac_01011():
|
|
"""Newer AC firmware (Tizen Lite, oneUiVersion "7.0 Air conditioner"; model
|
|
TP1X_DA-AC-RAC-01011) reports temperature via the vendor /temperatures/vs/0
|
|
items[] resource and adds a /light/vs/0 display light, with extra vendor
|
|
housekeeping hrefs -- issue #17 for this model class (PR #36)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp1x_da_ac_rac_01011")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp1x_da_ac_rac_01011.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp1x_da_ac_rac_01011", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp2x_rac_20k():
|
|
"""TP2X_RAC_20K (issue #37) -- reports no oneUiVersion; resolved via the
|
|
'_RAC_' modelNum token fallback in for_device_by_model."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp2x_rac_20k")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp2x_rac_20k.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp2x_rac_20k", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_caww_tp2():
|
|
"""A-CAWW-TP2-20-COMMON (issue #52, System AC / multi-indoor-unit
|
|
commercial install) reports no oneUiVersion and no '_RAC_'/'_PRAC_'
|
|
token; resolved via the '-CAWW-' modelNum fallback in
|
|
for_device_by_model. Otherwise binds cleanly against the existing
|
|
airconditioner registry -- same TP1X/TP2X-class resource surface, plus
|
|
one SAC-only installation-topology resource (ignored)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_caww_tp2")
|
|
golden = json.loads((GOLDEN / "airconditioner_caww_tp2.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_caww_tp2", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_window_ac():
|
|
"""TP1X_DA_AC_WAC_01001_0000 (issue #87, Bespoke Window AC AW06C7155EWAZ)
|
|
reports no oneUiVersion and carries the '_WAC_' (Window Air Conditioner)
|
|
token instead of '_RAC_'/'_PRAC_'; resolved via the '_WAC_' modelNum
|
|
fallback in for_device_by_model. Otherwise binds cleanly against the
|
|
existing airconditioner registry with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_window_ac")
|
|
golden = json.loads((GOLDEN / "airconditioner_window_ac.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_window_ac", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp1x_rac():
|
|
"""TP1X_DA-AC-RAC-01001_0000 (issue #38) -- fuller RAC board with display
|
|
light, self-check, mute-once, and a current-limit setting."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp1x_rac")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp1x_rac.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp1x_rac", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_tp1x_rac_coolonly():
|
|
"""TP1X_DA-AC-RAC-01001 cool-only global variant (issue #91) whose
|
|
/otninformation/vs/0 ships no swVersionInfo block -- resolves via the
|
|
'RAC' board token in its modelNum."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp1x_rac_coolonly")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp1x_rac_coolonly.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp1x_rac_coolonly", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_windfree():
|
|
"""ARTIK051_PRAC_20K, WindFree-capable unit (issue #75) -- same modelNum
|
|
family as the original issue #17 fixture, but its /mode/convenient/vs/0
|
|
additionally reports Nano/NanoSleep/MotionDirect/MotionIndirect, its
|
|
/wind/direction/vs/0 reports Left_And_Right, and /humidity/vs/0's
|
|
fivepercentHumidity is actually populated."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_windfree")
|
|
golden = json.loads((GOLDEN / "airconditioner_windfree.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_windfree", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_lnx_rac_heatpump():
|
|
"""Lennox-branded heat pump on the Samsung RAC board family (issue #173,
|
|
modelNum TP1X_LNX-AC-RAC-01001_0000) -- routes via the existing '-RAC-'
|
|
token, same registry as the plain RAC family. Adds two AI-feature
|
|
resources not seen on prior AC dumps: /mds/absencepowersaving/vs/0
|
|
(absence-detection power saving) and /option/motiondetectwind/stateful/vs/0
|
|
(avoid-direct-wind-on-motion), both exposed read-only per the 'don't
|
|
guess' rule."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_lnx_rac_heatpump")
|
|
golden = json.loads((GOLDEN / "airconditioner_lnx_rac_heatpump.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_lnx_rac_heatpump", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range():
|
|
"""Range/cooktop-oven combo (model TP1X_DA-KS-RANGE-0102X, issue #44) --
|
|
reports no oneUiVersion; resolved via the '-RANGE-' modelNum token
|
|
fallback in for_device_by_model. Reuses the oven family's cavity/
|
|
setpoint/mode/operational-state capabilities and adds range.py's
|
|
per-burner capabilities for the 4 burners this dump reports."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("range")
|
|
golden = json.loads((GOLDEN / "range.json").read_text())
|
|
state_keys = _new_state_keys("range", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_induction_cooktop():
|
|
"""Standalone induction cooktop, no oven attached (model
|
|
TP1X_DA-KS-COOKTOP-01011, issue #86) -- reports no oneUiVersion and a
|
|
hyphenated '-COOKTOP-' modelNum token, resolved via
|
|
for_device_by_model into its own 'induction_cooktop' registry (not
|
|
cooktop.REGISTRY, which is the unrelated NA9300K gas-cooktop family)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("induction_cooktop")
|
|
golden = json.loads((GOLDEN / "induction_cooktop.json").read_text())
|
|
state_keys = _new_state_keys("induction_cooktop", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_induction_cooktop_nv9000d():
|
|
"""NV9000D-/KO2 compatibility fixture: the same three-burner registry,
|
|
with the safety-shutoff status but without optional probe/hood resources."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("induction_cooktop_nv9000d")
|
|
golden = json.loads((GOLDEN / "induction_cooktop_nv9000d.json").read_text())
|
|
state_keys = _new_state_keys("induction_cooktop_nv9000d", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range_no_info():
|
|
"""NE63B8411SS (issue #74) -- reports no oneUiVersion *and* no
|
|
/information/vs/0 at all, so neither for_device nor
|
|
for_device_by_model has anything to key off; resolved via the 'Bake'-
|
|
in-supportedModes + /cooktopmonitoring/vs/0 signature in
|
|
for_device_by_resources. This board's local API has no per-burner
|
|
/cooktop/status/vs/0 array either -- only the coarse
|
|
/cooktopmonitoring/vs/0 monitoring resource covered by range.py's
|
|
COOKTOP_MONITORING."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("range_no_info")
|
|
golden = json.loads((GOLDEN / "range_no_info.json").read_text())
|
|
state_keys = _new_state_keys("range_no_info", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_purifier():
|
|
"""ARTIK051_TVTL_18K (issue #56) -- reports no oneUiVersion; resolved via
|
|
the '_TVTL_' modelNum token fallback in for_device_by_model."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_purifier")
|
|
golden = json.loads((GOLDEN / "air_purifier.json").read_text())
|
|
state_keys = _new_state_keys("air_purifier", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_oven():
|
|
"""Wall oven (model TP1X_DA-KS-OVEN-0107X, issue #55) -- reports no
|
|
oneUiVersion; resolved via the '-OVEN-' modelNum token fallback in
|
|
for_device_by_model, mirroring the '-RANGE-' fallback added for
|
|
issue #44. Before that fallback existed the device type came back
|
|
'unknown' and every href fell through to the global CAPABILITIES
|
|
registry instead of the oven family's own."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("oven")
|
|
golden = json.loads((GOLDEN / "oven.json").read_text())
|
|
state_keys = _new_state_keys("oven", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_washer_wa55a7700av():
|
|
"""DA_WM_TP1_21_COMMON top-load washer (model WA55A7700AV, issue #111)
|
|
-- a different board generation than the WA8000T's TP2_20_COMMON,
|
|
reached through the same 'WA' consumer-model-prefix fallback (issue
|
|
#106). Binds cleanly against the existing washer registry with zero
|
|
unbound hrefs.
|
|
|
|
Also gains drum_clean_last_cleaned (issue #258) -- see the wa8000t test
|
|
above for why."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("washer_wa55a7700av")
|
|
golden = json.loads((GOLDEN / "washer_wa55a7700av.json").read_text())
|
|
state_keys = _new_state_keys("washer_wa55a7700av", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range_ne8300d():
|
|
"""TP1X_DA-KS-RANGE-0102X (model NE8300D, issue #112) -- reports no
|
|
oneUiVersion; resolved via the '-RANGE-' modelNum token fallback.
|
|
Binds cleanly against the existing range registry, including
|
|
/cooktopmonitoring/vs/0 via range.COOKTOP_MONITORING, with zero
|
|
unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("range_ne8300d")
|
|
golden = json.loads((GOLDEN / "range_ne8300d.json").read_text())
|
|
state_keys = _new_state_keys("range_ne8300d", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range_ne63a6511():
|
|
"""NE63A6511SS/AA (issue #138) -- reports no /information/vs/0 at all,
|
|
same shape as issue #74's NE63B8411SS; resolved via the same
|
|
'Bake'-in-supportedModes + /cooktopmonitoring/vs/0 signature in
|
|
for_device_by_resources. Binds cleanly against the existing range
|
|
registry with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("range_ne63a6511")
|
|
golden = json.loads((GOLDEN / "range_ne63a6511.json").read_text())
|
|
state_keys = _new_state_keys("range_ne63a6511", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_ara_ww_tp1_22():
|
|
"""ARA-WW-TP1-22-COMMON wall-mount RACs (model AR10/13/18BYEAAWKNME,
|
|
issues #115/#116/#117/#120) report no oneUiVersion and no
|
|
'_RAC_'/'-RAC-' token -- resolved via the 'ARA-WW-' modelNum fallback.
|
|
Same resource surface as the other TP1X-class room ACs; binds cleanly
|
|
against the existing airconditioner registry with zero unbound hrefs,
|
|
so no new device type was needed."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_ara_ww_tp1_22")
|
|
golden = json.loads((GOLDEN / "airconditioner_ara_ww_tp1_22.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_ara_ww_tp1_22", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_windfree_oscillation():
|
|
"""TP1X_DA-AC-RAC-01011_0000 Bespoke AI WindFree Deluxe (model
|
|
AR60H10D1JWNME, issue #126) -- the same board family as the
|
|
airconditioner_tp1x_da_ac_rac_01011 fixture, but a newer firmware
|
|
variant reporting no /wind/direction/vs/0 at all: swing lives on the
|
|
2-axis /wind/oscillation/vs/0 resource instead (airconditioner.
|
|
HREF_WIND_OSCILLATION, climate.py's oscillation fallback), and it adds
|
|
an /anomalyload/vs/0 overload-response resource (airconditioner.
|
|
ANOMALY_LOAD, read-only per the 'don't guess' rule). Binds cleanly
|
|
with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_windfree_oscillation")
|
|
golden = json.loads((GOLDEN / "airconditioner_windfree_oscillation.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_windfree_oscillation", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_microwave_mw7300b():
|
|
"""TP1X_DA-KS-MICROWAVE-01041 combi microwave (model MW7300B, issue
|
|
#121) -- reports no oneUiVersion; resolved via the '-MICROWAVE-'
|
|
modelNum token fallback onto its own microwave registry. Shares the
|
|
oven board family's operational-state/door/connected/recipe-cook
|
|
Capability objects (reused directly from oven.py), but has its own
|
|
cooking-mode vocabulary, setpoint bounds, and cavity power-level sensor
|
|
(capabilities/microwave.py) -- this device's initial routing folded it
|
|
into the oven registry (issue #121); split into its own device type per
|
|
user feedback that microwaves shouldn't show up as ovens."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("microwave_mw7300b")
|
|
golden = json.loads((GOLDEN / "microwave_mw7300b.json").read_text())
|
|
state_keys = _new_state_keys("microwave_mw7300b", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_qooker_mw7500a():
|
|
"""Bespoke Qooker MW7500A uses Samsung's OVEN board/type metadata but
|
|
proves its microwave semantics through MicroWave mode plus cavity
|
|
powerLevel. The routing correction must expose microwave state keys
|
|
without the misleading oven_mode/oven_state entities."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("qooker_mw7500a")
|
|
golden = json.loads((GOLDEN / "qooker_mw7500a.json").read_text())
|
|
state_keys = _new_state_keys("qooker_mw7500a", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_microwave_me7500d():
|
|
"""TP1X_DA-KS-MICROWAVE-01051 plain microwave (model ME7500D, issues
|
|
#137/#142) -- the same microwave registry as MW7300B above, but this
|
|
board also reports the built-in vent fan's `/hood/fanspeed/vs/0`
|
|
resource, previously unbound. Bound via range_hood.HOOD_FAN (reused
|
|
directly, same resource shape a standalone range hood reports); unlike
|
|
a standalone hood this board has no sibling `/power/0` or
|
|
`/power/vs/0` resource, so fan.py's LocalThingsRangeHoodFan treats
|
|
fan speed 0 as the off state instead of writing a separate power
|
|
resource -- see its `_speed_zero_is_off` check. This board also has
|
|
no `/temperatures/vs/0` or `x.com.samsung.da.hood.autoOperation`
|
|
field, unlike MW7300B, so `setpoint`/`current_temp_c` and
|
|
`automatic_operation` are correctly absent here."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("microwave_me7500d")
|
|
golden = json.loads((GOLDEN / "microwave_me7500d.json").read_text())
|
|
state_keys = _new_state_keys("microwave_me7500d", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_microwave_me7500d_lamp_high():
|
|
"""Same TP1X_DA-KS-MICROWAVE-01051/ME7500D board as microwave_me7500d
|
|
above, but this live capture (issue #152) is the first to report a
|
|
non-Off Lamp token ('Lamp_High'). Locks in that the lamp switch reads
|
|
it as on rather than the previously-hardcoded 'On'-only comparison."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("microwave_me7500d_lamp_high")
|
|
golden = json.loads((GOLDEN / "microwave_me7500d_lamp_high.json").read_text())
|
|
state_keys = _new_state_keys("microwave_me7500d_lamp_high", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_purifier_tp1x_da_ac_air():
|
|
"""TP1X_DA-AC-AIR-01031_0000 (issue #130) self-reports oneUiVersion
|
|
'7.0 Air purifier' (unused for routing) and resolves via its 'AIR'
|
|
board token onto the existing
|
|
air_purifier registry (shared with the older ARTIK051_TVTL family via
|
|
per-href match_fn discrimination -- see capabilities/air_purifier.py).
|
|
Its /mode/vs/0 reports modes/supportedModes directly (Smart/Max/Mid/
|
|
WindFree/Sleep) rather than the older family's packed options[] scheme,
|
|
which the new air_purifier.FAN capability now binds to a real `fan`
|
|
entity (PRESET_MODE, not an ordered speed -- see fan.py). Binds with
|
|
zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_purifier_tp1x_da_ac_air")
|
|
golden = json.loads((GOLDEN / "air_purifier_tp1x_da_ac_air.json").read_text())
|
|
state_keys = _new_state_keys("air_purifier_tp1x_da_ac_air", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_vacuum_station():
|
|
"""A-VSKR-TP1-22-VS9500AL stick-vacuum clean/auto-empty station (issue
|
|
#131) -- reports no oneUiVersion; resolved via the new '-VSKR-'
|
|
modelNum fallback onto a new vacuum_station registry (see
|
|
capabilities/vacuum_station.py's module docstring for why this needed
|
|
a new device type rather than reusing an existing one: the dump has no
|
|
vacuum-body state at all, only station-specific dustbag/dustbin/
|
|
UV-sanitize resources that share no hrefs with anything else already
|
|
modeled). Binds with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("vacuum_station")
|
|
golden = json.loads((GOLDEN / "vacuum_station.json").read_text())
|
|
state_keys = _new_state_keys("vacuum_station", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_artik051_krac_18k():
|
|
"""ARTIK051_KRAC_18K (issue #136) -- reports no oneUiVersion, and its
|
|
'_KRAC_' token was invisible to for_device_by_model's '_RAC_' check (the
|
|
'K' sits between the underscore and 'RAC'), so it fell back to 'unknown'
|
|
and exposed nothing but power. Same ARTIK051 board generation as the
|
|
'_TVTL_' air purifier: no /wind/* resources at all (fan and vane live in
|
|
/airflow/vs/0), no /mode/convenient/vs/0 (the preset is a Comode_* token),
|
|
and several settings carried as /mode/vs/0 options[] tokens."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_artik051_krac_18k")
|
|
golden = json.loads((GOLDEN / "airconditioner_artik051_krac_18k.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_artik051_krac_18k", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_rac_01001():
|
|
"""TP1X_DA-AC-RAC-01001_0000 (model AR07C9150HZN, issue #155) -- binds
|
|
cleanly against the existing airconditioner registry with zero unbound
|
|
hrefs (the registry/discovery side was never the gap here). Its
|
|
/wind/strength/vs/0 reports supportedModes "0"/"31"-"35" instead of the
|
|
"0"-"4" scale climate.py's _DEVICE_TO_FAN was built from, which silently
|
|
dropped every fan speed but Auto -- see
|
|
test_airconditioner_tp1x_rac_01001_fan.py for the climate-level fix."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp1x_rac_01001")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp1x_rac_01001.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp1x_rac_01001", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_rac_odor_controller():
|
|
"""TP1X_DA-AC-RAC-01001_0000 -- reporter's dump: fan (0-4 wind-strength
|
|
scale) and WindFree (Nano/NanoSleep convenient-mode codes) already bind
|
|
via the composite climate entity, no code change needed there. The
|
|
genuine gap was /mode/vs/0's SmartCoolClean_/ProgressSmartClean_ option
|
|
tokens (the cloud custom.airConditionerOdorController capability),
|
|
previously unbound to any entity -- now odor_controller_active/
|
|
odor_controller_progress."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp1x_rac_odor_controller")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp1x_rac_odor_controller.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp1x_rac_odor_controller", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_dresser():
|
|
"""DA_DF_A51_20_COMMON AirDresser (model DF8600T, issue #162) -- reports
|
|
no oneUiVersion; resolved via the '_DF_' modelNum token fallback. Has no
|
|
/wm/editcourse/vs/0 at all, so the course select's option list comes
|
|
entirely from laundry.cycle_options' supportedOptions fallback. Binds
|
|
cleanly against the new air_dresser registry with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_dresser")
|
|
golden = json.loads((GOLDEN / "air_dresser.json").read_text())
|
|
state_keys = _new_state_keys("air_dresser", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_dresser_tp2_20():
|
|
"""DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157) -- a
|
|
different board generation than issue #162's DA_DF_A51_20_COMMON, also
|
|
routed via the '_DF_' modelNum fallback into the same air_dresser
|
|
registry. Unlike #162's board, this one populates /wm/editcourse/vs/0's
|
|
editCourseList directly (no supportedOptions fallback needed) and
|
|
reports two AirDresser-specific resources #162 doesn't have:
|
|
/st/airdressercourse/vs/0 (course table id, ignored.py) and
|
|
/airdresseroption/sanitize/vs/0 (air_dresser.AIR_DRESSER_SANITIZE).
|
|
Binds cleanly with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_dresser_tp2_20")
|
|
golden = json.loads((GOLDEN / "air_dresser_tp2_20.json").read_text())
|
|
state_keys = _new_state_keys("air_dresser_tp2_20", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_dresser_tp1_21():
|
|
"""DA_DF_TP1_21_COMMON AirDresser (model DF3000B, issue #208) -- another
|
|
board generation routed via the '_DF_' modelNum token fallback into the
|
|
same air_dresser registry as #162's and #157's boards (its dump also
|
|
happens to be the first AirDresser one to carry /oic/d's device type,
|
|
'x.com.st.d.steamcloset' -- see test_by_type.py's
|
|
TestForDeviceByOicType for that mapping's own coverage). The first
|
|
AirDresser dump to report /buzzersound/vs/0 (laundry.BUZZER_SOUND,
|
|
added for this issue -- #162's and #157's boards don't report it at
|
|
all). Binds cleanly with zero unbound hrefs; course codes for this
|
|
board's table still aren't identified (same as #162's), so 'cycle'
|
|
remains the raw code rather than a table-specific translation."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_dresser_tp1_21")
|
|
golden = json.loads((GOLDEN / "air_dresser_tp1_21.json").read_text())
|
|
state_keys = _new_state_keys("air_dresser_tp1_21", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_monitor():
|
|
"""Samsung Air Monitor Plus (ASM-KR-TP1-22-ACMB1M, issue #210) -- a
|
|
standalone battery-powered air-quality sensor puck, the first device
|
|
this integration has no controllable-appliance concept for at all (no
|
|
/power/*, only /energy/battery/vs/0). Routes via both /oic/d
|
|
('x.com.st.d.airqualitysensor') and the 'ASM' modelNum board token.
|
|
Reuses air_purifier.AIR_QUALITY's /sensors/vs/0 decode
|
|
(common.sensor_item_value) for dust/fine_dust/super_fine_dust/odor/
|
|
clean_level, adding a CO2 reading those families don't report. Binds
|
|
cleanly with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_monitor")
|
|
golden = json.loads((GOLDEN / "air_monitor.json").read_text())
|
|
state_keys = _new_state_keys("air_monitor", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_purifier_vtww():
|
|
"""A-VTWW-TP2-21-COMMON BESPOKE Cube Air (issue #151) -- reports no
|
|
oneUiVersion; resolved via the '-VTWW-' modelNum token fallback into
|
|
the existing air_purifier registry. Its fan lives on
|
|
/wind/strength/vs/0 (air_purifier.WIND_STRENGTH_FAN) rather than the
|
|
/mode/vs/0 FAN the other two board generations in this registry use.
|
|
Binds cleanly with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_purifier_vtww")
|
|
golden = json.loads((GOLDEN / "air_purifier_vtww.json").read_text())
|
|
state_keys = _new_state_keys("air_purifier_vtww", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora():
|
|
"""TP2X_FAC_BORA_21K Wind-Free 2-in-1 (floor-standing + wall-mounted
|
|
indoor units sharing one outdoor unit and one local IP, issues
|
|
#150/#153) -- reports no oneUiVersion; resolved via the '_FAC_'
|
|
modelNum fallback. Binds cleanly against the existing airconditioner
|
|
registry (including a real climate entity, the actual reported gap)
|
|
with zero unbound hrefs -- /subdevices/vs/0 and /runn/vs/0 are the only
|
|
hrefs this board reports that no other AC family does, both added to
|
|
_AC_IGNORED as undocumented/not-locally-actionable."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_fac_bora")
|
|
golden = json.loads((GOLDEN / "airconditioner_fac_bora.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_fac_bora", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def _new_subdevice_aware_state_keys(name, device_types=()):
|
|
"""Like _new_state_keys, but runs the full subdevice-aware pipeline
|
|
(enumerate_subdevices + discover_partitioned, issue #177) instead of a
|
|
single discover() call, so the golden for a composite-device fixture
|
|
captures every materialized subdevice's keys
|
|
(subdevice1_-/subdevice_<uuid>_-prefixed), not just the master's.
|
|
|
|
`device_types` threads through to discover_partitioned's own
|
|
`oic_device_types` -- needed for a board with no /information/vs/0 at
|
|
all to route from (issue #324's range)."""
|
|
from custom_components.localthings.registry.adapter import flatten
|
|
from tests.conftest import _discover_full, _load_device_full
|
|
|
|
resources, oic_res, seeds = _load_device_full(name)
|
|
bound, _materialized, _skipped, full_resources, _device_type_name = _discover_full(
|
|
resources,
|
|
oic_res,
|
|
seeds,
|
|
device_types,
|
|
)
|
|
state = flatten(bound, full_resources)
|
|
return sorted(state.keys())
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_artik051_dongle_fac_18k():
|
|
"""The reporter's ARTIK051_DONGLE_FAC_18K (issue #177, Pattern A -- indexed
|
|
siblings): a real v0.16.0 dump with a genuine second indoor subdevice at
|
|
`/device/1` (subdevice1_-prefixed keys below) and an unused SmartThings
|
|
slot at `/device/2` that answers its seed but never produces a
|
|
materialized subdevice (see DESIGN-177.md section 4 and
|
|
test_subdevice_discovery.py's explicit "/device/2 produces no entities"
|
|
assertion) -- so this golden has no `subdevice2_`-prefixed keys at all,
|
|
which is the point."""
|
|
name = "airconditioner_artik051_dongle_fac_18k"
|
|
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
|
state_keys = _new_subdevice_aware_state_keys(name)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_cac():
|
|
"""TP1X_DA-AC-CAC-01001_0000 (issue #191) -- fell back to 'unknown' in
|
|
0.16.0 when oneUiVersion detection was dropped, since 'CAC' had never
|
|
been added to the modelNum board-token table. Resolved via the new 'CAC'
|
|
token onto the existing airconditioner registry. Not fully covered yet --
|
|
two hrefs remain unbound (sound-optimization, smart-sensing-cooling),
|
|
both genuinely new to this board generation and out of scope for the
|
|
routing fix; see test_airconditioner_cac.py for the documented gap."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_cac")
|
|
golden = json.loads((GOLDEN / "airconditioner_cac.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_cac", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora_2in1():
|
|
"""The reporter's TP2X_FAC_BORA_21K (issue #177, Pattern B -- UUID-prefixed
|
|
tree): device0/oic_res are real; the wall-mounted subdevice's own
|
|
/information/vs/0 is real (confirmed live by the reporter), the rest of
|
|
its seed tree is constructed (see the fixture's own seeds_note) -- just
|
|
enough to bind a real climate card under the
|
|
`subdevice_6c2dff6dee5cdad16a5e000000000001_` prefix below. Distinct from
|
|
tests/fixtures/airconditioner_fac_bora_device.json, which is
|
|
deliberately left unchanged as the redacted-subdeviceIdList regression
|
|
case (zero subdevices)."""
|
|
name = "airconditioner_fac_bora_2in1"
|
|
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
|
state_keys = _new_subdevice_aware_state_keys(name)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora_205_flat():
|
|
"""The same reporter's same physical TP2X_FAC_BORA_21K unit as the _2in1
|
|
fixture above, but a later capture (issue #205) where /<uuid>/device/0 doesn't
|
|
answer -- contrary to what that fixture's own seed batch assumed the
|
|
Collection endpoint would do. device0/oic_res are real; the only
|
|
UUID-prefixed data is the one href ever actually confirmed live
|
|
(/information/vs/0, same real capture the _2in1 fixture uses), fed
|
|
through registry.subdevices.enumerate_subdevices' per-href flat
|
|
fallback instead of a Collection batch. /information/vs/0 alone binds
|
|
no entity, so the candidate is found but never materializes -- this
|
|
golden has no `subdevice_...`-prefixed keys at all, same shape as
|
|
tests/fixtures/golden/airconditioner_fac_bora.json, which is the point:
|
|
a device whose sibling can't yet be confirmed live must regress to
|
|
exactly the master-only state, never a phantom or partial subdevice."""
|
|
name = "airconditioner_fac_bora_205_flat"
|
|
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
|
state_keys = _new_subdevice_aware_state_keys(name)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_artik051_krac_18k_slot():
|
|
"""The issue #214 reporter's single-split AR12NXWXCWKNEU: a
|
|
non-composite ARTIK051_KRAC_18K whose `/device/1` answers a full-shaped
|
|
batch with every operational rep empty {} and a populated
|
|
/energy/consumption/vs/1. The lifetime kWh counter used to be enough to
|
|
pass discover_partitioned's liveness gate, materializing a phantom
|
|
second air conditioner in HA; a meter is now excluded from that gate
|
|
(subdevices._has_live_primary_entity), so this golden must stay
|
|
byte-identical to airconditioner_artik051_krac_18k.json -- master keys
|
|
only, no `subdevice1_` prefix anywhere -- which is what
|
|
test_krac_18k_energy_only_slot_is_not_materialized asserts structurally."""
|
|
name = "airconditioner_artik051_krac_18k_slot"
|
|
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
|
state_keys = _new_subdevice_aware_state_keys(name)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
master_golden = json.loads((GOLDEN / "airconditioner_artik051_krac_18k.json").read_text())
|
|
assert set(state_keys) == set(master_golden["state_keys"])
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_air_purifier_avt_ww():
|
|
"""AVT-WW-TP1-23-AXX500 (issue #190) -- next-gen BESPOKE Cube Air board;
|
|
reports device_type 'unknown' with empty oneUiVersion because 'VTWW' as a
|
|
whole token doesn't match this board's 'AVT'/'WW' split. Resolved via the
|
|
new 'AVT' modelNum board-token fallback into the existing air_purifier
|
|
registry. Binds cleanly with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("air_purifier_avt_ww")
|
|
golden = json.loads((GOLDEN / "air_purifier_avt_ww.json").read_text())
|
|
state_keys = _new_state_keys("air_purifier_avt_ww", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_artik051_krac_energy():
|
|
"""AR12NXWXCWKNEU/ARTIK051_KRAC_18K (issue #193) -- same legacy board
|
|
generation as the artik051_krac_18k fixture, but this dump has a nonzero
|
|
cumulativePower. Locks in state_keys; the actual /100000 scale fix is
|
|
asserted separately in test_airconditioner_capabilities.py since golden
|
|
only compares key sets, not values."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_artik051_krac_energy")
|
|
golden = json.loads((GOLDEN / "airconditioner_artik051_krac_energy.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_artik051_krac_energy", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_refrigerator_definite_cooler():
|
|
"""RT42DG6630B1FZ (issue #186) -- a single-door "cooler only" fridge whose
|
|
/temperature/definite/cooler/vs/0 doesn't match either
|
|
TEMP_CURRENT_GENERIC's '/temperature/current/' or TEMP_SETPOINT's
|
|
'/temperature/desired/' href prefix, so temperature control was entirely
|
|
unbound. Resolved via the new DEFINITE_TEMPERATURE_COOLER capability
|
|
(a select over the device's own discrete supportedList, not a
|
|
continuous NumberDesc range -- 5 and 6 aren't valid setpoints on this
|
|
model). Binds cleanly with zero unbound hrefs."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_definite_cooler")
|
|
golden = json.loads((GOLDEN / "refrigerator_definite_cooler.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_definite_cooler", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range_ne6516a():
|
|
"""NE6516A-class range (issue #183) -- no /information/vs/0, resolved via
|
|
the same 'Bake'-in-supportedModes + /cooktopmonitoring/vs/0 signature as
|
|
the other no-info range fixtures. Its /mode/vs/0 options[] carries
|
|
EnergySaving_On and BurnerOnAlert_Off (previously unbound entirely) but
|
|
no fastpreheat_*/NaturalSteam_* tokens at all -- locks in that those two
|
|
switches now correctly stay unbound instead of binding as always-off,
|
|
does-nothing phantom controls."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("range_ne6516a")
|
|
golden = json.loads((GOLDEN / "range_ne6516a.json").read_text())
|
|
state_keys = _new_state_keys("range_ne6516a", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dehumidifier_tp1x_dhm01001():
|
|
"""TP1X_DA_AC_DHM_01001_0000 revision (issues #271/#231) additionally
|
|
reports /display/vs/0 (air_purifier.DISPLAY's exact shape, reused as-is)
|
|
and /watertank/lighting/vs/0 (new WATERTANK_LIGHTING capability), both
|
|
unbound on the original issue #88 dump."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dehumidifier_tp1x_dhm01001")
|
|
golden = json.loads((GOLDEN / "dehumidifier_tp1x_dhm01001.json").read_text())
|
|
state_keys = _new_state_keys("dehumidifier_tp1x_dhm01001", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_fac_time_23k():
|
|
"""TP1X_FAC_TIME_23K (issue #270) reports a UV-C LED, a ventilation-alarm
|
|
toggle, and a second (PM1-rated, no live usage field) dust filter, none
|
|
previously modeled. Also a 2-in-1 system on this dump (numofsubdevice=2,
|
|
a UUID-prefixed sibling materializes via the flat fallback) -- the three
|
|
new/ignored hrefs are fixed on their canonical form so the sibling picks
|
|
them up too, per the adding-device-support skill's 'never index or
|
|
prefix a registry href' rule."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("airconditioner_tp1x_fac_time_23k")
|
|
golden = json.loads((GOLDEN / "airconditioner_tp1x_fac_time_23k.json").read_text())
|
|
state_keys = _new_state_keys("airconditioner_tp1x_fac_time_23k", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dryer_tp1_21_drum_clean():
|
|
"""DA_WM_TP1_21_COMMON/DV9400B (issue #258) is the first dryer dump with
|
|
live DrumCleanProposal_/WashingTimes_ tokens (dryer's default fixture
|
|
only ever had 'DrumCleanLog_Empty', same as the parallel washer capability
|
|
started from) -- locks in the new drum_clean_cycles_remaining/
|
|
drum_clean_last_cleaned entities, including the dryer-specific
|
|
'|'-joined multi-timestamp DrumCleanLog_ shape."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dryer_tp1_21_drum_clean")
|
|
golden = json.loads((GOLDEN / "dryer_tp1_21_drum_clean.json").read_text())
|
|
state_keys = _new_state_keys("dryer_tp1_21_drum_clean", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_definite():
|
|
"""TP1X_REF_21K (issue #229) reports the discrete definite-setpoint
|
|
pattern (issue #186) on *both* compartments -- previously only the
|
|
cooler half was modeled; DEFINITE_TEMPERATURE_FREEZER covers the
|
|
freezer's identical-shape /temperature/definite/freezer/vs/0."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp1x_ref_21k_definite")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_definite.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_definite", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_autodoor():
|
|
"""TP1X_REF_21K, single-freezer-door Auto Door Open variant (issue
|
|
#328): /autodoor/single/vs/0 + /autodoor/timer/vs/0, plus
|
|
STATUS_LOCK's ado.voicecontrol sibling toggle."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp1x_ref_21k_autodoor")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_autodoor.json").read_text())
|
|
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_autodoor", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_kimchi():
|
|
"""Kimchi-refrigerator Auto Door Open variant (issue #328) -- resolves
|
|
via /oic/d's oic.d.krefrigerator, not modelNum (the board is the same
|
|
TP1X_REF_21K as the regular fridge, so only the OCF type tells them
|
|
apart before checking hrefs)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_tp1x_ref_21k_kimchi")
|
|
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_kimchi.json").read_text())
|
|
state_keys = _new_state_keys(
|
|
"refrigerator_tp1x_ref_21k_kimchi",
|
|
resources,
|
|
device_types=("oic.wk.d", "oic.d.krefrigerator"),
|
|
)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_refrigerator_winecellar():
|
|
"""Wine-cellar refrigerator variant (issue #328) -- resolves via /oic/d's
|
|
x.com.st.d.winecellar, same TP1X_REF_21K board as the other two. Adds
|
|
the deodorizing filter at its own href, the multi-compartment pantry
|
|
select, and the table-revision info resource."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("refrigerator_winecellar")
|
|
golden = json.loads((GOLDEN / "refrigerator_winecellar.json").read_text())
|
|
state_keys = _new_state_keys(
|
|
"refrigerator_winecellar",
|
|
resources,
|
|
device_types=("oic.wk.d", "x.com.st.d.winecellar"),
|
|
)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_range_tp1x_da_ks_range_0101x():
|
|
"""Dual-cavity range (issue #324): no /information/vs/0 at all, so
|
|
routing depends entirely on /oic/d's oic.d.range -- and the second
|
|
cavity is a genuine Pattern A indexed subdevice at /device/1."""
|
|
name = "range_tp1x_da_ks_range_0101x"
|
|
golden = json.loads((GOLDEN / f"{name}.json").read_text())
|
|
state_keys = _new_subdevice_aware_state_keys(name, device_types=("oic.wk.d", "oic.d.range"))
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_resources_from_batch_preferred_over_flat():
|
|
from tests.conftest import _resources_from_dump
|
|
|
|
dump = {
|
|
"device0": [
|
|
{"di": "device"}, # [0] device-level rep, skipped
|
|
{"href": "/foo", "rep": {"x": 1}},
|
|
],
|
|
"resources": {"/foo": {"x": 99}},
|
|
}
|
|
result = _resources_from_dump(dump)
|
|
assert result == {"/foo": {"x": 1}}
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_washer_ww5000c_cloud():
|
|
"""The issue #342 reporter's WW5000C (DA_WM_TP1_21_COMMON, Table_02),
|
|
captured while sitting on its Download course with a one-time Jeans
|
|
program loaded over a saved Sports default.
|
|
|
|
The cloud "Download" programs it advertises add no entity of their own
|
|
-- they ride in the existing cycle select -- so this golden is the guard
|
|
that discovering them never grows the entity set.
|
|
|
|
Its /wm/editcourse/vs/0 is empty, so its course list comes from the
|
|
supportedOptions fallback (issue #1)."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("washer_ww5000c_cloud")
|
|
golden = json.loads((GOLDEN / "washer_ww5000c_cloud.json").read_text())
|
|
state_keys = _new_state_keys("washer_ww5000c_cloud", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|
|
|
|
|
|
def test_registry_reproduces_golden_state_keys_for_dishwasher_dw5000c_cloud():
|
|
"""A DW5000C dishwasher (issues #113/#123) that advertises four
|
|
downloaded programs it has never loaded -- CloudExtraCourse_ with no
|
|
CloudCourse_/OneTimeCloudCourse_ payload alongside it (issue #342).
|
|
|
|
Proves the cloud-cycle machinery is not washer-only and, more usefully,
|
|
that a device can advertise programs whose payloads have never been
|
|
seen. It adds no entity of its own either way."""
|
|
from tests.conftest import _load_device
|
|
|
|
resources = _load_device("dishwasher_dw5000c_cloud")
|
|
golden = json.loads((GOLDEN / "dishwasher_dw5000c_cloud.json").read_text())
|
|
state_keys = _new_state_keys("dishwasher_dw5000c_cloud", resources)
|
|
assert set(state_keys) == set(golden["state_keys"]), (
|
|
f"state_keys mismatch:\n"
|
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
|
)
|