Files
houseplan-card/demo/smoke_vacuum_firstuse.mjs
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

109 lines
5.5 KiB
JavaScript

// 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);