# 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](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

```bash
# 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
```
