unifi2mqtt

September 16, 2026 · View on GitHub

mqtt-smarthome npm License

Ubiquiti UniFi network controller to MQTT: client presence, devices, WLANs

Connects to a UniFi Network controller — a UniFi OS console (Dream Machine, Dream Router, Cloud Key Gen2, Cloud Gateway) or a self-hosted legacy controller — and bridges it to an MQTT broker, following the mqtt-smarthome architecture. Every wireless and wired client gets a retained presence topic, connect/disconnect events arrive instantly over the controller's event websocket, WLANs and device LEDs can be switched, and everything is announced to Home Assistant via MQTT discovery (one device_tracker per client).

Status: 2.0.0 is a complete rewrite that has not yet been verified against a real controller. It follows the documented UniFi OS and legacy APIs and is tested against fixture responses; see Controller compatibility and please report what works and what does not. 1.x users: see Upgrading from 1.x.

Install

npm install -g unifi2mqtt

Requires Node.js ^20.19, ^22.12 or >= 24. unifi2mqtt 2 is built on mqtt-interfaces-core (mqtt-smarthome spec 2.x) like the author's other adapters.

Create a local user on the controller for unifi2mqtt (UniFi OS: Settings → Admins & Users → Admin, "Restrict to local access only"; a read-only role is enough for status, the set topics need an admin). Cloud/SSO accounts and MFA do not work with the API.

Usage

unifi2mqtt --controller https://192.168.1.1 --username unifi2mqtt --password s3cret -k --mqtt-url mqtt://broker
optiondefaultdescription
-c, --controllercontroller url, required: https://<console> (UniFi OS) or https://<host>:8443 (legacy controller)
--username, --passwordcontroller credentials, required: a local account without 2fa (not a Ubiquiti sso login)
--sitedefaultsite id (the part after /site/ in the controller url; not the display name)
--modeautoapi flavour: unifi-os (UDM, UDR, UCK G2, Cloud Gateway) or legacy (self-hosted controller); auto detects
-k, --insecureoffaccept the controller's tls certificate without validation (consoles ship self-signed certificates)
--eventsonsubscribe to the controller's event websocket for instant client events; --no-events for polling only
--poll-interval60seconds between polls of clients, devices and wlans (min 5)
--presence-timeout0seconds a client may be gone before it is reported absent (0 = immediately)
--clientsallonly publish these clients: mac addresses, names or hostnames, comma separated
--client-keynamehow clients are named in topics: name (alias, else hostname, else mac), hostname or mac
--ha-clientsonannounce a device_tracker per client to Home Assistant; --no-ha-clients for controller and devices only
--publish-rawoffadditionally publish every websocket frame of the controller as <name>/raw
-u, --mqtt-urlmqtt://localhostbroker URL, see MQTT.js
--mqtt-username, --mqtt-passwordbroker credentials
--mqtt-tls-caCA certificate file for mqtts://
-n, --nameunifiinstance name, used as topic prefix
--json-payloadsonstatus as {"val", "ts", "lc"} JSON; --no-json-payloads for plain values
--ha-discoveryonHome Assistant MQTT discovery (--no-ha-discovery disables and clears it)
--ha-prefixhomeassistantdiscovery prefix
--maintenanceonaccept <name>/maintenance/set/{loglevel,restart}; --no-maintenance disables
-v, --verbosityinfoerror, warn, info, debug
--install / --uninstallinstall/remove the systemd service unifi2mqtt@<name>
--config-schemaprint the JSON Schema of all options and exit

Every option can also be set via environment variable with the prefix UNIFI2MQTT_, e.g. UNIFI2MQTT_CONTROLLER=https://192.168.1.1 UNIFI2MQTT_PASSWORD=… unifi2mqtt; the broker settings fall back to the unprefixed MQTT_URL, MQTT_USERNAME, MQTT_PASSWORD.

Run as a systemd service

sudo unifi2mqtt --install --name unifi -c https://192.168.1.1 --username unifi2mqtt --password s3cret -k -u mqtt://192.168.1.2

--install creates a system user unifi2mqtt, writes the given options to /etc/unifi2mqtt/<name>.env (UNIFI2MQTT_* variables, 0640 root:unifi2mqtt — edit and systemctl restart unifi2mqtt@<name> to change), installs the template unit /etc/systemd/system/unifi2mqtt@.service and enables + starts unifi2mqtt@<name>. The instance name is the --name option, i.e. the MQTT topic prefix. Broker settings shared by all mqtt-interfaces adapters on the host can go to /etc/mqtt-interfaces/broker.env (MQTT_URL, MQTT_USERNAME, MQTT_PASSWORD). Logs: journalctl -u unifi2mqtt@<name> -f.

Several sites or controllers: run --install once per site with a different --name — each becomes its own instance with its own config and topic prefix, sharing one template unit and one system user:

sudo unifi2mqtt --install --name unifi        -c https://192.168.1.1 --site default …
sudo unifi2mqtt --install --name unifi-office -c https://192.168.1.1 --site office …
systemctl status 'unifi2mqtt@*'

sudo unifi2mqtt --uninstall --name unifi-office removes one instance (the template unit goes with the last one). she can install, configure and update instances from its Services page (config form from --config-schema, the password masked).

Docker

docker run -d --name unifi2mqtt --restart unless-stopped \
  -e UNIFI2MQTT_CONTROLLER=https://192.168.1.1 \
  -e UNIFI2MQTT_USERNAME=unifi2mqtt \
  -e UNIFI2MQTT_PASSWORD=s3cret \
  -e UNIFI2MQTT_INSECURE=true \
  -e UNIFI2MQTT_MQTT_URL=mqtt://broker \
  ghcr.io/hobbyquaker/unifi2mqtt

Topics

<name> defaults to unifi. Names of clients, devices and WLANs are used as topic levels after sanitising: whitespace, /, + and # become _ (Home WiFiHome_WiFi); duplicate client names get a _<last 4 mac digits> suffix.

<name>/connected

Retained. 0 = not connected to the broker (set via last will), 1 = connected to the broker but not to the controller (login failed, controller unreachable, last poll failed), 2 = the controller answers.

<name>/info and <name>/maintenance/set/…

<name>/info (retained JSON) describes the running instance: package name and version, mqtt-smarthome spec version, node version, host, pid, start time, controller url, site, detected api flavour (mode), whether the event websocket is connected (events), poll interval. <name>/maintenance/set/loglevel (error|warn|info|debug) changes the log level at runtime, <name>/maintenance/set/restart exits cleanly so the service manager restarts the process; --no-maintenance turns both off.

<name>/status/<item>

Retained status reports (except events), published after every poll and instantly on websocket events — only when the value changed. Every status is {"val": <value>, "ts": <ms received>, "lc": <ms last changed>}; with --no-json-payloads the plain value (objects as JSON).

itemtypenotes
client/<key>/presentbooltrue while the controller lists the client (or a connect event arrived), false after a disconnect or a poll without it (see below)
client/<key>/detailsobject{mac, name, hostname, ip, wired, ssid, band, channel, ap, network}band is 2.4 GHz / 5 GHz / 6 GHz, ap the access point / switch name, null fields are unknown
event/clientobjectnot retained: {type, client, mac, hostname, ssid, ap, wired, guest, time, msg}; type is connected, disconnected, roam, roam_radio
client_countintclients present, all networks
client_count/wirelessintwireless clients present
client_count/wiredintwired clients present
wifi/<ssid>/enabledboolWLAN enabled — settable
wifi/<ssid>/client_countintclients on this SSID
device/<key>/onlineboolaccess point / switch / gateway is connected to the controller
device/<key>/ledstringon, off or default (site setting) — settable
device/<key>/clientsintclients on this device
device/<key>/detailsobject{mac, name, model, type, ip, version}

<key> of a client is, with the default --client-key name, the alias set in the controller, else the hostname, else the mac address (--client-key hostname / mac to choose). A client that was seen once keeps its topics; when it is renamed the old topics are cleared and the new ones published. With --clients only the listed clients get topics (counts still cover everyone).

After a restart the adapter reads back what it retained on the broker in a previous run and, once the first poll is in, clears every client/…, device/… and wifi/… topic the controller no longer reports — a client that left while the adapter was down, a renamed client, a client now excluded by --clients. Nothing stays present: true from a previous life (#22); the log says how many items were cleared. Needs mqtt-interfaces-core 0.16; on an older core the readback is skipped.

Presence comes from two sources: the client list (stat/sta) polled every --poll-interval seconds, and the controller's event websocket (connect / disconnect / roam events, instant). A client disappears (present: false) when a disconnect event arrives or a poll no longer lists it — immediately with the default --presence-timeout 0, or after that many seconds without a sighting, which smooths phones that drop off the WLAN for a moment. Devices and WLANs are polled with the clients; device events (adopted, restarted, …) trigger an extra refresh.

<name>/set/<item>

Change requests. Payload is a plain value or mqtt-smarthome style JSON ({"val": true}).

topicpayload
<name>/set/wifi/<ssid>/enabledtrue/false, 1/0, on/off
<name>/set/device/<key>/ledon, off, default (booleans map to on/off)
<name>/set/device/<key>/provisionany payload — force-provision the device (cmd/devmgr), for an AP that did not pick up a WLAN change
mosquitto_pub -t unifi/set/wifi/Guests/enabled -m false
mosquitto_pub -t unifi/set/device/AP_Living_Room/led -m off

The status is re-read from the controller after the command; a rejected command is logged at warn.

<name>/raw

With --publish-raw every frame the controller sends on its event websocket is published as <name>/raw (not retained) — useful to see what your controller version emits (sta:sync, device:sync, alert, events, …) and to collect fixtures.

Home Assistant

MQTT discovery is on by default (HA ≥ 2024.11, device-based discovery). The controller/site appears as a device with client count sensors and one switch (enabled) + one sensor (clients) per WLAN. Every access point, switch and gateway is its own device (linked via the bridge) with a connectivity sensor, an LED select (on/off/default) and a client count. Every client is a device with a device_tracker entity (source_type: router, home/not_home) whose attributes are the client's details (mac, ip, ssid, ap, …); --no-ha-clients skips the clients, --clients limits them. Availability follows <name>/connected.

Device and entity ids are based on mac addresses (unifi2mqtt_<name>_client_<mac>), so renaming a client in the controller keeps its HA history. --no-ha-discovery disables discovery and removes the announcements on startup; --ha-prefix changes the discovery prefix.

Controller compatibility

unifi2mqtt talks to the controller directly (no node-unifi, no ubnt-unifi): POST /api/auth/login + /proxy/network/api/s/<site>/… on UniFi OS, POST /api/login + /api/s/<site>/… on legacy controllers, the session cookie and CSRF token as the ui uses them, the event websocket at …/wss/s/<site>/events. The account must be a local controller user without two-factor authentication — a Ubiquiti SSO login does not work with the local login endpoints, and a 2FA challenge cannot be answered non-interactively; the adapter says so in the log instead of retrying. --mode auto decides by GET /: a UniFi OS console answers 200, a legacy controller redirects to /manage. --mode unifi-os / legacy overrides.

controllerstatus
UniFi OS consoles (UDM, UDM Pro/SE, UDR, UCK G2+, UCG)implemented after the documented api, not yet verified
legacy self-hosted Network application (:8443)implemented after the 1.x adapter's api use, not yet verified

A login the controller rate-limits (HTTP 429 — UniFi OS answers AUTHENTICATION_FAILED_LIMIT_REACHED after too many failed logins and locks the account for a while) is not retried every 10 s, because each attempt can extend the lockout: the adapter waits for Retry-After when the controller sends it, else 5 minutes, doubling up to 30, and says so at warn on every attempt (#23).

Run with --verbosity debug to see every request and websocket frame. The quickest way to help is a dump from your controller:

git clone https://github.com/hobbyquaker/unifi2mqtt && cd unifi2mqtt && npm ci
node scripts/dump.js -c https://192.168.1.1 --username unifi2mqtt --password … -k --seconds 120

It logs in, fetches stat/sta, stat/device, rest/wlanconf and a few more endpoints, listens to the event websocket for --seconds (connect or disconnect a wifi client meanwhile) and writes everything to ./dump/, redacted: macs keep their vendor part, public ips, names, SSIDs, serials and every secret are replaced consistently. Skim the files, then attach the directory to a GitHub issue together with the controller model and Network version. Nothing is written to the controller, no broker is needed.

Upgrading from 1.x

unifi2mqtt 2.0 is a rewrite on mqtt-interfaces-core; the topic layout changed:

1.x2.0
status/wifi/<ssid>/client/<hostname> {val, mac, ts}status/client/<key>/present {val, ts, lc} + status/client/<key>/details
status/wifi/<ssid>/event/connected {val: hostname, …}status/event/client {val: {type: "connected", client, mac, ssid, ap, …}}
status/wifi/<ssid>/event/disconnectedstatus/event/client with type: "disconnected"
status/wifi/<ssid>/clientCountstatus/wifi/<ssid>/client_count
status/wifi/<ssid>/enabled, set/wifi/<ssid>/enabledunchanged (SSID sanitised: Home WiFiHome_WiFi)
status/clientCount (wireless)status/client_count (all), status/client_count/wireless, …/wired
status/device/<name>/led, set/device/<name>/ledunchanged (name sanitised) + device/<name>/online, /clients, /details
-a/--unifi-host, -p/--unifi-port-c/--controller https://host[:port]
-c/--unifi-user, -s/--unifi-password, -w/--unifi-site--username, --password, --site
-u/--url-u/--mqtt-url (--url alias kept)
env unifi-password=…UNIFI2MQTT_PASSWORD=… (see --config-schema)
pm2sudo unifi2mqtt --install --name unifi … or Docker

Wired clients, UniFi OS consoles, <name>/info, maintenance topics, --presence-timeout, --clients, Home Assistant discovery and the systemd installer are new. See CHANGELOG.md.

License

MIT © Sebastian Raff