diff --git a/custom_components/localthings/registry/capabilities/oven.py b/custom_components/localthings/registry/capabilities/oven.py index 4b47a38..d866927 100644 --- a/custom_components/localthings/registry/capabilities/oven.py +++ b/custom_components/localthings/registry/capabilities/oven.py @@ -51,6 +51,15 @@ SETPOINT_STEP_F = 5 # Mode options seen on NV7000BS-class. No dump exists so this list is inferred # from Samsung documentation and firmware observations. The firmware will # reject unknown modes; missing entries here are a coverage gap, not a bug. +# +# ConvectionRoast/KeepWarm/BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean +# are confirmed against issue #138's range dump (NE63A6511SS/AA) -- its +# /mode/vs/0 supportedModes lists all of them, and the range family shares +# this select via range.py reusing oven.OVEN_MODE wholesale. 'AirFryer' is +# kept alongside 'AirFry' rather than replacing it -- the NV7000BS-class +# dump this list was originally inferred from spells it without the +# trailing 'er', and neither model's actual token is independently +# confirmed, so both are treated as real device-reported spellings. _OVEN_MODES = ( 'NoOperation', 'Bake', @@ -58,10 +67,17 @@ _OVEN_MODES = ( 'Convection', 'ConvectionBake', 'ConvectionBroil', + 'ConvectionRoast', 'FrozenPizzaPlus', 'SlowCook', 'PlateWarm', 'AirFry', + 'AirFryer', + 'KeepWarm', + 'BreadProof', + 'Dehydrate', + 'SelfClean', + 'SteamClean', ) _SAMSUNG_STATE_TO_OCF = { diff --git a/tests/fixtures/golden/range_ne63a6511.json b/tests/fixtures/golden/range_ne63a6511.json new file mode 100644 index 0000000..a7ef5a5 --- /dev/null +++ b/tests/fixtures/golden/range_ne63a6511.json @@ -0,0 +1,27 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "cloud_connected", + "cook_time", + "cooktop_running_state", + "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", + "oven_state", + "power_switch", + "progress_percentage", + "remote_control", + "sound", + "warming_center_state" + ] +} diff --git a/tests/fixtures/range_ne63a6511_device.json b/tests/fixtures/range_ne63a6511_device.json new file mode 100644 index 0000000..973891b --- /dev/null +++ b/tests/fixtures/range_ne63a6511_device.json @@ -0,0 +1,200 @@ +{ + "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": "2024-01-01T00:00:00" + } + ] + } + }, + { + "href": "/configuration/vs/0", + "rep": {} + }, + { + "href": "/connected/vs/0", + "rep": { + "x.com.samsung.da.connected": "On" + } + }, + { + "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" + ] + } + }, + { + "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": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "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", + "UpperLamp_Off", + "Sound_Off", + "AdjustingTemp_0", + "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\"},{\"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\"},{\"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\"}]", + "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" + } + }, + { + "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" + ] + } + } + ] +} diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 917e12c..7846277 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -544,6 +544,23 @@ def test_registry_reproduces_golden_state_keys_for_range_ne8300d(): ) +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 diff --git a/tests/test_range_ne63a6511_capabilities.py b/tests/test_range_ne63a6511_capabilities.py new file mode 100644 index 0000000..7b99006 --- /dev/null +++ b/tests/test_range_ne63a6511_capabilities.py @@ -0,0 +1,61 @@ +"""Tests for the NE63A6511SS/AA range (issue #138) -- same no-/information/vs/0, +no-burner-status shape as issue #74's NE63B8411SS, confirming the existing +range_no_info detection path still resolves this model with zero unbound +hrefs, plus the ConvectionRoast/KeepWarm/BreadProof/AirFryer/Dehydrate/ +SelfClean/SteamClean modes this dump's supportedModes adds to oven.OVEN_MODE.""" +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.capabilities import oven +from custom_components.localthings.registry.discovery import discover + +from tests.conftest import _load_device + + +def _range(): + resources = _load_device('range_ne63a6511') + 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_expected_entities_present(): + state = _state() + for key in ( + 'power_switch', 'oven_setpoint', 'current_temp_c', 'oven_mode', + 'machine_state', 'door_open', 'cloud_connected', 'child_lock', + 'cooktop_running_state', 'warming_center_state', + ): + assert key in state, key + + +def test_oven_mode_accepts_this_devices_supported_modes(): + """This dump's /mode/vs/0 supportedModes reports ConvectionRoast, + KeepWarm, BreadProof, AirFryer, Dehydrate, SelfClean, and SteamClean -- + none of which were in oven._OVEN_MODES before issue #138, so writes to + them were silently rejected even though the device advertises them.""" + desc = oven.OVEN_MODE.entities[0] + rep = {'x.com.samsung.da.modes': ['NoOperation']} + for mode in ( + 'ConvectionRoast', 'KeepWarm', 'BreadProof', 'AirFryer', + 'Dehydrate', 'SelfClean', 'SteamClean', + ): + path, body = desc.write_fn(mode, rep) + assert path == ['mode', 'vs', '0'] + assert body['x.com.samsung.da.modes'] == [mode]