mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
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:
@@ -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")
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user