fix(operational): support delayEndTime as an alternate delay-start field

This commit is contained in:
Marc Billow
2026-07-11 12:38:17 -05:00
parent 1b5e39ba46
commit 219cb70b77
2 changed files with 47 additions and 4 deletions
@@ -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: (
+32
View File
@@ -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'}