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.
This commit is contained in:
5 files changed
+166
-79
No files matched your search
@@ -3,7 +3,7 @@
|
||||
from custom_components.localthings.registry.capabilities.operational import (
|
||||
OPERATIONAL_STATE,
|
||||
_just_finished,
|
||||
_live_progress_code,
|
||||
_new_cycle_running,
|
||||
)
|
||||
from custom_components.localthings.registry.entities import NumberDesc
|
||||
|
||||
@@ -41,27 +41,46 @@ class TestJustFinished:
|
||||
assert not _just_finished({"x.com.samsung.da.state": "Run"})
|
||||
|
||||
|
||||
class TestLiveProgressCode:
|
||||
"""`_live_progress_code` is progress/progress_percentage's
|
||||
sticky_bypass_fn (issue #345) -- see sensor.py's _apply_sticky."""
|
||||
class TestNewCycleRunning:
|
||||
"""`_new_cycle_running` is progress/progress_percentage's
|
||||
sticky_bypass_fn -- the early-release condition for the #345 hold.
|
||||
See sensor.py's _apply_sticky."""
|
||||
|
||||
def test_true_for_a_concrete_non_finish_code(self):
|
||||
assert _live_progress_code({"x.com.samsung.da.progress": "Wash"})
|
||||
def test_true_for_a_concrete_non_finish_code_while_active(self):
|
||||
assert _new_cycle_running(
|
||||
{"x.com.samsung.da.state": "Run", "x.com.samsung.da.progress": "Wash"}
|
||||
)
|
||||
|
||||
def test_true_regardless_of_state(self):
|
||||
"""Not gated on machine_state -- a new cycle's own real progress
|
||||
must win over a held hold even while paused (e.g. adding a sock
|
||||
mid-hold), not just while actively running."""
|
||||
assert _live_progress_code(
|
||||
def test_false_for_a_running_stage_reported_after_state_left_active(self):
|
||||
"""Issue #358, the whole reason for the `state` gate: the reporting
|
||||
dryer replays a running stage ('Drying', its first supportedProgress
|
||||
entry) for a few seconds after Finish while winding down. That is
|
||||
the finished cycle's tail, not a new cycle -- releasing the hold on
|
||||
it is what produced 'Drying, Cooling, Finish, Drying, Idle'."""
|
||||
assert not _new_cycle_running(
|
||||
{"x.com.samsung.da.state": "Ready", "x.com.samsung.da.progress": "Drying"}
|
||||
)
|
||||
|
||||
def test_false_while_paused(self):
|
||||
"""Paused is not evidence a new cycle is running, and the tail
|
||||
above can't be told apart from it. rep_fn shows 'Idle' whenever
|
||||
state isn't active anyway, so there is no live value being
|
||||
withheld here -- only a hold that expires on its own instead of
|
||||
being released early."""
|
||||
assert not _new_cycle_running(
|
||||
{"x.com.samsung.da.state": "Pause", "x.com.samsung.da.progress": "Wash"}
|
||||
)
|
||||
|
||||
def test_false_for_finish(self):
|
||||
assert not _live_progress_code({"x.com.samsung.da.progress": "Finish"})
|
||||
assert not _new_cycle_running(
|
||||
{"x.com.samsung.da.state": "Run", "x.com.samsung.da.progress": "Finish"}
|
||||
)
|
||||
|
||||
def test_false_when_absent_or_none(self):
|
||||
assert not _live_progress_code({})
|
||||
assert not _live_progress_code({"x.com.samsung.da.progress": "None"})
|
||||
assert not _new_cycle_running({})
|
||||
assert not _new_cycle_running(
|
||||
{"x.com.samsung.da.state": "Run", "x.com.samsung.da.progress": "None"}
|
||||
)
|
||||
|
||||
|
||||
class TestProgressPercentage:
|
||||
|
||||
Reference in new issue
Block a user