Files
claudetools/.claude/skills/unifi-wifi/scripts/watch-ap.sh
Howard Enos 60b763df0c sync: auto-sync from HOWARD-HOME at 2026-06-16 00:40:03
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-16 00:40:03
2026-06-16 00:40:12 -07:00

61 lines
4.0 KiB
Bash

#!/usr/bin/env bash
# watch-ap.sh — live, low-latency RF watch of a UniFi AP straight from the AP (no controller lag).
# SSHes into the AP and streams, per radio, every INTERVAL seconds: channel utilization + clients +
# tx-retries (from mca-dump) and channel busy% + noise floor (from `iw survey`, diffed). Ideal for
# watching an AP react to a targeted change (power/channel/neighbor-disable) in seconds.
#
# REQUIRES: L3 reach to the AP's mgmt IP. At Cascades the APs are on 192.168.2.x/3.x (mgmt VLANs) —
# bring up the Cascades VPN first. Device-auth SSH cred is vaulted (clients/cascades-tucson/unifi-ap-ssh).
# AUTH (UniFi device-auth is password-based): uses `sshpass` if installed, otherwise falls back to
# OpenSSH's SSH_ASKPASS helper (no sshpass needed). NOTE the fallback uses `ssh` from PATH: on Windows
# that must be MSYS/Git-bash ssh — Win10/11 system OpenSSH cannot exec a shell askpass (CreateProcessW
# error 193); on Linux/macOS system ssh works fine. Find AP IPs via:
# echo 'db.device.find({site_id:"685f39068e65331c46ef6dd2",type:"uap"},{name:1,ip:1}).forEach(printjson)' | bash .claude/scripts/uos-mongo.sh
#
# Usage: bash .claude/skills/unifi-wifi/scripts/watch-ap.sh <ap-ip> [interval=2] [vault-path]
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
AP="${1:?usage: watch-ap.sh <ap-ip> [interval] [vault-path]}"; INT="${2:-2}"; VP="${3:-clients/cascades-tucson/unifi-ap-ssh}"
U="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
[ -n "$U" ] && [ -n "$P" ] || { echo "[BLOCKED] no AP device-auth cred at vault:$VP"; echo " Vault this client's cred: bash .claude/skills/vault/scripts/vault-helper.sh new $VP --kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi --set username=<u> --set password=<pw>"; exit 2; }
# Auth method: sshpass if available, else SSH_ASKPASS fallback (no sshpass needed).
SSH_OPTS=(-o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
if command -v sshpass >/dev/null 2>&1; then
run_ssh() { SSHPASS="$P" sshpass -e ssh "${SSH_OPTS[@]}" "$@"; }
echo "[INFO] auth: sshpass"
else
ASKPASS="$(mktemp)"; printf '#!/bin/sh\nprintf "%%s\\n" "$WATCH_AP_PW"\n' > "$ASKPASS"; chmod +x "$ASKPASS"
trap 'rm -f "$ASKPASS"' EXIT
run_ssh() { WATCH_AP_PW="$P" SSH_ASKPASS="$ASKPASS" SSH_ASKPASS_REQUIRE=force DISPLAY="${DISPLAY:-:0}" ssh "${SSH_OPTS[@]}" "$@"; }
echo "[INFO] auth: SSH_ASKPASS fallback (sshpass not installed)"
fi
echo "[INFO] watching $AP every ${INT}s (Ctrl-C to stop). Needs Cascades VPN reach."
# Run the sampling loop ON the AP so each tick is one round-trip; mca-dump for cu/clients, iw survey for busy%/noise.
run_ssh "$U@$AP" "INT=$INT sh -s" <<'REMOTE' 2>&1 | grep -viE 'Warning: Permanently|pq.html'
# physical radios only — exclude virtual APs (wifi0ap0...) and mld-* (hyphen breaks ${VAR} expansion)
radios=$(iw dev 2>/dev/null | awk '/Interface/{print $2}' | grep -E '^(wifi[0-9]+|ath[0-9]+)$' || echo "wifi0 wifi1 wifi2")
prev=""
while :; do
ts=$(date +%H:%M:%S)
# cu_total + num_sta + tx_retries per radio from mca-dump (JSON; parse with grep/sed, no jq on AP)
dump=$(mca-dump 2>/dev/null)
for r in $radios; do
# iw survey: in-use channel active/busy/noise
surv=$(iw dev "$r" survey dump 2>/dev/null | awk '
/\[in use\]/{u=1} u&&/frequency/{f=$2} u&&/noise/{n=$2} u&&/channel active time/{a=$4}
u&&/channel busy time/{b=$4; print f,n,a,b; u=0}')
set -- $surv; freq="${1:-?}"; noise="${2:-?}"; act="${3:-0}"; busy="${4:-0}"
key="$r"; pa=$(eval echo \${ACT_$r:-0}); pb=$(eval echo \${BSY_$r:-0})
da=$((act-pa)); db=$((busy-pb)); pct="?"; [ "$da" -gt 0 ] 2>/dev/null && pct=$((100*db/da))
eval ACT_$r=$act; eval BSY_$r=$busy
echo "$ts $r f=${freq} noise=${noise}dBm busy=${pct}%"
done
sleep "${INT:-2}"
done
REMOTE