docs/user-guide-apple-homepod.md
Ambient intelligence for Apple Home. Run RuView as a native HomeKit accessory so your HomePod discovers it, Siri understands it, and Apple Home automations govern it — no Home Assistant required.
RuView turns WiFi radio reflections into spatial intelligence (presence, breathing, fall risk, activity patterns). When paired with a HomePod or Apple TV acting as your Home Hub, RuView becomes an invisible sensor that feeds Siri, automations, and scenes:
ESP32-C6 CSI node (living room)
↓ (UDP feature stream)
RuView Sensing Server (announces presence, vital signs, BFLD events)
↓ (HTTP polling)
HAP Bridge (advertises HomeKit accessory on mDNS)
↓ (Bonjour discovery)
HomePod or Apple TV (Home Hub)
↓ (forwards to Home app + Siri)
iPhone, iPad, Mac, Watch, Apple Home automations
The integration leverages HomeKit Accessory Protocol (HAP-1.1) — the same standard that Philips Hue, Eve, and Nanoleaf use. Your HomePod discovers the bridge within seconds of launch, pairing is one-tap from the Home app, and Siri queries work immediately: "Hey Siri, is anyone in the living room?"
For design rationale and privacy safeguards, see ADR-125 — RuView ↔ Apple Home native HAP bridge.
Eight incremental iterations landed in PR #797 on the feat/adr-125-apple-fabric branch:
| Iteration | Capability | Commit | Status |
|---|---|---|---|
| 1 | Multi-characteristic HomeKit accessory (Motion + Occupancy + StatelessProgrammableSwitch) | 48db60a65 | Runtime-live |
| 2 | Sensing-server HTTP endpoints for bridge polling (/api/v1/vitals, /api/v1/bfld, /api/v1/semantic-events) | 194a2e163 | Runtime-live, curl-validated |
| 3 | HAP bridge with N child accessories; Siri-by-room (name each room, Siri voices it) | 63b77f760 | Runtime-live, two bridges advertising |
| 4 | Semantic-events endpoint per §2.1.d (Unknown Presence, Unexpected Occupancy, Unrecognized Activity Pattern) | 3d30261e7 | Runtime-live, privacy invariant I1 enforced |
| 5 | rvagent MCP consumer (agentic chain); 12 MCP tools for Claude Code integration | c19742d71 | Runtime-validated on real C6 |
| 6 | PyO3 BFLD PrivacyClass binding (SOTA rust crate exposed to Python) | de0712d43 | Source-built (cargo check green) |
| 7 | Shortcuts-as-glue (launchd job + Speak Text on HomePod via iCloud Home graph, bypasses Bonjour blocker) | d0525359d | Runtime-validated, osascript trigger green |
| 8 | Custom characteristic UUID scaffold for Eve.app rendering (design complete; runtime HAP-python JSON-loader follow-up) | 3bb8c1621 | Design scaffolded |
What you can do today:
192.168.1.0/24).Connect the C6 via USB and run the provisioning script:
python firmware/esp32-csi-node/provision.py \
--port /dev/ttyUSB0 \
--ssid "YourWiFiSSID" \
--password "YourWiFiPassword" \
--target-ip 192.168.1.20
Verify the C6 boots on the network:
ping 192.168.1.20
mkdir -p ~/ruview-hap
cd ~/ruview-hap
python3 -m venv venv
source venv/bin/activate
pip install HAP-python
From the repository (e.g., cloned on your Mac), copy these files:
cp scripts/c6-presence-watcher.py ~/ruview-hap/
cp scripts/ruview-sensing-server.py ~/ruview-hap/
cp scripts/ruview-hap-bridge.py ~/ruview-hap/
Terminal 1: Start the C6 presence watcher (reads UDP packets from the C6, applies BFLD privacy gate)
cd ~/ruview-hap
source venv/bin/activate
python c6-presence-watcher.py --node-id 1 --esp32-ip 192.168.1.20 --privacy-class 2
Output: Writes presence events to /tmp/ruview-state.json.
Terminal 2: Start the sensing server (HTTP polling interface for the HAP bridge)
cd ~/ruview-hap
source venv/bin/activate
python ruview-sensing-server.py --port 3000
Output: Listening on http://127.0.0.1:3000/api/v1/....
Terminal 3: Start the HAP bridge (advertises HomeKit accessory on mDNS)
cd ~/ruview-hap
source venv/bin/activate
python ruview-hap-bridge.py --port 51826 --pin 200-70-910
Output: Look for setup code in the terminal output, e.g., Setup code: 200-70-910.
Once paired, ask Siri:
"Hey Siri, is anyone in the living room?"
Siri will respond with the current occupancy state. Walk past the C6 and ask again — the presence value should update within 1–2 seconds.
To monitor multiple rooms, run multiple C6 nodes, each with its own c6-presence-watcher.py instance:
# Terminal: Room 1 (Living Room, node_id=1)
python c6-presence-watcher.py --node-id 1 --esp32-ip 192.168.1.20 \
--output /tmp/ruview-state.living-room.json
# Terminal: Room 2 (Bedroom, node_id=2)
python c6-presence-watcher.py --node-id 2 --esp32-ip 192.168.1.21 \
--output /tmp/ruview-state.bedroom.json
# Terminal: HAP bridge (auto-discovers both state files)
python ruview-hap-bridge.py --port 51826 --rooms "Living Room,Bedroom"
The HAP bridge auto-discovers *.json files in /tmp/ruview-state* and creates a child HomeKit accessory per room. Each room appears separately in the Home app and can be assigned to its physical location.
RuView's BFLD (Beamforming Feedback Layer for Detection) uses a privacy class gate that enforces what data can cross the HomeKit boundary. Only Classes 2 and 3 (Anonymous and Restricted) are eligible; Class 0/1 (Raw identity information) is never exposed.
HomeKit exposes thresholded events, not raw probabilities:
| Event | HomeKit Characteristic | Meaning | Example Automation |
|---|---|---|---|
| Unknown Presence | MotionSensor (stateful) | Person detected + no matching identity record for >30s | "Turn on porch light when Unknown Presence detected after 9pm" |
| Unexpected Occupancy | OccupancySensor | Occupancy outside the operator's defined schedule | "Send notification if office is occupied on weekends" |
| Unrecognized Activity Pattern | ProgrammableSwitch (momentary) | Activity drift or recalibration gate fires | "Run a re-learning sequence when activity changes" |
The following are never exposed to HomeKit:
identity_risk_score (numeric 0–1 confidence) — only thresholded semantic events cross the boundaryrf_signature_hash — cryptographic internal stateThis enforces ADR-125 §2.1.d invariant I1: raw identity information never exits the node. The semantic framing is intentional — "Unknown Presence" reads as who's-here-and-it's-fine-but-worth-noting, not as an accusation.
For the technical definition, see ADR-118 — Beamforming Feedback Layer for Detection.
Name each HomeKit accessory after its room. The HAP bridge pulls room names from the state file prefixes:
python c6-presence-watcher.py --node-id 1 \
--output /tmp/ruview-state.LIVING_ROOM.json
# HAP bridge sees this and names the accessory "Living Room"
When paired in the Home app, Siri knows the room:
| Query | Result |
|---|---|
| "Is anyone in the living room?" | Queries the Living Room accessory's motion sensor |
| "Is anyone home?" | Queries all room accessories; returns true if any motion is detected |
| "Turn on the bedroom lights when occupancy is detected" | Automation triggers on the Bedroom accessory only |
Each room also exposes a StatelessProgrammableSwitch that fires on semantic-event boundaries (Unrecognized Activity Pattern, Recalibration, etc.). This is the HomeKit primitive for momentary triggers:
The easiest way to announce RuView events on a HomePod is through Shortcuts-as-glue — a native macOS launchd job that watches RuView's semantic events and triggers a Shortcut you define.
This path bypasses the Bonjour reflector blocker that can prevent HomePod discovery in some mesh networks. Instead of direct mDNS, the Mac uses the Home graph (iCloud-paired) to reach the HomePod.
RuView Announce (exact name).osascript -e 'tell application "Shortcuts Events" to run shortcut "RuView Announce" with input "Test from RuView"'
Your HomePod should speak "Test from RuView" in your chosen voice.
Copy the launchd plist from the repository:
cp scripts/macos-shortcuts/ruview-watcher.plist \
~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist
launchctl load ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist
launchctl list | grep ruvnet # Confirm it's loaded
Tail the log in one terminal:
tail -f /tmp/ruview-watcher.log
In another terminal, walk past the C6 and trigger a presence detection. The log should show:
[17:10:12] unknown_presence rising-edge → running 'RuView Announce'
And your HomePod should announce the event in its configured voice.
To announce different events in different rooms, create multiple Shortcuts in Shortcuts.app:
RuView Announce KitchenRuView Announce BedroomThen run multiple watcher jobs with different --shortcut-name flags:
# Kitchen events on HomePod mini in kitchen
scripts/macos-shortcuts/announce-via-homepod.sh \
--node-id 1 --event unknown_presence \
--shortcut-name "RuView Announce Kitchen" \
--poll-interval 2 &
# Bedroom events on HomePod in bedroom
scripts/macos-shortcuts/announce-via-homepod.sh \
--node-id 2 --event unknown_presence \
--shortcut-name "RuView Announce Bedroom" \
--poll-interval 2 &
Because the Shortcut is operator-editable in Shortcuts.app, you can extend it to do anything:
This is the flexibility of the Shortcuts-as-glue approach — no code change needed in RuView, all customization in the operator's own Shortcuts library.
For complete setup details and troubleshooting, see scripts/macos-shortcuts/README.md.
RuView's sensing stream is also available through Model Context Protocol (MCP) — the standard interface for Claude Code and other AI agents to query RuView data.
@ruvnet/rvagent npm package (v0.1.0)The package exposes 12 MCP tools that let Claude Code agents:
In your Claude Code project:
npm install -D @ruvnet/[email protected]
# Or, add via MCP:
claude mcp add rvagent -- npx -y @ruvnet/[email protected]
Then in your Claude Code chat:
/claude-flow-help # Lists all available MCP tools
| Tool | Input | Output |
|---|---|---|
ruview_csi_latest | node_id | Latest CSI window (1024 subcarriers, 30 OFDM symbols) |
ruview_pose_infer | CSI window | 17-keypoint skeleton (x, y, confidence per joint) |
ruview_count_infer | CSI window | Person count + 95% CI |
ruview_registry_list | query (optional) | List of 105+ available edge modules |
ruview_train_count | epochs, learning_rate | Kickoff training job ID |
ruview_job_status | job_id | Progress, ETA, current loss |
ruview.bfld.last_scan | node_id | Latest BFLD scan: privacy_class, person_count (identity_risk_score=null per I1 invariant) |
ruview.bfld.subscribe | node_id, event_filter | Stream BFLD windows until you close the stream |
ruview.presence.now | room (optional) | Current occupancy per room |
ruview.vitals.get_breathing | node_id | Breathing rate (BPM) + confidence |
ruview.vitals.get_heart_rate | node_id | Heart rate (BPM) + confidence |
ruview.vitals.get_all | node_id | Breathing + heart rate + metadata |
# Claude-flow agent pseudocode
import claude_code
tools = claude_code.mcp_tools("rvagent")
# Query latest presence
presence = tools["ruview.presence.now"](room="living room")
print(f"Living room occupancy: {presence.occupancy}") # True/False
# Check vitals
vitals = tools["ruview.vitals.get_all"](node_id=1)
print(f"Breathing: {vitals.breathing_bpm} BPM")
# Stream BFLD events in real-time
for event in tools["ruview.bfld.subscribe"](node_id=1, event_filter="unknown_presence"):
print(f"Unknown presence detected: privacy_class={event.privacy_class}")
For the full MCP specification, see ADR-124 — rvagent MCP / RuVector npm integration.
dns-sd -B _airplay._tcp local. from the MacLikely cause: HomePod and Mac are on different subnets despite being on the same SSID. Some mesh networks segment 2.4 GHz and 5 GHz bands onto different /24 subnets, or place guest devices on a separate VLAN.
Check:
192.168.1.x).192.168.1.x vs 192.168.100.x), enable IGMP Proxying in your router settings (common on Netgear Nighthawk). If available, enable Bonjour Repeater or mDNS Reflector instead.Note: The Shortcuts-as-glue path (Tier 2) doesn't need this fix — it routes announcements through the iCloud Home graph, not mDNS.
Likely cause: The HAP bridge's pairing state is corrupt or out of sync with mDNS.
Fix:
rm -rf ~/.ruview-hap-prod/accessory.state
Expected behavior. HAP-python regenerates the setup code if the pairing persist file is missing or corrupt. Once you've paired successfully, the pairing key is stored separately in ~/.ruview-hap-prod/ and survives restarts — the setup code itself is transient and only matters during initial pairing.
If you lose the setup code before pairing, simply delete the state and restart to get a new one.
Likely cause: The HTTP polling loop in ruview-sensing-server.py is blocked, or the C6 is not sending UDP packets.
Check:
ping 192.168.1.20.nc -u -l 5005 & # Listen on UDP 5005
# You should see occasional packets from the C6
curl http://127.0.0.1:3000/api/v1/vitals/latest
These items are intentionally deferred or beyond the current release:
| Item | Status | Timeline |
|---|---|---|
| Matter Protocol (P3) | Deferred | Waiting for matter-rs SDK stabilization; HAP-1.1 covers 95% of the UX today |
| Rust-native HAP (P2) | Planned | Replaces Python HAP-python sidecar; expected after operator feedback from 5+ real pairings |
| PyO3 BFLD wheel deployment (ADR-117 P5) | Pending | Runtime import flip so Python scripts use the Rust BFLD crate; source-built (✅ cargo check green) but wheel not yet published |
| Custom characteristic UUIDs for Eve.app (Iter 8 runtime) | Scaffolded | Design complete; awaiting HAP-python JSON-loader implementation (small follow-up PR) |
| AirPlay 2 voice synthesis (pyatv) | Network-pending | Requires HomePod visible on Bonjour from the Mac; Shortcuts-as-glue (Tier 2) is the working alternative |