Both FilterRemind_*/RemindBeep_* option-array tokens are already present -- and both On and Off already confirmed -- on the existing ME7500D fixtures (issue #152), so this is a straight sibling of the Sound/Lamp switches rather than new discovery work. Gated with exists_fn like Lamp since the MW7300B combi dump has neither token. Doesn't address the rest of issue #181 (power-level slider, non-reported cooking modes, 3-level light, child lock, send-to- microwave) -- those need write-contract confirmation this dump doesn't carry.
286 lines
12 KiB
Python
286 lines
12 KiB
Python
"""Unit tests for the microwave-family capabilities (issue #121/#66 split
|
|
into their own device type instead of being folded into oven.py)."""
|
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
|
from custom_components.localthings.registry.capabilities import microwave
|
|
from custom_components.localthings.registry.discovery import discover
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Device-type detection + full-dump coverage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_microwave_fixture_resolves_and_has_no_unbound_hrefs():
|
|
from tests.conftest import _load_device
|
|
resources = _load_device('microwave_mw7300b')
|
|
info = resources['/information/vs/0']
|
|
reg = for_device_by_model(
|
|
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
|
assert reg is not None
|
|
assert reg.name == 'microwave'
|
|
|
|
unbound = []
|
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
|
assert unbound == []
|
|
|
|
|
|
def test_microwave_hood_fan_fixture_resolves_and_has_no_unbound_hrefs():
|
|
"""Issues #137/#142: `/hood/fanspeed/vs/0` (the combi unit's built-in
|
|
vent fan) was previously unbound on this family."""
|
|
from tests.conftest import _load_device
|
|
resources = _load_device('microwave_me7500d')
|
|
info = resources['/information/vs/0']
|
|
reg = for_device_by_model(
|
|
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
|
assert reg is not None
|
|
assert reg.name == 'microwave'
|
|
|
|
unbound = []
|
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
|
assert unbound == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MICROWAVE_SETPOINT — NumberDesc with RMW write semantics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_microwave_setpoint_write_is_read_modify_write():
|
|
desc = microwave.MICROWAVE_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.desired': '0'}]}
|
|
path, body = desc.write_fn(180, rep)
|
|
assert path == ['temperatures', 'vs', '0']
|
|
assert body['x.com.samsung.da.items'][0]['x.com.samsung.da.desired'] == '180'
|
|
|
|
|
|
def test_microwave_setpoint_rmw_preserves_other_item_fields():
|
|
desc = microwave.MICROWAVE_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{
|
|
'x.com.samsung.da.current': '150',
|
|
'x.com.samsung.da.desired': '150',
|
|
}]}
|
|
path, body = desc.write_fn(180, rep)
|
|
item = body['x.com.samsung.da.items'][0]
|
|
assert item['x.com.samsung.da.desired'] == '180'
|
|
assert item['x.com.samsung.da.current'] == '150'
|
|
|
|
|
|
def test_microwave_setpoint_clamps_to_step():
|
|
desc = microwave.MICROWAVE_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.desired': '0'}]}
|
|
_, body = desc.write_fn(182, rep) # nearest 5 = 180
|
|
assert body['x.com.samsung.da.items'][0]['x.com.samsung.da.desired'] == '180'
|
|
|
|
|
|
def test_microwave_setpoint_rejects_out_of_range():
|
|
desc = microwave.MICROWAVE_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.desired': '100'}]}
|
|
assert desc.write_fn(20, rep) is None # below min (40)
|
|
assert desc.write_fn(210, rep) is None # above max (200)
|
|
|
|
|
|
def test_microwave_setpoint_rejects_missing_items():
|
|
desc = microwave.MICROWAVE_SETPOINT.entities[0]
|
|
assert desc.write_fn(180, {}) is None
|
|
|
|
|
|
def test_microwave_setpoint_exists_only_for_celsius():
|
|
"""No Fahrenheit dump exists for this family (unlike oven.py's, verified
|
|
against issue #44) -- the writable setpoint stays hidden rather than
|
|
showing unverified bounds under the wrong unit."""
|
|
desc = microwave.MICROWAVE_SETPOINT.entities[0]
|
|
celsius_rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.unit': 'Celsius'}]}
|
|
fahrenheit_rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.unit': 'Fahrenheit'}]}
|
|
assert desc.exists_fn(celsius_rep, {}) is True
|
|
assert desc.exists_fn(fahrenheit_rep, {}) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MICROWAVE_CAVITY — power_level sensor
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_power_level_parses_watt_suffix():
|
|
"""Issue #121's combi dump reports e.g. '0W'."""
|
|
desc = next(e for e in microwave.MICROWAVE_CAVITY.entities if e.key == 'power_level')
|
|
assert desc.value_fn('900W') == 900
|
|
|
|
|
|
def test_power_level_parses_bare_number():
|
|
"""Issue #137's plain microwave reports the bare number, no 'W' suffix."""
|
|
desc = next(e for e in microwave.MICROWAVE_CAVITY.entities if e.key == 'power_level')
|
|
assert desc.value_fn('0') == 0
|
|
|
|
|
|
def test_power_level_handles_missing_value():
|
|
desc = next(e for e in microwave.MICROWAVE_CAVITY.entities if e.key == 'power_level')
|
|
assert desc.value_fn(None) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MICROWAVE_MODE — SelectDesc with non-empty, family-specific options
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_microwave_mode_options_nonempty():
|
|
desc = microwave.MICROWAVE_MODE.entities[0]
|
|
assert callable(desc.options)
|
|
options = desc.options({})
|
|
assert len(options) > 0
|
|
assert 'MicroWave' in options
|
|
assert 'AirFryer' in options # distinct spelling from oven.py's 'AirFry'
|
|
|
|
|
|
def test_microwave_mode_options_reads_live_supported_modes():
|
|
"""issue #152's ME7500D reports only 4 of the 11 union-of-all-dumps
|
|
_MICROWAVE_MODES -- the live supportedModes list is used verbatim when
|
|
present, same live-first pattern as oven._oven_mode_options, instead of
|
|
offering users modes their own unit doesn't have."""
|
|
desc = microwave.MICROWAVE_MODE.entities[0]
|
|
resources = {'/mode/vs/0': {
|
|
'x.com.samsung.da.supportedModes': ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm'],
|
|
}}
|
|
assert desc.options(resources) == ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm']
|
|
|
|
|
|
def test_microwave_mode_write_round_trips():
|
|
desc = microwave.MICROWAVE_MODE.entities[0]
|
|
path, body = desc.write_fn('MicroWave', {})
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body['x.com.samsung.da.modes'] == ['MicroWave']
|
|
|
|
|
|
def test_microwave_mode_rejects_unknown():
|
|
desc = microwave.MICROWAVE_MODE.entities[0]
|
|
assert desc.write_fn('SpaghettiMode', {}) is None
|
|
|
|
|
|
def test_microwave_mode_write_validates_against_live_supported_modes():
|
|
"""A device reporting its own supportedModes is validated against that
|
|
list, not the static union-of-all-dumps fallback -- 'AirFryer' is a
|
|
valid _MICROWAVE_MODES entry but must still be rejected for a unit
|
|
whose own supportedModes doesn't include it."""
|
|
desc = microwave.MICROWAVE_MODE.entities[0]
|
|
rep = {'x.com.samsung.da.supportedModes': ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm']}
|
|
path, body = desc.write_fn('MicroWave', rep)
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body['x.com.samsung.da.modes'] == ['MicroWave']
|
|
assert desc.write_fn('AirFryer', rep) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MICROWAVE_MODE — lamp/sound options-array writes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_sound_write_is_single_token():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'sound')
|
|
rep = {'x.com.samsung.da.options': ['Sound_On']}
|
|
path, body = desc.write_fn('Off', rep)
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body == {'x.com.samsung.da.options': ['Sound_Off']}
|
|
|
|
|
|
def test_lamp_gated_absent_when_no_lamp_option():
|
|
"""Issue #121's combi dump has no 'Lamp_*' token at all -- unlike
|
|
oven.py's lamp switch (assumed universal), this one self-gates off."""
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
|
rep = {'x.com.samsung.da.options': ['DeviceType_MW7300B-/EU1', 'Sound_Off']}
|
|
assert desc.exists_fn(rep, {}) is False
|
|
|
|
|
|
def test_lamp_gated_present_when_lamp_option_reported():
|
|
"""Issue #137's plain microwave reports 'Lamp_Off'."""
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
|
rep = {'x.com.samsung.da.options': ['Lamp_Off', 'Sound_On']}
|
|
assert desc.exists_fn(rep, {}) is True
|
|
|
|
|
|
def test_lamp_write_is_single_token():
|
|
"""issue #152: the device has never been observed accepting 'On' --
|
|
only 'High'/'Off' -- so the switch's "on" write uses 'High'."""
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
|
rep = {'x.com.samsung.da.options': ['Lamp_Off']}
|
|
path, body = desc.write_fn('On', rep)
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body == {'x.com.samsung.da.options': ['Lamp_High']}
|
|
|
|
|
|
def test_lamp_write_requires_existing_options():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
|
assert desc.write_fn('On', {}) is None
|
|
|
|
|
|
def test_lamp_reads_off_as_false():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
|
assert desc.value_fn(['Lamp_Off']) is False
|
|
|
|
|
|
def test_lamp_reads_any_non_off_level_as_true():
|
|
"""issue #152's ME7500D reports 'Lamp_High', not the binary 'Lamp_On'
|
|
#137's dump implied -- any non-Off/non-absent value must read as on, not
|
|
just a literal 'On'."""
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
|
assert desc.value_fn(['Lamp_High']) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MICROWAVE_MODE — filter_remind/remind_beep options-array writes (issue #181)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_filter_remind_gated_absent_when_no_option():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
|
|
rep = {'x.com.samsung.da.options': ['DeviceType_MW7300B-/EU1', 'Sound_Off']}
|
|
assert desc.exists_fn(rep, {}) is False
|
|
|
|
|
|
def test_filter_remind_gated_present_when_option_reported():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
|
|
rep = {'x.com.samsung.da.options': ['FilterRemind_Off']}
|
|
assert desc.exists_fn(rep, {}) is True
|
|
|
|
|
|
def test_filter_remind_reads_on_off():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
|
|
assert desc.value_fn(['FilterRemind_On']) is True
|
|
assert desc.value_fn(['FilterRemind_Off']) is False
|
|
|
|
|
|
def test_filter_remind_write_is_single_token():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
|
|
rep = {'x.com.samsung.da.options': ['FilterRemind_Off']}
|
|
path, body = desc.write_fn('On', rep)
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body == {'x.com.samsung.da.options': ['FilterRemind_On']}
|
|
|
|
|
|
def test_filter_remind_write_requires_existing_options():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'filter_remind')
|
|
assert desc.write_fn('On', {}) is None
|
|
|
|
|
|
def test_remind_beep_gated_absent_when_no_option():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
|
|
rep = {'x.com.samsung.da.options': ['DeviceType_MW7300B-/EU1', 'Sound_Off']}
|
|
assert desc.exists_fn(rep, {}) is False
|
|
|
|
|
|
def test_remind_beep_gated_present_when_option_reported():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
|
|
rep = {'x.com.samsung.da.options': ['RemindBeep_On']}
|
|
assert desc.exists_fn(rep, {}) is True
|
|
|
|
|
|
def test_remind_beep_reads_on_off():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
|
|
assert desc.value_fn(['RemindBeep_On']) is True
|
|
assert desc.value_fn(['RemindBeep_Off']) is False
|
|
|
|
|
|
def test_remind_beep_write_is_single_token():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
|
|
rep = {'x.com.samsung.da.options': ['RemindBeep_On']}
|
|
path, body = desc.write_fn('Off', rep)
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body == {'x.com.samsung.da.options': ['RemindBeep_Off']}
|
|
|
|
|
|
def test_remind_beep_write_requires_existing_options():
|
|
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'remind_beep')
|
|
assert desc.write_fn('On', {}) is None
|