mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
@@ -3,7 +3,8 @@ import { html, nothing, type TemplateResult } from 'lit';
|
||||
|
||||
import type { I18nKey } from '../i18n';
|
||||
import {
|
||||
radarConfigFromDraft, radarDraft, radarSourceCandidates, recognizeRadar,
|
||||
freshRadarInstallationId, radarConfigFromDraft, radarDraft, radarSourceCandidates,
|
||||
recognizeRadar,
|
||||
type RadarEditorDraft, type RadarHassLike,
|
||||
} from '../radar-editor';
|
||||
import type { RadarSetupController } from '../radar-setup';
|
||||
@@ -50,7 +51,10 @@ export interface RadarSectionOptions {
|
||||
}
|
||||
|
||||
/** Rendered from the lazy editor chunk; ordinary View never loads this file. */
|
||||
export function renderRadarSection(options: RadarSectionOptions): TemplateResult {
|
||||
export function renderRadarSection(
|
||||
options: RadarSectionOptions,
|
||||
placement: 'main' | 'additional' = 'main',
|
||||
): TemplateResult {
|
||||
const { dialog: d, device } = options;
|
||||
if (!device) return html``;
|
||||
options.setup.syncMarker(device.marker?.id || device.id);
|
||||
@@ -59,6 +63,27 @@ export function renderRadarSection(options: RadarSectionOptions): TemplateResult
|
||||
const remove = () => options.updateDialog({
|
||||
radar: null, radarTouched: true, radarRemove: true,
|
||||
});
|
||||
const begin = () => {
|
||||
const space = options.spaceModelById(device.space) || options.currentSpace();
|
||||
if (!space) return;
|
||||
const point = options.position(device);
|
||||
const radar = radarDraft(device, space, [point.x, point.y], options.registryHass, true);
|
||||
if (!radar) return;
|
||||
options.updateDialog({
|
||||
radar, radarEligible: true, radarTouched: true, radarRemove: false,
|
||||
});
|
||||
};
|
||||
const manualEntry = !savedUnsupported && d.bindingMode !== 'virtual'
|
||||
&& (!d.radar || d.radarRemove) && !recognition.eligible;
|
||||
if (placement === 'additional') {
|
||||
if (!manualEntry) return html``;
|
||||
return html`<details class="markerlightgroup radaradditional">
|
||||
<summary>${options.t('radar.additional_actions')}</summary>
|
||||
<button class="btn ghost" type="button" @click=${begin}>
|
||||
<ha-icon icon="mdi:radar"></ha-icon>${options.t('radar.declare')}
|
||||
</button>
|
||||
</details>`;
|
||||
}
|
||||
if ((!d.radar || d.radarRemove) && options.setup.isActive()) options.setup.reset();
|
||||
if (savedUnsupported || d.bindingMode === 'virtual' && !!device.marker?.radar) {
|
||||
return html`<fieldset class="markerlightgroup radargroup">
|
||||
@@ -72,16 +97,6 @@ export function renderRadarSection(options: RadarSectionOptions): TemplateResult
|
||||
</fieldset>`;
|
||||
}
|
||||
if (d.bindingMode === 'virtual') return html``;
|
||||
const begin = () => {
|
||||
const space = options.spaceModelById(device.space) || options.currentSpace();
|
||||
if (!space) return;
|
||||
const point = options.position(device);
|
||||
const radar = radarDraft(device, space, [point.x, point.y], options.registryHass, true);
|
||||
if (!radar) return;
|
||||
options.updateDialog({
|
||||
radar, radarEligible: true, radarTouched: true, radarRemove: false,
|
||||
});
|
||||
};
|
||||
if (!d.radar || d.radarRemove) {
|
||||
if (recognition.eligible) {
|
||||
return html`<fieldset class="markerlightgroup radargroup">
|
||||
@@ -93,12 +108,7 @@ export function renderRadarSection(options: RadarSectionOptions): TemplateResult
|
||||
</button>
|
||||
</fieldset>`;
|
||||
}
|
||||
return html`<details class="markerlightgroup radaradditional">
|
||||
<summary>${options.t('radar.additional_actions')}</summary>
|
||||
<button class="btn ghost" type="button" @click=${begin}>
|
||||
<ha-icon icon="mdi:radar"></ha-icon>${options.t('radar.declare')}
|
||||
</button>
|
||||
</details>`;
|
||||
return html``;
|
||||
}
|
||||
|
||||
const radar = d.radar;
|
||||
@@ -172,17 +182,26 @@ export function renderRadarSection(options: RadarSectionOptions): TemplateResult
|
||||
});
|
||||
}
|
||||
};
|
||||
const configureOnPlan = () => {
|
||||
const room = roomOptions.find((candidate) => candidate.id === radar.roomId);
|
||||
if (!space || !room || !Array.isArray(room.poly) || room.poly.length < 3
|
||||
|| !radarConfigFromDraft(radar, space.cellCm || 5)) {
|
||||
const configureOnPlan = (draft = radar) => {
|
||||
const room = roomOptions.find((candidate) => candidate.id === draft.roomId);
|
||||
if (!space || !room || !radarConfigFromDraft(draft, space.cellCm || 5)) {
|
||||
options.toast(options.t('radar.invalid'));
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(room.poly) || room.poly.length < 3) {
|
||||
options.toast(options.t('radar.no_contour'));
|
||||
}
|
||||
if (!options.setup.begin(
|
||||
device.marker?.id || device.id, radar, room, space.cellCm || 5, options.configRev,
|
||||
device.marker?.id || device.id, draft, room, space.cellCm || 5, options.configRev,
|
||||
)) options.toast(options.t('radar.invalid'));
|
||||
};
|
||||
const changeInstallation = () => configureOnPlan({
|
||||
...radar,
|
||||
installationId: freshRadarInstallationId(),
|
||||
calibrationOverride: undefined,
|
||||
inspection: undefined,
|
||||
inspectError: undefined,
|
||||
});
|
||||
const setAxis = (axis: 'xEntities' | 'yEntities', index: number, value: string) => {
|
||||
const next = [...radar[axis]];
|
||||
next[index] = value;
|
||||
@@ -419,9 +438,12 @@ export function renderRadarSection(options: RadarSectionOptions): TemplateResult
|
||||
<span>${options.t('radar.mirror')}</span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<button class="btn" type="button" @click=${configureOnPlan}>
|
||||
<button class="btn" type="button" @click=${() => configureOnPlan()}>
|
||||
<ha-icon icon="mdi:map-marker-radius"></ha-icon>${options.t('radar.configure_on_plan')}
|
||||
</button>
|
||||
${radar.original ? html`<button class="btn ghost" type="button" @click=${changeInstallation}>
|
||||
<ha-icon icon="mdi:restart"></ha-icon>${options.t('radar.change_installation')}
|
||||
</button>` : nothing}
|
||||
<button class="btn" type="button" ?disabled=${radar.inspectBusy} @click=${inspect}>
|
||||
<ha-icon icon="mdi:radar"></ha-icon>
|
||||
${radar.inspectBusy ? options.t('radar.checking') : options.t('radar.check_sources')}
|
||||
@@ -435,9 +457,12 @@ export function renderRadarSection(options: RadarSectionOptions): TemplateResult
|
||||
targets: radar.inspection.frame.targets?.length || 0,
|
||||
ranges: radar.inspection.frame.ranges?.length || 0,
|
||||
})}</span>
|
||||
${(radar.inspection.sources || []).map((source) => html`<code>
|
||||
${source.entity_id}: ${source.state ?? '—'}
|
||||
</code>`)}
|
||||
${(radar.inspection.sources || []).map((source) => {
|
||||
const age = typeof source.reported_at === 'number' && Number.isFinite(source.reported_at)
|
||||
? Math.max(0, Math.round(Date.now() / 1000 - source.reported_at)) : null;
|
||||
return html`<code>${source.entity_id}: ${source.state ?? '—'}${age == null ? ''
|
||||
: ` · ${options.t('radar.report_age', { seconds: age })}`}</code>`;
|
||||
})}
|
||||
</div>` : nothing}
|
||||
${options.setup.render()}
|
||||
<div class="row">
|
||||
|
||||
@@ -187,11 +187,11 @@ import type {
|
||||
MarkerValueBadge, ValueBadgePosition, ValueBadgeSource, ZeroWallStyle,
|
||||
} from './types';
|
||||
import {
|
||||
radarConfigFromDraft, radarDraft, recognizeRadar,
|
||||
radarAfterBindingChange, radarConfigFromDraft, radarDraft, recognizeRadar,
|
||||
type RadarEditorDraft,
|
||||
} from './radar-editor';
|
||||
import { renderRadarSection } from './editors/radar-section';
|
||||
import { RadarSetupController } from './radar-setup';
|
||||
import { radarDiscardRequest, RadarSetupController } from './radar-setup';
|
||||
import {
|
||||
COLUMN_MAX_CM, canonicalColumnAngle, clampColumnCm, columnBody,
|
||||
directionalOccluders, floorMinusBodies, geometryArea, geometryOuterRings,
|
||||
@@ -1231,6 +1231,7 @@ export class HouseplanEditorRuntime {
|
||||
...host._markerDialog, radar, radarEligible: true, radarTouched: true, radarRemove: false,
|
||||
};
|
||||
},
|
||||
confirmDiscard: () => host._confirmDanger(radarDiscardRequest((key) => host._t(key))),
|
||||
});
|
||||
host._editorSecondary = new EditorSecondaryController({
|
||||
root: () => host.renderRoot as ShadowRoot,
|
||||
@@ -7327,8 +7328,13 @@ public _openDeviceInbox(): void {
|
||||
};
|
||||
}
|
||||
|
||||
public _closeMarkerDialog(): void {
|
||||
this._radarSetup.reset();
|
||||
public _closeMarkerDialog(): void { void this._closeMarkerDialogGuarded(); }
|
||||
private async _closeMarkerDialogGuarded(): Promise<void> {
|
||||
if (!await this._radarSetup.discardIfAllowed()) {
|
||||
await this.host.updateComplete; this.host.renderRoot
|
||||
.querySelector<HpDialog>('#marker-dialog')?.rejectClose();
|
||||
return;
|
||||
}
|
||||
this.host._markerDialog = null;
|
||||
if (this.host._deviceInboxReturn) {
|
||||
const restored = { ...this.host._deviceInboxReturn };
|
||||
@@ -12856,10 +12862,7 @@ public _setMarkerGlowMode(mode: 'auto' | 'color' | 'fixed'): void {
|
||||
};
|
||||
}
|
||||
|
||||
public _renderRadarSection(
|
||||
d: NonNullable<HouseplanEditorHostPort['_markerDialog']>,
|
||||
device: DevItem | null,
|
||||
): TemplateResult {
|
||||
public _renderRadarSection(d: NonNullable<HouseplanEditorHostPort['_markerDialog']>, device: DevItem | null, placement: 'main' | 'additional' = 'main'): TemplateResult {
|
||||
return renderRadarSection({
|
||||
dialog: d,
|
||||
device,
|
||||
@@ -12879,7 +12882,7 @@ public _renderRadarSection(
|
||||
toast: (message) => this.host._showToast(message),
|
||||
setup: this._radarSetup,
|
||||
configRev: this.host._cfgRev,
|
||||
});
|
||||
}, placement);
|
||||
}
|
||||
|
||||
public _renderMarkerDialog(): TemplateResult {
|
||||
@@ -12978,7 +12981,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
if (k === 'device') return this.host._fullRegistryHass.devices[ref]?.name_by_user || this.host._fullRegistryHass.devices[ref]?.name || ref;
|
||||
return this.host._fullRegistryHass.entities[ref]?.name || this.host.hass.states[ref]?.attributes?.friendly_name || ref;
|
||||
})();
|
||||
return html`<hp-dialog .hass=${this.host.hass} data-kind="marker"
|
||||
return html`<hp-dialog id="marker-dialog" .hass=${this.host.hass} data-kind="marker"
|
||||
.title=${d.devId ? this.host._t('info.device_header') : this.host._t('marker.new_device')}
|
||||
icon="mdi:shape-plus" wide @hp-close=${() => this._closeMarkerDialog()}>
|
||||
<div class="body">
|
||||
@@ -13016,9 +13019,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
...d, bindingMode: 'virtual' as const, binding: 'virtual', bindingOpen: false,
|
||||
controls: persistedExternalControls('virtual', d.controls),
|
||||
autoIcon: this.host._autoIconForBinding('virtual'),
|
||||
radar: null, radarEligible: false,
|
||||
radarTouched: d.radarTouched || !!d.radar,
|
||||
radarRemove: d.radarRemove || !!d.radar,
|
||||
...radarAfterBindingChange(d.radar, d.radarTouched, d.radarRemove),
|
||||
};
|
||||
this.host._markerDialog = this._announceToggleDraft({
|
||||
...next, ...this._valueBadgeForBinding(next, 'virtual'),
|
||||
@@ -13074,9 +13075,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
c.value, d.controls, this.host._bindingEntities(c.value),
|
||||
),
|
||||
autoIcon: this.host._autoIconForBinding(c.value),
|
||||
radar: null, radarEligible: false,
|
||||
radarTouched: d.radarTouched || !!d.radar,
|
||||
radarRemove: d.radarRemove || !!d.radar,
|
||||
...radarAfterBindingChange(d.radar, d.radarTouched, d.radarRemove),
|
||||
};
|
||||
this.host._markerDialog = this._announceToggleDraft({
|
||||
...next, ...this._valueBadgeForBinding(next, c.value),
|
||||
@@ -13578,6 +13577,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
@change=${(e: Event) => this._pickMarkerFiles(e)} />
|
||||
</span>
|
||||
</div>
|
||||
${this._renderRadarSection(d, previewDevice, 'additional')}
|
||||
</div>
|
||||
<div class="row markerfooter" slot="footer">
|
||||
<div class="markeractions">
|
||||
|
||||
@@ -474,6 +474,10 @@
|
||||
"radar.show_live": "Live-Ziele anzeigen",
|
||||
"radar.configure": "Radar konfigurieren",
|
||||
"radar.configure_on_plan": "Auf dem Plan konfigurieren",
|
||||
"radar.change_installation": "Installation ändern",
|
||||
"radar.no_contour": "Dieser Raum hat keine Begrenzung. Radarpunkte werden nicht zugeschnitten.",
|
||||
"radar.discard_setup_title": "Radareinrichtung verwerfen?",
|
||||
"radar.discard_setup": "Die nicht gespeicherte Radareinrichtung geht verloren.",
|
||||
"radar.step_place_mount": "Markieren Sie die physische Sensorposition",
|
||||
"radar.step_place_heading": "Markieren Sie einen Punkt direkt vor dem Sensor",
|
||||
"radar.step_reference": "Markieren Sie Referenzposition {n} und starten Sie die Messung",
|
||||
@@ -488,6 +492,7 @@
|
||||
"radar.ambiguous_target": "Mehr als ein Ziel wurde gemeldet. Wiederholen Sie die Messung allein.",
|
||||
"radar.insufficient_samples": "Nicht genügend aktuelle Koordinaten. Wiederholen Sie die Messung oder richten Sie sie manuell ein.",
|
||||
"radar.bad_references": "Wählen Sie getrennte Referenzen in verschiedenen Richtungen vom Sensor.",
|
||||
"radar.ambiguous_sources": "Die Messungen lassen zwei gleich plausible Richtungen zu. Wählen Sie Referenzpunkte, die links und rechts unterscheiden.",
|
||||
"radar.bad_fit": "Die Messungen stimmen nicht überein. Prüfen Sie Einheiten, Sensorposition und Referenzen.",
|
||||
"radar.desktop_hint": "Verwenden Sie für eine präzise Einrichtung einen Computer. Die Bearbeitung auf Touch-Geräten ist nur eingeschränkt verfügbar.",
|
||||
"radar.detected": "House Plan hat Radarmetadaten erkannt. Prüfe vor dem Speichern die exakten Quellen.",
|
||||
@@ -548,6 +553,9 @@
|
||||
"radar.health_ok": "Aktuelle Daten empfangen",
|
||||
"radar.health_off": "Keine Ziele erkannt",
|
||||
"radar.health_stale": "Ein Teil der Koordinaten ist veraltet",
|
||||
"radar.health_partial": "Einige Quellen sind aktuell, andere veraltet",
|
||||
"radar.health_restricted": "Der Zugriff auf eine oder mehrere Quellen ist eingeschränkt",
|
||||
"radar.health_incomplete": "Der aktuelle Radarbericht ist unvollständig",
|
||||
"radar.health_position_unavailable": "Präsenz gemeldet; aktuelle Position nicht verfügbar",
|
||||
"radar.health_unavailable": "Sensordaten nicht verfügbar",
|
||||
"radar.health_needs_setup": "Installation muss eingerichtet werden",
|
||||
@@ -562,6 +570,7 @@
|
||||
"radar.error_conflict": "Der Plan wurde geändert. Öffnen Sie die Radareinrichtung erneut und versuchen Sie es noch einmal.",
|
||||
"radar.error_source_unavailable": "Mindestens eine ausgewählte Radarquelle ist nicht verfügbar.",
|
||||
"radar.error_unsupported_capability": "Diese Radarfunktion wird von der installierten Integration nicht unterstützt.",
|
||||
"radar.report_age": "vor {seconds} s",
|
||||
"space.show_lqi": "Zigbee-Signal (LQI) neben Geräten anzeigen",
|
||||
"space.hide_decor": "Dekorationsschicht ausblenden",
|
||||
"space.hide_decor_tip": "Linien, Formen, Beschriftungen und Möbel bleiben an ihrem Platz — im Hintergrundeditor sichtbar, aber nicht auf dem Plan.",
|
||||
|
||||
@@ -474,6 +474,10 @@
|
||||
"radar.show_live": "Show live targets",
|
||||
"radar.configure": "Configure radar",
|
||||
"radar.configure_on_plan": "Configure on plan",
|
||||
"radar.change_installation": "Change installation",
|
||||
"radar.no_contour": "This room has no boundary. Radar points will not be clipped.",
|
||||
"radar.discard_setup_title": "Discard radar setup?",
|
||||
"radar.discard_setup": "The unsaved radar setup will be lost.",
|
||||
"radar.step_place_mount": "Mark the physical sensor position",
|
||||
"radar.step_place_heading": "Mark a point straight ahead of the sensor",
|
||||
"radar.step_reference": "Mark reference position {n}, then start measurement",
|
||||
@@ -488,6 +492,7 @@
|
||||
"radar.ambiguous_target": "More than one target was reported. Repeat the measurement alone.",
|
||||
"radar.insufficient_samples": "Not enough current coordinates. Retry or use manual setup.",
|
||||
"radar.bad_references": "Choose separated references in different directions from the sensor.",
|
||||
"radar.ambiguous_sources": "The measurements allow two equally plausible directions. Choose reference points that distinguish left from right.",
|
||||
"radar.bad_fit": "Measurements do not match. Check units, mounting point and references.",
|
||||
"radar.desktop_hint": "Use a computer for precise setup. Editing on touch devices is best effort.",
|
||||
"radar.detected": "House Plan recognized radar metadata for this device. Review the exact sources before saving.",
|
||||
@@ -548,6 +553,9 @@
|
||||
"radar.health_ok": "Current data received",
|
||||
"radar.health_off": "No targets detected",
|
||||
"radar.health_stale": "Some coordinate data is out of date",
|
||||
"radar.health_partial": "Some sources are current; others are out of date",
|
||||
"radar.health_restricted": "Access to one or more sources is restricted",
|
||||
"radar.health_incomplete": "The current radar report is incomplete",
|
||||
"radar.health_position_unavailable": "Presence reported; current position unavailable",
|
||||
"radar.health_unavailable": "Sensor data unavailable",
|
||||
"radar.health_needs_setup": "Installation needs setup",
|
||||
@@ -562,6 +570,7 @@
|
||||
"radar.error_conflict": "The plan changed. Reopen the radar setup and try again.",
|
||||
"radar.error_source_unavailable": "One or more selected radar sources are unavailable.",
|
||||
"radar.error_unsupported_capability": "This radar capability is not supported by the installed integration.",
|
||||
"radar.report_age": "{seconds} s ago",
|
||||
"space.show_lqi": "Show zigbee signal (LQI) next to devices",
|
||||
"space.hide_decor": "Hide the decorative layer",
|
||||
"space.hide_decor_tip": "Lines, shapes, labels and furniture stay where they are — visible in the backdrop editor, not on the plan.",
|
||||
|
||||
@@ -474,6 +474,10 @@
|
||||
"radar.show_live": "Afficher les cibles en direct",
|
||||
"radar.configure": "Configurer le radar",
|
||||
"radar.configure_on_plan": "Configurer sur le plan",
|
||||
"radar.change_installation": "Changer l’installation",
|
||||
"radar.no_contour": "Cette pièce n’a pas de contour. Les points radar ne seront pas limités.",
|
||||
"radar.discard_setup_title": "Abandonner la configuration du radar ?",
|
||||
"radar.discard_setup": "La configuration du radar non enregistrée sera perdue.",
|
||||
"radar.step_place_mount": "Indiquez la position physique du capteur",
|
||||
"radar.step_place_heading": "Indiquez un point droit devant le capteur",
|
||||
"radar.step_reference": "Indiquez la position de référence {n}, puis lancez la mesure",
|
||||
@@ -488,6 +492,7 @@
|
||||
"radar.ambiguous_target": "Plusieurs cibles ont été signalées. Recommencez la mesure seul.",
|
||||
"radar.insufficient_samples": "Coordonnées actuelles insuffisantes. Réessayez ou utilisez le réglage manuel.",
|
||||
"radar.bad_references": "Choisissez des références espacées dans des directions différentes du capteur.",
|
||||
"radar.ambiguous_sources": "Les mesures permettent deux directions également plausibles. Choisissez des repères distinguant la gauche de la droite.",
|
||||
"radar.bad_fit": "Les mesures ne correspondent pas. Vérifiez les unités, la position du capteur et les références.",
|
||||
"radar.desktop_hint": "Utilisez un ordinateur pour un réglage précis. L’édition sur appareils tactiles est prise en charge au mieux.",
|
||||
"radar.detected": "House Plan a reconnu les métadonnées radar de cet appareil. Vérifiez les sources exactes avant d’enregistrer.",
|
||||
@@ -548,6 +553,9 @@
|
||||
"radar.health_ok": "Données actuelles reçues",
|
||||
"radar.health_off": "Aucune cible détectée",
|
||||
"radar.health_stale": "Certaines coordonnées sont obsolètes",
|
||||
"radar.health_partial": "Certaines sources sont à jour, d’autres sont obsolètes",
|
||||
"radar.health_restricted": "L’accès à une ou plusieurs sources est limité",
|
||||
"radar.health_incomplete": "Le rapport radar actuel est incomplet",
|
||||
"radar.health_position_unavailable": "Présence signalée ; position actuelle indisponible",
|
||||
"radar.health_unavailable": "Données du capteur indisponibles",
|
||||
"radar.health_needs_setup": "L’installation doit être configurée",
|
||||
@@ -562,6 +570,7 @@
|
||||
"radar.error_conflict": "Le plan a changé. Rouvrez la configuration radar et réessayez.",
|
||||
"radar.error_source_unavailable": "Une ou plusieurs sources radar sélectionnées sont indisponibles.",
|
||||
"radar.error_unsupported_capability": "Cette fonction radar n’est pas prise en charge par l’intégration installée.",
|
||||
"radar.report_age": "il y a {seconds} s",
|
||||
"space.show_lqi": "Afficher le signal Zigbee (LQI) à côté des appareils",
|
||||
"space.hide_decor": "Masquer la couche décorative",
|
||||
"space.hide_decor_tip": "Les lignes, formes, étiquettes et meubles restent à leur place — visibles dans l’éditeur d’arrière-plan, mais pas sur le plan.",
|
||||
|
||||
@@ -474,6 +474,10 @@
|
||||
"radar.show_live": "Показывать живые цели",
|
||||
"radar.configure": "Настроить радар",
|
||||
"radar.configure_on_plan": "Настроить на плане",
|
||||
"radar.change_installation": "Сменить установку",
|
||||
"radar.no_contour": "У комнаты нет контура. Точки радара не будут обрезаться по комнате.",
|
||||
"radar.discard_setup_title": "Отменить настройку радара?",
|
||||
"radar.discard_setup": "Несохранённая настройка радара будет потеряна.",
|
||||
"radar.step_place_mount": "Укажите физическое положение датчика",
|
||||
"radar.step_place_heading": "Укажите точку прямо перед датчиком",
|
||||
"radar.step_reference": "Укажите контрольное положение {n}, затем начните измерение",
|
||||
@@ -488,6 +492,7 @@
|
||||
"radar.ambiguous_target": "Обнаружено несколько целей. Повторите измерение в одиночестве.",
|
||||
"radar.insufficient_samples": "Недостаточно актуальных координат. Повторите или настройте вручную.",
|
||||
"radar.bad_references": "Выберите разнесённые положения в разных направлениях от датчика.",
|
||||
"radar.ambiguous_sources": "Измерения допускают два одинаково вероятных направления. Выберите точки, по которым можно различить левую и правую стороны.",
|
||||
"radar.bad_fit": "Измерения не совпадают. Проверьте единицы, положение датчика и контрольные точки.",
|
||||
"radar.desktop_hint": "Для точной настройки используйте компьютер. Редактирование на тач-устройствах поддерживается по остаточному принципу.",
|
||||
"radar.detected": "House Plan распознал признаки радара у этого устройства. Перед сохранением проверьте точные источники.",
|
||||
@@ -548,6 +553,9 @@
|
||||
"radar.health_ok": "Актуальные данные получены",
|
||||
"radar.health_off": "Цели не обнаружены",
|
||||
"radar.health_stale": "Часть координат устарела",
|
||||
"radar.health_partial": "Часть источников актуальна, часть устарела",
|
||||
"radar.health_restricted": "Нет доступа к одному или нескольким источникам",
|
||||
"radar.health_incomplete": "Текущий отчёт радара неполон",
|
||||
"radar.health_position_unavailable": "Есть присутствие, но актуальное положение недоступно",
|
||||
"radar.health_unavailable": "Данные датчика недоступны",
|
||||
"radar.health_needs_setup": "Требуется настроить установку",
|
||||
@@ -562,6 +570,7 @@
|
||||
"radar.error_conflict": "План изменился. Откройте настройку радара заново и повторите попытку.",
|
||||
"radar.error_source_unavailable": "Один или несколько выбранных источников радара недоступны.",
|
||||
"radar.error_unsupported_capability": "Установленная интеграция не поддерживает эту возможность радара.",
|
||||
"radar.report_age": "{seconds} с назад",
|
||||
"space.show_lqi": "Показывать зигби-сигнал (LQI) у устройств",
|
||||
"space.hide_decor": "Скрыть декоративный слой",
|
||||
"space.hide_decor_tip": "Линии, фигуры, надписи и мебель остаются на месте — их видно в редакторе подложки, но не на плане.",
|
||||
|
||||
+24
-10
@@ -52,6 +52,28 @@ export interface RadarRecognition {
|
||||
reason: 'saved' | 'saved_unsupported' | 'ld2450' | 'radar_metadata' | 'none';
|
||||
}
|
||||
|
||||
const LD2450_MODELS = new Set(['ld2450', 'hlkld2450', 'hilinkld2450']);
|
||||
|
||||
const canonicalModel = (value: unknown): string => String(value || '')
|
||||
.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
export const freshRadarInstallationId = (): string => (
|
||||
globalThis.crypto?.randomUUID?.()
|
||||
|| `radar_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
||||
);
|
||||
|
||||
export const radarAfterBindingChange = (
|
||||
radar: RadarEditorDraft | null, touched: boolean, remove: boolean,
|
||||
) => {
|
||||
const saved = !!radar?.original;
|
||||
return {
|
||||
radar: saved ? radar : null,
|
||||
radarEligible: saved,
|
||||
radarTouched: saved ? touched : touched || !!radar,
|
||||
radarRemove: saved ? remove : remove || !!radar,
|
||||
};
|
||||
};
|
||||
|
||||
interface RadarRegistryEntity { device_id?: string }
|
||||
interface RadarRegistryDevice {
|
||||
model?: string;
|
||||
@@ -95,18 +117,12 @@ export function recognizeRadar(
|
||||
const deviceId = device.bindingKind === 'device'
|
||||
? device.bindingRef : registryEntity?.device_id;
|
||||
const registryDevice = deviceId ? registryHass?.devices?.[deviceId] : null;
|
||||
const haystack = [
|
||||
device.model, registryDevice?.model, registryDevice?.manufacturer,
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
const entities = deviceEntityIds(device);
|
||||
if (/\bld[-_ ]?2450\b/.test(haystack)
|
||||
if (LD2450_MODELS.has(canonicalModel(registryDevice?.model || device.model))
|
||||
&& entities.some((entityId) => suffixScore(entityId, 'x', 1))
|
||||
&& entities.some((entityId) => suffixScore(entityId, 'y', 1))) {
|
||||
return { eligible: true, profile: 'esphome_ld2450_v1', reason: 'ld2450' };
|
||||
}
|
||||
if (/\b(?:mmwave|mm-wave|radar|presence radar|fp2|fp1e|ld24(?:10|12|50))\b/.test(haystack)) {
|
||||
return { eligible: true, profile: 'presence_v1', reason: 'radar_metadata' };
|
||||
}
|
||||
return { eligible: false, profile: 'presence_v1', reason: 'none' };
|
||||
}
|
||||
|
||||
@@ -174,9 +190,7 @@ export function radarDraft(
|
||||
showLive: original?.show_live !== false,
|
||||
profile,
|
||||
roomId,
|
||||
installationId: original?.mount?.installation_id
|
||||
|| globalThis.crypto?.randomUUID?.()
|
||||
|| `radar_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
installationId: original?.mount?.installation_id || freshRadarInstallationId(),
|
||||
// The editor canvas is 0..1000; persisted plan coordinates remain the
|
||||
// canonical 0..1 space used by rooms and wall geometry.
|
||||
mountX: finiteText(original?.mount?.x == null ? undefined : original.mount.x * 1000,
|
||||
|
||||
+14
-3
@@ -15,6 +15,8 @@ export function radarMarkerLiveInSpace(marker: Marker, space: string): boolean {
|
||||
|
||||
const RADAR_HEALTH_KEYS: Partial<Record<string, I18nKey>> = {
|
||||
ok: 'radar.health_ok', off: 'radar.health_off', stale: 'radar.health_stale',
|
||||
partial: 'radar.health_partial', restricted: 'radar.health_restricted',
|
||||
incomplete: 'radar.health_incomplete',
|
||||
position_unavailable: 'radar.health_position_unavailable',
|
||||
unavailable: 'radar.health_unavailable', needs_setup: 'radar.health_needs_setup',
|
||||
inconsistent: 'radar.health_inconsistent', unknown: 'radar.health_unknown',
|
||||
@@ -44,7 +46,12 @@ export interface RadarLiveRange {
|
||||
segments?: readonly (readonly (readonly [number, number])[])[];
|
||||
}
|
||||
|
||||
export interface RadarLiveZone { id: string; state: boolean | number | null }
|
||||
export interface RadarLiveZone {
|
||||
id: string;
|
||||
state: boolean | number | null;
|
||||
/** Server-owned geometry for a verified adapter; the client never invents it. */
|
||||
polygon?: readonly (readonly [number, number])[];
|
||||
}
|
||||
|
||||
export interface RadarLiveFrame {
|
||||
server_session_id: string;
|
||||
@@ -98,8 +105,12 @@ export function normalizeRadarFrame(value: unknown): RadarLiveFrame | null {
|
||||
const item = entry as Record<string, unknown>;
|
||||
if (typeof item.id !== 'string') return [];
|
||||
const state = item.state;
|
||||
return state === null || typeof state === 'boolean' || finite(state)
|
||||
? [{ id: item.id, state } as RadarLiveZone] : [];
|
||||
if (!(state === null || typeof state === 'boolean' || finite(state))) return [];
|
||||
const polygon = Array.isArray(item.polygon) ? item.polygon.slice(0, 64).flatMap((point) => {
|
||||
if (!Array.isArray(point) || point.length !== 2 || !finite(point[0]) || !finite(point[1])) return [];
|
||||
return [[point[0], point[1]] as const];
|
||||
}) : [];
|
||||
return [{ id: item.id, state, ...(polygon.length >= 3 ? { polygon } : {}) } as RadarLiveZone];
|
||||
}) : [];
|
||||
return {
|
||||
server_session_id: raw.server_session_id,
|
||||
|
||||
+11
-2
@@ -13,7 +13,15 @@ export function renderRadarLive(
|
||||
if (!items.length) return nothing;
|
||||
const dots: TemplateResult[] = [];
|
||||
const arcs: TemplateResult[] = [];
|
||||
const zones: TemplateResult[] = [];
|
||||
for (const frame of items) {
|
||||
for (const zone of frame.zones) {
|
||||
const occupied = zone.state === true || typeof zone.state === 'number' && zone.state > 0;
|
||||
if (!occupied || !zone.polygon) continue;
|
||||
const projected = zone.polygon.map((point) => project([point[0] * 1000, point[1] * 1000]));
|
||||
zones.push(svg`<polygon class="radar-zone" data-radar-zone=${zone.id}
|
||||
points=${projected.map((point) => `${point[0]},${point[1]}`).join(' ')}></polygon>`);
|
||||
}
|
||||
for (const range of frame.ranges) {
|
||||
// An empty authoritative segment list means clipping removed the whole
|
||||
// arc. Never resurrect it with an unclipped client-side fallback.
|
||||
@@ -34,7 +42,8 @@ export function renderRadarLive(
|
||||
aria-hidden="true"></span>`);
|
||||
}
|
||||
}
|
||||
return html`${arcs.length ? svg`<svg class="radar-ranges" data-hp-live-viewbox="camera"
|
||||
if (!arcs.length && !zones.length && !dots.length) return nothing;
|
||||
return html`${arcs.length || zones.length ? svg`<svg class="radar-ranges" data-hp-live-viewbox="camera"
|
||||
viewBox="${view.x} ${view.y} ${view.w} ${view.h}" preserveAspectRatio="none"
|
||||
aria-hidden="true">${arcs}</svg>` : nothing}${dots}`;
|
||||
aria-hidden="true">${zones}${arcs}</svg>` : nothing}${dots}`;
|
||||
}
|
||||
|
||||
+17
-4
@@ -7,6 +7,14 @@ import {
|
||||
projectRadarLocal, radarMedianSample, solveRadarTwoPoint,
|
||||
} from './radar-geometry';
|
||||
import type { RadarEditorDraft } from './radar-editor';
|
||||
|
||||
type RadarDiscardKey = 'radar.discard_setup_title' | 'radar.discard_setup' | 'btn.close' | 'btn.cancel';
|
||||
|
||||
export const radarDiscardRequest = (t: (key: RadarDiscardKey) => string) => ({
|
||||
key: 'discard-radar-setup', kind: 'warning' as const,
|
||||
title: t('radar.discard_setup_title'), message: t('radar.discard_setup'),
|
||||
confirmLabel: t('btn.close'), cancelLabel: t('btn.cancel'),
|
||||
});
|
||||
import type { MarkerRadar, RoomCfg } from './types';
|
||||
|
||||
const COUNTDOWN_MS = 10_000;
|
||||
@@ -63,6 +71,7 @@ export interface RadarSetupHost {
|
||||
t(key: I18nKey, vars?: Record<string, string | number>): string;
|
||||
configFromDraft(draft: RadarEditorDraft, cellCm: number): MarkerRadar | null;
|
||||
apply(draft: RadarEditorDraft): void;
|
||||
confirmDiscard(): Promise<boolean>;
|
||||
}
|
||||
|
||||
function pointFromEvent(event: PointerEvent): Point | null {
|
||||
@@ -118,13 +127,17 @@ export class RadarSetupController {
|
||||
this.active = null;
|
||||
}
|
||||
|
||||
public cancel(): boolean {
|
||||
if (!this.active) return false;
|
||||
public async discardIfAllowed(): Promise<boolean> {
|
||||
if (!this.active) return true;
|
||||
const dirty = this.active.mount !== null || this.active.phase !== 'mount';
|
||||
if (dirty && !await this.host.confirmDiscard()) return false;
|
||||
this.reset();
|
||||
this.host.requestUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
public cancel(): Promise<boolean> { return this.discardIfAllowed(); }
|
||||
|
||||
public isActive(): boolean { return this.active !== null; }
|
||||
|
||||
public syncMarker(markerId: string): void {
|
||||
@@ -313,7 +326,7 @@ export class RadarSetupController {
|
||||
state.error = undefined;
|
||||
} catch (error: unknown) {
|
||||
const message = String((error as Error)?.message);
|
||||
state.error = message === 'ambiguous_sources' ? 'radar.bad_references' : 'radar.bad_fit';
|
||||
state.error = message === 'ambiguous_sources' ? 'radar.ambiguous_sources' : 'radar.bad_fit';
|
||||
state.refs = [];
|
||||
state.phase = 'reference_1';
|
||||
}
|
||||
@@ -386,7 +399,7 @@ export class RadarSetupController {
|
||||
<div class="radarsetup-head">
|
||||
<strong>${this.instruction(state)}</strong>
|
||||
<button class="iconbtn" type="button" title=${this.host.t('btn.cancel')}
|
||||
@click=${() => this.cancel()}><ha-icon icon="mdi:close"></ha-icon></button>
|
||||
@click=${() => { void this.cancel(); }}><ha-icon icon="mdi:close"></ha-icon></button>
|
||||
</div>
|
||||
<svg viewBox="0 0 1000 1000" preserveAspectRatio="xMidYMid meet"
|
||||
@pointerdown=${(event: PointerEvent) => this.choosePoint(event)}
|
||||
|
||||
@@ -1445,6 +1445,13 @@ export const planStyles = css`
|
||||
vector-effect: non-scaling-stroke;
|
||||
opacity: 0.72;
|
||||
}
|
||||
.radar-ranges .radar-zone {
|
||||
fill: color-mix(in srgb, var(--success-color, #43a047) 8%, transparent);
|
||||
stroke: var(--success-color, #43a047);
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.stage.mode-devices .dev { cursor: grab; }
|
||||
.stage.mode-devices .dev:active { cursor: grabbing; }
|
||||
.physical-hit {
|
||||
|
||||
@@ -124,10 +124,6 @@ export interface RadarRangeSource {
|
||||
export interface RadarZoneSource {
|
||||
id: string; kind: 'occupancy' | 'count'; entity_id: string;
|
||||
}
|
||||
export interface RadarLocalZone {
|
||||
id: string; name: string; poly: RadarPoint[];
|
||||
state: { kind: 'targets' } | { kind: 'occupancy' | 'count'; entity_id: string };
|
||||
}
|
||||
export interface MarkerRadar {
|
||||
version: 1;
|
||||
enabled: boolean;
|
||||
@@ -150,8 +146,6 @@ export interface MarkerRadar {
|
||||
method: 'manual' | 'two_point' | 'not_required'; mirror: boolean; cell_cm: number;
|
||||
refs?: { plan: RadarPoint; local_cm: RadarPoint }[]; rms_cm?: number;
|
||||
};
|
||||
zones?: { local?: RadarLocalZone[]; hardware?: Record<string, unknown> };
|
||||
reflectors?: { id: string; name: string; a: RadarPoint; b: RadarPoint; enabled: boolean }[];
|
||||
allowed_room_ids?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -314,8 +308,6 @@ export interface ServerConfig {
|
||||
radar?: {
|
||||
version?: 1;
|
||||
show_live?: boolean;
|
||||
fusion_groups?: unknown[];
|
||||
room_outputs?: unknown[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user