Vacuum adapter vs a live Dreame X50 Master

Checked against the real robot at the dacha: room centres arrive as
plain x/y next to the bbox, and the active-map name (selected_map,
'Первый этаж'/'Второй этаж') lives on the VACUUM entity, not on the
camera — without reading it both floors would silently share one
calibration matrix. Parser and card resolver adjusted; the captured
attribute shape is now a unit fixture.
This commit is contained in:
Matysh
2026-07-31 09:28:20 +03:00
parent 40abbccbf0
commit 0142d37bec
6 changed files with 37 additions and 9 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+15 -3
View File
@@ -4420,6 +4420,18 @@ class HouseplanCard extends LitElement {
</div>`;
}
/**
* The active-map id. The camera rarely names its map; Dreame keeps the
* human-readable one on the vacuum entity (selected_map, verified against a
* 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;
const ve = this._vacEntity(d);
const sel = ve ? this.hass?.states[ve]?.attributes?.selected_map : null;
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;
@@ -4458,7 +4470,7 @@ class HouseplanCard extends LitElement {
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, tele.mapId, res.matrix);
this._vacSaveMatrix(d.id, src, this._vacMapId(d, tele), res.matrix);
this._showToast(subst(this._t('vac.autocal_done'), { rooms: String(res.matched.length) }));
}
@@ -4471,7 +4483,7 @@ class HouseplanCard extends LitElement {
return;
}
this._markerDialog = null;
this._vacCal = { markerId: d.id, source: src, mapId: tele.mapId, pairs: [] };
this._vacCal = { markerId: d.id, source: src, mapId: this._vacMapId(d, tele), pairs: [] };
}
private _vacCalClick(ev: MouseEvent): void {
@@ -4516,7 +4528,7 @@ class HouseplanCard extends LitElement {
if (!src) continue;
const tele = readVacTelemetry(this.hass?.states[src]?.attributes);
if (!tele) continue;
const matrix = d.marker?.vacuum?.calibration?.[tele.mapId] as Affine | undefined;
const matrix = d.marker?.vacuum?.calibration?.[this._vacMapId(d, tele)] as Affine | undefined;
if (!matrix || matrix.length !== 6) continue;
const rt = this._vacRt.get(d.id);
const moving = rt?.moving ?? false;
+3
View File
@@ -120,6 +120,9 @@ export function readVacTelemetry(attrs: Record<string, any> | null | undefined):
cx = (x0 + x1) / 2; cy = (y0 + y1) / 2;
}
}
// Tasshack dreame-vacuum: the room centre is plain x/y (verified against
// a live X50 Master; x/y sits within its own x0..x1 bbox)
if (cx == null || cy == null) { cx = num(r.x); cy = num(r.y); }
if (name && cx != null && cy != null) rooms.push({ id, name, cx, cy });
}
const mapId = String(attrs.map_name ?? attrs.current_map ?? attrs.map_index ?? attrs.selected_map ?? 'default');
+13
View File
@@ -48,6 +48,19 @@ test('readVacTelemetry: Valetudo/Tasshack shapes + junk safety', () => {
assert.ok(!isVacSourceState({ attributes: { battery: 1 } }));
});
test('readVacTelemetry: Tasshack room centres come as plain x/y', () => {
// shape captured from a live Dreame X50 Master (dacha, 2026-07-31)
const t = readVacTelemetry({
vacuum_position: { x: 1399, y: -55, a: 181 },
rooms: { 2: { room_id: 2, name: 'Кладовка', x0: 800, y0: -2000, x1: 4200, y1: 300, x: 2575, y: -825 } },
});
assert.equal(t.rooms[0].name, 'Кладовка');
// bbox wins when present (both are valid anchors); x/y covers bbox-less dialects
assert.equal(t.rooms[0].cx, 2500);
const t2 = readVacTelemetry({ vacuum_position: { x: 0, y: 0 }, rooms: { 2: { name: 'Кладовка', x: 2575, y: -825 } } });
assert.equal(t2.rooms[0].cx, 2575);
});
test('autoCalibrate matches by name and solves', () => {
const f = ([x, y]) => [0.01 * x + 100, -0.01 * y + 900];
const vac = [