v1.54.2: HP-1541-01 — vacuum selected_map fallback obeys the not-nullish map-id contract

The v1.54.1 contract (first not-None value wins, zero is a value) covered
the source entity but not the card's fallback on the vacuum's own
selected_map: _vacMapId still used truthiness, so selected_map: 0 became
'default' on the frontend while trails.py resolve_map_id stored the run
under '0'. Calibration and server trails split across two keys and the
recorded run never rendered after reload.

The fallback is now the shared pure helper vacMapIdWithFallback (nullish
check), mirroring resolve_map_id. Cross-runtime regressions added for
selected_map = 0, '0' and '' on both sides; the frontend cases fail on the
old truthiness code.
This commit is contained in:
Matysh
2026-07-31 13:44:10 +03:00
parent 909bb6fbc7
commit fad2e87ab5
14 changed files with 109 additions and 17 deletions
+5 -4
View File
@@ -27,7 +27,7 @@ import {
import { ContentSigner } from './signing';
import {
Affine, applyAffine, solveAffine, affineResidual, readVacTelemetry, isVacSourceState,
autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, vacMapIdWithFallback, VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
FitParams, fitMatrix, fitFromMatrix, initialFit, reanchorFit, VacRoom,
VAC_TRAIL_LINGER_MS, Pt as VacPt,
} from './vacuum';
@@ -42,7 +42,7 @@ import { cardStyles } from './styles';
import { fitInSquare, contentBounds, spaceModels } from './space-geometry';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.54.1';
const CARD_VERSION = '1.54.2';
const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
@@ -4505,10 +4505,11 @@ class HouseplanCard extends LitElement {
* live X50 Master) — without this both floors would share one matrix.
*/
private _vacMapId(d: DevItem, tele: { mapId: string }): string {
if (tele.mapId !== 'default') return tele.mapId;
// HP-1541-01: nullish, not truthy — selected_map: 0 is a real map id and
// must equal what trails.py resolve_map_id stores server-side.
const ve = this._vacEntity(d);
const sel = ve ? this.hass?.states[ve]?.attributes?.selected_map : null;
return sel ? String(sel) : 'default';
return vacMapIdWithFallback(tele.mapId, sel);
}
/** Persist a solved matrix into marker.vacuum.calibration[mapId].
+13
View File
@@ -94,6 +94,19 @@ export function vacMapIdFromAttrs(attrs: Record<string, any>): string {
return String(attrs.map_name ?? attrs.current_map ?? attrs.map_index ?? attrs.selected_map ?? 'default');
}
/**
* The card-side fallback half of that contract (HP-1541-01): when source
* telemetry names no map ('default'), the vacuum entity's own selected_map
* decides — under the SAME not-nullish rule as above. The old truthiness
* check in _vacMapId turned `selected_map: 0` into 'default' while the
* server recorder (trails.py resolve_map_id) stored the trail under '0', so
* calibration and saved runs lived under a key the renderer never matched.
*/
export function vacMapIdWithFallback(teleMapId: string, selectedMap: unknown): string {
if (teleMapId !== 'default') return teleMapId;
return selectedMap != null ? String(selectedMap) : 'default';
}
/**
* Normalise the attribute zoo. One parser instead of per-brand classes: the
* three Tier-A integrations (Xiaomi Cloud Map Extractor, Tasshack