diff --git a/scripts/macos-shortcuts/README.md b/scripts/macos-shortcuts/README.md new file mode 100644 index 00000000..26aa3c2f --- /dev/null +++ b/scripts/macos-shortcuts/README.md @@ -0,0 +1,96 @@ +# macOS Shortcuts ↔ RuView bridge (ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue") + +This directory ships the small set of glue you drop onto an always-on +Mac (like `ruv-mac-mini`) so RuView's BFLD-gated sensing events can +trigger native Apple Home actions — including HomePod announcements, +scene activations, cross-device notifications, and any third-party +HomeKit accessory the operator has paired. + +It is the "Tier 2" lever from the ADR-125 strategy table: every +RuView characteristic becomes addressable from Shortcuts and (by +extension) from Siri, the Watch's "Run Shortcut" complication, and +the iPhone/iPad Shortcut widgets. + +## Architecture + +``` +real C6 (192.168.1.179, ruv.net) + → UDP feature_state → c6-presence-watcher.py → BFLD PrivacyGate + → /tmp/ruview-last-feature.json + → ruview-sensing-server.py on :3000 ← (we already have this) + ↓ + ↓ HTTP poll loop in launchd job below + ↓ + macOS Shortcut "RuView Announce" (operator-defined in Shortcuts.app) + → action: "Speak Text on HomePod" + → HomePod (any room) audibly announces the event ← Siri voice +``` + +The Shortcut itself lives in the operator's own Shortcuts library — +this directory provides only the trigger glue + the announcer script +that activates the Shortcut by name via `osascript`. + +## One-time setup on the Mac + +1. **Create the Shortcut** in `Shortcuts.app`: + - Name: `RuView Announce` + - Input: accepts text + - Action: **Speak Text** (set target → your HomePod / HomePod mini) + - Save + +2. **Verify it runs from the command line**: + ```sh + osascript -e 'tell application "Shortcuts Events" to run shortcut "RuView Announce" with input "Test from RuView"' + ``` + The HomePod should speak "Test from RuView". + +3. **Install the launchd job**: + ```sh + cp ruview-watcher.plist ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist + launchctl load ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist + ``` + `launchctl list | grep ruvnet` should show the job loaded. + +4. **Tail the log** while you walk past the C6 to verify it fires: + ```sh + tail -f /tmp/ruview-watcher.log + ``` + +## Files + +| File | Purpose | +|------|---------| +| `announce-via-homepod.sh` | Polls `/api/v1/semantic-events//latest`; on rising-edge events, invokes the named Shortcut via `osascript` | +| `ruview-watcher.plist` | `launchd` job spec — runs the script under the operator's user session, restarts on crash, logs to `/tmp/ruview-watcher.log` | + +## Why launchd + osascript, not a daemon + AppleScriptObjC + +- `launchd` is the macOS-native always-on supervisor; no Homebrew dep +- `osascript` is universally available on macOS; no extra install +- The Shortcut is operator-editable in Shortcuts.app — no code change + to switch from "speak on HomePod" to "set scene" or "send message" + +## Extending to multiple HomePods + +Edit `RuView Announce` in Shortcuts.app: +- Add a "Choose from List" action with each HomePod target, OR +- Create per-room Shortcuts (`RuView Announce Kitchen`, + `RuView Announce Bedroom`) and pass the room name into the + script's `--shortcut-name` flag + +The script supports `--shortcut-name ` so multiple watchers can +target different shortcuts per room without changing this code. + +## Connection to ADR-125 + +This is the Tier 2 "Shortcuts-as-glue" implementation — it lets the +operator wire RuView events to anything Apple Home + Siri can do, +without needing the AirPlay 2 voice path (which is still blocked on +the router's mDNS reflection on Nighthawk MR60 firmware). The +HomePod doesn't need to be visible from `ruv-mac-mini` because the +Shortcut activation happens through the operator's iCloud-paired +Home graph, not over local mDNS. + +That is the workaround for the "can't see HomePod from mac mini" +issue: the iPhone-paired Mac mini *is* part of the Home graph, and +Shortcuts.app uses that graph (not Bonjour) to reach the HomePod. diff --git a/scripts/macos-shortcuts/announce-via-homepod.sh b/scripts/macos-shortcuts/announce-via-homepod.sh new file mode 100644 index 00000000..9eba3abb --- /dev/null +++ b/scripts/macos-shortcuts/announce-via-homepod.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# +# announce-via-homepod.sh — ADR-125 §1.4 Tier 2 glue. +# +# Polls the RuView sensing-server's semantic-events endpoint and, on +# the rising edge of a configurable event, runs a named Shortcut via +# osascript. The Shortcut itself is owned by the operator in +# Shortcuts.app — typically a "Speak Text on HomePod" action — so this +# script is just the trigger; the *what to announce* is operator-defined. +# +# Run manually for testing: +# bash announce-via-homepod.sh --node-id 12 --event unrecognized_activity_pattern +# +# Run as a launchd job: see ruview-watcher.plist + README.md. + +set -euo pipefail + +SENSING_URL="${RUVIEW_SENSING_URL:-http://localhost:3000}" +NODE_ID="12" +EVENT="unrecognized_activity_pattern" +SHORTCUT_NAME="RuView Announce" +ANNOUNCEMENT="" +POLL_INTERVAL="5" +LOG_FILE="${RUVIEW_LOG:-/tmp/ruview-watcher.log}" + +usage() { + cat >&2 < Sensing-server node id (default: 12) + --event Event to watch — one of: + unknown_presence + unexpected_occupancy + unrecognized_activity_pattern + (default: unrecognized_activity_pattern) + --shortcut-name Shortcut to invoke (default: "RuView Announce") + --announcement Text to speak when event fires (default: event name) + --sensing-url Sensing-server base URL (default: http://localhost:3000) + --poll-interval Poll interval in seconds (default: 5) + --once Single poll + exit (for testing) + -h, --help Show this help +EOF +} + +ONCE=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --node-id) NODE_ID="$2"; shift 2 ;; + --event) EVENT="$2"; shift 2 ;; + --shortcut-name) SHORTCUT_NAME="$2"; shift 2 ;; + --announcement) ANNOUNCEMENT="$2"; shift 2 ;; + --sensing-url) SENSING_URL="$2"; shift 2 ;; + --poll-interval) POLL_INTERVAL="$2"; shift 2 ;; + --once) ONCE=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +ANNOUNCEMENT="${ANNOUNCEMENT:-$(echo "$EVENT" | tr '_' ' ')}" + +run_shortcut() { + local text="$1" + if ! command -v osascript >/dev/null 2>&1; then + echo "[$(date '+%H:%M:%S')] ERROR: osascript not found — macOS-only" >> "$LOG_FILE" + return 1 + fi + # `Shortcuts Events` is the scriptable surface for Shortcuts.app. + # Passing input via `with input "..."` requires the Shortcut to + # have a "Receive Text input" trigger. + osascript <> "$LOG_FILE" 2>&1 +tell application "Shortcuts Events" + run shortcut "$SHORTCUT_NAME" with input "$text" +end tell +EOF +} + +read_event_active() { + # Returns "true" or "false" from the semantic-events endpoint. + local node_id="$1" event="$2" + curl -fsS --max-time 3 \ + "$SENSING_URL/api/v1/semantic-events/$node_id/latest" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); \ +print(str(d.get('events',{}).get('$event',{}).get('active', False)).lower())" \ + 2>/dev/null || echo "unknown" +} + +last_state="unknown" +echo "[$(date '+%H:%M:%S')] start: node=$NODE_ID event=$EVENT shortcut=\"$SHORTCUT_NAME\"" \ + >> "$LOG_FILE" + +while true; do + current="$(read_event_active "$NODE_ID" "$EVENT")" + if [[ "$current" != "$last_state" && "$current" == "true" ]]; then + echo "[$(date '+%H:%M:%S')] $EVENT rising-edge → running '$SHORTCUT_NAME'" \ + >> "$LOG_FILE" + run_shortcut "$ANNOUNCEMENT" || \ + echo "[$(date '+%H:%M:%S')] shortcut invocation failed" >> "$LOG_FILE" + fi + last_state="$current" + [[ "$ONCE" == "1" ]] && break + sleep "$POLL_INTERVAL" +done diff --git a/scripts/macos-shortcuts/ruview-watcher.plist b/scripts/macos-shortcuts/ruview-watcher.plist new file mode 100644 index 00000000..1169ef83 --- /dev/null +++ b/scripts/macos-shortcuts/ruview-watcher.plist @@ -0,0 +1,75 @@ + + + + + + Label + com.ruvnet.ruview.watcher + + ProgramArguments + + /bin/bash + + /Users/cohen/announce-via-homepod.sh + --node-id + 12 + --event + unrecognized_activity_pattern + --shortcut-name + RuView Announce + --announcement + RuView detected an unrecognized activity pattern + --poll-interval + 5 + + + EnvironmentVariables + + RUVIEW_SENSING_URL + http://localhost:3000 + RUVIEW_LOG + /tmp/ruview-watcher.log + PATH + /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin + + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + StandardOutPath + /tmp/ruview-watcher.stdout + + StandardErrorPath + /tmp/ruview-watcher.stderr + + ProcessType + Background + +