Files
houseplan-card/docs/VACUUM.md
T
Matysh 69c5a4c41d Vacuum first-use path: materialize the marker, count rectangle rooms, fix the toasts
Audit HP-1540-01 (High): an auto-discovered vacuum has no config marker
until the device dialog is saved once, yet the live-position section was
already interactive. setVac, _vacSaveMatrix and auto-calibration all did
cfg.markers.find(...) and silently bailed out — while the auto-calibration
toast still claimed success. Every vacuum edit now materializes a minimal
marker (same id/binding the dialog Save would produce), _vacSaveMatrix
reports whether the write landed, and success toasts are gated on it.

HP-1540-04: the auto-calibration room matcher accepted only polygon rooms
and told users their room names did not match. It goes through the shared
roomPoly() now, so legacy x/y/w/h rectangles count like everywhere else.

HP-1540-06: the no-rooms/no-match/rough-fit toasts pointed at the removed
point calibration; they now point at the fit panel that shipped instead,
and docs/VACUUM.md Setup UX describes the actual UI. Also extracted
vacMapIdFromAttrs as the explicit frontend half of the map-id contract
(backend half lands with HP-1540-02).

Regressions: demo/smoke_vacuum_firstuse.mjs starts from cfg.markers=[]
(the fixture gap the audit called out) with rectangle plan rooms and a
zero map_index, and fails 11 checks on the v1.54.0 bundle; i18n unit test
asserts no point/точк wording in either language.
2026-07-31 13:07:16 +03:00

157 lines
8.5 KiB
Markdown

# Live robot vacuums on the plan — the spec (source of truth)
Status: approved by the owner 2026-07-31. Scope decisions final: all
three Tier-A adapters in P1, the trail ships in P1, the marker's placed
position IS the dock, and tap-to-clean is out — display only, no
commands (owner: «не нужно вообще»).
## Principle
The device marker «Пылесос» NEVER moves: it stands where the user
placed it — that is the base/dock — with its normal badge, states and
tap actions. While the robot cleans, a SECOND visual appears: a round
puck (the vacuum icon in a circle), no badge plate, with a soft
pulse, driving around the plan on live coordinates. Owner's exact
wording: «Иконка движущегося пылесоса должна быть круглой, без
подложки и с легкой пульсацией. Основное устройство "Пылесос" при этом
остается всегда на своем месте (база)». The puck is not clickable UI
chrome duplicating the device — a tap on it opens the same more-info as
the base marker. When the robot docks, the puck drives home and
dissolves into the base marker.
## Data tiers (auto-detected, zero user input)
- **Tier A — coordinates + own calibration data.** `vacuum_position
{x,y,angle}` (+ `calibration_points`, rooms, sometimes `path`):
Xiaomi Cloud Map Extractor, dreame-vacuum (Tasshack), Valetudo camera
(sca075). All three ship in P1.
- **Tier B — bare coordinates.** Roomba core (`position {x,y,theta}`),
raw Valetudo MQTT, template sensors. Works after manual calibration.
P3.
- **Tier C — current room only.** `current_room` / `current_segment`.
The room being cleaned gets a soft fill pulse + a 🤖 badge in the
room card; no puck. Segment→room matched by name, manual override in
the dialog.
- **Tier D — nothing.** Today's behaviour; yellow badge on the base
marker while cleaning per the «yellow = working right now» principle.
Tiers degrade gracefully: coords gone → room highlight; that gone too →
just the base marker.
## Coordinate binding
Affine transform (translate+rotate+scale+mirror), 6 numbers, least
squares over ≥3 point pairs. Stored per robot map:
`marker.vacuum.calibration[map_id]` — multi-floor robots get one matrix
per map; an active map without a matrix falls back to Tier C.
- **Path 1, auto-calibration by rooms (the default):** Tier-A adapters
expose the robot's room list with coordinates; our rooms carry HA
area bindings. Match by name, take centroids of ≥3 matches, solve,
show a live preview («робот сейчас здесь») — one click to confirm.
- **Path 2, the fit panel (replaced the 3-point wizard, owner call
2026-07-31):** the robot's rooms render as a translucent dashed ghost
over the plan; the user DRAGS the ghost into place and stretches it by
its four corner handles (uniform scale about the opposite corner, like
a graphics-editor frame). Rotation is quarter-turn buttons, mirror is
one toggle — both re-anchor about the ghost centre. Mirror defaults ON:
every robot map seen so far has Y flipped versus the screen. No numeric
fields; the old park-the-robot-three-times wizard is gone entirely —
it was the most fragile part of the feature.
- The panel folds into the same stored 6-number matrix
(S·R(rot)·mirror + offset); legacy matrices reopen in the panel with
the rotation snapped to the nearest quarter.
## Puck behaviour
Appears when the robot leaves `docked` AND live coords flow; CSS
interpolation ~1.2 s between updates; a data gap over 10 s teleports
without animation (no gliding through walls on sparse cloud updates).
Soft pulse always while visible (respects prefers-reduced-motion: the
pulse freezes, position still animates). Size: same --icon-size
system as devices, circle, transparent background, no badge plate.
Stale coords (>60 s while «cleaning»): puck freezes and dims. On
`docked`/`idle`-at-base the puck rides home and fades out. Hidden
markers render neither base nor puck (general hidden rules). Works in
the full card, static card and kiosk; in editors no puck — the base
marker is edited as usual.
## Trail (ships in P1)
- Integration `path` when available (Map Extractor) — transform and
draw as-is, includes history from before the card was opened.
- Otherwise self-recorded: client-side ring buffer, ~600 points with
Douglas-Peucker thinning, one SVG polyline.
- Style fixed, no options: cartography casing — a dark translucent halo under a light core. Neutral (pure black/white alphas) and readable over any room fill; blend modes were rejected: each has a blind luminance where the line vanishes, and mix-blend-mode is costly on old kiosk WebViews. Lifecycle:
appears on cleaning start, lives until dock + 10 min, dissolves;
`docked → cleaning` clears the old trail. Recorded SERVER-SIDE by the integration itself (trails.py): it watches the source entity, so the path records with zero cards open and every screen sees the same line. Stored per marker: the current run and ONE previous run (owner call — users compare cleaned vs uncleaned). The previous run renders at 40% opacity even at rest; the current run trims its live tail while moving. Rotation on run start or map switch; 2000-point cap with decimation; store writes debounced 10 s; houseplan_trail_updated notifies live cards.
- Marker option «Показывать след уборки», on by default where data
exists.
## Setup UX
A «Живая позиция» section in the device dialog, vacuum markers only:
status line (which source was found / nothing), the «Настроить
автоматически» button (Tier A — the integration reports a room list),
the «Подогнать вручную» button opening the fit panel (drag the ghost
map, stretch by the corners, rotate/mirror), one «Живая позиция на
плане» checkbox (on by default), the «Показывать путь робота» select
(never / while cleaning / always), and for multi-map robots a list of
calibrated maps. The source entity is discovered via the device
registry — no YAML, no entity pickers. The section works before the
marker is ever saved: the first vacuum edit materialises the marker
itself (HP-1540-01).
## Storage & validation
`marker.vacuum: { live, trail, room_highlight, source, calibration:
{[map_id]: [6 numbers]}, segment_map: {[segment]: room_id} }` — all
optional (old configs stay valid), matrices are 6 finite numbers,
backend-validated. Calibration is server-side state shared by every
screen, like the whole plan.
## Cases covered explicitly
Two vacuums (independent markers, calibrations, pucks); one robot on
two floors (maps ↔ spaces, the puck only renders in the space whose map
is active); robot outside the plan (±4 canvas bounds allow it, trail
clipped by viewport); integration restart changes map_id (rebind by
room-list match, else ask to recalibrate); user redraws rooms (the
matrix does not depend on rooms); demo stand gets a scripted synthetic
robot as the showcase.
## Out of scope
Commands of any kind (owner decision: display only) — no tap-to-clean,
no zone sending. No-go zones, cleaned-area polygons, cleaning history,
multi-robot collision avoidance — v2 candidates.
## Trail display modes
`marker.vacuum.trail_mode`: `never` | `cleaning` (default — the line hides
the instant the run ends) | `always` (the only mode that also draws the
previous run, at 40% opacity). The legacy boolean `trail` still maps in
(`false` → never). Recording is independent of the mode: the server always
records, the mode only decides what is drawn.
The last segment is a rAF-driven tip line whose endpoint is glued to the
puck's animated centre every frame, so the path pours out from under the
icon instead of popping in when the next telemetry point lands.
## Phases
- **P1:** adapter framework + all three Tier-A adapters +
auto-calibration by rooms + manual 3-point wizard + the puck + trail (both sources).
- **P2 (next):** Tier C room highlight + the demo-stand scripted robot.
- **P3:** Tier B zoo (Roomba, raw Valetudo MQTT), Deebot, multi-map
polish by feedback.
## Shipped in P1
Adapters for the three Tier-A integrations, auto-calibration by room
names, the drag-and-stretch fit panel, the puck, server-side trails
(current + previous run) with the three display modes. Verified against a
live Dreame X50 Master: room centres arrive as plain x/y, the active map
name lives on the vacuum entity (`selected_map`), and the robot's Y axis
is flipped versus the screen — hence mirror-on by default.