Address independent code review findings on PR #167

Two real bugs, both latent (no shipped fixture exercised them), plus a
consistency gap and a couple of correctness/DRY nits flagged by review:

- async_set_fan_mode resolved a fan_mode label against the static
  _FAN_TO_DEVICE reverse map before checking whether the resulting code is
  actually one of the unit's own supportedModes. A board using non-standard
  wind-strength codes while still spelling a standard-looking label in
  modesName (e.g. codes "31"-"33" named "Low"/"High"/"Turbo") would silently
  write a code ("1"/"3"/"4") the device never advertised. Now validates the
  static hit against the unit's own supported codes before trusting it,
  falling through to the live modesName scan otherwise.

- air_purifier.WIND_STRENGTH_FAN reused key='fan', the same key as FAN in
  the same registry -- BoundEntity's unique_id is built from key alone, not
  href, so a board reporting both hrefs would have one fan entity silently
  shadow the other. Renamed to 'wind_strength_fan' (translation_key
  unchanged). No shipped fixture reports both hrefs today, but the two caps
  living in the same registry made this a real latent hazard, the exact one
  AIRFLOW_GENERIC's own comment already documents and deliberately avoids.

- microwave.py's cooking_mode select still used a static, union-of-all-
  dumps mode list, even though both shipped microwave fixtures already
  report x.com.samsung.da.supportedModes on /mode/vs/0 -- the same shape
  oven._oven_mode_options was just built to prefer over exactly this kind
  of static list (issue #138's follow-up, this same PR's skill update).
  ME7500D advertises 4 modes; the select was offering 11. Applied the same
  live-first, static-fallback pattern.

- Added the issue #152 fixture the microwave lamp fix was missing (the
  SKILL.md step this PR itself added asks for one).

- climate.py's _legacy_airflow rebuilt a 2-key presence dict from
  coordinator.resource()'s truthiness, which collapses "href absent" and
  "href present with an empty {} rep" to the same falsy value -- while
  is_legacy_board (and discover()'s own binding) test key membership, not
  truthiness. Simplified to pass last_resources through directly, matching
  is_legacy_board's actual contract instead of a cheaper approximation of
  it, so the "can never disagree" claim in both docstrings is actually true.

- Hoisted the 'power' payload branch duplicated verbatim across
  _airflow_fan_write/_fan_write/_wind_strength_fan_write into one
  _power_write helper (registry/capabilities/air_purifier.py).

- Removed two now-unused imports (test_air_dresser_capabilities.py,
  test_air_purifier_vtww_fan.py) and replaced a tautological
  code-in-_DEVICE_TO_FAN check with one that actually exercises the live
  climate entity's fan_modes/fan_mode (test_climate_ac_modes.py).

- Fixed a pre-existing (not from this PR) no-op test on main --
  test_registry_reproduces_golden_state_keys_for_induction_cooktop computed
  golden/state_keys and never asserted on them.

756 tests pass.
This commit is contained in:
Marc Billow
2026-07-28 14:37:32 +00:00
parent dc3e1255d1
commit 5a73a25005
12 changed files with 508 additions and 51 deletions
+22 -18
View File
@@ -268,19 +268,18 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
Delegates the board-generation test to is_legacy_board (the same
test capabilities/airconditioner.py's token entities are gated on)
instead of re-implementing it, via a minimal presence dict built
from the two hrefs it actually inspects -- cheaper than
last_resources' full snapshot copy, since is_legacy_board only
checks key membership.
instead of re-implementing it. Uses last_resources rather than a
two-key presence dict built from coordinator.resource()'s truthiness
-- resource() collapses "href absent" and "href present with an
empty {} rep" to the same falsy value, while is_legacy_board (and
discover()'s own binding) test key membership, not truthiness. A
presence dict built from truthiness alone would disagree with the
token entities on a board reporting a genuinely empty /airflow/vs/0,
silently reintroducing the drift this delegation exists to prevent.
"""
airflow = self.coordinator.resource(AIRFLOW_HREF)
wind_strength = self.coordinator.resource(WIND_STRENGTH_HREF)
presence = {}
if airflow:
presence[AIRFLOW_HREF] = airflow
if wind_strength:
presence[WIND_STRENGTH_HREF] = wind_strength
return airflow if is_legacy_board(presence) else {}
if not is_legacy_board(self.coordinator.last_resources):
return {}
return self.coordinator.resource(AIRFLOW_HREF) or {}
def _legacy_preset(self) -> bool:
"""Whether presets come from the Comode_* token rather than a resource.
@@ -563,14 +562,19 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
await self.coordinator.async_send_command(
self._bound, ('fan_legacy', level))
return
supported = self._supported(WIND_STRENGTH_HREF)
device = _FAN_TO_DEVICE.get(fan_mode)
if device is None:
# fan_mode came from _wind_strength_label's dynamic path (issue
# #155) -- resolve back to the device's own code the same way
# async_set_preset_mode does for its dynamic codes.
# A static hit is only trustworthy if this unit's own supportedModes
# actually includes that code -- a board can use non-standard codes
# (issue #155's "31"-"35") while still spelling a standard label
# ("Low"/"High") in modesName, in which case _FAN_TO_DEVICE.get would
# return a plausible-looking code ('1'/'3') the device never
# advertised at all. Fall through to the live scan whenever the
# static guess isn't actually one of this unit's own codes.
if device is None or (supported and device not in supported):
rep = self._rep(WIND_STRENGTH_HREF)
for code in self._supported(WIND_STRENGTH_HREF):
if code not in _DEVICE_TO_FAN and _wind_strength_label(code, rep) == fan_mode:
for code in supported:
if _wind_strength_label(code, rep) == fan_mode:
device = code
break
if device is not None:
@@ -137,14 +137,22 @@ DEVICE_ACTIVE = Capability(
),
)
def _power_write(power_href, value):
"""Shared 'power' payload handling for this family's three FanDesc write
functions -- targets whichever power href fan.py's _power_payload picked
(the board may only report /power/0); a hardcoded vendor href here would
silently no-op on such a board even though the entity's own is_on
already falls back to reading it correctly."""
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
def _airflow_fan_write(payload, rep, href=None):
kind, value, *args = payload
if kind == 'power':
power_href = args[0] if args else '/power/vs/0'
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
return _power_write(args[0] if args else '/power/vs/0', value)
if kind == 'speed':
return ['airflow', '0'], {'speed': int(value)}
return None
@@ -234,15 +242,7 @@ MODE = Capability(
def _fan_write(payload, rep, href=None):
kind, value, *args = payload
if kind == 'power':
# Targets whichever power href fan.py's _power_payload picked (the
# board may only report /power/0) -- a hardcoded vendor href here
# would silently no-op on such a board even though the entity's
# own is_on already falls back to reading it correctly.
power_href = args[0] if args else '/power/vs/0'
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
return _power_write(args[0] if args else '/power/vs/0', value)
if kind == 'mode':
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]}
return None
@@ -277,11 +277,7 @@ FAN = Capability(
def _wind_strength_fan_write(payload, rep, href=None):
kind, value, *args = payload
if kind == 'power':
power_href = args[0] if args else '/power/vs/0'
if power_href == '/power/0':
return ['power', '0'], {'value': bool(value)}
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
return _power_write(args[0] if args else '/power/vs/0', value)
if kind == 'mode':
return ['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value}
return None
@@ -294,11 +290,17 @@ def _wind_strength_fan_write(payload, rep, href=None):
# LocalThingsAirPurifierFan._label_for_code rather than a hardcoded
# per-model map. modes here is a bare string ('87'), not a single-element
# list like HREF_MODE's -- _wind_strength_fan_write writes it back as-is.
#
# key is 'wind_strength_fan', NOT 'fan' -- FAN above shares this registry
# and also uses a FanDesc; BoundEntity's unique_id is built from key alone
# (entity.py's _key), not href, so two same-key FanDescs in one registry
# would collide if a board ever bound both (see AIRFLOW_GENERIC's own
# comment on this exact hazard -- missed here in the initial cut).
WIND_STRENGTH_FAN = Capability(
href=HREF_WIND_STRENGTH,
poll_tier='warm',
entities=(
FanDesc(key='fan', translation_key='air_purifier_fan',
FanDesc(key='wind_strength_fan', translation_key='air_purifier_fan',
field='x.com.samsung.da.modes', write_fn=_wind_strength_fan_write),
),
)
@@ -112,8 +112,21 @@ def _power_level_watts(v):
return int_or_none(s)
def _cooking_mode_options(resources):
"""Live mode list from the device's own /mode/vs/0 supportedModes when
it reports one (both known dumps do); the union-of-all-dumps
_MICROWAVE_MODES guess otherwise. Same live-first, static-fallback
pattern as oven._oven_mode_options -- a fixed list here would offer
users modes their own unit doesn't have (issue #152's ME7500D reports
only 4 of _MICROWAVE_MODES' 11)."""
rep = resources.get('/mode/vs/0') or {}
live = rep.get('x.com.samsung.da.supportedModes')
return list(live) if live else list(_MICROWAVE_MODES)
def _mode_write(p, rep, href=None):
if p not in _MICROWAVE_MODES:
valid = rep.get('x.com.samsung.da.supportedModes') or _MICROWAVE_MODES
if p not in valid:
return None
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
@@ -187,7 +200,7 @@ MICROWAVE_MODE = Capability(
# 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,
options=_cooking_mode_options,
value_fn=lambda v: v[0] if v else None,
write_fn=_mode_write),
SwitchDesc(key='sound', field='x.com.samsung.da.options',
+2 -2
View File
@@ -5,13 +5,13 @@
"device_active",
"dust",
"energy_kwh",
"fan",
"fine_dust",
"firmware_update",
"hepa_filter_status",
"hepa_filter_usage",
"odor",
"power_switch",
"super_fine_dust"
"super_fine_dust",
"wind_strength_fan"
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"state_keys": [
"alarm_code",
"cavity_state",
"child_lock",
"cloud_connected",
"cook_time",
"cooking_mode",
"cycle_active",
"door_open",
"energy_kwh",
"fan",
"finish_time",
"firmware_update",
"lamp",
"machine_state",
"operation_time_minutes",
"power_level",
"progress_percentage",
"sound"
]
}
+269
View File
@@ -0,0 +1,269 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.s"
],
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "OV_E_OFF",
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
}
]
}
},
{
"href": "/connected/vs/0",
"rep": {
"x.com.samsung.da.connected": "On",
"rt": [
"x.com.samsung.da.connected"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
}
],
"rt": [
"x.com.samsung.da.doors"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "103700",
"x.com.samsung.da.cumulativeUnit": "Wh"
}
},
{
"href": "/hood/fanspeed/vs/0",
"rep": {
"x.com.samsung.da.hood.fanSpeed": "0",
"x.com.samsung.da.hood.supportedFanSpeed": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.hood.settableMaxFanSpeed": "4",
"x.com.samsung.da.hood.settableMinFanSpeed": "0",
"rt": [
"x.com.samsung.da.hood.fanSpeed"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA-KS-MICROWAVE-01051|40475141|50040000011811000A00000000000000",
"x.com.samsung.da.description": "ME7500D-/AA2",
"x.com.samsung.da.serialNum": "REDACTED",
"x.com.samsung.da.otnDUID": "REDACTED",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "24111400",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "04751A23121900",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "04754B23120800",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "KM5",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"NoOperation",
"MicroWave",
"Autocook",
"KeepWarm"
],
"x.com.samsung.da.modes": [
"NoOperation"
],
"x.com.samsung.da.options": [
"DeviceType_ME7500D-/AA2",
"TimeAutoSync_On",
"weight_LBS",
"TimeSystem_12",
"Sound_Off",
"RemindBeep_Off",
"FilterRemind_Off",
"Lamp_High"
],
"x.com.samsung.da.defaultMode": "MicroWave",
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.operationTime": "00:00:00",
"x.com.samsung.da.remainingTime": "00:00:00",
"x.com.samsung.da.progressPercentage": "0",
"rt": [
"x.com.samsung.da.operation"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"flashingProgress": "",
"otnStatus": "None",
"otnList": [
{
"type": "WIFI",
"modelId": "AKS-WW-TP1-23-MICROWAVE-OTR",
"versions": [
"40241114"
],
"visVersion": "241114"
},
{
"type": "Micom",
"modelId": "074240475141FFFFFFFF",
"versions": [
"23121900",
"FFFFFFFF"
],
"visVersion": "231219"
},
{
"type": "Micom",
"modelId": "074240475442FFFFFFFF",
"versions": [
"23120800",
"FFFFFFFF"
],
"visVersion": "231208"
}
]
}
},
{
"href": "/oven/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.recipe": "00000000000000",
"x.com.samsung.da.powerLevel": "0",
"rt": [
"x.com.samsung.da.oven"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "America/New_York",
"offset": "-04:00",
"DST": "ON"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "REDACTED",
"macaddressBLE": "REDACTED"
}
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tests for the AirDresser device type (DA_DF_A51_20_COMMON, issue #162)."""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import air_dresser, for_device_by_model
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
+38 -1
View File
@@ -9,7 +9,7 @@ already. LocalThingsAirPurifierFan._label_for_code resolves both shapes
without a per-model map.
"""
from custom_components.localthings.fan import LocalThingsAirPurifierFan
from custom_components.localthings.registry.by_type import air_purifier, for_device_by_model
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.capabilities.air_purifier import HREF_WIND_STRENGTH
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import FanDesc
@@ -89,3 +89,40 @@ async def test_set_preset_mode_writes_back_the_raw_code():
def test_is_on_reads_vendor_power():
entity = _entity(_resources())
assert entity.is_on is False
def test_wind_strength_fan_key_does_not_collide_with_mode_fan():
"""WIND_STRENGTH_FAN and FAN both live in this registry and both are
FanDesc-typed; BoundEntity's unique_id is built from key alone (entity.py's
_key), not href, so a shared key would silently shadow one entity if a
board ever bound both (see AIRFLOW_GENERIC's own comment on this exact
hazard). No real dump reports both hrefs today, but the keys must stay
distinct regardless."""
from custom_components.localthings.registry.capabilities import air_purifier
fan_keys = {
entity.key
for cap in (air_purifier.FAN, air_purifier.WIND_STRENGTH_FAN)
for entity in cap.entities
}
assert fan_keys == {'fan', 'wind_strength_fan'}
def test_both_fan_hrefs_bound_simultaneously_produce_distinct_entities():
"""Synthetic combination (no real dump reports both hrefs) proving the
two FanDescs don't shadow each other in flatten()'s key-based state dict
even if a future board did report both."""
from custom_components.localthings.registry.adapter import flatten
resources = _resources()
resources['/mode/vs/0'] = {
'x.com.samsung.da.modes': ['Smart'],
'x.com.samsung.da.supportedModes': ['Smart', 'Max', 'Mid', 'WindFree', 'Sleep'],
}
reg = _reg(resources)
unbound = []
bound = discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
state = flatten(bound, resources)
assert state['fan'] == 'Smart'
assert state['wind_strength_fan'] == '87'
@@ -103,3 +103,32 @@ async def test_set_fan_mode_still_resolves_standard_scale_labels():
await entity.async_set_fan_mode('auto')
assert coordinator.commands[-1][1] == ('fan', '0')
async def test_set_fan_mode_does_not_misroute_when_static_map_and_live_codes_collide():
"""A board can use non-standard codes ('31'-'33') while modesName still
spells a standard-looking label ('Low'/'High') that _FAN_TO_DEVICE's
static reverse map also happens to have an entry for ('1'/'3') -- but
that entry is for a *different* code this unit never advertises at all.
Resolving the static hit without checking it against this unit's own
supportedModes would silently write a code the device doesn't have.
"""
resources = _load_device(FIXTURE)
resources['/wind/strength/vs/0'] = {
'x.com.samsung.da.modes': '0',
'x.com.samsung.da.supportedModes': ['0', '31', '32', '33'],
'x.com.samsung.da.modesName': ['Auto', 'Low', 'High', 'Turbo'],
}
coordinator = _FakeCoordinator(resources)
entity = _climate(resources, coordinator)
assert entity.fan_modes == ['auto', 'low', 'high', 'turbo']
await entity.async_set_fan_mode('high')
assert coordinator.commands[-1][1] == ('fan', '32')
await entity.async_set_fan_mode('low')
assert coordinator.commands[-1][1] == ('fan', '31')
await entity.async_set_fan_mode('turbo')
assert coordinator.commands[-1][1] == ('fan', '33')
+36 -4
View File
@@ -7,7 +7,7 @@ testable directly.
from homeassistant.components.climate import HVACMode
from custom_components.localthings.climate import (
_AI_COMFORT_MODE, _DEVICE_TO_FAN, _DEVICE_TO_HVAC, _HVAC_TO_DEVICE,
_AI_COMFORT_MODE, _DEVICE_TO_HVAC, _HVAC_TO_DEVICE,
PRESET_AI_COMFORT, _preset_to_ha,
)
@@ -81,6 +81,38 @@ def test_fac_bora_wind_strength_codes_fit_the_standard_scale():
(0/2/3/4, skipping 1/'low') and modesName (Auto/Mid/High/Turbo) already
match _DEVICE_TO_FAN's own mapping exactly -- no dynamic modesName
fallback needed for this particular board, unlike issue #155's
TP1X_DA-AC-RAC-01001_0000."""
for code in ('0', '2', '3', '4'):
assert code in _DEVICE_TO_FAN
TP1X_DA-AC-RAC-01001_0000.
Asserts the live climate entity's actual fan_modes/fan_mode output
(not just that the module constant _DEVICE_TO_FAN happens to have
these keys) -- a bare `code in _DEVICE_TO_FAN` check would still pass
even if fan_modes/fan_mode were completely broken, since it never
touches the entity at all.
"""
from custom_components.localthings.climate import LocalThingsClimate
from custom_components.localthings.registry import by_type
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import ClimateDesc
from tests.conftest import _load_device
class _FakeCoordinator:
device_serial = 'TEST-FAC-BORA-SERIAL'
device_info = {}
data = {}
def __init__(self, resources):
self.last_resources = resources
def resource(self, href):
return self.last_resources.get(href, {})
resources = _load_device('airconditioner_fac_bora')
info = resources['/information/vs/0']
reg = by_type.for_device_by_model(
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
climate_bound = next(item for item in bound if isinstance(item.desc, ClimateDesc))
entity = LocalThingsClimate(_FakeCoordinator(resources), climate_bound)
assert entity.fan_modes == ['auto', 'medium', 'high', 'turbo']
assert entity.fan_mode == 'auto'
+22
View File
@@ -478,6 +478,12 @@ def test_registry_reproduces_golden_state_keys_for_induction_cooktop():
resources = _load_device('induction_cooktop')
golden = json.loads((GOLDEN / 'induction_cooktop.json').read_text())
state_keys = _new_state_keys('induction_cooktop', 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_registry_reproduces_golden_state_keys_for_range_no_info():
"""NE63B8411SS (issue #74) -- reports no oneUiVersion *and* no
@@ -668,6 +674,22 @@ def test_registry_reproduces_golden_state_keys_for_microwave_me7500d():
)
def test_registry_reproduces_golden_state_keys_for_microwave_me7500d_lamp_high():
"""Same TP1X_DA-KS-MICROWAVE-01051/ME7500D board as microwave_me7500d
above, but this live capture (issue #152) is the first to report a
non-Off Lamp token ('Lamp_High'). Locks in that the lamp switch reads
it as on rather than the previously-hardcoded 'On'-only comparison."""
from tests.conftest import _load_device
resources = _load_device('microwave_me7500d_lamp_high')
golden = json.loads((GOLDEN / 'microwave_me7500d_lamp_high.json').read_text())
state_keys = _new_state_keys('microwave_me7500d_lamp_high', 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_registry_reproduces_golden_state_keys_for_air_purifier_tp1x_da_ac_air():
"""TP1X_DA-AC-AIR-01031_0000 (issue #130) self-reports oneUiVersion
'7.0 Air purifier' and resolves via for_device() onto the existing
+30 -3
View File
@@ -120,9 +120,23 @@ def test_power_level_handles_missing_value():
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'
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():
@@ -137,6 +151,19 @@ def test_microwave_mode_rejects_unknown():
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
# ---------------------------------------------------------------------------