diff --git a/custom_components/localthings/registry/by_type/oven.py b/custom_components/localthings/registry/by_type/oven.py index 107ca9c..ea1973f 100644 --- a/custom_components/localthings/registry/by_type/oven.py +++ b/custom_components/localthings/registry/by_type/oven.py @@ -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, ] ), ) diff --git a/custom_components/localthings/registry/capabilities/oven.py b/custom_components/localthings/registry/capabilities/oven.py index 50d3847..d340483 100644 --- a/custom_components/localthings/registry/capabilities/oven.py +++ b/custom_components/localthings/registry/capabilities/oven.py @@ -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"), ), diff --git a/tests/fixtures/golden/oven_tp2x_ks_walloven.json b/tests/fixtures/golden/oven_tp2x_ks_walloven.json new file mode 100644 index 0000000..5c4cb1b --- /dev/null +++ b/tests/fixtures/golden/oven_tp2x_ks_walloven.json @@ -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" + ] +} diff --git a/tests/fixtures/oven_tp2x_ks_walloven_device.json b/tests/fixtures/oven_tp2x_ks_walloven_device.json new file mode 100644 index 0000000..f5a183c --- /dev/null +++ b/tests/fixtures/oven_tp2x_ks_walloven_device.json @@ -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" + ] + } + } + ] +} diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 19b0983..568169f 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -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; diff --git a/tests/test_oven_tp2x_ks_walloven_capabilities.py b/tests/test_oven_tp2x_ks_walloven_capabilities.py new file mode 100644 index 0000000..016e363 --- /dev/null +++ b/tests/test_oven_tp2x_ks_walloven_capabilities.py @@ -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