@koush/unifi-protect
v3.0.5
Published
A complete implementation of the UniFi Protect API.
Downloads
22
Maintainers
Readme
UniFi Protect API
A complete UniFi Protect API implementation.
unifi-protect
is a library that enabled you to connect to and communicate with the Ubiquiti UniFi Protect API and ecosystem. UniFi Protect is Ubiquiti's next-generation video security platform, with rich camera, doorbell, and NVR controller hardware options for you to choose from, as well as an app which you can use to view, configure and manage your video camera and doorbells.
Why use this library for UniFi Protect support?
In short - because I use it every day to support a very popular Homebridge plugin named homebridge-unifi-protect that I maintain. I have been occasionally asked if I would consider packaging the core API library separately from the plugin so that other open source projects can take advantage of the work that's been done here to understand and decode the UniFi Protect API.
In addition, this implementation is unique: it's the first complete open source implementation of the realtime UniFi Protect update API, enabling instantaneous updates to Protect-related events.
Finally - the most significant reason that you should use this library: it's very well-tested, it is modern, and most importantly, it just works. It's quite easy to add support for UniFi Protect in your project using this library, and you can rely on the fact that the code is used by a significant population of users out there who ensure its continued robustness.
How you can contribute and make this library even better
This implementation is largely feature complete. I strive to add support for meaningful features to a broad groups of people in order to avoid any unnecessary cruft and technical debt that may accrue over time.
The UniFi Protect API is undocumented and implementing a library like this one is the result of many hours of trial and error as well as community support.
Features
- Full access to the UniFi Protect NVR JSON.
- The ability to retrieve the JSON details, including status, of any supported UniFi Protect device.
- The ability to modify the Protect NVR JSON or Protect devices.
Changelog
- Changelog: changes and release history of this library.
Installation
To use this library in Node, install it from the command line:
npm install unifi-protect
Documentation
If you'd like to see all this in action in a well-documented, real-world example, please take a good look at my homebridge-unifi-protect project. It relies heavily on this library for the core functionality it provides.
ProtectApi(nvrAddress: string, username: string, password: string [, log: protectLogging])
Initialize the UniFi Protect API using the UniFi Protect account information contained in username
and password
. log
is an optional parameter that enables you to customize the type of logging that can be generated, including debug logging. If log
isn't specified, the Protect API will default to logging to the console.
refreshDevices()
This is where the magic happens. This function:
- Logs into the Protect API, if we don't already have an access token.
- If we do have an access token and it's nearly time to refresh it, it will do so.
- It then requests a refresh of the UniFi Protect NVR state which includes device information for all the Protect devices associated with that NVR. There are failsafes in place to ensure the Protect NVR doesn't get slammed with API requests causing potential performance issues with the NVR.
Note: refreshDevices()
must be called at least once after instantiating the API in order to populate the list of UniFi Protect devices associated with an account.
Returns: true
if successful, false
otherwise.
protectApi.cameras[]
The cameras
property maintains the list of all known Protect camera devices. It is an array of ProtectCameraConfig
objects, and you can look through protect-types.ts for a sense of what's contained in a ProtectCameraConfig
object.
This property is refreshed each time refreshDevices()
is called.
protectApi.lights[]
The lights
property maintains the list of all known Protect light devices. It is an array of ProtectLightConfig
objects, and you can look through protect-types.ts for a sense of what's contained in a ProtectLightConfig
object.
This property is refreshed each time refreshDevices()
is called.
protectApi.sensors[]
The sensors
property maintains the list of all known Protect sensor devices. It is an array of ProtectSensorConfig
objects, and you can look through protect-types.ts for a sense of what's contained in a ProtectSensorConfig
object.
This property is refreshed each time refreshDevices()
is called.
protectApi.viewers[]
The viewers
property maintains the list of all known Protect viewer devices. It is an array of ProtectViewerConfig
objects, and you can look through protect-types.ts for a sense of what's contained in a ProtectViewerConfig
object.
This property is refreshed each time refreshDevices()
is called.
More to come...
UniFi Protect Realtime Updates API
So...how does UniFi Protect provide realtime updates? On UniFi OS-based controllers, it uses a websocket called updates
. This connection provides a realtime stream of health, status, and events that the cameras encounter - including motion events and doorbell ring events.
Reverse engineering the realtime updates API is a bit more difficult than the system events API because it's based on a binary protocol. The Protect system events API is a steady stream of JSONs published on all UniFi OS controllers over the system
websocket. It's used by more than just UniFi Protect, which makes it interesting for future exploration.
The Protect realtime updates API, however, is a binary protocol published over the updates
websocket, and until now has been undocumented. I spent time analyzing what's happening in the Protect browser webUI as well as observing the controller and various Protect versions themselves to reverse engineer what's going on. Pouring through obfuscated code is like solving a puzzle with all the pieces in front of you - you know it's all there, you're just not always sure how it fits together.
For the impatient, you can take a look at the code for how to decode and read the binary protocol here in protect-updates-api.ts and the interface information located in protect-types.ts as well.
I welcome any additions or corrections to the protocol for the benefit of the community. I hope this helps others launch their own exploration and create new and interesting Protect-enabled capabilities.
Non-Ubiquiti Apps Using the Protect API
This list represents all known apps that are using the realtime updates API for UniFi Protect. If you're using the information you discovered on this page for your own UniFi Protect-based solution, please open an issue and I'm happy to add a link to it below. I hope this can serve as a repository of sorts for UniFi Protect-based apps and solutions in the community.
- homebridge-unifi-protect: Seamless integration of UniFi Protect into HomeKit with support for cameras, doorbells, and more.
Connecting
- Login to the UniFi Protect controller and obtain the bootstrap JSON. The URL is:
https://protect-nvr-ip/proxy/protect/api/bootstrap
. You can look through protect-api.ts for a better understanding of the Protect login process and how to obtain the bootstrap JSON. - Open the websocket to the updates URL. The URL is:
wss://protect-nvr-ip/proxy/protect/ws/updates?lastUpdateId?lastUpdateId=X
. You can grab lastUpdateId from the bootstrap JSON in the prior step. You can see an example in protect-api.ts. - Then you're ready to listen to messages. You can see an example of this in protect-nvr.ts.
Those are the basics and gets us up and running. Now, to explain how the updates API works...
Updates Websocket Binary Format
UniFi OS update data packets are used to provide a realtime stream of updates to Protect. It differs from the system events API in that the system events API appears to be shared across other applications (Network, Access, etc.) while the updates events API appears to only be utilized by Protect and not shared by other applications, although the protocol is shared.
The updates
websocket is used by the UniFi Protect webUI and native applications to provide realtime updates back to the controller. UniFi cameras and doorbells also use a websocket to provide those same updates to the Protect controller. The updates
websocket uses a binary protocol to encode data largely to minimize bandwidth requirements, and provide access to more than JSON data, if needed.
So how does it all work? Cameras continuously stream updates to the UniFi Protect controller containing things like camera health, statistics, and - crucially for us - events such as motion and doorbell ring. A complete update packet is composed of four frames:
Header Frame (8 bytes)
----------------------
Action Frame
----------------------
Header Frame (8 bytes)
----------------------
Data Frame
Let's look at each of these.
Header Frame
The header frame is required overhead since websockets provide only a transport medium. It's purpose is to tell us what's coming in the frame that follows.
A packet header is composed of 8 bytes in this order:
| Byte Offset | Description | Bits | Values |----------------|-----------------|---------|----------------------------------------- | 0 | Packet Type | 8 | 1 - action frame, 2 - payload frame. | 1 | Payload Format | 8 | 1 - JSON object, 2 - UTF8-encoded string, 3 - Node Buffer. | 2 | Deflated | 8 | 0 - uncompressed, 1 - deflated / compressed (zlib-based). | 3 | Unknown | 8 | Always 0. Possibly reserved for future use by Ubiquiti? | 4-7 | Payload Size | 32 | Size of payload in network-byte order (big endian).
If the header has marked the payload as deflated (compressed), you'll need to inflate (uncompress) the payload before you can use it.
Action Frame
The action frame identifies what the action and category that the update contains:
| Property | Description
|-------------|------------------------------------------------------------------------------------
| action | What action is being taken. Known actions are add
and update
.
| id | The identifier for the device we're updating.
| modelKey | The device model category that we're updating.
| newUpdateId | A new UUID generated on a per-update basis. This can be safely ignored it seems.
Data Frame
The final part of the update packet is the data frame. The data frame can be three different types of data - although in practice, JSONs are all that come across, I've found. Those types are:
| Payload Type | Description
|--------------|------------------------------------------------------------------------------------
| 1 | JSON. If the action frame's action
property is set to update
and the modelKey
property is not set to event
(e.g. camera
), this will always a subset of the configuration bootstrap JSON.
| 2 | A UTF8-encoded string.
| 3 | Node Buffer.
Tips
update
actions are always tied to any valid modelKey that exists in the bootstrap JSON. The exception isevents
which is tied to the Protect events history list that it maintains. The supported modelKeys from the bootstrap JSON are:bridge
,camera
,group
,light
,liveview
,nvr
,sensor
,user
, andviewer
.add
actions are always tied to theevent
modelKey and indicate the beginning of an event item in the Protect events list. A subsequentupdate
action is sent signaling the end of the event capture, and it's confidence score for motion detection.- This is not the same thing as motion detection. If you want to detect motion, you should watch the
update
action forcamera
modelKeys, and look for a JSON that updateslastMotion
. For doorbell rings,lastRing
. The Protect events list is useful for the Protect app, but it's of limited utility to HomeKit, and it's slow relative to just looking for thelastMotion
JSON that tends to be much more timely in its delivery. If you want true realtime updates, you want to look at theupdate
action. - JSONs are only payload type that seems to be sent, although the protocol is designed to accept all three.
- With the exception of
update
actions with amodelKey
ofevent
, JSONs are always a subset of the bootstrap JSON, indexed off ofmodelKey
. So for amodelKey
ofcamera
, the data payload is always a subset of ProtectCameraConfigInterface (see protect-types.ts).
Library Development Dashboard
This is mostly of interest to the true developer nerds amongst us.