Course codes on the shared /course/vs/0 contract aren't guaranteed consistent across board generations: washer/combo devices report course table Table_02, dryer devices Table_03 (x.com.samsung.da.st.courseTable, previously fully ignored), and every code in washer_cycle/dryer_cycle was confirmed exclusively against those. FlexWash's older DA_WM_A51 board reports Table_00 instead -- applying the same translations there risked showing a wrong name for any code that happens to numerically collide between tables, not just an untranslated one. SelectDesc.translation_key can now be a callable (resources -> key or None), mirroring the existing pattern for `options`. laundry.cycle_select gains optional table_href/validated_table params: when given, the renamed washer_cycle_table_02/dryer_cycle_table_03 keys only apply when the device's own course table matches exactly -- a different table, or no table id at all, gets no translation_key (raw code display) rather than a guess. dishwasher's call site is unchanged (static key, unconditional): no equivalent table-id resource exists in any dump seen, and no evidence its course codes vary by table the way washer/dryer's do. entity.py and select.py resolve a callable translation_key once (via coordinator.last_resources) and reuse that resolved value everywhere _display() needs it, rather than re-checking the raw descriptor field.
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""Tests for dryer support and washer/dryer consistency (issue #14)."""
|
|
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.capabilities import dryer, ignored, laundry
|
|
from custom_components.localthings.registry.discovery import discover
|
|
|
|
from tests.conftest import _load_device
|
|
|
|
|
|
def _dryer():
|
|
resources = _load_device('dryer')
|
|
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 = _dryer()
|
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
|
return flatten(bound, resources)
|
|
|
|
|
|
def test_dryer_model_resolves_to_dryer_registry():
|
|
reg, _ = _dryer()
|
|
assert reg is not None and reg.name == 'dryer'
|
|
|
|
|
|
def test_no_unbound_hrefs():
|
|
"""Every resource in the issue #14 dump binds or is ignored -- clears the
|
|
coverage-gap repair."""
|
|
reg, resources = _dryer()
|
|
unbound = []
|
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
|
assert unbound == []
|
|
|
|
|
|
def test_expected_entities_present():
|
|
state = _state()
|
|
for key in ('buzzer_sound', 'cycle', 'power_switch', 'child_lock',
|
|
'remote_control', 'dry_level', 'wrinkle_prevent', 'energy_kwh'):
|
|
assert key in state, key
|
|
|
|
|
|
def test_job_beginning_status_reads_current_status():
|
|
"""The dump carries x.com.samsung.da.currentStatus (not the old
|
|
jobBeginingStatus field the dryer descriptor used to read), so the sensor
|
|
is populated instead of blank."""
|
|
assert _state()['job_beginning_status'] == 'None' # the dump's currentStatus value
|
|
|
|
|
|
def test_power_watts_gated_for_dead_sentinel():
|
|
"""instantaneousPower is the dead '-500' sentinel on this dryer, so the
|
|
misleading 0 W power sensor is suppressed; cumulative energy still shows."""
|
|
state = _state()
|
|
assert 'power_watts' not in state
|
|
assert 'energy_kwh' in state
|
|
|
|
|
|
def test_course_bound_to_shared_course_vs_0():
|
|
"""Dryer course uses the shared /course/vs/0 cycle select with
|
|
dryer_cycle_table_03 translations, consistent with washer/dishwasher --
|
|
gated on the device's own course table matching Table_03, the only one
|
|
dryer_cycle_table_03's names were confirmed against (see
|
|
laundry.cycle_select)."""
|
|
assert dryer.DRYER_COURSE.href == '/course/vs/0'
|
|
desc = next(e for e in dryer.DRYER_COURSE.entities if e.key == 'cycle')
|
|
assert callable(desc.translation_key)
|
|
validated = {'/st/dryercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_03'}}
|
|
assert desc.translation_key(validated) == 'dryer_cycle_table_03'
|
|
assert desc.translation_key({}) is None
|
|
assert desc.options is laundry.cycle_options
|
|
rep = {'x.com.samsung.da.options': ['Course_16', 'GMT_02']}
|
|
assert desc.rep_fn(rep) == '16'
|
|
|
|
|
|
def test_st_dryercourse_is_ignored():
|
|
"""/st/dryercourse/vs/0 re-encodes the course exposed via /course/vs/0 and
|
|
is globally ignored -- the mirror of /st/washercourse/vs/0."""
|
|
ignored_hrefs = {c.href for c in ignored.IGNORED}
|
|
assert '/st/dryercourse/vs/0' in ignored_hrefs
|
|
assert '/st/washercourse/vs/0' in ignored_hrefs
|