From 8d1ecb4f2af2925c0f9597ddd32103bdb81e2135 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Wed, 12 Aug 2026 15:25:54 +0000 Subject: [PATCH] laundry: add Table_00 cycle labels for WF45R6300 washer and DVE45R6300 dryer Adds washer_cycle_table_00 and dryer_cycle_table_00 translation catalog entries, confirmed by the issue #357 reporter selecting each cycle on a WF45R6300AW/US washer and DVE45R6300W/A3 dryer and reading back the raw course code. Table_00 is a separate, older course-code family from the existing Table_02/Table_03 catalogs -- laundry.cycle_select's table_href scoping already keeps them apart, so this is a translations-only change. Table_00 was previously used only as an example of an unconfirmed table in tests; those now use Table_99 for that role, and new tests assert the confirmed codes translate and that the resolved key routes to the new table-scoped catalog entries. Mirrored to all shipped languages (cs/de/es/it/ko/nl) to keep tests/test_translations.py's key-for-key invariant. --- .../registry/capabilities/dryer.py | 7 ++++ .../registry/capabilities/washer.py | 9 ++++++ .../localthings/translations/cs.json | 32 +++++++++++++++++++ .../localthings/translations/de.json | 32 +++++++++++++++++++ .../localthings/translations/en.json | 32 +++++++++++++++++++ .../localthings/translations/es.json | 32 +++++++++++++++++++ .../localthings/translations/it.json | 32 +++++++++++++++++++ .../localthings/translations/ko.json | 32 +++++++++++++++++++ .../localthings/translations/nl.json | 32 +++++++++++++++++++ tests/test_dryer_capabilities.py | 14 ++++++++ tests/test_laundry_capabilities.py | 2 +- tests/test_washer_capabilities.py | 21 +++++++++--- 12 files changed, 271 insertions(+), 6 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/dryer.py b/custom_components/localthings/registry/capabilities/dryer.py index 8f2b988..edcfd52 100644 --- a/custom_components/localthings/registry/capabilities/dryer.py +++ b/custom_components/localthings/registry/capabilities/dryer.py @@ -46,6 +46,13 @@ DRYER_SETTINGS = Capability( # (issue #244). /st/dryercourse/vs/0 re-encodes the same selected course # and is ignored (ignored.py), mirroring /st/washercourse/vs/0 for washers. # +# dryer_cycle_table_00 is a separate, older course-code family reported by +# a DVE45R6300W/A3 (issue #357), confirmed the same way: the reporter +# selected each cycle on the appliance and read back the resulting raw +# code. It shares no codes with Table_03 above -- 'a5' Bedding here and +# '01' Normal are both table-scoped, so a Table_03 dryer never picks up a +# Table_00 label or vice versa (see laundry.cycle_select's table_href). +# # Drum Clean+ maintenance tracking (issue #258) reuses washer.py's # DrumCleanProposal_/WashingTimes_/DrumCleanLog_ tokens on this same # options[] array -- see laundry.drum_clean_cycles_remaining/ diff --git a/custom_components/localthings/registry/capabilities/washer.py b/custom_components/localthings/registry/capabilities/washer.py index 4e3f910..81be9d8 100644 --- a/custom_components/localthings/registry/capabilities/washer.py +++ b/custom_components/localthings/registry/capabilities/washer.py @@ -58,6 +58,15 @@ from .laundry import ( # the owner or device metadata falls back to washer_cycle_fallback, which # surfaces a personal-course name only -- no invented English label for an # unrecognized standard code (PR #251 review). +# +# washer_cycle_table_00 (issue #357) is a separate, older course-code family +# reported by a WF45R6300AW/US -- confirmed by the reporter selecting each +# cycle on the appliance and reading back the raw code, the same method used +# for Table_02's WF50A8600AV/US codes above. A device reporting Table_00 with +# an unconfirmed code (FlexWash's washer_flexwash_device fixture, for +# instance) still renders that code raw rather than borrowing a Table_02 +# label -- the two tables are unrelated code spaces despite a handful of +# overlapping hex values. # --------------------------------------------------------------------------- # /washer/vs/0 -- wash temperature, spin speed, rinse cycle count. diff --git a/custom_components/localthings/translations/cs.json b/custom_components/localthings/translations/cs.json index 00b9d40..9873b61 100644 --- a/custom_components/localthings/translations/cs.json +++ b/custom_components/localthings/translations/cs.json @@ -324,6 +324,22 @@ "4": "Alarm 4" } }, + "dryer_cycle_table_00": { + "name": "Cyklus", + "state": { + "01": "Normální", + "9c": "Intenzivní", + "a5": "Ložní prádlo", + "9e": "Nežehlivé prádlo", + "9b": "Parní dezinfekce+", + "27": "Osvěžení", + "a0": "Provětrání", + "a4": "Časové sušení", + "a6": "Rychlé sušení", + "a3": "Sportovní oblečení", + "a2": "Jemné prádlo" + } + }, "dryer_cycle_table_03": { "name": "Cyklus", "state": { @@ -604,6 +620,22 @@ "extra_hot": "Extra horká" } }, + "washer_cycle_table_00": { + "name": "Cyklus", + "state": { + "01": "Normální", + "70": "Intenzivní", + "55": "Bílé prádlo", + "71": "Ložní prádlo", + "72": "Dezinfekce", + "77": "Nežehlivé prádlo", + "57": "Samočištění+", + "73": "Máchání + odstřeďování", + "74": "Sportovní oblečení", + "75": "Jemné prádlo", + "78": "Rychlé praní" + } + }, "washer_cycle_table_02": { "name": "Cyklus", "state": { diff --git a/custom_components/localthings/translations/de.json b/custom_components/localthings/translations/de.json index 4c493ed..649bce1 100644 --- a/custom_components/localthings/translations/de.json +++ b/custom_components/localthings/translations/de.json @@ -324,6 +324,22 @@ "4": "Alarm 4" } }, + "dryer_cycle_table_00": { + "name": "Programm", + "state": { + "01": "Normal", + "9c": "Intensiv", + "a5": "Bettwäsche", + "9e": "Pflegeleicht", + "9b": "Dampf-Hygiene+", + "27": "Auffrischen", + "a0": "Lüften", + "a4": "Zeittrocknen", + "a6": "Schnelltrocknen", + "a3": "Sportkleidung", + "a2": "Feinwäsche" + } + }, "dryer_cycle_table_03": { "name": "Programm", "state": { @@ -567,6 +583,22 @@ "extra_hot": "Extra heiß" } }, + "washer_cycle_table_00": { + "name": "Programm", + "state": { + "01": "Normal", + "70": "Intensiv", + "55": "Weißwäsche", + "71": "Bettwäsche", + "72": "Hygienespülung", + "77": "Pflegeleicht", + "57": "Selbstreinigung+", + "73": "Spülen + Schleudern", + "74": "Sportkleidung", + "75": "Feinwäsche", + "78": "Schnellwäsche" + } + }, "washer_cycle_table_02": { "name": "Programm", "state": { diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 0602fe5..b4e96c8 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -324,6 +324,22 @@ "4": "Alarm 4" } }, + "dryer_cycle_table_00": { + "name": "Cycle", + "state": { + "01": "Normal", + "9c": "Heavy Duty", + "a5": "Bedding", + "9e": "Perm Press", + "9b": "Steam Sanitize+", + "27": "Refresh", + "a0": "Air Fluff", + "a4": "Time Dry", + "a6": "Quick Dry", + "a3": "Active Wear", + "a2": "Delicates" + } + }, "dryer_cycle_table_03": { "name": "Cycle", "state": { @@ -604,6 +620,22 @@ "extra_hot": "Extra hot" } }, + "washer_cycle_table_00": { + "name": "Cycle", + "state": { + "01": "Normal", + "70": "Heavy Duty", + "55": "Whites", + "71": "Bedding", + "72": "Sanitize", + "77": "Perm Press", + "57": "Self Clean+", + "73": "Rinse + Spin", + "74": "Active Wear", + "75": "Delicates", + "78": "Quick Wash" + } + }, "washer_cycle_table_02": { "name": "Cycle", "state": { diff --git a/custom_components/localthings/translations/es.json b/custom_components/localthings/translations/es.json index 81c3be4..05bbe46 100644 --- a/custom_components/localthings/translations/es.json +++ b/custom_components/localthings/translations/es.json @@ -517,6 +517,22 @@ "4": "Alarma 4" } }, + "dryer_cycle_table_00": { + "name": "Ciclo", + "state": { + "01": "Normal", + "9c": "Servicio intensivo", + "a5": "Ropa de cama", + "9e": "Planchado fácil", + "9b": "Desinfección por vapor+", + "27": "Renovar", + "a0": "Aireación", + "a4": "Secado por tiempo", + "a6": "Secado rápido", + "a3": "Ropa deportiva", + "a2": "Delicados" + } + }, "dryer_cycle_table_03": { "name": "Ciclo", "state": { @@ -797,6 +813,22 @@ "extra_hot": "Muy caliente" } }, + "washer_cycle_table_00": { + "name": "Ciclo", + "state": { + "01": "Normal", + "70": "Servicio intensivo", + "55": "Blancos", + "71": "Ropa de cama", + "72": "Desinfección", + "77": "Planchado fácil", + "57": "Autolimpieza+", + "73": "Aclarar + Centrifugar", + "74": "Ropa deportiva", + "75": "Delicados", + "78": "Lavado rápido" + } + }, "washer_cycle_table_02": { "name": "Ciclo", "state": { diff --git a/custom_components/localthings/translations/it.json b/custom_components/localthings/translations/it.json index 52378be..f96e426 100644 --- a/custom_components/localthings/translations/it.json +++ b/custom_components/localthings/translations/it.json @@ -324,6 +324,22 @@ "4": "Allarme 4" } }, + "dryer_cycle_table_00": { + "name": "Ciclo", + "state": { + "01": "Normale", + "9c": "Intenso", + "a5": "Biancheria da letto", + "9e": "Pronto da stirare", + "9b": "Igienizzante a vapore+", + "27": "Rinfresca", + "a0": "Arieggiatura", + "a4": "Asciugatura a tempo", + "a6": "Asciugatura rapida", + "a3": "Abbigliamento sportivo", + "a2": "Delicati" + } + }, "dryer_cycle_table_03": { "name": "Ciclo", "state": { @@ -604,6 +620,22 @@ "extra_hot": "Extra caldo" } }, + "washer_cycle_table_00": { + "name": "Ciclo", + "state": { + "01": "Normale", + "70": "Intenso", + "55": "Bianchi", + "71": "Biancheria da letto", + "72": "Igienizzante", + "77": "Pronto da stirare", + "57": "Self Clean+", + "73": "Risciacquo+Centrifuga", + "74": "Abbigliamento sportivo", + "75": "Delicati", + "78": "Lavaggio rapido" + } + }, "washer_cycle_table_02": { "name": "Ciclo", "state": { diff --git a/custom_components/localthings/translations/ko.json b/custom_components/localthings/translations/ko.json index e04d70e..c6230ff 100644 --- a/custom_components/localthings/translations/ko.json +++ b/custom_components/localthings/translations/ko.json @@ -324,6 +324,22 @@ "4": "알림음 4" } }, + "dryer_cycle_table_00": { + "name": "코스", + "state": { + "01": "표준건조", + "9c": "강력건조", + "a5": "이불", + "9e": "구김방지", + "9b": "스팀살균+", + "27": "리프레시", + "a0": "송풍", + "a4": "시간건조", + "a6": "쾌속건조", + "a3": "운동복", + "a2": "섬세의류" + } + }, "dryer_cycle_table_03": { "name": "코스", "state": { @@ -604,6 +620,22 @@ "extra_hot": "고온수" } }, + "washer_cycle_table_00": { + "name": "코스", + "state": { + "01": "표준세탁", + "70": "강력세탁", + "55": "흰옷", + "71": "이불", + "72": "살균", + "77": "구김방지", + "57": "통세척+", + "73": "헹굼+탈수", + "74": "운동복", + "75": "섬세의류", + "78": "쾌속세탁" + } + }, "washer_cycle_table_02": { "name": "코스", "state": { diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index d9ea055..12875d3 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -324,6 +324,22 @@ "4": "Alarm 4" } }, + "dryer_cycle_table_00": { + "name": "Programma", + "state": { + "01": "Normaal", + "9c": "Intensief", + "a5": "Beddengoed", + "9e": "Strijkvrij", + "9b": "Stoomhygiëne+", + "27": "Opfrissen", + "a0": "Luchtdrogen", + "a4": "Tijdprogramma", + "a6": "Snel drogen", + "a3": "Sportkleding", + "a2": "Fijne was" + } + }, "dryer_cycle_table_03": { "name": "Programma", "state": { @@ -604,6 +620,22 @@ "extra_hot": "Extra heet" } }, + "washer_cycle_table_00": { + "name": "Programma", + "state": { + "01": "Normaal", + "70": "Intensief", + "55": "Witte was", + "71": "Beddengoed", + "72": "Hygiëne", + "77": "Strijkvrij", + "57": "Self Clean+", + "73": "Spoelen + centrifugeren", + "74": "Sportkleding", + "75": "Fijne was", + "78": "Snelle was" + } + }, "washer_cycle_table_02": { "name": "Programma", "state": { diff --git a/tests/test_dryer_capabilities.py b/tests/test_dryer_capabilities.py index a6d3ee4..4bdd19d 100644 --- a/tests/test_dryer_capabilities.py +++ b/tests/test_dryer_capabilities.py @@ -89,6 +89,20 @@ def test_course_bound_to_shared_course_vs_0(): assert desc.rep_fn(rep) == "16" +def test_reported_table_00_course_codes_are_translated(): + """The reporter confirmed these codes on a DVE45R6300W/A3 by selecting + each cycle and reading back the raw course code (issue #357).""" + from custom_components.localthings.catalog import translated_states + + desc = next( + e for e in dryer.DRYER_COURSE.entities if e.key == "cycle" and isinstance(e, SelectDesc) + ) + table_00 = {"/st/dryercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_00"}} + assert desc.translation_key(table_00) == "dryer_cycle_table_00" + confirmed = {"01", "9c", "a5", "9e", "9b", "27", "a0", "a4", "a6", "a3", "a2"} + assert confirmed <= translated_states("select", "dryer_cycle_table_00") + + 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.""" diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 05322fe..ecdba3b 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -369,7 +369,7 @@ class TestCycleSelectTableGating: def test_untranslated_table_uses_generic_cycle_key(self): """An unknown table does not claim another board's state labels.""" desc = self._desc() - resources = {"/st/washercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_00"}} + resources = {"/st/washercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_99"}} assert desc.translation_key(resources) == "cycle" def test_resolves_to_generic_cycle_when_table_id_is_unknown(self): diff --git a/tests/test_washer_capabilities.py b/tests/test_washer_capabilities.py index 9eb92e9..71ddcb9 100644 --- a/tests/test_washer_capabilities.py +++ b/tests/test_washer_capabilities.py @@ -99,16 +99,18 @@ class TestWasherCourse: def test_translation_key(self): """Table-scoped (issue: course codes aren't guaranteed consistent - across board generations sharing /course/vs/0 -- FlexWash's older - board reports Table_00, not the Table_02 every washer_cycle_table_02 - name was confirmed against) -- see laundry.cycle_select. Only a - verified table gets table-specific state translations.""" + across board generations sharing /course/vs/0 -- a device reporting + an unrecognized table id must not borrow another board generation's + labels) -- see laundry.cycle_select. Only a verified table gets + table-specific state translations.""" desc = next(e for e in washer.WASHER_COURSE.entities if e.key == "cycle") assert callable(desc.translation_key) table_02 = {"/st/washercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_02"}} assert desc.translation_key(table_02) == "washer_cycle_table_02" table_00 = {"/st/washercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_00"}} - assert desc.translation_key(table_00) == "cycle" + assert desc.translation_key(table_00) == "washer_cycle_table_00" + unrecognized = {"/st/washercourse/vs/0": {"x.com.samsung.da.st.courseTable": "Table_99"}} + assert desc.translation_key(unrecognized) == "cycle" assert desc.translation_key({}) == "cycle" def test_reads_raw_course_code_from_options_array(self): @@ -146,6 +148,15 @@ class TestWasherCourse: } assert confirmed <= translated_states("select", "washer_cycle_table_02") + def test_reported_table_00_course_codes_are_translated(self): + """The reporter confirmed these codes on a WF45R6300AW/US by + selecting each cycle and reading back the raw course code + (issue #357).""" + from custom_components.localthings.catalog import translated_states + + confirmed = {"01", "70", "55", "71", "72", "77", "57", "73", "74", "75", "78"} + assert confirmed <= translated_states("select", "washer_cycle_table_00") + def test_missing_course_option_returns_none(self): desc = next(e for e in washer.WASHER_COURSE.entities if e.key == "cycle") assert desc.rep_fn is not None