Fresh start - excluded large ROM JSON files
This commit is contained in:
59
skills/unifi/README.md
Normal file
59
skills/unifi/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# UniFi Network Monitoring Skill
|
||||
|
||||
📡 Monitor your UniFi network via the local controller API.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Copy the config template**:
|
||||
```bash
|
||||
mkdir -p ~/.clawdbot/credentials/unifi
|
||||
cp config.json.example ~/.clawdbot/credentials/unifi/config.json
|
||||
```
|
||||
|
||||
2. **Edit the config** with your UniFi controller details:
|
||||
```json
|
||||
{
|
||||
"url": "https://HA_IP:8443",
|
||||
"username": "admin",
|
||||
"password": "your_password",
|
||||
"site": "default"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Run a command**:
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/unifi
|
||||
bash scripts/devices.sh
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `dashboard.sh` | Full network dashboard (all stats) |
|
||||
| `devices.sh` | UniFi devices (APs, switches, gateway) |
|
||||
| `clients.sh` | Connected clients |
|
||||
| `health.sh` | Network health overview |
|
||||
| `top-apps.sh [N]` | Top bandwidth apps (default: 10) |
|
||||
| `alerts.sh [N]` | Recent alerts (default: 20) |
|
||||
|
||||
All commands support `json` argument for raw JSON output:
|
||||
```bash
|
||||
bash scripts/clients.sh json
|
||||
```
|
||||
|
||||
## For USG with Controller on Home Assistant
|
||||
|
||||
Since your controller runs on the HA box at `192.168.0.39`, the API URL is:
|
||||
- `https://192.168.0.39:8443` (standard UniFi controller port)
|
||||
|
||||
You may need to:
|
||||
1. Create a local admin account in UniFi (not SSO)
|
||||
2. Allow insecure HTTPS (the scripts use `-k` flag)
|
||||
3. Ensure DPI is enabled in UniFi settings for `top-apps.sh` to work
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Login fails**: Check username/password, ensure local admin account (not UniFi SSO)
|
||||
**Empty data**: DPI may be disabled in UniFi settings
|
||||
**Connection refused**: Verify controller is running and accessible at the URL
|
||||
136
skills/unifi/SKILL.md
Normal file
136
skills/unifi/SKILL.md
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
name: unifi
|
||||
description: Query and monitor UniFi network via local gateway API (Cloud Gateway Max / UniFi OS). Use when the user asks to "check UniFi", "list UniFi devices", "show who's on the network", "UniFi clients", "UniFi health", "top apps", "network alerts", "UniFi DPI", or mentions UniFi monitoring/status/dashboard.
|
||||
version: 1.0.1
|
||||
metadata:
|
||||
clawdbot:
|
||||
emoji: "📡"
|
||||
requires:
|
||||
bins: ["curl", "jq"]
|
||||
---
|
||||
|
||||
# UniFi Network Monitoring Skill
|
||||
|
||||
Monitor and query your UniFi network via the local UniFi OS gateway API (tested on Cloud Gateway Max).
|
||||
|
||||
## Purpose
|
||||
|
||||
This skill provides **read-only** access to your UniFi network's operational data:
|
||||
- Devices (APs, switches, gateway) status and health
|
||||
- Active clients (who's connected where)
|
||||
- Network health overview
|
||||
- Traffic insights (top applications via DPI)
|
||||
- Recent alarms and events
|
||||
|
||||
All operations are **GET-only** and safe for monitoring/reporting.
|
||||
|
||||
## Setup
|
||||
|
||||
Create the credentials file: `~/.clawdbot/credentials/unifi/config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://10.1.0.1",
|
||||
"username": "api",
|
||||
"password": "YOUR_PASSWORD",
|
||||
"site": "default"
|
||||
}
|
||||
```
|
||||
|
||||
- `url`: Your UniFi OS gateway IP/hostname (HTTPS)
|
||||
- `username`: Local UniFi OS admin username
|
||||
- `password`: Local UniFi OS admin password
|
||||
- `site`: Site name (usually `default`)
|
||||
|
||||
## Commands
|
||||
|
||||
All commands support optional `json` argument for raw JSON output (default is human-readable table).
|
||||
|
||||
### Network Dashboard
|
||||
|
||||
Comprehensive view of all network stats (Health, Devices, Clients, Networks, DPI, etc.):
|
||||
|
||||
```bash
|
||||
bash scripts/dashboard.sh
|
||||
bash scripts/dashboard.sh json # Raw JSON for all sections
|
||||
```
|
||||
|
||||
**Output:** Full ASCII dashboard with all metrics.
|
||||
|
||||
### List Devices
|
||||
|
||||
Shows all UniFi devices (APs, switches, gateway):
|
||||
|
||||
```bash
|
||||
bash scripts/devices.sh
|
||||
bash scripts/devices.sh json # Raw JSON
|
||||
```
|
||||
|
||||
**Output:** Device name, model, IP, state, uptime, connected clients
|
||||
|
||||
### List Active Clients
|
||||
|
||||
Shows who's currently connected:
|
||||
|
||||
```bash
|
||||
bash scripts/clients.sh
|
||||
bash scripts/clients.sh json # Raw JSON
|
||||
```
|
||||
|
||||
**Output:** Hostname, IP, MAC, AP, signal strength, RX/TX rates
|
||||
|
||||
### Health Summary
|
||||
|
||||
Site-wide health status:
|
||||
|
||||
```bash
|
||||
bash scripts/health.sh
|
||||
bash scripts/health.sh json # Raw JSON
|
||||
```
|
||||
|
||||
**Output:** Subsystem status (WAN, LAN, WLAN), counts (up/adopted/disconnected)
|
||||
|
||||
### Top Applications (DPI)
|
||||
|
||||
Top bandwidth consumers by application:
|
||||
|
||||
```bash
|
||||
bash scripts/top-apps.sh
|
||||
bash scripts/top-apps 15 # Show top 15 (default: 10)
|
||||
```
|
||||
|
||||
**Output:** App name, category, RX/TX/total traffic in GB
|
||||
|
||||
### Recent Alerts
|
||||
|
||||
Recent alarms and events:
|
||||
|
||||
```bash
|
||||
bash scripts/alerts.sh
|
||||
bash scripts/alerts.sh 50 # Show last 50 (default: 20)
|
||||
```
|
||||
|
||||
**Output:** Timestamp, alarm key, message, affected device
|
||||
|
||||
## Workflow
|
||||
|
||||
When the user asks about UniFi:
|
||||
|
||||
1. **"What's on my network?"** → Run `bash scripts/devices.sh` + `bash scripts/clients.sh`
|
||||
2. **"Is everything healthy?"** → Run `bash scripts/health.sh`
|
||||
3. **"Any problems?"** → Run `bash scripts/alerts.sh`
|
||||
4. **"What's using bandwidth?"** → Run `bash scripts/top-apps.sh`
|
||||
5. **"Show me a dashboard"** or general checkup → Run `bash scripts/dashboard.sh`
|
||||
|
||||
Always confirm the output looks reasonable before presenting it to the user (check for auth failures, empty data, etc.).
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires network access to your UniFi gateway
|
||||
- Uses UniFi OS login + `/proxy/network` API path
|
||||
- All calls are **read-only GET requests**
|
||||
- Tested endpoints are documented in `references/unifi-readonly-endpoints.md`
|
||||
|
||||
## Reference
|
||||
|
||||
- [Tested Endpoints](references/unifi-readonly-endpoints.md) — Full catalog of verified read-only API calls on your Cloud Gateway Max
|
||||
BIN
skills/unifi/__pycache__/unifi_monitor.cpython-313.pyc
Normal file
BIN
skills/unifi/__pycache__/unifi_monitor.cpython-313.pyc
Normal file
Binary file not shown.
6
skills/unifi/config.json
Normal file
6
skills/unifi/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"url": "https://192.168.0.39:8443",
|
||||
"username": "corey",
|
||||
"password": "is41945549",
|
||||
"site": "default"
|
||||
}
|
||||
6
skills/unifi/config.json.example
Normal file
6
skills/unifi/config.json.example
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"url": "https://192.168.0.39:8443",
|
||||
"username": "admin",
|
||||
"password": "YOUR_PASSWORD_HERE",
|
||||
"site": "default"
|
||||
}
|
||||
70
skills/unifi/manual_test.py
Normal file
70
skills/unifi/manual_test.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""UniFi Monitor - To be run manually"""
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
from datetime import datetime
|
||||
|
||||
# Disable SSL warnings
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Config
|
||||
CONTROLLER = "https://192.168.0.39:8443"
|
||||
USER = "corey"
|
||||
PASS = "is41945549"
|
||||
SITE = "default"
|
||||
|
||||
print("="*50)
|
||||
print("UniFi Network Monitor")
|
||||
print("="*50)
|
||||
print(f"Controller: {CONTROLLER}")
|
||||
print(f"User: {USER}")
|
||||
print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
print()
|
||||
|
||||
# Step 1: Login
|
||||
print("Step 1: Logging in...")
|
||||
login_url = f"{CONTROLLER}/api/auth/login"
|
||||
resp = requests.post(login_url, json={"username": USER, "password": PASS}, verify=False, timeout=10)
|
||||
print(f" Status: {resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
print(" ❌ Login failed!")
|
||||
print(f" Response: {resp.text[:200]}")
|
||||
exit(1)
|
||||
|
||||
print(" ✅ Login successful")
|
||||
cookies = resp.cookies
|
||||
|
||||
# Step 2: Get clients
|
||||
print("\nStep 2: Getting clients...")
|
||||
clients_url = f"{CONTROLLER}/proxy/network/api/s/{SITE}/stat/sta"
|
||||
resp = requests.get(clients_url, cookies=cookies, verify=False, timeout=10)
|
||||
clients = resp.json().get('data', [])
|
||||
print(f" Found: {len(clients)} clients")
|
||||
|
||||
# Step 3: Get devices
|
||||
print("\nStep 3: Getting devices...")
|
||||
devices_url = f"{CONTROLLER}/proxy/network/api/s/{SITE}/stat/device"
|
||||
resp = requests.get(devices_url, cookies=cookies, verify=False, timeout=10)
|
||||
devices = resp.json().get('data', [])
|
||||
print(f" Found: {len(devices)} devices")
|
||||
|
||||
# Generate report
|
||||
print("\n" + "="*50)
|
||||
print("REPORT")
|
||||
print("="*50)
|
||||
print(f"\nConnected Clients: {len(clients)}")
|
||||
|
||||
wired = [c for c in clients if c.get('is_wired', False)]
|
||||
wireless = [c for c in clients if not c.get('is_wired', False)]
|
||||
print(f" - Wireless: {len(wireless)}")
|
||||
print(f" - Wired: {len(wired)}")
|
||||
|
||||
print(f"\nUniFi Devices: {len(devices)}")
|
||||
for d in devices[:3]:
|
||||
name = d.get('name', d.get('mac', 'Unknown')[:17])
|
||||
model = d.get('model', 'Unknown')
|
||||
print(f" - {name} ({model})")
|
||||
|
||||
print("\n" + "="*50)
|
||||
133
skills/unifi/references/unifi-readonly-endpoints.md
Normal file
133
skills/unifi/references/unifi-readonly-endpoints.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# UniFi Local Gateway (UniFi OS / UCG Max) — Read-Only API Calls (Best-Effort)
|
||||
|
||||
This is a **best-effort** catalog of *read-only* endpoints commonly available on UniFi OS gateways (UDM/UDR/UCG Max) running the UniFi Network application.
|
||||
|
||||
## Base Paths (UCG Max / UniFi OS)
|
||||
|
||||
After logging in to UniFi OS (`POST /api/auth/login`), the Network app API is typically accessed via:
|
||||
|
||||
- `https://<gateway>/proxy/network/api/...`
|
||||
|
||||
Most controller-style endpoints below assume:
|
||||
|
||||
- **Site**: `default`
|
||||
- **Prefix**: `/proxy/network` (UniFi OS difference)
|
||||
|
||||
So, for example:
|
||||
|
||||
- `GET https://<gateway>/proxy/network/api/s/default/stat/health`
|
||||
|
||||
## UniFi OS (console-level) endpoints (read-only)
|
||||
|
||||
These are not site-scoped.
|
||||
|
||||
- `GET /status` — basic gateway status (only endpoint often reachable without auth)
|
||||
- `GET /api/users/self` (aka `/api/self` on older controllers) — logged-in user
|
||||
- `GET /api/self/sites` — list sites
|
||||
- `GET /api/stat/sites` — sites + health/alerts summary
|
||||
- `GET /api/stat/admin` — admins + permissions (requires sufficient rights)
|
||||
|
||||
## Network app endpoints (site-scoped, GET-only reads)
|
||||
|
||||
All of these are under `/api/s/{site}/...` (remember to prefix with `/proxy/network` on UniFi OS).
|
||||
|
||||
### Identity / meta
|
||||
- `GET /api/s/{site}/self` — logged-in user (site context)
|
||||
- `GET /api/s/{site}/stat/sysinfo` — controller + site high-level info
|
||||
- `GET /api/s/{site}/stat/ccode` — country codes
|
||||
- `GET /api/s/{site}/stat/current-channel` — available RF channels
|
||||
|
||||
### Health / monitoring
|
||||
- `GET /api/s/{site}/stat/health` — health overview
|
||||
- `GET /api/s/{site}/stat/event` — recent events (newest-first, often ~3000 cap)
|
||||
- `GET /api/s/{site}/stat/alarm` — recent alarms (newest-first, often ~3000 cap)
|
||||
|
||||
### Clients
|
||||
- `GET /api/s/{site}/stat/sta` — **active** clients
|
||||
- `GET /api/s/{site}/rest/user` — **known/configured** clients
|
||||
|
||||
### Devices
|
||||
- `GET /api/s/{site}/stat/device-basic` — minimal device info (mac/type/state)
|
||||
- `GET /api/s/{site}/stat/device` — full device list
|
||||
- `GET /api/s/{site}/stat/device/{mac}` — UniFi OS variant for a single device by mac (UDM/UCG)
|
||||
|
||||
### Routing / WAN
|
||||
- `GET /api/s/{site}/stat/routing` — active routes
|
||||
- `GET /api/s/{site}/stat/dynamicdns` — DynamicDNS status
|
||||
|
||||
### Wireless / RF
|
||||
- `GET /api/s/{site}/stat/rogueap` — neighboring/rogue APs
|
||||
- `GET /api/s/{site}/stat/spectrumscan` — RF scan results (optionally per-device)
|
||||
|
||||
### DPI / traffic (read-only when used with GET)
|
||||
- `GET /api/s/{site}/stat/sitedpi` — site-wide DPI stats (apps/categories)
|
||||
- `GET /api/s/{site}/stat/stadpi` — per-client DPI stats
|
||||
|
||||
### Port forwards
|
||||
- `GET /api/s/{site}/rest/portforward` — configured port forwards
|
||||
|
||||
### Profiles / config (treat as read-only by using GET)
|
||||
These *can* be writable via PUT/POST in general, but are safe if you only **GET**.
|
||||
|
||||
- `GET /api/s/{site}/rest/setting` — site settings
|
||||
- `GET /api/s/{site}/rest/networkconf` — networks
|
||||
- `GET /api/s/{site}/rest/wlanconf` — WLANs
|
||||
- `GET /api/s/{site}/rest/wlanconf/{_id}` — WLAN details
|
||||
- `GET /api/s/{site}/rest/firewallrule` — user firewall rules
|
||||
- `GET /api/s/{site}/rest/firewallgroup` — firewall groups
|
||||
- `GET /api/s/{site}/rest/routing` — user-defined routes (read)
|
||||
- `GET /api/s/{site}/rest/dynamicdns` — DynamicDNS config
|
||||
- `GET /api/s/{site}/rest/portconf` — switch port profiles
|
||||
- `GET /api/s/{site}/rest/radiusprofile` — RADIUS profiles
|
||||
- `GET /api/s/{site}/rest/account` — RADIUS accounts
|
||||
|
||||
## Notes / caveats
|
||||
|
||||
- UniFi's local API is largely **undocumented** and varies by Network app version.
|
||||
- Some endpoints support POST filters (e.g., `stat/device` filter by macs). Those can still be read-only, but we should treat *all POSTs as suspicious* unless we confirm they don't mutate state.
|
||||
- For a Clawdbot skill, safest posture is:
|
||||
- Use **GET-only** to the `stat/*` and selected `rest/*` endpoints
|
||||
- Avoid anything under `/cmd/*` and any `PUT/POST/DELETE`
|
||||
|
||||
## Tested On Your Gateway
|
||||
|
||||
Tested against UniFi OS gateway using the UniFi OS login + `/proxy/network` path.
|
||||
|
||||
| Group | Path | HTTP | OK | Note |
|
||||
|---|---|---:|:--:|---|
|
||||
| console | `/status` | 200 | OK | 200 |
|
||||
| console | `/api/users/self` | 200 | OK | 200 |
|
||||
| console | `/api/self/sites` | 404 | FAIL | http 404 |
|
||||
| console | `/api/stat/sites` | 404 | FAIL | http 404 |
|
||||
| console | `/api/stat/admin` | 404 | FAIL | http 404 |
|
||||
| network | `/api/s/default/self` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/sysinfo` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/ccode` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/current-channel` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/health` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/event` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/alarm` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/sta` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/user` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/device-basic` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/device` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/routing` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/dynamicdns` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/rogueap` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/spectrumscan` | 404 | FAIL | api.err.NotFound |
|
||||
| network | `/api/s/default/stat/sitedpi` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/stat/stadpi` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/portforward` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/setting` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/networkconf` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/wlanconf` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/firewallrule` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/firewallgroup` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/routing` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/dynamicdns` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/portconf` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/radiusprofile` | 200 | OK | meta.rc=ok |
|
||||
| network | `/api/s/default/rest/account` | 200 | OK | meta.rc=ok |
|
||||
|
||||
## Source
|
||||
- Community reverse-engineered list (includes UniFi OS notes): https://ubntwiki.com/products/software/unifi-controller/api
|
||||
21
skills/unifi/scripts/alerts.sh
Normal file
21
skills/unifi/scripts/alerts.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Recent UniFi alarms/alerts
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/unifi-api.sh"
|
||||
|
||||
LIMIT="${1:-20}"
|
||||
|
||||
data=$(unifi_get "/api/s/$UNIFI_SITE/stat/alarm")
|
||||
|
||||
echo "$data" | jq -r --argjson limit "$LIMIT" '
|
||||
["TIME", "KEY", "MESSAGE", "AP/DEVICE"],
|
||||
["----", "---", "-------", "----------"],
|
||||
(.data[:$limit][] | [
|
||||
(.datetime | strftime("%Y-%m-%d %H:%M")),
|
||||
.key,
|
||||
(.msg // "N/A"),
|
||||
(.ap_name // .gw_name // .sw_name // "N/A")
|
||||
]) | @tsv
|
||||
' | column -t -s $'\t'
|
||||
28
skills/unifi/scripts/clients.sh
Normal file
28
skills/unifi/scripts/clients.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# List active UniFi clients
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/unifi-api.sh"
|
||||
|
||||
FORMAT="${1:-table}"
|
||||
|
||||
data=$(unifi_get "/api/s/$UNIFI_SITE/stat/sta")
|
||||
|
||||
if [ "$FORMAT" = "json" ]; then
|
||||
echo "$data"
|
||||
else
|
||||
# Table format
|
||||
echo "$data" | jq -r '
|
||||
["HOSTNAME", "IP", "MAC", "AP", "SIGNAL", "RX/TX (Mbps)"],
|
||||
["--------", "--", "---", "--", "------", "------------"],
|
||||
(.data[] | [
|
||||
(.hostname // .name // "Unknown"),
|
||||
.ip,
|
||||
.mac,
|
||||
(.ap_mac // "N/A")[0:17],
|
||||
((.signal // 0 | tostring) + " dBm"),
|
||||
(((.rx_rate // 0) / 1000 | floor | tostring) + "/" + ((.tx_rate // 0) / 1000 | floor | tostring))
|
||||
]) | @tsv
|
||||
' | column -t -s $'\t'
|
||||
fi
|
||||
285
skills/unifi/scripts/dashboard.sh
Normal file
285
skills/unifi/scripts/dashboard.sh
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
# UniFi Network Dashboard - Comprehensive overview
|
||||
# Usage: bash dashboard.sh [json]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/unifi-api.sh"
|
||||
OUTPUT_FILE="$HOME/clawd/memory/bank/unifi-inventory.md"
|
||||
|
||||
# Disable strict mode for dashboard (we handle errors gracefully)
|
||||
set +e
|
||||
|
||||
OUTPUT_JSON="${1:-}"
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$(dirname "$OUTPUT_FILE")"
|
||||
|
||||
# Create shared session for all requests
|
||||
export UNIFI_COOKIE_FILE=$(mktemp)
|
||||
trap "rm -f '$UNIFI_COOKIE_FILE'" EXIT
|
||||
unifi_login "$UNIFI_COOKIE_FILE"
|
||||
|
||||
# Fetch all data upfront (using full API path format)
|
||||
HEALTH=$(unifi_get "/api/s/$UNIFI_SITE/stat/health")
|
||||
DEVICES=$(unifi_get "/api/s/$UNIFI_SITE/stat/device")
|
||||
CLIENTS=$(unifi_get "/api/s/$UNIFI_SITE/stat/sta")
|
||||
PORTFWD=$(unifi_get "/api/s/$UNIFI_SITE/rest/portforward")
|
||||
FWRULES=$(unifi_get "/api/s/$UNIFI_SITE/rest/firewallrule")
|
||||
NETWORKS=$(unifi_get "/api/s/$UNIFI_SITE/rest/networkconf")
|
||||
WLANS=$(unifi_get "/api/s/$UNIFI_SITE/rest/wlanconf")
|
||||
ALARMS=$(unifi_get "/api/s/$UNIFI_SITE/stat/alarm")
|
||||
ROUTING=$(unifi_get "/api/s/$UNIFI_SITE/stat/routing")
|
||||
SYSINFO=$(unifi_get "/api/s/$UNIFI_SITE/stat/sysinfo")
|
||||
|
||||
# Debug: Dump all JSON to a file for troubleshooting
|
||||
jq -n \
|
||||
--argjson health "$HEALTH" \
|
||||
--argjson devices "$DEVICES" \
|
||||
--argjson clients "$CLIENTS" \
|
||||
--argjson portforward "$PORTFWD" \
|
||||
--argjson networks "$NETWORKS" \
|
||||
--argjson wlans "$WLANS" \
|
||||
'{health: $health, devices: $devices, clients: $clients, networks: $networks, wlans: $wlans}' \
|
||||
> dashboard_debug_dump.json 2>/dev/null
|
||||
|
||||
if [ "$OUTPUT_JSON" = "json" ]; then
|
||||
jq -n \
|
||||
--argjson health "$HEALTH" \
|
||||
--argjson devices "$DEVICES" \
|
||||
--argjson clients "$CLIENTS" \
|
||||
--argjson portforward "$PORTFWD" \
|
||||
--argjson firewall "$FWRULES" \
|
||||
--argjson networks "$NETWORKS" \
|
||||
--argjson wlans "$WLANS" \
|
||||
--argjson alarms "$ALARMS" \
|
||||
--argjson routing "$ROUTING" \
|
||||
--argjson sysinfo "$SYSINFO" \
|
||||
'{health: $health, devices: $devices, clients: $clients, portforward: $portforward, firewall: $firewall, networks: $networks, wlans: $wlans, alarms: $alarms, routing: $routing, sysinfo: $sysinfo}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate dashboard output and save to file
|
||||
{
|
||||
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ UNIFI NETWORK DASHBOARD ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# System Info
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ SYSTEM INFO │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
echo "$SYSINFO" | jq -r '
|
||||
.data[0] |
|
||||
"Version: \(.version // "N/A") | Hostname: \(.hostname // "N/A") | Timezone: \(.timezone // "N/A")"
|
||||
' 2>/dev/null || echo " Unable to fetch system info"
|
||||
echo ""
|
||||
|
||||
# Health Overview
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ HEALTH STATUS │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
echo "$HEALTH" | jq -r '
|
||||
.data[] |
|
||||
"\(.subsystem | ascii_upcase): \(if .status == "ok" then "OK" else "\(.status)" end) | Gateways: \(.gw_mac // "N/A") | Clients: \(.num_user // 0)"
|
||||
' 2>/dev/null | head -10
|
||||
echo ""
|
||||
|
||||
# Devices
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ UNIFI DEVICES │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
printf "%-20s %-10s %-15s %-8s %-10s %-8s\n" "NAME" "MODEL" "IP" "STATE" "UPTIME" "CLIENTS"
|
||||
printf "%-20s %-10s %-15s %-8s %-10s %-8s\n" "----" "-----" "--" "-----" "------" "-------"
|
||||
echo "$DEVICES" | jq -r '
|
||||
.data[] |
|
||||
[
|
||||
((.name // .hostname // .mac)[:20]),
|
||||
((.model // "N/A")[:10]),
|
||||
((.ip // "N/A")[:15]),
|
||||
(if .state == 1 then "OK" else "FAIL" end),
|
||||
(((.uptime // 0) / 3600 | floor | tostring + "h")),
|
||||
((.num_sta // 0 | tostring))
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r name model ip state uptime clients; do
|
||||
printf "%-20s %-10s %-15s %-8s %-10s %-8s\n" "$name" "$model" "$ip" "$state" "$uptime" "$clients"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Client Summary
|
||||
TOTAL_CLIENTS=$(echo "$CLIENTS" | jq '.data | length' 2>/dev/null || echo 0)
|
||||
WIRED_CLIENTS=$(echo "$CLIENTS" | jq '[.data[] | select(.is_wired == true)] | length' 2>/dev/null || echo 0)
|
||||
WIFI_CLIENTS=$(echo "$CLIENTS" | jq '[.data[] | select(.is_wired == false)] | length' 2>/dev/null || echo 0)
|
||||
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ CLIENTS: $TOTAL_CLIENTS total ($WIRED_CLIENTS wired, $WIFI_CLIENTS wireless) │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
printf "%-25s %-15s %-18s %-10s %-12s\n" "HOSTNAME" "IP" "MAC" "TYPE" "TX/RX MB/s"
|
||||
printf "%-25s %-15s %-18s %-10s %-12s\n" "--------" "--" "---" "----" "----------"
|
||||
echo "$CLIENTS" | jq -r '
|
||||
.data | sort_by(-(.["wired-rx_bytes"] // .rx_bytes // 0)) | .[0:15] | .[] |
|
||||
[
|
||||
((.name // .hostname // .mac // "Unknown") | tostring | .[:25]),
|
||||
((.ip // .last_ip // "N/A") | tostring | .[:15]),
|
||||
((.mac // "N/A") | tostring | .[:18]),
|
||||
(if .is_wired == true then "Wired" else "WiFi" end),
|
||||
"\( ((.["tx_bytes-r"] // .["wired-tx_bytes-r"] // 0) / 1000000 * 10 | floor / 10) )/\( ((.["rx_bytes-r"] // .["wired-rx_bytes-r"] // 0) / 1000000 * 10 | floor / 10) )"
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r hostname ip mac type rate; do
|
||||
printf "%-25s %-15s %-18s %-10s %-12s\n" "$hostname" "$ip" "$mac" "$type" "$rate"
|
||||
done || echo " (error parsing client data)"
|
||||
echo " (showing top 15 by traffic)"
|
||||
echo ""
|
||||
|
||||
# Networks
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ NETWORKS │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
printf "%-25s %-8s %-18s %-10s %-15s\n" "NAME" "VLAN" "SUBNET" "PURPOSE" "DHCP"
|
||||
printf "%-25s %-8s %-18s %-10s %-15s\n" "----" "----" "------" "-------" "----"
|
||||
# Check if networks returned an error
|
||||
if echo "$NETWORKS" | jq -e '.error' >/dev/null 2>&1; then
|
||||
echo " (API returned 401 - REST endpoints may require additional permissions)"
|
||||
else
|
||||
echo "$NETWORKS" | jq -r '
|
||||
.data[] |
|
||||
[
|
||||
((.name // "N/A") | tostring | .[:25]),
|
||||
((.vlan // "-") | tostring | .[:8]),
|
||||
((.ip_subnet // "N/A") | tostring | .[:18]),
|
||||
((.purpose // "N/A") | tostring | .[:10]),
|
||||
(if .dhcpd_enabled == true then "Enabled" else "Disabled" end)
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r name vlan subnet purpose dhcp; do
|
||||
printf "%-25s %-8s %-18s %-10s %-15s\n" "$name" "$vlan" "$subnet" "$purpose" "$dhcp"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# WLANs
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ WIRELESS NETWORKS │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
printf "%-30s %-10s %-15s %-10s\n" "SSID" "ENABLED" "SECURITY" "BAND"
|
||||
printf "%-30s %-10s %-15s %-10s\n" "----" "-------" "--------" "----"
|
||||
# Check if wlans returned an error
|
||||
if echo "$WLANS" | jq -e '.error' >/dev/null 2>&1; then
|
||||
echo " (API returned 401 - REST endpoints may require additional permissions)"
|
||||
else
|
||||
echo "$WLANS" | jq -r '
|
||||
.data[] |
|
||||
[
|
||||
((.name // "N/A") | tostring | .[:30]),
|
||||
(if .enabled == true then "YES" else "NO" end),
|
||||
((.security // "open") | tostring | .[:15]),
|
||||
((if .wlan_band then .wlan_band else "both" end) | tostring | .[:10])
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r ssid enabled security band; do
|
||||
printf "%-30s %-10s %-15s %-10s\n" "$ssid" "$enabled" "$security" "$band"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Port Forwards
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ PORT FORWARDS │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
PF_COUNT=$(echo "$PORTFWD" | jq '.data | length' 2>/dev/null || echo 0)
|
||||
if [ "$PF_COUNT" -eq 0 ]; then
|
||||
echo " No port forwards configured"
|
||||
else
|
||||
printf "%-25s %-10s %-8s %-15s %-8s %-10s\n" "NAME" "ENABLED" "PROTO" "FWD TO" "PORT" "SRC PORT"
|
||||
printf "%-25s %-10s %-8s %-15s %-8s %-10s\n" "----" "-------" "-----" "------" "----" "--------"
|
||||
echo "$PORTFWD" | jq -r '
|
||||
.data[] |
|
||||
[
|
||||
((.name // "N/A") | tostring | .[:25]),
|
||||
(if .enabled == true then "YES" else "NO" end),
|
||||
((.proto // "tcp") | tostring | .[:8]),
|
||||
((.fwd // "N/A") | tostring | .[:15]),
|
||||
((.fwd_port // "-") | tostring | .[:8]),
|
||||
((.src_port // .dst_port // "-") | tostring | .[:10])
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r name enabled proto fwd port srcport; do
|
||||
printf "%-25s %-10s %-8s %-15s %-8s %-10s\n" "$name" "$enabled" "$proto" "$fwd" "$port" "$srcport"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Firewall Rules
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ FIREWALL RULES │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
FW_COUNT=$(echo "$FWRULES" | jq '.data | length' 2>/dev/null || echo 0)
|
||||
if [ "$FW_COUNT" -eq 0 ]; then
|
||||
echo " No custom firewall rules configured"
|
||||
else
|
||||
printf "%-25s %-10s %-10s %-10s %-10s %-10s\n" "NAME" "ENABLED" "ACTION" "PROTO" "RULESET" "INDEX"
|
||||
printf "%-25s %-10s %-10s %-10s %-10s %-10s\n" "----" "-------" "------" "-----" "-------" "-----"
|
||||
echo "$FWRULES" | jq -r '
|
||||
.data | sort_by(.rule_index) | .[] |
|
||||
[
|
||||
((.name // "N/A") | tostring | .[:25]),
|
||||
(if .enabled == true then "YES" else "NO" end),
|
||||
((.action // "N/A") | tostring | .[:10]),
|
||||
((.protocol // "all") | tostring | .[:10]),
|
||||
((.ruleset // "N/A") | tostring | .[:10]),
|
||||
((.rule_index // 0) | tostring)
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r name enabled action proto ruleset idx; do
|
||||
printf "%-25s %-10s %-10s %-10s %-10s %-10s\n" "$name" "$enabled" "$action" "$proto" "$ruleset" "$idx"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Static Routes
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ ROUTES │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
ROUTE_COUNT=$(echo "$ROUTING" | jq '.data | length' 2>/dev/null || echo 0)
|
||||
if [ "$ROUTE_COUNT" -eq 0 ]; then
|
||||
echo " No static routes (using default)"
|
||||
else
|
||||
printf "%-20s %-18s %-15s %-12s %-10s\n" "NAME" "DESTINATION" "NEXT HOP" "INTERFACE" "METRIC"
|
||||
printf "%-20s %-18s %-15s %-12s %-10s\n" "----" "-----------" "--------" "---------" "------"
|
||||
echo "$ROUTING" | jq -r '
|
||||
.data[] |
|
||||
[
|
||||
((.name // .pfx // "N/A") | tostring | .[:20]),
|
||||
((.pfx // "N/A") | tostring | .[:18]),
|
||||
((.nh[0].t // .nh[0].gw // "N/A") | tostring | .[:15]),
|
||||
((.nh[0].intf_name // "N/A") | tostring | .[:12]),
|
||||
((.metric // 0) | tostring)
|
||||
] | @tsv
|
||||
' 2>/dev/null | head -10 | while IFS=$'\t' read -r name dest nexthop intf metric; do
|
||||
printf "%-20s %-18s %-15s %-12s %-10s\n" "$name" "$dest" "$nexthop" "$intf" "$metric"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Recent Alarms
|
||||
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ RECENT ALARMS (last 10) │"
|
||||
echo "└─────────────────────────────────────────────────────────────────────────────┘"
|
||||
ALARM_COUNT=$(echo "$ALARMS" | jq '.data | length' 2>/dev/null || echo 0)
|
||||
if [ "$ALARM_COUNT" -eq 0 ]; then
|
||||
echo " No recent alarms"
|
||||
else
|
||||
printf "%-20s %-50s\n" "TIME" "MESSAGE"
|
||||
printf "%-20s %-50s\n" "----" "-------"
|
||||
echo "$ALARMS" | jq -r '
|
||||
.data | sort_by(-.time) | .[0:10] | .[] |
|
||||
[
|
||||
((.datetime // (.time | todate? // "N/A"))[:20]),
|
||||
((.msg // .key // "N/A")[:50])
|
||||
] | @tsv
|
||||
' 2>/dev/null | while IFS=$'\t' read -r time msg; do
|
||||
printf "%-20s %-50s\n" "$time" "$msg"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Dashboard generated at $(date '+%Y-%m-%d %H:%M:%S') ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
|
||||
} | tee "$OUTPUT_FILE"
|
||||
28
skills/unifi/scripts/devices.sh
Normal file
28
skills/unifi/scripts/devices.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# List UniFi devices (APs, switches, gateway)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/unifi-api.sh"
|
||||
|
||||
FORMAT="${1:-table}"
|
||||
|
||||
data=$(unifi_get "/api/s/$UNIFI_SITE/stat/device")
|
||||
|
||||
if [ "$FORMAT" = "json" ]; then
|
||||
echo "$data"
|
||||
else
|
||||
# Table format
|
||||
echo "$data" | jq -r '
|
||||
["NAME", "MODEL", "IP", "STATE", "UPTIME", "CLIENTS"],
|
||||
["----", "-----", "--", "-----", "------", "-------"],
|
||||
(.data[] | [
|
||||
.name // .mac,
|
||||
.model,
|
||||
.ip,
|
||||
.state_name // .state,
|
||||
(.uptime | if . then (. / 3600 | floor | tostring) + "h" else "N/A" end),
|
||||
(.num_sta // 0 | tostring)
|
||||
]) | @tsv
|
||||
' | column -t -s $'\t'
|
||||
fi
|
||||
27
skills/unifi/scripts/health.sh
Normal file
27
skills/unifi/scripts/health.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# UniFi site health summary
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/unifi-api.sh"
|
||||
|
||||
FORMAT="${1:-table}"
|
||||
|
||||
data=$(unifi_get "/api/s/$UNIFI_SITE/stat/health")
|
||||
|
||||
if [ "$FORMAT" = "json" ]; then
|
||||
echo "$data"
|
||||
else
|
||||
# Table format
|
||||
echo "$data" | jq -r '
|
||||
["SUBSYSTEM", "STATUS", "# UP", "# ADOPTED", "# DISCONNECTED"],
|
||||
["---------", "------", "----", "---------", "--------------"],
|
||||
(.data[] | [
|
||||
.subsystem,
|
||||
.status,
|
||||
(.num_user // .num_ap // .num_sw // .num_gw // 0 | tostring),
|
||||
(.num_adopted // 0 | tostring),
|
||||
(.num_disconnected // 0 | tostring)
|
||||
]) | @tsv
|
||||
' | column -t -s $'\t'
|
||||
fi
|
||||
4
skills/unifi/scripts/test_cookie.txt
Normal file
4
skills/unifi/scripts/test_cookie.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
26
skills/unifi/scripts/top-apps.sh
Normal file
26
skills/unifi/scripts/top-apps.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Top applications by traffic (DPI)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/unifi-api.sh"
|
||||
|
||||
LIMIT="${1:-10}"
|
||||
|
||||
data=$(unifi_get "/api/s/$UNIFI_SITE/stat/sitedpi")
|
||||
|
||||
echo "$data" | jq -r --argjson limit "$LIMIT" '
|
||||
["APP", "CATEGORY", "RX (GB)", "TX (GB)", "TOTAL (GB)"],
|
||||
["---", "--------", "-------", "-------", "----------"],
|
||||
(.data[0].by_app // []
|
||||
| sort_by(-.tx_bytes + -.rx_bytes)
|
||||
| .[:$limit][]
|
||||
| [
|
||||
.app,
|
||||
.cat,
|
||||
((.rx_bytes // 0) / 1073741824 | . * 100 | floor / 100 | tostring),
|
||||
((.tx_bytes // 0) / 1073741824 | . * 100 | floor / 100 | tostring),
|
||||
(((.rx_bytes // 0) + (.tx_bytes // 0)) / 1073741824 | . * 100 | floor / 100 | tostring)
|
||||
]
|
||||
) | @tsv
|
||||
' | column -t -s $'\t'
|
||||
94
skills/unifi/scripts/unifi-api.sh
Normal file
94
skills/unifi/scripts/unifi-api.sh
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# UniFi API helper - handles login and authenticated calls
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="${UNIFI_CONFIG_FILE:-$HOME/.clawdbot/credentials/unifi/config.json}"
|
||||
|
||||
# Load config
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Error: UniFi not configured. Create $CONFIG_FILE with {\"url\": \"https://...\", \"username\": \"...\", \"password\": \"...\", \"site\": \"default\"}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UNIFI_URL=$(jq -r '.url' "$CONFIG_FILE")
|
||||
UNIFI_USER=$(jq -r '.username' "$CONFIG_FILE")
|
||||
UNIFI_PASS=$(jq -r '.password' "$CONFIG_FILE")
|
||||
UNIFI_SITE=$(jq -r '.site // "default"' "$CONFIG_FILE")
|
||||
|
||||
# Login and store cookie
|
||||
# Usage: unifi_login [cookie_file_path]
|
||||
unifi_login() {
|
||||
local cookie_file="${1:-${UNIFI_COOKIE_FILE:-$(mktemp)}}"
|
||||
|
||||
# If it's a temp file we created just now, export it so subsequent calls use it
|
||||
if [ -z "${UNIFI_COOKIE_FILE:-}" ]; then
|
||||
export UNIFI_COOKIE_FILE="$cookie_file"
|
||||
fi
|
||||
|
||||
local payload
|
||||
payload=$(jq -nc --arg username "$UNIFI_USER" --arg password "$UNIFI_PASS" '{username:$username,password:$password}')
|
||||
|
||||
# Try UniFi OS login first, fallback to classic controller
|
||||
local login_response
|
||||
login_response=$(curl -sk -c "$cookie_file" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"$UNIFI_URL/api/auth/login" \
|
||||
--data "$payload" 2>/dev/null)
|
||||
|
||||
# Check if UniFi OS login succeeded
|
||||
if echo "$login_response" | jq -e '.meta.rc == "ok"' >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback to classic controller login
|
||||
curl -sk -c "$cookie_file" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"$UNIFI_URL/api/login" \
|
||||
--data "$payload" >/dev/null 2>&1
|
||||
|
||||
if [ ! -s "$cookie_file" ]; then
|
||||
echo "Error: Login failed (empty cookie file)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Make authenticated GET request
|
||||
# Usage: unifi_get <endpoint>
|
||||
# Endpoint should be like "stat/sta" or "rest/portforward" - site path is added automatically
|
||||
# Uses UNIFI_COOKIE_FILE if set, otherwise logs in temporarily
|
||||
unifi_get() {
|
||||
local endpoint="$1"
|
||||
local temp_cookie=false
|
||||
|
||||
# Ensure we have a cookie
|
||||
if [ -z "${UNIFI_COOKIE_FILE:-}" ] || [ ! -f "$UNIFI_COOKIE_FILE" ]; then
|
||||
temp_cookie=true
|
||||
export UNIFI_COOKIE_FILE=$(mktemp)
|
||||
unifi_login "$UNIFI_COOKIE_FILE"
|
||||
fi
|
||||
|
||||
# Handle both old format (/api/s/site/...) and new format (stat/...)
|
||||
local full_url
|
||||
if [[ "$endpoint" == /api/* ]]; then
|
||||
# Old format - use as-is
|
||||
full_url="$UNIFI_URL$endpoint"
|
||||
else
|
||||
# New format - try UniFi OS first, fallback to classic
|
||||
full_url="$UNIFI_URL/proxy/network/api/s/$UNIFI_SITE/$endpoint"
|
||||
fi
|
||||
|
||||
curl -sk -b "$UNIFI_COOKIE_FILE" "$full_url"
|
||||
|
||||
# Cleanup if we created a temp cookie just for this request
|
||||
if [ "$temp_cookie" = true ]; then
|
||||
rm -f "$UNIFI_COOKIE_FILE"
|
||||
unset UNIFI_COOKIE_FILE
|
||||
fi
|
||||
}
|
||||
|
||||
export -f unifi_login
|
||||
export -f unifi_get
|
||||
export UNIFI_URL UNIFI_SITE
|
||||
52
skills/unifi/simple_test.py
Normal file
52
skills/unifi/simple_test.py
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simple UniFi test - generates report"""
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
from datetime import datetime
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
URL = "https://192.168.0.39:8443"
|
||||
USER = "corey"
|
||||
PASS = "is41945549"
|
||||
|
||||
# Login
|
||||
login = requests.post(f"{URL}/api/auth/login", json={"username": USER, "password": PASS}, verify=False, timeout=10)
|
||||
login.raise_for_status()
|
||||
cookies = login.cookies
|
||||
|
||||
# Get clients
|
||||
clients = requests.get(f"{URL}/proxy/network/api/s/default/stat/sta", cookies=cookies, verify=False, timeout=10).json().get('data', [])
|
||||
|
||||
# Get devices
|
||||
devices = requests.get(f"{URL}/proxy/network/api/s/default/stat/device", cookies=cookies, verify=False, timeout=10).json().get('data', [])
|
||||
|
||||
# Count by type
|
||||
wired = sum(1 for c in clients if c.get('is_wired', False))
|
||||
wireless = len(clients) - wired
|
||||
|
||||
# Generate simple report
|
||||
report = f"""# UniFi Network Report - {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
## Connected Clients: {len(clients)}
|
||||
- Wireless: {wireless}
|
||||
- Wired: {wired}
|
||||
|
||||
## UniFi Devices: {len(devices)}
|
||||
|
||||
### Sample Wireless Clients:
|
||||
"""
|
||||
|
||||
for idx, c in enumerate([x for x in clients if not x.get('is_wired', False)][:5], 1):
|
||||
name = c.get('name', c.get('hostname', 'Unknown'))
|
||||
ip = c.get('ip', 'N/A')
|
||||
report += f"{idx}. {name} - {ip}\n"
|
||||
|
||||
report += f"\n### UniFi Devices:\n"
|
||||
for idx, d in enumerate(devices[:5], 1):
|
||||
name = d.get('name', d.get('mac', 'Unknown'))
|
||||
model = d.get('model', 'Unknown')
|
||||
report += f"{idx}. {name} ({model})\n"
|
||||
|
||||
print(report)
|
||||
6
skills/unifi/temp_config.json
Normal file
6
skills/unifi/temp_config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"url": "https://192.168.0.39:8443",
|
||||
"username": "corey",
|
||||
"password": "is41945549",
|
||||
"site": "default"
|
||||
}
|
||||
73
skills/unifi/test_connection.py
Normal file
73
skills/unifi/test_connection.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test UniFi Controller API connection"""
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
# Disable SSL warnings (self-signed certs)
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Config
|
||||
CONTROLLER_URL = "https://192.168.0.39:8443"
|
||||
USERNAME = "corey"
|
||||
PASSWORD = "is41945549"
|
||||
SITE_ID = "default"
|
||||
|
||||
print("Testing UniFi Controller connection...")
|
||||
print(f"URL: {CONTROLLER_URL}")
|
||||
|
||||
# Step 1: Login
|
||||
login_url = f"{CONTROLLER_URL}/api/auth/login"
|
||||
login_data = {
|
||||
"username": USERNAME,
|
||||
"password": PASSWORD
|
||||
}
|
||||
|
||||
try:
|
||||
print("\n1. Attempting login...")
|
||||
response = requests.post(
|
||||
login_url,
|
||||
json=login_data,
|
||||
verify=False,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(" ✅ Login successful!")
|
||||
|
||||
# Step 2: Get clients
|
||||
api_url = f"{CONTROLLER_URL}/proxy/network/api/s/{SITE_ID}/stat/sta"
|
||||
cookies = response.cookies
|
||||
|
||||
print(f"\n2. Fetching clients from site '{SITE_ID}'...")
|
||||
api_response = requests.get(
|
||||
api_url,
|
||||
cookies=cookies,
|
||||
verify=False,
|
||||
timeout=10
|
||||
)
|
||||
api_response.raise_for_status()
|
||||
|
||||
data = api_response.json()
|
||||
clients = data.get('data', [])
|
||||
|
||||
print(f" ✅ Found {len(clients)} connected clients")
|
||||
|
||||
print("\n3. Sample clients:")
|
||||
for idx, client in enumerate(clients[:5], 1):
|
||||
name = client.get('name', client.get('hostname', 'Unknown'))
|
||||
ip = client.get('ip', 'N/A')
|
||||
mac = client.get('mac', 'N/A')
|
||||
device_type = 'wireless' if client.get('is_wired', True) is False else 'wired'
|
||||
print(f" {idx}. {name} ({device_type})")
|
||||
print(f" IP: {ip} | MAC: {mac}")
|
||||
|
||||
print("\n✅ Connection test PASSED!")
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"\n ❌ Connection failed: {e}")
|
||||
print(" Make sure the controller is running and URL is correct")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"\n ❌ HTTP error: {e}")
|
||||
print(" Check username/password")
|
||||
except Exception as e:
|
||||
print(f"\n ❌ Error: {e}")
|
||||
248
skills/unifi/unifi_monitor.py
Normal file
248
skills/unifi/unifi_monitor.py
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UniFi Network Monitor
|
||||
Comprehensive monitoring for UniFi Controller
|
||||
"""
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Disable SSL warnings
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
class UniFiMonitor:
|
||||
def __init__(self, controller_url, username, password, site_id="default"):
|
||||
self.controller_url = controller_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.site_id = site_id
|
||||
self.cookies = None
|
||||
self.authenticated = False
|
||||
|
||||
def login(self):
|
||||
"""Authenticate with UniFi Controller"""
|
||||
url = f"{self.controller_url}/api/auth/login"
|
||||
data = {
|
||||
"username": self.username,
|
||||
"password": self.password
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, json=data, verify=False, timeout=10)
|
||||
response.raise_for_status()
|
||||
self.cookies = response.cookies
|
||||
self.authenticated = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Login failed: {e}")
|
||||
return False
|
||||
|
||||
def get_clients(self):
|
||||
"""Get all connected clients"""
|
||||
if not self.authenticated:
|
||||
return []
|
||||
|
||||
url = f"{self.controller_url}/proxy/network/api/s/{self.site_id}/stat/sta"
|
||||
try:
|
||||
response = requests.get(url, cookies=self.cookies, verify=False, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json().get('data', [])
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get clients: {e}")
|
||||
return []
|
||||
|
||||
def get_devices(self):
|
||||
"""Get all UniFi devices (APs, switches, gateways)"""
|
||||
if not self.authenticated:
|
||||
return []
|
||||
|
||||
url = f"{self.controller_url}/proxy/network/api/s/{self.site_id}/stat/device"
|
||||
try:
|
||||
response = requests.get(url, cookies=self.cookies, verify=False, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json().get('data', [])
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get devices: {e}")
|
||||
return []
|
||||
|
||||
def get_health(self):
|
||||
"""Get network health status"""
|
||||
if not self.authenticated:
|
||||
return {}
|
||||
|
||||
url = f"{self.controller_url}/proxy/network/api/s/{self.site_id}/stat/health"
|
||||
try:
|
||||
response = requests.get(url, cookies=self.cookies, verify=False, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json().get('data', {})
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get health: {e}")
|
||||
return {}
|
||||
|
||||
def get_alerts(self, hours=24):
|
||||
"""Get recent alerts"""
|
||||
if not self.authenticated:
|
||||
return []
|
||||
|
||||
url = f"{self.controller_url}/proxy/network/api/s/{self.site_id}/stat/event"
|
||||
try:
|
||||
response = requests.get(url, cookies=self.cookies, verify=False, timeout=10)
|
||||
response.raise_for_status()
|
||||
events = response.json().get('data', [])
|
||||
|
||||
# Filter alerts from last N hours
|
||||
cutoff = datetime.now() - timedelta(hours=hours)
|
||||
alerts = []
|
||||
for event in events:
|
||||
if event.get('subsystem') in ['wlan', 'wan', 'lan']:
|
||||
event_time = datetime.fromtimestamp(event.get('time', 0) / 1000)
|
||||
if event_time > cutoff:
|
||||
alerts.append(event)
|
||||
return alerts[:10] # Last 10 alerts
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get alerts: {e}")
|
||||
return []
|
||||
|
||||
def get_bandwidth_stats(self):
|
||||
"""Get bandwidth statistics"""
|
||||
devices = self.get_devices()
|
||||
total_rx = 0
|
||||
total_tx = 0
|
||||
|
||||
for device in devices:
|
||||
stats = device.get('stat', {})
|
||||
total_rx += stats.get('rx_bytes', 0)
|
||||
total_tx += stats.get('tx_bytes', 0)
|
||||
|
||||
# Convert to GB
|
||||
total_rx_gb = total_rx / (1024**3)
|
||||
total_tx_gb = total_tx / (1024**3)
|
||||
|
||||
return {
|
||||
'rx_gb': round(total_rx_gb, 2),
|
||||
'tx_gb': round(total_tx_gb, 2),
|
||||
'total_gb': round(total_rx_gb + total_tx_gb, 2)
|
||||
}
|
||||
|
||||
def generate_report(self, output_format="text"):
|
||||
"""Generate comprehensive network report"""
|
||||
if not self.login():
|
||||
return "[ERROR] Failed to authenticate with UniFi Controller"
|
||||
|
||||
# Collect data
|
||||
clients = self.get_clients()
|
||||
devices = self.get_devices()
|
||||
health = self.get_health()
|
||||
alerts = self.get_alerts(hours=24)
|
||||
bandwidth = self.get_bandwidth_stats()
|
||||
|
||||
report = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'summary': {
|
||||
'connected_clients': len(clients),
|
||||
'total_devices': len(devices),
|
||||
'alerts_24h': len(alerts),
|
||||
'wan_status': 'Unknown'
|
||||
},
|
||||
'clients': clients,
|
||||
'devices': devices,
|
||||
'bandwidth': bandwidth,
|
||||
'alerts': alerts
|
||||
}
|
||||
|
||||
if output_format == "json":
|
||||
return json.dumps(report, indent=2)
|
||||
|
||||
# Text format
|
||||
text_report = f"""# UniFi Network Report - {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
|
||||
## [STATS] Network Overview
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Connected Clients | {len(clients)} |
|
||||
| UniFi Devices | {len(devices)} |
|
||||
| WAN Status | {health.get('wan', {}).get('status', 'Unknown')} |
|
||||
| Alerts (24h) | {len(alerts)} |
|
||||
|
||||
## [NET] Bandwidth Usage (All Time)
|
||||
|
||||
| Direction | Usage |
|
||||
|-----------|-------|
|
||||
| Download (RX) | {bandwidth['rx_gb']} GB |
|
||||
| Upload (TX) | {bandwidth['tx_gb']} GB |
|
||||
| **Total** | **{bandwidth['total_gb']} GB** |
|
||||
|
||||
## [CLIENTS] Connected Clients
|
||||
|
||||
"""
|
||||
|
||||
# Group clients by connection type
|
||||
wired = []
|
||||
wireless = []
|
||||
|
||||
for client in clients:
|
||||
if client.get('is_wired', False):
|
||||
wired.append(client)
|
||||
else:
|
||||
wireless.append(client)
|
||||
|
||||
if wireless:
|
||||
text_report += f"### [WIFI] Wireless ({len(wireless)} clients)\n\n"
|
||||
for idx, client in enumerate(wireless[:10], 1):
|
||||
name = client.get('name', client.get('hostname', 'Unknown'))
|
||||
ip = client.get('ip', 'N/A')
|
||||
mac = client.get('mac', 'N/A')
|
||||
signal = client.get('signal', 0)
|
||||
ap_name = client.get('ap_name', 'Unknown AP')
|
||||
text_report += f"{idx}. **{name}**\n"
|
||||
text_report += f" - IP: {ip} | MAC: {mac}\n"
|
||||
text_report += f" - Signal: {signal} dBm | AP: {ap_name}\n\n"
|
||||
|
||||
if wired:
|
||||
text_report += f"### [WIRED] Wired ({len(wired)} clients)\n\n"
|
||||
for idx, client in enumerate(wired[:5], 1):
|
||||
name = client.get('name', client.get('hostname', 'Unknown'))
|
||||
ip = client.get('ip', 'N/A')
|
||||
mac = client.get('mac', 'N/A')
|
||||
switch = client.get('sw_name', 'Unknown Switch')
|
||||
text_report += f"{idx}. **{name}** - {ip}\n"
|
||||
|
||||
# Network Devices
|
||||
text_report += f"\n## [DEVICES] UniFi Devices ({len(devices)} total)\n\n"
|
||||
for device in devices[:10]:
|
||||
name = device.get('name', device.get('mac', 'Unknown'))
|
||||
model = device.get('model', 'Unknown')
|
||||
status = "[OK] Online" if device.get('state') == 1 else "[DOWN] Offline"
|
||||
uptime = device.get('uptime', 0)
|
||||
uptime_str = f"{uptime // 86400}d {(uptime % 86400) // 3600}h"
|
||||
text_report += f"- **{name}** ({model}) - {status} - Uptime: {uptime_str}\n"
|
||||
|
||||
# Alerts
|
||||
if alerts:
|
||||
text_report += f"\n## [ALERTS] Recent Alerts ({len(alerts)} in 24h)\n\n"
|
||||
for alert in alerts[:5]:
|
||||
msg = alert.get('msg', 'Unknown alert')
|
||||
time = datetime.fromtimestamp(alert.get('time', 0) / 1000).strftime('%H:%M')
|
||||
text_report += f"- [{time}] {msg}\n"
|
||||
else:
|
||||
text_report += "\n## [OK] No Alerts (24h)\n\n"
|
||||
|
||||
return text_report
|
||||
|
||||
def main():
|
||||
"""Main entry point for cron jobs"""
|
||||
# Load credentials from environment or config
|
||||
controller_url = os.getenv('UNIFI_URL', 'https://192.168.0.39:8443')
|
||||
username = os.getenv('UNIFI_USER', 'corey')
|
||||
password = os.getenv('UNIFI_PASS', 'is41945549')
|
||||
|
||||
monitor = UniFiMonitor(controller_url, username, password)
|
||||
report = monitor.generate_report(output_format="text")
|
||||
print(report)
|
||||
return report
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user