https://github.com/oliverziegert/unifi-port-profile-switcher
Switch UniFi switch ports between VLAN/port-profile presets — Go CLI and Home Assistant add-on
https://github.com/oliverziegert/unifi-port-profile-switcher
cli go golang home-assistant home-assistant-addon homeassistant-addons network-automation networking port-profile ubiquiti udm udr unifi vlan
Last synced: 20 days ago
JSON representation
Switch UniFi switch ports between VLAN/port-profile presets — Go CLI and Home Assistant add-on
- Host: GitHub
- URL: https://github.com/oliverziegert/unifi-port-profile-switcher
- Owner: oliverziegert
- License: gpl-3.0
- Created: 2026-05-14T12:22:55.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-09T17:01:21.000Z (about 1 month ago)
- Last Synced: 2026-06-09T19:04:48.288Z (about 1 month ago)
- Topics: cli, go, golang, home-assistant, home-assistant-addon, homeassistant-addons, network-automation, networking, port-profile, ubiquiti, udm, udr, unifi, vlan
- Language: Go
- Homepage: https://github.com/oliverziegert/unifi-port-profile-switcher
- Size: 323 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# UniFi Port Profile Switcher
A small Go CLI that flips a UniFi switch port between named profile presets, by
talking to a self-hosted UniFi OS controller's internal REST API.
Built to solve a specific problem: a docking station shared between a personal
and a work laptop. Because the dock presents one MAC, the controller can't
auto-assign different VLANs — but a one-shot CLI can.
## Install
```sh
go build -o unifi-port-profile-switcher .
sudo install -m 0755 ./unifi-port-profile-switcher /usr/local/bin/
```
### Releases
Prebuilt binaries for `linux/{amd64,arm64}` and `darwin/{amd64,arm64}` are
attached to each [GitHub Release](https://github.com/oliverziegert/unifi-port-profile-switcher/releases),
along with a `SHA256SUMS` file:
```sh
curl -L -o unifi-port-profile-switcher \
"https://github.com/oliverziegert/unifi-port-profile-switcher/releases/latest/download/unifi-port-profile-switcher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)"
chmod +x unifi-port-profile-switcher
```
The binaries are **unsigned**. On macOS, clear Gatekeeper quarantine before
running the first time:
```sh
xattr -d com.apple.quarantine ./unifi-port-profile-switcher
```
## Configure
Create a dedicated local admin user on the UniFi Network app (e.g.
`port-switcher`) — used only by this tool. Then write the config file:
```sh
sudo mkdir -p /etc/unifi-port-profile-switcher
sudo install -m 0600 /dev/null /etc/unifi-port-profile-switcher/config.yaml
sudo $EDITOR /etc/unifi-port-profile-switcher/config.yaml
```
Example:
```yaml
controller:
url: https://192.168.1.1
site: default
username: port-switcher
password: REPLACE_ME
insecure_tls: true # self-signed cert on UDM/UDR
presets:
work-laptop:
switch: "Office USW-24" # device name OR MAC ("aa:bb:cc:dd:ee:ff")
port: 5
profile: "Work VLAN"
personal-laptop:
switch: "Office USW-24"
port: 5
profile: "Personal VLAN"
```
The tool refuses to load a config that is group- or world-readable. Keep it `0600`.
The tool will also look at `$XDG_CONFIG_HOME/unifi-port-profile-switcher/config.yaml`
(or `~/.config/unifi-port-profile-switcher/config.yaml`) before falling back to
`/etc/...`. Override with `--config /path/to/config.yaml`.
## Usage
```sh
unifi-port-profile-switcher list # list presets
unifi-port-profile-switcher status work-laptop # current vs target
unifi-port-profile-switcher --dry-run work-laptop # show change, do not apply
unifi-port-profile-switcher work-laptop # flip the port
```
Apply is idempotent: running it twice in a row is a no-op the second time. Other
ports' overrides on the same switch are preserved.
### Flags
- `--config PATH` — override config file path
- `--dry-run` — compute the change without writing
- `--json` — emit a single JSON object on stdout (machine-readable)
- `-v`, `--verbose` — debug logging on stderr
### Exit codes
| code | meaning |
| ---: | --------------------------------------------------- |
| 0 | success or no-op |
| 1 | generic / unexpected error |
| 2 | preset not found in config |
| 3 | controller authentication failure |
| 4 | switch, port, or profile not found |
| 5 | controller API error (non-2xx or `meta.rc != "ok"`) |
## Why not API keys?
Ubiquiti's local API keys authorize the `/proxy/network/integration/v1/*` surface,
which does **not** include port-override endpoints. The legacy
`/proxy/network/api/s//rest/device/` path (used by the web UI) still
requires session-cookie + `X-CSRF-Token` auth. This tool therefore uses a local
admin username/password.
## Home Assistant add-on
The recommended way to use this tool on Home Assistant OS is via the **Home Assistant add-on**, which ships the same binary as an HTTP API service that HA can call from automations and dashboard buttons.
Full documentation: [unifi-port-profile-switcher/DOCS.md](unifi-port-profile-switcher/DOCS.md)
### Quick install
1. In HA go to **Settings → Add-ons → Add-on Store → Repositories** and add:
```
https://github.com/oliverziegert/unifi-port-profile-switcher
```
2. Install the **UniFi Port Profile Switcher** add-on from the store.
3. Configure controller credentials, presets, and a bearer token in the **Configuration** tab.
4. Start the add-on. The HTTP API is now reachable inside HA at port `8099`.
### HTTP endpoints
| Method | Path | Auth | Purpose |
|--------|-------------------------------------|--------|---------------------------------------------------------------|
| GET | `/healthz` | none | Liveness probe. |
| GET | `/presets` | bearer | List configured presets. |
| GET | `/presets/{name}/status` | bearer | Current vs target profile for the named preset. |
| POST | `/presets/{name}/apply` | bearer | Apply the named preset (idempotent). `?dry_run=1` to preview. |
| GET | `/ports/{switch}/{port}/active` | bearer | Which configured preset is currently active on a port. |
### Phase 2 verification
Confirm the add-on is talking to the controller:
```sh
# 1. Healthcheck (no auth)
curl http://:8099/healthz
# → {"ok":true}
# 2. List presets (bearer token required)
curl -H "Authorization: Bearer " http://:8099/presets
# 3. Dry-run apply — reads controller state, no write
curl -X POST -H "Authorization: Bearer " \
"http://:8099/presets/work-laptop/apply?dry_run=1"
# → {"changed":false,"dry_run":true,...}
# 4. Real apply
curl -X POST -H "Authorization: Bearer " \
http://:8099/presets/work-laptop/apply
# → {"changed":true,...}
# 5. Idempotent re-run (same apply again)
# → {"changed":false,...}
# 6. Active-preset lookup — read-only, one call per port instead of per preset
curl -H "Authorization: Bearer " \
"http://:8099/ports/Office%20USW-24/5/active"
# → {"switch":"Office USW-24","port":5,"active_preset":"work-laptop",...}
```
## Dependency updates
This repository uses the [Renovate GitHub App](https://github.com/apps/renovate) to keep dependencies up to date across Go modules, GitHub Actions, and the add-on Dockerfile base image.
**How it works:**
- Minor and patch updates are grouped into a single weekly PR per ecosystem (Go modules, GitHub Actions, Docker), opening Monday mornings (Europe/Berlin).
- Vulnerability-driven updates open immediately as individual PRs, labelled `security`.
- Major version bumps open as individual PRs immediately, outside any grouping, so they can be reviewed deliberately.
- Auto-merge is intentionally **disabled** — every Renovate PR must be merged by a human reviewer.
**Opting a dependency out:** add a `packageRules` entry to `renovate.json`:
```json
{
"matchPackageNames": ["example/package"],
"enabled": false
}
```
**Note for contributors:** GitHub Actions `uses:` references are pinned to a 40-character commit SHA with a trailing version comment (e.g., `uses: actions/checkout@abc123 # v4.2.0`). Do not remove or "clean up" these comments — Renovate relies on them to track the current version. This includes `uses:` references inside the local composite action at `.github/actions/find-addons-filtered/action.yaml` — Renovate tracks those too.
## Develop
```sh
go test ./...
go vet ./...
go build ./...
```
### Lint
CI runs `golangci-lint` against this module with a curated rule set covering
the bug categories that have shown up in this codebase's history. The eight
enabled checks are:
- `errcheck` — unchecked errors
- `govet` — Go's built-in suspicious-construct check
- `ineffassign` — assignments whose value is never read
- `revive` — replacement for the deprecated `golint`
- `staticcheck` — broad static-analysis pass
- `unused` — unused identifiers
- `gofmt` — canonical formatting
- `goimports` — canonical formatting + local-import grouping
Run the same set locally from `unifi-port-profile-switcher/`:
```sh
golangci-lint run --config ../.golangci.yaml ./...
```