From 36240b44e6a376c3dae221bd5babf9c5df916fa9 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Wed, 29 Jul 2026 13:26:39 +0000 Subject: [PATCH] fix(oven): gate fast_preheat/natural_steam on their own tokens (#183) Both switches were shipped unconditionally (no exists_fn) as an unverified guess -- the module docstring already flagged them as "unproven." Every range/oven fixture in the corpus, including the new issue #183 dump, reports neither fastpreheat_* nor NaturalSteam_* in /mode/vs/0's options at all, so both were phantom controls: always read as off, and toggling them wrote a token the firmware never recognized in the first place. That matches the reporter's exact complaint ("doesn't appear to do anything"). Also bound two tokens confirmed present on this dump but never modeled at all: EnergySaving_On (the 120-hour energy-saving standby from the app) and BurnerOnAlert_Off (cooktop-on alert), following the same single-token options-merge pattern as the existing lamp/sound/fast_preheat switches. Child lock, the setpoint mismatch, and the missing cook-start control from this issue are not code bugs -- see the issue comment for what was verified and what still needs more information from the reporter. --- .../localthings/registry/capabilities/oven.py | 54 +++++ .../localthings/translations/en.json | 6 + .../localthings/translations/nl.json | 6 + tests/fixtures/golden/oven.json | 4 +- tests/fixtures/golden/range.json | 5 +- tests/fixtures/golden/range_ne63a6511.json | 6 +- tests/fixtures/golden/range_ne6516a.json | 27 +++ tests/fixtures/golden/range_ne8300d.json | 3 +- tests/fixtures/golden/range_no_info.json | 6 +- tests/fixtures/range_ne6516a_device.json | 215 ++++++++++++++++++ tests/test_golden_regression.py | 19 ++ tests/test_range_ne6516a_capabilities.py | 62 +++++ 12 files changed, 399 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/golden/range_ne6516a.json create mode 100644 tests/fixtures/range_ne6516a_device.json create mode 100644 tests/test_range_ne6516a_capabilities.py diff --git a/custom_components/localthings/registry/capabilities/oven.py b/custom_components/localthings/registry/capabilities/oven.py index 0a28cc9..8c20b7f 100644 --- a/custom_components/localthings/registry/capabilities/oven.py +++ b/custom_components/localthings/registry/capabilities/oven.py @@ -130,6 +130,21 @@ def _option_value(options, prefix): return None +def _has_option(prefix): + """exists_fn for an options-array switch: bind only when the device's own + options[] actually carries a `_` token. + + fast_preheat/natural_steam were shipped unconditionally (no exists_fn) as + an unverified guess (see module docstring) -- issue #183's dump (model + NE6516A) reports neither `fastpreheat_*` nor `NaturalSteam_*` in its + options[] at all, so both switches were phantom controls: always read as + off, and toggling them wrote a token the firmware never recognized in + the first place, hence "does not appear to do anything." + """ + return lambda rep, resources: _option_value( + rep.get('x.com.samsung.da.options'), prefix) is not None + + def _option_write(prefix, new_value): """A one-token x.com.samsung.da.options write, mirroring laundry.option_write. NOT independently confirmed on an oven -- issue @@ -239,6 +254,26 @@ def _naturalsteam_write(p, rep, href=None): } +def _energysaving_write(p, rep, href=None): + if p not in ('On', 'Off'): + return None + if not rep.get('x.com.samsung.da.options'): + return None + return ['mode', 'vs', '0'], { + 'x.com.samsung.da.options': _option_write('EnergySaving', p), + } + + +def _burneronalert_write(p, rep, href=None): + if p not in ('On', 'Off'): + return None + if not rep.get('x.com.samsung.da.options'): + return None + return ['mode', 'vs', '0'], { + 'x.com.samsung.da.options': _option_write('BurnerOnAlert', p), + } + + # --------------------------------------------------------------------------- # Capabilities # --------------------------------------------------------------------------- @@ -385,11 +420,30 @@ OVEN_MODE = Capability( write_fn=_sound_write), SwitchDesc(key='fast_preheat', field='x.com.samsung.da.options', icon='mdi:fire', + exists_fn=_has_option('fastpreheat'), value_fn=lambda opts: _option_value(opts, 'fastpreheat') == 'On', write_fn=_fastpreheat_write), SwitchDesc(key='natural_steam', field='x.com.samsung.da.options', icon='mdi:kettle-steam', + exists_fn=_has_option('NaturalSteam'), value_fn=lambda opts: _option_value(opts, 'NaturalSteam') == 'On', write_fn=_naturalsteam_write), + # 120-hour energy-saving standby (issue #183): confirmed present in + # this unit's options[] (EnergySaving_On) and directly requested -- + # unlike fast_preheat/natural_steam above, this token is real on this + # hardware, just previously unbound entirely. + SwitchDesc(key='energy_saving', field='x.com.samsung.da.options', + icon='mdi:leaf', entity_category='config', + exists_fn=_has_option('EnergySaving'), + value_fn=lambda opts: _option_value(opts, 'EnergySaving') == 'On', + write_fn=_energysaving_write), + # Cooktop-on alert (issue #183): also confirmed present + # (BurnerOnAlert_Off) though the reporter noted it mainly matters for + # the SmartThings app's own alerting, not local automation. + SwitchDesc(key='cooktop_on_alert', field='x.com.samsung.da.options', + icon='mdi:alert-circle-outline', entity_category='config', + exists_fn=_has_option('BurnerOnAlert'), + value_fn=lambda opts: _option_value(opts, 'BurnerOnAlert') == 'On', + write_fn=_burneronalert_write), ), ) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 811f2c9..4e5b414 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -1025,6 +1025,12 @@ "natural_steam": { "name": "Natural steam" }, + "energy_saving": { + "name": "Energy saving" + }, + "cooktop_on_alert": { + "name": "Cooktop-on alert" + }, "power_switch": { "name": "Power" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 10883ed..9e64254 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -1025,6 +1025,12 @@ "natural_steam": { "name": "Natuurlijke stoom" }, + "energy_saving": { + "name": "Energiebesparing" + }, + "cooktop_on_alert": { + "name": "Waarschuwing kookplaat aan" + }, "power_switch": { "name": "Voeding" }, diff --git a/tests/fixtures/golden/oven.json b/tests/fixtures/golden/oven.json index e2f168d..59a059a 100644 --- a/tests/fixtures/golden/oven.json +++ b/tests/fixtures/golden/oven.json @@ -7,12 +7,10 @@ "current_temp_c", "cycle_active", "door_open", - "fast_preheat", "finish_time", "firmware_update", "lamp", "machine_state", - "natural_steam", "operation_time_minutes", "oven_mode", "oven_setpoint", @@ -22,4 +20,4 @@ "remote_control", "sound" ] -} +} \ No newline at end of file diff --git a/tests/fixtures/golden/range.json b/tests/fixtures/golden/range.json index f386342..f7fcf75 100644 --- a/tests/fixtures/golden/range.json +++ b/tests/fixtures/golden/range.json @@ -27,12 +27,11 @@ "current_temp_c", "cycle_active", "door_open", - "fast_preheat", + "energy_saving", "finish_time", "firmware_update", "lamp", "machine_state", - "natural_steam", "operation_time_minutes", "oven_mode", "oven_setpoint", @@ -42,4 +41,4 @@ "remote_control", "sound" ] -} +} \ No newline at end of file diff --git a/tests/fixtures/golden/range_ne63a6511.json b/tests/fixtures/golden/range_ne63a6511.json index a7ef5a5..e43255a 100644 --- a/tests/fixtures/golden/range_ne63a6511.json +++ b/tests/fixtures/golden/range_ne63a6511.json @@ -4,16 +4,16 @@ "child_lock", "cloud_connected", "cook_time", + "cooktop_on_alert", "cooktop_running_state", "current_temp_c", "cycle_active", "door_open", - "fast_preheat", + "energy_saving", "finish_time", "firmware_update", "lamp", "machine_state", - "natural_steam", "operation_time_minutes", "oven_mode", "oven_setpoint", @@ -24,4 +24,4 @@ "sound", "warming_center_state" ] -} +} \ No newline at end of file diff --git a/tests/fixtures/golden/range_ne6516a.json b/tests/fixtures/golden/range_ne6516a.json new file mode 100644 index 0000000..65c82ac --- /dev/null +++ b/tests/fixtures/golden/range_ne6516a.json @@ -0,0 +1,27 @@ +{ + "state_keys": [ + "alarm_code", + "cooktop_on_alert", + "child_lock", + "cloud_connected", + "cook_time", + "cooktop_running_state", + "current_temp_c", + "cycle_active", + "door_open", + "energy_saving", + "finish_time", + "firmware_update", + "lamp", + "machine_state", + "operation_time_minutes", + "oven_mode", + "oven_setpoint", + "oven_state", + "power_switch", + "progress_percentage", + "remote_control", + "sound", + "warming_center_state" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/range_ne8300d.json b/tests/fixtures/golden/range_ne8300d.json index 49f4d08..b77d20e 100644 --- a/tests/fixtures/golden/range_ne8300d.json +++ b/tests/fixtures/golden/range_ne8300d.json @@ -8,12 +8,11 @@ "current_temp_c", "cycle_active", "door_open", - "fast_preheat", + "energy_saving", "finish_time", "firmware_update", "lamp", "machine_state", - "natural_steam", "operation_time_minutes", "oven_mode", "oven_setpoint", diff --git a/tests/fixtures/golden/range_no_info.json b/tests/fixtures/golden/range_no_info.json index a7ef5a5..e43255a 100644 --- a/tests/fixtures/golden/range_no_info.json +++ b/tests/fixtures/golden/range_no_info.json @@ -4,16 +4,16 @@ "child_lock", "cloud_connected", "cook_time", + "cooktop_on_alert", "cooktop_running_state", "current_temp_c", "cycle_active", "door_open", - "fast_preheat", + "energy_saving", "finish_time", "firmware_update", "lamp", "machine_state", - "natural_steam", "operation_time_minutes", "oven_mode", "oven_setpoint", @@ -24,4 +24,4 @@ "sound", "warming_center_state" ] -} +} \ No newline at end of file diff --git a/tests/fixtures/range_ne6516a_device.json b/tests/fixtures/range_ne6516a_device.json new file mode 100644 index 0000000..474ad22 --- /dev/null +++ b/tests/fixtures/range_ne6516a_device.json @@ -0,0 +1,215 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false" + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "On" + } + }, + { + "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-07-28T23:15:35" + } + ] + } + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Run" + } + }, + { + "href": "/configuration/vs/0", + "rep": {} + }, + { + "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": "/remotectrl/vs/0", + "rep": { + "x.com.samsung.da.remoteControlEnabled": "true", + "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": "175", + "x.com.samsung.da.increment": "1", + "x.com.samsung.da.unit": "Fahrenheit" + } + ], + "rt": [ + "x.com.samsung.da.temperatures" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "Bake", + "Broil", + "ConvectionBake", + "ConvectionRoast", + "KeepWarm", + "BreadProof", + "AirFryer", + "Dehydrate", + "SelfClean", + "SteamClean" + ], + "x.com.samsung.da.modes": [ + "NoOperation" + ], + "x.com.samsung.da.options": [ + "DeviceType_NE6516A-/AA0", + "SettingPossible_0", + "meatprobe_disconnected", + "UpperLamp_Off", + "Sound_On", + "AdjustingTemp_23", + "Sabbath_Off", + "EnergySaving_On", + "BurnerOnAlert_Off" + ], + "x.com.samsung.da.defaultMode": "ConvectionBake", + "x.com.samsung.da.modeSpec": "[{\"mode\":\"Bake\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"175\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"350\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"Broil\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"61441\",\"tempMaxC\":\"61442\",\"tempDefaultC\":\"61441\",\"tempListLengthC\":\"2\",\"tempListDataC\":[\"61441\",\"61442\"],\"tempMinF\":\"61441\",\"tempMaxF\":\"61442\",\"tempDefaultF\":\"61441\",\"tempListLengthF\":\"2\",\"tempListDataF\":[\"61441\",\"61442\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"ConvectionBake\",\"version\":\"0100\",\"default\":\"Default\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"ConvectionRoast\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"KeepWarm\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"80\",\"tempDefaultC\":\"80\",\"tempListLengthC\":\"1\",\"tempListDataC\":[\"80\"],\"tempMinF\":\"175\",\"tempMaxF\":\"175\",\"tempDefaultF\":\"175\",\"tempListLengthF\":\"1\",\"tempListDataF\":[\"175\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"BreadProof\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"35\",\"tempMaxC\":\"35\",\"tempDefaultC\":\"35\",\"tempListLengthC\":\"1\",\"tempListDataC\":[\"35\"],\"tempMinF\":\"95\",\"tempMaxF\":\"95\",\"tempDefaultF\":\"95\",\"tempListLengthF\":\"1\",\"tempListDataF\":[\"95\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"AirFryer\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"175\",\"tempMaxC\":\"260\",\"tempDefaultC\":\"220\",\"tempListLengthC\":\"0\",\"tempMinF\":\"350\",\"tempMaxF\":\"500\",\"tempDefaultF\":\"425\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"Dehydrate\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"40\",\"tempMaxC\":\"105\",\"tempDefaultC\":\"65\",\"tempListLengthC\":\"0\",\"tempMinF\":\"100\",\"tempMaxF\":\"225\",\"tempDefaultF\":\"150\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"SelfClean\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"NotSupported\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"SteamClean\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"NotSupported\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"}]", + "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": "/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": "Lock" + } + ], + "rt": [ + "x.com.samsung.da.doors" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "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": "/cooktopmonitoring/vs/0", + "rep": { + "x.com.samsung.da.warmingCenterState": "Off", + "x.com.samsung.da.cooktopMonitoring": "0", + "x.com.samsung.da.cooktopRunningState": "Ready", + "supportedHoodLampStateList": [ + "Run", + "Ready" + ], + "rt": [ + "x.com.samsung.da.cooktopMonitoring" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 29303ea..4024099 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -917,6 +917,25 @@ def test_registry_reproduces_golden_state_keys_for_refrigerator_definite_cooler( ) +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_resources_from_batch_preferred_over_flat(): from tests.conftest import _resources_from_dump dump = { diff --git a/tests/test_range_ne6516a_capabilities.py b/tests/test_range_ne6516a_capabilities.py new file mode 100644 index 0000000..ccf905c --- /dev/null +++ b/tests/test_range_ne6516a_capabilities.py @@ -0,0 +1,62 @@ +"""Tests for the NE6516A-class range (issue #183). + +Same no-/information/vs/0, no-burner-status shape as issues #74/#138. Its +/mode/vs/0 options[] carries EnergySaving_On and BurnerOnAlert_Off -- both +previously unbound entirely -- but no fastpreheat_*/NaturalSteam_* tokens at +all, which is what exposed the pre-existing fast_preheat/natural_steam +phantom-switch bug (both were shipped with no exists_fn, so they bound +unconditionally, always read off, and any write went to a token this +firmware never recognized). +""" +from custom_components.localthings.registry.adapter import flatten +from custom_components.localthings.registry.by_type import for_device_by_resources +from custom_components.localthings.registry.discovery import discover + +from tests.conftest import _load_device + + +def _range(): + resources = _load_device('range_ne6516a') + reg = for_device_by_resources(resources) + return reg, resources + + +def _state(): + reg, resources = _range() + bound = discover(resources, reg.capabilities, reg.pattern_capabilities) + return flatten(bound, resources) + + +def test_resolves_to_range_registry(): + reg, _ = _range() + assert reg is not None and reg.name == 'range' + + +def test_no_unbound_hrefs(): + reg, resources = _range() + unbound = [] + discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append) + assert unbound == [] + + +def test_fast_preheat_and_natural_steam_absent_without_their_tokens(): + """Neither token is in this dump's options[] -- both switches must stay + unbound rather than silently reading as an always-off phantom control.""" + state = _state() + assert 'fast_preheat' not in state + assert 'natural_steam' not in state + + +def test_energy_saving_and_cooktop_alert_present(): + state = _state() + assert state['energy_saving'] is True + assert state['cooktop_on_alert'] is False + + +def test_child_lock_already_reads_correctly(): + """issue #183 also reported child lock as unreadable -- but this dump's + /kidslock/vs/0 ('Run') and /doors/vs/0's door lock field ('Lock') already + agree the door is locked, and the existing common.KIDS_LOCK_VS_FALLBACK + already reads that correctly with no code change needed.""" + state = _state() + assert state['child_lock'] is True