Split microwaves into their own device type instead of the oven registry
Microwaves (combi and plain) were routed onto the oven registry (issue #121), which meant entities carried oven-flavored keys (oven_state, oven_mode, oven_setpoint) and inherited oven-specific behavior that's wrong for this family: a 30-270C setpoint range instead of this family's actual 40-200C, a cooking-mode list missing MicroWave/MicroWaveGrill/ MicroWaveConvection/KeepWarm entirely, and a lamp switch that read/wrote the oven's 'UpperLamp' option token instead of this family's 'Lamp' token. Adds a microwave device type (by_type/microwave.py, capabilities/microwave.py) that reuses the oven board family's shared operational-state/door/connected/recipe-cook capabilities but defines its own cooking-mode, setpoint, and cavity capabilities with the corrected bounds/vocabulary, plus a new power_level sensor for the cavity's Watt setting that was previously unexposed.
This commit is contained in:
11 files changed
+472
-34
No files matched your search
@@ -25,7 +25,8 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
|
||||
| Air purifier | `by_type/air_purifier.py` |
|
||||
| Dehumidifier | `by_type/dehumidifier.py` |
|
||||
| Dryer | `by_type/dryer.py` |
|
||||
| Oven (including combi microwaves) | `by_type/oven.py` |
|
||||
| Oven | `by_type/oven.py` |
|
||||
| Microwave (combi and plain) | `by_type/microwave.py` |
|
||||
| Gas cooktop (read-only burner status) | `by_type/cooktop.py` |
|
||||
| Range hood | `by_type/range_hood.py` |
|
||||
| Range | `by_type/range.py` |
|
||||
|
||||
@@ -4,8 +4,8 @@ from typing import Optional
|
||||
from ._base import DeviceRegistry
|
||||
from . import (
|
||||
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
|
||||
induction_cooktop, oven, range as _range, range_hood, refrigerator,
|
||||
vacuum_station, washer, water_purifier,
|
||||
induction_cooktop, microwave, oven, range as _range, range_hood,
|
||||
refrigerator, vacuum_station, washer, water_purifier,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -24,6 +24,7 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
|
||||
'dishwasher': dishwasher.REGISTRY,
|
||||
'dryer': dryer.REGISTRY,
|
||||
'induction_cooktop': induction_cooktop.REGISTRY,
|
||||
'microwave': microwave.REGISTRY,
|
||||
'oven': oven.REGISTRY,
|
||||
'hood': range_hood.REGISTRY,
|
||||
'range': _range.REGISTRY,
|
||||
@@ -232,15 +233,18 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
# oneUiVersion and doesn't match the washer/dryer/dishwasher prefix map.
|
||||
if key is None and '-OVEN-' in (model_num or '').upper():
|
||||
key = 'oven'
|
||||
# Combi microwaves (e.g. TP1X_DA-KS-MICROWAVE-01041, issue #121) -- same
|
||||
# board family as the wall oven above (an '/oven/vs/0' cavity resource
|
||||
# with Convection/AirFryer/Grill/MicroWave modes on /mode/vs/0), just a
|
||||
# different cavity. Reports no oneUiVersion and doesn't match the
|
||||
# washer/dryer/dishwasher prefix map either. Binds cleanly against the
|
||||
# existing oven registry once routed here (confirmed against the issue
|
||||
# #121 dump) -- no microwave-specific device type needed.
|
||||
# Microwaves, both combi (e.g. TP1X_DA-KS-MICROWAVE-01041, issue #121)
|
||||
# and plain (e.g. TP2X_DA-KS-MICROWAVE-01011, issue #66) -- same board
|
||||
# family as the wall oven above (an '/oven/vs/0' cavity resource, same
|
||||
# /operational/state/vs/0 + /doors/vs/0 shape), but a distinct mode
|
||||
# vocabulary (Convection/AirFryer/Grill/MicroWave*) and setpoint bounds
|
||||
# from the oven registry, plus a powerLevel field ovens don't report --
|
||||
# its own device type rather than folded into 'oven' (issue #121 shipped
|
||||
# it onto the oven registry initially; split out per user feedback).
|
||||
# Reports no oneUiVersion and doesn't match the washer/dryer/dishwasher
|
||||
# prefix map either.
|
||||
if key is None and '-MICROWAVE-' in (model_num or '').upper():
|
||||
key = 'oven'
|
||||
key = 'microwave'
|
||||
# Standalone induction cooktops (e.g. TP1X_DA-KS-COOKTOP-01011, issue
|
||||
# #86) -- same board family and '/cooktop/status/vs/0' resource shape
|
||||
# as the range combo above, but no oven attached at all. Distinct from
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Microwave device registry (combi and plain microwaves, issues #66/#121).
|
||||
|
||||
Shares the oven board family's cavity/cook-cycle resource shape, so the
|
||||
operational-state, door, cloud-connected, and quick-recipe-display
|
||||
Capability objects are reused directly from oven.py rather than duplicated.
|
||||
Cooking mode, setpoint, cavity power level, and lamp are genuinely
|
||||
different for this family (different mode vocabulary, different setpoint
|
||||
bounds, an extra powerLevel field, a differently-named lamp option) and are
|
||||
defined fresh in capabilities/microwave.py -- see that module's docstring.
|
||||
"""
|
||||
from ..capabilities import common, ignored, microwave, oven
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='microwave',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
*common.POWER,
|
||||
microwave.MICROWAVE_CAVITY,
|
||||
microwave.MICROWAVE_SETPOINT,
|
||||
microwave.MICROWAVE_MODE,
|
||||
oven.OVEN_OPERATIONAL_STATE,
|
||||
oven.OVEN_DOOR,
|
||||
oven.OVEN_CONNECTED,
|
||||
oven.OVEN_RECIPE_COOK,
|
||||
]),
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Capabilities for the Samsung microwave family (TP1X_DA-KS-MICROWAVE-*
|
||||
class boards, both combi units and plain microwaves).
|
||||
|
||||
Shares the oven board family's cavity/cook-cycle resource shape
|
||||
(`/operational/state/vs/0`, `/doors/vs/0`, `/connected/vs/0`,
|
||||
`/recipe/cook/vs/0`) -- those Capability objects are reused directly from
|
||||
oven.py in by_type/microwave.py rather than duplicated. What's genuinely
|
||||
different from an oven, and defined fresh here:
|
||||
|
||||
* Cooking-mode vocabulary: MicroWave/MicroWaveGrill/MicroWaveConvection/
|
||||
KeepWarm never appear on an oven's /mode/vs/0, and this family spells
|
||||
some shared-sounding modes differently than oven.py's own constants
|
||||
(e.g. 'AirFryer', not oven.py's 'AirFry') -- a distinct SelectDesc and
|
||||
mode list, not oven.OVEN_MODE.
|
||||
* Setpoint bounds: this family's Convection/MicroWaveConvection modeSpec
|
||||
(issue #121's MW7300B dump) reports 40-200 C / step 5, not oven.py's
|
||||
30-270 C range (verified against a different, bake-oven-class board).
|
||||
* Cavity: /oven/vs/0 here also carries a `powerLevel` field (100W-900W
|
||||
on the MicroWave mode's powerListData) that plain ovens don't report --
|
||||
exposed as its own sensor.
|
||||
* Lamp: this family's option-array token is bare 'Lamp' (issue #137's
|
||||
'Lamp_Off'), not oven.py's 'UpperLamp' -- and it's genuinely absent on
|
||||
the combi dump (issue #121), so it's gated with exists_fn rather than
|
||||
assumed universal like oven.py's lamp switch.
|
||||
|
||||
Note: cooking-mode writes are unproven here, same caveat as oven.py's
|
||||
OVEN_MODE -- exposed as a SelectDesc for fidelity, first real-world write
|
||||
is also the test.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import NumberDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import int_or_none, normalize_temp_unit
|
||||
from .laundry import option_value, option_write
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Union of every mode seen across the two known dumps: issue #121's combi
|
||||
# MW7300B (NoOperation/Autocook/AutocookCustom/Convection/AirFryer/Grill/
|
||||
# MicroWave/MicroWaveGrill/MicroWaveConvection/Deodorization) and issue #137's
|
||||
# plain ME7500D (NoOperation/MicroWave/Autocook/KeepWarm). No dump has shown
|
||||
# every mode below on one device -- the select surfaces whatever a given
|
||||
# board's own /mode/vs/0 supportedModes reports; an entry here that a device
|
||||
# never sends just never gets picked.
|
||||
_MICROWAVE_MODES = (
|
||||
'NoOperation',
|
||||
'MicroWave',
|
||||
'MicroWaveGrill',
|
||||
'MicroWaveConvection',
|
||||
'Convection',
|
||||
'AirFryer',
|
||||
'Grill',
|
||||
'Autocook',
|
||||
'AutocookCustom',
|
||||
'Deodorization',
|
||||
'KeepWarm',
|
||||
)
|
||||
|
||||
# Convection/MicroWaveConvection modeSpec on issue #121's dump: tempMinC 40,
|
||||
# tempMaxC 200, tempIntervalC 5. No Fahrenheit dump exists for this family;
|
||||
# unlike oven.py's own SETPOINT_MIN_F/MAX_F/STEP_F (independently verified
|
||||
# against issue #44's range dump), there's nothing to verify a microwave's
|
||||
# Fahrenheit bounds against, so this module only exposes the setpoint
|
||||
# control when the live unit is Celsius (see _microwave_temp_unit below).
|
||||
SETPOINT_MIN_C = 40
|
||||
SETPOINT_MAX_C = 200
|
||||
SETPOINT_STEP_C = 5
|
||||
|
||||
|
||||
def _microwave_temp_unit(rep):
|
||||
"""Same shape as oven.py's _oven_temp_unit: /temperatures/vs/0 items[]
|
||||
carries a per-item x.com.samsung.da.unit field. Both known dumps for
|
||||
this family report 'Celsius'; kept live rather than hardcoded per the
|
||||
fridge/oven convention (issue #7)."""
|
||||
items = rep.get('x.com.samsung.da.items') or []
|
||||
unit = items[0].get('x.com.samsung.da.unit') if items else None
|
||||
return normalize_temp_unit(unit, default='°C')
|
||||
|
||||
|
||||
def _setpoint_write(p, rep, href=None):
|
||||
"""RMW write to /temperatures/vs/0 items array -- unproven for this
|
||||
family (no live write confirmed against a real unit), same "exposed for
|
||||
fidelity" caveat as the mode select."""
|
||||
try:
|
||||
temp = float(p)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
temp_i = int(round(temp / SETPOINT_STEP_C) * SETPOINT_STEP_C)
|
||||
if not (SETPOINT_MIN_C <= temp_i <= SETPOINT_MAX_C):
|
||||
return None
|
||||
items = rep.get('x.com.samsung.da.items')
|
||||
if not items:
|
||||
return None
|
||||
items = [dict(it) for it in items]
|
||||
items[0]['x.com.samsung.da.desired'] = str(temp_i)
|
||||
return ['temperatures', 'vs', '0'], {'x.com.samsung.da.items': items}
|
||||
|
||||
|
||||
def _power_level_watts(v):
|
||||
"""'100W'..'900W' (issue #121) or a bare '0' (issue #137) -> int watts."""
|
||||
if v is None:
|
||||
return None
|
||||
s = str(v).strip()
|
||||
if s.upper().endswith('W'):
|
||||
s = s[:-1]
|
||||
return int_or_none(s)
|
||||
|
||||
|
||||
def _mode_write(p, rep, href=None):
|
||||
if p not in _MICROWAVE_MODES:
|
||||
return None
|
||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
|
||||
|
||||
|
||||
def _sound_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('Sound', p),
|
||||
}
|
||||
|
||||
|
||||
def _lamp_exists(rep, resources):
|
||||
return option_value(rep.get('x.com.samsung.da.options'), 'Lamp') is not None
|
||||
|
||||
|
||||
def _lamp_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('Lamp', p),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capabilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MICROWAVE_CAVITY = Capability(
|
||||
href='/oven/vs/0',
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
SensorDesc(key='cavity_state', field='x.com.samsung.da.state'),
|
||||
SensorDesc(key='power_level', field='x.com.samsung.da.powerLevel',
|
||||
unit='W', value_fn=_power_level_watts),
|
||||
),
|
||||
)
|
||||
|
||||
MICROWAVE_SETPOINT = Capability(
|
||||
href='/temperatures/vs/0',
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
NumberDesc(key='setpoint', field='x.com.samsung.da.items',
|
||||
device_class='temperature', unit_fn=_microwave_temp_unit,
|
||||
native_min=float(SETPOINT_MIN_C), native_max=float(SETPOINT_MAX_C),
|
||||
step=float(SETPOINT_STEP_C), icon='mdi:thermometer-chevron-up',
|
||||
exists_fn=lambda rep, resources: _microwave_temp_unit(rep) == '°C',
|
||||
value_fn=lambda items: int_or_none(
|
||||
(items[0].get('x.com.samsung.da.desired') if items else None)),
|
||||
write_fn=_setpoint_write),
|
||||
SensorDesc(key='current_temp_c', field='x.com.samsung.da.items',
|
||||
device_class='temperature',
|
||||
state_class='measurement', unit_fn=_microwave_temp_unit,
|
||||
value_fn=lambda items: int_or_none(
|
||||
(items[0].get('x.com.samsung.da.current') if items else None))),
|
||||
),
|
||||
)
|
||||
|
||||
MICROWAVE_MODE = Capability(
|
||||
href='/mode/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
# SelectDesc first — test_microwave_mode_options_nonempty uses entities[0]
|
||||
SelectDesc(key='cooking_mode', field='x.com.samsung.da.modes',
|
||||
icon='mdi:tune',
|
||||
options=_MICROWAVE_MODES,
|
||||
value_fn=lambda v: v[0] if v else None,
|
||||
write_fn=_mode_write),
|
||||
SwitchDesc(key='sound', field='x.com.samsung.da.options',
|
||||
icon='mdi:volume-high',
|
||||
entity_category='config',
|
||||
value_fn=lambda opts: option_value(opts, 'Sound') == 'On',
|
||||
write_fn=_sound_write),
|
||||
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
|
||||
icon='mdi:track-light',
|
||||
exists_fn=_lamp_exists,
|
||||
value_fn=lambda opts: option_value(opts, 'Lamp') == 'On',
|
||||
write_fn=_lamp_write),
|
||||
),
|
||||
)
|
||||
@@ -165,6 +165,9 @@
|
||||
"oven_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"sound_volume": {
|
||||
"name": "Sound volume"
|
||||
},
|
||||
@@ -344,6 +347,22 @@
|
||||
"high": "High"
|
||||
}
|
||||
},
|
||||
"cooking_mode": {
|
||||
"name": "Cooking mode",
|
||||
"state": {
|
||||
"no_operation": "No operation",
|
||||
"micro_wave": "Microwave",
|
||||
"micro_wave_grill": "Microwave + grill",
|
||||
"micro_wave_convection": "Microwave + convection",
|
||||
"convection": "Convection",
|
||||
"air_fryer": "Air fry",
|
||||
"grill": "Grill",
|
||||
"autocook": "Auto cook",
|
||||
"autocook_custom": "Auto cook (custom)",
|
||||
"deodorization": "Deodorize",
|
||||
"keep_warm": "Keep warm"
|
||||
}
|
||||
},
|
||||
"oven_mode": {
|
||||
"name": "Cooking mode",
|
||||
"state": {
|
||||
@@ -704,6 +723,12 @@
|
||||
"oven_state": {
|
||||
"name": "Cavity state"
|
||||
},
|
||||
"cavity_state": {
|
||||
"name": "Cavity state"
|
||||
},
|
||||
"power_level": {
|
||||
"name": "Power level"
|
||||
},
|
||||
"paired_hood_fan_speed": {
|
||||
"name": "Paired hood fan speed"
|
||||
},
|
||||
|
||||
@@ -165,6 +165,9 @@
|
||||
"oven_setpoint": {
|
||||
"name": "Instelpunt"
|
||||
},
|
||||
"setpoint": {
|
||||
"name": "Instelpunt"
|
||||
},
|
||||
"sound_volume": {
|
||||
"name": "Geluidsvolume"
|
||||
},
|
||||
@@ -344,6 +347,22 @@
|
||||
"high": "Hoog"
|
||||
}
|
||||
},
|
||||
"cooking_mode": {
|
||||
"name": "Bereidingsmodus",
|
||||
"state": {
|
||||
"no_operation": "Niet actief",
|
||||
"micro_wave": "Magnetron",
|
||||
"micro_wave_grill": "Magnetron + grill",
|
||||
"micro_wave_convection": "Magnetron + hetelucht",
|
||||
"convection": "Hetelucht",
|
||||
"air_fryer": "Airfryen",
|
||||
"grill": "Grillen",
|
||||
"autocook": "Automatisch koken",
|
||||
"autocook_custom": "Automatisch koken (aangepast)",
|
||||
"deodorization": "Geurverwijdering",
|
||||
"keep_warm": "Warmhouden"
|
||||
}
|
||||
},
|
||||
"oven_mode": {
|
||||
"name": "Bereidingsmodus",
|
||||
"state": {
|
||||
@@ -704,6 +723,12 @@
|
||||
"oven_state": {
|
||||
"name": "Status ovenruimte"
|
||||
},
|
||||
"cavity_state": {
|
||||
"name": "Status ovenruimte"
|
||||
},
|
||||
"power_level": {
|
||||
"name": "Vermogensniveau"
|
||||
},
|
||||
"paired_hood_fan_speed": {
|
||||
"name": "Ventilatorsnelheid gekoppelde afzuigkap"
|
||||
},
|
||||
|
||||
+5
-7
@@ -1,25 +1,23 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"cavity_state",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"cooking_mode",
|
||||
"current_temp_c",
|
||||
"cycle_active",
|
||||
"door_open",
|
||||
"energy_kwh",
|
||||
"fast_preheat",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"lamp",
|
||||
"machine_state",
|
||||
"natural_steam",
|
||||
"operation_time_minutes",
|
||||
"oven_mode",
|
||||
"oven_setpoint",
|
||||
"oven_state",
|
||||
"power_level",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"setpoint",
|
||||
"sound"
|
||||
]
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -329,19 +329,19 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_oven_via_microwave_token(self):
|
||||
"""Issue #121: a combi microwave (MW7300B-/EU1) reports no
|
||||
def test_microwave_via_microwave_token(self):
|
||||
"""Issue #121/#66: a combi microwave (MW7300B-/EU1) reports no
|
||||
oneUiVersion and an unrecognized consumer token; falls back to the
|
||||
'-MICROWAVE-' token in modelNum onto the *existing* oven registry
|
||||
(same '/oven/vs/0' cavity + cook-mode shape), not a new device
|
||||
type."""
|
||||
'-MICROWAVE-' token in modelNum onto its own microwave registry
|
||||
(initially folded into 'oven' for issue #121, split out into a
|
||||
distinct device type per user feedback)."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA-KS-MICROWAVE-01041|40475341|50040100021811000A00000000000000',
|
||||
'MW7300B-/EU1',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'oven'
|
||||
assert reg.name == 'microwave'
|
||||
|
||||
def test_vacuum_station_via_vskr_token(self):
|
||||
"""Issue #131: a stick-vacuum clean/auto-empty station
|
||||
|
||||
@@ -568,19 +568,20 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_windfree_oscil
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_oven_mw7300b():
|
||||
def test_registry_reproduces_golden_state_keys_for_microwave_mw7300b():
|
||||
"""TP1X_DA-KS-MICROWAVE-01041 combi microwave (model MW7300B, issue
|
||||
#121) -- reports no oneUiVersion; resolved via the '-MICROWAVE-'
|
||||
modelNum token fallback onto the *existing* oven registry rather than
|
||||
a new device type, since it shares the same '/oven/vs/0' cavity and
|
||||
'/mode/vs/0' cook-mode shape (Convection/AirFryer/Grill/MicroWave*).
|
||||
The only href the oven registry didn't already cover was
|
||||
/recipe/cook/vs/0 (oven.OVEN_RECIPE_COOK, an empty quick-recipe-display
|
||||
blob with no entity)."""
|
||||
modelNum token fallback onto its own microwave registry. Shares the
|
||||
oven board family's operational-state/door/connected/recipe-cook
|
||||
Capability objects (reused directly from oven.py), but has its own
|
||||
cooking-mode vocabulary, setpoint bounds, and cavity power-level sensor
|
||||
(capabilities/microwave.py) -- this device's initial routing folded it
|
||||
into the oven registry (issue #121); split into its own device type per
|
||||
user feedback that microwaves shouldn't show up as ovens."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('oven_mw7300b')
|
||||
golden = json.loads((GOLDEN / 'oven_mw7300b.json').read_text())
|
||||
state_keys = _new_state_keys('oven_mw7300b', resources)
|
||||
resources = _load_device('microwave_mw7300b')
|
||||
golden = json.loads((GOLDEN / 'microwave_mw7300b.json').read_text())
|
||||
state_keys = _new_state_keys('microwave_mw7300b', 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"
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""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 == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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 len(desc.options) > 0
|
||||
assert 'MicroWave' in desc.options
|
||||
assert 'AirFryer' in desc.options # distinct spelling from oven.py's 'AirFry'
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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():
|
||||
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_On']}
|
||||
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user