fix: polish vacuum map routes

Treat explicit empty route lists as authoritative, preserve them through single-space export, group deleted-space routes, and render vacuums from the immutable vacuum-only snapshot subset.

Issue: #443
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-03 23:24:08 +03:00
parent 83201da15d
commit cebd20a9b5
42 changed files with 553 additions and 292 deletions
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
- Removing a robot vacuum's last map route now keeps the route list explicitly
empty instead of reviving legacy calibration; routes to deleted spaces are
grouped clearly in the editor, and the live layer avoids rescanning every
non-vacuum device ([#443](https://github.com/Matysh/houseplan-card/issues/443)).
- A robot vacuum's second and later maps can now be added: the editor asks for
the target floor before saving the route, and a Home Assistant rejection no
longer leaves a phantom local route
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- После удаления последнего маршрута карты робота список теперь остаётся явно
пустым и не возвращает старую калибровку; маршруты в удалённые пространства
собраны в понятную группу, а живой слой больше не перебирает повторно все
устройства, не являющиеся роботами
([#443](https://github.com/Matysh/houseplan-card/issues/443)).
- Вторую и последующие карты робота теперь можно добавить: сначала
редактор просит выбрать этаж и только потом сохраняет маршрут,
а отказ Home Assistant не оставляет ложную локальную запись
+9 -5
View File
@@ -84,10 +84,12 @@ validated boolean.
and the pair `(source, map_id)` unique inside the marker. It requires no model
or store version bump: absence reads as the historical behaviour.
Reading is lossless in both directions. Without `map_routes` every valid
`calibration[map_id]` is an effective route into the dock's space, so nothing
is migrated on load and an ordinary save of other marker fields leaves the
vacuum block untouched. The first explicit routing edit converts the whole
Reading is lossless in both directions. When `map_routes` is absent or `null`,
every valid `calibration[map_id]` is an effective route into the dock's space,
so nothing is migrated on load and an ordinary save of other marker fields
leaves the vacuum block untouched. Any array is explicit authority: an empty
`map_routes: []` means that no route is configured and suppresses every
retained legacy matrix. The first explicit routing edit converts the whole
legacy dictionary at once and needs an exact source to do it; partial
conversion is refused, and `calibration` is removed only after the config write
succeeds.
@@ -97,7 +99,9 @@ never blocks an unrelated save, while an edited one must be valid or the write
is refused atomically with `invalid_vacuum_map_route`. A full export/import
round-trips routes verbatim. A single-space export drops routes that point at
other spaces and counts them in `dropped_marker_links`, because their target
would not exist in the imported document.
would not exist in the imported document. If that removes the last explicit
route, the export keeps `map_routes: []` rather than writing `null`, so a later
import cannot make legacy calibration authoritative again.
Downgrade: an older frontend ignores `map_routes` and falls back to the legacy
`calibration` dictionary, which the conversion removed — such a plan shows the
+3
View File
@@ -1507,6 +1507,9 @@ Map Extractor, dreame-vacuum (Tasshack) и Valetudo-подобные камер
текущая карта. Пока карта не сопоставлена, не откалибрована или неотличима
от другой, робот не рисуется нигде — у дока появляется предупреждение
с причиной.
Маршруты, которые ссылаются на уже удалённое пространство, собраны в конце
списка в отдельной группе «Пространство удалено». Их можно сопоставить другому
пространству или удалить; House Plan не переносит такие маршруты автоматически.
Если максимальное расхождение больше 40 см, настройки не меняются до выбора
«Применить». Можно вместо этого открыть ручную подгонку или отменить операцию.
+10 -7
View File
@@ -191,13 +191,16 @@ marker.vacuum = {
}
```
All fields are optional and old plans remain readable. Without `map_routes`
every valid `calibration[map_id]` is read as an effective route into the dock's
space, so a plan made before #162 renders byte for byte as before. The first
explicit routing edit converts the whole dictionary at once — all matrices or
none — and needs an exact source to do it; `calibration` is dropped only after
the config write succeeds. Where both exist, `map_routes` is the only
authority and the legacy dictionary takes no part in rendering.
All fields are optional and old plans remain readable. When `map_routes` is
absent or `null`, every valid `calibration[map_id]` is read as an effective
route into the dock's space, so a plan made before #162 renders byte for byte
as before. Any array is explicit authority: `map_routes: []` means that no map
is assigned and must not revive a retained legacy matrix. The first explicit
routing edit converts the whole dictionary at once — all matrices or none —
and needs an exact source to do it; `calibration` is dropped only after the
config write succeeds. Where both exist, `map_routes` is the only authority
and the legacy dictionary takes no part in rendering. A single-space export
also preserves an explicit empty array when all foreign routes are filtered.
A stored run carries the route that wrote it:
`{ route_id, source, map_id, started, ended, points }`. A run recorded before