Skip to main content

Typed Python client for the UniFi Network API

Project description

matonb-unifi

CI PyPI version Python versions License: MIT

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 (UnifiError and friends) — no need to catch httpx

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=False to 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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

matonb_unifi-0.1.0.tar.gz (15.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

matonb_unifi-0.1.0-py3-none-any.whl (10.7 kB view details)

Uploaded Python 3

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

Hashes for matonb_unifi-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1c7a6a559b5e284ddb352442c428f7c3d3885726f2d9b24a4ad1298610e2018f
MD5 780080d6f3fd1010dde786aff98a45f5
BLAKE2b-256 94a5e8f661610ebf2b2029dd10333c53820d62d543ce49d5b062ade9e2838f43

See more details on using hashes here.

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

Hashes for matonb_unifi-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f457f7dcb82ddafe9a1a5971e5895605dba2aff4d6c446888b3d1e79d25d2a31
MD5 1a8907d41006da59c9afe77471715564
BLAKE2b-256 e9adbdceba72edb6c9111c256b46fd56876d868894d6205cfafbdb5c7e0682de

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page