Close coverage gap and fix phantom lamp switch for TP2X_DA-KS-WALLOVEN (issue #300)

/diagnosis/vs/0 was the dump's only unbound href, now covered via
dishwasher.DIAGNOSIS (same shape already reused by airconditioner.py).

This steam-oven-class board's /mode/vs/0 options[] carries no UpperLamp_
token at all, unlike the NV7000BS-class board LAMP was proven against --
LAMP had no exists_fn, so it registered anyway, always read Off, and any
write to it was a no-op the device had no reason to honor. Gives it the
same options-token exists_fn gate issue #183 already added to
fast_preheat/natural_steam/energy_saving/cooktop_on_alert.
This commit is contained in:
Marc Billow
2026-08-07 14:22:32 +00:00
parent df5b704f3e
commit 42fd9c2aa4
6 changed files with 372 additions and 1 deletions
@@ -1,6 +1,6 @@
"""Oven device registry."""
from ..capabilities import common, ignored, oven
from ..capabilities import common, dishwasher, ignored, oven
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
@@ -18,6 +18,9 @@ REGISTRY = DeviceRegistry(
oven.OVEN_CONNECTED,
oven.OVEN_SPEC,
oven.OVEN_RECIPE_COOK,
# issue #300: /diagnosis/vs/0 is the same diagnosisStart shape
# dishwasher.py and airconditioner.py already reuse.
dishwasher.DIAGNOSIS,
]
),
)
@@ -401,10 +401,17 @@ OVEN_MODE = Capability(
value_fn=lambda v: v[0] if v else None,
write_fn=_oven_mode_write,
),
# No exists_fn on the NV7000BS-class board this was proven against
# (UpperLamp_ is always in its options[]) -- but issue #300's
# steam-oven-class WALLOVEN board's options[] has no UpperLamp_
# token at all, so this was a phantom, always-off, write-does-
# nothing switch there. Same fastpreheat/NaturalSteam-class gap
# issue #183 already fixed on the other switches below.
SwitchDesc(
key="lamp",
field="x.com.samsung.da.options",
icon="mdi:track-light",
exists_fn=_has_option("UpperLamp"),
value_fn=lambda opts: _option_value(opts, "UpperLamp") == "On",
write_fn=_option_switch_write("UpperLamp"),
),
+24
View File
@@ -0,0 +1,24 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"cloud_connected",
"cook_time",
"current_temp_c",
"cycle_active",
"diagnosis_status",
"door_open",
"energy_saving",
"finish_time",
"firmware_update",
"machine_state",
"operation_time_minutes",
"oven_mode",
"oven_setpoint",
"oven_state",
"power_switch",
"progress_percentage",
"remote_control",
"sound"
]
}
+260
View File
@@ -0,0 +1,260 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.s"
],
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "OV_E_OFF",
"x.com.samsung.da.triggeredTime": "2026-08-05T14:20:14"
}
]
}
},
{
"href": "/connected/vs/0",
"rep": {
"x.com.samsung.da.connected": "On",
"rt": [
"x.com.samsung.da.connected"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close",
"x.com.samsung.da.lock": "Unlock"
}
],
"rt": [
"x.com.samsung.da.doors"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP2X_DA-KS-WALLOVEN-000002|40441841|5002011E021011150100000000000000",
"x.com.samsung.da.description": "NW9000KD/AA1",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "240205",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "DE92-04418A_20062500, DE92-04011A_17041300",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "610",
"x.com.samsung.da.diagMinVersion": "1.0"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"ConvectionBake",
"ConvectionRoast",
"Bake",
"Broil",
"SteamBake",
"SteamRoast",
"Easycook3",
"Descale",
"PyroFree",
"Drain",
"SelfClean",
"NoOperation"
],
"x.com.samsung.da.modes": [
"NoOperation"
],
"x.com.samsung.da.options": [
"DeviceType_NW9000KD/AA1",
"keepWarmReservation_Off",
"meatprobe_disconnected",
"NoPreheat_Off",
"waterInlet_Closed",
"steamAddLevel_0",
"descaleAlarm_Normal",
"descaleNewWaterAlarm_Off",
"descaleEmptyWaterAlarm_Off",
"steamGeneratorLevel_Empty",
"steamUsingTime_66",
"drainRequired_00",
"steamState_Standby",
"descaleState_Standby",
"waterTankInSwitch_On",
"pyroFreeState_Standby",
"waterTankOutSwitch_Off",
"Sound_On",
"AdjustingTemp_0",
"Sabbath_Off",
"EnergySaving_On"
],
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.operationTime": "00:00:00",
"x.com.samsung.da.remainingTime": "00:00:00",
"x.com.samsung.da.progressPercentage": "1",
"rt": [
"x.com.samsung.da.operation"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/oven/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"rt": [
"x.com.samsung.da.oven"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On",
"rt": [
"x.com.samsung.da.operation"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false",
"rt": [
"x.com.samsung.da.configuration"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Temperature",
"x.com.samsung.da.desired": "0",
"x.com.samsung.da.current": "0",
"x.com.samsung.da.increment": "0",
"x.com.samsung.da.unit": "Fahrenheit"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Temperature",
"x.com.samsung.da.desired": "0",
"x.com.samsung.da.current": "0",
"x.com.samsung.da.increment": "0",
"x.com.samsung.da.unit": "Fahrenheit"
}
],
"rt": [
"x.com.samsung.da.temperatures"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
}
]
}
+23
View File
@@ -152,6 +152,29 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_ailp_fac():
)
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;
@@ -0,0 +1,54 @@
"""Tests for the TP2X_DA-KS-WALLOVEN-000002 steam oven (issue #300).
The reporter's "not fully supported" notification was /diagnosis/vs/0,
now covered via dishwasher.DIAGNOSIS. Their "doesn't respond to commands"
complaint is broader: oven.py's own module docstring already documents
setpoint/cook-time/mode writes as unproven on this local-OCF firmware, and
this board's options[] carries no UpperLamp_ token at all -- confirming
`lamp` was previously a phantom, write-does-nothing switch here (same gap
issue #183 fixed for fast_preheat/natural_steam on a different board).
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
_DEVICE_TYPES = ("oic.wk.d", "oic.d.oven")
def _bound():
resources = _load_device("oven_tp2x_ks_walloven")
reg = resolve(resources, device_types=_DEVICE_TYPES)
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def test_oic_device_type_resolves_to_oven_registry():
reg = for_device_by_oic_type(_DEVICE_TYPES)
assert reg is not None and reg.name == "oven"
def test_no_unbound_hrefs():
resources = _load_device("oven_tp2x_ks_walloven")
reg = resolve(resources, device_types=_DEVICE_TYPES)
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_lamp_not_bound_without_an_upperlamp_token():
"""This board's options[] has no UpperLamp_ entry -- confirms the new
exists_fn keeps `lamp` from registering a switch that would always
read Off and never actually do anything on write."""
bound, resources = _bound()
state = flatten(bound, resources)
assert "lamp" not in state
def test_sound_and_energy_saving_bound_from_real_options_tokens():
"""Unlike lamp, this board's options[] does carry Sound_On and
EnergySaving_On -- these switches should reflect real state."""
bound, resources = _bound()
state = flatten(bound, resources)
assert state["sound"] is True
assert state["energy_saving"] is True