diff --git a/custom_components/houseplan/radar.py b/custom_components/houseplan/radar.py
index b25f95b1..35373004 100644
--- a/custom_components/houseplan/radar.py
+++ b/custom_components/houseplan/radar.py
@@ -31,7 +31,11 @@ from .radar_geometry import (
polygon_is_convex,
project_local,
)
-from .radar_validation import radar_source_entity_ids, validate_radar_draft
+from .radar_validation import (
+ radar_registry_evidence,
+ radar_source_entity_ids,
+ validate_radar_draft,
+)
from .store import HouseplanData
MAX_FRAME_HZ = 4
@@ -103,6 +107,8 @@ class RadarCoordinator:
async def async_setup(self) -> None:
await self.async_refresh()
+ if self.closed:
+ return
@callback
def config_updated(_event: Event) -> None:
@@ -117,7 +123,10 @@ class RadarCoordinator:
if self.closed:
return
stored = await self.runtime.config_store.async_load() or {}
+ if self.closed:
+ return
config = stored.get("config") or {}
+ registry = radar_registry_evidence(self.hass)
self.config = config
self.config_rev = int(stored.get("rev", 0))
self.radars = {}
@@ -129,7 +138,7 @@ class RadarCoordinator:
continue
marker_id = str(marker.get("id"))
try:
- validate_radar_draft(config, marker_id, marker["radar"])
+ validate_radar_draft(config, marker_id, marker["radar"], registry)
except vol.Invalid:
# Change-aware config compatibility may retain an old or
# future-invalid block. It stays lossless but never runs.
@@ -540,7 +549,9 @@ class RadarCoordinator:
if occupancy is True and not frame["targets"] and not frame["ranges"]:
frame["health"] = "position_unavailable"
elif any_stale:
- frame["health"] = "stale"
+ has_current_evidence = bool(frame["ranges"]) if profile == "range_v1" \
+ else explicit_slots > 0
+ frame["health"] = "partial" if has_current_evidence else "stale"
expires = [item["expires_at"] for item in [*frame["targets"], *frame["ranges"]]]
frame["expires_at"] = min(expires) if expires else now
count = _numeric_state(self.hass.states.get(sources.get("count_entity"))) \
diff --git a/custom_components/houseplan/radar_validation.py b/custom_components/houseplan/radar_validation.py
index 9510f562..19c24322 100644
--- a/custom_components/houseplan/radar_validation.py
+++ b/custom_components/houseplan/radar_validation.py
@@ -26,6 +26,7 @@ ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
ENTITY_RE = re.compile(r"^[a-z0-9_]+\.[a-z0-9_]+$")
MAX_RADARS = 32
CANVAS_LIMIT = 5000.0
+LD2450_MODELS = frozenset({"ld2450", "hlkld2450", "hilinkld2450"})
class RadarValidationError(vol.Invalid):
@@ -111,24 +112,74 @@ def radar_source_entity_ids(radar: Any) -> set[str]:
return set()
sources = radar.get("sources")
out = _source_ids(sources) if isinstance(sources, dict) else set()
- zones = radar.get("zones")
- if isinstance(zones, dict):
- for item in zones.get("local") or []:
- state = item.get("state") if isinstance(item, dict) else None
- if isinstance(state, dict) and isinstance(state.get("entity_id"), str):
- out.add(state["entity_id"])
- hardware = zones.get("hardware")
- if isinstance(hardware, dict):
- for value in hardware.values():
- if isinstance(value, str) and "." in value:
- out.add(value)
- for slot in hardware.get("slots") or []:
- if isinstance(slot, dict):
- out.update(value for key, value in slot.items()
- if key.endswith("_entity") and isinstance(value, str))
return out
+def radar_registry_evidence(hass: Any) -> dict[str, dict[str, dict[str, Any]]]:
+ """Capture the small registry subset needed by verified adapters.
+
+ The returned plain mapping is safe to pass into executor-side validation.
+ Generic/manual profiles deliberately do not depend on registry evidence.
+ """
+ from homeassistant.helpers import device_registry as dr
+ from homeassistant.helpers import entity_registry as er
+
+ entities = {
+ str(entry.entity_id): {
+ "device_id": str(entry.device_id) if entry.device_id else None,
+ "platform": str(getattr(entry, "platform", "") or ""),
+ }
+ for entry in er.async_get(hass).entities.values()
+ }
+ devices = {
+ str(entry.id): {
+ "model": str(getattr(entry, "model", "") or ""),
+ }
+ for entry in dr.async_get(hass).devices.values()
+ }
+ return {"entities": entities, "devices": devices}
+
+
+def _canonical_model(value: Any) -> str:
+ return re.sub(r"[^a-z0-9]", "", str(value or "").lower())
+
+
+def _validate_verified_adapter(
+ profile: str,
+ sources: dict[str, Any],
+ marker: dict[str, Any],
+ registry: dict[str, dict[str, dict[str, Any]]] | None,
+) -> None:
+ """Require structural same-device evidence for the LD2450 adapter."""
+ if profile != "esphome_ld2450_v1" or registry is None:
+ return
+ entities = registry.get("entities") or {}
+ source_ids = _source_ids(sources)
+ rows = [entities.get(entity_id) for entity_id in source_ids]
+ device_ids = {
+ str(row.get("device_id"))
+ for row in rows
+ if isinstance(row, dict) and row.get("device_id")
+ }
+ if len(rows) != len(source_ids) or any(not isinstance(row, dict) for row in rows) \
+ or len(device_ids) != 1 \
+ or any(str(row.get("platform") or "") != "esphome" for row in rows if row):
+ _invalid("LD2450 sources must belong to the same ESPHome device")
+ device_id = next(iter(device_ids))
+ model = (registry.get("devices") or {}).get(device_id, {}).get("model")
+ if _canonical_model(model) not in LD2450_MODELS:
+ _invalid("LD2450 adapter requires verified LD2450 device metadata")
+ binding = str(marker.get("binding") or "")
+ owner_device: str | None = None
+ if binding.startswith("device:"):
+ owner_device = binding.removeprefix("device:")
+ elif binding.startswith("entity:"):
+ owner = entities.get(binding.removeprefix("entity:"))
+ owner_device = str(owner.get("device_id")) if isinstance(owner, dict) and owner.get("device_id") else None
+ if owner_device != device_id:
+ _invalid("LD2450 sources must belong to the marker device")
+
+
def _validate_sources(profile: str, sources: Any) -> None:
obj = _mapping(sources, "radar.sources")
slots = _array(obj.get("slots", []), "radar.sources.slots", 8)
@@ -215,81 +266,12 @@ def _validate_sources(profile: str, sources: Any) -> None:
_entity(obj["availability_entity"], "radar.sources.availability_entity", {"binary_sensor"})
-def _polygon(points: Any, name: str) -> None:
- items = _array(points, name, 64)
- if len(items) < 3:
- _invalid(f"{name} requires at least three points")
- parsed = [_point(point, name) for point in items]
- if any(parsed[index] == parsed[(index + 1) % len(parsed)] for index in range(len(parsed))):
- _invalid(f"{name} contains duplicate adjacent points")
- area = sum(parsed[i][0] * parsed[(i + 1) % len(parsed)][1]
- - parsed[(i + 1) % len(parsed)][0] * parsed[i][1]
- for i in range(len(parsed))) / 2
- if abs(area) <= 1e-9:
- _invalid(f"{name} has zero area")
-
-
-def _validate_stage2(radar: dict[str, Any]) -> None:
- zones = radar.get("zones")
- if zones is not None:
- obj = _mapping(zones, "radar.zones")
- local = _array(obj.get("local", []), "radar.zones.local", 32)
- seen: set[str] = set()
- for index, zone in enumerate(local):
- item = _mapping(zone, f"radar.zones.local[{index}]")
- ident = _identifier(item.get("id"), "radar zone id")
- if ident in seen:
- _invalid("radar zone ids must be unique")
- seen.add(ident)
- if not isinstance(item.get("name"), str) or not 1 <= len(item["name"].strip()) <= 80:
- _invalid("radar zone name is invalid")
- _polygon(item.get("poly"), "radar zone polygon")
- state = _mapping(item.get("state"), "radar zone state")
- kind = state.get("kind")
- if kind not in {"targets", "occupancy", "count"}:
- _invalid("radar zone state kind is invalid")
- if kind != "targets":
- _entity(state.get("entity_id"), "radar zone state entity",
- {"binary_sensor"} if kind == "occupancy" else {"sensor"})
- hardware = obj.get("hardware")
- if hardware is not None:
- item = _mapping(hardware, "radar.zones.hardware")
- if item.get("adapter") != "esphome_ld2450_numbers_v1":
- _invalid("unsupported radar hardware adapter")
- _entity(item.get("mode_entity"), "radar hardware mode", {"select"})
- slots = _array(item.get("slots"), "radar hardware slots", 3)
- if len(slots) != 3:
- _invalid("radar hardware requires three slots")
- entity_ids: set[str] = {item["mode_entity"]}
- for expected, slot in enumerate(slots, 1):
- entry = _mapping(slot, "radar hardware slot")
- if entry.get("slot") != expected:
- _invalid("radar hardware slots must be ordered 1..3")
- for key in ("x1_entity", "y1_entity", "x2_entity", "y2_entity"):
- entity_id = _entity(entry.get(key), f"radar hardware {key}", {"number"})
- if entity_id in entity_ids:
- _invalid("radar hardware entities must be distinct")
- entity_ids.add(entity_id)
-
- reflectors = _array(radar.get("reflectors", []), "radar.reflectors", 8)
- seen_reflectors: set[str] = set()
- for item in reflectors:
- line = _mapping(item, "radar reflector")
- ident = _identifier(line.get("id"), "radar reflector id")
- if ident in seen_reflectors:
- _invalid("radar reflector ids must be unique")
- seen_reflectors.add(ident)
- if not isinstance(line.get("name"), str) or not 1 <= len(line["name"].strip()) <= 80:
- _invalid("radar reflector name is invalid")
- if not isinstance(line.get("enabled"), bool):
- _invalid("radar reflector enabled must be boolean")
- a = _point(line.get("a"), "radar reflector a")
- b = _point(line.get("b"), "radar reflector b")
- if a == b:
- _invalid("radar reflector has zero length")
-
-
-def _validate_radar(radar: Any, spaces: dict[str, dict[str, Any]], marker: dict[str, Any]) -> None:
+def _validate_radar(
+ radar: Any,
+ spaces: dict[str, dict[str, Any]],
+ marker: dict[str, Any],
+ registry: dict[str, dict[str, dict[str, Any]]] | None = None,
+) -> None:
obj = _mapping(radar, "marker.radar")
if obj.get("version") != 1:
_invalid("unsupported radar version")
@@ -301,6 +283,7 @@ def _validate_radar(radar: Any, spaces: dict[str, dict[str, Any]], marker: dict[
if profile not in RADAR_PROFILES:
_invalid("unsupported radar profile")
_validate_sources(profile, obj.get("sources"))
+ _validate_verified_adapter(profile, obj["sources"], marker, registry)
room_id = obj.get("room_id")
if not isinstance(room_id, str) or not room_id:
_invalid("radar.room_id is required")
@@ -364,7 +347,6 @@ def _validate_radar(radar: Any, spaces: dict[str, dict[str, Any]], marker: dict[
_invalid("manual calibration cannot contain captured references")
if calibration.get("rms_cm") is not None:
_finite(calibration["rms_cm"], "radar calibration rms_cm", 0, 30)
- _validate_stage2(obj)
allowed = obj.get("allowed_room_ids")
if allowed is not None:
room_ids = _array(allowed, "radar.allowed_room_ids", 32)
@@ -373,54 +355,17 @@ def _validate_radar(radar: Any, spaces: dict[str, dict[str, Any]], marker: dict[
_invalid("radar allowed rooms must be unique rooms in the marker space")
-def _validate_settings(settings: Any, spaces: dict[str, dict[str, Any]],
- markers: dict[str, dict[str, Any]]) -> None:
+def _validate_settings(settings: Any) -> None:
obj = _mapping(settings, "settings.radar")
if obj.get("version") not in (None, 1):
_invalid("unsupported radar settings version")
if obj.get("show_live") is not None and not isinstance(obj.get("show_live"), bool):
_invalid("settings.radar.show_live must be boolean")
- groups = _array(obj.get("fusion_groups", []), "settings.radar.fusion_groups", 32)
- group_ids: set[str] = set()
- used_markers: set[str] = set()
- for group in groups:
- item = _mapping(group, "radar fusion group")
- ident = _identifier(item.get("id"), "radar fusion group id")
- if ident in group_ids:
- _invalid("radar fusion group ids must be unique")
- group_ids.add(ident)
- if not isinstance(item.get("enabled"), bool):
- _invalid("radar fusion group enabled must be boolean")
- space_id = item.get("space_id")
- if space_id not in spaces:
- _invalid("radar fusion group space is invalid")
- marker_ids = _array(item.get("marker_ids"), "radar fusion marker_ids", 8)
- if len(marker_ids) < 2 or len(set(marker_ids)) != len(marker_ids):
- _invalid("radar fusion group requires 2..8 unique markers")
- for marker_id in marker_ids:
- marker = markers.get(str(marker_id))
- if marker is None or marker.get("space") != space_id or marker_id in used_markers:
- _invalid("radar fusion marker ownership is invalid")
- used_markers.add(str(marker_id))
- outputs = _array(obj.get("room_outputs", []), "settings.radar.room_outputs", 64)
- output_ids: set[str] = set()
- for output in outputs:
- item = _mapping(output, "radar room output")
- ident = _identifier(item.get("id"), "radar room output id")
- if ident in output_ids:
- _invalid("radar room output ids must be unique")
- output_ids.add(ident)
- space = spaces.get(str(item.get("space_id")))
- if space is None or str(item.get("room_id")) not in {
- str(room.get("id")) for room in space.get("rooms") or []
- }:
- _invalid("radar room output owner is invalid")
- if item.get("presence") not in (None, True, False) or item.get("estimated_count") not in (None, True, False):
- _invalid("radar room output flags must be boolean")
def validate_marker_radars(config: dict[str, Any], previous: dict[str, Any] | None = None,
- *, validate_all: bool = False) -> None:
+ *, validate_all: bool = False,
+ registry: dict[str, dict[str, dict[str, Any]]] | None = None) -> None:
"""Validate only newly created/changed known radar namespaces.
This mirrors the project's lossless compatibility doctrine: untouched
@@ -442,7 +387,7 @@ def validate_marker_radars(config: dict[str, Any], previous: dict[str, Any] | No
configured += 1
old = previous_markers.get(marker_id, {}).get("radar", object())
if validate_all or radar != old:
- _validate_radar(radar, spaces, marker)
+ _validate_radar(radar, spaces, marker, registry)
if configured > MAX_RADARS:
_invalid("at most 32 radars may be configured")
@@ -450,11 +395,12 @@ def validate_marker_radars(config: dict[str, Any], previous: dict[str, Any] | No
settings = (config.get("settings") or {}).get("radar", missing)
previous_settings = ((previous or {}).get("settings") or {}).get("radar", missing)
if settings is not missing and (validate_all or settings != previous_settings):
- _validate_settings(settings, spaces, markers)
+ _validate_settings(settings)
def validate_radar_draft(
config: dict[str, Any], marker_id: str, radar: Any,
+ registry: dict[str, dict[str, dict[str, Any]]] | None = None,
) -> tuple[dict[str, Any], set[str]]:
"""Validate an unsaved setup block against its exact stored marker owner."""
marker = next(
@@ -467,5 +413,5 @@ def validate_radar_draft(
candidate = copy.deepcopy(marker)
candidate["radar"] = copy.deepcopy(radar)
spaces = {str(space.get("id")): space for space in config.get("spaces") or []}
- _validate_radar(candidate["radar"], spaces, candidate)
+ _validate_radar(candidate["radar"], spaces, candidate, registry)
return candidate, radar_source_entity_ids(candidate["radar"])
diff --git a/custom_components/houseplan/radar_websocket.py b/custom_components/houseplan/radar_websocket.py
index 9f8088b5..955856c1 100644
--- a/custom_components/houseplan/radar_websocket.py
+++ b/custom_components/houseplan/radar_websocket.py
@@ -18,7 +18,11 @@ from homeassistant.helpers.event import (
from .auth import may_write
from .radar import MAX_FRAME_HZ, RadarCoordinator
-from .radar_validation import RadarValidationError, validate_radar_draft
+from .radar_validation import (
+ RadarValidationError,
+ radar_registry_evidence,
+ validate_radar_draft,
+)
from .store import get_data
_INSPECT_CALLS: dict[str, deque[float]] = defaultdict(deque)
@@ -41,6 +45,7 @@ def _validated_draft(
raise vol.Invalid("radar draft is too large")
marker, source_ids = validate_radar_draft(
coordinator.config, message["marker_id"], radar,
+ radar_registry_evidence(coordinator.hass),
)
except RadarValidationError:
connection.send_error(message["id"], "invalid_radar", "invalid_radar")
@@ -51,12 +56,22 @@ def _validated_draft(
if not _can_read(connection, source_ids):
connection.send_error(message["id"], "source_restricted", "source_restricted")
return None
+ if any(
+ coordinator.hass.states.get(entity_id) is None
+ or str(coordinator.hass.states.get(entity_id).state) in {"unknown", "unavailable"}
+ for entity_id in source_ids
+ ):
+ connection.send_error(message["id"], "source_unavailable", "source_unavailable")
+ return None
return marker, radar, source_ids
def _coordinator(hass: HomeAssistant, connection, msg_id: int) -> RadarCoordinator | None:
runtime = get_data(hass)
coordinator = getattr(runtime, "radar_coordinator", None) if runtime else None
+ if runtime is not None and coordinator is None:
+ connection.send_error(msg_id, "unsupported_capability", "unsupported_capability")
+ return None
if not isinstance(coordinator, RadarCoordinator) or coordinator.closed:
connection.send_error(msg_id, "not_ready", "not_ready")
return None
diff --git a/custom_components/houseplan/websocket_api.py b/custom_components/houseplan/websocket_api.py
index dd01119c..7ff76f41 100755
--- a/custom_components/houseplan/websocket_api.py
+++ b/custom_components/houseplan/websocket_api.py
@@ -81,7 +81,11 @@ from .plans import (
reserve_filename,
)
from .projection import project_config, project_layout
-from .radar_validation import RadarValidationError, validate_marker_radars
+from .radar_validation import (
+ RadarValidationError,
+ radar_registry_evidence,
+ validate_marker_radars,
+)
from .registry_snapshot import import_registry_snapshot
from .store import (
LAYOUT_STORE_CORE_KEYS,
@@ -1650,6 +1654,8 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
baseline_counts = baseline[1] if baseline and baseline[0] == current_rev else None
readable_entity_ids = _readable_entity_ids(hass, connection)
+ radar_registry = radar_registry_evidence(hass)
+
def _validate_config_cpu():
def _normalize(candidate):
validate_wall_model_transition(candidate, data.get("config"))
@@ -1662,7 +1668,9 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
msg["config"].update(checked)
validate_marker_controls(msg["config"], data.get("config"))
validate_marker_light_entities(msg["config"], data.get("config"))
- validate_marker_radars(msg["config"], data.get("config"))
+ validate_marker_radars(
+ msg["config"], data.get("config"), registry=radar_registry,
+ )
validate_marker_value_badges(msg["config"], data.get("config"))
validate_marker_vacuum_routes(msg["config"], data.get("config"))
validate_opening_passages(msg["config"], data.get("config"))
@@ -2014,6 +2022,8 @@ async def ws_plan_optimize(hass: HomeAssistant, connection, msg: dict[str, Any])
# #330 §4.1: the same executor treatment as config/set — Optimize
# carries schema + a possible full migration, the costliest CPU path
# of all writers, and it used to run on the event loop.
+ optimize_radar_registry = radar_registry_evidence(hass)
+
def _validate_optimize_cpu():
def _normalize(candidate):
validate_wall_model_transition(candidate, config_data.get("config"))
@@ -2046,7 +2056,10 @@ async def ws_plan_optimize(hass: HomeAssistant, connection, msg: dict[str, Any])
msg["config"].update(checked)
validate_marker_controls(msg["config"], config_data.get("config"))
validate_marker_light_entities(msg["config"], config_data.get("config"))
- validate_marker_radars(msg["config"], config_data.get("config"))
+ validate_marker_radars(
+ msg["config"], config_data.get("config"),
+ registry=optimize_radar_registry,
+ )
validate_marker_value_badges(msg["config"], config_data.get("config"))
validate_marker_vacuum_routes(msg["config"], config_data.get("config"))
validate_opening_passages(msg["config"], config_data.get("config"))
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 99554add..393844c2 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -45,7 +45,10 @@
House Plan recognizes ESPHome LD2450 coordinate sets, also supports explicit
Cartesian, polar, range, zone-state and presence-only mappings, and provides
an on-plan installation/calibration flow with source-health diagnostics. Raw
- observations and the short diagnostic trail remain session-only
+ observations and the short diagnostic trail remain session-only. LD2450
+ auto-detection now requires an exact ESPHome, same-device registry match;
+ changing a saved binding keeps its radar setup available for repair, while
+ setup cancellation and source-health states are reported consistently
([#485](https://github.com/Matysh/houseplan-card/issues/485)).
## v1.73.0-beta.6 — 2026-09-08
diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md
index 38051cc6..3e7322fd 100755
--- a/docs/CHANGELOG.ru.md
+++ b/docs/CHANGELOG.ru.md
@@ -56,7 +56,11 @@
наборы ESPHome LD2450, поддерживает явное сопоставление декартовых, полярных,
дальностных, зональных и presence-only источников и проводит установку и
калибровку прямо на плане с диагностикой состояния данных. Сырые наблюдения и
- короткий диагностический след остаются только в текущей сессии
+ короткий диагностический след остаются только в текущей сессии. Автоопределение
+ LD2450 теперь требует точного совпадения модели и общего устройства ESPHome;
+ смена сохранённой привязки не удаляет настройку радара, оставляя её доступной
+ для исправления, а отмена установки и статусы источников обрабатываются
+ согласованно
([#485](https://github.com/Matysh/houseplan-card/issues/485)).
## v1.73.0-beta.6 — 2026-09-08
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index b96800f1..d440a18c 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -750,7 +750,7 @@ unavailable sources and presence without a usable position.
Live dots and range arcs do not intercept clicks and are clipped to the selected
room. Their short trail and smoothing exist only in the open browser session;
House Plan does not save raw radar samples or target history. Disable either
-the radar itself or **General settings → Show presence on the plan** to hide the
+the radar itself or **General settings → Show live presence on the plan** to hide the
layer. See [Presence radars](RADAR.md) for supported profiles, setup and privacy.
## 11. Tap actions
diff --git a/docs/USER-GUIDE.ru.md b/docs/USER-GUIDE.ru.md
index 8a331257..8c8b78ae 100644
--- a/docs/USER-GUIDE.ru.md
+++ b/docs/USER-GUIDE.ru.md
@@ -1107,7 +1107,7 @@ HA. «Показать» заблокировано до активации. П
Живые точки и дуги дальности не перехватывают клики и обрезаются по выбранной
комнате. Короткий след и сглаживание живут только в открытой сессии браузера:
сырые измерения и история целей не сохраняются. Слой можно выключить у
-конкретного радара или глобально в **«Общие настройки → Показывать присутствие
+конкретного радара или глобально в **«Общие настройки → Показывать текущее присутствие
на плане»**. Подробности о профилях, настройке и приватности — в
[документе о радарах присутствия](RADAR.md).
diff --git a/src/editors/radar-section.ts b/src/editors/radar-section.ts
index e7f58c6f..52c49457 100644
--- a/src/editors/radar-section.ts
+++ b/src/editors/radar-section.ts
@@ -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`${options.t('radar.additional_actions')}
+
+