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.
This commit is contained in:
@@ -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 `<prefix>_<value>` 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),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1025,6 +1025,12 @@
|
||||
"natural_steam": {
|
||||
"name": "Natuurlijke stoom"
|
||||
},
|
||||
"energy_saving": {
|
||||
"name": "Energiebesparing"
|
||||
},
|
||||
"cooktop_on_alert": {
|
||||
"name": "Waarschuwing kookplaat aan"
|
||||
},
|
||||
"power_switch": {
|
||||
"name": "Voeding"
|
||||
},
|
||||
|
||||
Vendored
+1
-3
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+2
-3
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+27
@@ -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"
|
||||
]
|
||||
}
|
||||
+1
-2
@@ -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",
|
||||
|
||||
+3
-3
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+215
File diff suppressed because one or more lines are too long
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user