Files
claudetools/.claude/skills/unifi-wifi/references/data-access.md
Mike Swanson 00de88fd38 sync: auto-sync from GURU-5070 at 2026-06-15 18:09:05
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-15 18:09:05
2026-06-15 18:09:29 -07:00

6.5 KiB

UniFi WiFi data access — what the UOS controller exposes and how to read it

This is the data-capability map for the unifi-wifi tuning skill, built from live recon of the UOS Server (172.16.3.29, self-hosted UniFi OS / classic Network app, Mongo DB ace) using the Cascades site (site_id 685f39068e65331c46ef6dd2) as the hard case (77 APs / 12 switches, ~550 concurrent wireless clients, severe 2.4GHz neighbor congestion). Access: infrastructure/uos-server-ssh-key (vaulted) + .claude/scripts/uos-mongo.sh. See also uos-server.

Two data planes (the key finding)

Plane Source Reach Holds
Config Mongo ace via uos-mongo.sh root SSH, available now radio config, foreign-interference map (rogue), channel-plan, device/floorplan
History Mongo ace_stat (db.getSiblingDB('ace_stat')) root SSH, available now per-AP/per-band airtime time-series (<band>-cu_total/cu_interf/num_sta, satisfaction, retries) in stat_hourly/stat_daily; the roam graph in wifi_connectivity_event; per-client history
Live (optional) Controller Network API (stat/device, stat/sta) needs a session / integration key — not wired current utilization + the live RF-neighbor table; nice for before/after validation

The accumulated history plane (ace_stat) is the key source for the interference / airtime model — it already holds what the UniFi UI shows. See interference-model.md.

The live per-AP utilization and per-client RF stats are NOT persisted in Mongo (the device collection carries config but no radio_table_stats; the user/client collection only keeps last_radio). So a first-pass audit + channel/interference plan comes entirely from Mongo; the live "current airtime / who's unhappy right now" feedback loop needs the local Network API (see "Live-stats gap" below).

Plane 1 — Mongo ace (available now)

device collection (per AP/switch; filter type:'uap' for APs)

Per-AP radio_table[] (the config we tune), one entry per radio:

  • radio: ng = 2.4GHz, na = 5GHz, 6e = 6GHz
  • channel, ht (width: 20/40/80/160), tx_power_mode (auto/low/medium/high/custom)
  • min_rssi_enabled, min_rssi (sticky-client / roaming floor, e.g. -77)
  • plus atf_enabled (airtime fairness), country_code, antenna_table, scan_radio_table, support_wifi6e, wifi_caps, model/firmware, num_sta is present at the device level (last reported).

Cascades is a U7-Pro / WiFi-6E fleet (tri-band ng/na/6e). Example AP: 2.4 ch11/20MHz, 5GHz ch153/80MHz, 6GHz ch145/160MHz, min_rssi -77 enabled.

rogue collection — the interference map (586,688 docs fleet-wide; the gold)

Every neighbor/over-the-air BSSID an AP has seen, with band, channel, ap_mac (which of our APs saw it), bssid, essid, rssi, age, site_id. Aggregate by channel to quantify co-channel congestion. Cascades 2.4GHz is brutal:

Band Channel Neighbor BSSIDs seen
2.4 (ng) 6 33,359
2.4 (ng) 1 19,275
2.4 (ng) 11 16,578
5 (na) 149 8,889
5 (na) 157 6,964
5 (na) 44 5,477
6 (6e) 69 86

Takeaways the skill uses: 2.4GHz is saturated on all three usable channels (so the fix is fewer 2.4 radios + tight power, not "find a clean channel"); 6GHz is nearly empty (steer capable clients up); 5GHz upper band (149/157) is busier than the UNII-1/DFS lower band.

channelplan collection

The controller's auto-channel-plan inputs/outputs per site: channels_ng/channels_na/channels_6e (allowed channel lists), ht_modes_ng/na/6e, method, optimize, exclude_devices, high_priority_devices, date. Lets the skill read/propose the channel plan the controller will honor.

user collection — client inventory/history

~1,807 client records for Cascades. Holds identity + last_radio (band) but not live RF; use for inventory/segmentation, not live signal.

Config-audit signals already computable from Mongo

  • 2.4GHz width != 20MHz (40MHz on 2.4 in density = self-inflicted overlap).
  • min_rssi_enabled=false on 2.4 radios → sticky-client risk. (Cascades: 6 of 77 APs have 2.4 min_rssi disabled.)
  • 2.4 channels not on the 1/6/11 plan; adjacent APs on the same channel.
  • TX-power mode (auto/high) on 2.4 in dense clusters (should be low/medium).
  • Per-AP radio enable: which dense-cluster APs should have their 2.4 radio disabled entirely.

Plane 2 — Live RF/airtime (the gap to wire next)

Live data the tuner ideally also wants (current utilization, satisfaction, per-client RSSI/SNR/ retries, roam events) lives in the classic Network API, session-authenticated:

  • GET /proxy/network/api/s/<site_short>/stat/device → per-AP radio_table_stats[]: cu_total (channel utilization %), cu_self_rx/cu_self_tx (our own airtime), num_sta, tx_retries, satisfaction, per radio.
  • GET /proxy/network/api/s/<site_short>/stat/sta → per-client: rssi, signal, noise, tx_rate/rx_rate, tx_retries, satisfaction, radio/channel, nss, anomalies.

Auth options (none wired yet): (a) a dedicated read-only local UniFi admin → login for a session cookie; (b) a Network integration API key (X-API-KEY vs /proxy/network/integration/v1/...). The cloud Site Manager key does NOT authenticate the local API (401); the existing local "Claude" integration key's value is hashed/unrecoverable. Action for phase 2: create a dedicated read-only admin or integration key on .29, vault it (infrastructure/uos-server-network-api), and read live stats from it. Until then the skill runs config+interference analysis (Plane 1), which already covers the 2.4GHz "first problem".

Quick recipes

# Cascades site id
bash .claude/scripts/uos-mongo.sh --sites | grep -i casc      # 685f39068e65331c46ef6dd2

# 2.4GHz neighbor congestion per channel for a site (the interference map)
cat <<'JS' | bash .claude/scripts/uos-mongo.sh
db.rogue.aggregate([{$match:{site_id:'685f39068e65331c46ef6dd2',band:'ng'}},
 {$group:{_id:'$channel',neighbors:{$sum:1}}},{$sort:{neighbors:-1}}]).forEach(printjson)
JS

# Per-AP 2.4GHz config audit (channel, width, power, min_rssi)
cat <<'JS' | bash .claude/scripts/uos-mongo.sh
db.device.find({site_id:'685f39068e65331c46ef6dd2',type:'uap'},{name:1,radio_table:1}).forEach(function(a){
  (a.radio_table||[]).forEach(function(r){ if(r.radio=='ng')
    print(a.name+"  ch="+r.channel+" ht="+r.ht+" pwr="+r.tx_power_mode+" min_rssi="+(r.min_rssi_enabled?r.min_rssi:'OFF')); });
});
JS