From 9870b2fb989ae7a523c2f771219d76a8581f3773 Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 31 Jul 2026 12:09:31 +0300 Subject: [PATCH] A booting HA must not split the cleanup run At startup the vacuum entity reads unavailable; the recorder took that for 'stopped' and ended the open run, so every HA restart mid-cleanup rotated the trail into previous and began a fresh one (observed live: a 21-point run became previous and restarted at 5). Unavailable and unknown now mean 'no verdict'. --- custom_components/houseplan/trails.py | 8 +++++++- tests_backend/test_trail_recorder.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/custom_components/houseplan/trails.py b/custom_components/houseplan/trails.py index ad00dfc..699933d 100755 --- a/custom_components/houseplan/trails.py +++ b/custom_components/houseplan/trails.py @@ -139,7 +139,13 @@ class TrailRecorder: return False marker, vac = pair st_vac = self.hass.states.get(vac) - if not st_vac or st_vac.state not in MOVING_STATES: + # "no state yet" is NOT "stopped": during HA boot the vacuum reads + # unavailable and ending the run here would split one cleanup into + # current+previous on every restart (observed live: 21 points became + # previous, the same run restarted at 5) + if not st_vac or st_vac.state in ("unavailable", "unknown"): + return False + if st_vac.state not in MOVING_STATES: return self.book.end_run(marker, now) st_src = self.hass.states.get(src) attrs = st_src.attributes if st_src else {} diff --git a/tests_backend/test_trail_recorder.py b/tests_backend/test_trail_recorder.py index adcff4a..1edc421 100644 --- a/tests_backend/test_trail_recorder.py +++ b/tests_backend/test_trail_recorder.py @@ -140,3 +140,15 @@ def test_object_style_position_is_read(): states["camera.map"] = S("idle", {"vacuum_position": Point(2020, 3096), "map_index": 1}) assert rec._sample("camera.map", 5.0) assert rec.book.data["m1"]["current"]["points"] == [[2020.0, 3096.0]] + + +def test_unavailable_vacuum_is_no_verdict(): + # HA boot: the vacuum reads unavailable — the open run must NOT be ended + rec, _hass, states = _rec() + rec._sample("camera.map", 1.0) + states["vacuum.x50"] = S("unavailable", {}) + assert not rec._sample("camera.map", 2.0) + assert rec.book.data["m1"]["current"]["ended"] is None + del states["vacuum.x50"] + assert not rec._sample("camera.map", 3.0) + assert rec.book.data["m1"]["current"]["ended"] is None