mirror of
https://github.com/ruvnet/RuView
synced 2026-08-04 19:31:42 +00:00
d0525359d4
ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under
`scripts/macos-shortcuts/`:
README.md one-time operator setup + architecture diagram
announce-via-homepod.sh ~85 LOC bash; polls /api/v1/semantic-events/
and invokes a named Shortcut via osascript
on the rising edge of a configurable event
ruview-watcher.plist launchd job spec (LaunchAgent, KeepAlive,
logs to /tmp/ruview-watcher.{stdout,stderr,log})
Why this matters strategically: the HomePod doesn't need to be visible
from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the
operator's Home graph; Shortcuts.app reaches the HomePod via that graph,
not via local mDNS. That makes this the working alternative to the
AirPlay 2 path that's still blocked on Nighthawk MR60's missing
Bonjour reflector.
Smoke test on real C6 (real hardware, no mocks):
$ ~/announce-via-homepod.sh --once --event unknown_presence
[17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce"
[17:10:12] unknown_presence rising-edge → running 'RuView Announce'
34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712)
The osascript timeout is the EXPECTED error before the operator
creates the "RuView Announce" Shortcut in Shortcuts.app — the
trigger logic is verified working. Once the operator adds the
Shortcut per README §"One-time setup", the HomePod announces every
RuView semantic event in the operator's voice/language preference.
Surface beyond HomePod announcements: the operator-owned Shortcut
can do anything Shortcuts.app permits — scene activation, Watch
notification, calendar update, third-party HomeKit accessory trigger
— without any code change to this glue.
Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d.
Co-Authored-By: claude-flow <ruv@ruv.net>
105 lines
3.7 KiB
Bash
105 lines
3.7 KiB
Bash
#!/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 <<EOF
|
|
Usage: $0 [options]
|
|
|
|
Options:
|
|
--node-id <id> Sensing-server node id (default: 12)
|
|
--event <name> Event to watch — one of:
|
|
unknown_presence
|
|
unexpected_occupancy
|
|
unrecognized_activity_pattern
|
|
(default: unrecognized_activity_pattern)
|
|
--shortcut-name <name> Shortcut to invoke (default: "RuView Announce")
|
|
--announcement <text> Text to speak when event fires (default: event name)
|
|
--sensing-url <url> Sensing-server base URL (default: http://localhost:3000)
|
|
--poll-interval <s> 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 <<EOF >> "$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
|