From b2db4bf35796b7bbedc640fa100e7bd2ad2f8c08 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:23:53 +0200 Subject: [PATCH 01/16] feat: add completion time and minutes sensors with parsing logic --- .../registry/capabilities/operational.py | 39 ++++++++++++++++++- tests/test_operational_capability.py | 35 ++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/operational.py b/custom_components/localthings/registry/capabilities/operational.py index 99d853d..7a94b90 100644 --- a/custom_components/localthings/registry/capabilities/operational.py +++ b/custom_components/localthings/registry/capabilities/operational.py @@ -70,6 +70,34 @@ def _finish_time(remaining_str): return None +def _parse_remaining_time(rep): + return ( + rep.get('x.com.samsung.da.remainingTime') + or rep.get('remainingTime') + ) + + +def _completion_time(rep): + return _parse_remaining_time(rep) + + +def _completion_minutes(rep): + raw = _parse_remaining_time(rep) + if not raw or not isinstance(raw, str): + return None + try: + parts = [int(p) for p in raw.split(':')] + if len(parts) == 3: + h, m, s = parts + return h * 60 + m + (1 if s > 0 else 0) + elif len(parts) == 2: + m, s = parts + return m + (1 if s > 0 else 0) + except (ValueError, TypeError): + pass + return None + + # Shared by dryer/dishwasher/oven/washer -- oven.py imports this directly # rather than keeping its own copy, since both wrote the identical # state='Ready' RMW. @@ -123,6 +151,15 @@ OPERATIONAL_STATE = Capability( or rep.get('x.com.samsung.da.progress') == 'Finish' else _finish_time(rep.get('x.com.samsung.da.remainingTime')) )), + SensorDesc(key='completion_time', name='Completion time', + icon='mdi:timer-sand', + exists_fn=lambda rep, resources: _completion_time(rep) is not None, + rep_fn=_completion_time), + SensorDesc(key='completion_minutes', name='Remaining minutes', + icon='mdi:clock-outline', unit='min', + device_class='duration', state_class='measurement', + exists_fn=lambda rep, resources: _completion_minutes(rep) is not None, + rep_fn=_completion_minutes), NumberDesc(key='delay_start_hours', name='Delay start', icon='mdi:timer-plus-outline', device_class='duration', unit='h', native_min=0, native_max=24, step=1, @@ -144,4 +181,4 @@ OPERATIONAL_STATE = Capability( {'x.com.samsung.da.state': p})), STOP_BUTTON, ), -) +) \ No newline at end of file diff --git a/tests/test_operational_capability.py b/tests/test_operational_capability.py index 992467f..3ac48fe 100644 --- a/tests/test_operational_capability.py +++ b/tests/test_operational_capability.py @@ -24,6 +24,39 @@ class TestProgressPercentage: assert desc.rep_fn(rep) == 42 +class TestCompletionTime: + """Unit tests for completion_time and completion_minutes entities.""" + + def test_completion_time_string(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_time') + rep = {'x.com.samsung.da.remainingTime': '01:25:30'} + assert desc.rep_fn(rep) == '01:25:30' + + def test_completion_time_fallback(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_time') + rep = {'remainingTime': '00:45:00'} + assert desc.rep_fn(rep) == '00:45:00' + + def test_completion_minutes_parsing(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') + + # 1 hour 25 mins 30 secs -> 85 mins + 1 sec ceiling = 86 mins + rep = {'x.com.samsung.da.remainingTime': '01:25:30'} + assert desc.rep_fn(rep) == 86 + + # Exact minutes: 1 hour 30 mins 00 secs -> 90 mins + rep_exact = {'x.com.samsung.da.remainingTime': '01:30:00'} + assert desc.rep_fn(rep_exact) == 90 + + def test_completion_time_missing_or_invalid(self): + desc_time = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_time') + desc_min = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') + + rep = {} + assert desc_time.rep_fn(rep) is None + assert desc_min.rep_fn(rep) is None + + class TestDelayFieldFallback: def test_reads_delay_end_time_when_delay_start_time_absent(self): from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE @@ -53,4 +86,4 @@ class TestDelayFieldFallback: desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'delay_start_hours') rep = {'x.com.samsung.da.delayStartTime': '00:00:00'} path, body = desc.write_fn(1.5, rep) - assert body == {'x.com.samsung.da.delayStartTime': '1:30:00'} + assert body == {'x.com.samsung.da.delayStartTime': '1:30:00'} \ No newline at end of file From e913a3c2c0f26c2e6ffa12477b3af69ff6435628 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:26:24 +0200 Subject: [PATCH 02/16] feat: add new washer cycle options for AI Wash and Jeans --- custom_components/localthings/strings.json | 4 +++- custom_components/localthings/translations/en.json | 4 +++- tests/test_laundry_capabilities.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/localthings/strings.json b/custom_components/localthings/strings.json index b3e0026..df01a1d 100644 --- a/custom_components/localthings/strings.json +++ b/custom_components/localthings/strings.json @@ -100,7 +100,9 @@ "37": "Air Wash", "38": "Cotton Dry", "39": "Synthetics Dry", - "1f": "Intense Cold" + "1f": "Intense Cold", + "2b": "AI Wash", + "2c": "Jeans" } }, "dryer_cycle": { diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index b3e0026..df01a1d 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -100,7 +100,9 @@ "37": "Air Wash", "38": "Cotton Dry", "39": "Synthetics Dry", - "1f": "Intense Cold" + "1f": "Intense Cold", + "2b": "AI Wash", + "2c": "Jeans" } }, "dryer_cycle": { diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 2a7d22c..7c3f09a 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -8,6 +8,7 @@ class TestCourseHelpers: assert laundry.parse_edit_course_list(raw) == [ '1C', '1D', '21', '1B', '1E', '29', '24', '33', '28', '26', '27', '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96', + '2b', '2c' ] def test_parse_edit_course_list_handles_missing_or_malformed(self): From b00390c093bf758331312e0de0ff640d433a95d6 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:27:28 +0200 Subject: [PATCH 03/16] fix: correct key for Jeans cycle in strings and translations --- custom_components/localthings/strings.json | 2 +- custom_components/localthings/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localthings/strings.json b/custom_components/localthings/strings.json index df01a1d..8b2762b 100644 --- a/custom_components/localthings/strings.json +++ b/custom_components/localthings/strings.json @@ -102,7 +102,7 @@ "39": "Synthetics Dry", "1f": "Intense Cold", "2b": "AI Wash", - "2c": "Jeans" + "2a": "Jeans" } }, "dryer_cycle": { diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index df01a1d..8b2762b 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -102,7 +102,7 @@ "39": "Synthetics Dry", "1f": "Intense Cold", "2b": "AI Wash", - "2c": "Jeans" + "2a": "Jeans" } }, "dryer_cycle": { From 86f07abf302e1d6c4a9379204eee8b27bc1dfbf0 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:29:53 +0200 Subject: [PATCH 04/16] fix: correct expected output for parse_edit_course_list test case --- tests/test_laundry_capabilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 7c3f09a..47d818d 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -8,7 +8,7 @@ class TestCourseHelpers: assert laundry.parse_edit_course_list(raw) == [ '1C', '1D', '21', '1B', '1E', '29', '24', '33', '28', '26', '27', '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96', - '2b', '2c' + '2b', '2a' ] def test_parse_edit_course_list_handles_missing_or_malformed(self): From f5d4e2219ae6d874a7ab66d444e1b408e7e8ae9e Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:47:53 +0200 Subject: [PATCH 05/16] fix: remove redundant course codes from parse_edit_course_list test case --- tests/test_laundry_capabilities.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 47d818d..8960608 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -7,8 +7,7 @@ class TestCourseHelpers: raw = 'EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F96' assert laundry.parse_edit_course_list(raw) == [ '1C', '1D', '21', '1B', '1E', '29', '24', '33', '28', '26', '27', - '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96', - '2b', '2a' + '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96' ] def test_parse_edit_course_list_handles_missing_or_malformed(self): From 2c283218b0cb5807c4ee7ecf467ee513ffda6880 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:10:17 +0200 Subject: [PATCH 06/16] feat: add completion_minutes and completion_time to state_keys in JSON fixtures --- tests/fixtures/golden/dishwasher.json | 2 ++ tests/fixtures/golden/dryer.json | 2 ++ tests/fixtures/golden/washer.json | 2 ++ tests/fixtures/golden/washer_dryer_combo.json | 2 ++ tests/fixtures/golden/washer_flexwash.json | 2 ++ tests/test_laundry_capabilities.py | 5 +++-- 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/golden/dishwasher.json b/tests/fixtures/golden/dishwasher.json index 2a50531..10c51a0 100644 --- a/tests/fixtures/golden/dishwasher.json +++ b/tests/fixtures/golden/dishwasher.json @@ -3,6 +3,8 @@ "alarm_code", "auto_release_dry", "child_lock", + "completion_minutes", + "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/dryer.json b/tests/fixtures/golden/dryer.json index 97f17c5..2fc8e2d 100644 --- a/tests/fixtures/golden/dryer.json +++ b/tests/fixtures/golden/dryer.json @@ -3,6 +3,8 @@ "alarm_code", "buzzer_sound", "child_lock", + "completion_minutes", + "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/washer.json b/tests/fixtures/golden/washer.json index b48cd1b..d814514 100644 --- a/tests/fixtures/golden/washer.json +++ b/tests/fixtures/golden/washer.json @@ -5,6 +5,8 @@ "bubble_soak", "buzzer_sound", "child_lock", + "completion_minutes", + "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/washer_dryer_combo.json b/tests/fixtures/golden/washer_dryer_combo.json index d2bab38..5cc40fd 100644 --- a/tests/fixtures/golden/washer_dryer_combo.json +++ b/tests/fixtures/golden/washer_dryer_combo.json @@ -3,6 +3,8 @@ "alarm_code", "bubble_soak", "child_lock", + "completion_minutes", + "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/washer_flexwash.json b/tests/fixtures/golden/washer_flexwash.json index 6db1e00..7230480 100644 --- a/tests/fixtures/golden/washer_flexwash.json +++ b/tests/fixtures/golden/washer_flexwash.json @@ -2,6 +2,8 @@ "state_keys": [ "alarm_code", "child_lock", + "completion_minutes", + "completion_time", "cycle_active", "delay_start_hours", "detergent_low", diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 8960608..810f046 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -4,10 +4,11 @@ from custom_components.localthings.registry.capabilities import laundry class TestCourseHelpers: def test_parses_edit_course_list(self): - raw = 'EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F96' + raw = 'EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F962b2a' assert laundry.parse_edit_course_list(raw) == [ '1C', '1D', '21', '1B', '1E', '29', '24', '33', '28', '26', '27', - '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96' + '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96', + '2b', '2a' ] def test_parse_edit_course_list_handles_missing_or_malformed(self): From 365a1c47224d91e4414efc5caacddea39b73d615 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:35:17 +0200 Subject: [PATCH 07/16] feat: enhance operational capabilities by adding completion_minutes and updating related logic; remove completion_time references --- .../registry/capabilities/operational.py | 132 ++++++++++-------- tests/fixtures/golden/dishwasher.json | 1 - tests/fixtures/golden/dryer.json | 1 - tests/fixtures/golden/washer.json | 1 - tests/fixtures/golden/washer_dryer_combo.json | 1 - tests/fixtures/golden/washer_flexwash.json | 1 - tests/test_laundry_capabilities.py | 4 +- tests/test_operational_capability.py | 39 +++--- 8 files changed, 95 insertions(+), 85 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/operational.py b/custom_components/localthings/registry/capabilities/operational.py index 7a94b90..4db8356 100644 --- a/custom_components/localthings/registry/capabilities/operational.py +++ b/custom_components/localthings/registry/capabilities/operational.py @@ -2,7 +2,8 @@ Shared by dryer/dishwasher/oven/washer families. """ -from datetime import datetime, timezone, timedelta +import math +from datetime import datetime, timedelta, timezone from ..capability import Capability from ..entities import BinarySensorDesc, ButtonDesc, NumberDesc, SensorDesc @@ -28,17 +29,41 @@ def _int(v): return None +def _is_active(rep): + """Check if appliance is actively running and cycle is not finished.""" + return ( + _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) == 'active' + and rep.get('x.com.samsung.da.progress') != 'Finish' + ) + + +def _remaining_seconds(raw): + """'HH:MM:SS' (or 'MM:SS') -> total seconds, or None.""" + if not isinstance(raw, str): + return None + try: + parts = [int(p) for p in raw.split(':')] + except (ValueError, TypeError): + return None + + if len(parts) == 3: + h, m, s = parts + elif len(parts) == 2: + h, m, s = 0, *parts + else: + return None + + return h * 3600 + m * 60 + s + + def _delay_hours(v): """delayStartTime is a duration until the cycle starts, not a wall-clock time -- "01:00" means "1 hour from when you press start", not "1 AM".""" - if not v: - return 0.0 - try: - h, m, s = v.split(':') - return int(h) + int(m) / 60 + int(s) / 3600 - except Exception: - return None + total_seconds = _remaining_seconds(v) + if total_seconds is None: + return 0.0 if not v else None + return total_seconds / 3600.0 def _format_delay(hours): @@ -53,59 +78,46 @@ def _delay_field(rep): wall-clock time -- see _delay_hours). Write back whichever key the device itself is using; default to delayStartTime for hardware that reports neither yet (matches prior behavior).""" - return ('x.com.samsung.da.delayEndTime' if 'x.com.samsung.da.delayEndTime' in rep - else 'x.com.samsung.da.delayStartTime') - - -def _finish_time(remaining_str): - if not remaining_str: - return None - try: - h, m, s = remaining_str.split(':') - total_s = int(h) * 3600 + int(m) * 60 + int(s) - if total_s == 0: - return None - return datetime.now(timezone.utc) + timedelta(seconds=total_s) - except Exception: - return None - - -def _parse_remaining_time(rep): return ( - rep.get('x.com.samsung.da.remainingTime') - or rep.get('remainingTime') + 'x.com.samsung.da.delayEndTime' if 'x.com.samsung.da.delayEndTime' in rep + else 'x.com.samsung.da.delayStartTime' ) -def _completion_time(rep): - return _parse_remaining_time(rep) +def _finish_time(rep): + if not _is_active(rep): + return None + total_s = _remaining_seconds(rep.get('x.com.samsung.da.remainingTime')) + if not total_s: + return None + return datetime.now(timezone.utc) + timedelta(seconds=total_s) def _completion_minutes(rep): - raw = _parse_remaining_time(rep) - if not raw or not isinstance(raw, str): + """Parse remaining time into minutes directly from device payload.""" + raw = rep.get('x.com.samsung.da.remainingTime') or rep.get('remainingTime') + total_s = _remaining_seconds(raw) + if total_s is None: return None - try: - parts = [int(p) for p in raw.split(':')] - if len(parts) == 3: - h, m, s = parts - return h * 60 + m + (1 if s > 0 else 0) - elif len(parts) == 2: - m, s = parts - return m + (1 if s > 0 else 0) - except (ValueError, TypeError): - pass - return None + + # Avoid the firmware bug where it freezes at 1 minute post-cycle + if rep.get('x.com.samsung.da.progress') == 'Finish': + return 0 + + return math.ceil(total_s / 60) # Shared by dryer/dishwasher/oven/washer -- oven.py imports this directly # rather than keeping its own copy, since both wrote the identical # state='Ready' RMW. -STOP_BUTTON = ButtonDesc(key='stop', field='', name='Stop', payload='Ready', - icon='mdi:stop', - write_fn=lambda p, rep, href=None: ( - ['operational', 'state', 'vs', '0'], - {'x.com.samsung.da.state': p})) +STOP_BUTTON = ButtonDesc( + key='stop', field='', name='Stop', payload='Ready', + icon='mdi:stop', + write_fn=lambda p, rep, href=None: ( + ['operational', 'state', 'vs', '0'], + {'x.com.samsung.da.state': p} + ) +) OPERATIONAL_STATE = Capability( href='/operational/state/vs/0', @@ -115,6 +127,7 @@ OPERATIONAL_STATE = Capability( name='Machine state', device_class='enum', options=('idle', 'active', 'pause'), translation_key='machine_state', value_fn=_to_ocf), + # cycle_active is a bool derived from machine_state; used by the # adapter to gate oven writes (cycle_active_field='cycle_active'). # Harmless for non-oven appliances — just an extra bool in state. @@ -126,40 +139,34 @@ OPERATIONAL_STATE = Capability( # vocabulary that doesn't fit an oven's bake/roast/etc. BinarySensorDesc(key='cycle_active', device_class='running', name='Running', - rep_fn=lambda rep: ( - _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) == 'active' - and rep.get('x.com.samsung.da.progress') != 'Finish' - )), + rep_fn=_is_active), + SensorDesc(key='progress', name='Progress', icon='mdi:progress-wrench', rep_fn=lambda rep: ( 'Idle' if _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) != 'active' else _progress(rep.get('x.com.samsung.da.progress')) )), + SensorDesc(key='progress_percentage', name='Progress percent', unit='%', state_class='measurement', rep_fn=lambda rep: ( 0 if _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) != 'active' else _int(rep.get('x.com.samsung.da.progressPercentage')) )), + # Only show finish time when machine is actively running. Samsung # firmware leaves a stale remainingTime after a cycle ends, and # freezes it at '00:01:00' when progress reaches 'Finish'. SensorDesc(key='finish_time', device_class='timestamp', name='Estimated finish', - rep_fn=lambda rep: ( - None if _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) != 'active' - or rep.get('x.com.samsung.da.progress') == 'Finish' - else _finish_time(rep.get('x.com.samsung.da.remainingTime')) - )), - SensorDesc(key='completion_time', name='Completion time', - icon='mdi:timer-sand', - exists_fn=lambda rep, resources: _completion_time(rep) is not None, - rep_fn=_completion_time), + rep_fn=_finish_time), + SensorDesc(key='completion_minutes', name='Remaining minutes', icon='mdi:clock-outline', unit='min', device_class='duration', state_class='measurement', exists_fn=lambda rep, resources: _completion_minutes(rep) is not None, rep_fn=_completion_minutes), + NumberDesc(key='delay_start_hours', name='Delay start', icon='mdi:timer-plus-outline', device_class='duration', unit='h', native_min=0, native_max=24, step=1, @@ -169,16 +176,19 @@ OPERATIONAL_STATE = Capability( write_fn=lambda p, rep, href=None: ( ['operational', 'state', 'vs', '0'], {_delay_field(rep): _format_delay(p)})), + ButtonDesc(key='start', field='', name='Start', payload='Run', icon='mdi:play', write_fn=lambda p, rep, href=None: ( ['operational', 'state', 'vs', '0'], {'x.com.samsung.da.state': p})), + ButtonDesc(key='pause', field='', name='Pause', payload='Pause', icon='mdi:pause', write_fn=lambda p, rep, href=None: ( ['operational', 'state', 'vs', '0'], {'x.com.samsung.da.state': p})), + STOP_BUTTON, ), ) \ No newline at end of file diff --git a/tests/fixtures/golden/dishwasher.json b/tests/fixtures/golden/dishwasher.json index 10c51a0..6dedaa7 100644 --- a/tests/fixtures/golden/dishwasher.json +++ b/tests/fixtures/golden/dishwasher.json @@ -4,7 +4,6 @@ "auto_release_dry", "child_lock", "completion_minutes", - "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/dryer.json b/tests/fixtures/golden/dryer.json index 2fc8e2d..f0dc9fb 100644 --- a/tests/fixtures/golden/dryer.json +++ b/tests/fixtures/golden/dryer.json @@ -4,7 +4,6 @@ "buzzer_sound", "child_lock", "completion_minutes", - "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/washer.json b/tests/fixtures/golden/washer.json index d814514..ca791c5 100644 --- a/tests/fixtures/golden/washer.json +++ b/tests/fixtures/golden/washer.json @@ -6,7 +6,6 @@ "buzzer_sound", "child_lock", "completion_minutes", - "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/washer_dryer_combo.json b/tests/fixtures/golden/washer_dryer_combo.json index 5cc40fd..da6f20c 100644 --- a/tests/fixtures/golden/washer_dryer_combo.json +++ b/tests/fixtures/golden/washer_dryer_combo.json @@ -4,7 +4,6 @@ "bubble_soak", "child_lock", "completion_minutes", - "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/fixtures/golden/washer_flexwash.json b/tests/fixtures/golden/washer_flexwash.json index ed65018..e100a59 100644 --- a/tests/fixtures/golden/washer_flexwash.json +++ b/tests/fixtures/golden/washer_flexwash.json @@ -3,7 +3,6 @@ "alarm_code", "child_lock", "completion_minutes", - "completion_time", "cycle", "cycle_active", "delay_start_hours", diff --git a/tests/test_laundry_capabilities.py b/tests/test_laundry_capabilities.py index 5fab635..863c05e 100644 --- a/tests/test_laundry_capabilities.py +++ b/tests/test_laundry_capabilities.py @@ -4,11 +4,11 @@ from custom_components.localthings.registry.capabilities import laundry class TestCourseHelpers: def test_parses_edit_course_list(self): - raw = 'EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F962b2a' + raw = 'EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F962B2A' assert laundry.parse_edit_course_list(raw) == [ '1C', '1D', '21', '1B', '1E', '29', '24', '33', '28', '26', '27', '22', '20', '23', '25', '32', '2F', '2E', '30', '66', '2D', '8F', '96', - '2b', '2a' + '2B', '2A' ] def test_parse_edit_course_list_handles_missing_or_malformed(self): diff --git a/tests/test_operational_capability.py b/tests/test_operational_capability.py index 3ac48fe..606efb6 100644 --- a/tests/test_operational_capability.py +++ b/tests/test_operational_capability.py @@ -1,3 +1,5 @@ +"""Unit tests for operational state capabilities.""" + from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE @@ -24,18 +26,8 @@ class TestProgressPercentage: assert desc.rep_fn(rep) == 42 -class TestCompletionTime: - """Unit tests for completion_time and completion_minutes entities.""" - - def test_completion_time_string(self): - desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_time') - rep = {'x.com.samsung.da.remainingTime': '01:25:30'} - assert desc.rep_fn(rep) == '01:25:30' - - def test_completion_time_fallback(self): - desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_time') - rep = {'remainingTime': '00:45:00'} - assert desc.rep_fn(rep) == '00:45:00' +class TestCompletionMinutes: + """Unit tests for completion_minutes entity.""" def test_completion_minutes_parsing(self): desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') @@ -48,13 +40,26 @@ class TestCompletionTime: rep_exact = {'x.com.samsung.da.remainingTime': '01:30:00'} assert desc.rep_fn(rep_exact) == 90 - def test_completion_time_missing_or_invalid(self): - desc_time = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_time') - desc_min = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') + def test_completion_minutes_fallback_key(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') + rep = {'remainingTime': '00:45:00'} + assert desc.rep_fn(rep) == 45 + + def test_completion_minutes_stale_finish_gated(self): + """Firmware freezes remainingTime at '00:01:00' when progress reaches 'Finish'. + Should return 0 to prevent stuck values.""" + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') + rep = { + 'x.com.samsung.da.progress': 'Finish', + 'x.com.samsung.da.remainingTime': '00:01:00', + } + assert desc.rep_fn(rep) == 0 + + def test_completion_minutes_missing_or_invalid(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'completion_minutes') rep = {} - assert desc_time.rep_fn(rep) is None - assert desc_min.rep_fn(rep) is None + assert desc.rep_fn(rep) is None class TestDelayFieldFallback: From 4a91f62a368a826c2acc5971653a15e70dcbd5e3 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:49:40 +0200 Subject: [PATCH 08/16] i18n: add new translations for AI Wash and Spijkerbroek --- custom_components/localthings/translations/nl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 3429868..b008764 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -400,7 +400,9 @@ "7e": "Zelfreinigend", "86": "Diep wassen", "8f": "Intensief koud", - "96": "Minder microvezels" + "96": "Minder microvezels", + "2b": "AI Wash", + "2a": "Spijkerbroek" } }, "washer_dry_level": { From edccb6ae3f41284be1a611889a5f08b8256b81c5 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:59:37 +0200 Subject: [PATCH 09/16] i18n: add new wash options for AI Wash and Jeans --- custom_components/localthings/translations/en.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index a091028..f7e93bc 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -400,7 +400,9 @@ "7e": "Self-Clean", "86": "Deep Wash", "8f": "Intense Cold", - "96": "Less Microfiber" + "96": "Less Microfiber", + "2b": "AI Wash", + "2a": "Jeans" } }, "washer_dry_level": { From 724686c42c0070130f15441ed6274680e2ab9041 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:01:40 +0200 Subject: [PATCH 10/16] i18n: add translations for AI Wash and Jeans --- custom_components/localthings/translations/nl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 3429868..b008764 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -400,7 +400,9 @@ "7e": "Zelfreinigend", "86": "Diep wassen", "8f": "Intensief koud", - "96": "Minder microvezels" + "96": "Minder microvezels", + "2b": "AI Wash", + "2a": "Spijkerbroek" } }, "washer_dry_level": { From ba8e41d310f0062dadda5ca844fc7492abbeb194 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:08:18 +0200 Subject: [PATCH 11/16] Implement code changes to enhance functionality and improve performance --- .../localthings/translations/en.json | 1607 ++++++++--------- 1 file changed, 788 insertions(+), 819 deletions(-) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index bde4346..3c98141 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -25,856 +25,825 @@ "defrost_active": { "name": "Defrost active" }, - "washer_cycle_table_02": { + "detergent_low": { + "name": "Detergent low" + }, + "device_active": { + "name": "Device active" + }, + "door_open": { + "name": "Door" + }, + "firmware_update": { + "name": "Firmware update available" + }, + "instance_open": { + "name": "{instance_name} open" + }, + "paired_hood_connected": { + "name": "Paired hood connected" + }, + "paired_hood_light": { + "name": "Paired hood light" + }, + "paired_hood_power": { + "name": "Paired hood power" + }, + "periodic_air_sensing": { + "name": "Periodic air sensing" + }, + "power_state": { + "name": "Power state" + }, + "rapid_freezing": { + "name": "Rapid freezing" + }, + "rapid_fridge": { + "name": "Rapid fridge" + }, + "remote_control": { + "name": "Smart Control" + }, + "softener_low": { + "name": "Softener low" + } + }, + "button": { + "diagnosis_start": { + "name": "Start diagnosis" + }, + "pause": { + "name": "Pause" + }, + "selfcheck_start": { + "name": "Start self-check" + }, + "start": { + "name": "Start" + }, + "stop": { + "name": "Stop" + } + }, + "climate": { + "airconditioner": { + "state_attributes": { + "fan_mode": { + "state": { + "turbo": "Turbo" + } + }, + "preset_mode": { + "state": { + "quiet": "Quiet", + "smart": "Smart", + "speed": "Speed" + } + } + } + } + }, + "number": { + "cook_time": { + "name": "Cook time" + }, + "delay_start_hours": { + "name": "Delay start" + }, + "instance_setpoint": { + "name": "{instance_name} setpoint" + }, + "oven_setpoint": { + "name": "Setpoint" + }, + "sound_volume": { + "name": "Sound volume" + } + }, + "select": { + "ai_energy_level": { + "name": "AI Energy Mode level" + }, + "beverage_zone_mode": { + "name": "Beverage zone mode", "state": { + "sp_ttype_beer_drinks": "Beverage", + "sp_ttype_wine_dessert": "Wine and dessert" + } + }, + "brightness_level": { + "name": "Night brightness", + "state": { + "33": "Low", + "66": "Medium", + "100": "High" + } + }, + "day_brightness": { + "name": "Cabinet brightness", + "state": { + "33": "Low", + "66": "Medium", + "100": "High" + } + }, + "buzzer_sound": { + "name": "Buzzer sound", + "state": { + "off": "Off", + "on": "On" + } + }, + "cycle": { + "name": "Cycle" + }, + "detergent_quantity": { + "name": "Detergent quantity", + "state": { + "00": "None", + "01": "Low", + "02": "Medium", + "03": "High" + } + }, + "detergent_water_hardness": { + "name": "Detergent water hardness", + "state": { + "01": "Soft", + "02": "Medium", + "03": "Hard" + } + }, + "dishwasher_cycle": { + "name": "Cycle", + "state": { + "80": "Delicate", + "83": "Express 60", + "84": "Heavy", + "86": "Normal", + "90": "Self clean", + "0e": "AI Wash", + "07": "Pre blast", + "8d": "Pots and pans", + "8e": "Plastic", + "8f": "Baby Care" + } + }, + "door_alert": { + "name": "Door alarm", + "state": { + "1": "Alarm 1", + "2": "Alarm 2", + "3": "Alarm 3", + "4": "Alarm 4" + } + }, + "dryer_cycle_table_03": { + "name": "Cycle", + "state": { + "16": "Cotton", + "18": "Synthetics", + "19": "Delicates", + "20": "Iron dry", + "23": "Quick Dry 35", + "24": "Cool air", + "25": "Warm air", + "27": "Time dry", + "1a": "Wool", + "1b": "Bedding", + "1c": "Shirts", + "1d": "Towels", + "1e": "Outdoor", + "1f": "Mixed load" + } + }, + "finish_sound": { + "name": "Finish sound", + "state": { + "off": "Off", + "on": "On" + } + }, + "flex_zone_mode": { + "name": "Flex zone mode", + "state": { + "cv_ttype_rf9000a_freeze": "Freeze", + "cv_ttype_rf9000a_softfreeze": "Soft freeze", + "cv_ttype_rf9000a_meat_fish": "Meat/Fish", + "cv_ttype_rf9000a_fruit_veggies": "Fruit & vegetables", + "cv_ttype_rf9000a_beverage": "Beverage", + "cv_fdr_wine": "Wine", + "cv_fdr_deli": "Deli", + "cv_fdr_beverage": "Beverage", + "cv_fdr_meat": "Meat", + "cv_fdr_soft_freezer": "Soft freezer" + } + }, + "heated_dry": { + "name": "Smart Dry", + "state": { + "off": "Off", + "low": "Low", + "high": "High", + "extra_high": "Extra high" + } + }, + "ice_type": { + "name": "{instance_name} type", + "state": { + "off": "Off", + "whiskey_iceball_3": "3 balls/day", + "whiskey_iceball_6": "6 balls/day", + "whiskey_iceball_9": "9 balls/day" + } + }, + "led_brightness": { + "name": "Door LED brightness", + "state": { + "low": "Low", + "high": "High" + } + }, + "led_night_brightness": { + "name": "Door LED night brightness", + "state": { + "low": "Low", + "high": "High" + } + }, + "oven_mode": { + "name": "Cooking mode", + "state": { + "no_operation": "No operation", + "bake": "Bake", + "broil": "Broil", + "convection": "Convection", + "convection_bake": "Convection bake", + "convection_broil": "Convection broil", + "frozen_pizza_plus": "Frozen Pizza+", + "slow_cook": "Slow cook", + "plate_warm": "Plate warm", + "air_fry": "Air fry" + } + }, + "pantry_zone_mode": { + "name": "Pantry zone mode", + "state": { + "fdr_wine": "Wine", + "fdr_deli": "Deli", + "fdr_drinks": "Drinks" + } + }, + "range_burner_power_level": { + "name": "Burner {number} power level", + "state": { + "0": "Off", + "boost": "Boost", + "simmer": "Simmer" + } + }, + "range_hood_lamp_brightness": { + "name": "Lamp brightness", + "state": { + "1": "Level 1", + "2": "Level 2" + } + }, + "rinse_cycles": { + "name": "Rinse cycles" + }, + "softener_concentration": { + "name": "Softener concentration", + "state": { + "01": "1x", + "02": "2x", + "03": "3x" + } + }, + "softener_quantity": { + "name": "Softener quantity", + "state": { + "00": "None", + "01": "Low", + "02": "Medium", + "03": "High" + } + }, + "sound_mode": { + "name": "Sound mode", + "state": { + "voice": "Voice", + "tone": "Tone", + "mute": "Mute" + } + }, + "spin_speed": { + "name": "Spin speed", + "state": { + "rinse_hold": "Rinse hold", + "no_spin": "No spin", + "low": "Low", + "medium": "Medium", + "high": "High" + } + }, + "wash_temperature": { + "name": "Wash temperature", + "state": { + "none": "None", + "cold": "Cold", + "cool": "Cool", + "warm": "Warm", + "hot": "Hot", + "extra_hot": "Extra hot" + } + }, + "washer_cycle_table_02": { + "name": "Cycle", + "state": { + "01": "Normal", + "04": "Quick Wash", + "17": "Downloaded", + "1b": "Cotton", "1c": "Eco 40-60", "1d": "Super Speed", - "21": "Colours", - "1b": "Cotton", "1e": "15' Quick Wash", - "29": "Drum Clean+", + "1f": "Intense Cold", + "20": "Hygiene Steam", + "21": "Colors", + "22": "Wool", + "23": "Outdoor", "24": "Towels", - "33": "Bedding", - "28": "Drain/Spin", + "25": "Synthetics", "26": "Delicates", "27": "Rinse+Spin", - "22": "Wool", - "20": "Hygiene Steam", - "23": "Outdoor", - "25": "Synthetics", - "32": "Shirts", - "2f": "Activewear", - "2e": "Baby Care", - "30": "Cloudy Day", - "66": "Denim", + "28": "Drain/Spin", + "29": "Drum Clean+", "2d": "Silent Wash", - "8f": "Intense Cold", - "96": "Less Microfiber", + "2e": "Baby Care", + "2f": "Activewear", + "30": "Cloudy Day", + "32": "Shirts", + "33": "Bedding", "36": "Wash+Dry", "37": "Air Wash", "38": "Cotton Dry", "39": "Synthetics Dry", - "1f": "Intense Cold", + "53": "Heavy Duty", + "55": "Activewear", + "57": "Delicate", + "5e": "Rinse+Spin", + "65": "Colors", + "66": "Denim", + "7c": "Whites", + "7d": "Bedding/Waterproof", + "7e": "Self-Clean", + "86": "Deep Wash", + "8f": "Intense Cold", + "96": "Less Microfiber", "2b": "AI Wash", "2a": "Jeans" - }, - "detergent_low": { - "name": "Detergent low" - }, - "device_active": { - "name": "Device active" - }, - "door_open": { - "name": "Door" - }, - "firmware_update": { - "name": "Firmware update available" - }, - "instance_open": { - "name": "{instance_name} open" - }, - "paired_hood_connected": { - "name": "Paired hood connected" - }, - "paired_hood_light": { - "name": "Paired hood light" - }, - "paired_hood_power": { - "name": "Paired hood power" - }, - "periodic_air_sensing": { - "name": "Periodic air sensing" - }, - "power_state": { - "name": "Power state" - }, - "rapid_freezing": { - "name": "Rapid freezing" - }, - "rapid_fridge": { - "name": "Rapid fridge" - }, - "remote_control": { - "name": "Smart Control" - }, - "softener_low": { - "name": "Softener low" } }, - "button": { - "diagnosis_start": { - "name": "Start diagnosis" - }, - "pause": { - "name": "Pause" - }, - "selfcheck_start": { - "name": "Start self-check" - }, - "start": { - "name": "Start" - }, - "stop": { - "name": "Stop" - } - }, - "climate": { - "airconditioner": { - "state_attributes": { - "fan_mode": { - "state": { - "turbo": "Turbo" - } - }, - "preset_mode": { - "state": { - "quiet": "Quiet", - "smart": "Smart", - "speed": "Speed" - } - } - } - } - }, - "number": { - "cook_time": { - "name": "Cook time" - }, - "delay_start_hours": { - "name": "Delay start" - }, - "instance_setpoint": { - "name": "{instance_name} setpoint" - }, - "oven_setpoint": { - "name": "Setpoint" - }, - "sound_volume": { - "name": "Sound volume" - } - }, - "select": { - "ai_energy_level": { - "name": "AI Energy Mode level" - }, - "beverage_zone_mode": { - "name": "Beverage zone mode", - "state": { - "sp_ttype_beer_drinks": "Beverage", - "sp_ttype_wine_dessert": "Wine and dessert" - } - }, - "brightness_level": { - "name": "Night brightness", - "state": { - "33": "Low", - "66": "Medium", - "100": "High" - } - }, - "day_brightness": { - "name": "Cabinet brightness", - "state": { - "33": "Low", - "66": "Medium", - "100": "High" - } - }, - "buzzer_sound": { - "name": "Buzzer sound", - "state": { - "off": "Off", - "on": "On" - } - }, - "cycle": { - "name": "Cycle" - }, - "detergent_quantity": { - "name": "Detergent quantity", - "state": { - "00": "None", - "01": "Low", - "02": "Medium", - "03": "High" - } - }, - "detergent_water_hardness": { - "name": "Detergent water hardness", - "state": { - "01": "Soft", - "02": "Medium", - "03": "Hard" - } - }, - "dishwasher_cycle": { - "name": "Cycle", - "state": { - "80": "Delicate", - "83": "Express 60", - "84": "Heavy", - "86": "Normal", - "90": "Self clean", - "0e": "AI Wash", - "07": "Pre blast", - "8d": "Pots and pans", - "8e": "Plastic", - "8f": "Baby Care" - } - }, - "door_alert": { - "name": "Door alarm", - "state": { - "1": "Alarm 1", - "2": "Alarm 2", - "3": "Alarm 3", - "4": "Alarm 4" - } - }, - "dryer_cycle_table_03": { - "name": "Cycle", - "state": { - "16": "Cotton", - "18": "Synthetics", - "19": "Delicates", - "20": "Iron dry", - "23": "Quick Dry 35", - "24": "Cool air", - "25": "Warm air", - "27": "Time dry", - "1a": "Wool", - "1b": "Bedding", - "1c": "Shirts", - "1d": "Towels", - "1e": "Outdoor", - "1f": "Mixed load" - } - }, - "finish_sound": { - "name": "Finish sound", - "state": { - "off": "Off", - "on": "On" - } - }, - "flex_zone_mode": { - "name": "Flex zone mode", - "state": { - "cv_ttype_rf9000a_freeze": "Freeze", - "cv_ttype_rf9000a_softfreeze": "Soft freeze", - "cv_ttype_rf9000a_meat_fish": "Meat/Fish", - "cv_ttype_rf9000a_fruit_veggies": "Fruit & vegetables", - "cv_ttype_rf9000a_beverage": "Beverage", - "cv_fdr_wine": "Wine", - "cv_fdr_deli": "Deli", - "cv_fdr_beverage": "Beverage", - "cv_fdr_meat": "Meat", - "cv_fdr_soft_freezer": "Soft freezer" - } - }, - "heated_dry": { - "name": "Smart Dry", - "state": { - "off": "Off", - "low": "Low", - "high": "High", - "extra_high": "Extra high" - } - }, - "ice_type": { - "name": "{instance_name} type", - "state": { - "off": "Off", - "whiskey_iceball_3": "3 balls/day", - "whiskey_iceball_6": "6 balls/day", - "whiskey_iceball_9": "9 balls/day" - } - }, - "led_brightness": { - "name": "Door LED brightness", - "state": { - "low": "Low", - "high": "High" - } - }, - "led_night_brightness": { - "name": "Door LED night brightness", - "state": { - "low": "Low", - "high": "High" - } - }, - "oven_mode": { - "name": "Cooking mode", - "state": { - "no_operation": "No operation", - "bake": "Bake", - "broil": "Broil", - "convection": "Convection", - "convection_bake": "Convection bake", - "convection_broil": "Convection broil", - "frozen_pizza_plus": "Frozen Pizza+", - "slow_cook": "Slow cook", - "plate_warm": "Plate warm", - "air_fry": "Air fry" - } - }, - "pantry_zone_mode": { - "name": "Pantry zone mode", - "state": { - "fdr_wine": "Wine", - "fdr_deli": "Deli", - "fdr_drinks": "Drinks" - } - }, - "range_burner_power_level": { - "name": "Burner {number} power level", - "state": { - "0": "Off", - "boost": "Boost", - "simmer": "Simmer" - } - }, - "range_hood_lamp_brightness": { - "name": "Lamp brightness", - "state": { - "1": "Level 1", - "2": "Level 2" - } - }, - "rinse_cycles": { - "name": "Rinse cycles" - }, - "softener_concentration": { - "name": "Softener concentration", - "state": { - "01": "1x", - "02": "2x", - "03": "3x" - } - }, - "softener_quantity": { - "name": "Softener quantity", - "state": { - "00": "None", - "01": "Low", - "02": "Medium", - "03": "High" - } - }, - "sound_mode": { - "name": "Sound mode", - "state": { - "voice": "Voice", - "tone": "Tone", - "mute": "Mute" - } - }, - "spin_speed": { - "name": "Spin speed", - "state": { - "rinse_hold": "Rinse hold", - "no_spin": "No spin", - "low": "Low", - "medium": "Medium", - "high": "High" - } - }, - "wash_temperature": { - "name": "Wash temperature", - "state": { - "none": "None", - "cold": "Cold", - "cool": "Cool", - "warm": "Warm", - "hot": "Hot", - "extra_hot": "Extra hot" - } - }, - "washer_cycle_table_02": { - "name": "Cycle", - "state": { - "01": "Normal", - "04": "Quick Wash", - "17": "Downloaded", - "1b": "Cotton", - "1c": "Eco 40-60", - "1d": "Super Speed", - "1e": "15' Quick Wash", - "1f": "Intense Cold", - "20": "Hygiene Steam", - "21": "Colors", - "22": "Wool", - "23": "Outdoor", - "24": "Towels", - "25": "Synthetics", - "26": "Delicates", - "27": "Rinse+Spin", - "28": "Drain/Spin", - "29": "Drum Clean+", - "2d": "Silent Wash", - "2e": "Baby Care", - "2f": "Activewear", - "30": "Cloudy Day", - "32": "Shirts", - "33": "Bedding", - "36": "Wash+Dry", - "37": "Air Wash", - "38": "Cotton Dry", - "39": "Synthetics Dry", - "53": "Heavy Duty", - "55": "Activewear", - "57": "Delicate", - "5e": "Rinse+Spin", - "65": "Colors", - "66": "Denim", - "7c": "Whites", - "7d": "Bedding/Waterproof", - "7e": "Self-Clean", - "86": "Deep Wash", - "8f": "Intense Cold", - "96": "Less Microfiber" - } - }, - "washer_dry_level": { - "name": "Dry level", - "state": { - "30": "30 min", - "60": "1 hr", - "90": "1 hr 30", - "120": "2 hr", - "180": "3 hr", - "240": "4 hr", - "none": "Off", - "cupboard": "Cupboard" - } - } - }, - "sensor": { - "air_filter_usage": { - "name": "Filter usage" - }, - "air_sensing_state": { - "name": "Air sensing state" - }, - "alarm_code": { - "name": "Alarm code" - }, - "auto_ventilation_action": { - "name": "Auto ventilation action" - }, - "automatic_ventilation_state": { - "name": "Automatic ventilation state" - }, - "burner_state": { - "name": "Burner {number} state" - }, - "clean_level": { - "name": "Clean level" - }, - "connection_mode": { - "name": "Connection mode", - "state": { - "observe": "Push (observe)", - "poll": "Polling" - } - }, - "cooktop_state": { - "name": "Cooktop state" - }, - "current_limit_level": { - "name": "Current limit level" - }, - "current_temp_c": { - "name": "Temperature" - }, - "diagnosis": { - "name": "Diagnosis" - }, - "diagnosis_status": { - "name": "Diagnosis status" - }, - "drum_clean_cycles_remaining": { - "name": "Drum clean due in" - }, - "drum_clean_last_cleaned": { - "name": "Drum last cleaned" - }, - "dry_level": { - "name": "Dry level" - }, - "dry_time": { - "name": "Dry time" - }, - "dryer_type": { - "name": "Dryer type" - }, - "dust": { - "name": "Dust" - }, - "energy_kwh": { - "name": "Energy" - }, - "energy_last_month_kwh": { - "name": "Energy (last month)" - }, - "energy_saved_kwh": { - "name": "Energy saved" - }, - "energy_this_month_kwh": { - "name": "Energy (this month)" - }, - "fan_direction": { - "name": "Fan direction" - }, - "fan_speed_level": { - "name": "Fan speed level" - }, - "filter_progress": { - "name": "Filter progress" - }, - "filter_usage": { - "name": "Filter usage" - }, - "fine_dust": { - "name": "Fine dust" - }, - "finish_time": { - "name": "Estimated finish" - }, - "freezer_temperature": { - "name": "Freezer temperature" - }, - "fridge_temperature": { - "name": "Fridge temperature" - }, - "hood_filter_capacity": { - "name": "Filter capacity" - }, - "hood_filter_usage": { - "name": "Filter usage" - }, - "instance_temperature": { - "name": "{instance_name} temperature" - }, - "job_beginning_status": { - "name": "Job beginning status" - }, - "last_air_sensing_level": { - "name": "Last air sensing level" - }, - "last_air_sensing_time": { - "name": "Last air sensing time" - }, - "main_timer_current": { - "name": "Timer current value" - }, - "main_timer_state": { - "name": "Timer state" - }, - "odor": { - "name": "Odor" - }, - "operating_mode": { - "name": "Operating mode" - }, - "operation_origin": { - "name": "Last operation source" - }, - "operation_time_minutes": { - "name": "Operation time" - }, - "oven_state": { - "name": "Cavity state" - }, - "paired_hood_fan_speed": { - "name": "Paired hood fan speed" - }, - "paired_hood_firmware": { - "name": "Paired hood firmware" - }, - "paired_hood_model": { - "name": "Paired hood model" - }, - "power_energy_kwh": { - "name": "Power energy" - }, - "power_watts": { - "name": "Power" - }, - "progress": { - "name": "Progress" - }, - "progress_percentage": { - "name": "Progress percent" - }, - "selfcheck_error": { - "name": "Self-check error" - }, - "selfcheck_result": { - "name": "Self-check result" - }, - "selfcheck_status": { - "name": "Self-check status" - }, - "super_fine_dust": { - "name": "Super fine dust" - }, - "water_liters": { - "name": "Water consumption" - }, - "machine_state": { - "name": "Machine state", - "state": { - "idle": "Idle", - "active": "Active", - "pause": "Paused" - } - }, - "filter_status": { - "name": "Filter status", - "state": { - "normal": "Normal", - "wash": "Wash", - "replace": "Replace" - } - }, - "ice_making_status": { - "name": "{instance_name} making status", - "state": { - "icestatus_stop": "Idle", - "icestatus_run": "Making ice" - } - } - }, - "switch": { - "ai_energy_level": { - "name": "AI Energy Mode" - }, - "air_purify": { - "name": "Air purification" - }, - "auto_clean": { - "name": "Auto clean" - }, - "auto_door_opener": { - "name": "Auto door opener" - }, - "auto_release_dry": { - "name": "Auto release dry" - }, - "autofill": { - "name": "Autofill pitcher" - }, - "bubble_soak": { - "name": "Bubble soak" - }, - "cabinet_light_dim": { - "name": "Brighten gradually" - }, - "cabinet_light_switch": { - "name": "Cabinet light" - }, - "child_lock": { - "name": "Child lock" - }, - "defrost_delay": { - "name": "Defrost delay" - }, - "display_light": { - "name": "Display light" - }, - "fast_preheat": { - "name": "Fast preheat" - }, - "fridge_sound": { - "name": "Sound" - }, - "ice_maker_enabled": { - "name": "Ice maker" - }, - "ice_night_mode": { - "name": "Nighttime ice quiet mode" - }, - "instance_enabled": { - "name": "{instance_name} enabled" - }, - "intensive": { - "name": "Intensive" - }, - "lamp": { - "name": "Lamp" - }, - "led_night_light": { - "name": "Door LED night light" - }, - "mute_once": { - "name": "Mute once" - }, - "natural_steam": { - "name": "Natural steam" - }, - "power_switch": { - "name": "Power" - }, - "pre_wash": { - "name": "Pre-wash" - }, - "rapid_freezing": { - "name": "Rapid freezing" - }, - "rapid_fridge": { - "name": "Rapid fridge" - }, - "sabbath_mode": { - "name": "Sabbath mode" - }, - "sanitize": { - "name": "Sanitize" - }, - "sound": { - "name": "Sound" - }, - "storm_wash": { - "name": "Storm Wash+" - }, - "welcome_lighting": { - "name": "Welcome lighting" - }, - "wrinkle_prevent": { - "name": "Wrinkle prevent" - } - }, - "time": { - "led_night_end": { - "name": "Door LED night end" - }, - "led_night_start": { - "name": "Door LED night start" - }, - "night_end": { - "name": "Night light end" - }, - "night_start": { - "name": "Night light start" + "washer_dry_level": { + "name": "Dry level", + "state": { + "30": "30 min", + "60": "1 hr", + "90": "1 hr 30", + "120": "2 hr", + "180": "3 hr", + "240": "4 hr", + "none": "Off", + "cupboard": "Cupboard" } } }, - "config": { - "step": { - "user": { - "title": "Add Samsung appliance", - "description": "Enter the IP address of your Samsung appliance and the AC14K_M CA credentials used to mint device certificates.", - "data": { - "host": "IP address", - "ca_cert_pem": "CA certificate (PEM)", - "ca_key_pem": "CA private key (PEM)" - }, - "data_description": { - "host": "Local IP address of your Samsung appliance (e.g. 192.168.1.50).", - "ca_cert_pem": "The PEM-encoded AC14K_M CA certificate chain. Paste the full contents of your fullchain PEM including all BEGIN/END CERTIFICATE blocks.", - "ca_key_pem": "The PEM-encoded AC14K_M private key. Paste the full contents including the BEGIN/END PRIVATE KEY header and footer." - } - }, - "user_reuse": { - "title": "Add Samsung appliance", - "description": "Enter the IP address of your Samsung appliance. The CA credentials from an existing LocalThings appliance will be reused.", - "data": { - "host": "IP address" - }, - "data_description": { - "host": "Local IP address of your Samsung appliance (e.g. 192.168.1.50)." - } - }, - "confirm_unknown_type": { - "title": "Appliance type not recognized", - "description": "This appliance reported oneUiVersion \"{one_ui_version}\", which isn't a recognized type. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway." - }, - "confirm_unknown_type_no_version": { - "title": "Appliance type not recognized", - "description": "This appliance did not report a oneUiVersion and its type couldn't be recognized. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway." + "sensor": { + "air_filter_usage": { + "name": "Filter usage" + }, + "air_sensing_state": { + "name": "Air sensing state" + }, + "alarm_code": { + "name": "Alarm code" + }, + "auto_ventilation_action": { + "name": "Auto ventilation action" + }, + "automatic_ventilation_state": { + "name": "Automatic ventilation state" + }, + "burner_state": { + "name": "Burner {number} state" + }, + "clean_level": { + "name": "Clean level" + }, + "connection_mode": { + "name": "Connection mode", + "state": { + "observe": "Push (observe)", + "poll": "Polling" } }, - "error": { - "cannot_connect": "Cannot connect to the device. Verify the IP address is reachable and the CA credentials are correct.", - "invalid_ca": "The CA certificate or private key could not be loaded. Verify the PEM contents are correct and the key matches the certificate.", - "unknown": "Unexpected error. Check the Home Assistant logs for details." + "cooktop_state": { + "name": "Cooktop state" }, - "abort": { - "already_configured": "Device is already configured" - } - }, - "options": { - "step": { - "init": { - "title": "LocalThings options", - "menu_options": { - "settings": "Remote control write settings", - "debug_write": "Debug: write to a resource" - } - }, - "settings": { - "title": "Remote control write settings", - "description": "Some devices accept certain writes (e.g. default detergent/softener dosing on a washer) even while reporting remote control off. By default, LocalThings blocks every write with a clear error whenever a device reports remote control off, rather than letting the device silently reject it. Only enable this if you've confirmed writes actually work on this device with remote control off -- otherwise you'll trade that clear error for a silent failure.", - "data": { - "bypass_remote_control_lock": "Allow writes even when remote control is reported off" - } - }, - "debug_write": { - "title": "Debug: write to a resource", - "description": "Power-user tool for pinning down device-specific write behavior. Pick the resource (href) you want to write to, or type a custom one that isn't listed. This bypasses the remote-control-off block and sends exactly the fields you provide -- it can misconfigure your appliance, so use it deliberately.", - "data": { - "href": "Resource href" - } - }, - "debug_edit": { - "title": "Debug write: {href}", - "description": "Current value of `{href}`:\n```\n{current_value}\n```\nEnter ONLY the field(s) you want to change below. What you enter is sent to the device as-is (a partial update); it is NOT merged with the fields shown above, so don't paste the whole value back. Mind the types: a numeric string like \"1\" must stay quoted -- a bare 1 is sent as an integer, which some devices reject.", - "data": { - "payload": "Payload to write" - } - }, - "debug_result": { - "title": "Debug write result", - "description": "The device returned CoAP code {code}. A 2.xx class means the write was accepted; 4.xx/5.xx means it was rejected. The resource now reads:\n```\n{new_value}\n```\nIf the value is unchanged, the device likely dropped the write. Pick what to do next:", - "menu_options": { - "debug_write": "Write another resource", - "finish": "Finish" - } + "current_limit_level": { + "name": "Current limit level" + }, + "current_temp_c": { + "name": "Temperature" + }, + "diagnosis": { + "name": "Diagnosis" + }, + "diagnosis_status": { + "name": "Diagnosis status" + }, + "drum_clean_cycles_remaining": { + "name": "Drum clean due in" + }, + "drum_clean_last_cleaned": { + "name": "Drum last cleaned" + }, + "dry_level": { + "name": "Dry level" + }, + "dry_time": { + "name": "Dry time" + }, + "dryer_type": { + "name": "Dryer type" + }, + "dust": { + "name": "Dust" + }, + "energy_kwh": { + "name": "Energy" + }, + "energy_last_month_kwh": { + "name": "Energy (last month)" + }, + "energy_saved_kwh": { + "name": "Energy saved" + }, + "energy_this_month_kwh": { + "name": "Energy (this month)" + }, + "fan_direction": { + "name": "Fan direction" + }, + "fan_speed_level": { + "name": "Fan speed level" + }, + "filter_progress": { + "name": "Filter progress" + }, + "filter_usage": { + "name": "Filter usage" + }, + "fine_dust": { + "name": "Fine dust" + }, + "finish_time": { + "name": "Estimated finish" + }, + "freezer_temperature": { + "name": "Freezer temperature" + }, + "fridge_temperature": { + "name": "Fridge temperature" + }, + "hood_filter_capacity": { + "name": "Filter capacity" + }, + "hood_filter_usage": { + "name": "Filter usage" + }, + "instance_temperature": { + "name": "{instance_name} temperature" + }, + "job_beginning_status": { + "name": "Job beginning status" + }, + "last_air_sensing_level": { + "name": "Last air sensing level" + }, + "last_air_sensing_time": { + "name": "Last air sensing time" + }, + "main_timer_current": { + "name": "Timer current value" + }, + "main_timer_state": { + "name": "Timer state" + }, + "odor": { + "name": "Odor" + }, + "operating_mode": { + "name": "Operating mode" + }, + "operation_origin": { + "name": "Last operation source" + }, + "operation_time_minutes": { + "name": "Operation time" + }, + "oven_state": { + "name": "Cavity state" + }, + "paired_hood_fan_speed": { + "name": "Paired hood fan speed" + }, + "paired_hood_firmware": { + "name": "Paired hood firmware" + }, + "paired_hood_model": { + "name": "Paired hood model" + }, + "power_energy_kwh": { + "name": "Power energy" + }, + "power_watts": { + "name": "Power" + }, + "progress": { + "name": "Progress" + }, + "progress_percentage": { + "name": "Progress percent" + }, + "selfcheck_error": { + "name": "Self-check error" + }, + "selfcheck_result": { + "name": "Self-check result" + }, + "selfcheck_status": { + "name": "Self-check status" + }, + "super_fine_dust": { + "name": "Super fine dust" + }, + "water_liters": { + "name": "Water consumption" + }, + "machine_state": { + "name": "Machine state", + "state": { + "idle": "Idle", + "active": "Active", + "pause": "Paused" } }, - "error": { - "empty_payload": "Enter at least one field to write.", - "write_failed": "The write failed. Check the Home Assistant logs for details." + "filter_status": { + "name": "Filter status", + "state": { + "normal": "Normal", + "wash": "Wash", + "replace": "Replace" + } }, - "abort": { - "not_loaded": "This device isn't connected yet. Try again once it has loaded." + "ice_making_status": { + "name": "{instance_name} making status", + "state": { + "icestatus_stop": "Idle", + "icestatus_run": "Making ice" + } } }, - "issues": { - "device_gap": { - "title": "Incomplete capability coverage for {device_name}", - "description": "This device is missing full capability coverage. Either its appliance type wasn't recognized, or some of the resources it exposes aren't modeled yet. It'll keep working with whatever is already supported. You can help expand support by going to Settings > Devices & Services > {device_name} > the menu (top right) > Download diagnostics, then filing it with the linked issue template." + "switch": { + "ai_energy_level": { + "name": "AI Energy Mode" + }, + "air_purify": { + "name": "Air purification" + }, + "auto_clean": { + "name": "Auto clean" + }, + "auto_door_opener": { + "name": "Auto door opener" + }, + "auto_release_dry": { + "name": "Auto release dry" + }, + "autofill": { + "name": "Autofill pitcher" + }, + "bubble_soak": { + "name": "Bubble soak" + }, + "cabinet_light_dim": { + "name": "Brighten gradually" + }, + "cabinet_light_switch": { + "name": "Cabinet light" + }, + "child_lock": { + "name": "Child lock" + }, + "defrost_delay": { + "name": "Defrost delay" + }, + "display_light": { + "name": "Display light" + }, + "fast_preheat": { + "name": "Fast preheat" + }, + "fridge_sound": { + "name": "Sound" + }, + "ice_maker_enabled": { + "name": "Ice maker" + }, + "ice_night_mode": { + "name": "Nighttime ice quiet mode" + }, + "instance_enabled": { + "name": "{instance_name} enabled" + }, + "intensive": { + "name": "Intensive" + }, + "lamp": { + "name": "Lamp" + }, + "led_night_light": { + "name": "Door LED night light" + }, + "mute_once": { + "name": "Mute once" + }, + "natural_steam": { + "name": "Natural steam" + }, + "power_switch": { + "name": "Power" + }, + "pre_wash": { + "name": "Pre-wash" + }, + "rapid_freezing": { + "name": "Rapid freezing" + }, + "rapid_fridge": { + "name": "Rapid fridge" + }, + "sabbath_mode": { + "name": "Sabbath mode" + }, + "sanitize": { + "name": "Sanitize" + }, + "sound": { + "name": "Sound" + }, + "storm_wash": { + "name": "Storm Wash+" + }, + "welcome_lighting": { + "name": "Welcome lighting" + }, + "wrinkle_prevent": { + "name": "Wrinkle prevent" } }, - "exceptions": { - "remote_control_disabled": { - "message": "Remote control is turned off on this device. Check your appliance's manual for how to enable remote control before Home Assistant can control it." + "time": { + "led_night_end": { + "name": "Door LED night end" }, - "debug_payload_empty": { - "message": "Debug write payload must be a non-empty object." + "led_night_start": { + "name": "Door LED night start" }, - "resource_href_required": { - "message": "A resource href is required." + "night_end": { + "name": "Night light end" }, - "bubble_soak_unavailable_for_cycle": { - "message": "Bubble Soak isn't available on the selected cycle." - }, - "pre_wash_unavailable_for_cycle": { - "message": "Pre-wash isn't available on the selected cycle." - }, - "intensive_unavailable_for_cycle": { - "message": "Intensive isn't available on the selected cycle." + "night_start": { + "name": "Night light start" } } - } \ No newline at end of file + }, + "config": { + "step": { + "user": { + "title": "Add Samsung appliance", + "description": "Enter the IP address of your Samsung appliance and the AC14K_M CA credentials used to mint device certificates.", + "data": { + "host": "IP address", + "ca_cert_pem": "CA certificate (PEM)", + "ca_key_pem": "CA private key (PEM)" + }, + "data_description": { + "host": "Local IP address of your Samsung appliance (e.g. 192.168.1.50).", + "ca_cert_pem": "The PEM-encoded AC14K_M CA certificate chain. Paste the full contents of your fullchain PEM including all BEGIN/END CERTIFICATE blocks.", + "ca_key_pem": "The PEM-encoded AC14K_M private key. Paste the full contents including the BEGIN/END PRIVATE KEY header and footer." + } + }, + "user_reuse": { + "title": "Add Samsung appliance", + "description": "Enter the IP address of your Samsung appliance. The CA credentials from an existing LocalThings appliance will be reused.", + "data": { + "host": "IP address" + }, + "data_description": { + "host": "Local IP address of your Samsung appliance (e.g. 192.168.1.50)." + } + }, + "confirm_unknown_type": { + "title": "Appliance type not recognized", + "description": "This appliance reported oneUiVersion \"{one_ui_version}\", which isn't a recognized type. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway." + }, + "confirm_unknown_type_no_version": { + "title": "Appliance type not recognized", + "description": "This appliance did not report a oneUiVersion and its type couldn't be recognized. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway." + } + }, + "error": { + "cannot_connect": "Cannot connect to the device. Verify the IP address is reachable and the CA credentials are correct.", + "invalid_ca": "The CA certificate or private key could not be loaded. Verify the PEM contents are correct and the key matches the certificate.", + "unknown": "Unexpected error. Check the Home Assistant logs for details." + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "LocalThings options", + "menu_options": { + "settings": "Remote control write settings", + "debug_write": "Debug: write to a resource" + } + }, + "settings": { + "title": "Remote control write settings", + "description": "Some devices accept certain writes (e.g. default detergent/softener dosing on a washer) even while reporting remote control off. By default, LocalThings blocks every write with a clear error whenever a device reports remote control off, rather than letting the device silently reject it. Only enable this if you've confirmed writes actually work on this device with remote control off -- otherwise you'll trade that clear error for a silent failure.", + "data": { + "bypass_remote_control_lock": "Allow writes even when remote control is reported off" + } + }, + "debug_write": { + "title": "Debug: write to a resource", + "description": "Power-user tool for pinning down device-specific write behavior. Pick the resource (href) you want to write to, or type a custom one that isn't listed. This bypasses the remote-control-off block and sends exactly the fields you provide -- it can misconfigure your appliance, so use it deliberately.", + "data": { + "href": "Resource href" + } + }, + "debug_edit": { + "title": "Debug write: {href}", + "description": "Current value of `{href}`:\n```\n{current_value}\n```\nEnter ONLY the field(s) you want to change below. What you enter is sent to the device as-is (a partial update); it is NOT merged with the fields shown above, so don't paste the whole value back. Mind the types: a numeric string like \"1\" must stay quoted -- a bare 1 is sent as an integer, which some devices reject.", + "data": { + "payload": "Payload to write" + } + }, + "debug_result": { + "title": "Debug write result", + "description": "The device returned CoAP code {code}. A 2.xx class means the write was accepted; 4.xx/5.xx means it was rejected. The resource now reads:\n```\n{new_value}\n```\nIf the value is unchanged, the device likely dropped the write. Pick what to do next:", + "menu_options": { + "debug_write": "Write another resource", + "finish": "Finish" + } + } + }, + "error": { + "empty_payload": "Enter at least one field to write.", + "write_failed": "The write failed. Check the Home Assistant logs for details." + }, + "abort": { + "not_loaded": "This device isn't connected yet. Try again once it has loaded." + } + }, + "issues": { + "device_gap": { + "title": "Incomplete capability coverage for {device_name}", + "description": "This device is missing full capability coverage. Either its appliance type wasn't recognized, or some of the resources it exposes aren't modeled yet. It'll keep working with whatever is already supported. You can help expand support by going to Settings > Devices & Services > {device_name} > the menu (top right) > Download diagnostics, then filing it with the linked issue template." + } + }, + "exceptions": { + "remote_control_disabled": { + "message": "Remote control is turned off on this device. Check your appliance's manual for how to enable remote control before Home Assistant can control it." + }, + "debug_payload_empty": { + "message": "Debug write payload must be a non-empty object." + }, + "resource_href_required": { + "message": "A resource href is required." + }, + "bubble_soak_unavailable_for_cycle": { + "message": "Bubble Soak isn't available on the selected cycle." + }, + "pre_wash_unavailable_for_cycle": { + "message": "Pre-wash isn't available on the selected cycle." + }, + "intensive_unavailable_for_cycle": { + "message": "Intensive isn't available on the selected cycle." + } + } +} \ No newline at end of file From 317b7d0c1b092369966fcdb47d54fc4928f8001e Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:09:55 +0200 Subject: [PATCH 12/16] fix: add missing newline at end of en.json file --- custom_components/localthings/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 3c98141..f7e93bc 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -846,4 +846,4 @@ "message": "Intensive isn't available on the selected cycle." } } -} \ No newline at end of file +} From 37bfff152d4b203cfa7d87fbbc761fffee00d700 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:15:34 +0200 Subject: [PATCH 13/16] refactor: remove unused strings.json file --- custom_components/localthings/strings.json | 268 --------------------- 1 file changed, 268 deletions(-) delete mode 100644 custom_components/localthings/strings.json diff --git a/custom_components/localthings/strings.json b/custom_components/localthings/strings.json deleted file mode 100644 index b276d37..0000000 --- a/custom_components/localthings/strings.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "entity": { - "select": { - "door_alert": { - "state": { - "1": "Alarm 1", - "2": "Alarm 2", - "3": "Alarm 3", - "4": "Alarm 4" - } - }, - "brightness_level": { - "state": { - "33": "Low", - "66": "Medium", - "100": "High" - } - }, - "range_hood_lamp_brightness": { - "state": { - "1": "Level 1", - "2": "Level 2" - } - }, - "ice_type": { - "state": { - "off": "Off", - "whiskey_iceball_3": "3 Balls/Day", - "whiskey_iceball_6": "6 Balls/Day", - "whiskey_iceball_9": "9 Balls/Day" - } - }, - "beverage_zone_mode": { - "state": { - "sp_ttype_beer_drinks": "Beverage", - "sp_ttype_wine_dessert": "Wine and Dessert" - } - }, - "pantry_zone_mode": { - "state": { - "fdr_wine": "Wine", - "fdr_deli": "Deli", - "fdr_drinks": "Drinks" - } - }, - "flex_zone_mode": { - "state": { - "cv_ttype_rf9000a_freeze": "Freeze", - "cv_ttype_rf9000a_softfreeze": "Soft Freeze", - "cv_ttype_rf9000a_meat_fish": "Meat/Fish", - "cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies", - "cv_ttype_rf9000a_beverage": "Beverage", - "cv_fdr_wine": "Wine", - "cv_fdr_deli": "Deli", - "cv_fdr_beverage": "Beverage", - "cv_fdr_meat": "Meat", - "cv_fdr_soft_freezer": "Soft Freezer" - } - }, - "dishwasher_cycle": { - "state": { - "0e": "AI Wash", - "07": "Pre blast", - "90": "Self clean", - "86": "Normal", - "83": "Express 60", - "84": "Heavy", - "8d": "Pots and pans", - "80": "Delicate", - "8e": "Plastic", - "8f": "Baby Care" - } - }, - "washer_cycle_table_02": { - "state": { - "1c": "Eco 40-60", - "1d": "Super Speed", - "21": "Colours", - "1b": "Cotton", - "1e": "15' Quick Wash", - "29": "Drum Clean+", - "24": "Towels", - "33": "Bedding", - "28": "Drain/Spin", - "26": "Delicates", - "27": "Rinse+Spin", - "22": "Wool", - "20": "Hygiene Steam", - "23": "Outdoor", - "25": "Synthetics", - "32": "Shirts", - "2f": "Activewear", - "2e": "Baby Care", - "30": "Cloudy Day", - "66": "Denim", - "2d": "Silent Wash", - "8f": "Intense Cold", - "96": "Less Microfiber", - "36": "Wash+Dry", - "37": "Air Wash", - "38": "Cotton Dry", - "39": "Synthetics Dry", - "1f": "Intense Cold", - "2b": "AI Wash", - "2a": "Jeans" - } - }, - "dryer_cycle_table_03": { - "state": { - "16": "Cotton", - "18": "Synthetics", - "19": "Delicates", - "1a": "Wool", - "1b": "Bedding", - "1c": "Shirts", - "1d": "Towels", - "1e": "Outdoor", - "1f": "Mixed Load", - "20": "Iron Dry", - "23": "Quick Dry 35", - "24": "Cool Air", - "25": "Warm Air", - "27": "Time Dry" - } - }, - "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" - } - }, - "washer_dry_level": { - "state": { - "none": "Off", - "cupboard": "Cupboard", - "30": "30 min", - "60": "1 hr", - "90": "1 hr 30", - "120": "2 hr", - "180": "3 hr", - "240": "4 hr" - } - } - }, - "sensor": { - "ice_making_status": { - "state": { - "icestatus_stop": "Idle", - "icestatus_run": "Making ice" - } - }, - "connection_mode": { - "state": { - "observe": "Push (observe)", - "poll": "Polling" - } - }, - "machine_state": { - "state": { - "idle": "Idle", - "active": "Active", - "pause": "Paused" - } - }, - "air_filter_status": { - "state": { - "normal": "Normal", - "wash": "Wash", - "replace": "Replace" - } - } - }, - "climate": { - "airconditioner": { - "state_attributes": { - "fan_mode": { - "state": { - "auto": "Auto", - "low": "Low", - "medium": "Medium", - "high": "High", - "turbo": "Turbo" - } - }, - "swing_mode": { - "state": { - "off": "Fixed", - "both": "All directions", - "vertical": "Up and down" - } - }, - "preset_mode": { - "state": { - "none": "Off", - "sleep": "Sleep", - "quiet": "Quiet", - "smart": "Smart", - "speed": "Speed" - } - } - } - } - } - }, - "config": { - "step": { - "user": { - "title": "Add Samsung Appliance", - "description": "Enter the IP address of your Samsung appliance and the AC14K_M CA credentials used to mint device certificates. {reuse_note}", - "data": { - "host": "IP Address", - "ca_cert_pem": "CA Certificate (PEM)", - "ca_key_pem": "CA Private Key (PEM)" - }, - "data_description": { - "host": "Local IP address of your Samsung appliance (e.g. 192.168.1.50).", - "ca_cert_pem": "The PEM-encoded AC14K_M CA certificate chain. Paste the full contents of your fullchain PEM including all BEGIN/END CERTIFICATE blocks.", - "ca_key_pem": "The PEM-encoded AC14K_M private key. Paste the full contents including the BEGIN/END PRIVATE KEY header and footer." - } - }, - "confirm_unknown_type": { - "title": "Appliance type not recognized", - "description": "This appliance reported oneUiVersion \"{one_ui_version}\", which isn't a recognized type. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway." - } - }, - "error": { - "cannot_connect": "Cannot connect to the device. Verify the IP address is reachable and the CA credentials are correct.", - "invalid_ca": "The CA certificate or private key could not be loaded. Verify the PEM contents are correct and the key matches the certificate.", - "unknown": "Unexpected error. Check the Home Assistant logs for details." - }, - "abort": { - "already_configured": "This device is already configured." - } - }, - "options": { - "step": { - "init": { - "title": "Local Things Options", - "description": "Some devices accept certain writes (e.g. default detergent/softener dosing on a washer) even while reporting remote control off. By default, LocalThings blocks every write with a clear error whenever a device reports remote control off, rather than letting the device silently reject it. Only enable this if you've confirmed writes actually work on this device with remote control off -- otherwise you'll trade that clear error for a silent failure.", - "data": { - "bypass_remote_control_lock": "Allow writes even when remote control is reported off" - } - } - } - }, - "issues": { - "device_gap": { - "title": "Incomplete capability coverage for {device_name}", - "description": "This device is missing full capability coverage. Either its appliance type wasn't recognized, or some of the resources it exposes aren't modeled yet. It'll keep working with whatever is already supported. You can help expand support by going to Settings > Devices & Services > {device_name} > the menu (top right) > Download diagnostics, then filing it with the linked issue template." - } - } -} From 9e290f27ee744faddd5dd2e81bdf52f09e8ba790 Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:24:10 +0200 Subject: [PATCH 14/16] refactor: update sensor descriptions for completion time and delay start --- .../localthings/registry/capabilities/operational.py | 5 ++--- custom_components/localthings/translations/en.json | 3 +++ custom_components/localthings/translations/nl.json | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/operational.py b/custom_components/localthings/registry/capabilities/operational.py index dbee0dd..6970140 100644 --- a/custom_components/localthings/registry/capabilities/operational.py +++ b/custom_components/localthings/registry/capabilities/operational.py @@ -156,16 +156,15 @@ OPERATIONAL_STATE = Capability( # firmware leaves a stale remainingTime after a cycle ends, and # freezes it at '00:01:00' when progress reaches 'Finish'. SensorDesc(key='finish_time', device_class='timestamp', - name='Estimated finish', rep_fn=_finish_time), - SensorDesc(key='completion_minutes', name='Remaining minutes', + SensorDesc(key='completion_minutes', icon='mdi:clock-outline', unit='min', device_class='duration', state_class='measurement', exists_fn=lambda rep, resources: _completion_minutes(rep) is not None, rep_fn=_completion_minutes), - NumberDesc(key='delay_start_hours', name='Delay start', icon='mdi:timer-plus-outline', + NumberDesc(key='delay_start_hours', icon='mdi:timer-plus-outline', device_class='duration', unit='h', native_min=0, native_max=24, step=1, rep_fn=lambda rep: _delay_hours( diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index f7e93bc..b224bfd 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -511,6 +511,9 @@ "finish_time": { "name": "Estimated finish" }, + "completion_minutes": { + "name": "Estimated finish (minutes)" + }, "freezer_temperature": { "name": "Freezer temperature" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index b008764..0272f1b 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -511,6 +511,9 @@ "finish_time": { "name": "Geschatte eindtijd" }, + "completion_minutes": { + "name": "Geschatte eindtijd (minuten)" + }, "freezer_temperature": { "name": "Vriezertemperatuur" }, From ef75db669798765d627d8949d98978bceca774dc Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:55:47 +0200 Subject: [PATCH 15/16] i18n: update completion time translation in English and Dutch --- custom_components/localthings/translations/en.json | 2 +- custom_components/localthings/translations/nl.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index b224bfd..035591e 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -512,7 +512,7 @@ "name": "Estimated finish" }, "completion_minutes": { - "name": "Estimated finish (minutes)" + "name": "Completion time (minutes)" }, "freezer_temperature": { "name": "Freezer temperature" diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 0272f1b..966b6b6 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -512,7 +512,7 @@ "name": "Geschatte eindtijd" }, "completion_minutes": { - "name": "Geschatte eindtijd (minuten)" + "name": "Voltooiingstijd (minuten)" }, "freezer_temperature": { "name": "Vriezertemperatuur" From 6de1a4090e10e32ccb58f3e5b8323f380a427dec Mon Sep 17 00:00:00 2001 From: Jeroen Hof <32430809+Metal-Eagle@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:02:45 +0200 Subject: [PATCH 16/16] i18n: simplify completion time translation in English and Dutch --- custom_components/localthings/translations/en.json | 2 +- custom_components/localthings/translations/nl.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 035591e..f990cc6 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -512,7 +512,7 @@ "name": "Estimated finish" }, "completion_minutes": { - "name": "Completion time (minutes)" + "name": "Completion time" }, "freezer_temperature": { "name": "Freezer temperature" diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 966b6b6..c922d82 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -512,7 +512,7 @@ "name": "Geschatte eindtijd" }, "completion_minutes": { - "name": "Voltooiingstijd (minuten)" + "name": "Voltooiingstijd" }, "freezer_temperature": { "name": "Vriezertemperatuur"