Files
houseplan-card/demo/smoke_vacuum_multifloor.mjs
2026-09-03 21:42:57 +00:00

127 lines
5.8 KiB
JavaScript

// #162: карты робота живут на разных этажах, док — на своём.
//
// Смок проверяет ровно то, чего не мог проверить ни один прежний: раньше
// маркер робота отбирался по пространству дока, поэтому на другом этаже
// оверлея не существовало в принципе.
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 M1 = [0.5, 0, 0, 0, 0.5, 0];
const M2 = [0.25, 0, 0, 0, 0.25, 0];
const attrs = (mapName) => ({ vacuum_position: { x: 600, y: 800, a: 45 }, map_name: mapName });
c.hass = { ...c.hass,
entities: { ...c.hass.entities,
'vacuum.robo': { entity_id: 'vacuum.robo', platform: 'demo', disabled_by: null },
'camera.robo_map': { entity_id: 'camera.robo_map', platform: 'demo', disabled_by: null } },
states: { ...c.hass.states,
'vacuum.robo': { state: 'cleaning', attributes: { friendly_name: 'Робот' } },
'camera.robo_map': { state: 'idle', attributes: attrs('m1') },
} };
const cfg = c._serverCfg;
cfg.markers = cfg.markers || [];
cfg.markers.push({
id: 'e_vacuum_robo', binding: 'entity:vacuum.robo', space: 'f1',
vacuum: {
source: 'camera.robo_map',
map_routes: [
{ id: 'vr1', source: 'camera.robo_map', map_id: 'm1', space: 'f1', calibration: M1 },
{ id: 'vr2', source: 'camera.robo_map', map_id: 'm2', space: 'garden', calibration: M2 },
],
},
});
c._layout['e_vacuum_robo'] = { s: 'f1', x: 0.1, y: 0.1 };
c._regSignature = '';
c._setMode('view');
c._space = 'f1';
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
const puck = () => sr().querySelector('.vacpuck');
const dock = () => sr().querySelector('.dev[data-id="e_vacuum_robo"]');
const warn = () => sr().querySelector('.vacwarn');
// Карта m1 → первый этаж: робот и док на одном этаже, как и раньше.
o.floor1DockThere = !!dock();
o.floor1PuckThere = !!puck();
o.floor1NoWarning = !warn();
// Робот переехал на карту m2 → второй этаж.
c.hass = { ...c.hass, states: { ...c.hass.states,
'camera.robo_map': { state: 'idle', attributes: attrs('m2') } } };
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.floor1DockStays = !!dock();
o.floor1PuckGone = !puck();
c._space = 'garden';
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.floor2PuckThere = !!puck();
o.floor2NoDock = !dock();
// Возврат на m1 возвращает картину без единой правки конфигурации.
const before = JSON.stringify(c._serverCfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum);
c.hass = { ...c.hass, states: { ...c.hass.states,
'camera.robo_map': { state: 'idle', attributes: attrs('m1') } } };
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.floor2PuckGoneAfterReturn = !puck();
c._space = 'f1';
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.floor1PuckBack = !!puck();
o.routesUntouched = JSON.stringify(
c._serverCfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum) === before;
// Несопоставленная карта: нигде не рисуем, но у дока говорим почему.
c.hass = { ...c.hass, states: { ...c.hass.states,
'camera.robo_map': { state: 'idle', attributes: attrs('m9') } } };
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.unmappedNoPuck = !puck();
o.unmappedWarns = !!warn();
o.unmappedWarnLabelled = !!warn()?.getAttribute('aria-label');
c._space = 'garden';
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.unmappedNoGhostOnOtherFloor = !puck();
// Донастройка матрицы с высоким residual обязана открыться на этаже
// МАРШРУТА: она решалась против его геометрии, а не геометрии дока.
c._space = 'f1';
c._setMode('devices');
await c.updateComplete; await new Promise((r) => setTimeout(r, 120));
c._vacCalConfirm = {
markerId: 'e_vacuum_robo', source: 'camera.robo_map', mapId: 'm2',
routeId: 'vr2', space: 'garden', matrix: M2, rooms: 3, error: '80 см',
};
await c.updateComplete;
await c._vacApplyCalibrationProposal(true);
await c.updateComplete; await new Promise((r) => setTimeout(r, 80));
o.manualFitSwitchedToRouteSpace = c._space === 'garden';
o.manualFitKeepsRoute = c._vacFit?.routeId === 'vr2';
c._vacFit = null;
c._setMode('view');
c._space = 'f1';
await c.updateComplete; await new Promise((r) => setTimeout(r, 80));
// Робот встал — предупреждение снимается: тревожить нечем.
c._space = 'f1';
c.hass = { ...c.hass, states: { ...c.hass.states,
'vacuum.robo': { state: 'docked', attributes: { friendly_name: 'Робот' } } } };
await c.updateComplete; await new Promise((r) => setTimeout(r, 60));
o.dockedNoWarning = !warn();
return o;
});
checkAll(out, {
floor1DockThere: true, floor1PuckThere: true, floor1NoWarning: true,
floor1DockStays: true, floor1PuckGone: true,
floor2PuckThere: true, floor2NoDock: true,
floor2PuckGoneAfterReturn: true, floor1PuckBack: true, routesUntouched: true,
unmappedNoPuck: true, unmappedWarns: true, unmappedWarnLabelled: true,
unmappedNoGhostOnOtherFloor: true,
manualFitSwitchedToRouteSpace: true, manualFitKeepsRoute: true,
dockedNoWarning: true,
});
await finish(browser);