2f7170c4486aa2b897ff6641b58ea92c58de4f7f
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f7170c448 |
laundry: a post-Finish running stage is the cycle ending, not a new one
Fixes #358, a regression from #346. That PR's sticky_bypass_fn released the Finish/100 hold on any concrete non-Finish progress code, ungated on machine_state, reasoning that a new cycle's own progress can appear before state catches up. But the reporting DA_WM_TP1_21_COMMON dryer replays a running stage on the way *out* of a cycle: the issue's history shows Cooling -> +60s Finish -> +24s 'Drying' -> +4s settled, twice, identically. The bypass read that tail as a new cycle, dropped the hold, and republished 'Drying' -- so progress read Drying, Cooling, Finish, Drying, Idle instead of ending at Finish, Idle. The tail is not new: rep_fn has always masked progress while state isn't active, which is why it was invisible before #346. What surfaced it was sticky_live_fn, a second, ungated view of the same field that the bypass returned in rep_fn's place -- letting the hold publish a value the entity otherwise never shows. Both halves are fixed: - The bypass (now _new_cycle_running) requires state == 'active' alongside the progress code. The arm condition stays ungated -- failing to arm loses the Finish entirely (#345), while releasing late costs nothing, since the hold expires on its own. - sticky_live_fn is gone. rep_fn is the only definition of a live value; the hold decides only whether to freeze, and the bypass returns rep_fn's own result. Also stop an already-open window from being restarted by a progress that flaps in and out of Finish, so sticky_seconds is measured from the first Finish of a cycle and the documented bound actually holds. A paused new cycle no longer cuts the hold short (it did under the old ungated bypass). Nothing live is withheld by that: rep_fn shows Idle while paused with or without a hold, so the only change is a stale Finish expiring on schedule -- and 'paused' cannot be told apart from this tail. |
||
|
|
ef66d1db71 |
washer/dryer: hold progress/progress_percentage at Finish/100 for 5 min
Fixes #345. progress and progress_percentage were gated on machine_state alone, falling straight to "Idle"/0 the instant state left 'active' -- but a washer can flip state away from 'active' within the same poll interval progress reaches 'Finish' (more reliably than the same-family dryer, per the report), so an automation watching for a real 'Finish' value could go a whole cycle without ever observing one. Implemented read-side, per-entity (sensor.py's new _apply_sticky), generalizing the existing _hysteresis_value/_apply_hysteresis pattern finish_time already uses, rather than writing a synthetic override back into the coordinator's cache. That cache is this integration's record of what the device actually said, and is read by several unrelated consumers -- write_fn, validate_fn, diagnostics, the observe-mode sweep comparison against /device/0, _completion_minutes' own stale-remainingTime workaround -- all of which would otherwise see fabricated state. A first pass gated the hold's arm condition on state=='active' AND progress=='Finish' occurring in the same rep, mirroring _is_active. A second review caught that this could make the whole fix a no-op on the one device #345 reports it for: if state has already reset by the time progress is ever observed at 'Finish' -- exactly what #345 describes -- the arm condition never fires. _just_finished now arms on progress== 'Finish' alone. That reopens the staleness risk the state check existed to guard against (a progress field stuck at 'Finish' forever would then arm forever too), so _apply_sticky is edge-triggered: only a fresh False->True transition (re)starts the window, and expiry is still checked on every call even while the condition keeps matching -- a stuck value still won't hold past sticky_seconds. Dropping the state requirement also exposed a second gap: the bypass that lets a new cycle's own real progress override a stale hold was keyed on machine_state=='active', so it missed a new cycle immediately paused (e.g. adding a sock) -- machine_state isn't 'active' while paused. It's keyed on a live, non-Finish progress code instead (_live_progress_code), independent of state, same reasoning as _just_finished. And since the bypass needs the *real* live value, not whatever rep_fn's own (differently gated) result says, SensorDesc grew sticky_live_fn alongside sticky_value_fn: rep_fn's progress gate still shows "Idle" while paused, but the real progress value read ungated must win over the hold regardless. registry/entities.py: SensorDesc gains sticky_fn/sticky_value_fn/ sticky_live_fn/sticky_bypass_fn/sticky_seconds -- see sensor.py's _apply_sticky docstring for the full contract. registry/capabilities/operational.py: progress/progress_percentage's rep_fn is unchanged; they gain the sticky_* wiring above. machine_state and the Running binary sensor are untouched -- still gated on real-time state (cycle_active now shares _is_active's rep_fn directly rather than a duplicate inline copy), so they never claim the appliance is still running once it isn't. |