mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 06:59:46 +00:00
docs: specify radar presence stages for #485
Issue: #485 User-Visible: no
This commit is contained in:
@@ -0,0 +1,534 @@
|
||||
# #485 — Stage 1: sources, calibration and live presence
|
||||
|
||||
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485).
|
||||
Read the [common contract](485-radar-presence.md) first. This is a planned
|
||||
implementation, not code already shipped. The owner requested stopping at S5
|
||||
after independent review; this document does not authorize starting S6 now.
|
||||
|
||||
## 1. Before / after, scope and integration seams
|
||||
|
||||
Before, an administrator can place the ordinary radar device but cannot see its
|
||||
coordinate observations on the plan. They read separate HA sensors/spreadsheets.
|
||||
After, they bind available sources, identify the physical installation, calibrate
|
||||
and see current observations; household members need no editor or hover action.
|
||||
Presence, position and source availability are separate facts.
|
||||
|
||||
Included: explicit source/capability model; safe discovery/manual mapping; physical
|
||||
mount, orientation, mirror and known units; two-position calibration and manual
|
||||
alternative; live dots/arcs/read-only known zones/presence; one-room clipping;
|
||||
short diagnostic trail; health, validation, permissions and lifecycle. Excluded:
|
||||
raw recording, zone writes/drawing, reflectors, multi-room fusion, heat maps and
|
||||
derived HA entities (specified in stages 2–3). No stable person identification.
|
||||
|
||||
Existing integration points, inspected on the common baseline:
|
||||
|
||||
| Existing file/module | Required change |
|
||||
|---|---|
|
||||
| `src/types.ts`, `src/config-store.ts` | Optional marker/settings fields and revisioned save; no local authoritative parallel config |
|
||||
| `src/houseplan-editor-runtime.ts` | Add section to existing marker dialog; delegate lazy setup instead of growing monolith |
|
||||
| `src/editor-secondary.ts`, `src/editors/` | Reuse context surface for selected radar setup and safe exit |
|
||||
| `src/houseplan-card.ts`, `render-device-snapshot.ts`, `houseplan-render-lifecycle.ts` | Subscribe selected space; render normalized live snapshots without reparsing HA values per marker |
|
||||
| `src/space-geometry.ts`, `src/live-viewport.ts` | Canonical square normalized/render conversion and existing projection/viewport seams |
|
||||
| `src/styles/{devices,dialogs,chrome}.styles.ts`, i18n dictionaries | Quiet live visuals, setup layout and en/ru text |
|
||||
| `custom_components/houseplan/{__init__,store}.py` | Coordinator setup/reconcile/unload ownership |
|
||||
| `custom_components/houseplan/{validation,websocket_api,auth}.py` | Change-aware validation, capabilities, authenticated commands and one `may_write` policy |
|
||||
| `scripts/config-schema.json`, `scripts/config-field-registry.mjs` | Generated schema parity and explicit compatibility classification |
|
||||
|
||||
Proposed pure/client modules: `src/radar-model.ts`, `radar-geometry.ts`,
|
||||
`radar-live.ts`, `radar-render.ts`, `src/editors/radar-setup.ts`. Backend:
|
||||
`radar.py` (source coordinator), `radar_geometry.py`, `radar_validation.py`,
|
||||
`radar_websocket.py`. Subsequent stages extend these contracts, not a second
|
||||
source pipeline. Existing vacuum calibration/trails are precedents, not shared
|
||||
mutable state. Names may change in review without changing the contracts.
|
||||
|
||||
## 2. Entry, defaults and ordinary use
|
||||
|
||||
Device editor → select an existing device or placed-entity marker → **Presence
|
||||
on plan**. The section is collapsed/off until explicitly configured. No automatic
|
||||
enabling on upgrade, placement or source discovery. A virtual marker cannot own
|
||||
a live radar binding. Source entities need not be separately placed on the plan.
|
||||
|
||||
The first section has: configure switch; connection/profile; capability summary;
|
||||
selected room; **Configure on plan**; **Show live presence**. Unsupported data
|
||||
gets a short explanation and manual source selection, not dozens of disabled
|
||||
advanced controls. Advanced displays exact source ids, values, units and report
|
||||
ages so an administrator can correct a connection without editing YAML.
|
||||
|
||||
General Settings → Display adds **Show presence on the plan**, default true.
|
||||
Both global and per-radar `show_live` must be true, and the radar must be enabled
|
||||
and valid. Hiding this layer does not alter HA presence, calibration, or later
|
||||
recording consent. The global switch hides all live radar layers for shared
|
||||
configuration; an unsaved diagnostic overlay remains local to its setup session.
|
||||
|
||||
Ordinary View shows configured current marks automatically. No diagnostic trails,
|
||||
sensor sectors, raw counts, point labels or setup handles. Device actions retain
|
||||
the selected `tap_action`; no extra action is forced onto the icon. Radar health
|
||||
appears in the existing device information surface when opened by the supported
|
||||
action/hold path, and in setup. Dot overlays never become independent devices.
|
||||
Presence-only sources keep the existing marker state; do not add a floor fill
|
||||
which competes with existing light effects. Known occupied zones use a thin
|
||||
outline/very low-opacity presence overlay distinct from light Glow.
|
||||
|
||||
## 3. Source resolution and capabilities
|
||||
|
||||
Persist exact entity ids only. Discovery can propose candidates from the owning
|
||||
HA device and a verified adapter signature; accepting the summary is explicit.
|
||||
Do not infer axes/units from translated friendly names, or select an unrelated
|
||||
global entity with a similar name. A placed entity resolves its current registry
|
||||
device for discovery; missing/limited metadata offers manual selection without
|
||||
claiming deleted or disabled. Existing binding-status authority remains in use.
|
||||
|
||||
For LD2450 automatic role mapping, require the same ESPHome device, recognized
|
||||
LD2450 descriptor and a complete unique role set exposed by registry metadata.
|
||||
If metadata cannot prove a unique x/y pairing/slot, preselect nothing: show the
|
||||
candidate list grouped by that device for manual confirmation. No entity registry
|
||||
enablement, ESPHome reflash, HA restart or `force_update` edit occurs here.
|
||||
|
||||
Profiles and capability inputs:
|
||||
|
||||
| Profile | Required sources / conventions | Output |
|
||||
|---|---|---|
|
||||
| `esphome_ld2450_v1` | 1–3 explicitly matched X/Y slots, units mm; canonical x right, y forward; optional per-slot presence/speed and global occupancy/count | Cartesian targets; optional known hardware-zone read capability |
|
||||
| `cartesian_v1` | 1–8 slots of exact numeric x/y sources, declared unit and axis mapping; optional gates | Cartesian targets |
|
||||
| `polar_v1` | 1–8 slots with distance and bearing, declared length/angle units and angle origin/direction | Convert to canonical Cartesian targets; no inferred bearing |
|
||||
| `range_v1` | One or two named distance channels, explicit length unit, optional occupancy gate | Arc(s), not two people; direction/FOV must be known or explicitly entered |
|
||||
| `zones_v1` | Existing exact occupancy/count entities; known adapter geometry if available | Read-only zone outlines; unknown geometry awaits Stage-2 manual mapping |
|
||||
| `presence_v1` | Exact `binary_sensor` occupancy, optional known diagnostic range/FOV | Presence state only; no position or count |
|
||||
|
||||
Known units mm/cm/m/in/ft convert by exact factors 0.1/1/100/2.54/30.48 to cm.
|
||||
Absent/unrecognized units require explicit user confirmation of the declared
|
||||
unit; conflicting present units are rejected with the source identified. Never
|
||||
guess from values. Cartesian axis mapping is a permutation of x/y and signs
|
||||
±1, no arbitrary formula. Polar input declares radians/degrees, zero-forward
|
||||
or zero-right and clockwise/counterclockwise; distance is nonnegative. Source
|
||||
strings `unknown`, `unavailable`, empty, NaN/Infinity are not numeric zero.
|
||||
Cartesian x=0, negative x and valid origin observations remain legal.
|
||||
|
||||
Global occupancy `off` suppresses all targets immediately; `unknown/unavailable`
|
||||
gate suppresses gated geometry without becoming `off`. Per-slot gates affect
|
||||
their own slots. `on` allows coordinates but never manufactures them. Optional
|
||||
count is a consistency/diagnostic input, not a stable assignment of slot numbers
|
||||
or permission to keep unknown slots visible. A contradictory fresh zero count
|
||||
with valid coordinates is `inconsistent`, not a silently chosen truth.
|
||||
|
||||
Backend capabilities are runtime facts: `coordinates`, `range`, `zone_state`,
|
||||
`known_zone_geometry`, `reported_presence`, `coherent_frames`, `hardware_zones`.
|
||||
Stage-1 hardware zones are read-only and require the complete same-device
|
||||
descriptor later specified in Stage 2; optimistic HA values are marked as HA
|
||||
reported geometry, not device-verified configuration. FP2 branding alone never
|
||||
grants coordinates or hardware writes.
|
||||
|
||||
## 4. Measurement health, pairing and delivery
|
||||
|
||||
Source freshness is **HA report freshness**, not proof that a UART/radio produced
|
||||
a new physical measurement. HA 2024.6.0 (the current minimum) already contains
|
||||
`state_reported` and `State.last_reported`. Subscribe with an event filter to
|
||||
the exact configured entity ids; never listen to all state reports or use
|
||||
`last_changed` as sample time. The backend timestamp is authoritative.
|
||||
|
||||
For independent numeric entities, `pair_quality:'bounded_latest'` means both
|
||||
values were reported within 3 seconds and their report times differ by <=1.5
|
||||
seconds. Coalesce reports for 100 ms before publishing a pair. This tolerates
|
||||
independent ~1 Hz channels but **does not claim an atomic sensor frame**. If a
|
||||
future verified adapter exposes a common sample sequence/timestamp in its
|
||||
defined payload, use it to establish `coherent`; generic arbitrary attribute
|
||||
expressions are not part of this implementation. Stages 2–3 may not upgrade
|
||||
`bounded_latest` to coherent merely because values are numerically close.
|
||||
|
||||
The standard LD2450 connection suppresses repeated coordinate values and its
|
||||
timeout filter repeats the last value only once. A stationary target, or a
|
||||
target with an unchanged axis, can therefore lose usable current coordinates.
|
||||
After either axis expires, hide its dot and show **Presence reported; current
|
||||
position unavailable** if occupancy remains on. Setup explains this telemetry
|
||||
limitation and shows which axis has no recent report; no silent firmware edits
|
||||
or guessed movement repair it. A profile that legitimately republishes values
|
||||
may keep them visible while fresh: UI calls these reported coordinates, never
|
||||
guaranteed physical freshness. An integration that continuously republishes
|
||||
incorrect data cannot be detected from HA state alone.
|
||||
|
||||
Range channels use the same <=3 s report lease. Binary presence/zone entities
|
||||
use HA's current on/off/unknown/unavailable contract, not the coordinate lease:
|
||||
a still person legitimately produces no binary transitions for a long time.
|
||||
Their last change is labelled as a state change, not “last measured”. Losing a
|
||||
bound entity or configured availability gate yields unknown/unavailable. Never
|
||||
equate HA node connectivity with proof of fresh radar coordinates.
|
||||
|
||||
On coordinator startup/reconnect, initial values keep their actual report ages;
|
||||
loading a snapshot does not renew them. After a failed/incomplete source epoch,
|
||||
require a complete eligible pair before showing a dot again. Invalid slot
|
||||
coordinates clear that slot immediately, even during a presence hold timeout.
|
||||
No guessed zero-target frame from an empty list of expired slots.
|
||||
|
||||
For LD2450, `unknown` X/Y by itself is not explicit absence: the driver also
|
||||
uses missing values when a slot is unused. Global occupancy `off` is explicit
|
||||
negative evidence under that source's current HA state semantics; an eligible
|
||||
fresh global target count of zero with no conflicting valid target is also
|
||||
explicit negative. Otherwise an unknown slot keeps coordinate completeness
|
||||
false unless the verified profile has an explicit per-slot absence signal.
|
||||
Do not silently deduce missing slots from a nonzero count or assume that target
|
||||
indices are packed. Generic profiles use exact per-slot/global gates for the
|
||||
same purpose. A complete negative produces an empty frame, not an error.
|
||||
|
||||
Normalized frame contains `server_session_id,marker_id,source_generation,
|
||||
calibration_revision,seq,reported_at,expires_at,health,reported_presence,
|
||||
complete,targets[],ranges[],zones[]`. A target includes `slot`, canonical cm
|
||||
`x,y`, report times, `pair_quality`, projected plan position, `included` and
|
||||
exclusion reason. `complete` means every configured slot is either a valid
|
||||
current observation or explicitly absent in a supported source report; stale
|
||||
missing data makes the frame incomplete. Source slot ids are ephemeral labels.
|
||||
|
||||
Live subscription is authenticated, selected by exact space id, maximum 32
|
||||
radars/space response, <=256 targets total. Read-only View receives only allowed
|
||||
live projected output/health, no raw coordinates, source ids or historical
|
||||
buffers. Require HA read permission for every bound entity contributing to a
|
||||
delivered radar; otherwise withhold that radar's values as restricted. Setup
|
||||
detail requires `may_write` plus the same source read checks. Revoked permissions
|
||||
are rechecked on publish and at most each minute even without source events;
|
||||
clear the affected connection's output. Do not publish coordinates on broad HA
|
||||
events. Use connection-scoped WS delivery.
|
||||
|
||||
Publish at most 4 geometry frames/s/radar, latest-wins within a bounded slot;
|
||||
health/clear transitions are immediate and cannot be dropped by rate limiting.
|
||||
Maximum one active subscription per card instance; server deduplicates source
|
||||
listeners. On space/mode change, page hidden, disconnect or unmount, unsubscribe
|
||||
and clear local arrays/trails; resume uses a new snapshot, never interpolation
|
||||
from the previous session. Browser uses remaining lease measured monotonically
|
||||
from receipt, reduced by transport age; expiration hides output even if WS dies
|
||||
without a clean close. Invalid or late session/sequence/revision messages are
|
||||
discarded. A slow consumer receives latest state, not an unbounded frame queue.
|
||||
|
||||
## 5. Geometry and calibration
|
||||
|
||||
### 5.1 Canonical space
|
||||
|
||||
Use actual current `src/space-geometry.ts`, not the legacy bitmap coordinate
|
||||
section of ARCHITECTURE: persisted coordinates are square normalized units;
|
||||
render space is `NORM_W=1000` square; `GRID_N=240`; `GRID_PITCH=1000/240`.
|
||||
One normalized unit represents `240 * cell_cm` physical centimetres. Stored
|
||||
spaces without `cell_cm` retain the existing 5 cm compatibility default; new
|
||||
space defaults are not changed by this issue.
|
||||
|
||||
Physical mount `m=(mx,my)` is stored in normalized plan units independently of
|
||||
the marker's decorative layout position. For local centimetres `(x,y)` with
|
||||
x right and y forward, heading θ clockwise from plan-up and mirror `s=-1`
|
||||
or nonmirror `s=1`, projection is:
|
||||
|
||||
`p = m + (s*x*cosθ + y*sinθ, s*x*sinθ - y*cosθ) / (240*cell_cm)`.
|
||||
|
||||
Inverse uses the transpose orthogonal matrix and the same known scale. No
|
||||
independent x/y stretch, fit-to-room rectangle, north/compass substitution or
|
||||
wallpaper transform. Plan up is not geographic north. Validate finite results
|
||||
within `CANVAS_LIMIT=5000` normalized; a physically impossible numerical input
|
||||
(absolute local distance >100 m) is invalid rather than clamped onto the plan.
|
||||
Shared TS/Python fixtures cover ±x, y=0, headings 0/90/180/270, mirror, imperial
|
||||
conversion, non-origin mount and 1/5 cm spaces representing the same room.
|
||||
|
||||
### 5.2 Wizard, no premature persistence
|
||||
|
||||
**Step 1: Installation.** Select the existing room; place the real sensor mount
|
||||
on the plan and drag its direction arrow. If decorative marker placement is
|
||||
available, offer it as a starting suggestion only, with text distinguishing it
|
||||
from physical installation. Show known/entered sector as an approximate guide;
|
||||
unknown FOV stays absent. The room must belong to the exact marker's owner
|
||||
space. A room without contour warns “No room boundary: points are not clipped”.
|
||||
|
||||
**Step 2: Two reference positions.** Place reference A on the plan → Start
|
||||
measurement → 10-second countdown to walk there → 5-second capture while alone
|
||||
and still. Collect at least three eligible coordinate pairs spanning >=2 s,
|
||||
with exactly one valid target throughout capture and no competing slot/gate
|
||||
failure. Median X/Y is the sample; reject if any sample is >15 cm from the
|
||||
median. Then do B. Missing/insufficient/ambiguous data offers Retry, never an
|
||||
arbitrary slot selection. All measurements remain browser memory only.
|
||||
|
||||
Mount and units are known. Each reference must be >=50 cm from mount; vectors
|
||||
to A and B must form an angle between 20° and 160° (nearly collinear vectors
|
||||
cannot determine mirror). For both mirror hypotheses, solve the best rigid
|
||||
rotation about the mount by least squares using both vectors, with unit scale
|
||||
fixed. Require each radial-distance mismatch <=max(20 cm,15% of reference
|
||||
distance), RMS projected error <=20 cm and individual error <=30 cm. If both
|
||||
mirror candidates pass with RMS difference <10 cm, report ambiguous rather
|
||||
than choose. These are explicit engineering tolerances, not accuracy promises.
|
||||
|
||||
Manual alternative exposes direction and mirror with live preview; it does
|
||||
not fit fake translation/scale or mark an unmeasured setup as auto-verified.
|
||||
Range-only requires mount/direction/known FOV, not a two-point position fit;
|
||||
zone/presence-only skips coordinate calibration. If range FOV is unknown,
|
||||
retain numeric range in setup and presence only in View until supplied.
|
||||
|
||||
**Step 3: Check.** Show live output and optional local 8-second diagnostic trail;
|
||||
offer a third independent marked position and display observed distance error
|
||||
without adjusting the fitted parameters. Failed data remains an explicit error.
|
||||
The third check is recommended, not a hidden Save prerequisite. Summary states
|
||||
auto-fit/manual, room, source class, unit and whether clipping is available.
|
||||
Save commits one revisioned radar block after server validation. Cancel/Esc,
|
||||
source/space removal, leaving the editor or lost permission disposes the draft,
|
||||
restores the previous view and makes no config/service write. Closing a dirty
|
||||
wizard asks to discard; losing a source does not silently save partial setup.
|
||||
|
||||
Two clients: Save with stale `expected_rev` returns conflict; keep draft for
|
||||
inspection, offer Reload, do not overwrite or rebase physical calibration
|
||||
silently. Changing source/unit/axis/installation during a draft resets captures.
|
||||
An incoming accepted change to that radar invalidates the draft save token.
|
||||
|
||||
### 5.3 Live membership and rendering
|
||||
|
||||
If selected room has a valid real polygon, use its existing physical room
|
||||
boundary authority, including concavity. Do not derive a rectangle from label
|
||||
coordinates. Points inside/on the boundary (physical tolerance 0.1 cm) remain;
|
||||
outside-room observations are excluded from display, not diagnosed as ghosts.
|
||||
Missing/invalid/deleted room ref suspends the radar; existing room with no
|
||||
usable contour allows unclipped calibrated output with the stated warning.
|
||||
|
||||
Membership uses raw projected points before interpolation. Arcs are clipped
|
||||
geometrically to the allowed polygon, not tested solely at their midpoint;
|
||||
known zone geometry intersects that polygon. If clipping removes all geometry,
|
||||
do not replace it with a point on the wall or mark the room empty on this basis.
|
||||
|
||||
Dots: 8 CSS px diameter, contrast rim 2 px and theme-aware presence accent;
|
||||
never depend on color alone. Render within a dedicated pointer-transparent
|
||||
floor-level overlay using existing pan/zoom/isometry transforms, compensating
|
||||
screen radius to avoid enormous dots at high zoom. Multiple independent radar
|
||||
observations may overlap; do not claim one-to-one people correspondence. Arcs
|
||||
use 2 CSS px stroke and explicit range uncertainty in the radar info surface.
|
||||
Known occupied zones use 2 px outline and <=0.08 fill opacity; unknown state
|
||||
does not show occupied fill. Ordinary presence-only markers retain existing
|
||||
visuals. No per-frame spoken announcements.
|
||||
|
||||
Visual smoothing lasts <=250 ms, no extrapolation, follows valid included
|
||||
endpoints from one slot/session/generation only. Disable for reduced motion,
|
||||
gaps, availability loss, room/filter changes or jumps >1 m; snap to the new
|
||||
observation instead of drawing a continuous travel claim. Never interpolate
|
||||
through an excluded area: clip the rendered segment/output to allowed geometry.
|
||||
Slots are not identities. Diagnostic trail retains at most 8 seconds/32 samples
|
||||
per slot, is segmented on the same gaps, and is deleted on setup exit/page hide.
|
||||
No Stage-1 persistent history or server raw trail exists.
|
||||
|
||||
## 6. Saved fields, validity and lifecycle
|
||||
|
||||
| Path | Type / semantics |
|
||||
|---|---|
|
||||
| `settings.radar` | Optional object; `show_live?:boolean`, absent true; Stage-3 extensions preserved |
|
||||
| `marker.radar` | Optional object; `version:1`, `enabled:boolean`, `show_live?:boolean` absent true; maximum 32 configured blocks per integration |
|
||||
| `radar.profile` | One of the six profile identifiers in §3 |
|
||||
| `radar.sources` | `{slots?:Slot[],ranges?:Range[],zones?:ZoneSource[],occupancy_entity?:string,count_entity?:string,availability_entity?:string}`; only applicable subsets accepted |
|
||||
| `Slot` | `{id,x_entity,y_entity,unit,swap_xy?,x_sign?,y_sign?,presence_entity?}` for Cartesian; polar instead `{id,distance_entity,angle_entity,unit,angle_unit,angle_zero,angle_clockwise,presence_entity?}`; max8, LD2450 max3; distinct bounded slot ids |
|
||||
| `Range` | `{id,entity_id,unit,presence_entity?}`; max2; moving/still labels are descriptive, not identities |
|
||||
| `ZoneSource` | `{id,kind:'occupancy'\|'count',entity_id}`; max32; Stage1 geometry only from verified adapter descriptor |
|
||||
| `radar.mount` | `{installation_id,x,y,heading_deg,range_cm?,fov_deg?}` installation UUID, normalized finite coordinates, heading [0,360), optional range (0,10000] cm/FOV (0,360]; physical install, not marker layout |
|
||||
| `radar.room_id` | Required exact existing room id in owner space; may be a room without a contour; no nearest-room fallback |
|
||||
| `radar.calibration` | `{method:'manual'\|'two_point'\|'not_required',mirror:boolean,cell_cm:number,refs?:[{plan:{x,y},local_cm:{x,y}}],rms_cm?:number}`; two_point exactly2 refs and valid §5 fit; no timestamps or raw captures |
|
||||
|
||||
Coordinate/length sources are exact `sensor.*`; gates `binary_sensor.*`; count
|
||||
integer >=0; supported bools strict. Existing entity-id maximum255 applies;
|
||||
slot/zone ids 1–64 `[A-Za-z0-9_-]`; no wildcards/templates/JS/attribute expressions.
|
||||
Expected source/entity data can be missing at runtime without deleting config;
|
||||
Save requires structurally valid mappings and confirmed units, not perpetual
|
||||
network availability. Invalid payload/new unknown version is rejected; untouched
|
||||
future version remains lossless/inert on unrelated writes.
|
||||
|
||||
Known-only field validation does not strip Stage-2/3 extensions or unknown
|
||||
siblings. Geometry follows existing normalized coordinate envelope and 2 MiB
|
||||
total config ceiling. Source generations are computed server-side from accepted
|
||||
configuration; a client cannot fake generation to reuse stale setup/history.
|
||||
Stored calibration's `cell_cm` detects a changed physical scale. Source schema
|
||||
permits missing calibration only when disabled; enabling requires a valid method.
|
||||
|
||||
The initial setup and explicit Change installation generate a fresh UUID in
|
||||
the saved mount. That UUID, mount x/y, bindings/conventions and owner space form
|
||||
the server generation fingerprint; heading/mirror/reference corrections alone
|
||||
form a calibration revision. A client-supplied UUID is only a change marker,
|
||||
never an authorization token or a way to preserve generation across changed
|
||||
physical coordinates. Moving a physical sensor away and back must use Change
|
||||
installation even when final x/y happen to be identical. There is no automatic
|
||||
way to detect an unreported physical move from HA coordinates.
|
||||
|
||||
Moving the marker's decorative icon is unrelated to `mount`. Explicit **Change
|
||||
installation** enters setup, suspends accepted projection only upon Save and
|
||||
creates a new source generation; Cancel keeps old calibration. Changing the
|
||||
owner space through existing marker relocation preserves original block inert
|
||||
as needing setup, never projects it onto another floor. Room reassignment is
|
||||
explicit and rechecks membership without silently moving physical mount.
|
||||
|
||||
Space import/duplicate/removal and full/plan-only exports follow the common
|
||||
contract. Extend `_space_marker_dependencies` and copy/remap/cleanup authorities
|
||||
to include radar owner/room refs. Shared wall optimization that preserves room
|
||||
coordinate geometry does not reset calibration. Geometry edits refresh clipping;
|
||||
room deletion stops output, preserved invalid ref requires repair. Physical
|
||||
scale change suspends projection until calibration is acknowledged anew. Empty
|
||||
spaces do not create dummy models or surviving subscriptions. Rename entity:
|
||||
follow existing positive registry-id evidence only if its binding resolver
|
||||
already supports it; never invent cross-name fallback. Otherwise show repair.
|
||||
|
||||
## 7. WebSocket contract and authorization
|
||||
|
||||
`config/get` advertises `radar_stage1_api:1` only after coordinator setup. Absent
|
||||
capability disables radar operations with an update message; fields still survive.
|
||||
Use exact space/marker ids, not arbitrary source arrays in ordinary subscribe.
|
||||
|
||||
| Command | Request | Authority/result |
|
||||
|---|---|---|
|
||||
| `houseplan/radar/subscribe` | `{space_id}` | Authenticated live subscription; per-source read permission; safe projected frames + health only |
|
||||
| `houseplan/radar/setup/inspect` | `{marker_id,draft_sources?}` | `may_write` + source read permission; bounded capability/source-value snapshot; no persistence/services |
|
||||
| `houseplan/radar/setup/subscribe` | `{marker_id,draft_sources?,expected_config_rev}` | Same guards; ephemeral setup values, <=1 draft subscription/user/marker, <=8 globally |
|
||||
|
||||
Draft sources use the same strict schema/size caps and owner checks. Setup
|
||||
does not gain permission to query arbitrary HA state using an unvalidated
|
||||
attribute path. Server rejects disabled/removed markers, stale revisions or
|
||||
invalid source graph before allocating listeners. Subscription ids use HA's
|
||||
normal unsubscribe protocol; no custom unauthenticated stream endpoint.
|
||||
Saving is ordinary `houseplan/config/set` with `expected_rev`, validated via
|
||||
the same source/geometry authority. Stage1 has no device-control command.
|
||||
|
||||
New stable errors: `invalid_radar`, `unsupported_capability`, `not_ready`,
|
||||
`source_unavailable`, `source_restricted`, `invalid_selection`, `conflict`,
|
||||
`rate_limited`. Each maps to localized text; no raw exception/source payload
|
||||
in UI/logs. Inspect <=10requests/min/user; subscribe max4 card streams/user
|
||||
plus the separate bounded setup stream; response <=256KiB. Limits do not extend
|
||||
sample leases. Teardown checks after awaits prevent resurrection on unload.
|
||||
|
||||
## 8. i18n EN/RU
|
||||
|
||||
Keys prefixed `radar.` in the existing dictionaries; reuse common Save/Cancel/
|
||||
Retry/Close and device/room/entity selection keys. Existing locale formatters
|
||||
handle values; every new dynamic error is mapped to a translated key.
|
||||
|
||||
| Key | English | Русский |
|
||||
|---|---|---|
|
||||
| `title` | Presence on plan | Присутствие на плане |
|
||||
| `show_global` | Show presence on the plan | Показывать присутствие на плане |
|
||||
| `enable` | Configure presence on plan | Настроить присутствие на плане |
|
||||
| `show_live` | Show live presence | Показывать текущее присутствие |
|
||||
| `configure` | Configure on plan | Настроить на плане |
|
||||
| `profile` | Connection type | Тип подключения |
|
||||
| `sources` | Data sources | Источники данных |
|
||||
| `manual_sources` | Select sources manually | Выбрать источники вручную |
|
||||
| `ambiguous_sources` | Check the source assignments | Проверьте назначение источников |
|
||||
| `unsupported` | This connection has no supported position data | Подключение не передаёт поддерживаемые данные о положении |
|
||||
| `room` | Observed room | Наблюдаемая комната |
|
||||
| `mount` | Physical sensor position | Физическое положение датчика |
|
||||
| `mount_hint` | Moving the device icon does not move this installation | Перемещение иконки не меняет физическую установку |
|
||||
| `change_installation` | Change installation | Изменить установку |
|
||||
| `heading` | Direction from plan up | Направление относительно верха плана |
|
||||
| `mirror` | Mirror the horizontal axis | Отразить горизонтальную ось |
|
||||
| `unit` | Coordinate unit | Единица координат |
|
||||
| `unit_required` | Confirm the source unit before continuing | Подтвердите единицу источника для продолжения |
|
||||
| `unit_conflict` | The source unit does not match the selected unit | Единица источника не совпадает с выбранной |
|
||||
| `step_install` | Installation | Установка |
|
||||
| `step_measure` | Reference positions | Контрольные положения |
|
||||
| `step_check` | Check and save | Проверить и сохранить |
|
||||
| `mark_reference` | Mark reference {point} on the plan | Укажите положение {point} на плане |
|
||||
| `start_measure` | Start measurement | Начать измерение |
|
||||
| `countdown` | Stand at the mark in {seconds} seconds | Встаньте в отмеченное место через {seconds} с |
|
||||
| `stay_still` | Stand still and alone while measuring | Во время измерения стойте неподвижно и в одиночестве |
|
||||
| `insufficient_samples` | Not enough current coordinates. Retry or use manual setup. | Недостаточно актуальных координат. Повторите или настройте вручную. |
|
||||
| `ambiguous_target` | More than one target: repeat alone | Несколько целей: повторите измерение в одиночестве |
|
||||
| `bad_references` | Choose separated references in different directions from the sensor | Выберите разнесённые положения в разных направлениях от датчика |
|
||||
| `bad_fit` | Measurements do not match. Check units, mount and references. | Измерения не совпадают. Проверьте единицы, установку и контрольные положения. |
|
||||
| `manual_calibration` | Set direction and mirror manually | Задать направление и отражение вручную |
|
||||
| `check_third` | Check at another position | Проверить в другом положении |
|
||||
| `error_distance` | Position difference: {distance} | Расхождение положения: {distance} |
|
||||
| `discard_setup` | Discard the unsaved radar setup? | Отменить несохранённую настройку радара? |
|
||||
| `no_contour` | No room boundary: points are not clipped | Нет контура комнаты: точки не ограничены её границами |
|
||||
| `needs_setup` | Installation needs setup | Требуется настройка установки |
|
||||
| `live` | Current reported positions | Текущие положения по данным датчика |
|
||||
| `clear` | No targets detected | Цели не обнаружены |
|
||||
| `position_missing` | Presence reported; current position unavailable | Датчик сообщает присутствие; актуальное положение недоступно |
|
||||
| `incomplete` | Some position data is unavailable | Часть данных о положении недоступна |
|
||||
| `offline` | Sensor data unavailable | Данные датчика недоступны |
|
||||
| `restricted` | No access to the selected sources | Нет доступа к выбранным источникам |
|
||||
| `inconsistent` | The source reports conflicting values | Источник передаёт противоречивые значения |
|
||||
| `report_age` | Last report: {age} ago | Последнее сообщение: {age} назад |
|
||||
| `report_only` | Report time does not verify a new physical measurement | Время сообщения не подтверждает новое физическое измерение |
|
||||
| `unchanged_axis` | This source may stop reporting an unchanged coordinate; presence can remain available without a position | Источник может не повторять неизменившуюся координату; присутствие может быть доступно без положения |
|
||||
| `range_uncertain` | Distance is known; direction within the arc is unknown | Дальность известна; направление внутри дуги неизвестно |
|
||||
| `coverage_estimate` | Approximate coverage, not a detection guarantee | Примерная область обзора, не гарантия обнаружения |
|
||||
| `trail` | Show the last 8 seconds while checking | Показывать последние 8 секунд при проверке |
|
||||
| `update_backend` | Update the House Plan integration to use presence on plan | Обновите интеграцию House Plan для присутствия на плане |
|
||||
| `desktop_hint` | Use a computer for precise setup | Для точной настройки используйте компьютер |
|
||||
| `profile_cartesian` | Coordinates X/Y | Координаты X/Y |
|
||||
| `profile_polar` | Distance and bearing | Дальность и угол |
|
||||
| `profile_range` | Distance only | Только дальность |
|
||||
| `profile_zones` | Zone states | Состояния зон |
|
||||
| `profile_presence` | Presence only | Только присутствие |
|
||||
| `invalid_radar` | Check the radar settings | Проверьте настройки радара |
|
||||
| `not_ready` | Radar processing is not ready | Обработка данных радара не готова |
|
||||
| `invalid_selection` | Select an existing room and valid sources | Выберите существующую комнату и допустимые источники |
|
||||
| `conflict` | Settings changed. Reload before saving. | Настройки изменились. Перечитайте их перед сохранением. |
|
||||
| `rate_limited` | Too many requests. Try again shortly. | Слишком много запросов. Повторите чуть позже. |
|
||||
|
||||
## 9. Acceptance criteria and protective witnesses
|
||||
|
||||
Every `S1-*` is required. Proposed test names are not claims of completed tests.
|
||||
|
||||
| AC | Contract | Evidence / how it must fail |
|
||||
|---|---|---|
|
||||
| S1-1 | Existing marker entry, explicit enable, global/per-radar switches; no new editor | setup smoke, en/ru light/dark golden; auto-enable on discover => no-write assertion fails |
|
||||
| S1-2 | Exact bindings, deterministic discovery or manual ambiguity, four classes without fabricated capabilities | unit/backend profile fixtures; wrong-name auto-binding or inferred bearing => output differs |
|
||||
| S1-3 | Units, axis/sign/polar conversion are explicit; zero valid, nonfinite rejected | shared TS/Python fixtures; replace invalid with0 or inches factor => numeric/rejection tests fail |
|
||||
| S1-4 | Current independent pairs meet report age/skew; no atomic-frame claim; missing-axis degradation is explained | fake-clock backend + setup smoke; last_changed/renew-on-snapshot/stale-axis mutant => hidden-point assertion fails |
|
||||
| S1-5 | Off/unknown/stale/partial/inconsistent/occupied-without-position distinct; invalid slot clears immediately | backend state table and UI golden; unknown=>clear or held invalid slot => state/set assertion fails |
|
||||
| S1-6 | One coordinator; bounded subscriptions/rate/size; teardown/reconnect never resurrects old data | backend multi-client/unload races; remove queue/session/cap guard => count/old-canary assertions fail |
|
||||
| S1-7 | Physical mount independent of decorative icon; projection uses actual normalized physical scale | unit shared fixtures + editor smoke; use wallpaper/viewbox/north or icon position => projected coordinate fails |
|
||||
| S1-8 | Two reference solve rejects collinear/noisy/ambiguous/multi-target data, no stretch | pure solver tests + source replay wizard; remove each guard => invalid-fit acceptance fails |
|
||||
| S1-9 | Manual/range/zone/presence paths honest; third check independent and optional | setup smoke state fixtures; silently alter fit on third check => parameter equality fails |
|
||||
| S1-10 | Save atomic/revisioned, Cancel/source deletion/revoked permission never persists partial setup | two-client smoke/backend writes; skip revision/cancel guard => config/service trace fails |
|
||||
| S1-11 | Real room polygon clips dots/arcs/zones; missing contour warns, missing room never retargets | concave/shared-boundary fixtures + smoke; replace with bbox/defaultroom => excluded-point canary fails |
|
||||
| S1-12 | 8s trail/smoothing are session-local, bounded, gap-safe and reduced-motion aware | unit clock + browser resume/rate smoke; bridge gap/persist trail => path/storage assertion fails |
|
||||
| S1-13 | Live pointer transparency, themes, kiosk/touch, room/device actions and floor projection remain correct | ordinary View/golden/gesture smoke; overlay hitbox or unprojected layer => interaction/geometry fail |
|
||||
| S1-14 | Live source read ACL and setup may_write; no source/raw/history leak in read-only delivery | HA user matrix, support/export/storage canary; remove ACL/filter => canary leak fails |
|
||||
| S1-15 | Optional/future/unknown fields safe; exact refs covered by import/copy/Optimize/removal/scale change | backend lifecycle + config roundtrip; retarget/drop sibling/keep invalidcalibration => invariant fails |
|
||||
| S1-16 | New/old frontend/backend safe; unsupported commands absent; original vacuum/Glow unaffected | rolling-matrix smoke, existing vacuum/light unit/golden; API capability bypass => unsupported-call assertion fails |
|
||||
| S1-17 | Editor lazy, active load bounded and no idle animation/subscriptions when disabled | bundle graph + baseline/candidate perf + backend timer count; eager editor or runaway listener => budget fails |
|
||||
| S1-18 | Text, status announcements and safe desktop/touch exit meet §2/5/8 | i18n parity, keyboard/touch smoke, reviewed screenshots; ordinary expected-vs-actual witnesses |
|
||||
|
||||
Implementation artifacts: `test/radar-geometry.test.mjs`, `test/radar-model.test.mjs`,
|
||||
`tests_backend/test_ha_radar.py`, shared JSON coordinate/source fixtures,
|
||||
`demo/smoke_radar_setup.mjs`, `demo/smoke_radar_live.mjs`, full-scene golden
|
||||
fixtures. Register protective backend/browser mutations per PROCESS. Include
|
||||
axis-aligned motion with one suppressed axis, true stationary presence, delayed
|
||||
HA delivery, unexpected unknown coordinates, concave room, duplicate slots,
|
||||
disconnected client, scale change and private-source canaries — not only moving
|
||||
ideal points. Consented field CSV is supplementary, not a blocking dependency.
|
||||
|
||||
## 10. Performance, release and rollback
|
||||
|
||||
Baseline/candidate workload: 32 configured radars, eight slots each, four output
|
||||
frames/s, three View clients including 360px phone and wall tablet; repeat with
|
||||
all disabled and with only one selected setup stream. No full config/layout
|
||||
rebuild per source frame; listener count depends on unique sources, not clients.
|
||||
Pure normalization/projection callback p95 <=10ms; browser incremental overlay
|
||||
update p95 <=8ms on the project's standard perf runner; no introduced >50ms
|
||||
long task from radar work. Report sample count/hardware/variance with results,
|
||||
not a generic speed claim. Existing suite budgets and initial gzip256000B remain.
|
||||
No continuous RAF when output unchanged/hidden; reduced-motion interpolation off.
|
||||
|
||||
Implementation loop typecheck/unit/build; Linux CI full HA harness and required
|
||||
smoke/golden/performance precede beta. Release artifacts follow common §8:
|
||||
both changelogs, guide/setup/data-quality caveat, architecture, compatibility,
|
||||
field registry, reviewed full-scene screenshots light/dark/2D/isometry/mobile,
|
||||
permission and mutation evidence. Current docs-only work runs no implementation
|
||||
gates and claims no shipped changes.
|
||||
|
||||
Rollback: hide live to remove visuals; disable radar to detach sources; restore
|
||||
saved config if needed. No Stage1 external write or raw store to reverse. Older
|
||||
frontend editing is discouraged; unsupported fields remain inert. Risks include
|
||||
unknown source conventions, independent coordinate reports, noisy calibration,
|
||||
loss of fresh stationary coordinates and potentially misleading coverage.
|
||||
The explicit degraded states and conservative solver handle uncertainty without
|
||||
pretending the sensor provides data it does not have.
|
||||
|
||||
## 11. Sources and engineering assumptions
|
||||
|
||||
Primary constraints: [HA 2024.6.0 state model](https://github.com/home-assistant/core/blob/2024.6.0/homeassistant/core.py),
|
||||
[HA report event](https://developers.home-assistant.io/blog/2024/03/20/state_reported_timestamp/),
|
||||
[ESPHome LD2450](https://esphome.io/components/sensor/ld2450/),
|
||||
[LD24xx duplicate suppression](https://github.com/esphome/esphome/blob/2026.8.2/esphome/components/ld24xx/ld24xx.h),
|
||||
[ESPHome filter behaviour](https://github.com/esphome/esphome/blob/2026.8.2/esphome/components/sensor/filter.cpp),
|
||||
[LD2410 distance-only data](https://esphome.io/components/sensor/ld2410/).
|
||||
|
||||
Timing/solver tolerances, screen sizes, module names and bounded query/resource
|
||||
limits are explicit engineering choices for independent review, not extra
|
||||
owner questions. Atomic frames are not assumed for separate HA sensors. A
|
||||
backend reporting API is deliberately required for uniform normalization and
|
||||
future browser-independent recording. Exact shared coordinate fixtures, not
|
||||
historical ARCHITECTURE pixel dimensions, are the mathematical acceptance truth.
|
||||
@@ -0,0 +1,700 @@
|
||||
# #485 — Radar presence, stage 2: zones and bounded observations
|
||||
|
||||
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485). Companion:
|
||||
[stage 1](485-radar-presence-stage1.md). This is an implementation contract,
|
||||
not a report that the feature exists. The issue labels remain the sole status.
|
||||
Owner-approved UX defaults Q1–Q4 apply: configured live display is on, the
|
||||
allowed area is one selected room, collection is explicit and limited, and
|
||||
cross-radar merging belongs to stage 3. This document changes no product code.
|
||||
|
||||
## 1. Scenario, persona, before and after
|
||||
|
||||
The home administrator, in the desktop Device editor of the main House Plan
|
||||
panel or full dashboard card, has calibrated a radar and now asks: “Does the
|
||||
sofa zone actually cover the detections from my sofa?” Household members and
|
||||
kiosk users continue seeing the quiet current-presence layer.
|
||||
|
||||
Before: the administrator compares millimetres and a separate spreadsheet.
|
||||
After: they outline a zone on their plan, collect a limited observation sample,
|
||||
and see whether observations land inside it before explicitly changing a device.
|
||||
|
||||
This serves J1/J4/J6 in `docs/SCOPE.md`. The approved exception to its generic
|
||||
history exclusion is a local, bounded spatial-calibration sample; this is not
|
||||
a history dashboard or a general replacement for HA Recorder. Raw movement
|
||||
collection and device writes are separately opt-in operations.
|
||||
|
||||
## 2. Scope and non-scope
|
||||
|
||||
Included: local polygon zones; mapping exact existing occupancy/count entities
|
||||
to those zones (including FP2-style zone-only connections); supported LD2450
|
||||
device-zone read/edit/apply; bounded server recording; cloud inspection and
|
||||
sample fractions; manually drawn reflection filters with preview; permissions,
|
||||
revision conflicts, interruption, retention and rolling compatibility.
|
||||
|
||||
Excluded: continuous multi-day collection, heat maps, CSV import/export, cloud
|
||||
upload, support attachment of observations, personal identities, trajectory
|
||||
fusion, multi-room routing, automatic zone/mirror discovery, HA automation
|
||||
creation and derived HA occupancy/count entities. Stage 3 must specify these
|
||||
separately. No new editor tab, always-visible radar console or polling loop
|
||||
that runs only because a View client is open. No generic arbitrary HA service
|
||||
proxy and no firmware installation or entity-registry administration.
|
||||
|
||||
Stage 1 is a prerequisite: exact source bindings, normalized live frames,
|
||||
physical installation, calibration, room filtering and source-health semantics
|
||||
are consumed, not independently reimplemented. Device-zone support is a
|
||||
capability of a verified connection, never inferred from a brand name.
|
||||
|
||||
## 3. Existing integration seams and proposed files
|
||||
|
||||
The existing files below were inspected on the issue branch; `radar*` modules
|
||||
are proposed additions, not existing implementations.
|
||||
|
||||
| Surface | Existing authority | Stage-2 work |
|
||||
|---|---|---|
|
||||
| Configuration types | `src/types.ts` (`Marker`, `ServerConfig`) | Add optional zones/reflectors beneath the stage-1 radar object |
|
||||
| Configuration transactions | `src/config-store.ts`; `custom_components/houseplan/websocket_api.py` (`houseplan/config/set`) | Preserve `expected_rev`, server acceptance and multi-client invalidation |
|
||||
| Strict/change-aware validation | `custom_components/houseplan/validation.py` | Delegate new/changed radar fields to proposed `radar_validation.py`; retain unknown siblings |
|
||||
| Authorization | `custom_components/houseplan/auth.py:may_write` | Use the same policy for raw reads, recording and hardware writes |
|
||||
| Runtime ownership | `custom_components/houseplan/__init__.py`, `store.py:HouseplanData` | Wire setup/unload/reconciliation; separate recorder locks and storage |
|
||||
| Existing bounded-recorder precedent | `custom_components/houseplan/trails.py:TrailRecorder` | Reuse lifecycle lessons, not vacuum-run semantics or its unrestricted read endpoint |
|
||||
| Privacy boundary | `custom_components/houseplan/support_package.py`; existing export/import APIs | Exclude operational history and raw telemetry; preserve safe config transfer |
|
||||
| Presentation | `src/render-device-snapshot.ts`, `houseplan-render-lifecycle.ts`, `src/editors/` | Feed one selected-radar lazy editor; keep View free of history/editor dependencies |
|
||||
|
||||
Proposed additions: `src/radar-zones.ts` (pure geometry/membership),
|
||||
`src/radar-observations.ts` (typed client), `src/editors/radar-zones-section.ts`,
|
||||
`src/editors/radar-observations-section.ts`; backend `radar_zones.py`,
|
||||
`radar_recording.py`, `radar_recording_store.py`, `radar_validation.py` and
|
||||
`radar_websocket.py`. Stage-1 modules remain the normalized-frame authority.
|
||||
Names are technical choices, not separate scope. Do not alter `trails.py`'s
|
||||
vacuum behaviour to fit radar data.
|
||||
|
||||
## 4. UX contract
|
||||
|
||||
### 4.1 One selected source and two kinds of zone
|
||||
|
||||
Entry: Device editor → selected device → Presence on plan → Configure on plan.
|
||||
The same contextual surface gains **Zones** and **Observation recording**.
|
||||
Only the selected radar's draft, cloud, sector, filters and short live trail
|
||||
are visible here. Closing configuration returns to the same device; returning
|
||||
from another route starts in View. Temporary layers are session-local.
|
||||
|
||||
**House Plan zones**: Add zone → outline a rectangle or polygon → name →
|
||||
choose its state source → preview → Save. State source is either this radar's
|
||||
normalized coordinate targets or one exact existing occupancy/count entity.
|
||||
Zone-only sources show entity selection and polygon drawing, no coordinate
|
||||
calibration or point recording. FP2 mapping does not write to Aqara and is not
|
||||
labelled “synchronized”. Deleting the mapping never deletes the HA entity.
|
||||
|
||||
For a coordinate zone, a valid current target inside its polygon means occupied;
|
||||
zero eligible targets means “no targets detected” only if the source has a
|
||||
fresh frame with stage-1 `complete:true`. An incomplete frame may establish
|
||||
positive occupancy from its usable slots, but cannot establish absence from
|
||||
the remaining slots. Missing/stale coordinates are unknown, never clear. A
|
||||
mapped binary sensor uses exact `on`/`off`; mapped count uses a finite integer
|
||||
`>=0`. Unknown/unavailable/invalid values produce unknown. There is no fallback
|
||||
from a missing selected entity to a similarly named sensor. Overlapping zones
|
||||
may both be occupied; their counts are never summed as people.
|
||||
|
||||
**Device zones**: a separate section with connection capabilities, one mode
|
||||
selector above the list, and at most three LD2450 slots. Drawing starts in the
|
||||
radar's local axes. A sensor-axis rectangle appears rotated on a rotated plan;
|
||||
no arbitrary polygon is silently converted to a bounding rectangle. Handle
|
||||
preview, numeric fields, transmitted values and reloaded outline use the same
|
||||
unit conversion and quantization. An unsupported connection may display known
|
||||
geometry read-only; the Apply button is absent with an explanation.
|
||||
|
||||
The one LD2450 mode is **Unrestricted / Detect inside / Ignore inside**,
|
||||
mapping to `Disabled / Detection / Filter`. It applies to the whole set, not
|
||||
to each zone independently. Unrestricted retains coordinates for later use.
|
||||
An unused slot is four zeros; this sentinel is not an active zero-area zone.
|
||||
Every active slot must have `x1<x2` and `y1<y2`. Mode and all slot values are
|
||||
shown in the confirmation, including unchanged slots affected by a full-set
|
||||
device write. Clearing all slots requires Unrestricted; do not install an
|
||||
empty Detection set by accident.
|
||||
|
||||
Saving House Plan polygons/reflectors changes shared plan configuration only.
|
||||
Applying hardware zones is a separate command with a warning that the radar's
|
||||
detection and existing HA automations may change. Saving a name, moving the
|
||||
decorative device icon or importing config never sends device commands.
|
||||
|
||||
### 4.2 Reflection filters
|
||||
|
||||
Advanced → False reflections → Add reflecting surface. The user draws a
|
||||
segment, names it, and sees the opposite half-plane relative to the physical
|
||||
sensor mount highlighted. The complete line through the two endpoints is the
|
||||
filter boundary; the endpoints are handles defining that line, not a promise
|
||||
that the effect stops at a mirror's visible ends. Text explicitly states that
|
||||
real targets behind the line may also be hidden. The line is a House Plan
|
||||
filter, not furniture, a wall, or a claim about electromagnetic propagation.
|
||||
|
||||
The same canonical membership predicate drives live preview, cloud exclusions
|
||||
and saved filtering. Points strictly on the opposite side are excluded; points
|
||||
on the line within 0.1 cm are retained. A sensor on/within 1 cm of the line,
|
||||
or coincident endpoints, is invalid. Multiple enabled lines use OR exclusion.
|
||||
Room clipping remains the stage-1 single-room filter. Outside-room points are
|
||||
out of the chosen display area, not declared physically false. Missing room
|
||||
geometry never produces a guessed boundary.
|
||||
|
||||
Preview is reversible and has zero HA side effects. Apply saves only local
|
||||
filters; the original HA presence entity and upstream automations are unchanged.
|
||||
Raw observations are retained before filtering. “Show excluded points” uses
|
||||
grey crosses and reasons `outside_room` / `reflector:<id>`; it is off in View.
|
||||
|
||||
### 4.3 Observation recording and inspection
|
||||
|
||||
No recording starts from opening View, configuring/calibrating a source,
|
||||
opening this section or enabling live display. The form offers 5 minutes,
|
||||
15 minutes, 1 hour (default), 24 hours; it explains local storage, browser-close
|
||||
continuation, automatic stop and rolling 24-hour raw retention before Start.
|
||||
The server's response, not the click, starts the visible countdown.
|
||||
|
||||
The section shows the shared run state, start/end, retained sample count, source
|
||||
gaps, recording limits and Stop. Another authorized client sees the same run;
|
||||
opening it does not create another recorder. Hiding live overlays does not stop
|
||||
recording. Disabling the radar explicitly stops its run after confirmation;
|
||||
turning it on again never restarts recording.
|
||||
|
||||
After a run, or during it, choose one retained run/epoch and an interval. The
|
||||
cloud is a distinct historical layer, without interpolated lines; live targets
|
||||
stay visually distinct. Switching selected radar disposes the old cloud and
|
||||
request. Never superimpose history from another source generation by default.
|
||||
Clear recording confirms permanent deletion of **all retained observations
|
||||
for this selected radar**, stops its active run, and updates every client.
|
||||
It leaves calibration, local zones, reflectors and hardware untouched.
|
||||
|
||||
Zone score is “{inside} of {total} recorded points ({percent}%) fall in this
|
||||
zone”, with interval, excluded-points policy and any sampling/loss disclaimer.
|
||||
Default denominator includes every valid retained point in the selected epoch,
|
||||
including points outside the selected room and excluded by filters; the user
|
||||
may explicitly choose “included points only”. Both numerator and denominator
|
||||
then use that same predicate. Zero denominator shows “No usable observations”,
|
||||
not 0%. Overlapping zones score independently. This is neither time occupied,
|
||||
probability, accuracy nor a pass/fail judgement of zone correctness.
|
||||
|
||||
Desktop is the reference editor. Touch editor: best effort / intentionally
|
||||
degraded; complex geometry may recommend desktop, but Cancel/exit, permission
|
||||
guards and destructive confirmation always work safely. View and kiosk retain
|
||||
full touch support, normal pan/zoom and actions. No point/zone history overlay
|
||||
intercepts a device tap; kiosk exposes neither recording nor configuration.
|
||||
|
||||
## 5. Persisted models and compatibility
|
||||
|
||||
Stage 1 owns `marker.radar={version:1,enabled,show_live,profile,sources,mount,
|
||||
room_id,calibration}` and `settings.radar.show_live` (default true). Add optional
|
||||
`zones` and `reflectors`; absence is equivalent to empty and triggers no write.
|
||||
No global model/store-version bump and no read migration are required.
|
||||
|
||||
| Path | Contract |
|
||||
|---|---|
|
||||
| `radar.zones.local[]` | Maximum 32; `{id,name,poly,state}`; id 1–64 ASCII `[A-Za-z0-9_-]`, unique; name trimmed 1–80 characters |
|
||||
| `local[].poly` | 3–64 finite `{x,y}` points in the same canonical plan coordinate system as room polygons; simple polygon, no holes, nonzero area; no duplicate adjacent points or repeated closing point |
|
||||
| `local[].state` | `{kind:'targets'}` or `{kind:'occupancy',entity_id}` or `{kind:'count',entity_id}`; targets requires coordinate capability, others require exact `binary_sensor.*` / `sensor.*` references |
|
||||
| `radar.zones.hardware` | Optional `{adapter:'esphome_ld2450_numbers_v1',mode_entity,slots[]}`; contains bindings/labels, never an assumed applied configuration |
|
||||
| `hardware.slots[]` | Exactly three ordered entries `{slot:1\|2\|3,name,x1_entity,y1_entity,x2_entity,y2_entity}`; one complete descriptor is required for writes |
|
||||
| `hardware.mode_entity` | Exact `select.*`; required options are the adapter's literal three modes |
|
||||
| `hardware.*_entity` | Exact `number.*`, no templates, wildcard ids, service names or arbitrary attribute paths; globally distinct inside this binding set |
|
||||
| `radar.reflectors[]` | Maximum 8; `{id,name,a:{x,y},b:{x,y},enabled}`; same plan units, id/name bounds as zones, strict boolean |
|
||||
|
||||
Entity ids use the existing bounded entity-id validation (maximum 255). Physical
|
||||
conversion uses the stage-1 canonical scale and calibration; neither renderer
|
||||
nor writer invents a second scale. Geometry is finite, within the space's
|
||||
existing accepted coordinate envelope, and subject to the total 2 MiB config
|
||||
limit. New/changed malformed radar records are rejected atomically with
|
||||
`invalid_radar_zones`; untouched malformed/future records remain lossless and
|
||||
inert on unrelated writes, as with existing change-aware compatibility fields.
|
||||
Unknown sibling fields round-trip. Invalid known members never reach rendering
|
||||
or services. No recursive rounding of unknown future numeric fields.
|
||||
|
||||
Full export/import preserves configuration and exact entity ids; it never
|
||||
starts a recorder or applies device settings. Space transfer remaps the owner
|
||||
space/room with the existing reference seam and keeps local geometry with its
|
||||
owner; HA ids remain literal. If duplicate policy virtualizes a marker, remove
|
||||
the complete HA-dependent radar block and report one dropped radar binding,
|
||||
instead of leaving device-write capabilities on a virtual copy. Plan-only
|
||||
transfer contains no radar marker data. Removing a room preserves the radar
|
||||
config as needing repair, stops its recording, and does not select another room.
|
||||
|
||||
Runtime snapshots, operation ids, confirmation tokens, applied/draft numbers,
|
||||
recordings and telemetry do not enter ServerConfig, layout, portable exports,
|
||||
support packages, localStorage or frontend crash reports. Support projection
|
||||
may expose capability booleans and bounded counts only, no source ids, names,
|
||||
coordinates, run times, geometry, raw samples or device command payloads.
|
||||
|
||||
New frontend + old backend: absent `radar_stage2_api:1` hides stage-2 operations
|
||||
with an update explanation, preserves fields, and makes zero attempted stage-2
|
||||
commands. Old frontend + new backend: fields survive unknown-field handling;
|
||||
ordinary unrelated saves must not strip them. Older clients that reconstruct
|
||||
markers may erase unknown fields: downgrade is read-only recommended, not
|
||||
promised lossless editing. Raw storage is independently versioned and ignored
|
||||
by old backends. Register fields in `scripts/config-field-registry.mjs` and
|
||||
document this matrix in `docs/CONFIG-COMPATIBILITY.md` during implementation.
|
||||
|
||||
## 6. Device read, draft, conflict and write protocol
|
||||
|
||||
### 6.1 Capability and snapshot
|
||||
|
||||
`config/get` advertises exact `radar_stage2_api:1` after runtime setup. The
|
||||
server resolves all entity ids from saved config, validates device membership
|
||||
for the LD2450 adapter (one ESPHome device), current state availability, number
|
||||
units, min/max/step and the mode options. A manual coordinate profile remains
|
||||
eligible for local zones/recording but does not gain hardware-write capability.
|
||||
|
||||
LD2450 adapter accepts millimetre number entities and converts local cm ×10;
|
||||
it rejects missing/unsupported units. Effective numeric bounds are those of the
|
||||
actual entities, not constants copied from marketing. Documentation and the
|
||||
ESPHome 2026.8.2 number schema have differed, so numeric fields carry authoritative
|
||||
limits in the snapshot. Quantize to `min+k*step` nearest, ties away from zero;
|
||||
the resulting rectangle, including quantization changes, is the confirmed draft.
|
||||
Out-of-range points are rejected, never silently clamped.
|
||||
|
||||
A snapshot contains all three slots, global mode, exact binding fingerprint,
|
||||
`config_rev`, `source_generation`, time, `snapshot_hash`, and
|
||||
`read_proof:'ha_state'|'device_readback'`. HA cached equality and state changes
|
||||
after `number.set_value` are **not** device-readback proof. The default
|
||||
`esphome_ld2450_numbers_v1` adapter can offer `ha_state` only; it must advertise
|
||||
`verified_apply:false` and cannot emit a green device-confirmed result.
|
||||
This restriction does not stop an explicitly warned, unverified write.
|
||||
|
||||
### 6.2 Draft and explicit confirmation
|
||||
|
||||
Opening reads a baseline; edits stay browser-memory-only. Cancel, leaving the
|
||||
source, or closing the page asks to discard dirty geometry and sends no command.
|
||||
Reload loses an unsaved draft; accepted shared polygons are unaffected. If a
|
||||
configuration update changes this radar's binding/generation, disable Apply,
|
||||
retain the draft for inspection, and require a fresh baseline.
|
||||
|
||||
Prepare compares the complete current set against the baseline snapshot under
|
||||
a per-device operation lock. Any observable external change returns conflict
|
||||
with no write; the UI offers Reload device settings, not silent merge. Changes
|
||||
made elsewhere but not exposed by HA cannot be detected: the confirmation says
|
||||
so for `ha_state` proof, and never claims universal conflict detection.
|
||||
|
||||
The server generates a user-bound, single-use confirmation token valid for 60
|
||||
seconds, tied to marker, full quantized candidate, baseline hash, config rev,
|
||||
source generation and exact binding set. Maximum two outstanding tokens per
|
||||
user and 32 globally; evict oldest unused tokens, not active operations. Apply
|
||||
accepts this token only, not arbitrary entity ids, services or changed values.
|
||||
|
||||
### 6.3 Apply and outcomes
|
||||
|
||||
Immediately before the first call recheck permission, token, exact ownership,
|
||||
source generation, config rev and complete available baseline. Changed/unknown
|
||||
baseline is conflict/unavailable and makes zero calls. Per-device lock prevents
|
||||
simultaneous writes through two marker bindings to the same hardware. It does
|
||||
not lock ordinary unrelated config saves for the duration of UART traffic.
|
||||
Binding/generation mutation during an operation is refused as `radar_busy`;
|
||||
unrelated config changes do not cancel an already accepted operation.
|
||||
|
||||
Immediately before attempting the first service call, stop any active raw
|
||||
recording for this physical device as `configuration_changed` and invalidate
|
||||
its evaluation epoch. When Stage 3 exists, the same signal stops affected
|
||||
heatmap runs and invalidates fusion/count commissioning. Include this effect
|
||||
in Prepare/confirmation. Neither partial/unverified completion nor a retry
|
||||
automatically restarts collection or restores count eligibility; the user
|
||||
checks the resulting detection and explicitly starts/recommissions. Prepare,
|
||||
Cancel, or a refusal before an operation is accepted does not stop collection.
|
||||
This prevents intermediate hardware filters from silently entering an old
|
||||
observation selection. Independently observed hardware configuration changes
|
||||
raise the same invalidation; unreported physical changes remain unknowable.
|
||||
|
||||
For the numbers adapter, write all twelve coordinate numbers in slot order
|
||||
`x1,y1,x2,y2`, then the final mode, sequentially. Send the final mode even when
|
||||
unchanged so the last command carries the final complete set. Use only
|
||||
`number.set_value` / `select.select_option`, blocking calls with the requesting
|
||||
HA user's context and normal HA service authorization; `may_write` is not an
|
||||
elevation above HA entity control permissions. Timeout is 5 seconds per call,
|
||||
whole operation 75 seconds. The confirmation warns that separate writes can
|
||||
expose intermediate detection configurations to existing HA automations; there
|
||||
is no implicit temporary disable, retry loop or atomicity claim.
|
||||
|
||||
First error, revoked permission or timeout stops remaining calls. Never replay
|
||||
a timed-out call automatically. Return counts of accepted/failed/not-sent
|
||||
calls and the affected complete slot/mode set, without asserting which values
|
||||
the hardware applied. The UI retains the draft and baseline; Retry or Restore
|
||||
previous values requires a new read/prepare/confirmation. Restore is another
|
||||
bounded write with the same risks, not atomic rollback.
|
||||
|
||||
Outcomes: `confirmed` only with independent device readback matching the full
|
||||
candidate; `sent_unverified` when all calls were accepted but proof is absent;
|
||||
`partial_unverified` after some calls with no proven final state; `failed`
|
||||
before any accepted call; `partial_confirmed` only when independent complete
|
||||
readback proves a differing applied set. For the default numbers adapter only
|
||||
the unverified/failed outcomes are reachable. “Refresh” rereads HA and retains
|
||||
its proof grade; it cannot upgrade an optimistic snapshot to hardware proof.
|
||||
|
||||
Runtime operation records survive disconnect in memory for 10 minutes (maximum
|
||||
32 terminal records); repeated Apply using the same token returns the same
|
||||
operation instead of resending. HA restart loses these records: UI reports
|
||||
“Result unknown after restart” and requires a fresh read before another write.
|
||||
There is no startup resend or automatic restoration of hardware settings.
|
||||
|
||||
## 7. Recording model, retention and lifecycle
|
||||
|
||||
### 7.1 Runtime record
|
||||
|
||||
Proposed `RadarRecordingStore` owns separate version-1 HA storage keys beneath
|
||||
`houseplan.radar_recordings`; only supported storage APIs write them. A small
|
||||
manifest owns runs and minute-sized sample chunks. Client requests never choose
|
||||
paths. File I/O and bounded aggregation run off the HA event loop. Raw data is
|
||||
never placed in the config/layout Store, HA entities or HA Recorder.
|
||||
|
||||
Run header: `{run_id,marker_id,source_generation,started_at,ends_at,stopped_at?,
|
||||
status,stop_reason?,collection_policy,epochs[],retained_points,lost_samples}`.
|
||||
The private manifest additionally stores `initiator_user_id` for permission
|
||||
rechecks; query/status responses do not expose that identifier.
|
||||
Server UTC timestamps are authority; runtime duration/expiry deadlines use a
|
||||
monotonic clock so wall-clock corrections do not extend authorized collection.
|
||||
The source generation and normalized physical centimetre coordinates are taken
|
||||
from the stage-1 frame before room/reflector filtering. Every accepted sample
|
||||
records server sequence/time, source `server_session_id`/frame `seq`, slot key,
|
||||
local x/y, both coordinate-component report times and the inherited
|
||||
`pair_quality`. Polar samples retain their distance/bearing component report
|
||||
times after conversion. `bounded_latest` remains bounded HA-report pairing,
|
||||
never an atomic hardware frame or proof of a new physical measurement.
|
||||
Slot keys are not identities. No speed/trajectory/person inference is required.
|
||||
|
||||
An observation key is `(source_generation,server_session_id,slot,
|
||||
component_1_report_time,component_2_report_time)`. A new output `seq`, filter
|
||||
revision, browser event or elapsed second alone does not make a new observation.
|
||||
The recorder retains the last accepted key per configured slot and does not
|
||||
sample it twice, including when a frame is republished for another slot or for
|
||||
health changes. A verified coherent adapter uses its stage-1 sample key instead;
|
||||
the recorder cannot infer or upgrade that capability. Newly reported equal
|
||||
values are allowed only under stage-1 eligibility and remain labelled reports,
|
||||
not independently verified stationary measurements.
|
||||
|
||||
Epoch header stores `source_generation`, `calibration_revision`, projection
|
||||
snapshot, room/reflector predicate snapshots and filter/zone revision
|
||||
fingerprints. Its private storage also holds an immutable `source_entity_ids`
|
||||
set derived by the backend from the exact bindings contributing to that epoch's
|
||||
observations and evaluation: coordinate components, gates, availability/count
|
||||
inputs and any mapped-zone inputs used. This is not reconstructed from the
|
||||
marker's current bindings after reconfiguration. Exact references are private
|
||||
authorization metadata, not returned in run/epoch/query/status responses or
|
||||
portable/support exports, and expire with the epoch under the same raw/metadata
|
||||
TTL rules; they do not become a permanent source registry.
|
||||
Historical included-only scoring uses that epoch's predicate;
|
||||
explicit current recalculation uses the current predicate and labels both
|
||||
revisions. Zone scores always refer to the currently selected saved zone outline,
|
||||
whose revision is returned; no score is labelled as an old zone's statistic.
|
||||
Physical move, source/unit/axis
|
||||
change, owner-space change, new `installation_id` (including a reinstallation
|
||||
at the same coordinates) or deleted selected room stops the current run as
|
||||
`configuration_changed`; restart requires explicit Start. A calibration-only
|
||||
heading/mirror/reference correction closes the epoch and opens the next within
|
||||
the same bounded run and source generation, per the common identity contract.
|
||||
Filter/zone edits likewise delimit evaluation epochs without destroying raw
|
||||
samples. Historical default view uses its saved epoch projection. Explicit
|
||||
“Recalculate with current calibration” is permitted only within the same
|
||||
source generation and explains the changed projection; it never mixes epochs
|
||||
silently. A physically moved sensor cannot reuse the old generation.
|
||||
|
||||
Collection accepts only stage-1 eligible coordinate observations, checked at
|
||||
collection time with the same component ages, skew and source-generation rules.
|
||||
For `bounded_latest`, each component age is at most 3 seconds and report skew
|
||||
at most 1.5 seconds; storage does not renew this eligibility. Usable slots from
|
||||
an incomplete frame may be recorded with `complete:false` and explicit missing
|
||||
slot/gap metadata; missing slots never become zero observations. Coordinate-
|
||||
unknown, stale, unavailable, source-gap and interrupted intervals produce gaps/
|
||||
counters, not fabricated zeros, duplicated observation keys or interpolated
|
||||
points. A fresh `complete:true` zero-target frame records availability but no
|
||||
target samples; an empty incomplete frame is a gap, not absence. Occupancy-only,
|
||||
range-only and zone-only profiles cannot start point recording.
|
||||
|
||||
### 7.2 Hard bounds and durability
|
||||
|
||||
| Bound | Required value/policy |
|
||||
|---|---|
|
||||
| Duration | `duration_s ∈ {300,900,3600,86400}`, default 3600; max 86400 per explicit run; no extension/resume endpoint |
|
||||
| Concurrency | One active run per marker, maximum 8 across the integration; a duplicate Start returns `already_recording`, never extends the deadline |
|
||||
| Collection rate | At most one eligible frame per server-monotonic second per radar, all usable slots up to the stage-1 slot cap; retain the first frame with new eligible observation keys in each second, skip already accepted keys; no duplicate-counting on output/client events |
|
||||
| Samples | Maximum 700,000 retained target samples per marker, including all unexpired runs |
|
||||
| Metadata | Maximum 64 retained runs per marker, 128 epochs per run and 4 MiB manifest data per marker; further Start is refused, or a recording is stopped before opening an excess epoch; ordinary config edits still succeed |
|
||||
| Storage | Maximum 32 MiB encoded recording data per marker and 128 MiB total, including manifests and pending chunks; stop the affected run as `quota_reached`, never evict another user's unexpired observations |
|
||||
| Memory | At most one minute of unflushed samples per active run; a global 8 MiB buffer ceiling; overflow stops affected run with `storage_error` |
|
||||
| Raw TTL | 24 hours from each server sample time, not 24 hours from run completion; exact cutoff enforced on every read/score, plus deletion sweep at startup and at least once per minute |
|
||||
| Metadata TTL | Empty run headers and operation/sample metadata removed no later than 24 hours after stop; no permanent audit of movement/recording times |
|
||||
| Browser result | Page size ≤2,000 samples, response ≤512 KiB; opaque cursor ≤256 chars; cloud preview ≤10,000 points by deterministic reservoir selection |
|
||||
| Requests | Query/score ≤1 in flight per user/marker; start/prepare ≤10 per minute per user; operation payload ≤64 KiB; excess returns `rate_limited`/`too_large` |
|
||||
|
||||
The score uses all retained eligible samples, not the downsampled preview. UI
|
||||
states capture cadence and “preview sampled” separately; point fraction is not
|
||||
time fraction. Counts, numerator/denominator and preview derive from one query
|
||||
selection including the epoch and exclusion policy.
|
||||
|
||||
Persist Start metadata before acknowledging Start. Flush chunks no less often
|
||||
than every 60 seconds and on Stop/unload; an interrupted write is replaced
|
||||
atomically by the storage implementation, never appended as an unvalidated
|
||||
partial record. A crash can lose at most the acknowledged buffer interval,
|
||||
which is reported as an interruption/loss, not falsely reconstructed. Storage
|
||||
failure stops recording, preserves prior durable chunks and shows failure.
|
||||
|
||||
Browser close/network loss does not stop a server-authorized run. HA restart,
|
||||
integration unload/reload, deleted/tombstoned marker, disabled radar or revoked
|
||||
initiator permission ends collection; on startup retained runs marked recording
|
||||
become `interrupted`, are swept for TTL, and are never silently resumed. Check
|
||||
the initiator's existence, current `may_write` and HA read permission for every
|
||||
exact source entity in the active epoch at least once per minute and before
|
||||
accepting a new chunk. Revocation stops collection, discards the not-yet-accepted
|
||||
pending chunk and records `permission_changed`; it does not disclose restricted
|
||||
data through status. Opening a new epoch performs the same source-set checks
|
||||
before accepting observations. Config changes reconcile by stable marker id under a
|
||||
recorder lock; the teardown-closed flag is checked after awaited loads so a
|
||||
late refresh cannot resubscribe after unload.
|
||||
|
||||
Clear takes the recorder lock, invalidates the active run generation, detaches
|
||||
collection and cancels pending saves before deleting chunks and metadata. Do
|
||||
not acknowledge until the deletion is durable; a write failure returns an
|
||||
error and never says “cleared”. Delayed saves cannot recreate cleared chunks.
|
||||
Deleting a marker explicitly cascades its recordings (stated in deletion
|
||||
confirmation); merely hiding/moving its decorative icon does not delete them.
|
||||
|
||||
TTL deletion is the disclosed consent of Start, not orphan-inference file GC.
|
||||
Expired data remains inaccessible if disk deletion fails; retry and report a
|
||||
bounded storage incident without logging coordinates. HA backups or copies
|
||||
made by the administrator are outside the component's deletion guarantee; the
|
||||
recording warning links this limitation. Downgrade/disabled integration cannot
|
||||
run the sweep: before downgrade, Stop and Clear with the current version.
|
||||
|
||||
## 8. Commands, errors and authorization
|
||||
|
||||
All commands are authenticated WebSocket commands in proposed
|
||||
`radar_websocket.py`; no public/signed content URL for raw samples. Every command
|
||||
below, including status/read/score, requires current `may_write(hass,user)` plus
|
||||
HA read permission for every exact source entity contributing to its result.
|
||||
For recording Start and the active run this is the backend-derived epoch source
|
||||
set. Historical query/score uses the selected epoch's saved source set, not only
|
||||
the current marker bindings; a current-projection score additionally checks
|
||||
any current inputs it uses. Status checks the source sets of all retained epochs
|
||||
whose metadata would be returned. If any required permission is denied or
|
||||
cannot be resolved, reject the complete response as `source_restricted`; do not
|
||||
return private run existence, counts, times or a partial cloud. Stop/Clear still
|
||||
require current `may_write` but may delete/stop without source read access; their
|
||||
minimal acknowledgement reveals no retained source values or run metadata.
|
||||
When `admin_only=false`, this intentionally grants the same rights to all
|
||||
signed-in editors; the settings explanation must say recording visibility
|
||||
follows this existing policy and the underlying entity read permissions. Do not
|
||||
substitute a hard-coded `is_admin` check. Device-zone read/prepare/operation
|
||||
results likewise check the exact hardware binding set; Apply additionally
|
||||
retains ordinary HA entity control authorization. No global unfiltered source
|
||||
snapshot is made available merely because `may_write` is true.
|
||||
|
||||
| Command after `houseplan/radar/` | Request | Result |
|
||||
|---|---|---|
|
||||
| `zones/read` | `marker_id` | Complete capability/snapshot/proof record from §6 |
|
||||
| `zones/prepare` | `marker_id,baseline_hash,expected_config_rev,source_generation,candidate` | Quantized full candidate, warning/proof grade, confirmation token/expiry; no service calls |
|
||||
| `zones/apply` | `token` | `operation_id,status`; progress/result fetched by operation id |
|
||||
| `zones/operation` | `operation_id` | Bounded outcome and call statuses, without unapproved retargeting |
|
||||
| `recording/start` | `marker_id,duration_s,expected_config_rev,source_generation` | Persisted run header; exact original end time |
|
||||
| `recording/status` | `marker_id` | Current state and retained run/epoch headers, no unbounded sample payload |
|
||||
| `recording/stop` | `marker_id,run_id` | Idempotent stopped header after durable flush; after source-read revocation, minimal stopped acknowledgement without private metadata; does not clear observations |
|
||||
| `recording/clear` | `marker_id,expected_recording_rev,confirm:true` | Durable deletion result + new recording revision |
|
||||
| `recording/query` | `marker_id,run_id,epoch_id,from,to,cursor?,limit?` | Bounded samples/preview, effective range, counts, gaps, truncation, recording revision |
|
||||
| `recording/score` | Same selection + `zone_ids[],included_only,projection:'recorded'\|'current'` | Numerator/denominator/fraction per zone, calibration/filter revision, no new persistent analytics |
|
||||
|
||||
`from/to` are server UTC milliseconds in the retained run, ordered, at most
|
||||
24 hours apart; maximum 32 requested zone ids. Query cursors bind user, marker,
|
||||
run, epoch, selection and recording revision. Clear/expiry invalidates them
|
||||
with `stale_cursor`, not a silently changed page. Query/score reread both
|
||||
`may_write` and the applicable exact-source permissions before delivery; no
|
||||
shared unfiltered cross-user cache. Progress subscriptions
|
||||
are scoped to authorized connections; permission loss closes them and clears
|
||||
the client buffer. Broad HA bus events contain only invalidation signals,
|
||||
never sample coordinates, source references or history metadata.
|
||||
|
||||
Stable domain errors: `unauthorized`, `not_ready`, `unsupported_capability`,
|
||||
`source_unavailable`, `source_restricted`, `permission_changed`,
|
||||
`invalid_radar_zones`, `invalid_selection`, `conflict`,
|
||||
`radar_busy`, `invalid_token`, `token_expired`, `already_recording`,
|
||||
`quota_reached`, `rate_limited`, `too_large`, `stale_cursor`, `storage_error`,
|
||||
`operation_unknown`. Error text is localized from codes; backend exception
|
||||
strings and arbitrary entity attribute contents are never displayed or logged
|
||||
as raw data. Rejected commands leave config, recording deadline and hardware
|
||||
unchanged unless the returned outcome explicitly records an already-started
|
||||
partial hardware operation.
|
||||
|
||||
## 9. i18n contract (English + Russian)
|
||||
|
||||
Use the existing i18n registry, lazy editor graph and HA duration/number
|
||||
formatters. Keys below live under `radar.` in en/ru; other languages retain the
|
||||
existing fallback policy. Dynamic names are text, never HTML. Existing shared
|
||||
Save/Cancel/Retry/Close and common error keys are reused, not duplicated.
|
||||
|
||||
| Key | English | Русский |
|
||||
|---|---|---|
|
||||
| `zones.title` | Zones | Зоны |
|
||||
| `zones.local` | House Plan zones | Зоны в House Plan |
|
||||
| `zones.device` | Device zones | Зоны датчика |
|
||||
| `zones.add` | Add zone | Добавить зону |
|
||||
| `zones.state_source` | Zone state source | Источник состояния зоны |
|
||||
| `zones.targets` | This radar's targets | Цели этого радара |
|
||||
| `zones.occupancy` | Presence sensor | Датчик присутствия |
|
||||
| `zones.count` | Target count sensor | Счётчик целей |
|
||||
| `zones.map_only` | This outline is stored only in House Plan | Этот контур хранится только в House Plan |
|
||||
| `zones.unrestricted` | Unrestricted | Не ограничивать |
|
||||
| `zones.detect_inside` | Detect inside | Обнаруживать внутри |
|
||||
| `zones.ignore_inside` | Ignore inside | Игнорировать внутри |
|
||||
| `zones.apply` | Apply to device | Применить к датчику |
|
||||
| `zones.apply_warning` | This changes device detection and may affect existing automations. Separate commands can temporarily apply intermediate values. | Изменится обнаружение датчика; это может повлиять на автоматизации. Отдельные команды могут временно применить промежуточные значения. |
|
||||
| `zones.collection_stop` | Applying stops active observations/heatmap collection for this device and requires checking combined counts again | Применение остановит активный сбор наблюдений/тепловой карты этого датчика; объединённый подсчёт потребуется проверить заново |
|
||||
| `zones.external_change` | Device settings changed. Reload before applying. | Настройки датчика изменились. Перечитайте их перед применением. |
|
||||
| `zones.cached_warning` | Only Home Assistant values are available. Device application and unreported external changes cannot be verified. | Доступны только значения Home Assistant. Нельзя проверить применение датчиком и неотражённые внешние изменения. |
|
||||
| `zones.confirmed` | Application confirmed by device | Применение подтверждено устройством |
|
||||
| `zones.unverified` | Commands sent; device application is unconfirmed | Команды отправлены; применение устройством не подтверждено |
|
||||
| `zones.partial_unverified` | Some commands were sent; the device result is unknown | Отправлена часть команд; результат на устройстве неизвестен |
|
||||
| `zones.partial_confirmed` | Device readback confirms a different applied set | Датчик подтвердил применение отличающегося набора |
|
||||
| `zones.restore` | Restore previous values | Восстановить предыдущие значения |
|
||||
| `zones.unknown_after_restart` | Result unknown after restart. Read settings again. | После перезапуска результат неизвестен. Перечитайте настройки. |
|
||||
| `recording.title` | Observation recording | Запись наблюдений |
|
||||
| `recording.duration` | Collect for | Собирать точки |
|
||||
| `recording.start` | Start recording | Начать запись |
|
||||
| `recording.stop` | Stop | Остановить |
|
||||
| `recording.clear` | Clear recording | Очистить запись |
|
||||
| `recording.clear_confirm` | Stop recording and permanently delete all retained observations for this radar on every client? Zones and calibration stay unchanged. | Остановить запись и навсегда удалить все сохранённые наблюдения этого радара у всех клиентов? Зоны и калибровка останутся. |
|
||||
| `recording.privacy` | Stored locally. Continues after closing this page. Each point expires after 24 hours; administrator backups are outside this deletion guarantee. | Хранится локально. Запись продолжится после закрытия страницы. Каждая точка удаляется через 24 часа; гарантия удаления не относится к резервным копиям администратора. |
|
||||
| `recording.editors_only` | Recording requires House Plan editing and source read permissions | Для доступа к записи нужны права редактирования House Plan и чтения источников |
|
||||
| `recording.ends_at` | Recording ends: {time} | Запись закончится: {time} |
|
||||
| `recording.interrupted` | Recording interrupted; start a new run to continue | Запись прервана; для продолжения запустите новую |
|
||||
| `recording.gap` | No valid coordinate data during this interval | В этом интервале нет достоверных координат |
|
||||
| `recording.no_points` | No usable observations | Нет пригодных наблюдений |
|
||||
| `recording.score` | {inside} of {total} recorded points ({percent}%) fall in this zone | {inside} из {total} записанных точек ({percent}%) попали в эту зону |
|
||||
| `recording.not_time` | This is a fraction of samples, not occupied time | Это доля отсчётов, а не время занятости |
|
||||
| `recording.included_only` | Included points only | Только учитываемые точки |
|
||||
| `recording.sampled` | Preview sampled; score uses the full retained selection | Превью прорежено; оценка использует всю сохранённую выборку |
|
||||
| `recording.cadence` | Up to one eligible reported frame per second | Не более одного пригодного кадра по сообщениям источника в секунду |
|
||||
| `recording.quota` | Recording stopped at the storage limit | Запись остановлена по лимиту хранения |
|
||||
| `recording.storage_error` | Recording stopped because storage failed | Запись остановлена из-за ошибки хранения |
|
||||
| `recording.epoch` | Installation/calibration period | Период установки/калибровки |
|
||||
| `recording.recalculate` | Recalculate with current calibration | Пересчитать с текущей калибровкой |
|
||||
| `recording.configuration_changed` | Configuration changed; start a new recording | Настройка изменилась; начните новую запись |
|
||||
| `reflectors.title` | False reflections | Ложные отражения |
|
||||
| `reflectors.add` | Add reflecting surface | Указать отражающую поверхность |
|
||||
| `reflectors.warning` | Hides the opposite side of the entire line, possibly including real targets. Does not change the original Home Assistant sensor. | Скрывает противоположную сторону всей линии, включая возможные настоящие цели. Исходный датчик Home Assistant не изменяется. |
|
||||
| `reflectors.excluded` | Show excluded points | Показать исключённые точки |
|
||||
| `reflectors.invalid` | Move the line away from the sensor and give it two distinct endpoints | Отодвиньте линию от датчика и задайте разные конечные точки |
|
||||
| `errors.unsupported_capability` | This connection does not support this operation | Это подключение не поддерживает операцию |
|
||||
| `errors.update_backend` | Update the House Plan integration to use zones and recordings | Обновите интеграцию House Plan для зон и записи |
|
||||
| `errors.permission_changed` | Permissions changed; recording access is closed | Права доступа изменились; доступ к записи закрыт |
|
||||
| `errors.source_restricted` | No access to all sources used by this observation period | Нет доступа ко всем источникам этого периода наблюдений |
|
||||
| `errors.invalid_geometry` | Check the zone outline and device limits | Проверьте контур зоны и пределы датчика |
|
||||
| `errors.busy` | This radar is already being updated | Этот радар уже обновляется |
|
||||
|
||||
## 10. Acceptance criteria and negative witnesses
|
||||
|
||||
Each `S2-*` criterion is mandatory. Test names/files below are planned artifacts,
|
||||
not claims that tests already ran. A protective criterion needs an explicit
|
||||
negative probe/mutation and its failing output in code review; expensive
|
||||
backend/browser guards receive a registered mutation-gate witness.
|
||||
|
||||
| AC | Observable contract | Proof | What makes the protection fail visibly |
|
||||
|---|---|---|---|
|
||||
| S2-1 | Only selected-radar configuration exposes zones/cloud; View/kiosk remain quiet and pointer-transparent | `smoke_radar_zones.mjs`; golden en/ru light/dark; existing kiosk smoke | Render history or hit areas in View: tap/pan assertions fail |
|
||||
| S2-2 | Polygon CRUD is revisioned, cancellation has no effect, unknown siblings survive reload/import | unit `radar-zones`; backend config roundtrip; smoke two clients | Remove expected revision check or reconstruct known keys only: stale writer/unknown sentinel tests fail |
|
||||
| S2-3 | FP2-style occupancy/count mapping uses exact entity; coordinate-zone clear requires complete:true; unknown never means clear, no fabricated coordinates | unit state table including one usable outside-zone slot plus one unknown slot; backend exact-ref fixture; smoke missing selected entity | Enable name fallback, cast unknown to zero or treat an incomplete frame as clear: wrong-state and zero-service-call assertions fail |
|
||||
| S2-4 | LD2450 uses max three sensor-axis rectangles and one mode; preview equals transmitted quantized values | shared TS/Python geometry fixtures; device-write harness | Add fourth slot, mixed per-zone modes or plan-axis bounding box: strict rejection/projection assertions fail |
|
||||
| S2-5 | Invalid domains, ownership, units, limits, degenerate geometry and empty Detection sets cause zero service calls | `test_ha_radar_zones.py` table | Remove each guard; invalid candidate reaches captured HA service spy |
|
||||
| S2-6 | Draft preparation is non-mutating; stale baseline/config/token refuses before writing | backend concurrency/token tests; two-client smoke | Accept consumed/other-user/expired token or changed binding: forbidden service-count assertion fails |
|
||||
| S2-7 | Default numbers adapter never calls optimistic equality device confirmation | backend optimistic-state fake; smoke unverified label | Treat HA state echo as device proof: expected amber/unverified outcome fails |
|
||||
| S2-8 | Partial send/timeout stops further calls; retry/restore requires new confirmation; no automatic restart replay | injected nth-call failures and restart; smoke recovery | Retry timed-out call or continue after failed call: service trace differs; false green label fails |
|
||||
| S2-9 | Recording starts explicitly, duration options/default/caps exact; duplicate start preserves deadline | backend fake-clock tests; smoke Start/Stop | Auto-start on subscription, accept 86401 s or extend duplicate deadline: start-count/deadline assertions fail |
|
||||
| S2-10 | Browser close leaves run collecting; HA restart interrupts without resume; Stop durably flushes | `test_ha_radar_recording.py`; disconnect/restart harness | Tie recording to WS lifetime or resume at startup: expected sample count/state fails |
|
||||
| S2-11 | Stage-1 pair quality/ages and unique observation keys survive recording; incomplete slots create explicit gaps, legitimate axis zero survives, no interpolation | shared normalized-frame fixture including bounded_latest, reused output seq and partial slots; backend fake clock | Upgrade quality, renew report age, count a repeated pair under a new seq, fill missing slots or drop `x=0,y>0`: exact samples/quality/gaps differ |
|
||||
| S2-12 | Raw TTL is per sample; expired samples inaccessible after clock jump/startup, disk failure does not expose them | backend fake clock + storage failure | Remove read-time cutoff while sweep fails: expired canary returns and test fails |
|
||||
| S2-13 | Concurrent runs/bytes/samples/buffers/query sizes are bounded without fresh-data eviction | backend quota/concurrency tables | Remove each limit or count only flushed bytes: 9th recorder/oversize request is accepted and test fails |
|
||||
| S2-14 | Raw/status/score require may_write plus exact historical epoch-source ACL; collection rechecks initiator write/read rights; HA writes retain control authorization; Stop/Clear can safely remove data after source-read revocation | HA users with admin_only true/false, rebound marker with restricted old source, revoked source/read/write access before chunk, minimal Stop/Clear response | Check only current bindings, omit metadata/source/chunk ACL, expose private source ids or bypass service authorization: historical canary/metadata/service spy appears |
|
||||
| S2-15 | Clear stops and durably erases selected radar across clients; delayed callbacks cannot resurrect data | backend racing clear/save/reload; two-client smoke | Permit late flush after generation invalidation: cleared canary reappears |
|
||||
| S2-16 | Different source generations never mix; changed calibration is explicit epoch/reprojection | backend epoch fixtures; cloud smoke | Join epochs or apply new mount to old generation: known geometry/selection assertion fails |
|
||||
| S2-17 | Score uses full retained selection with matching numerator/denominator, not preview or elapsed time | unit/backend 100-point fixture: 31 inside; empty, overlaps, exclusions | Use preview size/time weights or filter one side only: exact 31/100 and empty-state assertions fail |
|
||||
| S2-18 | Reflection/room preview and saved filter agree; no upstream sensor write or history deletion | shared geometry fixture; smoke Apply/Cancel; service spy | Flip side, treat segment as finite without label, or call service: point set/zero-call assertions fail |
|
||||
| S2-19 | Exports/support/logs/cache carry no recording canary; imports never start/write hardware | backend export/support test; browser storage/network inspection | Add raw data to config/event/support/localStorage: canary scanner fails |
|
||||
| S2-20 | Missing stage-2 capability, malformed/future fields and downgrade remain inert without data erasure | rolling client/backend matrix; unit preservation tests | Call unsupported command or drop future siblings on unrelated save: trace/roundtrip differs |
|
||||
| S2-21 | Desktop polygon/line gestures cancel safely; touch pinch/cancel sends no geometry or device writes; View still works | targeted desktop and touch safety smoke | Turn cancelled pointer into commit: config rev/service trace unexpectedly changes |
|
||||
| S2-22 | Collection/preview stay within §7 budgets and do not load the editor graph in View | backend load test, selected-radar browser perf, bundle budget | Unbounded response or editor import in initial graph exceeds cap; baseline/candidate artifact exposes regression |
|
||||
|
||||
## 11. Test execution and performance/security evidence
|
||||
|
||||
Implement shared fixtures for local centimetres → calibrated plan coordinates,
|
||||
rectangle quantization, polygon edges, exclusion line side and generations.
|
||||
Include real-data-shaped ghosts, off-room valid targets, count-only zones and
|
||||
deliberately malformed snapshots. A contributed CSV may become a test fixture
|
||||
only after contributor consent and removal of identifying metadata; synthetic
|
||||
fixtures are sufficient for mandatory automated contracts.
|
||||
|
||||
Planned commands: `npm run typecheck`, `npm test`, `npm run build`,
|
||||
`npm run bundle:sync`, `npm run bundle:budget`,
|
||||
`node demo/smoke_radar_zones.mjs`, `node demo/smoke_radar_recording.mjs`,
|
||||
`node scripts/check-docs.mjs`, and `python -m pytest tests_backend -q` in the
|
||||
supported Linux/WSL HA harness. Native Windows pure tests do not prove the HA
|
||||
authorization/storage harness. Register the new protective backend/smoke
|
||||
witnesses in `scripts/mutation-gate.mjs` during implementation.
|
||||
|
||||
Capture a baseline/candidate artifact using eight synthetic active recorders,
|
||||
one 10,000-point selected cloud, and a normal large-house View client. UI
|
||||
updates cloud at most once per second; no full config rebuild per sample, no
|
||||
per-point SVG nodes and no continuous polling in View. Storage/score work must
|
||||
yield or run in an executor; HA event-loop callback p95 budget is 10 ms for
|
||||
accepted frames. Query/score cancels stale client work; at most one request
|
||||
per selected source. Report actual elapsed times, encoded bytes, buffer peaks,
|
||||
and frame/callback counts, not a generic “fast”. Initial View dependency graph
|
||||
must remain within the existing 256,000-byte gzip budget. Existing selected
|
||||
render/interaction/performance gates still apply to touched modules.
|
||||
|
||||
Security artifact: table of authenticated editor/read-only/disabled-integration
|
||||
and revoked users, each command exercised, raw canary leakage checks, service
|
||||
authorization outcomes, quotas and negative witnesses. No public screenshot
|
||||
or release artifact contains a real home's raw cloud without separate consent.
|
||||
|
||||
## 12. Risks, rollback and release artifacts
|
||||
|
||||
Risks: optimistic HA numbers cannot confirm UART persistence; intermediate
|
||||
device states can affect automation; room/reflection filters can hide real
|
||||
targets; biased/throttled sampling cannot measure occupied time; source
|
||||
reconfiguration can invalidate old geometry; rights changes and late callbacks
|
||||
can leak/revive raw data; big recordings can block I/O or exhaust storage.
|
||||
The explicit proof grades, epoch boundaries, server guards and caps above are
|
||||
required mitigations, not optional improvements.
|
||||
|
||||
Operational rollback: Stop recording, Clear observations with the current
|
||||
version, disable local reflectors/zone display or set `radar.enabled:false`.
|
||||
This does not revert device settings: use explicit Restore previous values
|
||||
with fresh confirmation, or the vendor's own editor. A code rollback preserves
|
||||
optional config fields and makes unsupported operations inert; recommend no
|
||||
marker editing with older frontends. There is no migration that silently
|
||||
deletes user configuration and no automatic hardware rollback. Merely disabling
|
||||
`settings.radar.show_live` hides output but is not a privacy Stop control.
|
||||
|
||||
Implementation release must include both `docs/CHANGELOG.md` and
|
||||
`docs/CHANGELOG.ru.md`, issue links, updated `docs/USER-GUIDE.ru.md` (desktop
|
||||
recommendation, permissions, hardware warnings, retention/backups), English
|
||||
user-facing documentation, `docs/ARCHITECTURE.md`, `docs/CONFIG-COMPATIBILITY.md`,
|
||||
field registry and `docs/DEVELOPMENT.md` for HA harness/device proof caveats.
|
||||
Golden/screenshots: selected local/device zones, rotated LD2450 rectangle,
|
||||
FP2 mapping, cloud/score, partial/unverified outcome, ordinary unchanged
|
||||
View/kiosk in light/dark en/ru. Capture/accept only through the repository's
|
||||
Linux CI/WSL policy and reviewed complete artifacts, never by accepting a
|
||||
partial baseline merely to make a gate green. Include the performance/security
|
||||
artifacts from §11. Update status/version/release notes only for the actually
|
||||
delivered stage, never claim heat maps/fusion shipped with this stage.
|
||||
|
||||
Exact-SHA Validate and required heavy gates precede a beta/RC; stable is later
|
||||
promotion-only. This specification commit needs no product build, new tests,
|
||||
screenshots, changelog claiming shipped behaviour, or product version bump.
|
||||
Reaching S5 authorizes future development through the process; it does not
|
||||
start implementation under the current user's documentation-only request.
|
||||
|
||||
## 13. Technical assumptions — accepted provisionally, change freely in review
|
||||
|
||||
The owner has settled visible defaults; no unanswered product choice remains
|
||||
here. Module names, WS suffixes, HA Store shard format, resource caps, token
|
||||
lifetime, quantization tie rule and test filenames are concrete engineering
|
||||
choices for review, not owner questions. They may change with equivalent
|
||||
boundedness, privacy, UX and acceptance witnesses. Runtime storage must remain
|
||||
separate from portable configuration regardless of implementation format.
|
||||
|
||||
Primary constraints checked for this task: [ESPHome LD2450](https://esphome.io/components/sensor/ld2450/),
|
||||
[number bounds in ESPHome 2026.8.2](https://github.com/esphome/esphome/blob/2026.8.2/esphome/components/ld2450/number/__init__.py),
|
||||
[optimistic number publication](https://github.com/esphome/esphome/blob/2026.8.2/esphome/components/ld2450/number/zone_coordinate_number.cpp),
|
||||
[Aqara FP2 connection capabilities](https://www.aqara.com/en/product/presence-sensor-fp2/).
|
||||
The skill Home Assistant Best Practices influenced the use of exact entity
|
||||
references, native authorization/storage boundaries and zero implicit HA
|
||||
automation/entity administration; it does not add a separate product dependency.
|
||||
@@ -0,0 +1,799 @@
|
||||
# #485 — Radar presence, stage 3: coverage and use of room results
|
||||
|
||||
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485).
|
||||
Prerequisites: [stage 1](485-radar-presence-stage1.md) and
|
||||
[stage 2](485-radar-presence-stage2.md). This is a proposed implementation
|
||||
contract, not a claim of implementation or an independent review. Issue labels
|
||||
remain authoritative. Read the [common contract](485-radar-presence.md) first.
|
||||
Owner defaults Q1–Q4 are accepted; §8 specifies the bounded engineering
|
||||
interpretation of the separately requested weekly heatmap, not permanent
|
||||
collection permission attributed to Q3. Current instruction stops at S5-ready.
|
||||
|
||||
## 1. Scenario, persona, before and after
|
||||
|
||||
The home administrator opens the existing Device editor on a desktop after
|
||||
calibrating individual radars. They want to find gaps between rooms, remove
|
||||
duplicate observations in overlaps and deliberately expose a useful room result
|
||||
to HA. Household members and guests continue using the quiet touch-first View.
|
||||
|
||||
Before: two radar marks can be mistaken for two people, and each integration's
|
||||
numbers must be interpreted separately. After: the administrator checks the
|
||||
selected coverage and sources on the plan; household members see a conservative
|
||||
current result, with uncertainty shown instead of an invented count.
|
||||
|
||||
J1/J4/J6 in `docs/SCOPE.md` are the relevant jobs. This is the issue-specific
|
||||
spatial-presence exception, not a general analytics dashboard, device manager
|
||||
or automation builder. The full track is required: new UX and public WS/native
|
||||
HA contracts, persisted optional fields, several surfaces and performance and
|
||||
privacy effects all fail the `small` criteria.
|
||||
|
||||
## 2. Scope, non-scope and delivery boundaries
|
||||
|
||||
Included:
|
||||
|
||||
- analysis of selected calibrated radars and explicitly hypothetical sectors;
|
||||
- an explicit per-radar allowed-room list, extending the single-room default;
|
||||
- conservative association of overlapping Cartesian observations and short
|
||||
room handoff; no personal identity or persistent tracking;
|
||||
- explicitly created integration-owned room `binary_sensor` and optional
|
||||
estimated-count `sensor` entities, independent of browser lifetime;
|
||||
- optional day/week spatial aggregate heatmap with §8's separate opt-in;
|
||||
- assisted reflection-line proposal from explicit correspondences, with manual
|
||||
acceptance through stage 2's existing filter preview/save contract.
|
||||
|
||||
Excluded: RF propagation or purchase recommendations, walls/material attenuation
|
||||
simulation, floor/3D tracking, general statistics, automatic hardware changes,
|
||||
HA automation/scene/script creation, management of source entities, person IDs,
|
||||
cross-day trajectories, raw retention longer than 24 hours, automatic continuous
|
||||
multi-day recording, CSV transfer, cloud upload, history in default View and
|
||||
silently inferred room/zone/reflector geometry.
|
||||
|
||||
Stage 3 does not weaken stage 1/2 source freshness, exclusions, raw permissions,
|
||||
single-source diagnostics or device-command confirmation. An unavailable
|
||||
stage-3 capability leaves stage 1/2 working. Coverage, fusion and derived
|
||||
entities do not require heatmap collection or access to saved observations.
|
||||
|
||||
## 3. Inspected seams and proposed implementation modules
|
||||
|
||||
These existing authorities were read on `issue/485-radar-presence`; names in
|
||||
the proposed column are additions, not assertions that implementations exist.
|
||||
|
||||
| Existing seam | Stage-3 responsibility / proposed additions |
|
||||
|---|---|
|
||||
| `src/types.ts`, `src/config-store.ts` | Optional radar room-list/settings types and lossless revisioned writes |
|
||||
| `src/houseplan-editor-runtime.ts`, `src/hp-dialog.ts`, `src/hp-confirm.ts` | Existing editor/dialog ownership; lazy `src/radar-analysis.ts` and `src/radar-analysis-view.ts` |
|
||||
| `src/houseplan-card.ts`, `src/render-device-snapshot.ts` | Consume backend result without association calculations in the render loop |
|
||||
| `custom_components/houseplan/websocket_api.py` (`async_register`, `_check_write`, `_runtime`) | Register bounded authenticated analysis/lifecycle commands |
|
||||
| `custom_components/houseplan/auth.py` (`may_write`) | Existing write policy; no separate radar ACL |
|
||||
| `custom_components/houseplan/store.py` (`HouseplanData`, `create_data`) | Separate versioned operational/aggregate stores and conflict protection |
|
||||
| `custom_components/houseplan/__init__.py` (`async_setup_entry`) | Set up/unload authoritative coordinator and new native entity platforms |
|
||||
| `custom_components/houseplan/validation.py`, `import_export.py`, `projection.py`, `support_package.py`, `diagnostics.py` | Delta validation, transfer policy, privacy projections |
|
||||
| `custom_components/houseplan/virtual_lights.py`, `trails.py` | Existing operational-lifecycle precedents only; do not reuse vacuum records for people |
|
||||
| New backend modules | `radar_fusion.py`, `radar_analysis.py`, `radar_heatmap.py`, `radar_room_outputs.py`, `binary_sensor.py`, `sensor.py` |
|
||||
| `src/i18n/en.json`, `src/i18n/ru.json`; backend `strings.json`, `translations/en.json`, `translations/ru.json` | Keys listed in §12, translated native names/errors |
|
||||
|
||||
Currently this integration initializes storage and a trail recorder; it does
|
||||
not already expose the proposed room entity platforms. Their config-entry
|
||||
platform lifecycle is new work. Do not implement derived entities by writing
|
||||
arbitrary state through REST, editing `.storage` directly, generating templates
|
||||
or creating YAML automations. Native entity values are memory-backed and
|
||||
subscription-driven; stable unique IDs register them normally. See
|
||||
[HA entity contract](https://developers.home-assistant.io/docs/core/entity/) and
|
||||
[HA entity registry](https://developers.home-assistant.io/docs/entity_registry_index/).
|
||||
|
||||
## 4. UX and permissions
|
||||
|
||||
Entry: **Device editor → Presence on plan → Additional tools → Coverage and
|
||||
room results**. The primary House Plan panel and full dashboard card share it.
|
||||
No fourth editor, permanent console, automatic dialog on upgrade or extra View
|
||||
toolbar. The compact Room View renderer is not extended by this stage. The
|
||||
selected workspace replaces the device properties with a large
|
||||
plan and one compact contextual panel; Back returns to the same device.
|
||||
|
||||
1. **Coverage:** select actual sources; only those sources' sectors and their
|
||||
physical installation anchors appear. **Add trial sector** creates a dashed,
|
||||
explicitly hypothetical in-session shape, not a Marker, HA entity or device.
|
||||
2. **Allowed rooms:** a named multiselect, initially the stage-1 room. Save
|
||||
previews newly included/excluded observed points and affected room outputs.
|
||||
3. **Combine observations:** select a group, inspect separate/associated marks,
|
||||
run the short commissioning check and explicitly enable its result. Details
|
||||
explain ambiguity without exposing tuning coefficients to the ordinary user.
|
||||
4. **Create room sensors:** choose rooms and exact sources, preview names,
|
||||
signal types, uncertainty and external retention warning, then confirm.
|
||||
5. **Heatmap:** a separate opt-in collection action and day/week selector; never
|
||||
implied by opening analysis, viewing coverage or starting a stage-2 raw run.
|
||||
6. **Reflection suggestion:** explicitly choose pairs, inspect a proposed line,
|
||||
then use the existing stage-2 excluded-side preview and Save. Nothing applies
|
||||
while merely selecting pairs or asking for a suggestion.
|
||||
|
||||
View remains current-only. Association changes marks, not source-marker tap
|
||||
actions. The full card/panel's room information may show **Presence detected**, **No presence detected**,
|
||||
**Estimated people: 2**, or **Estimate unavailable**; it never says the home is
|
||||
definitively empty. No global house count is introduced. When association is
|
||||
ambiguous, keep independent source marks and say **Observations cannot be
|
||||
combined reliably** in details, not a confident sum. Optional counts always
|
||||
retain the word **Estimated**; source slots are not named people.
|
||||
|
||||
All analysis, raw witnesses, aggregate history, settings and start/stop/clear
|
||||
commands require `may_write` on the server. Ordinary authenticated users retain
|
||||
only the stage-1 safe live projection and current room summaries. Existing HA
|
||||
permission policy controls access to explicitly created native entities; House
|
||||
Plan does not promise its private-history ACL can hide HA entity state.
|
||||
Source reads and preview/create validation also enforce the calling user's HA
|
||||
entity-read permissions; `may_write` alone cannot authorize another entity.
|
||||
Persisted derived-output authorization does not relax source access controls.
|
||||
|
||||
Native entity creation/removal additionally requires HA admin permission because
|
||||
it changes integration-owned registry lifecycle. A House Plan editor who is not
|
||||
an HA admin may prepare the plan result but sees a clear **Ask an HA
|
||||
administrator to create entities** message. No inferred grant from client UI.
|
||||
|
||||
Touch editor: **best effort / intentionally degraded** for precise geometry and
|
||||
pair selection, with desktop recommendation. View/kiosk remain fully supported:
|
||||
44 px effective actions, no hover dependency, no hit interception by marks,
|
||||
room/device actions and pan/pinch preserved. At 360 px the analysis controls and
|
||||
room information stack vertically. Escape/Back and canceled/multi-touch
|
||||
gestures cannot accept settings, create entities or start collection.
|
||||
|
||||
## 5. Coverage and allowed-room contract
|
||||
|
||||
Geometry uses the existing canonical plan/centimetre conversion and physical
|
||||
`radar.mount`, never the decorative marker layout. Coverage is the union of
|
||||
selected geometric sectors clipped to the union of explicitly allowed room
|
||||
polygons. The label is always **Indicative geometric coverage — not a guarantee
|
||||
of detection**. Wall/furniture geometry is context only: no ray-through-wall
|
||||
claim, RF shadow, attenuation or confidence score.
|
||||
|
||||
- Use verified angle/range only when actually provided; otherwise require an
|
||||
explicit administrator estimate labeled **Manually estimated**. Missing
|
||||
parameters mean **Coverage unknown**, not a filled default sector.
|
||||
- Trial sectors have an explicit manual origin/direction/range/angle. At most
|
||||
8 per analysis session, discarded on exit/reload; no purchase or connection
|
||||
implication. Actual and trial coverage totals are separate and never combined
|
||||
under the word **Installed**.
|
||||
- Optional uncovered area is the geometric difference inside the selected
|
||||
valid polygons, in the same measured units as the plan. It says nothing about
|
||||
detection probability. Exclude rooms without valid contours from the numeric
|
||||
denominator and list them as unknown; never invent a raster-room boundary.
|
||||
- `marker.radar.allowed_room_ids` absent means the stage-1 `room_id` singleton;
|
||||
an explicit empty array means no rooms, not inheritance. Maximum 32 unique
|
||||
existing room IDs, all in the radar's space. Known polygon boundaries are
|
||||
authoritative. Unknown/missing room IDs survive unrelated writes but are
|
||||
inert and visibly require repair; they are not retargeted by name or HA Area.
|
||||
- With the list absent, preserve the complete stage-1 single-room behaviour:
|
||||
an existing room without a contour still allows calibrated unclipped live
|
||||
observations with its warning; a missing room reference suspends output.
|
||||
Stage 3 does not turn the former into the latter. For an explicit multi-room
|
||||
list, a contourless room contributes no invented boundary or assignment:
|
||||
coverage and point-derived room attribution/count remain unknown where the
|
||||
required real polygons are absent. Exact room-mapped binary evidence remains
|
||||
usable under its own contract; it does not invent coordinate membership.
|
||||
- At a point within 10 cm of more than one allowed room boundary, retain its
|
||||
visible observation but mark room assignment uncertain. Do not count it in
|
||||
two rooms. This boundary band is measured in centimetres, not screen pixels.
|
||||
- Room-only/zone-only presence is attributed only to an explicit exact room
|
||||
mapping. A single broad occupancy signal allowed across several rooms does
|
||||
not turn every room on; room attribution is unknown. Range-only data likewise
|
||||
cannot become Cartesian points or room counts.
|
||||
|
||||
Editing allowed rooms invalidates the affected fusion/output revision and any
|
||||
active analysis preview. It does not change source HA states or hardware zones.
|
||||
Any newly allowed room requires explicit Save; room split/merge, Area changes,
|
||||
imports or discovery never expand the list silently.
|
||||
|
||||
## 6. Conservative association, count and room handoff
|
||||
|
||||
### 6.1 Input and bounds
|
||||
|
||||
One backend coordinator consumes the normalized stage-1 frame: source identity,
|
||||
`source_generation`, `calibration_revision`, `server_session_id`, `seq`, report
|
||||
times, `pair_quality`, availability and raw/projected centimetre targets. Apply stage-2 room/reflector exclusions
|
||||
through the same authoritative projection before fusion. A frontend must not
|
||||
merge another time, retain stale coordinates or supply its own accepted frame.
|
||||
|
||||
At most 4 enabled groups per installation, 2–8 radars per group and at most 8
|
||||
slots per radar under the common 32-radar ceiling. A radar belongs to one enabled
|
||||
group only. Group sources must share one space and calibrated metric frame;
|
||||
cross-floor groups are invalid. Only valid Cartesian observations participate;
|
||||
range, raw count, occupancy and zone-only sources are not point substitutes.
|
||||
|
||||
Evaluate at most 4 Hz. Preserve the source's `pair_quality`; geometric agreement
|
||||
does not upgrade `bounded_latest` to `coherent`. There are two explicit branches:
|
||||
|
||||
- **Coherent:** only a verified adapter-provided sample sequence/time establishes
|
||||
atomic pairs. Use shared-time normalized sample skew ≤250 ms when the adapter
|
||||
can prove comparable clocks, candidate distance ≤60 cm and at least 3 distinct
|
||||
accepted sample witnesses from each participating radar spanning ≥500 ms.
|
||||
Coherent local pairs without comparable cross-source clocks use the bounded
|
||||
estimate branch below; do not invent clock synchronization.
|
||||
- **Bounded-latest estimate:** the normal independent-HA-entity path, including
|
||||
`esphome_ld2450_v1`, is eligible for conservative estimated association after
|
||||
commissioning. Every axis retains Stage 1's ≤3 s report age and ≤1.5 s
|
||||
within-pair report skew. Compare the report interval from oldest to newest
|
||||
axis; intervals must overlap or be separated by ≤250 ms, and their latest
|
||||
report times must differ by ≤1.5 s. Candidate distance is ≤40 cm, with at
|
||||
least 3 distinct accepted pair witnesses from each participating radar
|
||||
spanning ≥2 s and mutually unique candidates throughout. These are report-
|
||||
time estimates, not atomic measurements or a physical-freshness claim.
|
||||
|
||||
A distinct witness requires a new source report/sample tuple and frame sequence
|
||||
in the same server session/epoch; reevaluating one cached frame three times is
|
||||
one witness. For independent X/Y the tuple includes both axis report times.
|
||||
Health-only frames and reordered/duplicate delivery cannot add witnesses. No
|
||||
branch extends Stage 1's leases or synthesizes an unchanged axis: stationary
|
||||
suppressed reports, expired axes, inadequate new samples or temporal ambiguity
|
||||
retain separate valid observations and an unknown affected count. Mixed-quality
|
||||
groups use the bounded estimate branch and expose that quality in details.
|
||||
|
||||
Build a 60 cm spatial grid per selected group and examine local neighbours.
|
||||
At most 8 candidate neighbours per observation. Exceeding that cap marks the
|
||||
local component ambiguous; do not truncate candidates and choose whichever
|
||||
arrived first. No all-installation Cartesian pair matrix or unbounded assignment
|
||||
search. Compute at most 64 active observations/group, at most 512 directed
|
||||
candidate edges/group/evaluation.
|
||||
|
||||
### 6.2 Association rule
|
||||
|
||||
Merge only mutually unique candidates from different radars after the temporal
|
||||
witness above. Each resulting component contains at most one observation per
|
||||
source, and every pair of its members must satisfy the candidate rule: A–B and
|
||||
B–C never imply A–C without evidence. Use the deterministic median projected
|
||||
position, keeping provenance internally; neither nearest-source tie-breaking
|
||||
nor a slot number is evidence of identity.
|
||||
|
||||
Near-equal alternatives, two close real people, crossing paths, duplicated slots
|
||||
from one radar, excessive report/sample skew, missing required report metadata, saturation, stale frame
|
||||
or source/config epoch change immediately invalidate the affected association.
|
||||
Fallback is separate valid observations and unknown affected count. Never
|
||||
silently choose the lower count, the higher count or a fractional person.
|
||||
|
||||
Commissioning is explicit per group: one-person traversal of each intended
|
||||
overlap followed by two simultaneously detected separated targets. It combines
|
||||
an explicit user's attestation about this physical exercise with backend proof
|
||||
that the required distinct frames, source epochs, pair quality, uniqueness and
|
||||
separated-target observations actually occurred. A client boolean alone cannot
|
||||
commission a group, and backend samples cannot prove the user's physical claim.
|
||||
|
||||
The commissioning inspect API in §11 starts a user-requested ephemeral capture
|
||||
for one selected saved group, at most 8 radars and 30 seconds per single-target
|
||||
or separated-target phase. At most one capture/user and four globally. Each
|
||||
phase requires the branch-specific distinct-frame proof for every participant;
|
||||
the separated phase must show two simultaneous targets at least 120 cm apart,
|
||||
without merging them. Every intended overlap must have a witnessed comparison;
|
||||
otherwise return an incomplete proof and keep count ineligible. Allow explicit
|
||||
repeat of the missing phase; never silently continue a capture beyond 30 s.
|
||||
Keep only bounded proof summaries after a phase, not a retained path; summaries
|
||||
expire after 10 minutes, at most two/group and four groups/user. Detailed
|
||||
in-memory capture is discarded on completion/cancel, disconnect or epoch change.
|
||||
It starts neither a stage-2 raw run nor heatmap collection.
|
||||
|
||||
Accept takes the user-bound proof token and explicit attestation, rechecks
|
||||
source read/write permissions and config revision, and computes the accepted
|
||||
fingerprint from server-owned group/source/calibration/filter/room epochs and
|
||||
algorithm version. Save a server-issued receipt and summary in config through
|
||||
the existing revisioned config transaction, not coordinates or the person's
|
||||
path. Generic config/set cannot mint/alter that receipt; unchanged server-issued
|
||||
metadata round-trips, and changed/imported group metadata loses eligibility.
|
||||
Any changed fingerprint input suspends count until a repeated check. Passing
|
||||
commissioning permits only the corresponding coherent/bounded estimate branch,
|
||||
never promotes source quality or certifies future identity; live guards remain.
|
||||
|
||||
### 6.3 Room state and count
|
||||
|
||||
Presence is three-valued per room:
|
||||
|
||||
| Evidence | Current room presence |
|
||||
|---|---|
|
||||
| Any valid positive Cartesian target strictly assigned to this room, or exact room-mapped positive occupancy/zone source | `on`, even if another selected source is unavailable; detail says partial if appropriate |
|
||||
| Every configured evidence source provides an explicit negative under its own Stage-1 health contract | `off` = no presence detected, not proof of emptiness |
|
||||
| No valid positive, and any required source is stale, missing, partial, unmappable or unable to assert a negative | `unknown`; never convert missing to `off` |
|
||||
|
||||
Binary occupancy/zone inputs use the current HA on/off/unknown/unavailable and
|
||||
bound-availability semantics from Stage 1; they do not receive a 3-second report
|
||||
lease or a requirement for recurring state transitions. Coordinate `complete`
|
||||
and report leases apply only to coordinate evidence/count. Thus a long-unchanged
|
||||
but available exact room-mapped binary off can be valid negative evidence,
|
||||
while an expired coordinate slot cannot. Raw zone counts remain diagnostic
|
||||
inputs, not permission to infer a person count or fabricate complete geometry.
|
||||
|
||||
Estimated count is an integer only when every configured count-contributing
|
||||
source is eligible, complete and fresh, commissioning matches the epochs, no
|
||||
unresolved candidate/boundary/handoff exists, and no source is at its maximum
|
||||
reported target capacity. Then count the independent/confirmed-associated
|
||||
observations assigned strictly to that room. Zero requires the same complete
|
||||
negative evidence. Unsupported or uncertain count is `null`, not zero, and is
|
||||
never inferred from occupancy or a distance. Count and presence have separate
|
||||
availability: presence can validly be on while count is unknown.
|
||||
|
||||
### 6.4 Handoff without identity
|
||||
|
||||
A short-lived association token is an implementation detail, never a user or
|
||||
person ID. It exists only in backend memory, expires after 2 seconds without
|
||||
valid support and is reset at restart, gap or epoch change; it is never stored
|
||||
in config, raw history, aggregates, HA attributes or exported diagnostics.
|
||||
|
||||
Across adjacent allowed room polygons in the same space, a unique continuation
|
||||
may keep the visual mark continuous only when the gap is ≤1 second, speed is
|
||||
≤250 cm/s and the crossing passes a known door/passage or zero-thickness shared
|
||||
boundary. No through-solid-wall or cross-floor handoff. A nonexistent opening
|
||||
or unknown room topology means separate observations, not inferred movement.
|
||||
Room count becomes unknown during a boundary-band or ambiguous handoff; do not
|
||||
add one to both rooms or animate a line over a gap. A valid fresh positive
|
||||
already inside the destination may establish its presence independently. No
|
||||
long path joins and no promise that a reappearing mark is the same person.
|
||||
|
||||
## 7. Optional native Home Assistant room outputs
|
||||
|
||||
### 7.1 Explicit lifecycle
|
||||
|
||||
**Create room sensors** previews each exact room, sources, proposed friendly
|
||||
names, occupancy/estimated-count selection and these warnings: outputs are
|
||||
derived/estimated, may become unknown, HA Recorder and external consumers may
|
||||
retain their states independently, and no automations are created or changed.
|
||||
The estimated-count checkbox is available only for an eligible commissioned
|
||||
Cartesian group. Occupancy-only creation remains useful independently.
|
||||
|
||||
Use integration-owned `BinarySensorEntity` with presence device class and
|
||||
`SensorEntity` with integer native value, no invented device class, no
|
||||
`total_increasing`/measurement statistics contract and no personal attributes.
|
||||
They are push-updated only on semantic result changes, at most 1 state write/s
|
||||
per entity. Unchanging coordinates do not force HA writes. Names use native
|
||||
translation keys and the room label as a placeholder, not hardcoded English.
|
||||
|
||||
Assign a random persistent output UUID at confirmed creation. Unique IDs are
|
||||
`houseplan_<config_entry_id>_radar_room_<output_uuid>_presence` and `_count`;
|
||||
they never contain a mutable room name, source entity ID or array position.
|
||||
Registry-chosen `entity_id` is returned after creation rather than promised
|
||||
from a suggested slug. HA-owned user customizations are preserved.
|
||||
|
||||
- Creation requires an unexpired user-bound preview plus current config/output
|
||||
revisions, server revalidation and HA admin + `may_write`. At most 32 room
|
||||
definitions / 64 native entities. Repeating the same operation ID is
|
||||
idempotent; conflicting content with the same operation ID is rejected.
|
||||
- Persist desired integration-owned outputs before registry reconciliation;
|
||||
report `active`, `pending` or `failed`, not atomic success for a partial
|
||||
platform error. Reconciliation only retries already explicitly authorized
|
||||
definitions. Reconnect/retry/restart cannot create duplicate unique IDs.
|
||||
- HA restart/unload starts values unknown/unavailable until current source
|
||||
evidence arrives. Never restore last night's count as a current state. Normal
|
||||
setup recreates runtime entity objects for authorized registry entries, not
|
||||
new entities. Unload removes subscriptions/tasks; disabling a native entity
|
||||
in HA is respected and House Plan never re-enables it.
|
||||
- Changing a room name, decorative icon or source order preserves output UUID,
|
||||
native entity ID and HA custom name. Changing input sources or room geometry
|
||||
preserves identity but invalidates proof and temporarily suspends count.
|
||||
- Deleting a source/room or losing a binding suspends the affected output and
|
||||
shows Repair; do not retarget it or delete its HA registry entry implicitly.
|
||||
Generic config writes/imports cannot grant or recreate output authorization.
|
||||
- **Remove created entities** is a separate confirmed preview with exact owned
|
||||
entity IDs and warning about dependent automations/dashboards. Known
|
||||
references are shown when inspectable, never claimed exhaustive. Only the
|
||||
entries matching this integration, config entry and recorded UUID are
|
||||
removable. Source entities and unrelated same-name entities are untouchable.
|
||||
Persist tombstones first; on partial failure retain a retryable pending
|
||||
removal. A restart cannot resurrect a tombstoned definition.
|
||||
- Turning off live display does not stop outputs. **Disable room processing**
|
||||
stops computation and makes outputs unavailable, retaining identities. To
|
||||
delete entities the administrator must use explicit Remove. Removing the
|
||||
integration uses the normal HA config-entry lifecycle, never a custom sweep
|
||||
of the user's registry or automations.
|
||||
|
||||
### 7.2 Unknown versus unavailable
|
||||
|
||||
Connected and functioning computation with insufficient/ambiguous evidence:
|
||||
binary value `None` or sensor native value `None` with `available=True`, shown
|
||||
by HA as `unknown`. Integration disabled/unloaded, coordinator failure or no
|
||||
reachable configured input at all: `available=False`, hence `unavailable`.
|
||||
Partial input loss with no positive gives unknown; a valid positive can keep
|
||||
presence on, but cannot rescue an ineligible count. Keep only a short bounded
|
||||
reason-code attribute; no coordinates, history, source slot IDs or timestamps
|
||||
updated every frame. The native state must match the backend room snapshot.
|
||||
|
||||
## 8. Optional heatmap: separate opt-in and bounded retention
|
||||
|
||||
The issue explicitly requests local opt-in day/week heatmaps. Retaining coarse
|
||||
aggregate cells for at most seven days is the bounded engineering interpretation
|
||||
of that weekly mode. It is not an assertion that Q3 authorized seven-day raw
|
||||
history or continuous collection: Q3's stage-2 raw TTL ≤24 hours and run ≤24
|
||||
hours remain unchanged. Coverage, fusion, reflection assistance and room outputs
|
||||
do not depend on heatmap collection.
|
||||
|
||||
Default: **off**, editor-only (`may_write`) manual opt-in for each bounded
|
||||
session; default 1 hour, choices 5/15/60/1440 minutes, no run over 24 hours, no
|
||||
automatic repetition, no resumed run after HA restart. Closing a browser does
|
||||
not stop a consented server run. Starting a new session always requires the
|
||||
visible retention notice and explicit action, even if an earlier session exists.
|
||||
|
||||
The notice states: **Only aggregate cells, stored locally for up to 7 days. No
|
||||
personal IDs or paths. Collection continues with this page closed until {end}.**
|
||||
Provide current end time, Stop and separately confirmed **Delete heatmap data**.
|
||||
Turning the display layer off does not stop collection; that difference is
|
||||
stated beside Stop. Revoking an initiating user's current write privilege or
|
||||
disabling the integration interrupts that run; it does not grant another
|
||||
client ownership or auto-resume later.
|
||||
|
||||
Minimal data contract:
|
||||
|
||||
- Consume accepted current projected observations directly. A heatmap session
|
||||
does **not** start or extend a stage-2 raw recording. Raw points, if separately
|
||||
requested under stage 2, remain on their ≤24-hour TTL and separate controls.
|
||||
No expired raw data is kept to permit later reaggregation.
|
||||
- Grid: fixed 50 cm square cells anchored in canonical centimetre space; sparse
|
||||
HA-local calendar-day buckets with explicit UTC start/end and timezone.
|
||||
Persist only cell presence-observation seconds, collection
|
||||
seconds, source-coverage completeness seconds, bucket date and a non-personal
|
||||
geometry/source/filter epoch fingerprint. No target/track IDs, per-observation
|
||||
timestamps, slot IDs or ordered paths. Multiple valid marks in one cell in
|
||||
one sampling second increment presence by one, not by the number of marks.
|
||||
- At most one sample per second; cap credit for any interval at 1 second.
|
||||
Missing/invalid data contributes to missing coverage, never a continuation
|
||||
of the last point. A cell outside known selected geometric coverage has no
|
||||
denominator. Value is **Seconds with an observation / seconds with usable
|
||||
coverage**, not percentage of time a named person spent there.
|
||||
- UTC boundaries distinguish 23/25-hour local days without duplicate buckets;
|
||||
day/week UI uses the saved HA timezone and explains actual included dates.
|
||||
A timezone change closes the active session and starts no replacement; old
|
||||
buckets remain under their recorded timezone. Buckets are deleted at age 7×24 hours based
|
||||
on bucket start (conservative early expiry, never late). Sweep before every
|
||||
read and on startup plus hourly; expired data is immediately unqueryable.
|
||||
- Maximum 4 concurrent aggregate sessions, 32 radars total, 20,000 occupied
|
||||
cell/bucket records per space and 32 MiB physical aggregate store total.
|
||||
Bound admission before allocation. At quota stop affected collection with a
|
||||
visible reason; do not silently evict nonexpired records or reduce resolution.
|
||||
- Same cell contributions from selected overlapping sources are unioned per
|
||||
second, not summed. A failed fusion does not justify a people heatmap: keep
|
||||
the observation-based label. Calibration/source-generation/room/filter changes
|
||||
close the current epoch; old epochs are separately selectable and never
|
||||
silently reprojected or stitched. Require a new explicit session after such
|
||||
a change. A changed plan scale cannot move historical bins without raw data.
|
||||
- A week aggregates only actually consented sessions. Show recorded hours,
|
||||
unavailable hours, calendar span and **Incomplete coverage**; no smoothing
|
||||
across gaps, false seven-day completeness or conversion of calibration
|
||||
samples into an automatically collected week. Restart truncates only the
|
||||
active partial aggregation interval; completed saved buckets remain.
|
||||
- Raw/aggregate stores are excluded from House Plan full/space/plan-only export,
|
||||
support bundles, logs, diagnostics, telemetry and native HA attributes. No
|
||||
cloud transport. Aggregate read APIs require `may_write`, as do raw APIs.
|
||||
- Clear is revisioned, bounded and installation-wide for the explicitly selected
|
||||
scope; cancel does nothing. Its confirmation explicitly says that matching
|
||||
active aggregate sessions will stop. Persist that stop and a clear tombstone
|
||||
in the same operation before deleting buckets. It deletes the matching aggregate buckets only,
|
||||
not raw samples, calibration, zones, fusion or native entities. Stop is not
|
||||
Clear. A tombstone prevents late flush/reconnect from recreating cleared data.
|
||||
|
||||
House Plan TTL is not a claim about copies in operator-managed HA filesystem
|
||||
backups or independent HA Recorder history of created entities. This limitation
|
||||
must appear in the privacy help. House Plan backup/export never embeds these
|
||||
stores; document how to exclude operational data from backup integrations that
|
||||
offer such a hook, and never advertise deletion of external backup copies.
|
||||
|
||||
## 9. Reflection-plane assistance: proposal, never automatic filtering
|
||||
|
||||
This extends stage 2's manual reflector workflow, not firmware processing.
|
||||
In one selected radar/epoch, an administrator marks at least 3 correspondence
|
||||
pairs: a believed direct observation and its believed reflected observation.
|
||||
Pairs must come from at least 3 spatial clusters separated by ≥75 cm, not three
|
||||
samples of one stationary pair. At most 20 pairs; at least one pair is reserved
|
||||
as a hold-out witness and does not fit the line. Thus acceptance requires at
|
||||
least 4 pairs when exactly 3 fitting clusters are used.
|
||||
|
||||
For the 2D mirror-line hypothesis, each pair's midpoint must lie on the proposed
|
||||
line and its connecting vector be perpendicular. Fit the unit-normal line to
|
||||
the fitting pairs; require reflected-point residual ≤20 cm for every fit and
|
||||
hold-out pair, no degenerate <30 cm pair separation, and a common line direction
|
||||
within 10°. Show residual and pair count, not a made-up confidence percentage.
|
||||
The hypothesis is unsupported if these conditions do not hold. A successful
|
||||
fit is still not proof of a physical mirror or that a removed point is false.
|
||||
|
||||
An optional **Find candidate pairs** action may offer mutually time-matched
|
||||
pairs (≤250 ms) from the selected retained stage-2 sample, with the same witness
|
||||
rules. It is user-invoked, editor-only and bounded to 2,000 sampled observations,
|
||||
20 proposed pairs, 100 tested line hypotheses and 2 seconds of work; on limit
|
||||
return **Insufficient evidence** rather than taking the best weak candidate.
|
||||
Never initiate recording or extend raw TTL. The administrator confirms each
|
||||
correspondence; candidates are not applied automatically.
|
||||
|
||||
Multiple real people, symmetric furniture, moving objects and multipath can
|
||||
produce false positives. The result screen says so and shows both kept and
|
||||
excluded sample points with the stage-2 side-of-line warning. **Use this line**
|
||||
creates an unsaved stage-2 reflector draft; the separate stage-2 Save, current
|
||||
config revision and exclusion preview remain mandatory. Cancel, expiry,
|
||||
insufficient witnesses or changed epoch changes nothing. No unattended
|
||||
background mirror search, hidden filter or HA source-state mutation.
|
||||
|
||||
## 10. Persisted model, operational state and compatibility
|
||||
|
||||
The stage-1 marker object stays `radar:{version:1,enabled,show_live,profile,
|
||||
sources,mount,room_id,calibration}`; stage 2 adds `zones`/`reflectors`. Stage 3
|
||||
adds optional `allowed_room_ids` without renaming or repurposing those keys.
|
||||
No personal tracking key is added to Marker or layout.
|
||||
|
||||
Proposed additive global settings object:
|
||||
|
||||
```text
|
||||
settings.radar = {
|
||||
version?: 1,
|
||||
show_live: true, // existing common display preference, preserved unchanged
|
||||
fusion_groups: [{id, enabled, space_id, marker_ids, commissioning}],
|
||||
room_outputs: [{id, space_id, room_id, marker_ids, fusion_group_id?, presence, estimated_count}]
|
||||
}
|
||||
commissioning = {receipt_id, attested:true, algorithm_version, pair_quality,
|
||||
checked_source_epochs, checked_config_fingerprint, distinct_frame_proof_summary}
|
||||
```
|
||||
|
||||
Only newly created fusion-group and output IDs are immutable nonempty UUIDs;
|
||||
existing space/room/marker references retain their exact current identifiers,
|
||||
not a new UUID syntax requirement. An output's room must exist in its explicit
|
||||
space, and all its selected markers/groups must belong to that same space.
|
||||
Array ordering is presentation only. Optional `settings.radar.version` is an
|
||||
additive stage-3 extension: absence remains valid, preserves the stage-1
|
||||
`show_live` behaviour and needs no load migration. A future unsupported extension
|
||||
version makes only its stage-3 fields inert, not the independent stage-1 display
|
||||
preference. The backend computes commissioning epochs/fingerprint/proof and
|
||||
issues the receipt through §11; accepting arbitrary client-written proof is
|
||||
forbidden. `attested` records the user's explicit exercise confirmation, not
|
||||
sensor proof that a known number of real people were present.
|
||||
`room_outputs` are definitions, **not authority to create HA entities**. A
|
||||
separate versioned `houseplan.radar_outputs` Store retains explicitly granted
|
||||
local output UUIDs, relevant config fingerprints, lifecycle revisions and
|
||||
create/remove tombstones. Generic config edits/imports cannot manufacture that
|
||||
grant. Do not persist live results as current truth across restart.
|
||||
|
||||
Separate `houseplan.radar_heatmap` storage holds the opt-in run metadata,
|
||||
coarse aggregates, revision and clear tombstones, with the bounds in §8. Raw
|
||||
history remains solely the stage-2 bounded store. Analysis trial sectors,
|
||||
pair-selection witnesses and fusion tokens are session/runtime memory only;
|
||||
no `settings` autosave per frame.
|
||||
|
||||
Register every new persisted path in `scripts/config-field-registry.mjs` with
|
||||
absent/off defaults, consumer, delta-validation and transfer rule. Reads are
|
||||
lossless and side-effect free. New/changed known values validate strictly and
|
||||
atomically with `expected_rev`; untouched unknown/future fields survive
|
||||
unrelated writes. Unsupported version is inert with a capability explanation,
|
||||
not coerced to version 1. Do not bump the plan geometry model or rewrite old
|
||||
files just to add optional radar state; new independent stores start at v1.
|
||||
|
||||
Full same-instance config transfer retains definitions and stable IDs, but
|
||||
never active collection or raw/aggregate contents. Restoring changed definitions
|
||||
suspends mismatched output grants for explicit reconciliation. Foreign/full or
|
||||
space import remaps internal marker/room/group references, drops out-of-scope
|
||||
links with a preview count, invalidates commissioning and imports outputs as
|
||||
inactive drafts with new local UUIDs. No automatic native-entity creation,
|
||||
recording, resume or cross-instance authorization. Plan-only transfer includes
|
||||
none of these source-dependent fields.
|
||||
|
||||
Room/source deletion never maps by display name. Reconciliation suspends
|
||||
affected outputs and prunes only runtime references; durable user definitions
|
||||
remain repairable until explicit removal. Source-generation, calibration,
|
||||
filter or room-boundary revision changes invalidate previews and epoch proof.
|
||||
Merely moving the decorative icon or changing the theme does neither.
|
||||
Use the common source-identity boundary: binding/unit/axis/owner-space changes,
|
||||
physical mount x/y and the physical `installation_id` affect source generation.
|
||||
Accepted heading/mirror corrections affect calibration revision. Explicit
|
||||
**Change installation** creates a new installation UUID even at identical saved
|
||||
coordinates. Stage 3 consumes these server-owned epochs rather than recomputing
|
||||
them from a wider/narrower client field set.
|
||||
|
||||
| Frontend/backend | Required behaviour |
|
||||
|---|---|
|
||||
| Old frontend + new backend | Ordinary stage-1/2 data survives; absent stage-3 controls do not enable anything; unauthorized config loss cannot erase active grants silently |
|
||||
| New frontend + old backend | Absent fresh capability = no stage-3 writes or fake local-success fallback; stage-1/2 usable |
|
||||
| New frontend + new backend | Backend revisions/epochs authoritative; identical results across clients and native entities |
|
||||
|
||||
Before permanent downgrade, Stop/Clear optional collection and Disable/Remove
|
||||
native outputs in a capable version. Older backend ignores new operational
|
||||
stores but cannot enforce their expiry while absent: do not downgrade with
|
||||
retained privacy data and promise continuing TTL. Re-upgrade purges expired
|
||||
data before any read and never resumes collection. An older editor may drop
|
||||
unknown fields if it reconstructs their parent; document this limitation and
|
||||
preserve read-only backup before such editing.
|
||||
|
||||
## 11. New WebSocket contract, consistency and budgets
|
||||
|
||||
Proposed names are explicit new APIs, not existing endpoints. Advertise the
|
||||
common fresh `radar_stage3_api:1` capability in `houseplan/config/get`, in addition
|
||||
to the stage-1/2 capabilities. Cached/local config never grants a capability. Reconnect,
|
||||
missing field or downgraded backend revokes it and stops requests.
|
||||
|
||||
| Proposed command | Request / response and authorization |
|
||||
|---|---|
|
||||
| `houseplan/radar/analysis/preview` | `expected_config_rev`, selected marker/room IDs, ≤8 trial sectors → bounded coverage and proof/unknown reasons; `may_write`, memory-only user-bound token, TTL 60 s |
|
||||
| `houseplan/radar/analysis/reflection_candidate` | Selected radar/epoch and bounded explicit witness IDs → line/residual/excluded preview; `may_write`, no mutation |
|
||||
| `houseplan/radar/commissioning/inspect` | saved group ID, phase `single`/`separated_pair`, `expected_config_rev`, explicit capture request ≤30 s → bounded server-computed phase proof and user-bound token; `may_write` + every source read permission, no persisted recording |
|
||||
| `houseplan/radar/commissioning/accept` | group ID, matching unexpired phase-proof tokens, explicit `attested:true`, current config revision → server-issued commissioning receipt via revisioned config transaction; same permissions rechecked; no client-supplied epochs accepted |
|
||||
| `houseplan/radar/rooms/subscribe` | ≤32 exact `{space_id,room_id}` references → safe current `{server_session_id,seq,revision,config_rev,space_id,room_id,presence,estimated_count,reason,expires_at}`; authenticated plus all contributing-source read ACLs, no raw payload |
|
||||
| `houseplan/radar/derived_entities/preview` | Explicit create/remove definitions + config/output revisions → exact owned IDs, operation diff and user-bound token; admin + `may_write` |
|
||||
| `houseplan/radar/derived_entities/apply` | token, operation UUID and revisions → durable lifecycle revision plus per-output active/pending/failed states; same authorization rechecked |
|
||||
| `houseplan/radar/heatmap/start` | exact source/room selection, duration, consent version, config/store revisions → run ID/end time; `may_write`; separate from raw recording |
|
||||
| `houseplan/radar/heatmap/stop` | exact run ID and revision → stopped/interrupted metadata; `may_write`; idempotent |
|
||||
| `houseplan/radar/heatmap/get` | bounded space/epoch/date range ≤7 days and cursor → ≤2,000 cells/page plus gaps/denominator; `may_write` |
|
||||
| `houseplan/radar/heatmap/clear/preview` | exact space/epoch/date scope and store revision → matched bucket/run counts plus user-bound token; `may_write`, read-only |
|
||||
| `houseplan/radar/heatmap/clear` | preview token, exact scope, expected revision, operation UUID → deleted scope/new revision; `may_write`, explicit confirmation |
|
||||
|
||||
Room subscriptions inherit Stage 1's per-source ACL checks on every publish and
|
||||
at least once per minute even without source events. If any contributing source
|
||||
is restricted, withhold the entire room result as restricted; never silently
|
||||
recalculate a different result from the visible subset or expose restricted
|
||||
source IDs. Native entities retain the separately disclosed HA entity ACL.
|
||||
Use the same server-session/monotonic-sequence and revision validation, clear on
|
||||
disconnect/reconnect, bounded latest-wins delivery and immediate health/clear
|
||||
transitions as Stage 1. Coordinate-derived room output expires no later than
|
||||
its contributing coordinate leases, including transport age; browser expiry
|
||||
clears that result if WS dies. Binary-only results use their native state health,
|
||||
not coordinate leases, but still clear on connection loss or access revocation.
|
||||
|
||||
Use existing config writes for saved lists/groups/reflector drafts, not a
|
||||
second unrevisioned config path. Commissioning accept is a server-validated
|
||||
operation using that same transaction helper and `expected_config_rev` barrier;
|
||||
it is the sole writer of accepted proof metadata. Collection APIs use their independent store
|
||||
revisions. Serialize config/operational lifecycle reconciliation; avoid a
|
||||
half-written grant or clear-then-late-flush resurrection. A transaction fails
|
||||
before mutation on stale preview, wrong user, lost privilege, revision/epoch
|
||||
change, unavailable capability or quota. Return stable localizable reason codes
|
||||
without raw points in errors. Exact retry is idempotent; reused operation ID
|
||||
with different content fails `operation_conflict`.
|
||||
|
||||
Analysis request ≤128 KiB; read/preview response ≤512 KiB; max 4 live preview
|
||||
tokens/user and 16/installation, expiry 60 s. No auto-enlarged request to bypass
|
||||
limits. Subscriptions coalesce to current snapshot, max 4 Hz and bounded one
|
||||
pending snapshot/connection; slow clients receive current state instead of an
|
||||
unbounded backlog. Unsubscribe, disconnect, page hidden and editor exit release
|
||||
unneeded UI consumers; explicitly authorized backend outputs/runs continue.
|
||||
|
||||
Performance witnesses use maximum accepted configuration (32 radars, four
|
||||
groups, 256 live slots) and pathological concentrated points. Association
|
||||
candidate work must remain inside §6 bounds; ≥60 seconds synthetic playback
|
||||
must show bounded memory/queues. No analysis/heatmap module joins the initial
|
||||
View graph; preserve `npm run bundle:budget`. On overload skip obsolete frames,
|
||||
publish unknown with a reason, and recover without inventing intermediate data.
|
||||
|
||||
## 12. i18n inventory (EN/RU)
|
||||
|
||||
Frontend keys beneath `radar.stage3`; backend reasons use stable codes and
|
||||
translated strings. Use the existing locale formatter for date/time, area,
|
||||
distance and counts. These are new keys, not a claim they already exist.
|
||||
|
||||
| Suffix | English | Русский |
|
||||
|---|---|---|
|
||||
| `tools` | Coverage and room results | Покрытие и результаты по комнатам |
|
||||
| `coverage` | Indicative geometric coverage | Ориентировочное геометрическое покрытие |
|
||||
| `coverage_hint` | Not a guarantee of detection | Не гарантия обнаружения |
|
||||
| `coverage_unknown` | Coverage unknown | Покрытие неизвестно |
|
||||
| `manual_estimate` | Manually estimated | Задано приблизительно вручную |
|
||||
| `trial_add` | Add trial sector | Добавить пробный сектор |
|
||||
| `trial` | Trial sector — not a connected device | Пробный сектор — не подключённый датчик |
|
||||
| `allowed_rooms` | Allowed rooms | Разрешённые комнаты |
|
||||
| `combine` | Combine observations | Объединять наблюдения |
|
||||
| `ambiguous` | Observations cannot be combined reliably | Наблюдения нельзя надёжно объединить |
|
||||
| `check_required` | Check this group again | Повторите проверку группы |
|
||||
| `presence_on` | Presence detected | Присутствие обнаружено |
|
||||
| `presence_off` | No presence detected | Присутствие не обнаружено |
|
||||
| `count` | Estimated people: {count} | Оценка числа людей: {count} |
|
||||
| `count_unknown` | Estimate unavailable | Оценка недоступна |
|
||||
| `create_outputs` | Create room sensors | Создать датчики комнат |
|
||||
| `remove_outputs` | Remove created entities | Удалить созданные сущности |
|
||||
| `external_retention` | HA and other consumers may retain these states separately | HA и другие потребители могут хранить эти состояния отдельно |
|
||||
| `admin_required` | Ask an HA administrator to create entities | Попросите администратора HA создать сущности |
|
||||
| `pending` | Application pending | Применение ожидается |
|
||||
| `repair` | Review the room and sources | Проверьте комнату и источники |
|
||||
| `heatmap` | Heatmap | Тепловая карта |
|
||||
| `heatmap_start` | Start aggregate collection | Начать сбор агрегированных данных |
|
||||
| `heatmap_notice` | Aggregate cells only, up to 7 days; collection ends {end} | Только агрегаты по ячейкам, до 7 дней; сбор до {end} |
|
||||
| `heatmap_incomplete` | Incomplete coverage | Неполные данные |
|
||||
| `heatmap_clear` | Delete heatmap data | Удалить данные тепловой карты |
|
||||
| `observed_seconds` | Seconds with an observation | Секунды с наблюдением |
|
||||
| `usable_seconds` | Seconds with usable coverage | Секунды с доступными данными покрытия |
|
||||
| `reflection_suggest` | Suggest a reflecting line | Предложить отражающую линию |
|
||||
| `insufficient` | Insufficient evidence | Недостаточно наблюдений |
|
||||
| `reflection_warning` | This can also exclude real targets | Это может исключить и настоящие цели |
|
||||
| `reflection_use` | Use this line | Использовать эту линию |
|
||||
| `quota` | Collection stopped: storage limit | Сбор остановлен: лимит хранения |
|
||||
| `interrupted` | Interrupted; restart manually | Прервано; запустите вручную |
|
||||
| `conflict` | Settings changed; refresh the preview | Настройки изменились; обновите предпросмотр |
|
||||
|
||||
Add backend `entity.binary_sensor.radar_room_presence.name` / RU
|
||||
`Присутствие — {room}` / EN `Presence — {room}`, and
|
||||
`entity.sensor.radar_room_estimated_people.name` / RU `Оценка числа людей — {room}`
|
||||
/ EN `Estimated people — {room}`. Translate `unknown`, `unavailable` through HA
|
||||
native state semantics; do not output the English literal as a numeric value.
|
||||
|
||||
## 13. Acceptance criteria and negative witnesses
|
||||
|
||||
The named test files below are proposed implementation deliverables. No test
|
||||
run is claimed by this docs-only task. Each protective AC requires the named
|
||||
negative mutation/input to fail; a passing happy path alone is insufficient.
|
||||
|
||||
| AC | Observable contract | Proof required | What must make the proof red |
|
||||
|---|---|---|---|
|
||||
| S3-1 | Opening/closing coverage does not change config, hardware or View; trial sectors are marked hypothetical | unit `radar-analysis`; smoke `radar_stage3` | Persist trial sector or invoke service from preview |
|
||||
| S3-2 | Coverage distinguishes known/manual/unknown and never treats walls as RF simulation | unit geometry fixtures + golden | Fill unknown sector or include unknown room in area denominator |
|
||||
| S3-3 | Explicit allowed list, empty list, missing/deleted room and boundary assignment follow §5 | backend + unit cross-language fixtures | Fallback from empty list; expand list after split/rename; assign boundary to two rooms |
|
||||
| S3-4 | Coherent and bounded-latest branches retain source quality and combine only mutually unique candidates supported by distinct accepted frame/report witnesses | unit `radar-fusion` + backend independent-X/Y LD2450 replay | Remove time gate, count one frame three times, promote bounded_latest to coherent, reject all valid LD2450 estimates, or permit transitive/tied merge |
|
||||
| S3-5 | Crossing/close people and missing/saturated sources preserve separate marks and unknown count | backend replay with labeled synthetic truth | Replace null with zero/sum or suppress ambiguous mark |
|
||||
| S3-6 | Short handoff cannot create personal identity, gap path, cross-wall/floor link or double room count | unit + smoke source switch | Persist token; join through wall; count boundary twice |
|
||||
| S3-7 | Native binary/count values match room snapshot, including partial input and unknown/unavailable; binary inputs keep Stage-1 state semantics | `test_ha_radar_room_outputs.py` | Treat missing as negative; expire unchanged binary on a coordinate lease; restore stale count; let UI count differ |
|
||||
| S3-8 | Create/retry/restart creates exactly requested owned entities with stable IDs; rename preserves HA customization | HA harness lifecycle tests | Derive ID from room name; duplicate on retry; re-enable HA-disabled entity |
|
||||
| S3-9 | Unauthorized/stale/replayed-different create/remove cannot mutate outputs or foreign entities; restricted sources cannot leak through room subscribe | HA WS permission tests + mutation | Remove admin/write/token/revision/ownership/source-ACL recheck; assert forbidden side effect or private canary |
|
||||
| S3-10 | Room/source removal suspends; confirmed owned removal is retryable and never resurrects | HA crash/restart tests | Delete unrelated registry row; late reconciliation re-adds tombstone |
|
||||
| S3-11 | Every heatmap run is separately consented, ≤24h, off by default, no restart resume or implicit raw collection | HA fake-clock + browser-close smoke | Auto-start on View/load/raw run; resume after restart; omit expiry |
|
||||
| S3-12 | Raw ≤24h and aggregate ≤7d; quota, clear, epoch isolation and no history in exports/attributes enforced | backend storage/negative payload tests | Expired response, raw/ID in bin, post-clear flush, foreign epoch sum, support leak |
|
||||
| S3-13 | Day/week exposes actual duration/gaps and observation-based metric without invented occupancy time | unit bucket/DST fixtures + golden | Fill gaps, multiply one cell by duplicate targets, show full week from 1h |
|
||||
| S3-14 | Reflection proposal needs separated fit/hold-out evidence and explicit final Save; bad/symmetric data is not auto-filtered | unit geometry + smoke | Remove witness/residual guard or apply proposal automatically |
|
||||
| S3-15 | Bounds hold at max config and dense adversarial targets; queues stay bounded and View remains responsive | backend perf replay + `radar_stage3` stress smoke | Remove candidate/page/quota cap; grow pending queue |
|
||||
| S3-16 | Capability downgrade and import preserve stage-1/2 behaviour, never create entities/restart collection or mint commissioning from client claims | backend round-trip + mixed-version/commissioning smoke | Trust cached capability; import active grant/run/proof; accept forged/duplicate-frame proof; erase unrelated siblings or reject inherited non-UUID room IDs |
|
||||
| S3-17 | View/kiosk gestures and safe actions work on desktop/touch; no history/editor leakage or diagnostic hit interception | targeted smoke + golden light/dark 360/736/1024 | Overlay intercepts pointer; hidden privileged controls dispatch writes |
|
||||
| S3-18 | EN/RU labels preserve estimated/unknown/hypothetical meaning and entity names remain translated | i18n parity + browser/native registry assertions | Raw key, hardcoded English name, unqualified “people” count |
|
||||
|
||||
## 14. Test plan, risks and release evidence
|
||||
|
||||
Unit fixtures: disjoint/missing room polygons; exact boundary band; rotated
|
||||
calibration; zero coordinates; timestamp skew; 2/3-source clique versus chain;
|
||||
two real close targets; crossing; slot reuse/saturation; source epoch churn;
|
||||
handoff at door, solid wall and different floor; reflection fit/hold-out failures;
|
||||
cell union, midnight/DST, TTL, quota and clear tombstones. Share normalized
|
||||
fixture data between frontend projection and backend, but independently assert
|
||||
expected values so two copies of the same wrong formula cannot agree unnoticed.
|
||||
|
||||
HA tests require the real Linux/WSL harness: platform setup/unload, registry
|
||||
rename/customization, disabled entity, auth downgrade, restart mid-create/remove,
|
||||
store failure, source loss and recovery, config conflict, import, quota,
|
||||
expiry-before-read and pending-flush-after-clear. Pure native-Windows tests do
|
||||
not prove HA lifecycle. No household CSV is committed or used without explicit
|
||||
permission; use synthetic fixtures plus separately consented, local-only field
|
||||
validation of count eligibility before enabling it for a connection profile.
|
||||
|
||||
Proposed commands after implementation: `npm run typecheck`, `npm test`,
|
||||
`npm run build`, `npm run bundle:sync`, `npm run bundle:budget`,
|
||||
`node scripts/no-new-any.mjs --base origin/dev --head HEAD`,
|
||||
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`,
|
||||
`node demo/smoke_radar_stage3.mjs`, `python -m pytest tests_backend -q` in
|
||||
the real HA harness. Add backend targeted files to the relevant selector and
|
||||
required mutation witnesses to `scripts/mutation-gate.mjs` for protective AC
|
||||
covered by expensive gates. Exact command/output and skipped checks accompany
|
||||
handoff; do not report proposed commands as passed.
|
||||
|
||||
Risks: false duplicate suppression/undercount, multipath mistaken for a mirror,
|
||||
unequal source clocks, geometric calibration drift, gaps hidden by aggregation,
|
||||
native registry partial failures and private-data copies outside House Plan.
|
||||
Mitigations are respectively conservative fallback, manual acceptance, temporal
|
||||
ineligibility, epoch invalidation, explicit denominators, durable reconciliation
|
||||
and honest retention boundaries. Count is informational, never a safety/security
|
||||
occupancy guarantee. No automation action is bundled with it.
|
||||
|
||||
Release artifacts for actual implementation: `docs/CHANGELOG.md` and
|
||||
`docs/CHANGELOG.ru.md`; `docs/USER-GUIDE.md` and `docs/USER-GUIDE.ru.md`;
|
||||
update `docs/ARCHITECTURE.md`,
|
||||
`docs/CONFIG-COMPATIBILITY.md`, field registry and privacy/support projection
|
||||
documentation. Capture representative actual coverage, ambiguous overlap,
|
||||
unknown native output and incomplete heatmap, light/dark
|
||||
and narrow/touch View. Use approved complete Linux CI documentation/golden
|
||||
artifacts and reviewed acceptance, not the conceptual UX sketch as a screenshot.
|
||||
Attach bounded-performance, permission/leak, native lifecycle and protective
|
||||
mutation results. Beta/RC at an exact green CI SHA precedes stable promotion.
|
||||
No version, shipped changelog, generated artifact or release is changed by this
|
||||
specification-only task.
|
||||
|
||||
## 15. Rollback and assumptions
|
||||
|
||||
Operational rollback: disable fusion groups to return to independent stage-1/2
|
||||
marks; restore single allowed room explicitly if desired; disable a saved
|
||||
reflector through stage 2; Stop/Clear aggregates; Disable or explicitly Remove
|
||||
native outputs. Hiding live display alone is not rollback of background work.
|
||||
No rollback sends source services or edits existing HA automations. Preserve
|
||||
config definitions for repair unless the user explicitly removes them. For
|
||||
binary/code downgrade follow §10's privacy purge and entity suspension steps;
|
||||
do not use a browser Labs flag to gate backend data, storage or permissions.
|
||||
|
||||
**Technical choices assumed, change freely in review:** module/API names,
|
||||
independent Store layout, sparse grid representation, UUID spelling, preview
|
||||
TTL, operation-journal implementation, candidate thresholds and quotas. Reviewer
|
||||
may change these with equivalent bounded tests and conservative semantics.
|
||||
They are not additional questions for the owner.
|
||||
|
||||
**Bounded weekly-mode assumption:** §8's local seven-day aggregates implement
|
||||
the explicitly requested weekly view, while every manual session remains ≤24h.
|
||||
Neither the weekly selector nor a prior consent authorizes continuous multi-day
|
||||
runs, raw retention changes or automatic renewal. This is recorded as an
|
||||
engineering interpretation, not an additional claim about the wording of Q3.
|
||||
No open owner question remains in this stage; independent review still decides
|
||||
readiness of the complete specification package.
|
||||
@@ -0,0 +1,244 @@
|
||||
# #485 — Presence radars on the plan: common acceptance contract
|
||||
|
||||
Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485).
|
||||
Author: Codex. Baseline: `dev` at
|
||||
`ed9ee026dc08054e12038b7dbd1b8525e7706d04`, 2026-09-08.
|
||||
This is a specification, not shipped behaviour. Status is exclusively the issue
|
||||
label. Current owner instruction: obtain independent specification approval,
|
||||
stop at **S5-ready**, and do not implement or proceed to S6.
|
||||
|
||||
## 1. Reading order and authority
|
||||
|
||||
The acceptance package consists of this common contract and all three documents:
|
||||
|
||||
1. [Stage 1 — source model, calibration and live presence](485-radar-presence-stage1.md).
|
||||
2. [Stage 2 — zones and bounded observations](485-radar-presence-stage2.md).
|
||||
3. [Stage 3 — coverage, aggregation and derived room states](485-radar-presence-stage3.md).
|
||||
|
||||
The stages are dependent implementation increments, **not** three ways to narrow
|
||||
the issue to its easiest part. The independent S4 review covers all four files;
|
||||
S5 means the complete package is ready. Later implementation/release reports
|
||||
must name exactly the stages delivered. No unfinished stage disappears because
|
||||
the live layer works. Creating follow-up issues, changing the agreed scope or
|
||||
closing this issue before all its AC are met requires an owner decision.
|
||||
|
||||
Authority: `docs/SCOPE.md`, repository process, the issue's owner request,
|
||||
[UX proposal](https://github.com/Matysh/houseplan-card/issues/485#issuecomment-5583682816),
|
||||
[analysis](https://github.com/Matysh/houseplan-card/issues/485#issuecomment-5583878376),
|
||||
and [accepted defaults](https://github.com/Matysh/houseplan-card/issues/485#issuecomment-5583909140).
|
||||
The numbered specs turn those requirements into testable contracts. Old open
|
||||
questions in the original issue are superseded by these explicit decisions;
|
||||
the external field report is evidence, not an instruction to copy a firmware.
|
||||
|
||||
## 2. Product intent and boundaries
|
||||
|
||||
Primary job: J1, understand presence **now**, on a familiar floor plan. The home
|
||||
administrator configures the connection once; household members, phones and
|
||||
wall kiosks see unobtrusive current observations. J4/J6 support safe setup and
|
||||
diagnostics, not a new technical console in ordinary View.
|
||||
|
||||
The owner explicitly requested zones, local observations, day/week heat maps
|
||||
and optional derived HA entities in #485. These are a bounded exception to
|
||||
SCOPE's exclusion of general historical analytics: only spatial radar setup
|
||||
and occupancy analysis, local and opt-in. This does not authorize general
|
||||
time-series charts, an automation editor, tracking named people or cloud upload.
|
||||
Original HA entities, automations and existing light Glow/spill remain untouched.
|
||||
|
||||
| Accepted decision | Consequence |
|
||||
|---|---|
|
||||
| Q1 default | Live marks appear after explicit configuration; global and per-radar switches hide them. No hover prerequisite. |
|
||||
| Q2 default | Stages 1–2 use one selected room if its polygon exists. Stage 3 permits multiple allowed rooms. No invented room rectangle. |
|
||||
| Q3 default | Stage-2 raw recording starts manually, defaults to 1 h, each run <=24 h, each point expires after 24 h, works without a browser, editor-only access. |
|
||||
| Q4 default | Cross-radar consolidation is Stage 3; earlier stages make no global people count or identity claim. |
|
||||
|
||||
The requested weekly heat map has its own explicit, default-off collection
|
||||
action; it is not enabled by raw recording or live display. Each collection
|
||||
authorization still lasts at most 24 h, without automatic renewal. Stage 3
|
||||
retains local aggregate bins for at most seven days, not seven days of raw
|
||||
trajectories; a week can contain gaps or separately authorized sessions. This
|
||||
is the bounded engineering interpretation of the owner's day/week requirement,
|
||||
not an assertion that Q3 authorized permanent collection.
|
||||
|
||||
## 3. One feature, several capabilities
|
||||
|
||||
There is no new editor mode or separate fleet of person/device markers. Add
|
||||
configuration under the existing device/placed-entity marker in Device editor:
|
||||
**Presence on plan**. Reuse the primary House Plan panel and full dashboard
|
||||
card's shared editor. The compact Room View card is not a second radar editor;
|
||||
this issue neither replaces its current renderer nor adds historical tools to it.
|
||||
|
||||
A device label/manufacturer is never proof of data availability. The backend
|
||||
derives capabilities from exact bound entities and a verified profile:
|
||||
|
||||
| Source evidence | Honest presentation | Forbidden inference |
|
||||
|---|---|---|
|
||||
| Valid local X/Y or explicitly described polar pair | One dot per current target slot | Stable person identity from a slot index |
|
||||
| Range without bearing | Arc of possible positions; no claimed angle | Turn two distances into X/Y or two people |
|
||||
| Occupancy/count for a known zone | Zone outline/state; Stage 2 provides manual polygons | Invent coordinates or remotely editable FP2 geometry |
|
||||
| Presence only | Existing presence marker and optional diagnostic coverage | A fabricated person position or occupied area from marketing range |
|
||||
|
||||
Reference adapters: ESPHome LD2450 (coordinate slots and separately verified
|
||||
zone-number bindings), generic explicit Cartesian/polar/range/occupancy inputs,
|
||||
and exact zone occupancy/count mappings (including FP2-style connections).
|
||||
Named products in the initial issue are examples, not a claim that every
|
||||
firmware is automatically supported. Unknown connections use manual bindings
|
||||
or an unsupported explanation, never guessed axes/units/writable capabilities.
|
||||
|
||||
## 4. Shared architecture and authoritative state
|
||||
|
||||
Existing seams: `src/types.ts`, `src/config-store.ts`,
|
||||
`src/space-geometry.ts`, `src/houseplan-editor-runtime.ts`,
|
||||
`src/houseplan-card.ts`, `src/render-device-snapshot.ts`,
|
||||
`src/houseplan-render-lifecycle.ts`, `src/live-viewport.ts`,
|
||||
`custom_components/houseplan/{__init__,store,auth,validation,websocket_api}.py`.
|
||||
New pure radar modules, a lazy editor and backend coordinator are specified in
|
||||
the stage documents. Do not duplicate the giant card's rendering/gesture logic.
|
||||
|
||||
Backend coordinator owns source normalization, freshness and room/filter
|
||||
classification. One coordinator per integration, not per browser. Stage 2 and
|
||||
3 consume its frames, never independently infer availability or reparse HA
|
||||
values. Browser projection helpers used for calibration must agree with the
|
||||
backend on shared numeric fixtures. Backend sampling never relies on UI frames.
|
||||
|
||||
One persisted optional namespace: `marker.radar.version=1` and
|
||||
`settings.radar`. Common settings start with `show_live` (absent => true);
|
||||
per-radar `enabled` requires explicit setup, absence means no radar operation.
|
||||
Stage 2 extends radar with `zones`/`reflectors`; Stage 3 extends it with
|
||||
`allowed_room_ids`, and `settings.radar` with fusion/room-output configuration.
|
||||
Capabilities `radar_stage1_api:1`, `radar_stage2_api:1`, `radar_stage3_api:1`
|
||||
are additive advertised runtime features, not stored user toggles.
|
||||
|
||||
Server-owned runtime identities:
|
||||
|
||||
- `source_generation`: opaque fingerprint/version of exact source bindings,
|
||||
mount x/y, installation UUID and coordinate conventions. It changes on a
|
||||
physical move, source/unit/axis change or owner-space change, not decorative
|
||||
icon drag. Explicit Change installation creates a new installation UUID even
|
||||
if the saved position is unchanged. Heading/mirror/reference correction alone
|
||||
changes calibration revision, not source generation.
|
||||
- `calibration_revision`: accepted projection correction within that same
|
||||
installation. Room/filter/zone revisions are separate evaluation epochs.
|
||||
- Frame `seq` and `server_session_id`: monotonic within one coordinator session;
|
||||
clients discard older/out-of-session deliveries and clear on reconnect.
|
||||
|
||||
Runtime ids, consent runs, histories, command tokens and telemetry are not
|
||||
portable configuration. All config writes keep the existing `expected_rev`
|
||||
transaction and `may_write` policy. New endpoints do not provide a generic
|
||||
service proxy. HA entity permissions are additionally respected when reading
|
||||
or controlling entity data; House Plan write access is not permission elevation.
|
||||
|
||||
## 5. Shared lifecycle / compatibility obligations
|
||||
|
||||
Optional additions do not change global config version or rewrite old plans on
|
||||
read. Validate new/changed known blocks; preserve untouched malformed/future
|
||||
blocks and unknown siblings losslessly, but do not execute them. Never coerce
|
||||
an invalid radar into a valid-looking coordinate at `(0,0)`.
|
||||
|
||||
New frontend/old backend: preserve config, display update-required explanation,
|
||||
make no unsupported calls and hide unsupported layer/tools. Old frontend/new
|
||||
backend: additive fields survive the existing unknown-field path; clients which
|
||||
reconstruct whole markers are not promised safe editing after downgrade. The
|
||||
documentation recommends read-only downgrade or current-version backup first.
|
||||
|
||||
The implementation must cover the existing full backup, config import/export,
|
||||
plan-only export, space duplicate/import/remove, room removal, marker movement
|
||||
between spaces and Optimize transactions, not just config/set. Stage documents
|
||||
define which references survive/remap. Full restore must retain exact configured
|
||||
refs but starts **no recording, device write or entity provisioning** implicitly.
|
||||
Duplicate space does not bind a copied radar to live hardware by accident:
|
||||
the existing duplicate virtualization policy also removes its radar bindings.
|
||||
|
||||
Disabled/removed/missing space or room is not permission to choose the first
|
||||
remaining space/room. Invalidated observations disappear; repair UI is explicit.
|
||||
Geometry-preserving Optimize keeps valid projections unchanged. A coordinate
|
||||
frame/physical-scale change invalidates calibration and suspends affected live
|
||||
projection until confirmed recalibration, rather than shifting observations
|
||||
silently. Pure pan, zoom, theme, fit-to-room, plan wallpaper transform and moving
|
||||
the decorative icon do not alter physical calibration. Room shape changes
|
||||
reevaluate membership, invalidate analysis epochs and affected room outputs.
|
||||
|
||||
Raw observations, aggregates and radar runtime snapshots are excluded from
|
||||
House Plan support packages/portable exports/logs/browser persistent storage.
|
||||
Full administrator HA backups may independently capture integration storage;
|
||||
the UI discloses that these copies are outside retention/deletion guarantees.
|
||||
No new cloud, telemetry, CSV upload or public download URL is introduced.
|
||||
|
||||
## 6. UX, visual and accessibility invariants
|
||||
|
||||
- View is quiet: small marks, no person names, trails, debug sectors, heat maps
|
||||
or editing handles unless explicitly entering setup/analysis. No new pulsing
|
||||
background competes with light Glow. Reduced motion disables interpolation.
|
||||
- Radar dots/arcs/fills are pointer-transparent. Existing device tap_action,
|
||||
full capsule hit area, room-fit, double-tap fit and pan/pinch retain priority.
|
||||
- Live layers are hidden in Plan/Background editors. Device setup shows only
|
||||
the selected radar's diagnostic geometry. Stage-3 analysis is a deliberate
|
||||
contextual surface, not a fourth permanently visible editor tab.
|
||||
- No rewrite of isometric projection: reuse the existing projection seam for
|
||||
floor-level live marks; never render a second unprojected floor layer. Precise
|
||||
calibration/analysis use 2D temporarily and restore the user's view on exit.
|
||||
- View/kiosk fully support touch (`docs/TOUCH-SUPPORT.md`), including resume,
|
||||
orientation changes and no-hover paths. Editors remain desktop-first, with
|
||||
safe cancellation/permissions on touch. Minimum essential action targets
|
||||
44 px; long labels wrap and dialogs retain visible Close/Cancel controls.
|
||||
- Keys/text are specified en+ru in each stage, escaped as text. Locale number,
|
||||
length and date formatting is reused. No coordinate speech on every frame;
|
||||
meaningful health changes are debounced polite status text.
|
||||
|
||||
## 7. Acceptance map and completion evidence
|
||||
|
||||
All stage AC are required in addition to these cross-cutting ones. Test paths
|
||||
named in this package are planned deliverables, not tests claimed as run.
|
||||
|
||||
| AC | Required outcome | Proof / negative witness |
|
||||
|---|---|---|
|
||||
| C-1 | Four capability classes produce only evidence-supported output | unit/backend state matrix; remove capability gate => invented point test fails |
|
||||
| C-2 | Setup opt-in; display toggle does not start/stop hidden collection | smoke + backend command spies; auto-start on View/toggle => zero-start assertion fails |
|
||||
| C-3 | One normalized source authority across clients/recording/entities | backend two-client/recorder fixture; stale or duplicated frame injection changes expected set and fails |
|
||||
| C-4 | All lifecycle seams in §5 preserve or explicitly invalidate refs without retargeting | backend import/duplicate/Optimize/removal + unit preservation tests; drop guard => wrong-space canary fails |
|
||||
| C-5 | No regression in devices, Glow, room actions, gestures or isometry | View/kiosk smoke and reviewed golden full scenes light/dark, 2D/isometry; layer hit-test mutation => click/pan failure |
|
||||
| C-6 | No raw/aggregate leakage or implicit external writes | backend permissions/support/export + browser persistence/network canary checks; include private field/skip permission => failure |
|
||||
| C-7 | Optional/unknown/future fields and rolling frontend/backend matrix remain safe | unit/backend roundtrip/API trace; remove capability gate/unknown preservation => failure |
|
||||
| C-8 | Full package is independently reviewed; no implementation under current instruction | review of spec links, exact branch SHA and diff restricted to documentation; S5 label, not author self-approval |
|
||||
|
||||
Protective AC require named negative witnesses and failing output at code review,
|
||||
per PROCESS §2.7. Expensive witnesses belong in `scripts/mutation-gate.mjs`;
|
||||
text-regex tests or a single happy-path screenshot cannot prove geometry,
|
||||
authorization or retention. TS/Python must consume identical coordinate fixtures.
|
||||
|
||||
Implementation loop: typecheck, unit, build. Targeted checks may diagnose a
|
||||
failure; complete golden/smoke/performance/security and full HA backend harness
|
||||
are pre-beta evidence on exact SHA in Linux CI/approved WSL, not native Windows
|
||||
claims. Existing initial-bundle gzip ceiling **256,000 bytes** remains binding;
|
||||
lazy editors/analysis cannot enter initial View graph. Stage documents add caps.
|
||||
|
||||
## 8. Release, rollback and risks
|
||||
|
||||
Future behaviour commits include `Issue: #485`, `User-Visible: yes`, both
|
||||
`docs/CHANGELOG.md` and `docs/CHANGELOG.ru.md`, user guide EN/RU, architecture,
|
||||
compatibility/field registry and status updates. Review complete synthetic
|
||||
golden scenes, not only crops; include compact phone, wall tablet and desktop,
|
||||
light/dark and representative live/error states. Record baseline/candidate
|
||||
performance and security/retention/mutation tables. A beta is issued only on
|
||||
owner command; current request creates no beta, code or version bump.
|
||||
|
||||
Rollback: hide live layers for visual rollback; disable affected radars to stop
|
||||
runtime processing; Stop/Clear private recordings/aggregates with the current
|
||||
version before downgrade. Removing optional derived HA entities is explicit
|
||||
and warns about downstream automations; hardware-zone rollback is another
|
||||
confirmed device operation, never automatic. Keep portable config backups.
|
||||
|
||||
Risks: coordinate ambiguity, suppressed unchanged HA reports, optimistic device
|
||||
echoes, overconfident counts, reflection false positives, source reconfiguration,
|
||||
privacy/storage growth and duplicate browser work. No specification can make
|
||||
an uninformative sensor report true position; degraded states are a necessary
|
||||
part of the feature, not defects to conceal.
|
||||
|
||||
## 9. Explicit engineering assumptions
|
||||
|
||||
The owner resolved Q1–Q4. Module names, bounded resource/timing constants,
|
||||
protocol version numbers, conservative confidence thresholds and test filenames
|
||||
are reviewable engineering choices. Seven-day aggregate expiry is the minimum
|
||||
window for the explicitly requested weekly view, with independent <=24 h opt-in
|
||||
sessions and visible gaps, not a continuous-history product expansion. Reference
|
||||
adapter firmware evidence is linked in stages; user-supplied private recordings
|
||||
are not prerequisites or public fixtures without contributor permission.
|
||||
@@ -102,6 +102,7 @@
|
||||
|---|---|
|
||||
| [#493](https://github.com/Matysh/houseplan-card/issues/493) Ограниченный picker и надёжный lifecycle сводной панели | [493-summary-panel-hardening.md](493-summary-panel-hardening.md) |
|
||||
| [#437](https://github.com/Matysh/houseplan-card/issues/437) Конфигурируемая сводная панель поверх плана | [437-summary-panel.md](437-summary-panel.md) |
|
||||
| [#485](https://github.com/Matysh/houseplan-card/issues/485) Радары присутствия: общий контракт и три этапа | [Общий контракт](485-radar-presence.md) · [Этап 1](485-radar-presence-stage1.md) · [Этап 2](485-radar-presence-stage2.md) · [Этап 3](485-radar-presence-stage3.md) |
|
||||
| [#487](https://github.com/Matysh/houseplan-card/issues/487) Пороги комфортной температуры для комнаты | [487-room-temperature-thresholds.md](487-room-temperature-thresholds.md) |
|
||||
| [#478](https://github.com/Matysh/houseplan-card/issues/478) Отказ от persisted-сущности `room_drafts` | [478-remove-room-drafts.md](478-remove-room-drafts.md) |
|
||||
| [#477](https://github.com/Matysh/houseplan-card/issues/477) Fixed point оптимизатора после штатного редактирования | [477-editor-writer-fixed-point.md](477-editor-writer-fixed-point.md) |
|
||||
|
||||
Reference in New Issue
Block a user