Skip to content

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UniFi Protect

An Omarchy bar widget for UniFi Protect cameras: one small icon in the bar, and the pictures in a panel behind it.

The panel open: one camera large, the others as thumbnails, and the frames filed the last time something moved

The bar shows a camera glyph and nothing else. It lights up, and the name of the camera slides in beside it, while Protect reports motion, so a camera that sees something is noticeable out of the corner of your eye without a video feed sitting in the bar all day.

Everything you would actually look at lives in the panel: the selected camera large and playing live, the other cameras as thumbnails underneath, and below those the frames your cameras filed the last time they saw something. Clicking any picture opens that camera in Protect's own detection browser, which is where you go when a glance is not enough.

Install

omarchy plugin add https://github.com/jankeesvw/omarchy-unifi-protect.git --enable

Open the camera icon in the bar. Until it has an API key the panel opens on settings instead of cameras:

  1. Set the console address if it is not already right.
  2. Paste an API key and save it.

The first time the widget talks to the console it files that certificate and keeps talking only to it. The same two steps can be done from the shell if you would rather script them: the sections below. The widget lands in the right section of the bar; move it with omarchy bar move, or from the bar's own settings panel.

The API key

Everything goes through Protect's integration API, which takes a plain X-API-KEY header. Make a key at https://<your-console>/unifi-api/protect.

⚠️ UniFi hands a key the full rights of the account that created it, and there is no way to scope it down to Protect over the API. A key made on your own admin login is your whole console in a file on your laptop.

Make a local user for it instead. Under Settings → Admins & Users → Add User, choose a local account and sign in as that user to make the key. The widget only ever lists cameras, pulls stills and reads stream urls, so Protect: View Only is all it ever needs, and a key that leaks is then a key that can look at your cameras rather than one that can reconfigure your network.

On at least some consoles the integrations page that hands out keys is only reachable by a Full Site Admin, so a view-only account cannot get to it. The way round it is to give that local user admin rights, make the key, and set it straight back to Protect: View Only. Thanks to @GeertJohan for working that out on a UDM Pro.

One thing worth knowing before you rely on that: we have not confirmed whether UniFi re-reads the account's rights on every request or freezes them when the key is made. If it is the latter, the key still carries admin rights however the account is set afterwards. Treat it as an admin key until somebody checks.

The panel's settings view is the ordinary way to file a key: paste it, save, and it is written to the key file over stdin so it never appears on a process command line or in shell.json. The widget never reads the stored key back into the UI; it only shows whether one is saved.

The same file can be written by hand, which is what a script should do. The widget looks for the key in two places, in this order:

  1. UNIFI_API_KEY in the environment.
  2. A file, by default ~/.config/unifi-protect/api-key. Set UNIFI_API_KEY_FILE to put it somewhere else.

The file is the one that works straight away, because a variable exported into your session does not reach a shell that is already running:

printf '%s\n' "paste-your-key-here" | ./bin/unifi-protect set-key

or, equivalently:

mkdir -p ~/.config/unifi-protect
(umask 077; printf '%s\n' "paste-your-key-here" > ~/.config/unifi-protect/api-key)

The first line with something on it is the key, trimmed. Nothing else in the file is parsed.

Pinning the console

Your console signs its own certificate. There is no authority that can vouch for it, so there is nothing to check it against, and the key above travels on every one of these connections. The first time the widget talks to the console it files whatever certificate is answering, then every later connection has to present that same key or be dropped before the API key goes out. That is trust-on-first-use: you do not have to pin anything in the panel.

unifi-protect trust is still there if you would rather look at the fingerprint first, or pin again after a console reset (the only other time the fingerprint should change, and the one case the widget will not overwrite for you):

~/.config/omarchy/plugins/jankeesvw.unifi-protect/bin/unifi-protect \
  --host 192.168.1.1 trust

It prints the fingerprint and waits. trust show prints the same facts as JSON without writing; trust accept <fingerprint> re-fetches the chain and pins it only if that fingerprint still matches, so the certificate you approved is the one filed.

Settings

A switch in the panel's header, beside the gear, mutes those popups for the rest of the session without a trip to settings: a quick "stop interrupting me" rather than a change to the config. It starts from notify each time the shell starts and does not write it back, and the bar icon still lights up on motion either way, since that part is silent anyway.

The console address, the API key, which cameras may interrupt you, and the motion notification live in the widget's own settings view (the gear on the panel). The rest, panel width, snapshot interval and how long the icon stays lit, stays in the bar's settings panel, or by hand in the widget's entry in ~/.config/omarchy/shell.json:

Setting Default What it does
host 192.168.1.1 The console running Protect: a UDM, a Cloud Key, whatever answers on your network. Address or hostname, no scheme.
alertCameras (empty) Camera names, comma separated, that may light the icon and file frames. Empty means all of them.
notify true Raise a desktop notification, with the frame in it, on motion. Sets the starting position of the switch in the panel's header, see below.
viewerCommand xdg-open {path} What clicking that notification runs, to see the frame full size.
panelWidth 800 Panel width in the shell's spacing units. Trimmed to fit a small screen.
motionHoldMs 45000 How long the icon stays lit after motion.
refreshMs 1000 How often the large view gets a fresh still while the stream is connecting.
captureThrottleMs 25000 How long a camera that just filed a frame files nothing more.
archiveKeep 60 Frames kept per camera.
rtspPort 7447 The plain RTSP port on the console.
eventsUrl (see below) Where clicking a picture takes you.

alertCameras is worth setting. The camera aimed at your own desk reports motion all day, and an icon that is always lit is an icon you stop reading:

{ "id": "jankeesvw.unifi-protect", "host": "10.0.0.1", "alertCameras": "Front door, Shed" }

Names are matched against what Protect calls the cameras, ignoring case. The setting gates the bar icon and the archive both; every camera stays visible in the panel regardless.

The notification carries the frame as its icon, which is a thumbnail; clicking it opens the same picture full size. viewerCommand is what runs, with {path} standing in for the frame:

{ "id": "jankeesvw.unifi-protect", "viewerCommand": "imv {path}" }

The default hands the file to whatever your desktop opens a JPEG with. Name a viewer to be sure of which one, or leave it empty for a popup that does nothing. The template is split on whitespace and goes out as an argv vector, which the shell's notification server keeps with the toast, so a frame is still one click away after the popup has moved into the notification history. The frame path is one argument however it is spelled, so a space or a dash in it stays part of the filename.

eventsUrl is Protect's detection browser filtered to one camera. {host} is the console and {camera} the camera id, and the default is:

https://{host}/protect/detections/find-anything?grade=all&labels=camera%3A{camera}&minConfidence=30

It is a setting rather than a constant because Protect is a single-page app: that route cannot be read off the console, it was taken from the address bar, and a UniFi update is free to move it.

What it costs to run

The large view plays the camera's real RTSP stream, and only while the panel is open. A stream left running behind a closed panel is a decoder and a few megabits a second spent on a picture nobody is looking at.

The thumbnails stay stills, refreshed one camera at a time: three decoders running so you can see which camera to click is a lot of CPU for a picture the size of a stamp. A still is also what fills the large view for the second or two the stream takes to connect, so opening the panel never starts on black.

With the panel closed, the widget holds one websocket open for motion events and does nothing else. There is no polling.

The archive

Every burst of motion on a camera you listed files one frame, whether or not the panel happens to be open, in ~/.local/state/unifi-protect/events/<camera-id>-<unix-time>.jpg. The newest four show up under the panel; clicking one puts it in the large view with how long ago it was, and clicking it again goes back to live.

Frames are stored around 960px wide, so archiveKeep at its default of 60 per camera is a few megabytes and covers a busy day. Protect keeps the actual recordings; this is the shortcut that answers "was somebody at the door" without opening anything.

The command line

The widget shells out to bin/unifi-protect in this repo for everything, and that script is usable on its own:

cd ~/.config/omarchy/plugins/jankeesvw.unifi-protect

./bin/unifi-protect list                  # cameras, as JSON
./bin/unifi-protect snapshot <camera-id>  # writes a JPEG, prints its path
./bin/unifi-protect events                # the archive, newest first
./bin/unifi-protect cached                # newest snapshot on disk per camera
./bin/unifi-protect stream <camera-id>    # the RTSP url
./bin/unifi-protect live <camera-id>      # opens the stream in mpv
./bin/unifi-protect watch                 # motion as NDJSON, until killed
./bin/unifi-protect status                # {ok, hasKey, trusted, host}
./bin/unifi-protect set-key               # writes a key from stdin
./bin/unifi-protect trust show            # the live certificate, as JSON
./bin/unifi-protect trust accept <fp>     # pins that fingerprint
./bin/unifi-protect trust                 # the same, interactively

--host, --rtsp-port and --archive-keep go before the command. It is also the first place to look when the bar icon stays dim: run list by hand and the error comes back as JSON instead of disappearing into the shell's log.

To exercise the icon without waiting for somebody to walk past a camera:

omarchy-shell jankeesvw.unifi-protect.test motion "Front door"
omarchy-shell jankeesvw.unifi-protect.test clear

Requirements

Omarchy with omarchy-shell (the Quickshell-based bar), and:

curl, jq talking to Protect
python3 + websockets motion events. Without it everything works except the icon lighting up, and the panel says so. If your distro has no package for it, unifi-protect setup offers to put one in a virtualenv of your own after telling you what it is about to do. It installs exactly the version and the file digests listed in requirements.txt in this repository, with pip --require-hashes, so what gets installed is what was reviewed rather than whatever the registry serves that day. A digest that does not match stops the install and leaves the virtualenv empty.
imagemagick shrinking archived frames. Without it they are stored full size.
libnotify the notification on motion
mpv only for unifi-protect live

On Arch:

sudo pacman -S --needed curl jq python python-websockets imagemagick libnotify mpv

Chain and hostname checking are off for calls to the console; its certificate names the console, not the address you reach it on, and nothing on your network can build a chain to it. What replaces them is the certificate filed on first contact: curl is given --pinnedpubkey, which it enforces regardless, and the motion websocket loads that certificate as its only trust root. A connection that presents anything else is dropped before the API key is sent.

The key itself never becomes an argument. /proc/PID/cmdline is mode 444 and readable by every account on the machine, and a widget that polls would hold that window open all day, so curl is handed the header on stdin through --config -. The motion websocket gets it on a pipe for the same reason one step further in: /proc/PID/environ is mode 400, which keeps other accounts out but not other processes of yours, and that watcher is meant to run for days, so anything running as you could read it out at leisure. A pipe is readable only while it is being read, and the watcher drains it in its first line. Nor does the key reach a log: bash -x prints the words of every command it runs, so the trace is switched off around the places that hold it.

The key file and the pinned certificate are written under umask 077, and the directory holding them is checked to be a directory of yours at mode 700, along with the frame cache and the archive, which are pictures of the inside of your house.

Camera ids come off those connections and end up in filenames, so they are checked against [A-Za-z0-9]{1,64}, Protect's own format, and anything else is refused rather than escaped.

The live view reads the stream off the plain RTSP port rather than the encrypted one, because Qt Multimedia gives no way to trust a self-signed certificate; unifi-protect live keeps the encrypted URL, since mpv can be told to accept it. Neither carries the API key: Protect hands back a per-camera stream alias and that is all that travels.

Remove

omarchy plugin remove jankeesvw.unifi-protect

The archived frames are yours, not the plugin's, so they stay in ~/.local/state/unifi-protect/. Delete that directory, and ~/.config/unifi-protect/ with the key and the pinned certificate in it, if you want them gone too.

License

MIT

About

Omarchy bar widget: watch your UniFi Protect cameras from the bar

Resources

Stars

17 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages