From 67b28ed10f4e34cb16a1072ab55bf11d70844fbe Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Wed, 12 Aug 2026 14:39:18 +0000 Subject: [PATCH] laundry: cite the machine_state history confirming #358's tail The reporter's machine_state history for the same two cycles flips to idle on the exact second progress reads 'Drying' (12:08:51 and 14:01:26), which settles what the previous commits had to infer: rep_fn returns 'Idle' whenever state isn't active, so it cannot have produced that value, and the only remaining path was the ungated sticky_live_fn the bypass returned in its place. Replaying the sequence against the pre-fix path reproduces the reported Cooling, Finish, Drying, Idle exactly; the fix holds Finish through it. Comments only -- swap the inference for the observation that confirms it. --- .../registry/capabilities/operational.py | 13 ++++++++----- tests/test_sensor_sticky.py | 13 +++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/operational.py b/custom_components/localthings/registry/capabilities/operational.py index 440cc8d..e54db0c 100644 --- a/custom_components/localthings/registry/capabilities/operational.py +++ b/custom_components/localthings/registry/capabilities/operational.py @@ -67,11 +67,14 @@ def _new_cycle_running(rep): early once a new cycle is genuinely running. Gated on `state == 'active'`, unlike _just_finished's arm condition - above: issue #358's dryer replays a running stage ('Drying') for a few - seconds after Finish while `state` already reads idle, and a bypass - keyed on the progress code alone read that tail as a new cycle and - republished it. Releasing late costs nothing -- an unreleased hold - still expires on its own -- so this side takes the stronger signal.""" + above: issue #358's dryer resets `progress` to its course's first + stage ('Drying') in the same moment `state` goes idle, ~4s before + settling to 'None' -- confirmed by the reporter's machine_state + history, which flips to idle on the exact second progress reads + 'Drying', in both captured cycles. A bypass keyed on the progress + code alone read that as a new cycle and republished it. Releasing + late costs nothing -- an unreleased hold still expires on its own -- + so this side takes the stronger signal.""" v = rep.get("x.com.samsung.da.progress") return _state_is_active(rep) and v is not None and v not in ("None", "Finish") diff --git a/tests/test_sensor_sticky.py b/tests/test_sensor_sticky.py index 962f0eb..0627a9b 100644 --- a/tests/test_sensor_sticky.py +++ b/tests/test_sensor_sticky.py @@ -166,12 +166,13 @@ def test_a_new_cycle_starting_overrides_the_hold(): def test_a_running_stage_after_finish_does_not_break_the_hold(): - """Issue #358: the reporting dryer replays a running stage after - Finish -- observed twice, identically, as Cooling -> +60s Finish -> - +24s 'Drying' -> +4s settled, with `state` already idle throughout the - tail. The hold must survive it, so the cycle still reads Drying, - Cooling, Finish, Idle rather than the reported Drying, Cooling, - Finish, Drying, Idle.""" + """Issue #358: the reporting dryer resets `progress` to its course's + first stage in the same moment `state` goes idle -- observed twice, + identically, as Cooling -> +60s Finish -> +24s 'Drying' -> +4s + settled, with the reporter's machine_state history flipping to idle on + the exact second progress reads 'Drying'. The hold must survive that, + so the cycle still reads Drying, Cooling, Finish, Idle rather than the + reported Drying, Cooling, Finish, Drying, Idle.""" desc = replace(_PROGRESS_DESC, sticky_seconds=0.2) sensor, coordinator = _sensor(desc)