A bridge that makes a non-UniFi device appear as a real, adopted UniFi device inside the UniFi Network controller — ports, stats, topology, and control. What it supports today: physical and virtual switches (an Arista, Proxmox nodes, a Podman host), a rack PDU and two kinds of UPS; the inform, adoption and control machinery underneath carries no vendor or device-type assumptions.
The controller only speaks its own device protocol. This bridge speaks the device side of it (the inform protocol, adoption, capabilities) on behalf of hardware it reads and configures through the vendor's API. UniFi becomes the place that device is managed from.
Everything below is verified live on UniFi Network 10.6 (a UniFi OS gateway), end to end from the UniFi UI — read (ports or outlets, stats, topology) and control (the controller's config applied back to the device).
| Device | Driver | Presented to UniFi as | Verified on |
|---|---|---|---|
| Arista EOS switch | arista-eos |
UDC48X6 ("USW Leaf") |
DCS-7160-48TC6-F, EOS 4.26.14M |
Proxmox VE node (vmbr0) |
proxmox |
UDC48X6 ("USW Leaf"), or USWF07D ("ECS Core") |
three-node PVE 9.1 cluster |
| APC switched rack PDU | apc-pdu |
USPPDUP ("Smart Power PDU Pro") |
AP7931, NMC AOS 3.9.2 |
| Podman host (containers as ports) | podman |
UDC48X6 ("USW Leaf") |
Podman 5.8.2, AlmaLinux 10.2 |
| APC Smart-UPS (Modbus TCP on its SmartConnect port) | apc-ups |
USPDA2B ("UPS 2U Pro"; USWDA25 "UPS 2U" also works) |
SMTL1500RM3UC, UPS 15.5 |
| APC Back-UPS Pro network model (its NMC's web pages) | apc-backups |
USPDA2B ("UPS 2U Pro") |
BG500, UPS 05.3, NMC AOS 6.0.1 |
"Verified on" means exactly that one device. What each driver has and has
not been tried on, and what a pull request needs to extend it (a PVE 8 node,
a Fedora CoreOS Podman host, another Arista model…), is in
docs/support-matrix.md — every gap there is marked
PRs welcome with the proof it needs.
disunified -list-drivers prints what a given build supports. Adding another
device means adding a driver: the neutral model, the wire protocol, adoption
and the inform loop carry no vendor assumptions. The model a driver claims
has to be one the bundled catalogue carries as type: usw or usp — which
is what -list-models prints: UniFi files its switches, its PDUs and two of
its UPSes as usw, and the UPS 2U Pro as usp (the power path). See
Working on this repo with an AI agent
and docs/adding-a-device.md.
What each driver does today:
arista-eos— a physical Arista switch read over eAPI (or SSH). Adoption, live port stats and counters, LLDP and MAC-table topology (clients appear behind the right port), optics, FEC, fans/PSUs; and control of port enable, names, speed, VLAN membership (plus VLAN creation), FEC, storm control, BPDU guard, STP mode/priority, IGMP snooping, and QSFP breakout split/join. EOS 4.26.14M is the last train for the 7160 (docs/drivers/arista-eos.md).proxmox— a Proxmox VE node's virtual switch,vmbr0: one port per guest NIC, numbered from 1 and the same on every node (the number lives in the guest's own Proxmox tags,unifi.p25.c), the node's physical NICs and bond members as the top ports counting down from 54. VM clients appear behind their ports in the topology, and control of port state and VLANs is written back as the guest'stag/trunks. The node's own networking underneath is reported, never reconfigured (apart from an optional, modelled-but-untested LACP conversion). Only cluster-wide numbering (numbering: cluster, the default) has been tested live (docs/drivers/proxmox.md).podman— a Podman host as a switch, the way a Proxmox node is one: every container network endpoint is a port named after the container, the host's NICs are the top ports with the default route's NIC as the uplink. A container on a macvlan network (its own LAN address) shows as the client behind its port; one on a Podman bridge network (NAT) is a port that counts traffic and, honestly, no client. Read over SSH; read-only (docs/drivers/podman.md).apc-ups— an APC Smart-UPS read over Modbus TCP from its own SmartConnect port (enabled at the LCD; no network card, no cable): the controller gets the battery pipeline (charge, runtime, on-battery, low battery, load, power, voltages), Safe Shutdown Pairing for a UniFi OS console, the outlet groups as switchable outlets, an optional load-shed policy written to the unit, and — behind the UI's "NUT Server" switch — a NUT server run by the bridge so other hosts can shut down on the same UPS (docs/drivers/apc-ups.md).apc-backups— an APC Back-UPS Pro network model (BG500/1000/1500) through the web pages of its embedded Network Management Card, which is all that card offers for this family (nothing over SNMP): battery, runtime, input, per-outlet watts, the two main and two switched outlet groups as outlets (the switched ones controllable through the card's two-step form), and the card's own address through config.ini (docs/drivers/apc-backups.md).apc-pdu— an APC switched rack PDU, whose outlets become outlets the controller can see, name and switch. The card has no API, so the driver uses two transports: SNMPv1 to read identity and outlet state and to switch an outlet, and a partialconfig.iniuploaded over FTP to set an outlet's name (the name objects are read-only over SNMP). Writes need an SNMP community of access typeWrite+, andcontrol.outlets— the power-device equivalent ofcontrol.ports— is off by default, because an outlet carries real load (docs/drivers/apc-pdu.md).
This is experimental, hobby software from one home user. Read this before running it against anything you care about.
- No warranty of any kind. It is provided "as is", without warranty of
any kind, express or implied, and without any liability for damages or
losses arising from its use (see
LICENSE). If it misconfigures your device, takes down your network, or locks you out, that is on you. - It writes to your device. With
controlenabled it applies whatever the controller pushes: it replaces the device's VLAN configuration, disables and re-enables ports, switches outlets and UPS outlet groups, changes speeds and breakouts, STP, LACP, storm control, NTP and syslog, and reboots the device when the controller asks; a UPS driver option writes a load-shed policy into the unit. With the opt-incontrol.addressit will even set the device's own management address from the controller's IP Settings (a switch's config, a PDU card's config.ini, a Podman host's NetworkManager profile) — the one setting that can strand the bridge from the device it manages. A wrong click in the UniFi UI, a controller bug, or a bug here can cut off the device, the hosts behind it, or the bridge itself. - Verified on very little hardware: the six drivers above, against UniFi
Network 10.6 on a UniFi OS gateway. Any other device, OS version or
controller version is untested. Several features are marked as modelled but
never verified live in
docs/feature-map.md. - It speaks an undocumented protocol reverse-engineered by the unifi-emu project and by watching real devices. A controller update can change it and break this bridge silently, or make the controller push something it never pushed before.
- Start safe: run
-collect-onceand then monitoring only; back up the device's running config; keep an out-of-band way into it; turn oncontrolfor one unused port beforeports: all(or one outlet beforeoutlets: all); use it only on controllers and devices you own or are authorized to administer.
UniFi controller <── inform (TNBU/AES-GCM, every ~70 s) ── disunified <── eAPI/SSH/SNMP/Modbus ── the device
── system_cfg pushes / adoption ──> ── config diffs ──>
internal/devicemodel— the vendor-neutral model of a device and the driver contract.internal/drivers/<driver>— one driver per vendor/OS (arista-eos,proxmox,podman,apc-pdu,apc-ups,apc-backups), each with its ownCLAUDE.mdof working notes; its write-up isdocs/drivers/<driver>.md, its capturesdocs/fixtures/<driver>-<version>/, its scrub scriptscripts/sanitize-<driver>.py, and its wire-contract and replay casesinternal/device/contract_<driver>_test.goandinternal/informloop/replay_<driver>_test.go.internal/device— the inform session (forked from unifi-emu), payload, capability claims, persisted adoption state.internal/unificfg— parses the controller'ssystem_cfgpushes.internal/informloop— collect → inform → apply/reconcile, every cycle.internal/unifimodel— picks the UniFi model to claim from the port layout, or the power-device model from the outlets.internal/unifiapi— controller REST API: names the device and its ports after the device itself on first provision, and seeds the controller's port/outlet overrides from the device's live state so the first push changes nothing.internal/sshrun— the SSH transport the host drivers (Proxmox, Podman) share;internal/nutd— the NUT server a UPS's "NUT Server" switch turns on.
docs/install.md — about 20 minutes: get it, -collect-once to check the
device, one small YAML config (deploy/config.example.yaml) with secrets in
the environment, adopt in the UI, then turn on control. Or hand the whole
thing to an agent — see Installing with an AI agent.
Every release publishes a multi-arch image (linux/amd64, linux/arm64), so the
install is a directory with deploy/compose.yaml, your config.yaml and your
env — nothing to build:
docker pull ghcr.io/techblueprints/disunified:latest # :v1.2.3 to pin, :edge to follow main
docker compose up -d # deploy/compose.yaml; or the Quadlet unit in deploy/From a checkout instead (also how you add a driver; Go 1.26+):
go build ./cmd/disunified
export DUI_DEVICE_USER=stu DUI_DEVICE_PASS=...
./disunified -collect-once -device-url https://192.0.2.3/command-api # prints the snapshot + suggested model
./disunified -config config.yaml # bridges every device in the fileThe config lists devices under devices: (the pre-rename switches: key is
still accepted). Identity (MAC, serial, hostname, uplink), the port layout,
the port speed capabilities and the UniFi model to claim all come from the
device. The adopted key lives under state/; keep it, one bridge per device.
Every controller reply is recorded under inform-log/.
The current environment-variable prefix is
DUI_(DUI_DEVICE_USER,DUI_UNIFI_API_KEY, …) and the current flags are-device-url/-device-ssh. The pre-renameSTU_*names and-switch-url/-switch-sshflags are still read, so an existing deployment keeps working unchanged.
The whole install is written to be handed to a coding agent (Claude Code was used to build and to deploy this project). Paste one of these and fill in the angle-bracketed parts:
- "Install disunified on the Podman (or Docker) host I have running at
<host>, bridging my<vendor>device at<address>to the UniFi controller at<controller>. Usedocs/install.md; start read-only (control.ports: off), and stop before turning oncontrolso I can check the device in the UniFi UI first." - "My
<vendor>device is showing in the UniFi controller under Pending Adoption through disunified. Walk me through adoption and then turn on control for one unused port only (port<N>), followingdocs/install.mdsteps 4–5, and confirm the round-trip reaches the device before enablingports: all." - "disunified is deployed on
<host>and the UniFi controller was upgraded to Network<version>. Check the reply log underinform-log/, confirm the device is still adopted and reporting, and tell me if anything regressed."
The repo is written to be driven by a coding agent as well. Prompts that work, to paste as they are and fill in:
- "Add a driver for a
<vendor/OS>device using its API documentation at<URL>, followingdocs/adding-a-device.md. Capture and scrub the fixtures from my device at<address>first, write the tests against them, then integrate it with my UniFi controller at<controller>and verify it end to end. Test only on port<N>, which is unused." - "My
<vendor>device is adopted through disunified but the UniFi UI's<setting>has no effect on it. Read the reply log underinform-log/anddocs/feature-map.md, find out whether the controller pushed it and what the driver did with it, and fix or document it." - "Open a pull request against
TechBlueprints/disunifiedadding my<vendor>driver. FollowCONTRIBUTING.md: file the issue first, include the scrubbed fixtures and the tests, runscripts/check-site-info.sh, and write the PR description as the reproducible verification script the maintainer can run." - "My UniFi controller was upgraded to Network
<version>. Capture a fresh support-bundle inform from a real device and the new controller replies, refresh the fixtures underdocs/fixtures/controller-<version>/, and tell me which wire keys changed." - "The
<driver>driver lists<gap>as untested indocs/support-matrix.md. I have<device / OS version>at<address>. Follow that file's "How to close a gap": capture and scrub a fixture, extend the collector tests, run it against my controller at<controller>read-only and then on unused port<N>, and open a PR that moves the row to verified with the evidence."
Read, in this order: CLAUDE.md (repo map and the rules that came from
live failures), docs/adding-a-device.md (the checklist, with the proof
each step needs), internal/drivers/CLAUDE.md (rules for the vendor
layer), docs/unifi-models.md (which UniFi model to claim and how
the catalogue is refreshed), and docs/feature-map.md (every protocol
feature and its status). Then follow CONTRIBUTING.md: issue first, PR
against it, fixtures captured and scrubbed before code, tests against the
fixtures, and end-to-end verification on a real controller with a browser
automation tool driving the UniFi UI. The Arista EOS driver is the
reference; copy its shape, not its commands.
This is an independent, unofficial project by a home user and fan of UniFi
equipment who wanted to manage more of their own hardware from the same
controller. It is not affiliated with, endorsed by, or supported by Ubiquiti
Inc. "UniFi" and "Ubiquiti" are trademarks of Ubiquiti Inc., used here only to
describe what the software talks to. The device protocol it speaks comes from
the open-source unifi-emu library (see docs/prior-art.md); use it on
controllers you own, at your own risk and with no warranty of any kind.
This work exists strictly for integration: it lets a device you already own be managed from a UniFi Network controller that you are authorized to use and that runs on genuine Ubiquiti hardware. It does not replace, clone or bypass any Ubiquiti product, licence or service, and it is not intended to be run against a controller you do not administer.
If Ubiquiti sees this repository and has any concerns, they are welcome to
contact me: open an issue on this repository, message
@cgoudie on GitHub, or reach me on Discord
(user ID 360473903417786371; I am on the official Ubiquiti Discord server).
MIT — see LICENSE (attribution and trademark notes in NOTICE). The inform wire format, crypto and model catalogue come
from jamesbraid/unifi-emu (MIT),
whose protocol documentation made this possible; internal/device is a fork
of its inform session.