From 219cb70b776cc3e04107e9f830505e5e4fbf0db6 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Sat, 11 Jul 2026 12:38:17 -0500 Subject: [PATCH] fix(operational): support delayEndTime as an alternate delay-start field --- .../registry/capabilities/operational.py | 19 ++++++++--- tests/test_operational_capability.py | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/operational.py b/custom_components/localthings/registry/capabilities/operational.py index 127240f..0292020 100644 --- a/custom_components/localthings/registry/capabilities/operational.py +++ b/custom_components/localthings/registry/capabilities/operational.py @@ -47,6 +47,16 @@ def _format_delay(hours): return f'{h}:{m:02d}:00' +def _delay_field(rep): + """Washer hardware reports the delay-until-start duration under + 'delayEndTime' instead of 'delayStartTime' (both hold a duration, not a + 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 @@ -96,14 +106,15 @@ OPERATIONAL_STATE = Capability( or rep.get('x.com.samsung.da.progress') == 'Finish' else _finish_time(rep.get('x.com.samsung.da.remainingTime')) )), - NumberDesc(key='delay_start_hours', field='x.com.samsung.da.delayStartTime', - name='Delay start', icon='mdi:timer-plus-outline', + 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, - value_fn=_delay_hours, + rep_fn=lambda rep: _delay_hours( + rep.get('x.com.samsung.da.delayStartTime') + or rep.get('x.com.samsung.da.delayEndTime')), write_fn=lambda p, rep, href=None: ( ['operational', 'state', 'vs', '0'], - {'x.com.samsung.da.delayStartTime': _format_delay(p)})), + {_delay_field(rep): _format_delay(p)})), ButtonDesc(key='start', field='', name='Start cycle', payload='Run', icon='mdi:play', write_fn=lambda p, rep, href=None: ( diff --git a/tests/test_operational_capability.py b/tests/test_operational_capability.py index 70bc014..3cfd259 100644 --- a/tests/test_operational_capability.py +++ b/tests/test_operational_capability.py @@ -6,3 +6,35 @@ def test_machine_state_maps_samsung_to_ocf(): assert ms.value_fn('Run') == 'active' assert ms.value_fn('Pause') == 'pause' assert ms.value_fn('Ready') == 'idle' + + +class TestDelayFieldFallback: + def test_reads_delay_end_time_when_delay_start_time_absent(self): + from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'delay_start_hours') + rep = {'x.com.samsung.da.delayEndTime': '02:30:00'} + assert desc.rep_fn(rep) == 2.5 + + def test_prefers_delay_start_time_when_both_present(self): + from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'delay_start_hours') + rep = { + 'x.com.samsung.da.delayStartTime': '01:00:00', + 'x.com.samsung.da.delayEndTime': '02:00:00', + } + assert desc.rep_fn(rep) == 1.0 + + def test_write_targets_delay_end_time_when_that_is_what_device_reports(self): + from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'delay_start_hours') + rep = {'x.com.samsung.da.delayEndTime': '00:00:00'} + path, body = desc.write_fn(1.5, rep) + assert path == ['operational', 'state', 'vs', '0'] + assert body == {'x.com.samsung.da.delayEndTime': '1:30:00'} + + def test_write_targets_delay_start_time_by_default(self): + from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE + 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'}