Fresh start - excluded large ROM JSON files

This commit is contained in:
OpenClaw Agent
2026-04-11 09:45:12 -05:00
commit 5deb387aa6
395 changed files with 47744 additions and 0 deletions

59
skills/unifi/README.md Normal file
View 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
View 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

Binary file not shown.

6
skills/unifi/config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"url": "https://192.168.0.39:8443",
"username": "corey",
"password": "is41945549",
"site": "default"
}

View File

@@ -0,0 +1,6 @@
{
"url": "https://192.168.0.39:8443",
"username": "admin",
"password": "YOUR_PASSWORD_HERE",
"site": "default"
}

View 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)

View 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

View 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'

View 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

View 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"

View 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

View 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

View 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.

View 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'

View 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

View 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)

View File

@@ -0,0 +1,6 @@
{
"url": "https://192.168.0.39:8443",
"username": "corey",
"password": "is41945549",
"site": "default"
}

View 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}")

View 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()