Scope washer/dryer cycle translations to the device's own course table

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.
This commit is contained in:
Marc Billow committed 2026-07-24 05:42:12 +00:00
1 parent b2c0517115
commit b2c32a90b6
12 files changed
+124 -27

No files matched your search

+9 -3
View File
@@ -59,11 +59,17 @@ def test_power_watts_gated_for_dead_sentinel():
def test_course_bound_to_shared_course_vs_0():
"""Dryer course uses the shared /course/vs/0 cycle select with dryer_cycle
translations, consistent with washer/dishwasher."""
"""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 desc.translation_key == 'dryer_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'
+40
View File
@@ -145,6 +145,46 @@ class TestCycleSelect:
assert desc.write_fn('1D', {}) is None
class TestCycleSelectTableGating:
"""translation_key becomes a resolver, not a plain string, once
table_href/validated_table are given -- washer/dryer's real call sites
(issue: course codes aren't guaranteed consistent across board
generations sharing the same /course/vs/0 contract; FlexWash's older
board reports a different course table than every device the shipped
translations were confirmed against)."""
def _desc(self):
return laundry.cycle_select(
translation_key='washer_cycle_table_02', icon='x',
table_href='/st/washercourse/vs/0', validated_table='Table_02',
)
def test_static_string_when_no_table_params_given(self):
"""dishwasher's call site -- no equivalent table-id resource in any
dump seen, no evidence of the same cross-board risk -- keeps the
plain static key unconditionally."""
desc = laundry.cycle_select(translation_key='dishwasher_cycle', icon='x')
assert desc.translation_key == 'dishwasher_cycle'
def test_resolves_to_the_key_when_table_matches(self):
desc = self._desc()
resources = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_02'}}
assert callable(desc.translation_key)
assert desc.translation_key(resources) == 'washer_cycle_table_02'
def test_resolves_to_none_for_a_different_table(self):
desc = self._desc()
resources = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_00'}}
assert desc.translation_key(resources) is None
def test_resolves_to_none_when_table_id_is_unknown(self):
"""Absence isn't evidence of a match -- no href, or an empty rep,
gets no translation_key either, not the family default."""
desc = self._desc()
assert desc.translation_key({}) is None
assert desc.translation_key({'/st/washercourse/vs/0': {}}) is None
class TestBuzzerSound:
def test_href(self):
assert laundry.BUZZER_SOUND.href == '/buzzersound/vs/0'
+2 -3
View File
@@ -1,9 +1,8 @@
"""Tests for select-option display casing (custom_components/localthings/select.py)."""
from custom_components.localthings.registry.entities import SelectDesc
from custom_components.localthings.select import _display
_UNTRANSLATED = SelectDesc(key='x', options=())
_TRANSLATED = SelectDesc(key='y', options=(), translation_key='door_alert')
_UNTRANSLATED = None
_TRANSLATED = 'door_alert'
def test_display_titlecases_a_fully_lowercase_device_native_token():
+9 -1
View File
@@ -61,8 +61,16 @@ class TestWasherCourse:
assert washer.WASHER_COURSE.href == '/course/vs/0'
def test_translation_key(self):
"""Table-gated (issue: FlexWash's older board reports a different
course table, Table_00, than every device washer_cycle_table_02's
names were confirmed against, Table_02) -- see laundry.cycle_select."""
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == 'cycle')
assert desc.translation_key == 'washer_cycle'
assert callable(desc.translation_key)
validated = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_02'}}
assert desc.translation_key(validated) == 'washer_cycle_table_02'
other_table = {'/st/washercourse/vs/0': {'x.com.samsung.da.st.courseTable': 'Table_00'}}
assert desc.translation_key(other_table) is None
assert desc.translation_key({}) is None
def test_reads_raw_course_code_from_options_array(self):
"""rep_fn returns the raw device code; display names come from