mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
fix: refine a high-residual calibration on the map's own floor
User-Visible: no Issue: #162
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import { TemplateResult, html, nothing } from 'lit';
|
||||
import type { HpConfirmRequest } from '../danger-confirm';
|
||||
import type { DevItem } from '../types';
|
||||
import type { VacSourceCandidate, VacSourceResolution } from '../vacuum';
|
||||
import { langOf, type I18nKey } from '../i18n';
|
||||
import { supportT, type SupportI18nKey } from '../i18n/support';
|
||||
import { VacuumMapRoute, effectiveRoutes, observedMapIds, resolveRoute } from '../vacuum-routes';
|
||||
@@ -49,6 +50,7 @@ const spaceName = (host: VacuumMapsCardHost, spaceId: string): string => {
|
||||
*/
|
||||
export function renderVacuumMapsSection(
|
||||
runtime: VacuumMapsHost, dev: DevItem, setVac: (patch: Record<string, unknown>) => void,
|
||||
sources: VacSourceResolution,
|
||||
): TemplateResult | typeof nothing {
|
||||
const host = runtime.host;
|
||||
const lang = langOf(host.hass, host._config?.language);
|
||||
@@ -96,6 +98,27 @@ export function renderVacuumMapsSection(
|
||||
));
|
||||
};
|
||||
|
||||
/**
|
||||
* Second supported integration shape: one camera per map (spec 9.3).
|
||||
*
|
||||
* The map id is read from the picked source with the same fallback the rest
|
||||
* of the card uses. A source that names no map cannot be told apart from
|
||||
* another later, so no route is created and the reason is shown instead of
|
||||
* a route that could never resolve.
|
||||
*/
|
||||
const addSource = (candidate: VacSourceCandidate) => {
|
||||
const mapId = host._vacObservedMapId(dev, candidate.entityId);
|
||||
if (mapId === undefined) return;
|
||||
writeRoutes((current) => addRoute(
|
||||
current,
|
||||
{ source: candidate.entityId, map_id: mapId, space: current.length ? '' : dev.space },
|
||||
newRouteId(takenIds()),
|
||||
));
|
||||
};
|
||||
|
||||
const spare = sources.candidates.filter((candidate) => candidate.entityId !== rootSource
|
||||
&& !routes.some((route) => route.source === candidate.entityId));
|
||||
|
||||
const retarget = async (route: VacuumMapRoute, space: string) => {
|
||||
if (!space || space === route.space) return;
|
||||
const accepted = await host._confirmDanger({
|
||||
@@ -186,6 +209,22 @@ export function renderVacuumMapsSection(
|
||||
<button type="button" class="btn" ?disabled=${!canAddCurrent}
|
||||
@click=${addCurrent}>${t('vac.route_add_current')}</button>
|
||||
</div>
|
||||
${spare.length ? html`<details class="vacroute-sources">
|
||||
<summary class="btn ghostbtn">${t('vac.route_add_source')}</summary>
|
||||
<div class="rhint">${t('vac.route_add_source_hint')}</div>
|
||||
<div class="vacsource-list">
|
||||
${spare.map((candidate) => {
|
||||
const mapId = host._vacObservedMapId(dev, candidate.entityId);
|
||||
return html`<button type="button" class="vacsource" ?disabled=${mapId === undefined}
|
||||
@click=${() => addSource(candidate)}>
|
||||
<span><b>${candidate.name}</b><small>${candidate.entityId}</small></span>
|
||||
<span class="vacsource-meta">${mapId === undefined
|
||||
? t('vac.route_source_no_map')
|
||||
: (mapId || t('vac.route_map_default'))}</span>
|
||||
</button>`;
|
||||
})}
|
||||
</div>
|
||||
</details>` : nothing}
|
||||
${rows.some((route) => !spaceIds.has(route.space)) ? html`
|
||||
<div class="warn">${t('vac.route_missing_space_hint')}</div>` : nothing}
|
||||
${routes.some((route) => route.map_id === 'default') ? html`
|
||||
|
||||
@@ -2512,7 +2512,7 @@ export class HouseplanCard extends LitElement {
|
||||
private _vacAllCameraCache: { devId: string; candidates: VacSourceCandidate[] } | null = null;
|
||||
/** Proposed high-residual auto-calibration. Config remains untouched until Apply. */
|
||||
private _vacCalConfirm: {
|
||||
markerId: string; source: string; mapId: string; routeId?: string; matrix: Affine;
|
||||
markerId: string; source: string; mapId: string; routeId?: string; space?: string; matrix: Affine;
|
||||
rooms: number; error: string;
|
||||
} | null = null;
|
||||
private _kioskDots = false;
|
||||
|
||||
@@ -1147,7 +1147,7 @@ export interface HouseplanEditorHostPort {
|
||||
_undoPoint: () => void;
|
||||
_vacAllCameraCache: { devId: string; candidates: VacSourceCandidate[]; } | null;
|
||||
_vacAllCamerasFor: string | null;
|
||||
_vacCalConfirm: { markerId: string; source: string; mapId: string; routeId?: string; matrix: Affine; rooms: number; error: string; } | null;
|
||||
_vacCalConfirm: { markerId: string; source: string; mapId: string; routeId?: string; space?: string; matrix: Affine; rooms: number; error: string; } | null;
|
||||
_vacEnsureMarker: (d: DevItem) => Marker | null;
|
||||
_vacEntity: (d: DevItem) => string | null;
|
||||
_vacMapId: (d: DevItem, tele: { mapId: string }, planHass?: any) => string;
|
||||
@@ -10970,7 +10970,7 @@ public _renderVacSection(dlg: any): TemplateResult | typeof nothing {
|
||||
${(['never', 'cleaning', 'always'] as const).map((mv) => html`
|
||||
<option value=${mv} ?selected=${vacTrailMode(v) === mv}>${this.host._t(('vac.trail_' + mv) as any)}</option>`)}
|
||||
</select>
|
||||
${renderVacuumMapsSection(this, dev, setVac)}
|
||||
${renderVacuumMapsSection(this, dev, setVac, resolution)}
|
||||
` : nothing}
|
||||
</div>`;
|
||||
}
|
||||
@@ -10981,9 +10981,7 @@ public _vacMapId(d: DevItem, tele: { mapId: string }, planHass = this.host._plan
|
||||
return this.host._vacMapId(d, tele, planHass);
|
||||
}
|
||||
|
||||
public _vacSaveMatrix(
|
||||
markerId: string, source: string, mapId: string, matrix: Affine, routeId = '',
|
||||
): boolean {
|
||||
public _vacSaveMatrix(markerId: string, source: string, mapId: string, matrix: Affine, routeId = ''): boolean {
|
||||
// HP-1540-01: a first-use vacuum has no marker yet — materialise it
|
||||
const dev = this.host._devices.find((x) => x.id === markerId);
|
||||
const m = dev ? this.host._vacEnsureMarker(dev)
|
||||
@@ -11034,7 +11032,7 @@ public _vacAutoCalibrate(d: DevItem): void {
|
||||
const residualCm = vacCalibrationResidualCm(res.residual, this.host._gridPitch, cellCm);
|
||||
if (residualCm > VAC_CALIBRATION_WARN_CM) {
|
||||
this.host._vacCalConfirm = {
|
||||
markerId: d.id, source: src, mapId, routeId: target.routeId, matrix: res.matrix,
|
||||
markerId: d.id, source: src, mapId, routeId: target.routeId, space: target.space, matrix: res.matrix,
|
||||
rooms: res.matched.length,
|
||||
error: formatLength(residualCm, this.host.hass?.config?.unit_system?.length === 'mi'),
|
||||
};
|
||||
@@ -11053,11 +11051,11 @@ public _vacApplyCalibrationProposal(manual: boolean): void {
|
||||
const fit = fitFromMatrix(proposal.matrix);
|
||||
if (!dev || !fit) return;
|
||||
this.host._markerDialog = null;
|
||||
if (dev.space !== this.host._space && !this.host._commitSpace(dev.space)) return;
|
||||
this.host._vacFit = {
|
||||
markerId: proposal.markerId, source: proposal.source, routeId: proposal.routeId,
|
||||
mapId: proposal.mapId, p: fit, drag: null,
|
||||
};
|
||||
// #162: матрица решена против пространства МАРШРУТА, а не дока.
|
||||
const space = proposal.space || dev.space;
|
||||
if (space !== this.host._space && !this.host._commitSpace(space)) return;
|
||||
this.host._vacFit = { markerId: proposal.markerId, source: proposal.source,
|
||||
routeId: proposal.routeId, mapId: proposal.mapId, p: fit, drag: null };
|
||||
return;
|
||||
}
|
||||
if (this._vacSaveMatrix(
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
"vac.route_space_confirm": "Verschieben",
|
||||
"vac.route_missing_space_hint": "Eine Karte verweist auf einen gelöschten Raum: neuen wählen oder Karte entfernen. Bis dahin wird der Roboter für diese Karte nicht gezeichnet.",
|
||||
"vac.route_default_hint": "Die Quelle meldet keine stabile Karten-ID. Mehrere Etagen lassen sich ohne andere Quelle oder die vom Roboter gewählte Karte nicht unterscheiden.",
|
||||
"vac.route_target_hint": "Die Station bleibt unabhängig von der aktuellen Karte im Raum „{space}“."
|
||||
"vac.route_target_hint": "Die Station bleibt unabhängig von der aktuellen Karte im Raum „{space}“.",
|
||||
"vac.route_add_source": "Kartenquelle hinzufügen",
|
||||
"vac.route_add_source_hint": "Für Integrationen, die jeder Karte eine eigene Kamera geben. Der Abschnitt „Alle Kameras“ oben erweitert diese Liste.",
|
||||
"vac.route_source_no_map": "keine Karte gemeldet"
|
||||
}
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
"vac.route_space_confirm": "Move",
|
||||
"vac.route_missing_space_hint": "One of the maps points at a deleted space: pick a new one or remove the map. Until then the robot is not drawn for that map.",
|
||||
"vac.route_default_hint": "The source reports no stable map id. Several floors cannot be told apart without another source or the robot's own selected map.",
|
||||
"vac.route_target_hint": "The dock stays in the “{space}” space regardless of the current map."
|
||||
"vac.route_target_hint": "The dock stays in the “{space}” space regardless of the current map.",
|
||||
"vac.route_add_source": "Add a map source",
|
||||
"vac.route_add_source_hint": "For integrations that give each map its own camera. The “All cameras” section above widens this list.",
|
||||
"vac.route_source_no_map": "no map reported"
|
||||
}
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
"vac.route_space_confirm": "Déplacer",
|
||||
"vac.route_missing_space_hint": "Une carte pointe vers un espace supprimé : choisissez-en un autre ou supprimez la carte. D'ici là, le robot n'est pas dessiné pour cette carte.",
|
||||
"vac.route_default_hint": "La source ne fournit pas d'identifiant de carte stable. Plusieurs étages ne peuvent pas être distingués sans une autre source ou la carte sélectionnée du robot.",
|
||||
"vac.route_target_hint": "La station reste dans l'espace « {space} » quelle que soit la carte actuelle."
|
||||
"vac.route_target_hint": "La station reste dans l'espace « {space} » quelle que soit la carte actuelle.",
|
||||
"vac.route_add_source": "Ajouter une source de carte",
|
||||
"vac.route_add_source_hint": "Pour les intégrations qui donnent à chaque carte sa propre caméra. La section « Toutes les caméras » ci-dessus élargit cette liste.",
|
||||
"vac.route_source_no_map": "aucune carte signalée"
|
||||
}
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
"vac.route_space_confirm": "Перенести",
|
||||
"vac.route_missing_space_hint": "Одна из карт ссылается на удалённое пространство: выберите новое или удалите карту. Пока ссылка не исправлена, робот на этой карте не рисуется.",
|
||||
"vac.route_default_hint": "Источник не сообщает стабильный идентификатор карты. Несколько этажей нельзя различить без другого источника или собственной карты робота.",
|
||||
"vac.route_target_hint": "Док остаётся в пространстве «{space}» независимо от текущей карты."
|
||||
"vac.route_target_hint": "Док остаётся в пространстве «{space}» независимо от текущей карты.",
|
||||
"vac.route_add_source": "Добавить источник карты",
|
||||
"vac.route_add_source_hint": "Для интеграций, где у каждой карты своя камера. Список расширяется разделом «Все камеры» выше.",
|
||||
"vac.route_source_no_map": "карта не читается"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user