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.
71 lines
2.6 KiB
Python
71 lines
2.6 KiB
Python
"""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 for_device_by_model
|
|
from custom_components.localthings.registry.discovery import discover
|
|
|
|
from tests.conftest import _load_device
|
|
|
|
|
|
def _air_dresser():
|
|
resources = _load_device('air_dresser')
|
|
info = resources['/information/vs/0']
|
|
reg = for_device_by_model(
|
|
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
|
return reg, resources
|
|
|
|
|
|
def _state():
|
|
reg, resources = _air_dresser()
|
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
|
return flatten(bound, resources)
|
|
|
|
|
|
def test_resolves_to_air_dresser_registry():
|
|
reg, _ = _air_dresser()
|
|
assert reg is not None and reg.name == 'air_dresser'
|
|
|
|
|
|
def test_no_unbound_hrefs():
|
|
reg, resources = _air_dresser()
|
|
unbound = []
|
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
|
assert unbound == []
|
|
|
|
|
|
def test_course_select_present_and_reads_current_selection():
|
|
"""This was the actual reported gap: /course/vs/0 was previously
|
|
entirely unbound, so no cycle/mode select existed at all."""
|
|
state = _state()
|
|
assert state['cycle'] == '01'
|
|
|
|
|
|
def test_course_options_derived_from_supported_options_fallback():
|
|
"""No /wm/editcourse/vs/0 on this board at all, so the option list must
|
|
come from laundry.cycle_options' supportedOptions decode rather than
|
|
editCourseList -- confirmed distinct codes, current selection included."""
|
|
from custom_components.localthings.registry.capabilities.laundry import cycle_options
|
|
_, resources = _air_dresser()
|
|
codes = cycle_options(resources)
|
|
assert codes == ['01', '02', '04', '03', '05', '1A', '1B', '1C', '07', '08']
|
|
|
|
|
|
def test_wrinkle_prevent_present_dry_level_fields_absent():
|
|
"""AIR_DRESSER_SETTINGS only binds wrinkle_prevent -- dryLevel/dryTime/
|
|
dryerType are dryer-only fields this device never reports, so they
|
|
should not appear as always-empty entities."""
|
|
state = _state()
|
|
assert state['wrinkle_prevent'] is False
|
|
for key in ('dry_level', 'dry_time', 'dryer_type'):
|
|
assert key not in state, key
|
|
|
|
|
|
def test_expected_entities_present():
|
|
state = _state()
|
|
for key in (
|
|
'power_switch', 'child_lock', 'remote_control', 'cycle',
|
|
'machine_state', 'progress', 'progress_percentage', 'finish_time',
|
|
'completion_minutes', 'delay_start_hours', 'diagnosis_status',
|
|
'job_beginning_status', 'wrinkle_prevent', 'energy_kwh',
|
|
):
|
|
assert key in state, key
|