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:
@@ -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
|
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.
|
(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
|
## Gotchas
|
||||||
- Direct WAN SSH/HTTPS to a standalone UDM is usually firewalled (Brooklyn 67.1.139.219:
|
- 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.
|
22/443/8443 filtered) - the connector is the remote path, not direct.
|
||||||
|
|||||||
@@ -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
|
# 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.
|
# rf-analyze.py (Site Manager API; see references/site-manager-api.md). The UOS Mongo path below is unchanged.
|
||||||
if [ "${1:-}" = "--console" ]; then
|
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')"
|
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; }
|
[ -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)"
|
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
|
fi
|
||||||
UOS="$REPO/.claude/scripts/uos-mongo.sh"
|
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}"
|
arg="${1:?usage: model-rank.sh <site> [days] [band] | model-rank.sh --console <name> [days] [band]}"; DAYS="${2:-7}"; BAND="${3:-ng}"
|
||||||
|
|||||||
@@ -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.
|
# 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.
|
# NEIGHBOR_JSON / ROAM_MIN / CAP / ZONE_DISABLE_PCT / REDUN_* env vars pass through to the analyzer.
|
||||||
if [ "${1:-}" = "--console" ]; then
|
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')"
|
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; }
|
[ -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)"
|
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
|
fi
|
||||||
UOS="$REPO/.claude/scripts/uos-mongo.sh"
|
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}"
|
arg="${1:?usage: optimize-radios.sh <site> [days] [band] | optimize-radios.sh --console <name> [days] [band]}"; DAYS="${2:-14}"; BAND="${3:-ng}"
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ import time as _t
|
|||||||
end = int(_t.time() * 1000); start = end - DAYS * 86400000
|
end = int(_t.time() * 1000); start = end - DAYS * 86400000
|
||||||
attrs = ['time', 'ap'] + ['%s-%s' % (BAND, f) for f in
|
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')]
|
('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', [])
|
rows = (rep or {}).get('data', [])
|
||||||
prof = {}
|
prof = {}
|
||||||
for d in rows:
|
for d in rows:
|
||||||
|
|||||||
Reference in New Issue
Block a user