The wire protocol UniFi Protect cameras and controllers speak — measured, not guessed
Project description
pyunifiwire
The wire protocol UniFi Protect cameras and controllers speak to each other. Measured off a real controller and a real camera, not guessed.
from unifiwire import envelope, flv, hevc, ws
message = envelope.decode(frame_payload) # what the camera just said
message.function_name # 'ubnt_avclient_hello'
for tag in flv.Deframer().feed(chunk): # the push stream, tag by tag
if tag.kind is flv.TagType.VIDEO and tag.is_keyframe:
units = hevc.split_nalus(hevc.video_packet(tag.body).payload)
This is the part that is identical whichever end you are building. It has no opinion about adoption, settings, or what a camera should do when told to move — those belong to the thing you are writing. What is here is what the bytes mean.
Why it exists
UniFi cameras do not speak ONVIF or RTSP to their controller. They speak a private protocol: a JSON envelope over a TLS WebSocket, and video pushed over plain TCP in an FLV variant with 20 bytes between tags instead of 4 and HEVC under codec id 8, which no standard demuxer maps. Every project that has needed to talk to one has re-derived that from scratch.
Two implementations use this package as their only shared code — a controller that isn't there and a camera that isn't there — and their test suites read each other's output through it. Anything wrong here fails on both sides at once.
Install
pip install pyunifiwire
Python 3.13+. No dependencies — standard library only, and typed throughout
(py.typed, mypy --strict clean).
What's in it
| Module | What it covers |
|---|---|
envelope |
The JSON envelope every control message rides in: functionName, messageId, inResponseTo, sender and recipient |
ws |
RFC 6455 framing and the server side of the upgrade — enough to accept a camera without a WebSocket library |
wsclient |
The dialling side: the handshake a camera sends, header for header, and masked frames |
flv |
The extendedFlv container — flags 0x07, the 16-byte inter-tag trailer, tag parsing that survives split reads |
hevc |
hvcC parameter sets, length-prefixed NAL units, and the AAC AudioSpecificConfig |
annexb |
Annex B ↔ hvcC, for turning an ordinary .h265 file into something a controller accepts |
amf |
Enough AMF0 to write the metadata tag that identifies a stream |
discovery |
The UDP 10001 probe and the TLV answer a device gives before adoption |
certs |
A self-signed certificate, which both ends need and neither validates |
SPEC.md writes out the format itself: the envelope, the framing, the
container, and the metadata — with the parts that trip people up called out.
What it is not
- Not a UniFi Protect API client. For the controller's HTTP API see
uiprotectorpyunifi— different protocol entirely. - Not a way to get a third-party camera into Protect. See
unifi-cam-proxy. - Not affiliated with or endorsed by Ubiquiti Inc.
Testing
./test.sh # mypy --strict, then pytest. No network, no hardware.
Licence
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyunifiwire-0.1.0.tar.gz.
File metadata
- Download URL: pyunifiwire-0.1.0.tar.gz
- Upload date:
- Size: 30.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb9d1dc5fb0d4b32e51273cba97da2b5ea577219add9ad04e27440765ff100ca
|
|
| MD5 |
c683a1c82dd89465df5f4d416415071a
|
|
| BLAKE2b-256 |
733b85f44b58ed96613c198b6d56cc40a90bace8e294e0d48a830fdb1a4ab54e
|
Provenance
The following attestation bundles were made for pyunifiwire-0.1.0.tar.gz:
Publisher:
publish.yml on rjmotion/pyunifiwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyunifiwire-0.1.0.tar.gz -
Subject digest:
eb9d1dc5fb0d4b32e51273cba97da2b5ea577219add9ad04e27440765ff100ca - Sigstore transparency entry: 2257310297
- Sigstore integration time:
-
Permalink:
rjmotion/pyunifiwire@ab424de6426826a9fba4cab7b5e20684355fea66 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/rjmotion
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ab424de6426826a9fba4cab7b5e20684355fea66 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyunifiwire-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyunifiwire-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f917aabb831a78e92428094807aeca656401bf37fdd28a59f008e54971ec4177
|
|
| MD5 |
6c1972f6867dff439ba338fdcd3c4fde
|
|
| BLAKE2b-256 |
19269bc17aedc5aff6cb1a7e2ce8e6690df4c399ab04473c53d80424d7700f4e
|
Provenance
The following attestation bundles were made for pyunifiwire-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on rjmotion/pyunifiwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyunifiwire-0.1.0-py3-none-any.whl -
Subject digest:
f917aabb831a78e92428094807aeca656401bf37fdd28a59f008e54971ec4177 - Sigstore transparency entry: 2257310307
- Sigstore integration time:
-
Permalink:
rjmotion/pyunifiwire@ab424de6426826a9fba4cab7b5e20684355fea66 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/rjmotion
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ab424de6426826a9fba4cab7b5e20684355fea66 -
Trigger Event:
push
-
Statement type: