Files
localthings/tests/test_microwave_capabilities.py
Marc Billow 07061c734d Fix more pre-existing ty diagnostics in test files
Continues narrowing SamsungEntityDescription accesses to the correct
subclass and asserting Optional write_fn/match_fn/exists_fn fields are
set before calling them, per the pattern established in the previous
commit.
2026-08-03 00:03:08 +00:00

430 lines
15 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,
resolve,
)
from custom_components.localthings.registry.capabilities import microwave
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import NumberDesc, SelectDesc, SwitchDesc
# ---------------------------------------------------------------------------
# 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 == []
def test_qooker_fixture_resolves_as_microwave_and_has_no_unbound_hrefs():
"""Bespoke Qooker MW7500A is microwave-shaped despite both its OCF type
and internal board token saying oven."""
from tests.conftest import _load_device
resources = _load_device("qooker_mw7500a")
reg = resolve(resources, device_types=("oic.wk.d", "oic.d.oven"))
assert reg is not None
assert reg.name == "microwave"
unbound = []
bound = discover(
resources,
reg.capabilities,
reg.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
keys = {entity.desc.key for entity in bound}
assert {"cooking_mode", "power_level", "setpoint"} <= keys
assert "oven_mode" not in keys
# ---------------------------------------------------------------------------
# MICROWAVE_SETPOINT — NumberDesc with RMW write semantics
# ---------------------------------------------------------------------------
def _microwave_setpoint_desc():
return next(e for e in microwave.MICROWAVE_SETPOINT.entities if isinstance(e, NumberDesc))
def test_microwave_setpoint_write_is_read_modify_write():
desc = _microwave_setpoint_desc()
rep = {"x.com.samsung.da.items": [{"x.com.samsung.da.desired": "0"}]}
assert desc.write_fn is not None
result = desc.write_fn(180, rep)
assert result is not None
path, body = result
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_setpoint_desc()
rep = {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.current": "150",
"x.com.samsung.da.desired": "150",
}
]
}
assert desc.write_fn is not None
result = desc.write_fn(180, rep)
assert result is not None
_path, body = result
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_setpoint_desc()
rep = {"x.com.samsung.da.items": [{"x.com.samsung.da.desired": "0"}]}
assert desc.write_fn is not None
result = desc.write_fn(182, rep) # nearest 5 = 180
assert result is not None
_, body = result
assert body["x.com.samsung.da.items"][0]["x.com.samsung.da.desired"] == "180"
def test_microwave_setpoint_rejects_out_of_range():
desc = _microwave_setpoint_desc()
rep = {"x.com.samsung.da.items": [{"x.com.samsung.da.desired": "100"}]}
assert desc.write_fn is not None
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_setpoint_desc()
assert desc.write_fn is not None
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_setpoint_desc()
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 is not None
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 _microwave_cooking_mode_desc():
return next(e for e in microwave.MICROWAVE_MODE.entities if isinstance(e, SelectDesc))
def test_microwave_mode_options_nonempty():
desc = _microwave_cooking_mode_desc()
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_cooking_mode_desc()
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_cooking_mode_desc()
assert desc.write_fn is not None
result = desc.write_fn("MicroWave", {})
assert result is not None
path, body = result
assert path == ["mode", "vs", "0"]
assert body["x.com.samsung.da.modes"] == ["MicroWave"]
def test_microwave_mode_rejects_unknown():
desc = _microwave_cooking_mode_desc()
assert desc.write_fn is not None
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_cooking_mode_desc()
rep = {"x.com.samsung.da.supportedModes": ["NoOperation", "MicroWave", "Autocook", "KeepWarm"]}
assert desc.write_fn is not None
result = desc.write_fn("MicroWave", rep)
assert result is not None
path, body = result
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["Sound_On"]}
assert desc.write_fn is not None
result = desc.write_fn("Off", rep)
assert result is not None
path, body = result
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["DeviceType_MW7300B-/EU1", "Sound_Off"]}
assert desc.exists_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["Lamp_Off", "Sound_On"]}
assert desc.exists_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["Lamp_Off"]}
assert desc.write_fn is not None
result = desc.write_fn("On", rep)
assert result is not None
path, body = result
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" and isinstance(e, SwitchDesc)
)
assert desc.write_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["DeviceType_MW7300B-/EU1", "Sound_Off"]}
assert desc.exists_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["FilterRemind_Off"]}
assert desc.exists_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["FilterRemind_Off"]}
assert desc.write_fn is not None
result = desc.write_fn("On", rep)
assert result is not None
path, body = result
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" and isinstance(e, SwitchDesc)
)
assert desc.write_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["DeviceType_MW7300B-/EU1", "Sound_Off"]}
assert desc.exists_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["RemindBeep_On"]}
assert desc.exists_fn is not None
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" and isinstance(e, SwitchDesc)
)
rep = {"x.com.samsung.da.options": ["RemindBeep_On"]}
assert desc.write_fn is not None
result = desc.write_fn("Off", rep)
assert result is not None
path, body = result
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" and isinstance(e, SwitchDesc)
)
assert desc.write_fn is not None
assert desc.write_fn("On", {}) is None