docs/adr/ADR-115-home-assistant-integration.md
| Field | Value |
|---|---|
| Status | Accepted (MQTT track P1–P7 + P8a + P9 + P10 shipped 2026-05-23 in PR #778, 410 lib tests, witness bundle VERIFIED) / Proposed (Matter SDK wiring P8b deferred to v0.7.1 per §9.10) |
| Date | 2026-05-23 |
| Deciders | ruv |
| Codename | HA-DISCO (MQTT) + HA-FABRIC (Matter) + HA-MIND (semantic primitives) |
| Relates to | ADR-018 (CSI binary frame format), ADR-021 (ESP32 vitals), ADR-031 (RuView sensing-first), ADR-039 (edge vitals packet 0xC511_0002), ADR-079 (camera ground-truth), ADR-103 (cog-person-count), ADR-110 (ESP32-C6 firmware), ADR-114 (cog-quantum-vitals) |
| Tracking issue | #776 — implementation in PR #778 |
| Related issues | #574 (mDNS for seed_url), #760 (sensing UI), #761 (HA competitor scan) |
RuView and the underlying WiFi-DensePose stack already expose rich human-sensing telemetry — presence, person count, 17-keypoint pose, breathing rate (BR), heart rate (HR), motion level, fall detection, RSSI, and zone occupancy — over a Rust wifi-densepose-sensing-server (v2/crates/wifi-densepose-sensing-server). The server emits three structured message types over its WebSocket at /ws/sensing:
Server message type | Source (main.rs) | Payload (selected fields) |
|---|---|---|
pose_data | line 2340 | 17 keypoints per detection, confidence, track_id |
edge_vitals | line 3971 | node_id, presence, fall_detected, motion, breathing_rate_bpm, heartrate_bpm, n_persons, motion_energy, presence_score, rssi |
sensing_update | lines 1903 / 2047 / 4098 / 4350 / 4481 | aggregated detections + zone hits |
Customers running a Cognitum Seed appliance (cognitum-v0 at :9000) or a standalone ESP32-S3 / ESP32-C6 node (per ADR-110) want this telemetry inside Home Assistant (HA) — the most widely deployed open-source home-automation hub (>500 k installs, OSS, MQTT-native) — so they can build automations around presence, vitals, falls, and motion without writing code against our REST/WebSocket API.
Two recent customer-facing issues show the same plug-and-play gap:
seed:// URL into the dashboard; they expect the hub to discover the node.Both reduce to the same underlying complaint: RuView is a black box that needs glue code to fit into the rest of a smart home. HA solves that problem industry-wide. We should meet users where they already are.
| Product | HA approach | Notes |
|---|---|---|
| espectre.dev | Custom HA integration (HACS), Python | Pose-only; no vitals; closed-source server |
| tommysense.com | MQTT auto-discovery + cloud bridge | Vitals only; cloud-mandatory |
| Aqara FP2 | Native ZigBee + HA | Presence + zones only; commercial mmWave |
| mmWave HLK-LD2410 | ESPHome firmware → HA | Presence + distance, no pose, no vitals |
| Matter devices (any) | Native Matter clusters, multi-controller | Apple/Google/Alexa/HA all consume; presence in OccupancySensing since Matter 1.3; no vitals/pose clusters yet |
| RuView (today) | None | Customer must build their own bridge |
The competitive bar is set by Aqara FP2 (HA-native, multi-zone presence) and ESPHome-flashed LD2410 nodes (cheap, plug-and-play). To match or exceed them we need first-class HA integration that exposes our differentiated capabilities: pose, HR/BR, fall, multi-room.
Adopt a dual-protocol integration strategy:
Primary — MQTT + Home Assistant auto-discovery (HA-DISCO). Add an MQTT publisher to wifi-densepose-sensing-server that connects to a user-supplied MQTT broker (default: mqtt://localhost:1883), publishes one HA-discovery message per capability per RuView node on startup and on periodic refresh (default 600 s), translates each WebSocket broadcast (edge_vitals, pose_data, sensing_update) into per-entity MQTT state messages, and honors a --privacy-mode flag that strips biometrics (HR / BR / pose keypoints) before publish.
Secondary — Matter Bridge (HA-FABRIC). Expose RuView nodes as Matter Bridged Devices over WiFi so the subset of capabilities Matter standardises today — presence (OccupancySensing), motion (BooleanState), fall events (SwitchCluster-as-event), person count (numeric attribute on the bridge) — are consumable by any Matter controller: Apple Home, Google Home, Amazon Alexa, Samsung SmartThings, and Home Assistant itself. Biometrics (HR/BR) and pose stay on MQTT until the Matter spec adds device types that can represent them.
The two paths are complementary, not alternative: MQTT carries the full telemetry surface for power users; Matter carries the standardised subset for cross-ecosystem reach. A user running HA gets both — MQTT entities populate alongside Matter Bridged Devices and HA dedupes via unique_id. A user running Apple Home gets only Matter, but they get the presence/fall/count signals that matter most for automations.
A Home Assistant HACS Python integration is sketched as a follow-on (§6.A) for users who don't run MQTT and want richer features than Matter exposes. A REST webhook path is rejected (§6.B).
| Criterion | A. MQTT auto-discovery | D. Matter Bridge | B. HACS Python integration | C. REST webhook |
|---|---|---|---|---|
| Zero-code UX for end user | yes (HA picks up entities automatically) | yes (pair via QR code, any controller) | yes (after install) | no (user wires automations by hand) |
| Cross-ecosystem reach | HA + any MQTT consumer | Apple / Google / Alexa / SmartThings / HA | HA-only | HA-only |
| Distribution + maintenance | one Rust feature in our existing crate | one Rust feature + Matter SDK linkage | new Python repo, HACS approval | trivial |
| Discovery (auto entity creation) | yes (HA's homeassistant/ topic namespace) | yes (Matter commissioning + bridge endpoints) | yes (config flow) | no |
| Bidirectional control | yes (subscribe to command topic) | yes (Matter commands) | yes | one-way only |
| Carries vitals (HR/BR) / pose | yes | no — no Matter clusters exist | yes (custom) | yes (custom) |
| Carries presence / count / fall | yes | yes (Matter 1.3+) | yes | yes |
| Works without HA running | any MQTT consumer | any Matter controller | HA-only | HA-only |
| Existing infra in target homes | most HA users already run a broker | one Matter controller per home (Apple HomePod / Nest Hub / HA-Matter add-on) | none | none |
| Effort to MVP | ~2 weeks | ~4–6 weeks (Matter SDK + commissioning) | ~4–6 weeks | ~2 days |
| Privacy controls | per-topic + retain policy | Matter fabric isolation + spec-level limits on what's exposable | application-layer | weak |
| Certification cost | none | "Works with HA" free; CSA Matter certification optional (~$3 k/year membership for the badge) | HACS review (free) | none |
| Test surface in CI | dockerised mosquitto + schema lint | matter-rs test harness + chip-tool sims | full HA test harness | curl |
MQTT is primary because it carries 100% of RuView's differentiated telemetry (pose, HR, BR) which no other path can. Matter is secondary because it covers the ~30% subset (presence/count/fall) that matters across the other 70% of smart-home buyers who don't run HA. Together they cover the whole market. Webhook (C) gives up too much (no entity discovery, no control plane) and is rejected. HACS (B) is strictly more polished than MQTT but strictly more expensive; revisit after MQTT adoption data is in.
Each RuView node becomes one HA device. Each capability becomes an entity on that device. ESP32 nodes behind a Cognitum Seed appliance are linked via HA's via_device field so the topology shows up in the HA UI.
| Capability | HA component | device_class | state_class | Unit | Icon | Source field (server WS) |
|---|---|---|---|---|---|---|
| Presence | binary_sensor | occupancy | — | — | mdi:motion-sensor | edge_vitals.presence |
| Person count | sensor | — | measurement | persons | mdi:account-group | edge_vitals.n_persons |
| Breathing rate | sensor | — | measurement | bpm | mdi:lungs | edge_vitals.breathing_rate_bpm |
| Heart rate | sensor | — | measurement | bpm | mdi:heart-pulse | edge_vitals.heartrate_bpm |
| Motion level | sensor | — | measurement | % | mdi:run | edge_vitals.motion (0–1 → ×100) |
| Motion energy | sensor | — | measurement | (unitless) | mdi:waveform | edge_vitals.motion_energy |
| Fall detected | event | — | — | — | mdi:human-fall | edge_vitals.fall_detected |
| Presence score | sensor | — | measurement | % | mdi:gauge | edge_vitals.presence_score (×100) |
| RSSI | sensor | signal_strength | measurement | dBm | mdi:wifi | edge_vitals.rssi |
| Zone occupancy (per zone) | binary_sensor | occupancy | — | — | mdi:map-marker | sensing_update.zones[*] |
| Pose keypoints | sensor (JSON attr) | — | — | — | mdi:human | pose_data.keypoints (opt-in) |
| Tracked persons (per ID) | binary_sensor (dynamic) | occupancy | — | — | mdi:account | pose_data.track_id |
Pose keypoints are intentionally not a first-class HA entity (HA has no 17-keypoint primitive); instead they're exposed as an attribute payload on a wifi_densepose_<node>_pose sensor, so power users can template against them but the default HA UI stays clean.
We follow HA's documented homeassistant/<component>/<object_id>/<entity>/config discovery convention. Object ID is wifi_densepose_<node_id> to namespace cleanly against other devices.
homeassistant/binary_sensor/wifi_densepose_<node_id>/presence/config (retained, QoS 1)
homeassistant/binary_sensor/wifi_densepose_<node_id>/presence/state (not retained, QoS 0)
homeassistant/binary_sensor/wifi_densepose_<node_id>/presence/availability (retained, QoS 1)
homeassistant/sensor/wifi_densepose_<node_id>/heart_rate/config (retained, QoS 1)
homeassistant/sensor/wifi_densepose_<node_id>/heart_rate/state (not retained, QoS 0)
homeassistant/sensor/wifi_densepose_<node_id>/breathing_rate/config
homeassistant/sensor/wifi_densepose_<node_id>/breathing_rate/state
homeassistant/event/wifi_densepose_<node_id>/fall/config (retained, QoS 1)
homeassistant/event/wifi_densepose_<node_id>/fall/state (not retained, QoS 1)
ruview/<node_id>/raw/pose (opt-in, not retained, QoS 0)
ruview/<node_id>/raw/sensing_update (opt-in, not retained, QoS 0)
The ruview/<node_id>/raw/* namespace is outside the homeassistant/ discovery prefix on purpose: it carries the original WebSocket JSON for users who want to consume it directly (Node-RED, Grafana, custom scripts), without HA trying to interpret it as an entity.
Presence (binary_sensor):
{
"name": "Presence",
"unique_id": "wifi_densepose_aabbccddeeff_presence",
"object_id": "wifi_densepose_aabbccddeeff_presence",
"state_topic": "homeassistant/binary_sensor/wifi_densepose_aabbccddeeff/presence/state",
"availability_topic": "homeassistant/binary_sensor/wifi_densepose_aabbccddeeff/presence/availability",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"device_class": "occupancy",
"qos": 1,
"device": {
"identifiers": ["wifi_densepose_aabbccddeeff"],
"name": "RuView node aabbccddeeff",
"manufacturer": "ruvnet",
"model": "ESP32-S3 CSI node",
"sw_version": "v0.6.7",
"via_device": "cognitum_seed_1"
},
"origin": {
"name": "wifi-densepose-sensing-server",
"sw_version": "0.7.0",
"support_url": "https://github.com/ruvnet/RuView"
}
}
Heart rate (sensor):
{
"name": "Heart rate",
"unique_id": "wifi_densepose_aabbccddeeff_heart_rate",
"state_topic": "homeassistant/sensor/wifi_densepose_aabbccddeeff/heart_rate/state",
"availability_topic": "homeassistant/sensor/wifi_densepose_aabbccddeeff/heart_rate/availability",
"unit_of_measurement": "bpm",
"state_class": "measurement",
"icon": "mdi:heart-pulse",
"value_template": "{{ value_json.bpm }}",
"json_attributes_topic": "homeassistant/sensor/wifi_densepose_aabbccddeeff/heart_rate/state",
"qos": 0,
"device": { "identifiers": ["wifi_densepose_aabbccddeeff"] }
}
State payload published to .../heart_rate/state:
{ "bpm": 68.2, "confidence": 0.91, "ts": "2026-05-23T14:00:00Z" }
Fall (event):
{
"name": "Fall detected",
"unique_id": "wifi_densepose_aabbccddeeff_fall",
"state_topic": "homeassistant/event/wifi_densepose_aabbccddeeff/fall/state",
"event_types": ["fall_detected"],
"icon": "mdi:human-fall",
"qos": 1,
"device": { "identifiers": ["wifi_densepose_aabbccddeeff"] }
}
State payload (fired once per fall, not retained):
{ "event_type": "fall_detected", "ts": "2026-05-23T14:00:00.123Z", "confidence": 0.87 }
device per RuView node (ESP32-S3 / S3-Mini / C6, or the host running sensing-server in mock mode).device.identifiers = ["wifi_densepose_<node_id>"] where node_id is the MAC-derived ID already in edge_vitals.node_id.device.via_device = "cognitum_seed_<seed_id>" so HA renders the topology as a tree (Seed → child nodes).| Topic | QoS | Retain | Refresh cadence | Rationale |
|---|---|---|---|---|
*/config | 1 | yes | on startup + every 600 s | HA expects retained discovery; re-publishing periodically self-heals if HA restarts before our state messages arrive |
*/state (sensor) | 0 | no | rate-limited per §3.7 | Best-effort; HA can tolerate occasional drops |
*/state (binary_sensor) | 1 | yes | on change only | Last value matters; new HA subscribers should see current state |
*/state (event) | 1 | no | on event | Falls must not be missed; never retained or HA replays old events |
*/availability | 1 | yes | LWT + 30 s heartbeat | Offline detection |
ruview/*/raw/* | 0 | no | as-emitted | Raw firehose; consumers opt in |
On connect, sensing-server sets an MQTT LWT on each entity's availability topic to offline (retained). On successful connect it publishes online (retained). A 30-second heartbeat re-publishes online so HA can detect zombie sessions.
LWT topic: homeassistant/binary_sensor/wifi_densepose_<node_id>/presence/availability
LWT payload: offline
LWT QoS: 1
LWT retain: true
Pose keypoints at 10 fps × 17 keypoints × 3 floats ≈ 4–8 kbit/s per person — fine over LAN, but pathological if a user accidentally routes it to a metered cellular MQTT bridge. Defaults:
| Entity type | Default rate | Configurable | Override flag |
|---|---|---|---|
| Presence (binary) | on change | yes | — |
| Person count | 1 Hz | yes | --mqtt-rate-count=1 |
| BR / HR | 0.2 Hz (every 5 s) | yes | --mqtt-rate-vitals=0.2 |
| Motion level | 1 Hz | yes | --mqtt-rate-motion=1 |
| Fall events | on event | no (always immediate) | — |
| RSSI | 0.1 Hz | yes | --mqtt-rate-rssi=0.1 |
| Pose keypoints | off by default, 1 Hz when on | yes | --mqtt-publish-pose --mqtt-rate-pose=1 |
| Zones | on change | yes | — |
New CLI flags on wifi-densepose-sensing-server (gated behind --mqtt):
--mqtt Enable MQTT publisher (default off)
--mqtt-host <HOST> MQTT broker host (default: localhost)
--mqtt-port <PORT> MQTT broker port (default: 1883, 8883 if --mqtt-tls)
--mqtt-username <USER> MQTT username
--mqtt-password-env <ENVVAR> Read password from env var (default: MQTT_PASSWORD)
--mqtt-client-id <ID> Client ID (default: wifi-densepose-<hostname>)
--mqtt-prefix <PREFIX> Discovery prefix (default: homeassistant)
--mqtt-tls Enable TLS (default off)
--mqtt-ca-file <PATH> CA bundle (default: system trust)
--mqtt-client-cert <PATH> Client cert for mTLS
--mqtt-client-key <PATH> Client key for mTLS
--mqtt-refresh-secs <N> Discovery refresh interval (default: 600)
--mqtt-rate-vitals <HZ> Vitals publish rate (default: 0.2)
--mqtt-rate-motion <HZ> Motion publish rate (default: 1.0)
--mqtt-rate-count <HZ> Person count publish rate (default: 1.0)
--mqtt-rate-rssi <HZ> RSSI publish rate (default: 0.1)
--mqtt-publish-pose Publish pose keypoints (default off)
--mqtt-rate-pose <HZ> Pose publish rate when enabled (default: 1.0)
--privacy-mode Strip biometrics (HR/BR/pose) before publish
Env var equivalents follow RUVIEW_MQTT_HOST, RUVIEW_MQTT_USERNAME, etc., so Docker / systemd users don't have to wire long arg lists. Configuration is loaded in the order: CLI > env > defaults.
WARN if --mqtt is enabled without --mqtt-tls and the broker is not localhost.--privacy-mode strips biometric + biometric-derivable channels before any MQTT publish, regardless of subscriber. Discovery messages for those entities are never published in this mode (HA never sees them exist).
| Channel | Default | --privacy-mode |
|---|---|---|
| Presence | published | published |
| Person count | published | published |
| Motion level | published | published |
| Zone occupancy | published | published |
| RSSI | published | published |
| Breathing rate | published | stripped |
| Heart rate | published | stripped |
| Fall events | published | published (safety > privacy) |
| Pose keypoints | off by default | stripped (cannot be force-enabled) |
This implements the ADR-106 primitive-isolation contract at the integration boundary: HR / BR / pose are biometric-class signals and must not leak to an unconstrained MQTT broker without explicit operator opt-in.
The Matter path runs in the same wifi-densepose-sensing-server process behind a --matter feature flag, gated independently of --mqtt. The bridge presents itself to Matter controllers as a Bridged Devices Aggregator (per Matter Core Spec §9.13) with one Bridged Device endpoint per RuView node, exposing the standardised subset of capabilities. Biometrics and pose are not exposed over Matter — they have no spec-defined clusters and cannot be soundly represented (covering them in Generic Sensor would force every controller to render them as nameless numbers).
| RuView capability | Matter cluster | Endpoint device type | Source field |
|---|---|---|---|
| Presence | OccupancySensing (0x0406) | OccupancySensor (0x0107) | edge_vitals.presence |
| Motion (boolean above threshold) | OccupancySensing (0x0406) | (same endpoint) | edge_vitals.motion > 0.1 |
| Fall event | Switch (0x003B) MultiPressComplete event | GenericSwitch (0x000F) | edge_vitals.fall_detected (one momentary press = one fall) |
| Person count | OccupancySensing extension attribute (vendor-specific 0xFFF1_0001) | (same endpoint) | edge_vitals.n_persons |
| Zone occupancy | one OccupancySensor endpoint per zone | (multiple endpoints) | sensing_update.zones[*] |
| RSSI / motion energy / presence score / breathing rate / heart rate / pose | not exposed over Matter | — | (MQTT only) |
The vendor-specific person-count attribute uses RuView's CSA-assigned vendor ID (open question §9.9). Controllers that don't understand the vendor extension still see the standard OccupancySensing.Occupancy boolean — graceful degradation.
--matter-setup-file <PATH> on first start. User scans with Apple Home / Google Home / HA Matter integration.OperationalCredentials cluster handles fabric isolation.--matter-reset CLI flag wipes stored fabric credentials so a node can be repaired against a new controller.Three viable Rust paths:
| Option | Pros | Cons |
|---|---|---|
matter-rs (project-chip/rs-matter) — pure-Rust SDK | No FFI, no C++ build chain, fits our Rust-only crate policy, MIT-licensed | Less mature than C++ chip-tool; certification path less proven |
project-chip/connectedhomeip via Rust FFI bindings | Reference implementation, every controller tested against it, certification-ready | Drags in CMake, C++ toolchain, ~50 MB of vendored code; clashes with our cargo-first build |
| External Matter bridge process (separate ESPHome-like daemon) | Decouples Rust crate from Matter SDK churn | Operational complexity; two processes to deploy |
Tentative: matter-rs for v0.7.0 ship; fall back to chip-tool-FFI if cert blockers emerge. Final decision deferred to P7 spike.
These are deliberate, not bugs — users must see them in docs/integrations/matter.md before pairing:
A node paired into HA shows up in two ways:
HA dedupes by unique_id (we set both paths' IDs to wifi_densepose_<node_id>_<entity>), so users don't see ghost devices. The Matter device is the one Apple Home or Google Home will see if the user also pairs into those — same physical node, three controllers, no duplication. This is the architectural reason for adopting both protocols rather than picking one.
Raw signals are not the product. Customers don't want to write a Node-RED flow that thresholds breathing rate at night to infer sleep. They want a binary_sensor.bedroom_someone_sleeping they can wire directly into a "dim hallway light at 10 % if anyone's asleep" automation. Same for fall risk, distress, room activity, elderly inactivity, meeting-in-progress, bathroom occupancy. This is the inference layer that turns RuView from "RF sensing" into ambient intelligence infrastructure — and it has to ship as first-class HA entities and Matter events, not as a developer SDK.
Each primitive is a fused state derived from one or more raw channels with a small finite-state machine. Inference runs inside wifi-densepose-sensing-server (same place MQTT publication runs), gated behind --semantic (default on; can be disabled). Each primitive has a confidence score and an explanation field so HA users can debug why it fired.
| Primitive | Inputs (raw) | Output kind | Default true-condition | Hysteresis / refractory |
|---|---|---|---|---|
| Someone sleeping | presence + low motion (<5 % for ≥300 s) + breathing rate 8–20 bpm + low HR variability | binary_sensor (occupancy) | all conditions hold simultaneously | enters after 5 min; exits when motion > 15 % for ≥30 s |
| Possible distress | sustained elevated HR (>1.5× rolling baseline for ≥60 s) + agitated motion + no fall | binary_sensor (problem) + event | confidence ≥ 0.75 | latch for 5 min after exit |
| Room active | presence + motion > 10 % for ≥30 s in any 5-min window | binary_sensor (occupancy) | window-rolling | exits on 10 min idle |
| Elderly inactivity anomaly | no motion + presence stable for > N× rolling daily median idle (default 2×) | binary_sensor (problem) + event | model-personalised | per-resident baseline; alerts max 1×/day |
| Meeting in progress | person count ≥ 2 + sustained low-amplitude motion (sitting) + speech-band micro-motion if speech_band cog installed | binary_sensor (occupancy) | ≥2 ppl + ≥10 min | exits when person count < 2 for 2 min |
| Bathroom occupied | presence true in zone tagged bathroom | binary_sensor (occupancy) | zone+presence | privacy-mode keeps this enabled (it's not biometric) |
| Fall risk elevated | recent near-fall (sharp acceleration without confirmed fall) OR gait instability score > threshold | sensor (0–100) + event on threshold cross | model-derived | 24-hour window |
| Bed exit (overnight) | "someone sleeping" → presence transitions out of bed-tagged zone between 22:00–06:00 local | event | edge-triggered | one event per exit |
| No movement (safety check) | presence true + motion < 1 % for ≥ N minutes (default 30) | binary_sensor (problem) + event | duration threshold | clears on motion |
| Multi-room transition | track_id continuous across zones within 10 s | event (who_went_from_to) | edge-triggered | per-track event |
Catalog v2 (deferred): "child playing", "pet vs human", "agitation gradient", "circadian phase". Owned by an ADR-1xx follow-on after the v1 primitives have field data.
| Layer | How a semantic primitive shows up |
|---|---|
| MQTT (HA-DISCO) | New topic namespace homeassistant/binary_sensor/wifi_densepose_<node>/<primitive>/ and homeassistant/event/wifi_densepose_<node>/<primitive>/ — full discovery payloads including the explanation field as json_attributes |
| Matter (HA-FABRIC) | Standard cluster mappings: sleeping/active/meeting/bathroom → OccupancySensing (separate endpoints); distress/inactivity/no-movement/bed-exit/fall-risk-cross → Switch.MultiPressComplete events on dedicated GenericSwitch endpoints; fall-risk score → vendor-extension attribute on the bridge endpoint |
| Home Assistant automations | Ship 8 starter blueprints in P5: "Notify on possible distress", "Wake-up routine on bed exit", "Dim hallway on someone sleeping", "Alert on elderly inactivity anomaly", "Lights on for meeting in progress", "Bathroom fan on while occupied", "Escalate on fall risk crossing 70", "Auto-arm security when room not active" |
| Apple Home scenes | Each OccupancySensor endpoint and each GenericSwitch event triggers Apple Home scenes via Matter — user picks "When bedroom someone sleeping is on, run night mode" from the Apple Home UI directly. No HA required for this path |
These eight cover the top automation requests from the smart-home market without needing video or wearables:
--privacy-mode deployment can keep semantic primitives ON and still strip HR/BR/pose, because the inference happens server-side and only the state crosses the wireEach primitive ships with:
docs/integrations/semantic-primitives-metrics.mdreason: ["motion<5%", "br=12bpm", "presence=true"] style attributes so HA users can debug--semantic-threshold-<primitive>=<float> (default published in the metrics doc)csi_calibration_in_progress states (per ADR-014)--semantic Enable inference layer (default: on)
--semantic-thresholds-file <PATH> Per-primitive thresholds (defaults shipped)
--semantic-zones-file <PATH> Zone-tag map (e.g. {"bathroom": ["zone_3"]})
--semantic-baseline-window-days <N> Days of history for personalised baselines (default: 14)
--no-semantic-<primitive> Disable a specific primitive (repeatable)
Inference lives in a new module semantic_inference.rs alongside mqtt_publisher.rs and matter_bridge.rs. It subscribes to the same tokio::broadcast channel everything else does, runs each primitive's FSM, and emits two output streams:
SemanticState event on a new broadcast channel that MQTT and Matter publishers both subscribe to (so the same inference drives both surfaces without duplication)semantic_events.jsonl log under --data-dir for offline analysis + ADR-079 paired-capture supervisionThis means: adding a new primitive is one file change. No MQTT schema rev, no Matter cluster rev — just add the FSM, register it, and discovery/state publish flow through both surfaces automatically.
| Phase | Scope | Status |
|---|---|---|
| P1 | Add mqtt feature flag to wifi-densepose-sensing-server Cargo.toml (depends on rumqttc = "0.24"). Wire CLI flags (§3.8) into cli.rs. No publishing yet, just config plumbing + unit tests on flag parsing. | pending |
| P2 | HA discovery message emitter. New module mqtt_discovery.rs. Emits all entity config topics on connect + every --mqtt-refresh-secs. Schema-validated against HA's published JSON schema. | pending |
| P3 | State publication. Subscribe to internal tokio::broadcast channel (the one tx.send(json) writes to on line 3983 of main.rs). Translate edge_vitals / sensing_update / pose_data messages into per-entity state payloads. Apply rate-limit + privacy-mode filters. | pending |
| P4 | Integration tests: dockerised mosquitto in CI (extend .github/workflows/firmware-qemu.yml pattern), schema-validate every emitted config against HA's homeassistant/components/mqtt JSON schemas (pin to a tested HA version). Add a smoke test that brings up sensing-server in --source mock --mqtt, subscribes with paho-mqtt test client, asserts on entity creation. | pending |
| P4.5 | Semantic inference layer (HA-MIND). New module semantic_inference.rs implementing the 10 v1 primitives from §3.12. Output broadcast channel consumed by both MQTT publisher (P3) and Matter bridge (P8). Per-primitive precision/recall baselines published to docs/integrations/semantic-primitives-metrics.md. Unit tests per FSM + integration tests via replay of ADR-079 paired captures. | pending |
| P5 | Docs: new docs/integrations/home-assistant.md with screenshots of the HA UI after auto-discovery completes, example HA dashboard YAML (Lovelace card configs), 8 starter blueprints from §3.12.2 (distress notify, wake routine, hallway dim, elderly anomaly alert, meeting lights, bathroom fan, fall-risk escalate, auto-arm security), and the raw-channel example automations: "turn on hall light when presence ON", "send notification on fall_detected event", "log HR/BR to InfluxDB". | pending |
| P6 | Ship --mqtt in the next sensing-server release (target: v0.7.0). Demo end-to-end on cognitum-v0 against a Mosquitto add-on running on a Home Assistant OS install. Update README hardware-options table with "Works with Home Assistant" badge. | pending |
| P7 | Matter Bridge spike: build a throwaway prototype with matter-rs exposing one OccupancySensor endpoint + one GenericSwitch for fall. Pair against Apple Home, Google Home, and HA's Matter integration. Decision gate: if pairing works on all three, proceed to P8; if blocked, switch to chip-tool FFI and re-spike. | pending |
| P8 | Matter Bridge production. Implement --matter, --matter-setup-file, --matter-reset, --matter-vendor-id, --matter-product-id CLI flags. Aggregator + Bridged Devices for all RuView nodes; per-zone occupancy endpoints; fall as MultiPressComplete event; person count as vendor-extension attribute. Integration tests via chip-tool sim. | pending |
| P9 | Multi-controller validation. Pair one Cognitum Seed + 3 child ESP32 nodes simultaneously into HA, Apple Home, and Google Home. Verify presence flips on all three within 1 s of a real motion change. Document the multi-admin flow in docs/integrations/matter.md. | pending |
| P10 | CSA Matter certification path (optional, ADR-1xx follow-up). Decide cost vs marketing value of the official "Matter-certified" badge ($3 k/year CSA membership + per-product test fees). Sketch only — production decision deferred. | pending |
Each phase ends with a checkbox PR. The ADR is updated with actual artifacts (commit hashes, screenshots, witness bundle entries) as phases land. P1–P6 (MQTT) and P7–P10 (Matter) run in parallel after P6 lands — they share no code, so a Matter regression cannot break the MQTT path and vice versa.
--privacy-mode flag that gives operators a single-knob biometric strip for compliance contexts.rumqttc) in wifi-densepose-sensing-server. Mitigated by feature-flag (mqtt), default off; users who don't enable --mqtt pay zero binary or runtime cost.matter-rs tentatively) gated behind --matter feature flag. Adds ~5 MB to release binary when enabled; zero cost when disabled. Tracking CSA spec churn is a real ongoing cost.homeassistant/<component>/.../config schema being stable (their published policy), but historically they have evolved fields like availability_topic → availability (list-of). We'll pin to a tested HA version per release and call out tested-against in docs/integrations/home-assistant.md.0xFFF1. Production deployment past P9 requires the membership decision (§9.9).Acceptance criteria are §8. Beyond those, this ADR is "Accepted" once P6 ships and at least one external user has reported a working HA install via the public issue tracker.
Rough sketch:
ruvnet/hass-wifi-densepose)./ws/sensing and REST at /api/*.Effort estimate: ~4–6 weeks (vs ~2 weeks for §2 MQTT path). Adds a Python codebase to maintain in a Rust-first org. Pays off in two scenarios:
Plan: revisit after P6 lands and we have real adoption data on the MQTT path. If MQTT covers 80%+ of installs, HACS becomes a nice-to-have. If not, it becomes ADR-1xx follow-up.
POSTs to HA's webhook endpoint (/api/webhook/<id>).Rejected because:
set_state / arm / disarm path back.Documented here so future readers know we considered it.
mDNS / Zeroconf lets HA (or any local client) discover sensing-server's IP without manual configuration. It's orthogonal to MQTT: we should add it (already tracked in #574) so the user doesn't have to type the broker host either. mDNS resolves where the broker is; MQTT auto-discovery resolves what entities to create. Both ship; neither blocks the other.
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Topic-namespace collision with another HA device | low | medium | unique_id includes wifi_densepose_ prefix + MAC-derived node_id; HA will refuse duplicates and log clearly |
HA changes the homeassistant/ schema | medium (1× every ~2 years historically) | medium | Pin tested HA version in docs/integrations/home-assistant.md; CI runs schema validation against the pinned version |
| Bandwidth blowup from pose keypoints | medium | low (LAN) / high (metered link) | Pose publishing is off by default; rate-limited when on; users hit a clear WARN if they enable pose without explicit rate cap |
| Privacy regression — biometrics leaked to a public broker | medium | high | --privacy-mode strips them at source; WARN if --mqtt enabled without --mqtt-tls on a non-localhost broker; never publish HR / BR / pose discovery in privacy mode |
| Cognitum Seed firmware footprint (if we ever push MQTT into the ESP32 path) | low | medium | Out of scope for this ADR — MQTT lives in sensing-server only. ESP32 keeps the lean UDP/WS path. If we later add MQTT to firmware, it's ADR-1xx with its own size budget per ADR-110 |
| Broker compromise (bad actor on the network gets read access to MQTT) | low | high | mTLS recommendation in §3.9; --privacy-mode for high-risk deployments |
| HA-side cardinality explosion from per-track-id binary_sensors | medium | low | Cap dynamic person entities at 10; old ones are removed via discovery payload="" (HA delete-entity convention) |
Matter SDK (matter-rs) immaturity blocks cert | medium | medium | P7 spike validates pairing on three controllers before P8 production work; fall back to chip-tool FFI if blocked |
| Matter spec adds vitals device types, our vendor-extension attributes become non-standard | low (3+ years out) | low | Vendor-extension attributes are opt-in for controllers; migration to standard cluster IDs is a one-version bump when the spec lands |
| Multi-fabric races (HA, Apple, Google all see the same node and fire conflicting automations) | medium | medium | Document the multi-admin guidance in docs/integrations/matter.md: pick one primary controller for automations, others for visibility |
| Apple Home / Google Home rendering misrepresents RuView (e.g. shows generic "Sensor") | medium | low | Set rich VendorName / ProductName / ProductLabel in BasicInformation cluster; ship a Matter App icon (per CSA brand guidelines) once vendor ID is real |
| CSA membership cost ($3 k/y) is a recurring spend with uncertain ROI | low (decision deferred to P10) | medium | Ship using dev VID 0xFFF1 through P9; commit to membership only after adoption data justifies it |
A reviewer can run all of the following without modifying source:
# 1. Start sensing-server with mock source + MQTT
cargo run -p wifi-densepose-sensing-server -- \
--source mock \
--mqtt \
--mqtt-host localhost \
--mqtt-prefix homeassistant
# 2. Observe discovery + state messages
mosquitto_sub -t 'homeassistant/#' -v
# Expected: discovery configs for presence, heart_rate, breathing_rate, motion,
# fall, person_count, rssi — one per entity per node — plus periodic state messages
# 3. Run the full workspace test suite
cd v2 && cargo test --workspace --no-default-features
# Expected: 1,031+ tests passed, 0 failed (new mqtt tests included)
# 4. Schema-validate discovery configs against HA's published schemas
cargo test -p wifi-densepose-sensing-server --features mqtt mqtt::discovery::schema
# Expected: green
# 5. Privacy mode strips biometrics
cargo run -p wifi-densepose-sensing-server -- --source mock --mqtt --privacy-mode &
mosquitto_sub -t 'homeassistant/#' -v | tee /tmp/privacy.log
# Expected: NO heart_rate, breathing_rate, or pose entities in discovery
grep -E "(heart_rate|breathing_rate|pose)" /tmp/privacy.log
# Expected: empty (exit 1)
# 6. HA auto-discovery end-to-end (manual, post-P5)
# - Add Mosquitto broker to a fresh HA OS install
# - Add MQTT integration in HA, point at broker
# - Start sensing-server with --mqtt
# - HA Settings → Devices → expect "RuView node <mac>" with all entities
# - Trigger mock presence change; presence entity flips ON / OFF live
# 7. LWT / availability
# - Run sensing-server, observe `online` published
# - Kill sensing-server (-9), wait 30 s
# - Expect `offline` on every entity's availability topic
# 8. Matter Bridge pairing (post-P7)
cargo run -p wifi-densepose-sensing-server -- \
--source mock \
--matter \
--matter-setup-file /tmp/matter-qr.txt
# Expected: setup code + QR string printed; bridge advertises over mDNS
# 9. Matter cross-controller test (post-P9; manual)
# - Pair the bridge into Apple Home (scan QR with iPhone)
# - Pair the same bridge into Home Assistant Matter integration (same QR)
# - Trigger mock presence change in sensing-server
# - Expected: occupancy entity flips ON in both controllers within 1 s
# 10. Matter privacy invariant
mosquitto_sub -t 'homeassistant/sensor/+/heart_rate/state' -v &
chip-tool occupancysensing read occupancy 0xDEADBEEF 1 # Matter endpoint 1
# Expected: MQTT still publishes HR (without --privacy-mode); Matter NEVER exposes HR cluster (no clusters exist for it)
All ten must pass before the ADR moves from Proposed → Accepted. Tests 1–7 cover MQTT (P1–P6); tests 8–10 cover Matter (P7–P9). Tests can be re-run incrementally as each phase lands.
All 13 questions resolved by maintainer @ruv on 2026-05-23. Status: ACCEPTED.
Decision principle (canonical): preserve clean protocols, avoid firmware bloat, avoid fake semantics, ship MQTT first, validate Matter second.
docs/integrations/home-assistant.md.homeassistant (HA's default). --mqtt-prefix remains overridable for users with custom HA setups.ruvnet/hass-wifi-densepose — wired into the support_url field of every discovery payload's origin block from P1.WARN if --mqtt enabled without --mqtt-tls on a non-localhost broker. v0.8.0 promotes to hard fail (exit non-zero) once docs cover the CA setup path.node_friendly_name. ✅ NVS / config only. No ADR-039 packet change. Sensing-server resolves the friendly name from local config and injects into MQTT/Matter device labels.docs/integrations/home-assistant.md and re-exported as wifi_densepose_core::pose::COCO17.via_device. Easier to debug; matches §3.4.0xFFF1 through P9. CSA membership decision gate at P10 (deferred; sketched only).matter-rs. Fall back to chip-tool FFI only if cert blockers emerge in P7 spike.Switch.MultiPressComplete. Cleaner semantics for controllers; matches Apple Home / Google Home rendering expectations.Occupancy: ON/OFF only — that's honest. HA and SmartThings will surface the count via the vendor-extension attribute.Empty as of 2026-05-23. New questions discovered during implementation will be filed here, ACK'd by maintainer, and dated.
rumqttc (chosen Rust MQTT client): https://docs.rs/rumqttc/0xC511_0002)cog-person-count (person count primitive)cog-quantum-vitalsADR-115 is the integration story that turns RuView from "another sensing platform" into "drop-in upgrade for any HA install and any Matter-controller home." MQTT carries the rich, differentiated telemetry; Matter carries the standardised subset across every controller ecosystem. Numbers 111 and 112 remain reserved per the project ADR-numbering policy.