unifi-wifi: validate connector RF analysis vs UOS (Cascades) - macs[] fix + --site passthrough

Validated the cloud-connector analysis against a KNOWN entity (Cascades, normally UOS-Mongo).
The connector reaches the self-hosted "UOS Server" host; Cascades is its site `va6iba3v`.

Two fixes from the validation:
- rf-analyze.py: pass macs:[<all uap macs>] to /stat/report/*.ap. The UniFi report endpoint
  returns only a small DEFAULT subset otherwise -- Cascades came back as 10 of 77 APs until the
  MAC list was supplied. Now profiles all 75 (uaps with 2.4 radios), matching the UOS path.
- model-rank.sh / optimize-radios.sh: --console now accepts --site <name> (internal short name
  from /api/self/sites) for multi-site controllers like the UOS Server (Cascades = va6iba3v).

Result lines up with the known UOS-Mongo figures: 75 APs, 2.4GHz util 65-90% / interf 53-78% /
~1 client each, all power-down, 0 disables (roam graph absent via connector -> same coverage-safe
degradation; disables still need NEIGHBOR_JSON). Apples-to-apples confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 08:53:36 -07:00
parent 1a90a48c82
commit dccd381820
4 changed files with 32 additions and 5 deletions

View File

@@ -84,6 +84,19 @@ Roam graph is usually EMPTY on small/stationary sites (no `/stat/event` roam log
ranks by airtime pressure only; optimize-radios returns power-down candidates and 0 disables
(coverage-safe). For disables, supply `NEIGHBOR_JSON` (AP-to-AP SNR from neighbor-collect) as on UOS.
**Multi-site controllers** (e.g. the self-hosted "UOS Server" host runs 47 sites): pass `--site <name>`
(the internal short name from `/api/self/sites`, e.g. Cascades = `va6iba3v`). Default is `default`.
```bash
bash .../model-rank.sh --console "UOS Server" --site va6iba3v 7 ng # Cascades 2.4GHz
```
**Gotcha (handled in rf-analyze.py):** the internal `/stat/report/*.ap` endpoint returns only a
small DEFAULT subset of APs unless you POST `macs:[<all uap macs>]` (Cascades returned 10 of 77
until the MAC list was supplied). The analyzer fetches the uap macs from `/stat/device` and passes them.
**Validation (2026-06-17):** Cascades via the connector matched the UOS-Mongo path - 75 APs profiled,
2.4GHz util 65-90% / interf 53-78% / ~1 client each, all power-down, 0 disables. Apples-to-apples (same
self-hosted controller + site). Connector lacks the roam graph, so disables need `NEIGHBOR_JSON`.
## Gotchas
- Direct WAN SSH/HTTPS to a standalone UDM is usually firewalled (Brooklyn 67.1.139.219:
22/443/8443 filtered) - the connector is the remote path, not direct.

View File

@@ -14,11 +14,17 @@ REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
# Cloud connector path (non-UOS console): `model-rank.sh --console "<name>" [days] [band]` routes to
# rf-analyze.py (Site Manager API; see references/site-manager-api.md). The UOS Mongo path below is unchanged.
if [ "${1:-}" = "--console" ]; then
CONSOLE="${2:?--console needs a console name}"; CDAYS="${3:-7}"; CBAND="${4:-ng}"
shift; CONSOLE=""; CSITE="default"; CPOS=()
while [ $# -gt 0 ]; do case "$1" in
--site) CSITE="${2:-default}"; shift 2 2>/dev/null || shift;;
*) if [ -z "$CONSOLE" ]; then CONSOLE="$1"; else CPOS+=("$1"); fi; shift;;
esac; done
[ -n "$CONSOLE" ] || { echo "usage: model-rank.sh --console <name> [--site <s>] [days] [band]"; exit 2; }
CDAYS="${CPOS[0]:-7}"; CBAND="${CPOS[1]:-ng}"
CKEY="$(bash "$REPO/.claude/scripts/vault.sh" get-field services/unifi-site-manager credentials.api_key 2>/dev/null | tr -d '\r\n')"
[ -n "$CKEY" ] || { echo "[ERROR] no UniFi Site Manager key (vault services/unifi-site-manager)"; exit 1; }
CPY="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v python3)"
exec env UNIFI_SM_KEY="$CKEY" "$CPY" "$REPO/.claude/skills/unifi-wifi/scripts/rf-analyze.py" rank --console "$CONSOLE" --days "$CDAYS" --band "$CBAND"
exec env UNIFI_SM_KEY="$CKEY" "$CPY" "$REPO/.claude/skills/unifi-wifi/scripts/rf-analyze.py" rank --console "$CONSOLE" --days "$CDAYS" --band "$CBAND" --site "$CSITE"
fi
UOS="$REPO/.claude/scripts/uos-mongo.sh"
arg="${1:?usage: model-rank.sh <site> [days] [band] | model-rank.sh --console <name> [days] [band]}"; DAYS="${2:-7}"; BAND="${3:-ng}"

View File

@@ -24,11 +24,17 @@ REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
# rf-analyze.py (Site Manager API; see references/site-manager-api.md). UOS Mongo path below is unchanged.
# NEIGHBOR_JSON / ROAM_MIN / CAP / ZONE_DISABLE_PCT / REDUN_* env vars pass through to the analyzer.
if [ "${1:-}" = "--console" ]; then
CONSOLE="${2:?--console needs a console name}"; CDAYS="${3:-14}"; CBAND="${4:-ng}"
shift; CONSOLE=""; CSITE="default"; CPOS=()
while [ $# -gt 0 ]; do case "$1" in
--site) CSITE="${2:-default}"; shift 2 2>/dev/null || shift;;
*) if [ -z "$CONSOLE" ]; then CONSOLE="$1"; else CPOS+=("$1"); fi; shift;;
esac; done
[ -n "$CONSOLE" ] || { echo "usage: optimize-radios.sh --console <name> [--site <s>] [days] [band]"; exit 2; }
CDAYS="${CPOS[0]:-14}"; CBAND="${CPOS[1]:-ng}"
CKEY="$(bash "$REPO/.claude/scripts/vault.sh" get-field services/unifi-site-manager credentials.api_key 2>/dev/null | tr -d '\r\n')"
[ -n "$CKEY" ] || { echo "[ERROR] no UniFi Site Manager key (vault services/unifi-site-manager)"; exit 1; }
CPY="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v python3)"
exec env UNIFI_SM_KEY="$CKEY" "$CPY" "$REPO/.claude/skills/unifi-wifi/scripts/rf-analyze.py" optimize --console "$CONSOLE" --days "$CDAYS" --band "$CBAND"
exec env UNIFI_SM_KEY="$CKEY" "$CPY" "$REPO/.claude/skills/unifi-wifi/scripts/rf-analyze.py" optimize --console "$CONSOLE" --days "$CDAYS" --band "$CBAND" --site "$CSITE"
fi
UOS="$REPO/.claude/scripts/uos-mongo.sh"
arg="${1:?usage: optimize-radios.sh <site> [days] [band] | optimize-radios.sh --console <name> [days] [band]}"; DAYS="${2:-14}"; BAND="${3:-ng}"

View File

@@ -71,7 +71,9 @@ import time as _t
end = int(_t.time() * 1000); start = end - DAYS * 86400000
attrs = ['time', 'ap'] + ['%s-%s' % (BAND, f) for f in
('cu_total', 'cu_self_rx', 'cu_self_tx', 'cu_interf', 'num_sta', 'tx_retries', 'wifi_tx_attempts', 'satisfaction')]
rep = api(CB + '/stat/report/hourly.ap', 'POST', {'attrs': attrs, 'start': start, 'end': end})
# IMPORTANT: pass macs[] explicitly -- the UniFi report endpoint returns only a small default
# subset of APs otherwise (Cascades returned 10 of 77 until the MAC list was supplied).
rep = api(CB + '/stat/report/hourly.ap', 'POST', {'attrs': attrs, 'start': start, 'end': end, 'macs': list(name.keys())})
rows = (rep or {}).get('data', [])
prof = {}
for d in rows: