unifi-cli
CLI for UniFi Network controller with an interactive TUI dashboard. Designed for both human operators and AI agents.
Quick start
# Install (pick one)
# Configure
# Use
Generate an API key in your UniFi controller under Settings > API.
Installation
From crates.io
From PyPI
# or run without installing:
From GitHub releases
Pre-built binaries for Linux (x64, arm64), macOS (x64, arm64), and Windows (x64) on the releases page.
Configuration
Run unifi config init for interactive setup, or configure manually:
Environment variables
# Optional for lab controllers with self-signed or otherwise invalid TLS certs:
Config file
~/.config/unifi/config.toml:
= "https://unifi.example.com"
= "YOUR_KEY"
# Optional; defaults to false.
= false
Multi-controller profiles
[]
= "https://home.example.com"
= "KEY_1"
[]
= "https://office.example.com"
= "KEY_2"
# or: UNIFI_PROFILE=office unifi clients list
CLI flags
Priority: CLI flags > environment variables > config file.
TLS certificates are verified by default. For a local controller with a
self-signed certificate, pass --accept-invalid-certs, set
UNIFI_ACCEPT_INVALID_CERTS=true, or set accept_invalid_certs = true in the
config file. Only use this on trusted networks because it weakens protection for
API keys, passwords, session cookies, and stream URLs.
When unifi config init cannot verify the controller's certificate, it offers
to trust the controller and saves accept_invalid_certs = true for you.
Destructive commands
clients block, clients unblock, clients kick, devices restart,
devices upgrade, ports cycle and protect rtsps delete all ask before they
act. On a terminal you get a yes/no question naming the target; declining exits
2 with kind: confirmation_required and sends nothing. When stdin is not a
terminal there is nobody to ask, so they refuse with the same error unless you
pass --yes, which skips the question everywhere.
TUI dashboard
Real-time dashboard with:
- Client list with bandwidth, connection info, and signal strength
- Device overview with status and firmware versions
- Event feed from the controller
- Client actions: kick, block/unblock, lock/unlock AP
- Device actions: restart, upgrade firmware, locate LED
- Filter clients by name with
/
Live port monitor
Commands
Clients
Devices
Ports
Find which switch port a device is plugged into, then power-cycle just that port instead of rebooting the whole switch:
# Which port is my Pi on? Matches by name (case-insensitive substring),
# MAC, or IP.
# Inspect it: PoE mode, class, voltage, current, and what's attached
# Bounce PoE on that port only, leaving the rest of the switch untouched
ports find's output feeds directly into show and cycle: device_mac
and port_idx are the switch's MAC and port index, not the attached
device's. A name is ambiguous only when it matches more than one device
that's actually on a switch port; that returns kind: conflict (exit 6)
listing the candidates rather than guessing. Other client records sharing
the name (a device's WiFi interface reporting under the same name as its
wired one, say) don't cause a conflict if they're not themselves on a port.
A device that has moved between switch ports appears once per port it has
ever used, with a connected field distinguishing its current port from
stale history.
ports show exposes PoE telemetry the CLI previously discarded:
poe_mode, poe_class, poe_voltage, poe_current, poe_good, and the
MAC of the attached device (attached_mac).
ports cycle is destructive. On a terminal it shows what is about to lose
power and asks for confirmation; when piped it requires --yes and
otherwise exits 2 with kind: confirmation_required. It reads the port
table first, then refuses without ever sending the power-cycle command
when:
- the port is not PoE-capable (an SFP+ port, say) →
kind: conflict, exit 6 - the port's PoE is administratively off →
kind: conflict, exit 6 - the port isn't currently delivering PoE (
poe_enable: false) →kind: conflict, exit 6 - the device has no such port index →
kind: not_found, exit 4
The off interval (how long the port stays unpowered) is chosen by the switch firmware, not by this CLI. The power-cycle command takes only the target port, with no duration parameter, on either the legacy endpoint or the Integration API, so the interval isn't configurable and varies by device model and firmware version. IEEE 802.3 PoE detection timing imposes a floor regardless: expect the port to sit dark for roughly 1-2 seconds at minimum before power returns.
List ports for one device, or across every device:
ports list returns the paginated {items, total, limit, offset} envelope
used by the other list commands. unifi devices ports <MAC> remains an
alias for unifi ports list <MAC>; it keeps its original bare-JSON-array
shape for backward compatibility, and both emit the same per-row fields,
including device_mac and device_name.
Events
Networks
System
Configuration
Shell completions
Agent-friendly design
unifi-cli is designed to work well with AI agents and automation scripts.
Automatic JSON output
When stdout is not a terminal (piped or redirected), output switches to JSON automatically:
# Human at terminal: formatted table
# Agent piping output: JSON automatically
data=
# Force JSON mode
Clean stdout/stderr separation
Data goes to stdout. Messages go to stderr. Piping always captures clean data:
Structured mutation responses
# {"action": "block", "mac": "AA:BB:CC:DD:EE:FF", "status": "ok"}
Runtime schema introspection
Distinct exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Configuration error |
| 3 | Authentication error (401/403) |
| 4 | Not found (404) |
| 5 | API error (server error) |
| 6 | Conflict (ambiguous match or failed precondition) |
Development
License
MIT