Trail recorder reads object-style positions — the real-world fix

Caught live on the owner's X50 mid-cleanup with temporary logging: the
recorder saw every camera state change and rejected every one, because
server-side Tasshack keeps vacuum_position as a Point OBJECT — it only
becomes a dict when serialised to the frontend, which is why the card
adapter (and MCP inspection) always saw a dict and the stub test
faithfully reproduced the same wrong assumption. getattr fallback added,
regression test pinned, diagnostic logging removed (setup line kept at
INFO).
This commit is contained in:
Matysh
2026-07-31 12:04:33 +03:00
parent 4e355dcbd2
commit 18ac9b459f
2 changed files with 25 additions and 5 deletions
+15 -5
View File
@@ -19,6 +19,9 @@ from homeassistant.helpers.storage import Store
from .const import DOMAIN
import logging
_LOGGER = logging.getLogger(__name__)
TRAIL_CAP = 2000 # raw points per run before decimation
SAVE_DELAY_S = 10 # debounce store writes — flash wear over precision
FIRE_THROTTLE_S = 2.0 # event-bus updates for live cards
@@ -99,6 +102,7 @@ class TrailRecorder:
if vac:
self.pairs[src] = (str(m.get("id")), vac)
ents = set(self.pairs) | {vac for _, vac in self.pairs.values()}
_LOGGER.info("Trail recorder: tracking %s", sorted(ents))
if ents:
self._unsub_track = async_track_state_change_event(
self.hass, sorted(ents), self._on_state
@@ -139,12 +143,18 @@ class TrailRecorder:
return self.book.end_run(marker, now)
st_src = self.hass.states.get(src)
attrs = st_src.attributes if st_src else {}
pos = attrs.get("vacuum_position") or attrs.get("robot_position")
if not isinstance(pos, dict):
return False
raw = attrs.get("vacuum_position") or attrs.get("robot_position")
# Server-side these attributes are often OBJECTS (Tasshack keeps a
# Point dataclass in memory — it only becomes a dict when serialised
# to the frontend). Caught live on the owner's X50: the recorder saw
# every state change and rejected every single one.
if isinstance(raw, dict):
px, py = raw.get("x"), raw.get("y")
else:
px, py = getattr(raw, "x", None), getattr(raw, "y", None)
try:
x, y = float(pos["x"]), float(pos["y"])
except (KeyError, TypeError, ValueError):
x, y = float(px), float(py) # type: ignore[arg-type]
except (TypeError, ValueError):
return False
map_id = str(
attrs.get("map_name")
+10
View File
@@ -130,3 +130,13 @@ def test_junk_position_ignored():
def test_unknown_source_is_noop():
rec, _hass, _states = _rec()
assert not rec._sample("camera.other", 1.0)
def test_object_style_position_is_read():
# Tasshack in-memory attributes hold a Point OBJECT, not a dict
class Point:
def __init__(self, x, y): self.x, self.y = x, y
rec, _hass, states = _rec()
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]]