Where should I start with "Groovy"?

So I want to learn Groovy. Mostly so I can finish porting over the Unifi NVR app and drivers from Smart Things. So far I have the App working but no go on the drivers for the cameras.

Where would you guys suggest I start with Groovy? I'm pretty good at php, ruby, bash, sh and even a tiny bit of python so it's not like I'd be a total noob at development. :slight_smile:

1 Like

not about groovy but for porting start with this thread:

think that might solve any driver porting issues you are having. on groovy see this and the various links on this page:

http://docs.smartthings.com/en/latest/getting-started/groovy-basics.html?highlight=groovy

1 Like

Yeah, I've done all the replacements and still get errors.

share the code snippet and the error you are getting?

I really appreciate the offer but having someone fix the code for me isn't exactly the point of this thread. I want to learn Groovy so I can fix this myself and learn another language.

Understood... However, a post like the following would seem to indicate that you're asking for some more assistance in some way, shape, or form... rather than just a personal blog of your activity... :wink:

While learning Groovy/Java is definitely helpful, I believe understanding the Hubitat architecture and the API to be much more valuable. I am no Groovy programmer, but I can easily Google the answer for groovy syntax questions. For the Hubitat API, this forum is the best resource until the Hubitat team posts some documentation. Porting a SmartThings piece of code over to Hubitat is more about API changes than the groovy programming language, IMHO.

1 Like

Then I must be looking at an API difference because I'm not getting a groovy error. The error I'm getting is one that is from an if statement in the driver code.

I'm getting the following error in logs:

2018-07-15 12:32:10.861:errornvr_cameraPoll() - [G] Spare (UVC G3 Dome) failed to return API call to NVR

([G] Spare (UVC G3 Dome) is the device name)

This is the result of the test "if( state.pollIsActive )" on line 178 of the driver code. However on line 79 I have "state.pollIsActive = false" which is clearly setting it to false.

    /**
 *  UniFi NVR Camera
 *
 *  Copyright 2018 Chris Story 
 *
 *  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/cstory777/unifi_hubitat 
 */
metadata {
    definition (name: "UniFi NVR Camera", namespace: "cstory777", author: "Chris Story") {
        capability "Motion Sensor"
        capability "Sensor"
        capability "Refresh"
        capability "Image Capture"
    }
   
    
    simulator {
        
    }
    
    tiles( scale: 2 ) {
        standardTile("cameraSnapshot", "device.image", width: 6, height: 4) { }
        
        standardTile("take", "device.image", width: 2, height: 2, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
            state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
            state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0"
            state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
        }
        
        standardTile("motion", "device.motion", width: 2, height: 2) {
            state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
            state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
        }
        
        standardTile( "connectionStatus", "device.connectionStatus", width: 2, height: 2 ) {
            state( "CONNECTED", label: "Connected", icon: "st.samsung.da.RC_ic_power", backgroundColor: "#79b821" )
            state( "DISCONNECTED", label: "Disconnected", icon: "st.samsung.da.RC_ic_power", backgroundColor: "#ffffff" )
        }
        
        main( "motion" )
        details( "cameraSnapshot", "take", "motion", "connectionStatus" )
    }
    
    preferences {
        input "pollInterval", "number", title: "Poll Interval", description: "Polling interval in seconds for motion detection", defaultValue: 5
        input "snapOnMotion", "bool", title: "Snapshot on motion", description: "If enabled, take a snapshot when the camera detects motion", defaultValue: false
    }
}

def installed()
{
    updated()
}

def updated()
{
    // Unschedule here to remove any zombie runIn calls that the platform
    // seems to keep around even if the code changes during dev
    unschedule()
    
    state.uuid                   = device.data( "uuid" )
    state.name                   = device.data( "name" )
    state.id                     = device.data( "id" )
    state.lastRecordingStartTime = null
    state.motion                 = "uninitialized"
    state.connectionStatus       = "uninitialized"
    state.pollInterval           = settings.pollInterval ? settings.pollInterval : 5
    state.pollIsActive           = false
    state.successiveApiFails     = 0
    state.lastPoll               = new Date().time
    
    log.info "${device.displayName} updated with state: ${state}"
    
    runEvery1Minute( nvr_cameraPollWatchdog )
    
    nvr_cameraPoll()
}

def refresh()
{
    _sendMotion( state.motion )
    _sendConnectionStatus( state.connectionStatus )
}

def take()
{
    def key = parent._getApiKey()
    def target = parent._getNvrTarget()
    
    if( state.connectionStatus == "CONNECTED" )
    {
        def hubAction = new hubitat.device.HubAction(
            [
                path: "/api/2.0/snapshot/camera/${state.id}?width=480&force=true&apiKey=${key}",
                method: "GET",
                HOST: target,
                headers: [
                    "Host":"${target}",
                    "Accept":"*/*"
                ]        
            ],
            null,
            [
                outputMsgToS3: true,
                callback: nvr_cameraTakeCallback 
            ]
        );
    
        sendHubCommand( hubAction )
    }
}

def nvr_cameraTakeCallback( hubitat.device.HubResponse hubResponse )
{
    //log.debug( "nvr_cameraTakeCallback: ${hubResponse.description}" )
    
    def descriptionMap = _parseDescriptionAsMap( hubResponse.description )
    
    if( descriptionMap?.tempImageKey )
    {
        try
        {
            storeTemporaryImage( descriptionMap.tempImageKey, _generatePictureName() )
        }
        catch( Exception e )
        {
            log.error e
        }
    }
    else
    {
        log.error "API for camera take() FAILED"
    }
}

/**
 * nvr_cameraPollWatchdog()
 * 
 * Uses a different scheduling method to watch for failures with the runIn method used by nvr_cameraPoll
 */
def nvr_cameraPollWatchdog()
{
    def now = new Date().time
    
    def elapsed = Math.floor( (now - state.lastPoll) / 1000 )
    
    //log.debug "nvr_cameraPollWatchdog: it has been ${elapsed} seconds since a poll for ${device.displayName}"
    
    if( elapsed > (state.pollInterval * 5) )
    {
        log.error "nvr_cameraPollWatchdog: expired for ${device.displayName}!"
        nvr_cameraPoll()
    }
}

/**
 * nvr_cameraPoll()
 *
 * Once called, starts cyclic call to itself periodically.  Main loop of the device handler to make API
 * to the NVR software to see if motion has changed.  API call result is handled by nvr_cameraPollCallback().
 */
def nvr_cameraPoll() 
{
    def key = parent._getApiKey()
    def target = parent._getNvrTarget()
    
    if( state.pollIsActive )
    {
        log.error "nvr_cameraPoll() - ${device.displayName} failed to return API call to NVR"
        
        ++state.successiveApiFails
        
        if( (state.connectionStatus == "CONNECTED") && (state.successiveApiFails > 5) )
        {
            log.error "nvr_cameraPoll() - ${device.displayName} has excessive consecutive failed API calls, forcing disconnect status"
            state.connectionStatus = "DISCONNECTED"
            _sendConnectionStatus( "${state.connectionStatus}" )
        }
    }
    else
    {
        state.successiveApiFails = 0;
    }
    
    state.pollIsActive = true
    
    def hubAction = new hubitat.device.HubAction(
        [
            path: "/api/2.0/camera/${state.id}?apiKey=${key}",
            method: "GET",
            HOST: target,
            headers: [
                "Host":"${target}",
                "Accept":"application/json"
            ]        
        ],
        null,
        [
            callback: nvr_cameraPollCallback 
        ]
    );
    
    sendHubCommand( hubAction );
    
    // Set overwrite to true instead of using unschedule(), which is expensive, to ensure no dups
    runIn( state.pollInterval, nvr_cameraPoll, [overwrite: true] )
}

/**
 * nvr_cameraPollCallback()
 *
 * Callback from HubAction with result from GET.
 */
def nvr_cameraPollCallback( hubitat.device.HubResponse hubResponse )
{
    def motion = "inactive"
    def data = hubResponse?.json?.data
    
    //log.debug "nvr_cameraPollCallback: ${device.displayName}, ${hubResponse}"
    
    state.pollIsActive = false;
    state.lastPoll = new Date().time
    
    if( !data[0] )
    {
        log.error "nvr_cameraPollCallback: no data returned";
        return;
    }
    
    data = data[0]
    
    if( data.state != state.connectionStatus )
    {
        state.connectionStatus = data.state
        _sendConnectionStatus( "${state.connectionStatus}" )
    }
    
    // Only do motion detection if the camera is connected and configured for it
    if( (state.connectionStatus == "CONNECTED") && (data.recordingSettings?.motionRecordEnabled) )
    {
    	// Motion is based on a new recording being present
    	if( state.lastRecordingStartTime && (state.lastRecordingStartTime != data.lastRecordingStartTime) )
        {
            motion = "active"
        }
        
        state.lastRecordingStartTime = data.lastRecordingStartTime;
    }
    else
    {
        //log.warn "nvr_cameraPollCallback: ${device.displayName} camera disconnected or motion not enabled"
    }
    
    // Fall-through takes care of case if camera motion was active but became disconnected before becoming inactive
    if( motion != state.motion )
    {
        // Send motion before doing the take to prioritize the reaction time to motion.  It isn't clear what
        // the ST platform does for scheduling or blocking in either the sendEvent or sendHubCommand calls.
        state.motion = motion
        _sendMotion( motion )
        
        if( snapOnMotion && (motion == "active") )
        {
            take()
        }
    }
}

/**
 * _sendMotion()
 *
 * Sends a motion event to the ST platform.
 *
 * @arg motion Either "active" or "inactive"
 */
private _sendMotion( motion )
{
    if( (motion != "active") && (motion != "inactive") )
    {
    	return
    }
    
    //log.debug( "_sendMotion( ${motion} )" )
    
    def description = (motion == "active" ? " detected motion" : " motion has stopped")
    
    def map = [ 
                name: "motion",
                value: motion, 
                descriptionText: device.displayName + description
              ]
    
    sendEvent( map )
}

private _sendConnectionStatus( connectionStatus )
{
    if( (connectionStatus != "CONNECTED") && (connectionStatus != "DISCONNECTED") )
    {
        return
    }
    
    //log.debug "_sendConnectionStatus: ${device.displayName} ${connectionStatus}"
    
    def map = [
                name: "connectionStatus",
                value: connectionStatus,
                descriptionText: device.displayName + " is ${connectionStatus}"
              ]
              
    sendEvent( map )
}

/**
 * _parseDescriptionAsMap()
 *
 * Converts a string of "key:value" separated by commas into a Map.
 */
private _parseDescriptionAsMap( description )
{
    description.split(",").inject([:]) { map, param ->
        def nameAndValue = param.split(":")
        map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
    }
}


/**
 * _generatePictureName()
 *
 * Builds a unique picture name for storing in S3.
 */
private _generatePictureName()
{
    def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
    def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg"
    
    return picName
}

Motion is being detected but taking a snapshot fails with "API for camera take() FAILED".

http://groovy-lang.org/learn.html

quick 60 minute walkthrough

https://learnxinyminutes.com/docs/groovy/

Most of this will work in our Groovy sandbox, some won't. Groovy is very similar to javascript than java, but depending on what language(s) you know there are some quirks that make groovy unique.

We are working on hubitat specific command documentation for system calls, etc. In the mean time, the forums here are the best source, and just ask if you are running into challenges with something specific.

1 Like

Thank you!

  • state.pollIsActive is set to false in functions updated() and nvr_cameraPollCallback(…)
  • state.pollIsActive is unconditionally set to true in function nvr_cameraPoll()

the updated() function is only invoked when the driver is installed or settings are updated and saved.

so if the function nvr_cameraPoll() is called twice in succession without an intermediate call to nvr_cameraPollCallback(…) that would trigger this error on the second pass.

any chance that is what is happening here?

I don't think it's happening twice, no. It doesn't happen twice and I'm not seeing the error in SmartThings and all I changed in the driver are the usual substitutions ("data" to "device.data" and "physicalgraph" to "hubitat"). I've made no other changes from what's working in ST.

I know it's able to poll the cameras because I'm able to use them as motion detectors in smart lighting and Rule Machine.

is there a way that you could log statements in nvr_cameraPoll() and nvr_cameraPollCallback(…) to check if it might be happening twice? that would eliminate this as a possibility.

Did that. I added a log.info for each as the first line just inside the def for each function. I'm seeing both info messages in the logs. Apparently it is happening twice.

Any idea, from looking at the driver code, what is causing this?

thanks for confirming that it is getting called twice. on ST the updated() function gets called twice. havent checked if that also happens on HE but see this:

if updated() does get called twice nvr_cameraPoll() would also get called twice without an intermediate call to nvr_cameraPollCallback(…)

EDIT: nvm … even if the updated() function is not called twice on HE in this case updated() will still be executed twice because within installed() there is also a call to updated().

in installed() comment out the call to updated() and give it a try.

1 Like

True, unless you have installed within updated too... :wink:

yeah ... that's what's going on here :slight_smile:

I will give that a shot tonight and post my results.

Thanks!

1 Like

No change, I'm still getting the following in logs:

2018-07-20 14:06:56.177:errornvr_cameraPoll() - [G] Back Yard S (UVC G3 Dome) failed to return API call to NVR

ok. from the log.info messages you added earlier you are able to confirm nvr_cameraPoll() is being called only once now?