Files
houseplan-card/docs/VACUUM.md
T
Codex 8ee1c91fff fix: seven small honesty fixes from the 29.08 audit (#369)
(a) documented: deleting a vacuum marker erases its server trail at once
and a re-added marker starts from scratch (VACUUM.md + both USER-GUIDEs).
(b) smoothVacPath reports dropped non-finite segments — one console warn
per call with the count — instead of hiding the whole trail silently on a
broken calibration matrix. (c) room climate (#317) now reaches legacy
markers whose exported config carries an ABSENT area key rather than an
explicit null: `== null` where the placement is decided. (d) the armed
furniture preview follows Shift without mouse movement — window
keydown/keyup listeners live exactly as long as the palette is armed,
detached at every palette teardown. (e) only the primary mouse button
places decor/furniture: a right or middle click with an armed tool is a
no-op, touch/pen untouched. (f) a device whose registry entities were ALL
deliberately disabled by the user no longer glows as an alive controller —
the #318 entityless-active rule now requires a genuinely empty roster.
(g) furniture-pack author corrected to Sergey Matyunin (Сергей Матюнин)
per the owner's decision — LICENSE.md, README.md, pack.json,
docs/FURNITURE.md, the provenance check in generate-furniture-assets and
its unit; the source archive bytes are unchanged and the README notes the
romanisation fix.

Proofs: units for (b)/(c)/(f) including the #318 regression pair; new
smoke_furniture_polish for (d)/(e) with listener add/remove counters and
both mouse buttons; five registry mutants, one per code change.

Issue: #369
User-Visible: yes
2026-08-29 11:29:42 +03:00

180 lines
8.5 KiB
Markdown

# Live robot vacuums on the plan
Status: implemented contract for the v1.61 development cycle. Stage 1 covers
Tier-A integrations. Roomba string-position support remains a separate Stage 2
issue and is not claimed here.
## What the user sees
The placed vacuum marker is the dock and never moves. While the vacuum is in
`cleaning`, `returning` or `on`, a second round puck follows the live position.
Clicking the puck opens the vacuum's HA more-info dialog. A hidden, deleted,
HA-disabled or `static_icon` marker has no puck, trail or room overlay.
## Integration coverage
| Integration family | Position | Rooms / auto-calibration | Integration path | Map ID | Source discovery |
|---|---:|---:|---:|---|---|
| Xiaomi Cloud Map Extractor | Yes, when the attributes below are enabled | Yes | Yes, including `path.path` subpaths | `map_name` when exposed | Usually explicit camera selection |
| dreame-vacuum (Tasshack) | Yes | Yes; explicit room `x/y` is the anchor | No | Vacuum `selected_map` fallback | Automatic on the same HA device |
| Valetudo camera conventions | Yes | Yes when room data is exposed | No | Often `default`; no stable multi-floor promise | Automatic on the same HA device |
| Roomba core `position` string | Not in Stage 1 | No | No | — | Stage 2 |
For Xiaomi Cloud Map Extractor the camera must expose:
```yaml
attributes:
- vacuum_position
- rooms
- path
- map_name
```
The card recognises finite `vacuum_position` or `robot_position` objects. A
generic `position` string on an unrelated sensor or tracker is never treated as
vacuum telemetry.
## Source resolution and diagnostics
The device dialog has one diagnostic block and one source picker. It reports
the selected entity, integration, status, position, room count, integration
path and map ID. Automatic mode considers compatible entities attached to the
same HA device. Candidate order cannot change the result; a compatible camera
outranks a non-camera candidate. The collapsed **All cameras** section is
scanned only when opened and is never used for automatic binding.
Choosing a candidate stores `marker.vacuum.source`. A stored source is pinned:
it is never silently replaced when it becomes missing, disabled, unavailable,
unverified or unsupported. Restoring the same HA entity restores operation
without editing the plan.
| Status | Meaning and behaviour |
|---|---|
| `ok` | Valid live position is available |
| `unsupported` | Entity exists but has no valid position; its rooms/path may still be usable |
| `unavailable` | Exact HA entity exists but is currently unavailable; stale attributes are not rendered |
| `disabled` | Entity is disabled in HA; stale attributes are not rendered |
| `missing` | Authoritative registry and live states both prove that the saved entity is absent |
| `unverified` | Current HA permissions cannot prove existence or removal; the pin is preserved |
| `none` | No source was selected or found |
Registry-less YAML entities are valid: an exact live HA state is positive
evidence even when a full entity-registry response has no row. A disabled row
still wins. A selected camera without position data gets the XCME attribute
hint; arbitrary unselected cameras do not.
## Calibration
The stored transform is a six-number affine matrix per map:
`marker.vacuum.calibration[map_id]`. Existing matrices are not migrated.
- **Automatic:** at least three room names must match. Robot anchors use
`cx/cy`, then `center.x/y`, then explicit `x/y`, then the polygon area
centroid of `outline`, and finally the centre of a complete `x0/y0/x1/y1`
bounding box. The bbox tier is a compatibility fallback for integrations
that expose no better room geometry. Plan rooms use the area-centroid
definition.
- **Manual fit:** move and uniformly resize the translucent robot-room map;
quarter-turn and mirror controls re-anchor around its centre.
The automatic residual is the worst matched-room error converted to physical
centimetres from the current grid. At `≤ 40 cm` the matrix is saved normally.
At `> 40 cm` nothing is saved until the user explicitly chooses **Apply**.
**Fit manually** opens the proposal in the fit overlay; **Cancel** leaves the
saved configuration byte-for-byte unchanged.
Map ID uses one nullish chain and deliberately ignores volatile values such as
`vacuum_json_id`:
`map_name → current_map → source map_index → source selected_map → vacuum selected_map → default`
Numeric `0`, string `"0"` and an empty string are valid IDs.
## Paths and trails
The current visible path has one authority:
1. drawable integration path;
2. drawable current server run;
3. drawable local runtime buffer;
4. no path.
An integration path can contain several subpaths. They are transformed and
thinned independently and rendered with separate SVG `M` commands, so a data
gap never becomes a long false line. Invalid points and segments shorter than
two points are discarded before limits are applied. The newest 64 drawable
subpaths are kept, with at most 4000 total points; both endpoints of every kept
subpath survive deterministic proportional thinning.
Current and previous trails use the same bounded rounded-corner curve. The
curve preserves every subpath endpoint and never leaves the recorded polyline
by more than 17.5 cm in physical plan coordinates. Smoothing happens after
vacuum calibration and before flat/isometric projection, so zoom, viewport,
DPR and projection do not change that limit. The live target is still trimmed
before rendering, and smoothing never bridges an integration data gap.
| Display mode | While moving | After movement stops |
|---|---|---|
| `never` | Hidden | Hidden |
| `cleaning` (default) | Current path | Hidden immediately |
| `always` | Current path | Current integration path or stored current/previous runs |
Server trails are recorded by `custom_components/houseplan/trails.py`, even
with no card open. It stores current and one previous run in raw robot
coordinates. An available non-moving state ends the visible current run
immediately, but a new point on the same map within an inclusive 30-minute
grace reopens that same run and keeps all earlier points. The first stop fixes
the timestamp: repeated dock/pause samples do not extend the window. A map
change, a longer stop, malformed persisted time or wall-clock rollback starts a
new run. `unavailable`, `unknown` and a missing state remain neutral. Without a
vendor task id, two genuinely separate same-map cleanups started inside the
grace may therefore appear as one run.
Server recording is independent of the display mode. The source
health monitor checks saved marker/source pairs on config refresh and restart:
one warning is emitted for a missing/disabled incident, reason changes are
deduplicated, and another warning is possible only after proven recovery.
Detection is intentionally refresh/restart based in Stage 1; no extra entity
registry subscription is installed.
## Storage and lifecycle
```text
marker.vacuum = {
live?, trail?, trail_mode?, source?,
calibration?: { [map_id]: [a,b,c,d,e,f] },
room_highlight?, segment_map?
}
```
All fields are optional and old plans remain readable. Hiding retains the
configuration. Deleting a vacuum marker removes its layout and server trails,
creates the normal removal tombstone and makes the HA device available for a
fresh add without resurrecting old runs. The backend reconciles both a removal
tombstone and a completely absent marker with the trail store after every
successful config change, so an interrupted browser-side cleanup is repaired.
An initial position sampled during integration startup follows the same
debounced persistence and live-update path as a later state event.
## Troubleshooting
1. Open the vacuum's device settings and read the source diagnostics.
2. If no same-device source is found, open **Choose source → All cameras** and
select the actual map camera.
3. For XCME, enable the four attributes shown above and reload that entity.
4. Ensure the active map has a calibration and the vacuum state is
`cleaning`, `returning` or `on`.
5. A disabled source must be re-enabled in HA or replaced explicitly; House
Plan will not guess a replacement.
Commands, zones/no-go polygons, cleaning-history UI and Roomba string parsing
are outside Stage 1.
## Deleting and restoring a vacuum marker (#369)
Deleting a vacuum marker erases its server-side trail immediately: the next
`config/set` purges trails of removed markers (#335), so an undeleted (re-added
or restored) vacuum starts its trail from scratch. This is deliberate — a
tombstone that kept trails alive used to leak the store — but it means trail
history does not survive marker deletion.