Files
claudetools/.claude/skills/unifi-wifi/scripts/model-rank.sh
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

64 lines
4.0 KiB
Bash

#!/usr/bin/env bash
# model-rank.sh — rank AP radios as airtime-reduction candidates from ACCUMULATED history.
# Data (all from ace_stat, already collected by the controller — no new collector needed):
# - airtime/interference: stat_hourly (o:'ap') <band>-cu_total, <band>-cu_interf, <band>-num_sta
# - coverage overlap: wifi_connectivity_event (client roams between AP pairs)
# Per band (ng/na/6e). A radio is a strong DISABLE candidate when it carries high interference
# airtime AND its clients heavily roam to other APs (redundant coverage); a POWER-DOWN candidate
# when busy/interfering but with less roam redundancy. This is a v1 ranker, not the final greedy
# optimizer — see references/interference-model.md.
#
# Usage: bash .claude/skills/unifi-wifi/scripts/model-rank.sh <site-name|site_id> [days=7] [band=ng|na|6e|all]
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
UOS="$REPO/.claude/scripts/uos-mongo.sh"
arg="${1:?usage: model-rank.sh <site> [days] [band]}"; DAYS="${2:-7}"; BAND="${3:-ng}"
if [[ "$arg" =~ ^[0-9a-f]{24}$ ]]; then SITE="$arg"; else
SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$arg" | awk '{print $1}' | head -1)"
[ -n "$SITE" ] || { echo "[ERROR] no site matching '$arg'"; exit 1; }
fi
echo "[INFO] site=$SITE window=${DAYS}d band=$BAND"
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
var SITE='$SITE', DAYS=$DAYS, BAND='$BAND';
var ace=db.getSiblingDB('ace'), st=db.getSiblingDB('ace_stat');
var since = new Date().getTime() - DAYS*86400000;
var bands = (BAND=='all') ? ['ng','na','6e'] : [BAND];
// ap mac -> name
var name={}; ace.device.find({site_id:SITE,type:'uap'},{mac:1,name:1}).forEach(function(a){name[a.mac]=a.name||a.mac;});
// roam volume per AP (coverage-overlap proxy: high roam = redundant neighbors exist)
var roam={};
st.wifi_connectivity_event.find({site_id:SITE, time:{\$gte:since}},{from_endpoint:1,to_endpoint:1}).forEach(function(e){
[e.from_endpoint&&e.from_endpoint.mac, e.to_endpoint&&e.to_endpoint.mac].forEach(function(m){ if(m) roam[m]=(roam[m]||0)+1; });
});
// airtime profile per AP per band from stat_hourly
var prof={};
st.stat_hourly.find({o:'ap', site_id:SITE, time:{\$gte:since}}).forEach(function(d){
var ap=d.ap; if(!ap) return; if(!prof[ap])prof[ap]={};
bands.forEach(function(b){
var cu=d[b+'-cu_total'], intf=d[b+'-cu_interf'], sta=d[b+'-num_sta'];
if(cu==null && intf==null) return;
if(!prof[ap][b])prof[ap][b]={cu:0,intf:0,sta:0,n:0};
var p=prof[ap][b]; p.cu+=(cu||0); p.intf+=(intf||0); p.sta+=(sta||0); p.n++;
});
});
bands.forEach(function(b){
var rows=[];
for(var ap in prof){ var p=prof[ap][b]; if(!p||!p.n) continue;
var avgCu=p.cu/p.n, avgIntf=p.intf/p.n, avgSta=p.sta/p.n, rm=roam[ap]||0;
// score: airtime pressure (cu+interf) weighted, * redundancy(roam) / (load+1)
var score = (avgCu + avgIntf) * Math.log(1+rm) / (1+avgSta);
rows.push({ap:ap, name:name[ap]||ap, cu:avgCu, intf:avgIntf, sta:avgSta, roam:rm, score:score});
}
rows.sort(function(a,b){return b.score-a.score;});
print("\\n==== band="+b+" top airtime-reduction candidates (disable/power-down) ====");
print(" rank AP cu% interf% ~clients roams score hint");
rows.slice(0,15).forEach(function(r,i){
var hint = (r.roam>50 && r.sta<3) ? "DISABLE (redundant, low load)" : (r.cu+r.intf>40 ? "POWER-DOWN (busy)" : "review");
print(" "+(i+1)+"\\t"+(r.name.substring(0,24)+" ").substring(0,24)+" "+r.cu.toFixed(0)+"\\t"+r.intf.toFixed(0)+"\\t"+r.sta.toFixed(1)+"\\t"+r.roam+"\\t"+r.score.toFixed(1)+"\\t"+hint);
});
print(" (APs profiled on "+b+": "+rows.length+")");
});
print("\\n[note] v1 heuristic: score = (cu_total + cu_interf) * log(1+roams) / (clients+1). High = busy+interfered AND clients have somewhere else to roam = safe to shrink/disable. Validate per-zone before applying. Full greedy coverage-safe optimizer = v2 (interference-model.md).");
JS