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