Ubiquiti UniFi Video and Protect NVR Integration

I am fairly new to ST, and I just upgraded to ubiquiti router and wifi access points. I really like their products so far.

I want to add 2-3 outdoor cameras, and am considering ubiquiti again.

How is the NVR Software?
How are the Ubiquiti UVC-G3 cameras?
How is the ST integration?
Can it create ST Motion triggers and other events?
Can ST triggers have it start to capture video?

Are there other cameras that I should consider?

I want poe, local storage, ST integration, outdoor, day/night.

1 Like

I am running this on my test box. I am running software 3.1.5 It always tells me to check API key. I am cutting and pasting. Not sure whats going on

37b05eed-bc06-4a4c-a56b-01a86a40696d 11:40:10 PM: error nvr_bootstrapPollCallback: unable to log in! Please check API key.
37b05eed-bc06-4a4c-a56b-01a86a40696d 11:40:10 PM: info nvr_initialize: NVR API is located at 192.168.10.81:7080
37b05eed-bc06-4a4c-a56b-01a86a40696d 11:40:10 PM: info UniFi NVR: updated with settings: [nvrAddress:192.168.10.81, nvrPort:7080, apiKey:jhhCEGayN34kNzMa]

I have only tested it with my setup which is version 3.5.2. I will add some code to give more debug information and maybe we can get it working on older stuff. Stand by.[quote=“Themlruts, post:40, topic:70161, full:true”]
One thing. Will the hub talk locally to my local NVR or do I need to open a firewall port and use external IP?
[/quote]

The hub will talk directly with the NVR and you do not need to open anything on the firewall.

NVR is pretty good. I like it. They have a nice mobile app as well.

I’m using the Dome camera and again, I think its pretty good. I have had it outdoors in NE weather (not directly exposed, they are mounted below an overhang) and its reliable. Good video quality.

ST integration is only through my device type handler and smart app here. There is no official support. It is severely limited by the lack of websockets support on the hub so I have to poll from the cloud, to the hub, to the NVR therefore its slow and unreliable. I get a > 5% average poll failure rate at 5 second intervals…not very good. The API is good so the ST app can integrate a ton of settings but I don’t see a reason to copy the Ubiquiti mobile app.

Yea through my code you can do motion triggers if the camera is set to record on motion only. For example, when I’m away I get notifications on motion. It isn’t great at doing real-time motion triggers for things like turning on lights because of the polling delay, but it works. When its after sunset and someone walks up to my front door, while I’m not home, the lights turn on. Delay in that case doesn’t matter.

There is no functionality in the Ubiquiti API to command a device to record. This would have to be done by switching the record mode from motion to always and then back again. However, again, there is no functionality in the API to detect motion so its not possible to stop the recording intelligently. The only way to detect motion is to detect that the camera has recorded something using record on motion. A catch 22.

There is another thread in the forums with answers to the best cam integration for ST. I did it the other way around - I wanted a camera that met my specs outside of ST and then integrated it myself for what I needed. There are better options if ST is the first priority.

3 Likes

That would be awesome. I have about 5 of these cams that I can’t upgrade. Let me know what you need from me

1 Like

I installed 3.1.5 in a VM to experiment. Turns out the SmartApp code is reporting the error correctly - using the API key does not work. I don’t know yet if there is a different use of the API key required or if it is just broken in this version. I do get a response, but it reports the login was not successful. I’ll keep experimenting.

Thanks this will be a huge help

No matter what I do with different users, API keys, versions of 3.1.x, it never says I’m logged in. It might be terminal. Try this…delete line 82 in the smart app which just says “return”. That way it will just look for camera data and not check “isLoggedIn”. I don’t have spare cameras to add to my sandbox NVR to test what the response will be so this is a shot in the dark. Make sure you have the logs running when you configure the smart app so we can see if it found cameras.

61b50d3f-7060-453b-a910-61f577a08749 9:13:59 PM: error java.lang.NullPointerException: Cannot get property ‘size’ on null object @ line 90
61b50d3f-7060-453b-a910-61f577a08749 9:13:59 PM: info nvr_bootstrapPollCallback: response from null
61b50d3f-7060-453b-a910-61f577a08749 9:13:59 PM: error nvr_bootstrapPollCallback: unable to log in! Please check API key.
61b50d3f-7060-453b-a910-61f577a08749 9:13:59 PM: info nvr_initialize: NVR API is located at 192.168.10.15:7080
61b50d3f-7060-453b-a910-61f577a08749 9:13:59 PM: info UniFi NVR: updated with settings: [nvrAddress:192.168.10.15, nvrPort:7080, apiKey:b9a7746a20976063]
61b50d3f-7060-453b-a910-61f577a08749 9:13:14 PM: error java.lang.NullPointerException: Cannot get property ‘size’ on null object @ line 90
61b50d3f-7060-453b-a910-61f577a08749 9:13:14 PM: info nvr_bootstrapPollCallback: response from null
61b50d3f-7060-453b-a910-61f577a08749 9:13:14 PM: error nvr_bootstrapPollCallback: unable to log in! Please check API key.
61b50d3f-7060-453b-a910-61f577a08749 9:13:13 PM: info nvr_initialize: NVR API is located at 192.168.10.15:7080
61b50d3f-7060-453b-a910-61f577a08749 9:13:13 PM: info UniFi NVR: updated with settings: [nvrAddress:192.168.10.15, nvrPort:7080, apiKey:b9a7746a20976063]
61b50d3f-7060-453b-a910-61f577a08749 9:13:13 PM: info UniFi NVR: installed with settings: [nvrAddress:192.168.10.15, nvrPort:7080, apiKey:b9a7746a20976063]

I can access recordings from the API

this is the json response {“data”:[],“meta”:{“totalCount”:0,“filteredCount”:0}}

Looks like posiblly the wrong url
This URL gives me bellow http://192.168.10.15:7080/api/2.0/camera?apiKey=b9a7746a20976063

{“data”:[{“t”:“camera”,“name”:“Driveway”,“uuid”:“643eb93c-af52-31e4-a256-2514c739892f”,“mac”:“0027226035D0”,“managed”:true,“provisioned”:true,“managementRequested”:true,“state”:“CONNECTED”,“host”:“192.168.10.110”,“internalHost”:“192.168.10.110”,“model”:“airCam”,“uptime”:1490580691058,“lastSeen”:1490580936388,“firmwareVersion”:“v3.1.4.39”,“upgradeStatus”:null,“firmwareBuild”:“7e42364”,“protocolVersion”:39,“targetFirmwareCode”:null,“updateTimestamp”:0,“systemInfo”:{“cpuName”:“FA626TE rev 1 (v5l)”,“cpuLoad”:34.0,“memory”:{“used”:119504896,“total”:130764800},“appMemory”:null,“nics”:[{“desc”:“eth0”,“mac”:"",“ip”:"",“rxBps”:184,“txBps”:113}],“disk”:null},“conflictedHost”:null,“okToOverrideConflict”:false,“deleted”:false,“username”:“ubnt”,“lastRecordingId”:“58bb9cb7e4b03a816d5dcd78”,“lastRecordingStartTime”:1488690347895,“serverUuid”:“27e400ab-5723-488b-aacc-ddbbce48d76b”,“controllerHost”:null,“deviceSettings”:{“name”:“Driveway”,“timezone”:“GMT-4”,“persists”:false},“videoSettings”:{“audio”:{“status”:1,“volume”:100},“video”:{“video1”:{“bitRateCbrAvg”:1128000,“bitRateVbrMax”:0,“fps”:15,“isCbr”:true},“video2”:{“bitRateCbrAvg”:532000,“bitRateVbrMax”:0,“fps”:15,“isCbr”:true},“video3”:{“bitRateCbrAvg”:152000,“bitRateVbrMax”:0,“fps”:15,“isCbr”:true}},“enableSuggestedSettings”:true},“videoProperties”:{“video”:{“video1”:{“idrInterval”:0,“width”:1280,“height”:720,“validBitrateRangeMax”:4096000,“validBitrateRangeMin”:256000,“validFpsValues”:[2,3,4,5,7,10,15,20,25,30]},“video2”:{“idrInterval”:0,“width”:640,“height”:368,“validBitrateRangeMax”:1024000,“validBitrateRangeMin”:64000,“validFpsValues”:[2,3,4,5,7,10,15,20,25,30]},“video3”:{“idrInterval”:0,“width”:320,“height”:176,“validBitrateRangeMax”:256000,“validBitrateRangeMin”:16000,“validFpsValues”:[2,3,4,5,7,10,15,20,25,30]}}},“channelSettings”:{“channel0”:{“enableVodRtsp”:false,“vodAlias”:null},“channel1”:{“enableVodRtsp”:false,“vodAlias”:null},“channel2”:{“enableVodRtsp”:false,“vodAlias”:null}},“ispSettings”:{“brightness”:50,“contrast”:50,“denoise”:50,“hue”:50,“saturation”:50,“sharpness”:50,“flip”:0,“mirror”:0,“gamma”:4,“wdr”:null,“aeMode”:“flick60”,“irLedMode”:null,“irLedLevel”:null,“focusMode”:null,“focusPosition”:null,“zoomPosition”:null,“irOnValBrightness”:null,“irOnStsBrightness”:null,“irOnValContrast”:null,“irOnStsContrast”:null,“irOnValDenoise”:null,“irOnStsDenoise”:null,“irOnValHue”:null,“irOnStsHue”:null,“irOnValSaturation”:null,“irOnStsSaturation”:null,“irOnValSharpness”:null,“irOnStsSharpness”:null},“osdSettings”:{“tag”:"",“overrideMessage”:false,“enableDate”:1,“enableLogo”:1},“recordingSettings”:{“motionRecordEnabled”:false,“fullTimeRecordEnabled”:false,“channel”:0,“prePaddingSecs”:0,“postPaddingSecs”:0,“storagePath”:null},“scheduleUuid”:null,“zones”:[{“name”:“AirCam”,“sensitivity”:20,“bitmap”:null,“coordinates”:[{“x”:0.0,“y”:0.0},{“x”:0.0,“y”:1.0},{“x”:1.0,“y”:1.0},{“x”:1.0,“y”:0.0}],"_id":“58d875c8e4b03a816d5dcd79”}],“mapSettings”:{“x”:0.0,“y”:0.0,“mapId”:null,“angle”:0.0,“radius”:0.0,“rotation”:0.0},“networkStatus”:{“connectionState”:7,“connectionStateDescription”:“Connected”,“essid”:null,“frequency”:0,“quality”:0,“qualityMax”:0,“signalLevel”:0,“ipAddress”:“192.168.10.110”,“linkSpeed”:0},“status”:{“recordingStatus”:{“0”:{“motionRecordingEnabled”:false,“fullTimeRecordingEnabled”:false},“1”:{“motionRecordingEnabled”:false,“fullTimeRecordingEnabled”:false},“2”:{“motionRecordingEnabled”:false,“fullTimeRecordingEnabled”:false}},“scheduledAction”:null,“remoteHost”:“192.168.10.15”,“remotePort”:7443},“sshPort”:0,“adoptionCode”:null,"_id":“581aabdfe07c95fd1454294d”,“isConnecting”:false,“isInFallbackMode”:false}],“meta”:{“totalCount”:1,“filteredCount”:1}}

It looks like bootstrap doesnt work with the API Everything else does

Do you think there is a way to skip the bootstrap and i could manually add the number of cameras I have?

Nice! With that info, we can make it work. The way the device handler is currently written there is no way to skip the bootstrap, but yes it would be possible with modification.

On that NVR, with that call to /api/2.0/camera, it reports only one camera. Do you have more than one connected?

I found another option as well. Instead of using the API key, just log in like normal. I can send you some prototype SmartApp code to see if it can log into your NVR and discover the camera. Let me work on it a bit.

I’ve created a branch that has the bootstrap refactor. Use the new SmartApp code and then in the prefs pane enter your username and password. It will log in and bootstrap but still us the API key for the camera operations so make sure whatever user you are using still has API access enabled.

I also made some small compatibility changes for 3.1.x. The API is slightly different and I’m glad that Ubiquiti has done an extend-don’t-modify as they kept the API version the same but added fields so its forwards compatible.

Now, I still don’t have any cameras attached to my 3.1.5 NVR since I don’t have spares or anything compatible that far back so I can’t test actually finding cameras. If this isn’t working, get me a dump of the bootstrap from logging in with a browser (you can use Chrome dev tools for this).

https://github.com/project802/smartthings/blob/bootstrap_refactor/unifi_nvr/unifi-nvr-smartapp.groovy

You are the man i I will give it try

We are so close thank you so much.

So the cams show in smartthings. But no video.
I see this error message under the cam
26bf088c-4d39-40bb-9bd8-eac3dff1221f 10:24:40 PM: error java.lang.NullPointerException: Cannot invoke method getAt() on null object @ line 207
26bf088c-4d39-40bb-9bd8-eac3dff1221f 10:24:40 PM: error nvr_cameraPoll() - UVC Dome (UVC Dome) failed to return API call to NVR
26bf088c-4d39-40bb-9bd8-eac3dff1221f 10:23:40 PM: error java.lang.NullPointerException: Cannot invoke method getAt() on null object @ line 207
26bf088c-4d39-40bb-9bd8-eac3dff1221f 10:23:40 PM: error nvr_cameraPoll() - UVC Dome (UVC Dome) failed to return API call to NVR
26bf088c-4d39-40bb-9bd8-eac3dff1221f 10:22:40 PM: error java.lang.NullPointerException: Cannot invoke method getAt() on null object @ line 207
26bf088c-4d39-40bb-9bd8-eac3dff1221f 10:22:40 PM: info UVC Dome (UVC Dome) updated with state: [uuid:e7479b15-2325-35df-a475-81f6e82e1e44, name:UVC Dome, id:58420fa2a095b77f079afdd1, lastRecordingStartTime:null, motion:inactive, connectionStatus:DISCONNECTED, pollInterval:5, pollIsActive:false, successiveApiFails:0]

Anyone else experiencing an issue where the cameras stop receiving motion updates? Like my driveway camera hasn’t seen any motion since Monday morning, but there’s been several recordings since. If I click on the configure for the cameras and hit done, they’ll disconnect/connect and work for a few days before breaking again.

I’ve been using UniFi 3.6.1/2 and now .3 as of today, but I’m doubting .3 will fix the issue.

I added the settings for the API.
Line 207
is return state.apiKey

/**
 *  UniFi NVR SmartApp
 *
 *  Copyright 2016 Chris Vincent
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  -----------------------------------------------------------------------------------------------------------------
 * 
 *  For more information, see https://github.com/project802/smartthings/unifi_nvr
 */
definition(
    name: "UniFi NVR",
    namespace: "project802",
    author: "Chris Vincent",
    description: "UniFi NVR SmartApp",
    category: "My Apps",
    iconUrl: "http://project802.net/smartthings/smartapp-icons/ubiquiti_nvr.png",
    iconX2Url: "http://project802.net/smartthings/smartapp-icons/ubiquiti_nvr_2x.png",
    iconX3Url: "http://project802.net/smartthings/smartapp-icons/ubiquiti_nvr_3x.png")


preferences {
    input name: "nvrAddress", type: "text", title: "NVR Address", description: "NVR IP address", required: true, displayDuringSetup: true, defaultValue: "192.168.10.15"
    input name: "nvrPort", type: "number", title: "NVR Port", description: "NVR HTTP port", required: true, displayDuringSetup: true, defaultValue: 7080
    input name: "username", type: "text", title: "Username", description: "Username", required: true, displayDuringSetup: true, defaultValue: "username"
    input name: "password", type: "text", title: "Password", description: "Password", required: true, displayDuringSetup: true, defaultValue: "password"
    input name: "apiKey", type: "text", title: "API Key", description: "API key", required: true, displayDuringSetup: true, defaultValue: "b9a7746a20976063"
}

/**
 * installed() - Called by ST platform
 */
def installed() {
    log.info "UniFi NVR: installed with settings: ${settings}"
}

/**
 * updated() - Called by ST platform
 */
def updated() {
    log.info "UniFi NVR: updated with settings: ${settings}"
    
    nvr_initialize()
}

/**
 * nvr_initialize() - Clear state and poll the bootstrap API and the result is handled by nvr_bootstrapPollCallback
 */
def nvr_initialize()
{
    state.nvrName = "Unknown"
    state.loginCookie = "";
    state.apiKey = "${settings.apiKey}"
    
    state.nvrTarget = "${settings.nvrAddress}:${settings.nvrPort}"
    log.info "nvr_initialize: NVR API is located at ${state.nvrTarget}"

    def hubAction = new physicalgraph.device.HubAction(
        [
            path: "/api/2.0/login HTTP/1.1\r\n",
            method: "POST",
            protocol: physicalgraph.device.Protocol.LAN,
            HOST: state.nvrTarget,
            body: "{\"email\":\"${settings.username}\", \"password\":\"${settings.password}\"}",
            headers: [
                "Host":"${state.nvrTarget}",
                "Accept":"application/json",
                "Content-Type":"application/json"
            ]        
        ],
        null,
        [
            callback: nvr_loginCallback 
        ]
    );

    sendHubCommand( hubAction );
}

def nvr_loginCallback( physicalgraph.device.HubResponse hubResponse )
{
    if( hubResponse.status != 200 )
    {
        log.error "nvr_loginCallback: unable to login.  Please check IP, username and password.  Status ${hubResponse.status}.";
        return;
    }
    
    String setCookieHeader = hubResponse?.headers['set-cookie'];
    def cookies = setCookieHeader.split(";").inject([:]) { cookies, item ->
        def nameAndValue = item.split("=");
        if( nameAndValue[0] == "JSESSIONID_AV" )
        {
            state.loginCookie = nameAndValue[1];
        }
    }
    
    if( !state.loginCookie )
    {
        log.error "nvr_loginCallback: unable to login.  Please check IP, username and password.";
        log.debug "nvr_loginCallback: loginCookie is ${loginCookie}";
        return;
    }
    else
    {
        log.info "nvr_loginCallback: login successful!";
    }
    
    state.apiKey = hubResponse.json?.data?.apiKey[0];
    
    def hubAction = new physicalgraph.device.HubAction(
        [
            path: "/api/2.0/bootstrap HTTP/1.1\r\n",
            method: "GET",
            protocol: physicalgraph.device.Protocol.LAN,
            HOST: state.nvrTarget,
            headers: [ 
                "Host":"${state.nvrTarget}", 
                "Accept":"application/json", 
                "Content-Type":"application/json",
                "Cookie":"JSESSIONID_AV=${state.loginCookie}"
            ]        
        ],
        null,
        [
            callback: nvr_bootstrapPollCallback 
        ]
    );

    sendHubCommand( hubAction );
}

/**
 * nvr_bootstrapPollCallback() - Callback from HubAction with result from GET
 */
def nvr_bootstrapPollCallback( physicalgraph.device.HubResponse hubResponse )
{
    def data = hubResponse.json?.data
    
    if( !data || !data.isLoggedIn )
    {
    	log.error "nvr_bootstrapPollCallback: unable to get data from NVR!"
        return
    }
    
    if( data.isLoggedIn[0] != true )
    {
    	log.error "nvr_bootstrapPollCallback: unable to log in!  Please check API key."
        return
    }
    
    state.nvrName = data.servers[0].name[0]
    log.info "nvr_bootstrapPollCallback: response from ${state.nvrName}"

    if( data.cameras[0].size < 1 )
    {
    	log.warn "nvr_bootstrapPollCallback: no cameras found!"
    	return
    }
    
    log.info "nvr_bootstrapPollCallback: found ${data.cameras[0].size} camera(s)"
    
    data.cameras[0].each { camera ->
        def dni = "${camera.mac}"
        def child = getChildDevice( dni )
        
        if( child )
        {
            log.info "nvr_bootstrapPollCallback: already have child ${dni}"
            child.updated()
        }
        else
        {
            def metaData = [   "label" : camera.name + " (" + camera.model + ")",
                               "data": [
                                   "uuid" : camera.uuid,
                                   "name" : camera.name,
                                   "id" : camera._id
                               ]
                           ]
                           
            log.info "nvr_bootstrapPollCallback: adding child ${dni} ${metaData}"
            
            try
            {
                addChildDevice( "project802", "UniFi NVR Camera", dni, location.hubs[0].id, metaData )
            }
            catch( exception )
            {
                log.error "nvr_bootstrapPollCallback: adding child ${dni} failed (child probably already exists), continuing..."
            }
        }
    }
}

/**
 * _getApiKey() - Here for the purpose of children
 */
def _getApiKey()
{
    return state.apiKey
}
/**
 * _getNvrTarget() - Here for the purpose of children
 */
def _getNvrTarget()
{
    return state.nvrTarget
}