Read oven cook modes live from the device instead of a hardcoded list
Follow-up to the issue #138 fix: rather than hand-adding ConvectionRoast/KeepWarm/BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean to oven._OVEN_MODES, read them from the device's own /mode/vs/0 supportedModes when it reports one, falling back to the static NV7000BS-era guess only when it doesn't. Matches the adding-device-support skill's preference for device-reported option lists over hardcoded ones, and the SelectDesc's write validation now checks the same live list it displays instead of a separate static tuple.
This commit is contained in:
3 files changed
+65
-24
No files matched your search
@@ -52,14 +52,13 @@ SETPOINT_STEP_F = 5
|
||||
# from Samsung documentation and firmware observations. The firmware will
|
||||
# reject unknown modes; missing entries here are a coverage gap, not a bug.
|
||||
#
|
||||
# This is a fallback only, used when a device's own /mode/vs/0 doesn't report
|
||||
# x.com.samsung.da.supportedModes at all -- see _oven_mode_options/
|
||||
# _oven_mode_write below. issue #138's range dump (NE63A6511SS/AA) reports
|
||||
# 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.
|
||||
# in its own supportedModes; those are read live rather than added here, per
|
||||
# the adding-device-support skill's preference for device-reported option
|
||||
# lists over hardcoded ones.
|
||||
_OVEN_MODES = (
|
||||
'NoOperation',
|
||||
'Bake',
|
||||
@@ -67,17 +66,10 @@ _OVEN_MODES = (
|
||||
'Convection',
|
||||
'ConvectionBake',
|
||||
'ConvectionBroil',
|
||||
'ConvectionRoast',
|
||||
'FrozenPizzaPlus',
|
||||
'SlowCook',
|
||||
'PlateWarm',
|
||||
'AirFry',
|
||||
'AirFryer',
|
||||
'KeepWarm',
|
||||
'BreadProof',
|
||||
'Dehydrate',
|
||||
'SelfClean',
|
||||
'SteamClean',
|
||||
)
|
||||
|
||||
_SAMSUNG_STATE_TO_OCF = {
|
||||
@@ -189,8 +181,20 @@ def _cook_time_write(p, rep, href=None):
|
||||
}
|
||||
|
||||
|
||||
def _oven_mode_options(resources):
|
||||
"""Live mode list from the device's own /mode/vs/0 supportedModes when
|
||||
it reports one; the NV7000BS-era _OVEN_MODES guess otherwise. Mirrors
|
||||
laundry.py's options_field pattern (buzzer_sound/finish_sound), but
|
||||
needs the callable form rather than options_field because a static
|
||||
fallback has to kick in when the device's own field is absent."""
|
||||
rep = resources.get('/mode/vs/0') or {}
|
||||
live = rep.get('x.com.samsung.da.supportedModes')
|
||||
return list(live) if live else list(_OVEN_MODES)
|
||||
|
||||
|
||||
def _oven_mode_write(p, rep, href=None):
|
||||
if p not in _OVEN_MODES:
|
||||
valid = rep.get('x.com.samsung.da.supportedModes') or _OVEN_MODES
|
||||
if p not in valid:
|
||||
return None
|
||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
|
||||
|
||||
@@ -367,7 +371,7 @@ OVEN_MODE = Capability(
|
||||
# SelectDesc first — test_oven_mode_options_nonempty uses entities[0]
|
||||
SelectDesc(key='oven_mode', field='x.com.samsung.da.modes',
|
||||
icon='mdi:tune',
|
||||
options=_OVEN_MODES,
|
||||
options=_oven_mode_options,
|
||||
value_fn=lambda v: v[0] if v else None,
|
||||
write_fn=_oven_mode_write),
|
||||
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
|
||||
|
||||
@@ -120,12 +120,29 @@ def test_oven_setpoint_native_bounds_track_live_unit():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_oven_mode_options_nonempty():
|
||||
assert len(oven.OVEN_MODE.entities[0].options) > 0
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
assert callable(desc.options)
|
||||
assert len(desc.options({})) > 0
|
||||
|
||||
|
||||
def test_oven_mode_options_falls_back_when_no_live_supported_modes():
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
assert desc.options({}) == list(oven._OVEN_MODES)
|
||||
|
||||
|
||||
def test_oven_mode_options_reads_live_supported_modes():
|
||||
"""issue #138: the device's own supportedModes list is used verbatim
|
||||
when present, instead of the static _OVEN_MODES guess."""
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
resources = {'/mode/vs/0': {
|
||||
'x.com.samsung.da.supportedModes': ['Bake', 'AirFryer', 'SelfClean'],
|
||||
}}
|
||||
assert desc.options(resources) == ['Bake', 'AirFryer', 'SelfClean']
|
||||
|
||||
|
||||
def test_oven_mode_write_round_trips():
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
valid_mode = desc.options[1] # e.g. 'Bake'
|
||||
valid_mode = desc.options({})[1] # e.g. 'Bake'
|
||||
path, body = desc.write_fn(valid_mode, {})
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == [valid_mode]
|
||||
@@ -136,6 +153,19 @@ def test_oven_mode_rejects_unknown():
|
||||
assert desc.write_fn('SpaghettiMode', {}) is None
|
||||
|
||||
|
||||
def test_oven_mode_write_validates_against_live_supported_modes():
|
||||
"""A device reporting its own supportedModes is validated against that
|
||||
list, not the static fallback -- issue #138's AirFryer/SelfClean/etc.
|
||||
are accepted, and a mode outside the device's own list is rejected even
|
||||
if some other model's static guess would have allowed it."""
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
rep = {'x.com.samsung.da.supportedModes': ['Bake', 'AirFryer', 'SelfClean']}
|
||||
path, body = desc.write_fn('AirFryer', rep)
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == ['AirFryer']
|
||||
assert desc.write_fn('FrozenPizzaPlus', rep) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OVEN_MODE options-array writes (lamp, sound, fast_preheat, natural_steam).
|
||||
# Confirmed on real hardware (issue #54): a write only needs to carry the
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""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."""
|
||||
hrefs, plus oven.OVEN_MODE reading this dump's ConvectionRoast/KeepWarm/
|
||||
BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean modes live from its own
|
||||
/mode/vs/0 supportedModes rather than needing them hardcoded."""
|
||||
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
|
||||
@@ -48,14 +49,20 @@ def test_expected_entities_present():
|
||||
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."""
|
||||
none of which were in the static oven._OVEN_MODES fallback, so a
|
||||
hardcoded write-validation list would've silently rejected them even
|
||||
though the device itself advertises them. write_fn reads supportedModes
|
||||
off the live rep (the same one it's called with in production -- see
|
||||
coordinator.py's async_send_command), so this passes without needing
|
||||
those modes added to any Python list."""
|
||||
_, resources = _range()
|
||||
live_rep = resources['/mode/vs/0']
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
rep = {'x.com.samsung.da.modes': ['NoOperation']}
|
||||
assert desc.options(resources) == live_rep['x.com.samsung.da.supportedModes']
|
||||
for mode in (
|
||||
'ConvectionRoast', 'KeepWarm', 'BreadProof', 'AirFryer',
|
||||
'Dehydrate', 'SelfClean', 'SteamClean',
|
||||
):
|
||||
path, body = desc.write_fn(mode, rep)
|
||||
path, body = desc.write_fn(mode, live_rep)
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == [mode]
|
||||
Reference in new issue
Block a user