From 35f76ce2dffa5c850fcf3a666522d51f54a8b0e2 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Sat, 18 Jul 2026 18:24:10 +0000 Subject: [PATCH] feat(washer): add label translations for detergent/softener dosing selects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assumes the LevelCtrl code scheme is None/Low/Medium/High (00-03) on both dispensers -- code 00 has no on-screen equivalent in the app's 3-choice Faible/Moyen/Élevé picker, assumed to be what "Activation" off collapses to -- and Level2Ctrl is Soft/Medium/Hard for detergent water hardness, 1x/2x/3x for softener concentration. detergent_quantity and softener_quantity share one translation_key (same vocabulary), same pattern as fridge.py's shared 'brightness_level' key. Not cross-device verified: only one dump + screenshot set (issue #9) to go on, and the softener concentration reading doesn't cleanly match its screenshot (assumed to be a setting changed between dump and screenshots, not a different code scheme -- see the comment in washer.py). --- .../registry/capabilities/washer.py | 25 +++++++++++++++---- custom_components/localthings/strings.json | 22 ++++++++++++++++ .../localthings/translations/en.json | 22 ++++++++++++++++ tests/test_washer_capabilities.py | 10 ++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/washer.py b/custom_components/localthings/registry/capabilities/washer.py index f6617ca..f7cebab 100644 --- a/custom_components/localthings/registry/capabilities/washer.py +++ b/custom_components/localthings/registry/capabilities/washer.py @@ -164,11 +164,22 @@ def _drum_clean_last_cleaned(rep): # valid raw codes for its field, same hex-pair shape as EditCourseList. # 'Alarm_' is a low-reservoir warning flag. # -# Raw codes are shown as-is (no translation_key): unlike Course_XX, there's -# no code->label mapping cross-referenced against multiple devices yet -- -# just a single dump's DetergentLevelCtrl_3/SoftenerLevelCtrl_3 plausibly -# matching that same report's screenshot of "Élevé" (High), which isn't -# enough to commit a full 00-03 mapping to strings.json. +# Label mapping (entity.select.washer_dosing_quantity/washer_detergent_ +# water_hardness/washer_softener_concentration in strings.json) is an +# assumed, not cross-device-verified, reading of the single issue #9 dump + +# screenshots: LevelCtrl's 4 codes as None/Low/Medium/High (00 has no +# on-screen equivalent -- the app's Quantité picker only offers +# Faible/Moyen/Élevé, i.e. codes 01-03; 00 is assumed to be what +# "Activation" off collapses to) matches DetergentLevelCtrl_3/ +# SoftenerLevelCtrl_3 = "Élevé" on both dispensers. Level2Ctrl's 3 codes as +# Soft/Medium/Hard for detergent (Dureté de l'eau: Douce/Moyenne/Dure) +# matches DetergentLevel2Ctrl_2 = "Moyenne". The same 3-code shape as +# 1x/2x/3x for softener concentration does *not* cleanly match +# SoftenerLevel2Ctrl_2 against the screenshot's "3x" -- assumed to be a +# setting the user changed in the app between the dump (issue body) and the +# screenshots (a later comment), not a different code scheme, since it's +# otherwise identical in shape to the detergent side. Revisit if a second +# device's dump contradicts this. def _supported_level_options(resources, prefix): rep = resources.get('/course/vs/0') or {} raw = _option_value(rep.get('x.com.samsung.da.options'), f'Supported{prefix}') @@ -221,6 +232,7 @@ WASHER_COURSE = Capability( exists_fn=lambda rep, resources: _drum_clean_last_cleaned(rep) is not None, rep_fn=_drum_clean_last_cleaned), SelectDesc(key='detergent_quantity', name='Detergent quantity', icon='mdi:cup-water', + translation_key='washer_dosing_quantity', entity_category='config', options=_level_options('DetergentLevelCtrl'), exists_fn=lambda rep, resources: bool( @@ -230,6 +242,7 @@ WASHER_COURSE = Capability( write_fn=_level_write('DetergentLevelCtrl')), SelectDesc(key='detergent_water_hardness', name='Detergent water hardness', icon='mdi:water-opacity', + translation_key='washer_detergent_water_hardness', entity_category='config', options=_level_options('DetergentLevel2Ctrl'), exists_fn=lambda rep, resources: bool( @@ -238,6 +251,7 @@ WASHER_COURSE = Capability( rep.get('x.com.samsung.da.options'), 'DetergentLevel2Ctrl'), write_fn=_level_write('DetergentLevel2Ctrl')), SelectDesc(key='softener_quantity', name='Softener quantity', icon='mdi:flask-outline', + translation_key='washer_dosing_quantity', entity_category='config', options=_level_options('SoftenerLevelCtrl'), exists_fn=lambda rep, resources: bool( @@ -247,6 +261,7 @@ WASHER_COURSE = Capability( write_fn=_level_write('SoftenerLevelCtrl')), SelectDesc(key='softener_concentration', name='Softener concentration', icon='mdi:flask-plus-outline', + translation_key='washer_softener_concentration', entity_category='config', options=_level_options('SoftenerLevel2Ctrl'), exists_fn=lambda rep, resources: bool( diff --git a/custom_components/localthings/strings.json b/custom_components/localthings/strings.json index 4a41e15..98cd5ff 100644 --- a/custom_components/localthings/strings.json +++ b/custom_components/localthings/strings.json @@ -78,6 +78,28 @@ "8f": "Intense Cold", "96": "Less Microfiber" } + }, + "washer_dosing_quantity": { + "state": { + "00": "None", + "01": "Low", + "02": "Medium", + "03": "High" + } + }, + "washer_detergent_water_hardness": { + "state": { + "01": "Soft", + "02": "Medium", + "03": "Hard" + } + }, + "washer_softener_concentration": { + "state": { + "01": "1x", + "02": "2x", + "03": "3x" + } } }, "sensor": { diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 4a41e15..98cd5ff 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -78,6 +78,28 @@ "8f": "Intense Cold", "96": "Less Microfiber" } + }, + "washer_dosing_quantity": { + "state": { + "00": "None", + "01": "Low", + "02": "Medium", + "03": "High" + } + }, + "washer_detergent_water_hardness": { + "state": { + "01": "Soft", + "02": "Medium", + "03": "Hard" + } + }, + "washer_softener_concentration": { + "state": { + "01": "1x", + "02": "2x", + "03": "3x" + } } }, "sensor": { diff --git a/tests/test_washer_capabilities.py b/tests/test_washer_capabilities.py index 6c3875d..378fa1c 100644 --- a/tests/test_washer_capabilities.py +++ b/tests/test_washer_capabilities.py @@ -185,6 +185,16 @@ class TestDetergentSoftenerDosing: assert self._desc('softener_quantity').rep_fn(rep) == '3' assert self._desc('softener_concentration').rep_fn(rep) == '2' + def test_translation_keys(self): + """detergent_quantity and softener_quantity share one translation_key + (same 00-03 -> None/Low/Medium/High vocabulary on both dispensers, + same shape as fridge.py's shared 'brightness_level' key); hardness + and concentration each have their own since their labels differ.""" + assert self._desc('detergent_quantity').translation_key == 'washer_dosing_quantity' + assert self._desc('softener_quantity').translation_key == 'washer_dosing_quantity' + assert self._desc('detergent_water_hardness').translation_key == 'washer_detergent_water_hardness' + assert self._desc('softener_concentration').translation_key == 'washer_softener_concentration' + def test_quantity_and_hardness_options_decode_supported_list(self): assert self._desc('detergent_quantity').options(_DOSING_RESOURCES) == ['00', '01', '02', '03'] assert self._desc('softener_quantity').options(_DOSING_RESOURCES) == ['00', '01', '02', '03']