mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
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.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// v1.54.0 audit regressions (HP-1540-01/-04/-06): the FIRST-USE path.
|
||||
// An auto-discovered vacuum has NO marker in the config — cfg.markers stays
|
||||
// empty here, unlike smoke_vacuum.mjs which always pushes one by hand (the
|
||||
// audit called that gap out explicitly). Every vacuum action must materialise
|
||||
// the marker and actually persist; plan rooms are legacy RECTANGLES.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
const { page, browser } = await launch();
|
||||
|
||||
const out = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const o = {};
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const cfg = c._serverCfg;
|
||||
|
||||
// legacy rectangle rooms (x/y/w/h, HP-1540-04) in the garden space where
|
||||
// the auto-discovered vacuum.mower already lives
|
||||
const g = cfg.spaces.find((s) => s.id === 'garden');
|
||||
g.rooms = [
|
||||
{ id: 'ga', name: 'Кухня', area: 'garden', x: 0.05, y: 0.10, w: 0.25, h: 0.30 },
|
||||
{ id: 'gb', name: 'Зал', x: 0.40, y: 0.10, w: 0.25, h: 0.30 },
|
||||
{ id: 'gc', name: 'Спальня', x: 0.05, y: 0.50, w: 0.25, h: 0.30 },
|
||||
];
|
||||
// robot rooms: same centres in "robot mm" (scale 4), map_index is a
|
||||
// NUMERIC ZERO — the cross-runtime zero-map contract (HP-1540-02)
|
||||
const robotRooms = {
|
||||
1: { name: 'Кухня', x0: 300, y0: 600, x1: 1100, y1: 1400 },
|
||||
2: { name: 'Зал', x0: 1700, y0: 600, x1: 2500, y1: 1400 },
|
||||
3: { name: 'Спальня', x0: 300, y0: 2200, x1: 1100, y1: 3000 },
|
||||
};
|
||||
const camAttrs = { vacuum_position: { x: 700, y: 1000, a: 0 }, map_index: 0, rooms: robotRooms };
|
||||
c.hass = { ...c.hass,
|
||||
entities: { ...c.hass.entities,
|
||||
'camera.mower_map': { entity_id: 'camera.mower_map', device_id: 'd_mower', platform: 'demo' } },
|
||||
states: { ...c.hass.states,
|
||||
'camera.mower_map': { state: 'idle', attributes: camAttrs } } };
|
||||
c._regSignature = '';
|
||||
c.requestUpdate(); await c.updateComplete;
|
||||
|
||||
// spy on config writes: a success toast is only honest after one of these
|
||||
const writes = [];
|
||||
const realWS = c.hass.callWS;
|
||||
c.hass.callWS = async (m) => { if (m.type === 'houseplan/config/set') writes.push(m); return realWS(m); };
|
||||
|
||||
const dev = c._devices.find((x) => x.id === 'd_mower');
|
||||
o.devFound = !!dev;
|
||||
o.freshNoMarker = (cfg.markers || []).length === 0 && !dev.marker;
|
||||
|
||||
// ---- auto-calibration from scratch (HP-1540-01 + HP-1540-04) ----
|
||||
c._vacAutoCalibrate(dev);
|
||||
await c.updateComplete;
|
||||
const m1 = (cfg.markers || []).find((x) => x.id === 'd_mower');
|
||||
o.markerMaterialised = !!m1 && m1.binding === 'device:d_mower';
|
||||
const cal0 = m1?.vacuum?.calibration?.['0'];
|
||||
o.rectRoomsCalibrated = Array.isArray(cal0) && cal0.length === 6 && cal0.every(Number.isFinite);
|
||||
o.sourceStored = m1?.vacuum?.source === 'camera.mower_map';
|
||||
// the matrix really maps robot mm → canvas: centre of «Кухня» → (175, 250)
|
||||
const ap = (mm, x, y) => [mm[0] * x + mm[1] * y + mm[2], mm[3] * x + mm[4] * y + mm[5]];
|
||||
const hit = cal0 ? ap(cal0, 700, 1000) : [0, 0];
|
||||
o.matrixMapsRooms = Math.hypot(hit[0] - 175, hit[1] - 250) < 2;
|
||||
o.successToastShown = (c._toast || '').startsWith('Done: bound via 3 rooms');
|
||||
await new Promise((r) => setTimeout(r, 700)); // debounced _saveConfig
|
||||
o.configWritePersisted = writes.length >= 1
|
||||
&& !!writes[writes.length - 1].config.markers.find((x) => x.id === 'd_mower')?.vacuum?.calibration?.['0'];
|
||||
|
||||
// ---- manual fit from scratch (HP-1540-01) ----
|
||||
cfg.markers = [];
|
||||
c._regSignature = ''; c.requestUpdate(); await c.updateComplete;
|
||||
const dev2 = c._devices.find((x) => x.id === 'd_mower');
|
||||
o.fitFreshNoMarker = !dev2.marker;
|
||||
c._vacStartFit(dev2); await c.updateComplete;
|
||||
c._vacFitSave(); await c.updateComplete;
|
||||
const m2 = (cfg.markers || []).find((x) => x.id === 'd_mower');
|
||||
const calFit = m2?.vacuum?.calibration?.['0'];
|
||||
o.fitMaterialises = !!m2 && Array.isArray(calFit) && calFit.length === 6;
|
||||
o.fitToastAfterSave = c._toast === c._t('vac.cal_done');
|
||||
|
||||
// ---- live checkbox + trail select from scratch (HP-1540-01) ----
|
||||
cfg.markers = [];
|
||||
c._regSignature = ''; c.requestUpdate(); await c.updateComplete;
|
||||
c._setMode('devices'); await c.updateComplete;
|
||||
c._openMarkerDialog(c._devices.find((x) => x.id === 'd_mower')); await c.updateComplete;
|
||||
const liveBox = sr().querySelector('.vacbox input[type=checkbox]');
|
||||
o.vacSectionShown = !!liveBox;
|
||||
if (liveBox) { liveBox.click(); await c.updateComplete; }
|
||||
const m3 = (cfg.markers || []).find((x) => x.id === 'd_mower');
|
||||
o.liveToggleMaterialises = !!m3 && m3.vacuum?.live === false;
|
||||
const sel = sr().querySelector('.vacbox select');
|
||||
if (sel) { sel.value = 'always'; sel.dispatchEvent(new Event('change')); await c.updateComplete; }
|
||||
o.trailModePersists = m3?.vacuum?.trail_mode === 'always';
|
||||
c._markerDialog = null; c._setMode('view'); await c.updateComplete;
|
||||
|
||||
// ---- error copy never points at the removed point calibration (HP-1540-06) ----
|
||||
const toasts = ['vac.autocal_no_rooms', 'vac.autocal_no_match', 'vac.autocal_res_warn']
|
||||
.map((k) => c._t(k)).join(' | ');
|
||||
o.noPointCalibrationCopy = !/point|точк/i.test(toasts);
|
||||
// and the real no-rooms path shows the reworded toast
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
'camera.mower_map': { state: 'idle', attributes: { vacuum_position: { x: 1, y: 2, a: 0 }, map_index: 0 } } } };
|
||||
await c.updateComplete;
|
||||
c._vacAutoCalibrate(c._devices.find((x) => x.id === 'd_mower'));
|
||||
o.noRoomsToastReworded = c._toast === c._t('vac.autocal_no_rooms') && !/point|точк/i.test(c._toast);
|
||||
|
||||
c.hass.callWS = realWS;
|
||||
return o;
|
||||
});
|
||||
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
+10
-6
@@ -91,12 +91,16 @@ marker is edited as usual.
|
||||
## Setup UX
|
||||
|
||||
A «Живая позиция» section in the device dialog, vacuum markers only:
|
||||
status line (which source was found / room-only / nothing), one button
|
||||
(«Настроить автоматически» for Tier A, «Калибровка по трём точкам» for
|
||||
Tier B), three checkboxes (live position / trail / room highlight, all
|
||||
on), and for multi-map robots a per-map calibration status list. The
|
||||
source entity is discovered via the device registry — no YAML, no
|
||||
entity pickers.
|
||||
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
|
||||
|
||||
|
||||
+57
-14
@@ -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.0';
|
||||
const CARD_VERSION = '1.54.1';
|
||||
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';
|
||||
@@ -4423,6 +4423,35 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HP-1540-01: an auto-discovered vacuum has NO config marker until the first
|
||||
* general Save of its dialog, yet the «Живая позиция» section is already
|
||||
* interactive. Every vacuum handler used to `cfg.markers.find(...)` and
|
||||
* silently no-op without one — auto-calibration even toasted success while
|
||||
* saving nothing. Materialise a minimal marker (same id/binding the dialog
|
||||
* Save would produce — see _saveMarker/markerIdForBinding) before any
|
||||
* vacuum write, so the write has somewhere real to land.
|
||||
*/
|
||||
private _vacEnsureMarker(d: DevItem): Marker | null {
|
||||
const cfg = this._serverCfg;
|
||||
if (!cfg) return null;
|
||||
cfg.markers = cfg.markers || [];
|
||||
const existing = cfg.markers.find((x: Marker) => x.id === d.id);
|
||||
if (existing) return existing;
|
||||
if ((d.bindingKind !== 'device' && d.bindingKind !== 'entity') || !d.bindingRef) return null;
|
||||
const m: Marker = {
|
||||
id: d.id,
|
||||
binding: d.bindingKind + ':' + d.bindingRef,
|
||||
space: d.space || null,
|
||||
area: d.area || null,
|
||||
// explicit like _saveMarker: a marker of any kind tells the seeder
|
||||
// "the user decided" (docs/FILTERING.md)
|
||||
hidden: d.hidden ? true : false,
|
||||
};
|
||||
cfg.markers.push(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
/** «Живая позиция» in the device dialog — vacuum markers only. */
|
||||
private _renderVacSection(dlg: any): TemplateResult | typeof nothing {
|
||||
const dev = this._devices.find((x) => x.id === dlg.devId);
|
||||
@@ -4436,8 +4465,9 @@ class HouseplanCard extends LitElement {
|
||||
: this._t('vac.status_none');
|
||||
const cals = Object.keys(v.calibration || {});
|
||||
const setVac = (patch: Record<string, unknown>) => {
|
||||
const cfg = this._serverCfg;
|
||||
const m = cfg?.markers?.find((x: Marker) => x.id === dev.id);
|
||||
// HP-1540-01: materialise the marker first — find() alone silently
|
||||
// dropped every edit for a vacuum that had never been saved
|
||||
const m = this._vacEnsureMarker(dev);
|
||||
if (!m) return;
|
||||
m.vacuum = { ...(m.vacuum || {}), ...patch };
|
||||
this._regSignature = '';
|
||||
@@ -4481,11 +4511,15 @@ class HouseplanCard extends LitElement {
|
||||
return sel ? String(sel) : 'default';
|
||||
}
|
||||
|
||||
/** Persist a solved matrix into marker.vacuum.calibration[mapId]. */
|
||||
private _vacSaveMatrix(markerId: string, source: string, mapId: string, matrix: Affine): void {
|
||||
const cfg = this._serverCfg;
|
||||
const m = cfg?.markers?.find((x: Marker) => x.id === markerId);
|
||||
if (!m) return;
|
||||
/** Persist a solved matrix into marker.vacuum.calibration[mapId].
|
||||
* Returns whether the write actually landed — callers must not toast
|
||||
* success otherwise (HP-1540-01). */
|
||||
private _vacSaveMatrix(markerId: string, source: string, mapId: string, matrix: Affine): boolean {
|
||||
// HP-1540-01: a first-use vacuum has no marker yet — materialise it
|
||||
const dev = this._devices.find((x) => x.id === markerId);
|
||||
const m = dev ? this._vacEnsureMarker(dev)
|
||||
: this._serverCfg?.markers?.find((x: Marker) => x.id === markerId);
|
||||
if (!m) return false;
|
||||
const v = { ...(m.vacuum || {}) };
|
||||
v.source = source;
|
||||
v.calibration = { ...(v.calibration || {}), [mapId]: matrix.map((n) => Number(n.toFixed(6))) };
|
||||
@@ -4493,6 +4527,7 @@ class HouseplanCard extends LitElement {
|
||||
this._regSignature = '';
|
||||
this._saveConfig();
|
||||
this.requestUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** «Настроить автоматически»: robot rooms ↔ plan rooms by name. */
|
||||
@@ -4505,9 +4540,14 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
const sp = this._spaceModel(d.space);
|
||||
const planRooms = (sp?.rooms || [])
|
||||
.filter((r: any) => r.name && r.poly?.length >= 3)
|
||||
.map((r: any) => {
|
||||
const c = poleOfInaccessibility(r.poly);
|
||||
// HP-1540-04: legacy rectangle rooms (x/y/w/h) are still first-class
|
||||
// everywhere else — roomPoly() gives the same 4-corner outline the
|
||||
// renderer uses, so they must count for name-matching too. The old
|
||||
// r.poly?.length filter dropped them and then blamed the room names.
|
||||
.map((r: any) => ({ r, poly: roomPoly(r) }))
|
||||
.filter(({ r, poly }: any) => r.name && poly)
|
||||
.map(({ r, poly }: any) => {
|
||||
const c = poleOfInaccessibility(poly);
|
||||
return { name: r.name, cx: c[0], cy: c[1] };
|
||||
});
|
||||
const res = autoCalibrate(tele.rooms, planRooms);
|
||||
@@ -4516,10 +4556,12 @@ class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
// residual gate: 5% of the canvas ≈ 40 cm in a typical house
|
||||
// HP-1540-01: toast success ONLY after the matrix verifiably landed in
|
||||
// the config — the old code always claimed victory, even as a no-op
|
||||
if (!this._vacSaveMatrix(d.id, src, this._vacMapId(d, tele), res.matrix)) return;
|
||||
if (res.residual > NORM_W * 0.05) {
|
||||
this._showToast(subst(this._t('vac.autocal_res_warn'), { rooms: String(res.matched.length) }));
|
||||
}
|
||||
this._vacSaveMatrix(d.id, src, this._vacMapId(d, tele), res.matrix);
|
||||
this._showToast(subst(this._t('vac.autocal_done'), { rooms: String(res.matched.length) }));
|
||||
}
|
||||
|
||||
@@ -4545,9 +4587,10 @@ class HouseplanCard extends LitElement {
|
||||
private _vacFitSave(): void {
|
||||
const f = this._vacFit;
|
||||
if (!f) return;
|
||||
this._vacSaveMatrix(f.markerId, f.source, f.mapId, fitMatrix(f.p));
|
||||
// HP-1540-01: no success toast for a save that did not happen
|
||||
const ok = this._vacSaveMatrix(f.markerId, f.source, f.mapId, fitMatrix(f.p));
|
||||
this._vacFit = null;
|
||||
this._showToast(this._t('vac.cal_done'));
|
||||
if (ok) this._showToast(this._t('vac.cal_done'));
|
||||
}
|
||||
|
||||
/** Rotate/mirror around the ghost centre so the map does not fly away. */
|
||||
|
||||
+3
-3
@@ -363,9 +363,9 @@
|
||||
"vac.live": "Live position on the plan",
|
||||
"vac.trail": "Show the robot's path",
|
||||
"vac.cal_maps": "Calibrated maps: {maps}",
|
||||
"vac.autocal_no_rooms": "The integration reports no room list — use point calibration",
|
||||
"vac.autocal_no_match": "Room names did not match (need ≥3 in common) — use point calibration",
|
||||
"vac.autocal_res_warn": "Matched {rooms} rooms but the fit is rough — verify and refine with points if needed",
|
||||
"vac.autocal_no_rooms": "The integration reports no room list — open “Fit manually”",
|
||||
"vac.autocal_no_match": "Room names did not match (need ≥3 in common) — open “Fit manually”",
|
||||
"vac.autocal_res_warn": "Matched {rooms} rooms but the fit is rough — verify and refine via “Fit manually” if needed",
|
||||
"vac.autocal_done": "Done: bound via {rooms} rooms. Start a cleanup and check",
|
||||
"vac.cal_need_pos": "The robot is not reporting coordinates — start a cleanup and pause it",
|
||||
"vac.cal_done": "Calibration saved. Start a cleanup and check",
|
||||
|
||||
+3
-3
@@ -363,9 +363,9 @@
|
||||
"vac.live": "Живая позиция на плане",
|
||||
"vac.trail": "Показывать путь робота",
|
||||
"vac.cal_maps": "Откалиброваны карты: {maps}",
|
||||
"vac.autocal_no_rooms": "Интеграция не отдаёт список комнат — используйте калибровку по точкам",
|
||||
"vac.autocal_no_match": "Не совпали имена комнат (нужно ≥3 общих) — используйте калибровку по точкам",
|
||||
"vac.autocal_res_warn": "Совпало комнат: {rooms}, но привязка грубовата — проверьте и при необходимости откалибруйте по точкам",
|
||||
"vac.autocal_no_rooms": "Интеграция не отдаёт список комнат — откройте «Подогнать вручную»",
|
||||
"vac.autocal_no_match": "Не совпали имена комнат (нужно ≥3 общих) — откройте «Подогнать вручную»",
|
||||
"vac.autocal_res_warn": "Совпало комнат: {rooms}, но привязка грубовата — проверьте и при необходимости откройте «Подогнать вручную»",
|
||||
"vac.autocal_done": "Готово: привязка по {rooms} комнатам. Запустите уборку и проверьте",
|
||||
"vac.cal_need_pos": "Робот сейчас не отдаёт координаты — запустите уборку и поставьте на паузу",
|
||||
"vac.cal_done": "Калибровка сохранена. Запустите уборку и проверьте",
|
||||
|
||||
+14
-1
@@ -81,6 +81,19 @@ const num = (v: unknown): number | null => {
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map-id normalisation contract, shared with the backend recorder
|
||||
* (custom_components/houseplan/trails.py: resolve_map_id). The FIRST value
|
||||
* that is not null/undefined wins — truthiness is wrong here, because a
|
||||
* zero-based `map_index: 0` is a perfectly valid first map and an empty
|
||||
* string is still an id. The backend used an `or`-chain and dropped the
|
||||
* zero, so server trails were stored under a key the renderer never looked
|
||||
* up (HP-1540-02).
|
||||
*/
|
||||
export function vacMapIdFromAttrs(attrs: Record<string, any>): string {
|
||||
return String(attrs.map_name ?? attrs.current_map ?? attrs.map_index ?? attrs.selected_map ?? 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the attribute zoo. One parser instead of per-brand classes: the
|
||||
* three Tier-A integrations (Xiaomi Cloud Map Extractor, Tasshack
|
||||
@@ -134,7 +147,7 @@ export function readVacTelemetry(attrs: Record<string, any> | null | undefined):
|
||||
rooms.push(room);
|
||||
}
|
||||
}
|
||||
const mapId = String(attrs.map_name ?? attrs.current_map ?? attrs.map_index ?? attrs.selected_map ?? 'default');
|
||||
const mapId = vacMapIdFromAttrs(attrs);
|
||||
if (!pos && !rooms.length && !path) return null;
|
||||
return { pos, path, rooms, mapId };
|
||||
}
|
||||
|
||||
@@ -25,3 +25,11 @@ test('i18n: placeholders match between languages', () => {
|
||||
assert.equal(ph(en[k]), ph(ru[k]), `placeholder mismatch in ${k}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('vac toasts never mention the removed point calibration (HP-1540-06)', () => {
|
||||
for (const [lang, d] of [['en', en], ['ru', ru]]) {
|
||||
for (const k of ['vac.autocal_no_rooms', 'vac.autocal_no_match', 'vac.autocal_res_warn']) {
|
||||
assert.ok(!/point|точк/i.test(d[k]), `${lang}:${k} still points at point calibration`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+18
-1
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { solveAffine, applyAffine, affineResidual, readVacTelemetry, autoCalibrate, thinPath, pushTrailPoint, isVacMoving, isVacSourceState } from '../test-build/vacuum.js';
|
||||
import { solveAffine, applyAffine, affineResidual, readVacTelemetry, autoCalibrate, thinPath, pushTrailPoint, isVacMoving, isVacSourceState, vacMapIdFromAttrs } from '../test-build/vacuum.js';
|
||||
|
||||
test('solveAffine recovers rotation+scale+mirror+offset exactly', () => {
|
||||
// target = mirror-X, rotate 90°, scale 0.02, offset (300, 400)
|
||||
@@ -135,3 +135,20 @@ test('fitMatrix/fitFromMatrix round-trip, mirror and quarters', async () => {
|
||||
const c2 = applyAffine(fitMatrix(r2), 500, 500);
|
||||
assert.ok(Math.abs(c2[0] - c[0]) < 1e-6 && Math.abs(c2[1] - c[1]) < 1e-6);
|
||||
});
|
||||
|
||||
// HP-1540-02: the map-id contract, mirrored by trails.py resolve_map_id —
|
||||
// the first value that is not null/undefined wins; zero and '' are valid ids.
|
||||
test('vacMapIdFromAttrs: first NOT-nullish value wins, zero survives', () => {
|
||||
assert.equal(vacMapIdFromAttrs({ map_index: 0 }), '0');
|
||||
assert.equal(vacMapIdFromAttrs({ map_index: '0' }), '0');
|
||||
assert.equal(vacMapIdFromAttrs({ map_name: '', selected_map: 'Floor' }), '');
|
||||
assert.equal(vacMapIdFromAttrs({ map_name: 'A', map_index: 0 }), 'A');
|
||||
assert.equal(vacMapIdFromAttrs({ current_map: 2 }), '2');
|
||||
assert.equal(vacMapIdFromAttrs({ selected_map: 'Vac' }), 'Vac');
|
||||
assert.equal(vacMapIdFromAttrs({}), 'default');
|
||||
});
|
||||
|
||||
test('readVacTelemetry keeps numeric zero map_index as map id (HP-1540-02)', () => {
|
||||
const t = readVacTelemetry({ vacuum_position: { x: 1, y: 2 }, map_index: 0 });
|
||||
assert.equal(t.mapId, '0');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user