Live robot vacuums, P1 (docs/VACUUM.md)

The base marker never moves — it is the dock. While the robot cleans, a
round pulsing puck (no badge plate) drives the plan over an affine
transform solved from vacuum-map coordinates: auto-calibration matches
the robot's room list against plan rooms by name, and a three-point
wizard covers integrations without room data. The trail rides the
integration's own path when offered (it predates the card being opened)
and a self-recorded thinned buffer otherwise, lingering ten minutes
after docking. Adapters read the Map Extractor / Tasshack / Valetudo
attribute dialects through one tolerant parser. Display only — no
commands, per the owner's decision.

vacuum.ts is pure logic under 8 new unit tests; the marker schema grew
an optional vacuum block (56 backend tests); smoke_vacuum drives 19
browser asserts including the wizard end to end.
This commit is contained in:
Matysh
2026-07-31 09:19:56 +03:00
parent 831e694f83
commit 40abbccbf0
15 changed files with 1348 additions and 99 deletions
+80
View File
@@ -1233,6 +1233,86 @@ export const cardStyles = css`
color: var(--hp-muted);
font-size: 11px;
}
/* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */
.vacpuck {
position: absolute;
width: var(--dev-size);
height: var(--dev-size);
border-radius: 50%;
transform: translate(-50%, -50%);
background: color-mix(in srgb, var(--hp-accent) 20%, transparent);
border: 1.5px solid var(--hp-accent);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 6;
/* glide between sparse position updates; .jump disables it so the robot
never appears to drive through walls after a data gap */
transition: left 1.2s linear, top 1.2s linear;
animation: vacpulse 2.2s ease-out infinite;
}
.vacpuck.jump { transition: none; }
.vacpuck.stale { opacity: 0.45; animation: none; }
.vacpuck ha-icon { --mdc-icon-size: calc(var(--dev-size) * 0.6); color: var(--hp-accent); }
.vacwedge {
position: absolute;
inset: 0;
pointer-events: none;
}
.vacwedge::before {
content: '';
position: absolute;
top: -5px;
left: 50%;
margin-left: -3.5px;
border-left: 3.5px solid transparent;
border-right: 3.5px solid transparent;
border-bottom: 6px solid var(--hp-accent);
}
@keyframes vacpulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); }
70% { box-shadow: 0 0 0 12px transparent; }
100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
.vacpuck { animation: none; }
}
.vactrail {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 5;
overflow: visible;
}
.vactrail polyline {
fill: none;
stroke: var(--hp-accent);
stroke-opacity: 0.35;
stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
gap: 12px;
align-items: center;
background: var(--hp-panel, #16212e);
color: var(--hp-fg, #e7eef7);
border: 1px solid var(--hp-accent);
border-radius: 10px;
padding: 10px 14px;
z-index: 60;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.candlist {
max-height: 160px;
overflow-y: auto;