From fe9a1128eaf6b94c31a3e0cc93a94864c03556c7 Mon Sep 17 00:00:00 2001 From: Jelle Lauwers Date: Fri, 31 Jul 2026 19:07:24 +0200 Subject: [PATCH] Round finish_time to the minute to stop recorder spam _finish_time added datetime.now(timezone.utc) (fresh seconds/ microseconds every call) to the device's remaining-time duration, so the returned timestamp differed at the sub-minute level on nearly every poll even when remainingTime itself hadn't changed. The recorder logged a new history/logbook entry each time, while the UI rounds the display down to the minute, making repeated polls look like duplicate identical entries. Round the result down to the minute so the entity only changes state when the estimate actually shifts. --- .../registry/capabilities/operational.py | 9 +++++- tests/test_operational_capability.py | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/registry/capabilities/operational.py b/custom_components/localthings/registry/capabilities/operational.py index 6970140..193efb9 100644 --- a/custom_components/localthings/registry/capabilities/operational.py +++ b/custom_components/localthings/registry/capabilities/operational.py @@ -90,7 +90,14 @@ def _finish_time(rep): 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) + # Round to whole minutes -- remainingTime itself only has minute + # resolution, but datetime.now() always carries fresh seconds/ + # microseconds, so an unrounded result changes on nearly every poll + # even when the device-reported remaining time hasn't. That floods + # the recorder history/logbook with values that look identical once + # the UI rounds them down for display. + finish = datetime.now(timezone.utc) + timedelta(seconds=total_s) + return finish.replace(second=0, microsecond=0) def _completion_minutes(rep): diff --git a/tests/test_operational_capability.py b/tests/test_operational_capability.py index 606efb6..5f50535 100644 --- a/tests/test_operational_capability.py +++ b/tests/test_operational_capability.py @@ -62,6 +62,34 @@ class TestCompletionMinutes: assert desc.rep_fn(rep) is None +class TestFinishTime: + """issue: remainingTime only has minute resolution, but datetime.now() + always carries fresh seconds/microseconds -- an unrounded finish_time + changed on nearly every poll even when remainingTime hadn't, flooding + the recorder history/logbook with values that looked identical once + the UI rounded them down to the minute for display.""" + + def test_seconds_and_microseconds_are_zeroed(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'finish_time') + rep = { + 'x.com.samsung.da.state': 'Run', + 'x.com.samsung.da.remainingTime': '00:29:00', + } + result = desc.rep_fn(rep) + assert result.second == 0 + assert result.microsecond == 0 + + def test_stable_across_polls_within_same_minute(self): + desc = next(e for e in OPERATIONAL_STATE.entities if e.key == 'finish_time') + rep = { + 'x.com.samsung.da.state': 'Run', + 'x.com.samsung.da.remainingTime': '00:29:00', + } + first = desc.rep_fn(rep) + second = desc.rep_fn(rep) + assert first == second + + class TestDelayFieldFallback: def test_reads_delay_end_time_when_delay_start_time_absent(self): from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE