Typed Python client for the UniFi Network API
Project description
matonb-unifi
A small, typed Python client for the UniFi Network API.
Built on httpx, it returns plain dataclasses
instead of raw JSON and supports both modern UniFi OS (UDM / API key) and legacy
standalone controllers (username / password).
Features
- API-key auth for modern UniFi OS, or username/password for legacy controllers
- Typed return values (
UnifiSta,UnifiDevice,Port,PortOverride) — no dict spelunking - Context-manager lifecycle that logs in and out for you
- Read client stations and infrastructure devices
- Look up a device by name or MAC with
find_device() - Set per-port PoE mode, including a built-in
cycle_port_poe()power-cycle helper - Typed exception hierarchy (
UnifiErrorand friends) — no need to catchhttpx
Requirements
- Python >= 3.10
httpx >= 0.27
Installation
pip install matonb-unifi
Or with uv:
uv add matonb-unifi
Internal homelab builds are published to the Gitea package registry at
gitea.bm-home.lan/bm-home/matonb-unifi.
Quick start
Modern UniFi OS (API key)
Generate an API key in the UniFi console under Settings → Control Plane → Integrations.
from matonb_unifi import UnifiClient
with UnifiClient("https://192.168.1.1", api_key="your-api-key") as unifi:
for sta in unifi.get_clients():
print(sta.name or sta.hostname or sta.mac, "->", sta.ip)
for dev in unifi.get_devices():
print(dev.name, dev.model, dev.ip)
Legacy controller (username / password)
from matonb_unifi import UnifiClient
with UnifiClient(
"https://unifi.example.com:8443",
username="admin",
password="secret",
legacy=True,
verify_ssl=False, # controller uses a self-signed certificate
) as unifi:
devices = unifi.get_devices()
When credentials are supplied, the context manager calls login() on enter and
logout() on exit automatically. With an API key, no login round-trip is needed.
TLS certificates are verified by default. Many UniFi controllers ship a self-signed certificate — pass
verify_ssl=Falseto connect to those, as an informed opt-out rather than a silent default.
Configuration
UnifiClient(base_url, **options)
| Argument | Type | Default | Description |
|---|---|---|---|
base_url |
str |
required | Controller URL, e.g. https://192.168.1.1 |
api_key |
str |
None |
API key for modern UniFi OS (X-API-KEY) |
username |
str |
None |
Username for legacy auth (requires password) |
password |
str |
None |
Password for legacy auth (requires username) |
site |
str |
"default" |
UniFi site name |
verify_ssl |
bool |
True |
Verify TLS certificates; set False for self-signed controllers |
legacy |
bool |
False |
Use legacy API paths (/api/s/... instead of /proxy/network/...) |
timeout |
float |
30.0 |
Per-request timeout in seconds |
You must provide either api_key or both username and password;
otherwise the constructor raises ValueError.
API
get_clients() -> list[UnifiSta]
Returns all connected client stations (wired and wireless).
@dataclass
class UnifiSta:
mac: str
ip: str | None = None # last_ip, falling back to fixed_ip
hostname: str = ""
name: str = ""
get_devices() -> list[UnifiDevice]
Returns all UniFi infrastructure devices (switches, APs, gateways, UDMs).
@dataclass
class UnifiDevice:
id: str # UniFi _id, used for updates
mac: str
name: str
device_type: str # e.g. "usw", "uap", "ugw", "udm"
model: str = "Generic"
ip: str | None = None
port_overrides: list[PortOverride] = [] # admin-configured overrides
ports: list[Port] = [] # live port_table status
def current_poe_mode(self, port_idx: int) -> str:
"""Effective PoE mode for a port: an override wins, else the live
port_table value, else "unknown"."""
@dataclass
class PortOverride:
port_idx: int
poe_mode: str = "auto"
@dataclass
class Port:
port_idx: int
name: str = ""
poe_mode: str = "" # live value reported by the device
poe_enable: bool = False
Use current_poe_mode() to read a port's effective state without juggling
overrides and port_table yourself:
switch = next(d for d in unifi.get_devices() if d.name == "office-switch")
print(switch.current_poe_mode(3)) # -> "auto", "off", or "unknown"
find_device(identifier) -> UnifiDevice | None
Look up a single device by name or MAC address (case-insensitive; the MAC
may use : or - separators). Returns None if nothing matches.
switch = unifi.find_device("office-switch") # by name
switch = unifi.find_device("de-ad-be-ef-00-02") # by MAC
set_port_poe(device, port_modes, current_overrides=None) -> None
Sets the PoE mode for one or more switch ports in a single API call. Existing overrides are merged, so untouched ports are left as-is.
| Argument | Type | Description |
|---|---|---|
device |
UnifiDevice | str |
A UnifiDevice or its _id |
port_modes |
dict[int, str] |
Map of port_idx → PoE mode ("auto", "off", …) |
current_overrides |
list[dict] | list[PortOverride] | None |
Existing overrides; if omitted they are taken from device or re-fetched |
# Turn PoE off on ports 3 and 5 — just pass the device, overrides are handled
switch = unifi.find_device("office-switch")
unifi.set_port_poe(switch, {3: "off", 5: "off"})
PoE modes:
"auto"enables controller-negotiated PoE ("on"),"off"disables it.
cycle_port_poe(device, port_idxs, *, delay=5.0, restore_mode="auto") -> None
Power-cycle PoE on one or more ports: turn them off, wait delay seconds, then
restore them (re-fetching the device so the restore merges cleanly). Handy for
rebooting a stuck PoE camera or AP.
switch = unifi.find_device("office-switch")
unifi.cycle_port_poe(switch, [3, 5], delay=5)
Lifecycle methods
login()— authenticate (legacy/credential auth only)logout()— end the session (best-effort; never raises)close()— close the underlying HTTP connection
Using the client as a context manager handles all three for you.
Error handling
The library raises its own exceptions, so callers never have to import or know
about httpx. All of them subclass UnifiError:
| Exception | Raised when |
|---|---|
UnifiAuthError |
Login failed, or a request returned 401/403 |
UnifiConnectionError |
The controller was unreachable (refused, timeout, DNS, TLS) |
UnifiAPIError |
The controller returned another non-success status (.status_code) |
UnifiError |
Base class — catch this to handle any of the above |
from matonb_unifi import UnifiClient, UnifiAuthError, UnifiError
try:
with UnifiClient("https://192.168.1.1", api_key="bad-key") as unifi:
unifi.get_clients()
except UnifiAuthError:
print("Check your API key")
except UnifiError as exc:
print(f"UniFi request failed: {exc}")
Development
# Run the test suite
uv run --extra test pytest
# Lint and format
uv run --extra dev ruff check .
uv run --extra dev ruff format --check .
# Type-check (strict)
uv run --extra typecheck mypy
See CONTRIBUTING.md for the full workflow, including the Conventional Commits convention used for automated releases.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file matonb_unifi-0.1.0.tar.gz.
File metadata
- Download URL: matonb_unifi-0.1.0.tar.gz
- Upload date:
- Size: 15.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c7a6a559b5e284ddb352442c428f7c3d3885726f2d9b24a4ad1298610e2018f
|
|
| MD5 |
780080d6f3fd1010dde786aff98a45f5
|
|
| BLAKE2b-256 |
94a5e8f661610ebf2b2029dd10333c53820d62d543ce49d5b062ade9e2838f43
|
File details
Details for the file matonb_unifi-0.1.0-py3-none-any.whl.
File metadata
- Download URL: matonb_unifi-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f457f7dcb82ddafe9a1a5971e5895605dba2aff4d6c446888b3d1e79d25d2a31
|
|
| MD5 |
1a8907d41006da59c9afe77471715564
|
|
| BLAKE2b-256 |
e9adbdceba72edb6c9111c256b46fd56876d868894d6205cfafbdb5c7e0682de
|