sync: auto-sync from HOWARD-HOME at 2026-06-15 20:40:48

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-15 20:40:48
This commit is contained in:
2026-06-15 20:40:57 -07:00
parent f341ee9398
commit c99615df7e
2 changed files with 73 additions and 15 deletions

View File

@@ -1,8 +1,12 @@
#!/usr/bin/env bash
# live-stats.sh — Plane-2 live RF/airtime from the UOS Network API (classic session API).
# Gives CURRENT per-AP per-radio cu_total / cu_self / num_sta / satisfaction / tx_retries and the
# AP RF-neighbor table — for before/after validation of changes and (the neighbor table) the
# materials-aware AP-to-AP coverage graph that unlocks confident radio DISABLES.
# Gives CURRENT per-AP per-radio cu_total / cu_self / num_sta / retry% and device-level
# satisfaction, plus the worst-clients view (signal / retry% / satisfaction_reason) — for
# before/after validation of changes.
# NOTE: satisfaction is populated at the DEVICE level (per-radio is -1 on this controller),
# and tx_retries is a cumulative counter so we report radio_table_stats.tx_retries_pct (a rate).
# TODO: AP-to-AP RF-neighbor table (for confident radio DISABLES) — not a single API field;
# build it from the `rogue` collection by matching our own APs' vap_table BSSIDs. Not done yet.
#
# AUTH (provision once): the classic API needs a controller admin session. Create a dedicated
# READ-ONLY admin in the UniFi UI (OS Settings -> Admins -> add a Viewer), then vault it:
@@ -52,21 +56,23 @@ echo "[INFO] site short=$SHORT"
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/device" | python -c "
import sys,json
for d in json.load(sys.stdin).get('data',[]):
if d.get('type')!='uap': continue
print('AP',d.get('name'),'clients=',d.get('num_sta'))
aps=[d for d in json.load(sys.stdin).get('data',[]) if d.get('type')=='uap']
print('# APs reporting:',len(aps))
for d in sorted(aps,key=lambda a:str(a.get('name'))):
# device-level satisfaction is the populated one (per-radio satisfaction is -1 on this controller)
print('AP',d.get('name'),'clients=',d.get('num_sta'),'satisfaction=',d.get('satisfaction'))
for r in d.get('radio_table_stats',[]):
print(' ',r.get('radio'),'ch',r.get('channel'),'cu_total',r.get('cu_total'),'cu_self_rx',r.get('cu_self_rx'),'cu_self_tx',r.get('cu_self_tx'),'num_sta',r.get('num_sta'),'tx_retries',r.get('tx_retries'),'satisfaction',r.get('satisfaction'))
# RF neighbor table (materials-aware AP-to-AP visibility) if present
for n in (d.get('radio_table') or []):
pass
" 2>&1 | head -60
print(' ',r.get('radio'),'ch',r.get('channel'),'cu_total',r.get('cu_total'),'cu_self_rx',r.get('cu_self_rx'),'cu_self_tx',r.get('cu_self_tx'),'num_sta',r.get('num_sta'),'retry%',r.get('tx_retries_pct'))
" 2>&1
if [ "$WANT_CLIENTS" = "--clients" ]; then
echo "=== clients (rssi/rate/retries) ==="
echo "=== worst wireless clients by satisfaction (signal / retry% / why) ==="
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/sta" | python -c "
import sys,json
for c in json.load(sys.stdin).get('data',[])[:40]:
print(' ',c.get('hostname') or c.get('mac'),'ap',c.get('ap_mac'),'rssi',c.get('rssi'),'signal',c.get('signal'),'tx_rate',c.get('tx_rate'),'retries',c.get('tx_retries'),'sat',c.get('satisfaction'))
" 2>&1 | head -45
cs=[c for c in json.load(sys.stdin).get('data',[]) if not c.get('is_wired')]
cs.sort(key=lambda c:(c.get('satisfaction') if isinstance(c.get('satisfaction'),(int,float)) else 999))
print('# wireless clients:',len(cs),' (worst 40 by satisfaction)')
for c in cs[:40]:
print(' ',(c.get('hostname') or c.get('mac')),'sat',c.get('satisfaction'),'signal',c.get('signal'),'noise',c.get('noise'),'retry%',c.get('wifi_tx_retries_percentage'),'band',c.get('radio'),'ch',c.get('channel'),'why',c.get('satisfaction_reason'))
" 2>&1
fi