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