/* * UnifiProtectAPI * * Description: * This Hubitat driver allows polling of the Unifi Protect API, initially geared around a Unifi Dream Machine Pro. * * Overall Setup: * 1) Add both the UnifiProtectAPI.groovy and the related child drivers as new user drivers * NOTE: Protect uses Access Points as bridge devices by default so in most cases you will definitely need the bridge child. * 2) Add a Virtual Device for the parent * 3) Enter the Unifi's IP/Hostname, Username, Password, and select the Controller Type in the Preference fields and Save Preferences * REQUIRED for "Other Unifi Controllers" Controller Type: Set the Controller Port #. This Preference will appear after you Save Preferences. * Set it, then Save Preferences again. This defaults to 7443 but newer version software may be using 443. * OPTIONAL: Refresh Rate, Logging, and whether WebSockets are enabled * * Features List: * WebSocket notifications allow immediate notification of motion and other events * Shows NVR CPU and memory/storage usage as well as uptime and general status * Shows Bridge uptime and general status * Shows Light data and status such as whether it is on or current has motion, although this is impacted by the refresh rate * Allow basic control of a Light-based device * Checks drdsnell.com for an updated driver on a daily basis * * Licensing: * Copyright 2024 David Snell * 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. * * Known Issue(s): * * Version Control: * 0.2.36 - Correction of isStateChange * 0.2.35 - Additional handling for webSocket types * 0.2.34 - Change to remove old driver-specific variables and events plus some data handling * 0.2.33 - Added PowerDown command * 0.2.32 - Added webSocket handling to try to deal with doorbell "ring" type and changed driver-specific attribute names to remove spaces * 0.2.31 - Added device recognition for UVC G4 Doorbell Pro PoE * 0.2.30 - Handling for additional data points returned by the API * 0.2.29 - Change to receiving CSRF to account for changes in Unifi OS 3.2.8 and additional data handling * 0.2.28 - Changes to parse to handle smart events better, using code from @blocklanders * 0.2.27 - Additional null data handling in WebSocket code * 0.2.26 - Additional handling for contact and motion from Sensors * 0.2.25 - Added Sensor settings to accomodate API changes * 0.2.24 - Handling for null epoch values * 0.2.23 - Added recognition of additional data, particularly around sensors * 0.2.22 - Adds ability to force child events to be isChanged * 0.2.21 - Including CSRF in Login even if null * 0.2.20 - Changes to accomodate UP-Sense sensor * 0.2.19 - Correction to smartDetect indices per @dcaton1220 and attempt at handling multiple types * 0.2.18 - Changes to Login error handling (trying to handle MFA better) and change to Uptime data handling for when devices add 000 * 0.2.17 - Revisions to WebSocket handling including input from @dcaton1220 * 0.2.16 - Removed calls for Login and refresh from within the WebSocket activities * 0.2.15 - Reduced events triggered by WebSocket status activity, overhaul of WebSocket returned events handling, and correction to parent GetSnapshot command * 0.2.14 - Adding smartDetectType into WebSocket data handling also changed how WebSocket Failures and LoginRetries are handled/tracked * 0.2.13 - Added some Trace logging to the WebSocket data portion * 0.2.12 - Changes to WebSocket code to try to more closely match @tomw's * 0.2.11 - Cookie expiration may have changed so Login now happens every 10 minutes to account for it and now ignoring liveviews data (not useful at this time) * 0.2.10 - Added Controller Port # preference for Other Unifi Controllers, removed PollingOK, site attribute, and excessive WebSocket logging, * added GetMotionEvents command however it just dumps data to Trace logging at this time * 0.2.9 - Handle null uptime data * 0.2.8 - Ability to get a snapshot image from a camera and better logging for adding child driver error * 0.2.7 - Corrections to WebSocket handling (due to Ubiquiti changes) as well as additional data points handled * 0.2.6 - Additional data points handled * 0.2.5 - Added ability to save settings for camera devices * 0.2.4 - Addition of initial websocket monitoring * 0.2.3 - Changes to the driver version checking, moved child device data processing to parent, and added support for Doorbell child * 0.2.2 - Initial addition of support for Camera devices * 0.2.1 - Additional commands for light devices * 0.2.0 - Setting up for different child devices with different drivers and attempting to have relevant child commands * 0.1.3 - Correction to driver update checking code * 0.1.2 - Added read-only support for recognize lights * 0.1.1 - Some cleanup from Network leftovers * 0.1.0 - Initial version * * Thank you(s): * @tomw for the WebSocket data parsing code * @Cobra for inspiration on driver version checking * @user2371 for letting me know about Hubitat adding the uploadHubFile command so images can now be captured */ // Returns the driver name def DriverName(){ return "UnifiProtectAPI" } // Returns the driver version def DriverVersion(){ return "0.2.35" } // Driver Metadata metadata{ definition( name: "UnifiProtectAPI", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/UnifiProtectAPI.groovy" ) { // Indicate what capabilities the device should be capable of capability "Sensor" capability "Refresh" capability "Actuator" // Commands //command "DoSomething" // Does something for development/testing purposes, should be commented before publishing //command "ConnectWebSocket" // Connect to the controller for WebSocket notices //command "CloseWebSocket" // Connect to the controller for WebSocket notices command "Login" // Logs in to the controller to get a cookie for the session command "GetSnapshot", [ [ name: "CameraID*", type: "STRING", description: "REQUIRED: Camera ID to get snapshot from" ] ] // Unifi Protect related commands command "GetProtectInfo" // Checks for general data on the Protect controller, same as refresh // System level commands command "PowerDown", [ [ name: "Confirmation", type: "STRING", description: "Type the word PowerDown to confirm intent to power down the controller." ] ] // Submits a reboot command to the controller. // API Methods Not Implemented // "api/events?end=EPOCH&start=EPOCH" // Appears to be able to show events between a range of epoch-based times // "api/backups" // // "api/cameras" // May show camera data command "GetMotionEvents", [ [ name: "Number*", type: "ENUM", description: "REQUIRED: Number of events to get", defaultValue: 10, constraints: [ 1, 5, 10, 25, 50, 100 ] ] ] // Attributes for the driver itself attribute "DriverName", "string" // Identifies the driver being used for update purposes attribute "DriverVersion", "string" // Handles version for driver attribute "DriverStatus", "string" // Handles version notices for driver // Attributes for the device attribute "Status", "string" // Show success/failure of commands performed attribute "Last Login", "string" // Shows when the last login was performed attribute "Last Refresh", "string" // Shows when the last refresh was performed attribute "Last Updated ID", "string" // Shows the ID for the last update from the bootstrap attribute "WebSocket Delay", "number" attribute "WebSocket Status", "enum", [ "OK", "Unknown", "Error" ] attribute "WebSocket Open", "enum", [ "open", "closed" ] } preferences{ section{ if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options input( type: "enum", name: "RefreshRate", title: "Stats Refresh Rate", required: true, multiple: false, options: [ "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "3 hours", "Manual" ], defaultValue: "Manual" ) input( type: "bool", name: "EnableWebSocket", title: "Enable WebSocket Monitoring
Required for notification rather than polling of device events", required: false, defaultValue: true ) input( type: "enum", name: "LogType", title: "Enable Logging?", required: false, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) input( type: "enum", name: "Controller", title: "Unifi Controller Type", required: true, multiple: false, options: [ "Unifi Dream Machine (inc Pro)", "Other Unifi Controllers" ], defaultValue: "Unifi Dream Machine (inc Pro)" ) if( Controller == "Other Unifi Controllers" ){ input( type: "string", name: "ControllerPort", title: "Controller Port #", defaultValue: "7443", required: true ) } input( type: "string", name: "UnifiURL", title: "Unifi Controller IP/Hostname", required: true ) input( type: "string", name: "Username", title: "Username", required: true ) input( type: "password", name: "Password", title: "Password", required: true ) input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } else { input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } } } } // Command to test fixes or other oddities during development def DoSomething(){ } // Create a WebSocket connection to monitor for events def ConnectWebSocket(){ try{ if( Controller == "Unifi Dream Machine (inc Pro)" ){ interfaces.webSocket.connect( "wss://${ UnifiURL }/proxy/protect/ws/updates?lastUpdateId=${ state.'Last Updated ID' }", ignoreSSLIssues: true, headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ) } else { interfaces.webSocket.connect( "wss://${ UnifiURL }:${ ControllerPort }/ws/updates?lastUpdateId=${ state.'Last Updated ID' }", ignoreSSLIssues: true, headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ) } } catch( Exception e ){ Logging( "Exception when opening WebSocket: ${ e }", 5 ) ProcessState( "WebSocket Status", "Error" ) } } // Close the WebSocket connection def CloseWebSocket(){ try{ ProcessState( "WebSocket Open", "closed" ) pauseExecution( 500 ) interfaces.webSocket.close() } catch( Exception e ){ Logging( "Exception when closing WebSocket: ${ e }", 5 ) ProcessState( "WebSocket Status", "WebSocket close error" ) } } // Send a WebSocket message, if needed at some point def SendWebSocketMessage( String Message ){ try{ interfaces.webSocket.sendMessage( Message ) } catch( Exception e ){ Logging( "Exception when sending WebSocket message: ${ e }", 5 ) ProcessState( "WebSocket Status", "WebSocket message error" ) } } // parse appears to be one of those "special" methods for when data is returned, for this driver the WebSocket data // Changes to beginning regarding modelKey and events from @blocklanders def parse( String description ){ def Data = DecodeWebSocket( description ) if( Data != null ){ Logging( "WebSocket Data = ${ Data }", 4 ) def modelKey = Data.actionPacket.actionPayload.modelKey def EventID def TempID if( modelKey == "event" ){ TempID = Data.actionPacket.actionPayload.recordId if( TempID.indexOf( "-" ) != -1 ){ TempID = Data.actionPacket.actionPayload.recordId.split( "-" ) EventID = TempID[ 0 ] TempID = TempID[ 1 ] } else { TempID = Data.actionPacket.actionPayload.recordId EventID = TempID } } else { TempID = Data.actionPacket.actionPayload.id if( TempID.indexOf( "-" ) != -1 ){ TempID = Data.actionPacket.actionPayload.id.split( "-" ) EventID = TempID[ 0 ] TempID = TempID[ 1 ] } else { TempID = Data.actionPacket.actionPayload.id EventID = TempID } } def Device def LastAction def LastActionID getChildDevices().each{ if( TempID == getChildDevice( it.deviceNetworkId ).ReturnState( "ID" ) ){ Device = it.deviceNetworkId LastAction = getChildDevice( it.deviceNetworkId ).ReturnState( "LastWSSAction" ) LastActionID = getChildDevice( it.deviceNetworkId ).ReturnState( "LastWSSID" ) } } if( ( Device == null ) && ( Data.dataPacket?.dataPayload != null ) ){ if( Data.dataPacket?.dataPayload.metadata!= null ){ if( Data.dataPacket?.dataPayload.metadata.sensorId != null ){ if( Data.dataPacket?.dataPayload.metadata.sensorId.text != null ){ TempID = Data.dataPacket?.dataPayload.metadata.sensorId.text getChildDevices().each{ if( TempID == getChildDevice( it.deviceNetworkId ).ReturnState( "ID" ) ){ Device = it.deviceNetworkId LastAction = getChildDevice( it.deviceNetworkId ).ReturnState( "LastWSSAction" ) LastActionID = getChildDevice( it.deviceNetworkId ).ReturnState( "LastWSSID" ) } } } } } } if( Device != null ){ if( Data.dataPacket?.dataPayload != null ){ Data.dataPacket?.dataPayload.each(){ switch( it.key ){ case "lastMotion": if( it.value != null ){ PostEventToChild( "${ Device }", "Last Motion", ConvertEpochToDate( "${ it.value }" ) ) } break case "lastRing": if( it.value != null ){ getChildDevice( "${ Device }" ).push( 1 ) } break case "isDark": if( it.value ){ PostEventToChild( "${ Device }", "Dark", "true" ) } else { PostEventToChild( "${ Device }", "Dark", "false" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "Dark" ) } PostStateToChild( "${ Device }", "LastWSSAction", "Dark" ) break case "isMotionDetected": if( it.value ){ PostEventToChild( "${ Device }", "motion", "active" ) } else { PostEventToChild( "${ Device }", "motion", "inactive" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "motionDetected" ) } PostStateToChild( "${ Device }", "LastWSSAction", "motionDetected" ) break case "isPirMotionDetected": if( it.value ){ PostEventToChild( "${ Device }", "motion", "active" ) } else { PostEventToChild( "${ Device }", "motion", "inactive" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "motionDetected" ) } PostStateToChild( "${ Device }", "LastWSSAction", "motionDetected" ) break case "isSmartDetected": if( it.value ){ PostEventToChild( "${ Device }", "motion", "active" ) } else { PostEventToChild( "${ Device }", "motion", "inactive" ) PostEventToChild( "${ Device }", "smartDetectType", "none" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "motionDetected" ) } PostStateToChild( "${ Device }", "LastWSSAction", "motionDetected" ) break case "smartDetectTypes": if( it.value.size() > 1 ){ def TempString def Count = 0 it.value.each{ TempString = "${ it.value[ Count ] }" Count = ( Count + 1 ) if( ( Count + 1 ) < it.value.size() ){ TempString = TempString + ", " } else if( ( Count + 1 ) == it.value.size() ){ TempString = TempString + " & " } } PostEventToChild( "${ Device }", "smartDetectType", TempString ) } else if( it.value.size() == 1 ){ PostEventToChild( "${ Device }", "smartDetectType", "${ it.value[ 0 ] }" ) } else { PostEventToChild( "${ Device }", "smartDetectType", "none" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "smartDetectType" ) } PostStateToChild( "${ Device }", "LastWSSAction", "smartDetectType" ) break case "isLightOn": if( it.value ){ PostEventToChild( "${ Device }", "switch", "on" ) } else { PostEventToChild( "${ Device }", "switch", "off" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "Light" ) } PostStateToChild( "${ Device }", "LastWSSAction", "Light" ) break case "isRecording": if( it.value ){ PostEventToChild( "${ Device }", "Recording Now", "true" ) } else { PostEventToChild( "${ Device }", "Recording Now", "false" ) } if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "Recording Now" ) } PostStateToChild( "${ Device }", "LastWSSAction", "Recording Now" ) break case "eventStats": if( it.value.motion.today != null ){ PostEventToChild( "${ Device }", "Motion Events Today", it.value.motion.today ) } break case "type": Logging( "WebSocket Type found = ${ it.value }", 4 ) switch( it.value ){ case "motion": PostEventToChild( "${ Device }", "motion", "active" ) if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "motion" ) } PostStateToChild( "${ Device }", "LastWSSAction", "motion" ) pauseExecution( 1000 ) PostEventToChild( "${ Device }", "motion", "inactive" ) break case "sensorMotion": PostEventToChild( "${ Device }", "motion", "active" ) if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "sensorMotion" ) } PostStateToChild( "${ Device }", "LastWSSAction", "sensorMotion" ) pauseExecution( 1000 ) PostEventToChild( "${ Device }", "motion", "inactive" ) break case "sensorOpened": PostEventToChild( "${ Device }", "contact", "open" ) if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "sensorOpened" ) } PostStateToChild( "${ Device }", "LastWSSAction", "sensorOpened" ) break case "sensorClosed": PostEventToChild( "${ Device }", "contact", "closed" ) if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "sensorClosed" ) } PostStateToChild( "${ Device }", "LastWSSAction", "sensorClosed" ) break case "ring": getChildDevice( "${ Device }" ).push( 1 ) if( Data.actionPacket?.actionPayload?.action?.toString() == "add" ){ ProcessState( "LastWSSAction", "ring" ) } PostStateToChild( "${ Device }", "LastWSSAction", "ring" ) //PostEventToChild( "${ Device }", "pushed", 1, null, true ) break case "smartDetectZone": PostEventToChild( "${ Device }", "smartDetectZone", "active" ) break case "access": PostEventToChild( "${ Device }", "access", "active" ) break default: Logging( "Unhandled WebSocket Type for ${ Device }: ${ it.value }", 3 ) break } break // Things to ignore case "uptime": case "upSince": case "lastSeen": case "recordingSchedules": case "wifiConnectionState": break default: Logging( "${ Device } Update: ${ it.key } = ${ it.value }", 4 ) break } ProcessState( "LastWSSID", EventID ) PostStateToChild( Device, "LastWSSID", EventID ) } } else if( ( Data.actionPacket?.actionPayload?.action?.toString() == "update" ) && ( ( Data.dataPacket?.dataPayload == null ) || ( Data.dataPacket?.dataPayload == "null" ) ) ){ if( LastActionID == EventID ){ switch( LastAction ){ case "Dark": PostEventToChild( "${ Device }", "Dark", "true" ) break case "motionDetected": PostEventToChild( "${ Device }", "motion", "inactive" ) break case "smartDetectType": PostEventToChild( "${ Device }", "smartDetectType", null ) break case "Light": PostEventToChild( "${ Device }", "switch", "off" ) break case "Recording Now": PostEventToChild( "${ Device }", "Recording Now", "false" ) break case "motion": PostEventToChild( "${ Device }", "motion", "inactive" ) break case "sensorMotion": PostEventToChild( "${ Device }", "motion", "inactive" ) break case "smartDetectZone": PostEventToChild( "${ Device }", "smartDetectZone", "active" ) break case "access": PostEventToChild( "${ Device }", "access", "active" ) break case "ring": getChildDevice( "${ Device }" ).release( 1 ) break } } } } } } // Beggining of @tomw's code (with minor changes) import java.io.ByteArrayOutputStream import java.util.zip.Inflater // Receive status from the WebSocket, kindof like the WebSocket version of parse(?) def webSocketStatus( String Response ){ Logging( "WebSocket Status = ${ Response }", 4 ) // @tomw: thanks for the idea: https://community.hubitat.com/t/websocket-client/11843/15 if( Response.startsWith( "status: open" ) ){ ProcessState( "WebSocket Delay", 1 ) ProcessState( "WebSocket Open", "open" ) ProcessState( "WebSocket Status", "OK" ) return } else if( Response.startsWith( "status: closing" ) ){ if( state.'WebSocket Open' != "open" ){ ProcessState( "WebSocket Open", "closed" ) return } reinitialize() return } else if( Response.startsWith( "failure:" ) ){ ProcessState( "WebSocket Failures", ( state.'WebSocket Failures' + 1 ) ) if( state.'WebSocket Failures' > 5 ){ ProcessState( "WebSocket Status", "Failed - Requires a manual login to reset" ) } else { ProcessState( "WebSocket Status", "Error" ) reinitialize() } return } } def reinitialize(){ // thanks @ogiewon for the example // first delay is 2 seconds, doubles every time def delayCalc = ( state.'WebSocket Delay' ?: 1 ) * 2 // upper limit is 600s def reconnectDelay = delayCalc <= 600 ? delayCalc : 600 ProcessState( "WebSocket Delay", reconnectDelay ) runIn( reconnectDelay, initialize ) } def initialize(){ if( EnableWebSocket && ( state.'WebSocket Failures' < 5 ) ){ //ProcessEvent( "WebSocket Status", "Unknown" ) try { unschedule( "initialize" ) CloseWebSocket() runIn( 5, ConnectWebSocket ) ProcessState( "WebSocket Status", "OK" ) } catch( Exception e ){ Logging( "WebSocket initialization failed: ${ e.message }", 5 ) ProcessState( "WebSocket Status", "Error" ) ProcessState( "WebSocket Failures", ( state.'WebSocket Failures' + 1 ) ) reinitialize() } } } private decompress(s){ // based on this example: https://dzone.com/articles/how-compress-and-uncompress def sBytes = hubitat.helper.HexUtils.hexStringToByteArray(s) Inflater inflater = new Inflater() inflater.setInput(sBytes) ByteArrayOutputStream outputStream = new ByteArrayOutputStream(sBytes.length) byte[] buffer = new byte[1024] while(!inflater.finished()) { int count = inflater.inflate(buffer) outputStream.write(buffer, 0, count) } outputStream.close() def resp = new String(outputStream.toByteArray()) return resp } private subBytes( arr, start, length ){ return arr.toList().subList( start, start + length ) as byte[] } private repackHeaderAsMap( header ){ def headerMap = [ packetType: subBytes(header, 0, 1), payloadFormat: subBytes(header, 1, 1), deflated: subBytes(header, 2, 1), payloadSize: hubitat.helper.HexUtils.hexStringToInt(hubitat.helper.HexUtils.byteArrayToHexString(subBytes(header, 4, 4))) ] } private DecodeWebSocket( hexString ){ // all of this is based on the packet formats described here: https://github.com/hjdhjd/homebridge-unifi-protect/blob/master/src/protect-api-updates.ts def actionHeader def actionLength def dataHeader def dataLength def bytes // // first, basic packet validation // try{ //logDebug("incoming message = ${hexString}") bytes = hubitat.helper.HexUtils.hexStringToByteArray(hexString) actionHeader = subBytes(bytes, 0, 8) actionLength = hubitat.helper.HexUtils.hexStringToInt(hubitat.helper.HexUtils.byteArrayToHexString(subBytes(actionHeader, 4, 4))) dataHeader = subBytes(bytes, actionHeader.size() + actionLength, 8) dataLength = hubitat.helper.HexUtils.hexStringToInt(hubitat.helper.HexUtils.byteArrayToHexString(subBytes(dataHeader, 4, 4))) def totalLength = actionHeader.size() + actionLength + dataHeader.size() + dataLength //logDebug("totalLength = ${totalLength}") //logDebug("bytes.size() = ${bytes.size()}") if( totalLength != bytes.size() ){ throw new Exception("Header/Packet mismatch.") } if( dataLength > 0x180 ){ // presumed status packet, which introduces processing overhead due to chattiness return null } } catch( Exception e ){ Logging( "Exception when parsing WebSocket response: ${ e }", 5 ) return null } // // then, decode and re-pack data // try{ def actionHeaderMap = repackHeaderAsMap( actionHeader) def dataHeaderMap = repackHeaderAsMap(dataHeader) def actionPacket = hubitat.helper.HexUtils.byteArrayToHexString(subBytes(bytes, actionHeader.size(), actionHeaderMap.payloadSize)) def dataPacket = hubitat.helper.HexUtils.byteArrayToHexString(subBytes(bytes, actionHeader.size() + actionLength + dataHeader.size(), dataHeaderMap.payloadSize)) def slurper = new groovy.json.JsonSlurper() def actionJson = (actionHeaderMap.deflated?.getAt(0) ? decompress(actionPacket) : makeString(actionPacket)) def actionJsonMap = slurper.parseText(actionJson.toString()) // pre-check whether this is a packet we want to further decode... def isInterestingPacket = ( (actionJsonMap?.modelKey?.toString() == "camera" && actionJsonMap?.action?.toString() == "update") || (actionJsonMap?.modelKey?.toString() == "event" && actionJsonMap?.action?.toString() == "add") || (actionJsonMap?.modelKey?.toString() == "light" && actionJsonMap?.action?.toString() == "update") ) // ...if not, bail out to avoid unnecessary processing def dataJson = isInterestingPacket ? (dataHeaderMap.deflated?.getAt(0) ? decompress(dataPacket) : makeString(dataPacket)) : null def dataJsonMap = isInterestingPacket ? (slurper.parseText(dataJson.toString())) : null def decodedPacket = [ actionPacket: [actionHeader: actionHeaderMap, actionPayload: actionJsonMap], dataPacket: [dataHeader: dataHeaderMap, dataPayload: dataJsonMap] ] return decodedPacket } catch( Exception e ){ Logging( "Exception when decoding WebSocket response: ${ e }", 5 ) return null } } private makeString(s){ return new String(hubitat.helper.HexUtils.hexStringToByteArray(s)) } // End of @tomw's code // updated is called whenever device parameters are saved def updated(){ Logging( "Updating...", 2 ) if( state."Driver Name" != null ){ state.remove( "Driver Name" ) state.remove( "Driver Version" ) device.deleteCurrentState( "Driver Name" ) device.deleteCurrentState( "Driver Version" ) } ProcessEvent( "DriverName", DriverName() ) ProcessEvent( "DriverVersion", DriverVersion() ) if( LogType == null ){ LogType = "Info" } if( Controller == null ){ Controller = "Unifi Dream Machine (inc Pro)" } if( ControllerPort == null ){ if( Controller == "Other Unifi Controllers" ){ ControllerPort = "7443" } } // Reset LoginRetries counter state.LoginRetries = 0 pauseExecution( 1000 ) SetScheduledTasks() if( EnableWebSocket ){ ProcessState( "WebSocket Failures", 0 ) } Logging( "Updated", 2 ) } // refresh performs a poll of data def refresh(){ if( EnableWebSocket ){ CloseWebSocket() } //if( state.LoginRetries < 5 ){ GetProtectInfo() //} if( EnableWebSocket && ( state.'WebSocket Failures' < 5 ) ){ ConnectWebSocket() } ProcessState( "Last Refresh", new Date() ) } // Set scheduled tasks def SetScheduledTasks(){ unschedule() // Schedule a login every 10 minutes def Hour = ( new Date().format( "h" ) as int ) def Minute = ( new Date().format( "m" ) as int ) def Second = ( new Date().format( "s" ) as int ) Second = ( (Second + 5) % 60 ) schedule( "${ Second } 0/10 * ? * *", "Login" ) // Check what the refresh rate is set for then run it switch( RefreshRate ){ case "1 minute": // Schedule the refresh check for every minute schedule( "${ Second } * * ? * *", "refresh" ) break case "5 minutes": // Schedule the refresh check for every 5 minutes schedule( "${ Second } 0/5 * ? * *", "refresh" ) break case "10 minutes": // Schedule the refresh check for every 10 minutes schedule( "${ Second } 0/10 * ? * *", "refresh" ) break case "15 minutes": // Schedule the refresh check for every 15 minutes schedule( "${ Second } 0/15 * ? * *", "refresh" ) break case "30 minutes": // Schedule the refresh check for every 30 minutes schedule( "${ Second } 0/30 * ? * *", "refresh" ) break case "1 hour": // Schedule the refresh check for every hour schedule( "${ Second } ${ Minute } * ? * *", "refresh" ) break case "3 hours": // Schedule the refresh check for every 3 hours schedule( "${ Second } ${ Minute } 0/3 ? * *", "refresh" ) break default: RefreshRate = "Manual" break } Logging( "Refresh rate: ${ RefreshRate }", 4 ) // Set the driver name and version before update checking is scheduled if( state."Driver Name" != null ){ state.remove( "Driver Name" ) state.remove( "Driver Version" ) device.deleteCurrentState( "Driver Name" ) device.deleteCurrentState( "Driver Version" ) } ProcessEvent( "DriverName", DriverName() ) ProcessEvent( "DriverVersion", DriverVersion() ) // Schedule checks that are only performed once a day schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "CheckForUpdate" ) } //Log in to Unifi def Login( Manual = true ){ def Params if( Manual || ( state.LoginRetries < 5 ) ){ if( Controller == "Unifi Dream Machine (inc Pro)" ){ Params = [ uri: "https://${ UnifiURL }/api/auth/login", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", body: "{\"username\":\"${ Username }\",\"password\":\"${ Password }\",\"remember\":\"true\"}", headers: [ 'X-CSRF-Token': state.CSRF ] ] } else { Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/api/login", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", body: "{\"username\":\"${ Username }\",\"password\":\"${ Password }\",\"remember\":\"true\"}", headers: [ 'X-CSRF-Token': state.CSRF ] ] } //Logging( "Login Parameters = ${ Params }", 4 ) try{ httpPost( Params ){ resp -> //Logging( "Login response = ${ resp.data }", 4 ) switch( resp.getStatus() ){ case 200: if( state.LoginRetries > 0 ){ ProcessState( "LoginRetries", 0 ) SetScheduledTasks() } if( Manual && EnableWebSocket ){ ProcessState( "WebSocket Failures", 0 ) } ProcessState( "Status", "Login successful." ) ProcessState( "Last Login", new Date() ) def Cookie resp.getHeaders().each{ if( ( it.value.split( '=' )[ 0 ].toString() == "unifises" ) || ( it.value.split( '=' )[ 0 ].toString() == "TOKEN" ) ){ Cookie = resp.getHeaders().'Set-Cookie' if( Controller == "Unifi Dream Machine (inc Pro)" ){ Cookie = Cookie.split( ";" )[ 0 ] + ";" } else { Cookie = Cookie.split( ";" )[ 0 ] } ProcessState( "Cookie", Cookie ) } else { def CSRF if( Controller == "Unifi Dream Machine (inc Pro)" ){ CSRF = it as String if( CSRF.split( ':' )[ 0 ].toUpperCase() == "X-CSRF-TOKEN" ){ ProcessState( "CSRF", it.value ) } } else { if( it.value.split( '=' )[ 0 ].toString() == "csrf_token" ){ CSRF = it.value.split( ';' )[ 0 ].split( '=' )[ 1 ] ProcessState( "CSRF", CSRF ) } } } } asynchttpPost( "ReceiveData", GenerateProtectParams( "api/auth/access-key" ), [ Method: "Auth Key" ] ) break case 403: Logging( "Forbidden", 3 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break case 404: Logging( "Not found", 3 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break case 408: Logging( "Request Timeout", 3 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break default: Logging( "Error logging in to controller: ${ resp.status }", 4 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break } } } catch( Exception e ){ if( state.LoginRetries > 5 ){ Logging( "Too many login failures. Please confirm the local username & password and perform a manual login.", 5 ) ProcessState( "Status", "Login failure, check account information and perform manual login." ) unschedule( "Login" ) unschedule( "refresh" ) } else { def Temp = "${ e }" Logging( "Temp Login Status = ${ Temp }", 3 ) def StatusCode = Temp.split( "status code: " )[ 1 ].split( "," )[ 0 ] switch( StatusCode as int ){ case 403: Logging( "Forbidden", 5 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break case 404: Logging( "Not found", 5 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break case 408: Logging( "Request Timeout", 5 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break case 499: Logging( "Connection Closed - Likely due to MFA, please confirm Login using MFA method", 3 ) ProcessState( "Status", "MFA Login Required" ) //ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break default: Logging( "Error logging in to controller: ${ e }", 5 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) break } } } } else { Logging( "Too many login failures. Please confirm the local username & password and perform a manual login.", 5 ) ProcessState( "Status", "Login failure, check account information and perform manual login." ) } } // Command to attempt to get general Protect info def GetProtectInfo(){ def Attempt = "api/bootstrap" asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "Bootstrap" ] ) } // Attempts to get a list of motion-based events def GetMotionEvents( Number ){ def Attempt = "api/events?allCameras=true&end&limit=${ Number }&orderDirection=DESC&start&types=motion&types=smartDetectZone&types=smartDetectLine&types=ring" Logging( "Attempting to get ${ Number } motion events", 4 ) asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "GetMotionEvents", Number: "${ Number }" ] ) } // Command to attempt to get a camera's snapshot def GetSnapshot( String CameraID, String DNI = null ){ if( DNI == null ){ getChildDevices().each{ if( CameraID == getChildDevice( it.deviceNetworkId ).ReturnState( "ID" ) ){ DNI = it.deviceNetworkId } } } def Attempt = "api/cameras/${ CameraID }/snapshot" Logging( "Attempting to GetSnapshot for ${ CameraID }", 4 ) asynchttpGet( "ReceiveImageData", GenerateProtectImageParams( "${ Attempt }" ), [ Method: "GetSnapshot", DNI: "${ DNI }", Camera: "${ CameraID }" ] ) } // Receive image data from the controller def ReceiveImageData( resp, data ){ switch( resp.getStatus() ){ case 200: ProcessState( "Status", "${ data.Method } successful." ) switch( data.Method ){ case "GetSnapshot": PostEventToChild( "${ data.DNI }", "image", "" ) //FileData = resp.data.getBytes() //uploadHubFile( "Camera_${ data.Camera }_Snapshot.jpg", FileData ) //PostEventToChild( "${ data.DNI }", "Snapshot", "" ) break } break case 400: // Bad request ProcessState( "Status", "${ data.Method } Bad Request" ) Logging( "Bad Request for ${ data.Method }", 5 ) break case 401: ProcessState( "Status", "${ data.Method } Unauthorized, please Login again" ) Logging( "Unauthorized for ${ data.Method } please Login again", 5 ) break case 404: ProcessState( "Status", "${ data.Method } Page not found error" ) Logging( "Page not found for ${ data.Method }", 5 ) break case 408: ProcessState( "Status", "Request timeout for ${ data.Method }" ) Logging( "Timeout for ${ data.Method } headers = ${ resp.getHeaders() }", 4 ) break default: ProcessState( "Status", "Error ${ resp.status } connecting for ${ data.Method }" ) Logging( "Error connecting to Unifi Controller: ${ resp.status } for ${ data.Method }", 5 ) break } } // Receive general data from the controller def ReceiveData( resp, data ){ switch( resp.getStatus() ){ case 200: def Json = parseJson( resp.data ) ProcessState( "Status", "${ data.Method } successful." ) switch( data.Method ){ case "Auth Key": ProcessState( "Auth Key", "${ Json.accessKey }" ) break case "Bootstrap": Logging( "Bootstrap = ${ resp.data }", 4 ) ProcessState( "Last Updated ID", Json.lastUpdateId ) Json.cameras.each(){ def DeviceType switch( it.type ){ case "UVC G4 Doorbell Pro PoE": // G4 Doorbell Pro PoE case "UVC G4 Doorbell Pro": // G4 Doorbell Pro case "UVC G4 Doorbell": // G4 Doorbell DeviceType = "Doorbell" break case "UVC G4 Instant": // Camera G4 Instant case "UVC G4 Flex": // Camera G4 Flex case "UVC G3 Instant": // Camera G3 Instant case "UVC G3 Flex": // Camera G3 Flex case "UVC G4 PTZ": // Camera G4 PTZ case "UVC G4 PRO": // Camera G4 Pro case "UVC AI Bullet": // Camera AI Bullet case "UVC AI 360": // Camera AI 360 case "UVC G3 PRO": // Camera G3 Pro case "UVC G4 BULLET": // Camera G4 Bullet case "UVC G4 DOME": // Camera G4 Dome case "UVC G3 BULLET": // Camera G3 Bullet default: DeviceType = "Camera" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } Json.liveviews.each(){ // Ignoring liveviews at this time because there is not much of anything that can be done with them //Logging( "liveviews Unhandled: ${ it }", 4 ) } if( Json.nvr.size() >= 1 ){ def DeviceType switch( Json.nvr.type ){ case "UDM-PRO": // Unifi Dream Machine Pro default: DeviceType = "NVR" break } ProcessData( "${ DeviceType } ${ Json.nvr.mac }", Json.nvr ) } Json.viewers.each(){ def DeviceType switch( it.type ){ default: DeviceType = "Viewer" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } Json.displays.each(){ def DeviceType switch( it.type ){ default: DeviceType = "Display" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } Json.lights.each(){ def DeviceType switch( it.type ){ case "UP FloodLight": // Floodlight default: DeviceType = "Light" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } Json.bridges.each(){ def DeviceType switch( it.type ){ case "UFP-UAP-B": // Unifi Access Point - Covers multiple actual models apparently default: DeviceType = "Bridge" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } Json.sensors.each(){ def DeviceType switch( it.type ){ default: DeviceType = "Sensor" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } Json.doorlocks.each(){ def DeviceType switch( it.type ){ default: DeviceType = "Doorlock" break } ProcessData( "${ DeviceType } ${ it.mac }", it ) } break case "GetChildStatus": Logging( "GetChildStatus for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "GetBridgeStatus": Logging( "GetBridgeStatus for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "GetCameraStatus": Logging( "GetCameraStatus for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "GetDoorbellStatus": Logging( "GetDoorbellStatus for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "GetLightStatus": Logging( "GetLightStatus for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "GetSensorStatus": Logging( "GetSensorStatus for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "GetMotionEvents": Logging( "GetMotionEvents: ${ resp.data }", 4 ) break case "SendBridgeSettings": Logging( "SendBridgeSettings for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "SendCameraSettings": Logging( "SendCameraSettings for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "SendDoorbellSettings": Logging( "SendDoorbellSettings for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "SendLightSettings": Logging( "SendLightSettings for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "SendSensorSettings": Logging( "SendSensorSettings for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "SendCameraSettings": Logging( "SendLightSettings for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "SwitchLight": Logging( "SwitchLight for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "LocateBridge": Logging( "LocateBridge for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "LocateCamera": Logging( "LocateCamera for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "LocateDoorbell": Logging( "LocateDoorbell for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "LocateLight": Logging( "LocateLight for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "LocateSensor": Logging( "LocateSensor for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break case "LightBrightness": Logging( "LightBrightness for ${ data.DNI }: ${ resp.data }", 4 ) ProcessData( "${ data.DNI }", Json ) break default: Logging( "${ data.Method } Unhandled: ${ resp.data }", 3 ) break } break case 400: // Bad request ProcessState( "Status", "${ data.Method } Bad Request, please Login again" ) Logging( "Bad Request for ${ data.Method } please Login again", 5 ) break case 401: // Unauthorized if( state.LoginRetries < 5 ){ ProcessState( "Status", "${ data.Method } Unauthorized, attempting login and retry..." ) Logging( "Unauthorized for ${ data.Method }, attempting login and retry...", 5 ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) Login( false ) switch( data.Method ){ case "Auth Key": break case "Bootstrap": GetProtectInfo() break case "GetChildStatus": break case "GetBridgeStatus": break case "GetCameraStatus": break case "GetDoorbellStatus": break case "GetLightStatus": break case "GetSensorStatus": break case "GetMotionEvents": GetMotionEvents() break case "SendBridgeSettings": break case "SendCameraSettings": break case "SendDoorbellSettings": break case "SendLightSettings": break case "SendSensorSettings": break case "SendCameraSettings": break case "SwitchLight": break case "LocateBridge": break case "LocateCamera": break case "LocateDoorbell": break case "LocateLight": break case "LocateSensor": break case "LightBrightness": break default: Logging( "${ data.Method } Unhandled: ${ resp.data }", 3 ) break } } else { ProcessState( "Status", "${ data.Method } Unauthorized, too many retry failures, try again manually." ) Logging( "Unauthorized for ${ data.Method }, too many retry failures, try again manually.", 5 ) } break case 404: ProcessState( "Status", "${ data.Method } Page not found error" ) Logging( "Page not found for ${ data.Method }", 5 ) break case 408: ProcessState( "Status", "Request timeout for ${ data.Method }" ) Logging( "Timeout for ${ data.Method } headers = ${ resp.getHeaders() }", 4 ) break default: ProcessState( "Status", "Error ${ resp.status } connecting for ${ data.Method }" ) Logging( "Error connecting to Unifi Controller: ${ resp.status } for ${ data.Method }", 5 ) break } } // Attempt to identify/locate a device def LocateLight( String DNI, String ChildID ){ def Attempt = "api/lights/${ ChildID }/locate" asynchttpPost( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "LocateLight", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Attempt to identify/locate a device def LocateCamera( String DNI, String ChildID ){ def Attempt = "api/cameras/${ ChildID }/locate" asynchttpPost( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "LocateCamera", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Attempt to identify/locate a device def LocateDoorbell( String DNI, String ChildID ){ def Attempt = "api/doorbells/${ ChildID }/locate" asynchttpPost( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "LocateDoorbell", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Attempt to identify/locate a device def LocateBridge( String DNI, String ChildID ){ def Attempt = "api/bridges/${ ChildID }/locate" asynchttpPost( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "LocateBridge", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Attempt to identify/locate a device def LocateSensor( String DNI, String ChildID ){ def Attempt = "api/sensors/${ ChildID }/locate" asynchttpPost( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "LocateSensor", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Configure device settings based on Preferences def SendBridgeSettings( String DNI, String ChildID, String Value ){ def Attempt = "api/bridges/" asynchttpPatch( "ReceiveData", GenerateProtectManageParams( "${ Attempt }", "${ ChildID }", "${ Value }" ), [ Method: "SendBridgeSettings", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // Configure device settings based on Preferences def SendLightSettings( String DNI, String ChildID, String Value ){ def Attempt = "api/lights/" asynchttpPatch( "ReceiveData", GenerateProtectManageParams( "${ Attempt }", "${ ChildID }", "${ Value }" ), [ Method: "SendLightSettings", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // Configure device settings based on Preferences def SendSensorSettings( String DNI, String ChildID, String Value ){ def Attempt = "api/sensors/" asynchttpPatch( "ReceiveData", GenerateProtectManageParams( "${ Attempt }", "${ ChildID }", "${ Value }" ), [ Method: "SendSensorSettings", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // Configure device settings based on Preferences def SendCameraSettings( String DNI, String ChildID, String Value ){ def Attempt = "api/cameras/" asynchttpPatch( "ReceiveData", GenerateProtectManageParams( "${ Attempt }", "${ ChildID }", "${ Value }" ), [ Method: "SendCameraSettings", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // Configure device settings based on Preferences def SendDoorbellSettings( String DNI, String ChildID, String Value ){ def Attempt = "api/doorbells/" asynchttpPatch( "ReceiveData", GenerateProtectManageParams( "${ Attempt }", "${ ChildID }", "${ Value }" ), [ Method: "SendDoorbellSettings", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // Handle switching light on or off def SwitchLight( String DNI, String ChildID, String Value ){ def Attempt = "api/lights/" if( Value == "on" ){ asynchttpPatch( "ReceiveData", GenerateProtectCommandParams( "${ Attempt }", "${ ChildID }", "{\"lightOnSettings\":{\"isLedForceOn\":true}}" ), [ Method: "SwitchLight", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } else { asynchttpPatch( "ReceiveData", GenerateProtectCommandParams( "${ Attempt }", "${ ChildID }", "{\"lightOnSettings\":{\"isLedForceOn\":false}}" ), [ Method: "SwitchLight", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } } // Handle switching light level def LightBrightness( String DNI, String ChildID, Value ){ def Attempt = "api/lights/" asynchttpPatch( "ReceiveData", GenerateProtectCommandParams( "${ Attempt }", "${ ChildID }", "{\"lightDeviceSettings\":{\"ledLevel\":${ Value }}}" ), [ Method: "LightBrightness", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // Get a specific child's status def GetBridgeStatus( String DNI, String ChildID ){ def Attempt = "api/bridges/${ ChildID }" asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "GetBridgeStatus", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Get a specific child's status def GetCameraStatus( String DNI, String ChildID ){ def Attempt = "api/cameras/${ ChildID }" asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "GetCameraStatus", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Get a specific child's status def GetDoorbellStatus( String DNI, String ChildID ){ def Attempt = "api/doorbells/${ ChildID }" asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "GetDoorbellStatus", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Get a specific child's status def GetLightStatus( String DNI, String ChildID ){ def Attempt = "api/lights/${ ChildID }" asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "GetLightStatus", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Get a specific child's status def GetSensorStatus( String DNI, String ChildID ){ def Attempt = "api/sensors/${ ChildID }" asynchttpGet( "ReceiveData", GenerateProtectParams( "${ Attempt }" ), [ Method: "GetSensorStatus", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Power down the controller def PowerDown( String Confirmation ){ if( Confirmation == "PowerDown" ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ Params = [ uri: "https://${ UnifiURL }:443/api/system/poweroff", ignoreSSLIssues: true, headers: [ Referer: "https://${ UnifiURL }/settings/advanced", Host: "${ UnifiURL }", Origin: "https://${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", "X-CSRF-Token": "${ state.CSRF }" ] ] } else { Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/api/system/poweroff", ignoreSSLIssues: true, headers: [ Referer: "https://${ UnifiURL }/settings/advanced", Host: "${ UnifiURL }", Origin: "https://${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", "X-CSRF-Token": "${ state.CSRF }" ] ] } Logging( "PowerDown Params = ${ Params }", 4 ) try{ httpPost( Params ){ resp -> switch( resp.getStatus() ){ case 200: case 204: ProcessEvent( "Status", "PowerDown command sent" ) Logging( "PowerDown command sent = ${ resp.data }", 4 ) break default: Logging( "PowerDown command error ${ resp.getStatus() }", 3 ) break } } } catch( Exception e ){ Logging( "PowerDown failed due to ${ e }", 5 ) } } else { Logging( "PowerDown confirmation incorrect. PowerDown command ignored.", 5 ) ProcessEvent( "Status", "PowerDown confirmation incorrect. PowerDown command ignored." ) } } // Generate Protect Params assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateProtectParams( String Path, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ], data:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ] } } else { if( Data != null ){ Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ], data:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ] } } Logging( "Parameters = ${ Params }", 4 ) return Params } // Generate Protect Image Params assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateProtectImageParams( String Path, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }", ignoreSSLIssues: true, headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ], data:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }", ignoreSSLIssues: true, headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ] } } else { if( Data != null ){ Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }", ignoreSSLIssues: true, headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ], data:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }", ignoreSSLIssues: true, headers: [ Host: "${ UnifiURL }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ] } } Logging( "Parameters = ${ Params }", 4 ) return Params } // Generate Protect Params assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateProtectCommandParams( String Path, String ChildID, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }/${ ChildID }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Referer: "https://${ UnifiURL }/protect/devices/${ ChildID }/general", Origin: "https://${ UnifiURL }", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }" ], body:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }/${ ChildID }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Referer: "https://${ UnifiURL }/protect/devices/${ ChildID }/general", Origin: "https://${ UnifiURL }", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }" ] ] } } else { if( Data != null ){ Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }/${ ChildID }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Host: "${ UnifiURL }:${ ControllerPort }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ], body:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }/${ ChildID }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Host: "${ UnifiURL }:${ ControllerPort }", Accept: "*/*", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }", 'accessKey': "${ state.'Auth Key' }" ] ] } } Logging( "Parameters = ${ Params }", 4 ) return Params } // Generate Protect Params assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateProtectManageParams( String Path, String ChildID, String Data ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }/${ ChildID }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Referer: "https://${ UnifiURL }/protect/devices/${ ChildID }/manage", Origin: "https://${ UnifiURL }", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }" ], body:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }/proxy/protect/${ Path }/${ ChildID }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Referer: "https://${ UnifiURL }/protect/devices/${ ChildID }/manage", Origin: "https://${ UnifiURL }", Cookie: "${ state.Cookie }", 'X-CSRF-Token': "${ state.CSRF }" ] ] } } Logging( "Parameters = ${ Params }", 4 ) return Params } // Process data coming in for a device def ProcessData( String Device, data ){ //Logging( "${ Device } Data: ${ data }", 4 ) data.each(){ if( it.key != null ){ switch( it.key ){ case "mac": PostStateToChild( "${ Device }", "MAC", it.value ) break case "type": PostStateToChild( "${ Device }", "Type", it.value ) break case "id": PostStateToChild( "${ Device }", "ID", it.value ) break case "version": PostStateToChild( "${ Device }", "Protect Version", it.value ) break case "name": if( it.value != null ){ if( getChildDevice( "${ Device }" ).label == null ){ getChildDevice( "${ Device }" ).label = it.value } PostStateToChild( "${ Device }", "DeviceName", it.value ) } break case "model": PostStateToChild( "${ Device }", "Model", it.value ) break case "isMotionDetected": case "isPirMotionDetected": if( it.value ){ PostEventToChild( "${ Device }", "motion", "active" ) } else { PostEventToChild( "${ Device }", "motion", "inactive" ) } break case "isLightOn": if( it.value ){ PostEventToChild( "${ Device }", "switch", "on" ) } else { PostEventToChild( "${ Device }", "switch", "off" ) } break case "isDark": if( it.value ){ PostEventToChild( "${ Device }", "Dark", "true" ) } else { PostEventToChild( "${ Device }", "Dark", "false" ) } break case "camera": if( it.value != null ){ PostStateToChild( "${ Device }", "Camera Paired", it.value ) } else { PostStateToChild( "${ Device }", "Camera Paired", "None" ) } break case "lightDeviceSettings": it.value.each(){ switch( it.key ){ case "ledLevel": PostEventToChild( "${ Device }", "Brightness", it.value ) switch( it.value ){ case 1: PostEventToChild( "${ Device }", "level", 10, "%" ) break case 2: PostEventToChild( "${ Device }", "level", 20, "%" ) break case 3: PostEventToChild( "${ Device }", "level", 40, "%" ) break case 4: PostEventToChild( "${ Device }", "level", 60, "%" ) break case 5: PostEventToChild( "${ Device }", "level", 80, "%" ) break case 6: PostEventToChild( "${ Device }", "level", 100, "%" ) break } break case "pirSensitivity": PostStateToChild( "${ Device }", "Motion Sensitivity", it.value ) break case "pirDuration": PostStateToChild( "${ Device }", "Motion Duration", ( it.value / 1000 ) ) break case "luxSensitivity": PostStateToChild( "${ Device }", "Lux Sensitivity", it.value ) break case "isIndicatorEnabled": PostStateToChild( "${ Device }", "Indicator Enabled", it.value ) break default: Logging( "Unhandled lightDeviceSettings for light ${ it.key } = ${ it.value }", 3 ) break } } break case "lightModeSettings": it.value.each(){ switch( it.key ){ case "mode": PostStateToChild( "${ Device }", "Light Trigger", it.value ) break case "enableAt": PostStateToChild( "${ Device }", "Trigger At", it.value ) break default: Logging( "Unhandled lightModeSetting for light ${ it.key } = ${ it.value }", 3 ) break } } break case "state": PostEventToChild( "${ Device }", "Device Status", it.value ) break case "isConnected": if( it.value ){ PostStateToChild( "${ Device }", "presence", "present" ) } else { PostStateToChild( "${ Device }", "presence", "not present" ) } break case "latestFirmwareVersion": PostStateToChild( "${ Device }", "Latest Firmware Version", it.value ) break case "firmwareVersion": PostStateToChild( "${ Device }", "Firmware Version", it.value ) break case "hardwareRevision": PostStateToChild( "${ Device }", "Hardware Revision", it.value ) break case "uptime": if( it.value != null ){ def TempUptime = it.value as int if( TempUptime >= 99999999 ){ TempUptime = ( ( TempUptime / 1000 ) as int ) } def TempUptimeDays = Math.round( TempUptime / 86400 ) def TempUptimeHours = Math.round( ( TempUptime % 86400 ) / 3600 ) def TempUptimeMinutes = Math.round( ( TempUptime % 3600 ) / 60 ) def TempUptimeString = "${ TempUptimeDays } Day" if( TempUptimeDays != 1 ){ TempUptimeString += "s" } TempUptimeString += " ${ TempUptimeHours } Hour" if( TempUptimeHours != 1 ){ TempUptimeString += "s" } TempUptimeString += " ${ TempUptimeMinutes } Minute" if( TempUptimeMinutes != 1 ){ TempUptimeString += "s" } PostStateToChild( "${ Device }", "Uptime", TempUptimeString ) } break case "platform": PostStateToChild( "${ Device }", "Platform", it.value ) break case "systemInfo": it.value.each(){ switch( it.key ){ case "cpu": it.value.each(){ switch( it.key ){ case "temperature": PostEventToChild( "${ Device }", "CPU Temp", ConvertTemperature( "C", it.value ), "°${ location.getTemperatureScale() }" ) break case "averageLoad": PostEventToChild( "${ Device }", "CPU Average Load", it.value, "%" ) break } } break case "memory": PostEventToChild( "${ Device }", "Memory Available", ( Math.round( ( it.value.available / it.value.total ) * 10000 ) / 100 ), "%" ) break case "storage": PostEventToChild( "${ Device }", "Storage Available", ( Math.round( ( it.value.available / it.value.size ) * 10000 ) / 100 ), "%" ) break } } break case "batteryStatus": PostEventToChild( "${ Device }", "battery", it.value.percentage, "%" ) break case "stats": it.value.each{ switch( it.key ){ case "light": PostEventToChild( "${ Device }", "illuminance", it.value.value ) PostEventToChild( "${ Device }", "LightAlert", it.value.status ) break case "temperature": PostEventToChild( "${ Device }", "temperature", ConvertTemperature( "C", it.value.value ), "°${ location.getTemperatureScale() }" ) PostEventToChild( "${ Device }", "TemperatureAlert", it.value.status ) break case "humidity": PostEventToChild( "${ Device }", "humidity", it.value.value ) PostEventToChild( "${ Device }", "HumidityAlert", it.value.status ) break } } break case "bluetoothConnectionState": it.value.each{ switch( it.key ){ case "signalQuality": PostEventToChild( "${ Device }", "lqi", it.value ) break case "signalStrength": PostEventToChild( "${ Device }", "rssi", it.value ) break } } break case "alarmTriggeredAt": PostStateToChild( "${ Device }", "Alarm_Triggered", ConvertEpochToDate( "${ it.value }" ) ) break case "openStatusChangedAt": PostStateToChild( "${ Device }", "Open_Status_Changed", ConvertEpochToDate( "${ it.value }" ) ) break case "tamperingDetectedAt": PostStateToChild( "${ Device }", "Tampering_Detected", ConvertEpochToDate( "${ it.value }" ) ) break case "leakDetectedAt": PostStateToChild( "${ Device }", "Leak_Detected", ConvertEpochToDate( "${ it.value }" ) ) break case "motionDetectedAt": PostStateToChild( "${ Device }", "Motion_Detected", ConvertEpochToDate( "${ it.value }" ) ) break case "lastDisconnect": PostStateToChild( "${ Device }", "Last_Disconnect", ConvertEpochToDate( "${ it.value }" ) ) break case "fwUpdateState": switch( it.value ){ case "upToDate": PostStateToChild( "${ Device }", "FirmwareUpdateState", "Up To Date" ) break default: PostStateToChild( "${ Device }", "FirmwareUpdateState", it.value ) break } break case "alarmSettings": it.value.each{ switch( it.key ){ case "isEnabled": if( it.value ){ PostEventToChild( "${ Device }", "Alarm_Enabled", "true" ) } else { PostEventToChild( "${ Device }", "Alarm_Enabled", "false" ) } break default: PostStateToChild( "${ Device }", it.key, it.value ) break } } break case "motionSettings": it.value.each{ switch( it.key ){ case "isEnabled": if( it.value ){ PostEventToChild( "${ Device }", "Motion_Enabled", "true" ) } else { PostEventToChild( "${ Device }", "Motion_Enabled", "false" ) } break case "sensitivity": PostEventToChild( "${ Device }", "Motion_Sensitivity", it.value ) break default: PostStateToChild( "${ Device }", it.key, it.value ) break } } break case "temperatureSettings": it.value.each{ switch( it.key ){ case "isEnabled": if( it.value ){ PostEventToChild( "${ Device }", "Temperature_Enabled", "true" ) } else { PostEventToChild( "${ Device }", "Temperature_Enabled", "false" ) } break case "margin": PostEventToChild( "${ Device }", "Temperature_Margin", it.value ) break case "lowThreshold": PostEventToChild( "${ Device }", "Temperature_Low_Threshold", it.value ) break case "highThreshold": PostEventToChild( "${ Device }", "Temperature_High_Threshold", it.value ) break default: PostStateToChild( "${ Device }", it.key, it.value ) break } } break case "humiditySettings": it.value.each{ switch( it.key ){ case "isEnabled": if( it.value ){ PostEventToChild( "${ Device }", "Humidity_Enabled", "true" ) } else { PostEventToChild( "${ Device }", "Humidity_Enabled", "false" ) } break case "margin": PostEventToChild( "${ Device }", "Humidity_Margin", it.value ) break case "lowThreshold": PostEventToChild( "${ Device }", "Humidity_Low_Threshold", it.value ) break case "highThreshold": PostEventToChild( "${ Device }", "Humidity_High_Threshold", it.value ) break default: PostStateToChild( "${ Device }", it.key, it.value ) break } } break case "lightSettings": it.value.each{ switch( it.key ){ case "isEnabled": if( it.value ){ PostEventToChild( "${ Device }", "Light_Enabled", "true" ) } else { PostEventToChild( "${ Device }", "Light_Enabled", "false" ) } break case "margin": PostEventToChild( "${ Device }", "Light_Margin", it.value ) break case "lowThreshold": PostEventToChild( "${ Device }", "Light_Low_Threshold", it.value ) break case "highThreshold": PostEventToChild( "${ Device }", "Light_High_Threshold", it.value ) break default: PostStateToChild( "${ Device }", it.key, it.value ) break } } break case "homekitSettings": PostStateToChild( "${ Device }", "Homekit_Settings", it.value ) break case "isOpened": if( it.value ){ PostEventToChild( "${ Device }", "contact", "open" ) } else { PostEventToChild( "${ Device }", "contact", "closed" ) } break case "bridge": PostStateToChild( "${ Device }", "BridgeID", it.value ) break case "nvrMac": PostStateToChild( "${ Device }", "NVR_MAC", it.value ) break case "publicIp": case "wanIp": PostStateToChild( "${ Device }", "Public_IP", it.value ) break case "countryCode": PostStateToChild( "${ Device }", "Country_Code", it.value ) break case "corruptionState": switch( it.value ){ case "healthy": PostStateToChild( "${ Device }", "Health_Status", "Healthy" ) break default: PostStateToChild( "${ Device }", "Health_Status", it.value ) break } break case "isWaterproofCaseAttached": if( it.value ){ PostStateToChild( "${ Device }", "Waterproof_Case", "true" ) } else { PostStateToChild( "${ Device }", "Waterproof_Case", "false" ) } break case "is4K": if( it.value ){ PostStateToChild( "${ Device }", "4K", "true" ) } else { PostStateToChild( "${ Device }", "4K", "false" ) } break case "is2K": if( it.value ){ PostStateToChild( "${ Device }", "2K", "true" ) } else { PostStateToChild( "${ Device }", "2K", "false" ) } break case "mountType": switch( it.value ){ case "door": PostEventToChild( "${ Device }", "MountType", "Door" ) break case "window": PostEventToChild( "${ Device }", "MountType", "Window" ) break case "garage": PostEventToChild( "${ Device }", "MountType", "Garage" ) break case "leak": PostEventToChild( "${ Device }", "MountType", "Leak" ) break case "none": case "null": default: PostEventToChild( "${ Device }", "MountType", "None" ) break } break case "recordingSchedulesV2": PostStateToChild( "${ Device }", "RecordingSchedules", it.value ) break case "hasRecordings": PostStateToChild( "${ Device }", "HasRecordings", it.value ) break case "uplinkDevice": PostStateToChild( "${ Device }", "UplinkDevice", it.value ) break case "deviceFirmwareSettings": PostStateToChild( "${ Device }", "FirmwareSettings", it.value ) break case "cameraCapacity": PostStateToChild( "${ Device }", "CameraCapacity", it.value ) break case "isNetworkInstalled": PostStateToChild( "${ Device }", "IsNetworkInstalled", it.value ) break case "isPtz": PostStateToChild( "${ Device }", "PanTiltZoom", it.value ) break // Not sure the specific value but providing them as a state case "ulpVersion": case "dbRecoveryOptions": PostStateToChild( "${ Device }", it.key, it.value ) break // Not doing anything with this data at this moment. Not sure of value. case "portStatus": case "globalCameraSettings": break // Ignored data - limited use at this time or redundant case "isDbAvailable": case "streamSharingAvailable": case "streamSharing": case "lightOnSettings": case "upSince": case "lastSeen": case "isCameraPaired": case "canAdopt": case "isSshEnabled": case "wiredConnectionState": case "connectedSince": case "connectionHost": case "host": case "isUpdating": case "isLocating": case "isAdopted": case "isAttemptingToConnect": case "isAdoptedByOther": case "modelKey": case "lastMotion": case "firmwareBuild": case "isAdopting": case "isProvisioned": case "isRebooting": case "phyRate": case "recordingSettings": case "smartDetectSettings": case "isRecording": case "isSmartDetected": case "isProbingForWifi": case "featureFlags": case "lenses": case "eventStats": case "privacyZones": case "apMac": case "hdrMode": case "isManaged": case "channels": case "videoReconfigurationInProgress": case "isLiveHeatmapEnabled": case "talkbackSettings": case "ledSettings": case "speakerSettings": case "isPoorNetwork": case "hasWifi": case "videoMode": case "recordingSchedules": case "ispSettings": case "smartDetectZones": case "smartDetectLines": case "micVolume": case "pirSettings": case "isMicEnabled": case "isWirelessUplinkEnabled": case "isDeleting": case "lastRing": case "anonymousDeviceId": case "hasSpeaker": case "wifiConnectionState": case "elementInfo": case "lastPrivacyZonePositionId": case "audioBitrate": case "voltage": case "canManage": case "apRssi": case "lcdMessage": case "marketName": case "motionZones": case "chimeDuration": case "osdSettings": case "analyticsData": case "isRecordingDisabled": case "skipFirmwareUpdate": case "errorCode": case "enableCrashReporting": case "isRecordingMotionOnly": case "hostType": case "isStatsGatheringEnabled": case "recordingRetentionDurationMs": case "isAway": case "isHardware": case "enableAutomaticBackups": case "isSetup": case "enableStatsReporting": case "hardwarePlatform": case "canAutoUpdate": case "hosts": case "disableAutoLink": case "isRecycling": case "storageStats": case "releaseChannel": case "hardwareId": case "cameraUtilization": case "timezone": case "wifiSettings": case "smartDetectAgreement": case "ports": case "network": case "ucoreVersion": case "temperatureUnit": case "ssoChannel": case "disableAudio": case "lastUpdateAt": case "hostShortname": case "doorbellSettings": case "uiVersion": case "enableBridgeAutoAdoption": case "maxCameraCapacity": case "isStation": case "timeFormat": case "locationSettings": case "avgMotions": case "lastDriveSlowEvent": case "isStacked": case "isPrimary": case "isUCoreSetup": case "isDownloadingFW": case "isInsightsEnabled": case "guid": case "bridgeCandidates": case "vaultCameras": case "hasGateway": case "isVaultRegistered": case "stopStreamLevel": case "userConfiguredAp": case "useGlobal": case "apMgmtIp": case "isRestoring": case "isProtectUpdatable": case "hardDriveState": case "isUcoreUpdatable": case "lastDeviceFWUpdatesCheckedAt": case "audioSettings": break default: Logging( "Unhandled data for ${ Device } ${ it.key } = ${ it.value }", 3 ) break } } } } // Handles data sent from a child to the parent for processing def ReceiveFromChild( String Type, String Child, Map Data ){ Logging( "Received ${ Type } from ${ Child } = ${ Data }", 4 ) switch( Type ){ case "State": ProcessState( "${ Data.Name }", Data.Value ) break case "Event": ProcessEvent( "${ Data.Name }", Data.Value ) break case "Logging": Logging( "Log from ${ Child }: ${ Data.Value }", Data.Level ) break case "Map": Data.each(){ switch( it.value.Type ){ case "State": ProcessState( "${ it.value.Name }", it.value.Value ) break case "Event": ProcessEvent( "${ it.value.Name }", it.value.Value ) break case "Logging": Logging( "Log from ${ Child }: ${ it.value.Value }", it.value.Level ) break default: Logging( "Test of ReceiveFromChild = Map = HUH?! ${ Child }, Map Data = ${ it }", 4 ) break } } break default: Logging( "Test of ReceiveFromChild = HUH?! ${ Child }, Data = ${ Data }", 3 ) break } } // installed is called when the device is installed def installed(){ Logging( "Installed", 2 ) } // uninstalling device so make sure to clean up children void uninstalled() { // Delete all children getChildDevices().each{ deleteChildDevice( it.deviceNetworkId ) } unschedule() Logging( "Uninstalled", 2 ) } // Used to convert epoch values to text dates def String ConvertEpochToDate( String Epoch ){ def date if( ( Epoch != null ) && ( Epoch != "" ) && ( Epoch != "null" ) ){ Long Temp = Epoch.toLong() if( Temp <= 9999999999 ){ date = new Date( ( Temp * 1000 ) ).toString() } else { date = new Date( Temp ).toString() } } else { date = "Null value provided" } return date } // Checks the location.getTemperatureScale() to convert temperature values def ConvertTemperature( String Scale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( location.getTemperatureScale() == "C" && Scale.toUpperCase() == "F" ){ ReturnValue = ( ( ( Value - 32 ) * 5 ) / 9 ) Logging( "Temperature Conversion ${ Value }°F to ${ ReturnValue }°C", 4 ) } else if( location.getTemperatureScale() == "F" && Scale.toUpperCase() == "C" ) { ReturnValue = ( ( ( Value * 9 ) / 5 ) + 32 ) Logging( "Temperature Conversion ${ Value }°C to ${ ReturnValue }°F", 4 ) } else if( ( location.getTemperatureScale() == "C" && Scale.toUpperCase() == "C" ) || ( location.getTemperatureScale() == "F" && Scale.toUpperCase() == "F" ) ){ ReturnValue = Value } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Process data to check against current state value and then send an event if it has changed def ProcessEvent( Variable, Value, Unit = null, ForceEvent = false ){ if( ( state."${ Variable }" != Value ) || ( ForceEvent == true ) ){ state."${ Variable }" = Value if( Unit != null ){ Logging( "Event: ${ Variable } = ${ Value }${ Unit }", 4 ) sendEvent( name: "${ Variable }", value: Value, unit: Unit, isStateChange: true ) } else { Logging( "Event: ${ Variable } = ${ Value }", 4 ) sendEvent( name: "${ Variable }", value: Value, isStateChange: true ) } //UpdateTile( "${ Value }" ) } } // Process data to check against current state value def ProcessState( Variable, Value ){ if( state."${ Variable }" != Value ){ Logging( "State: ${ Variable } = ${ Value }", 4 ) state."${ Variable }" = Value //UpdateTile( "${ Value }" ) } } // Post data to child device def PostEventToChild( Child, Variable, Value, Unit = null, ForceEvent = null ){ if( "${ Child }" != null ){ if( getChildDevice( "${ Child }" ) == null ){ TempChild = Child.split( " " ) def ChildType = "" switch( TempChild[ 0 ] ){ case "Light": ChildType = "Light" break case "Doorbell": ChildType = "Doorbell" break case "Camera": ChildType = "Camera" break case "Bridge": ChildType = "Bridge" break case "Sensor": ChildType = "Sensor" break default: ChildType = "Generic" break } addChild( "${ Child }", ChildType ) } if( getChildDevice( "${ Child }" ) != null ){ if( Unit != null ){ if( ForceEvent != null ){ getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }", ForceEvent ) Logging( "Child Event: ${ Variable } = ${ Value }${ Unit }", 4 ) } else { getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }" ) Logging( "Child Event: ${ Variable } = ${ Value }", 4 ) } } else { if( ForceEvent != null ){ getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, null, ForceEvent ) Logging( "Child Event: ${ Variable } = ${ Value }${ Unit }", 4 ) } else { getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value ) Logging( "Child Event: ${ Variable } = ${ Value }", 4 ) } } } else { if( Unit != null ){ Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }${ Unit }", 5 ) } else { Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }", 5 ) } } } else { Logging( "Failure to add child because child name was null", 5 ) } } // Post data to child device def PostStateToChild( Child, Variable, Value ){ if( "${ Child }" != null ){ if( getChildDevice( "${ Child }" ) == null ){ TempChild = Child.split( " " ) def ChildType = "" switch( TempChild[ 0 ] ){ case "Light": ChildType = "Light" break case "Doorbell": ChildType = "Doorbell" break case "Camera": ChildType = "Camera" break case "Bridge": ChildType = "Bridge" break case "Sensor": ChildType = "Sensor" break default: ChildType = "Generic" break } addChild( "${ Child }", ChildType ) } if( getChildDevice( "${ Child }" ) != null ){ Logging( "${ Child } State: ${ Variable } = ${ Value }", 4 ) getChildDevice( "${ Child }" ).ProcessState( "${ Variable }", Value ) } else { Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }", 5 ) } } else { Logging( "Failure to add child because child name was null", 5 ) } } // Adds a UnifiProtectChild child device // Based on @mircolino's method for child sensors def addChild( String DNI, String ChildType ){ try{ Logging( "addChild(${ DNI })", 4 ) switch( ChildType ){ case "Light": addChildDevice( "UnifiProtectChild-Light", DNI, [ name: "${ DNI }" ] ) break case "Camera": addChildDevice( "UnifiProtectChild-Camera", DNI, [ name: "${ DNI }" ] ) break case "Doorbell": addChildDevice( "UnifiProtectChild-Doorbell", DNI, [ name: "${ DNI }" ] ) break case "Bridge": addChildDevice( "UnifiProtectChild-Bridge", DNI, [ name: "${ DNI }" ] ) break case "Sensor": addChildDevice( "UnifiProtectChild-Sensor", DNI, [ name: "${ DNI }" ] ) break default: addChildDevice( "UnifiProtectChild", DNI, [ name: "${ DNI }" ] ) break } } catch( Exception e ){ def Temp = e as String if( Temp.contains( "not found" ) ){ if( ChildType != null ){ Logging( "UnifiProtectChild-${ ChildType } driver is not loaded, this is required for the child device.", 5 ) } else { Logging( "UnifiProtectChild driver is not loaded, this is required for the child device.", 5 ) } } else { Logging( "addChild Error, likely child already exists: ${ Temp }", 5 ) } } } // Handles whether logging is enabled and thus what to put there. def Logging( LogMessage, LogLevel ){ // Add all messages as info logging if( ( LogLevel == 2 ) && ( LogType != "None" ) ){ log.info( "${ device.displayName } - ${ LogMessage }" ) } else if( ( LogLevel == 3 ) && ( ( LogType == "Debug" ) || ( LogType == "Trace" ) ) ){ log.debug( "${ device.displayName } - ${ LogMessage }" ) } else if( ( LogLevel == 4 ) && ( LogType == "Trace" ) ){ log.trace( "${ device.displayName } - ${ LogMessage }" ) } else if( LogLevel == 5 ){ log.error( "${ device.displayName } - ${ LogMessage }" ) } } // Checks drdsnell.com for the latest version of the driver // Original inspiration from @cobra's version checking def CheckForUpdate(){ ProcessEvent( "DriverName", DriverName() ) ProcessEvent( "DriverVersion", DriverVersion() ) httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp -> switch( resp.status ){ case 200: if( resp.data."${ DriverName() }" ){ CurrentVersion = DriverVersion().split( /\./ ) if( resp.data."${ DriverName() }".version == "REPLACED" ){ ProcessEvent( "DriverStatus", "Driver replaced, please use ${ resp.data."${ state.DriverName }".file }" ) } else if( resp.data."${ DriverName() }".version == "REMOVED" ){ ProcessEvent( "DriverStatus", "Driver removed and no longer supported." ) } else { SiteVersion = resp.data."${ DriverName() }".version.split( /\./ ) if( CurrentVersion == SiteVersion ){ Logging( "Driver version up to date", 2 ) ProcessEvent( "DriverStatus", "Up to date" ) } else if( ( CurrentVersion[ 0 ] as int ) > ( SiteVersion [ 0 ] as int ) ){ Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 4 ) ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){ Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 4 ) ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){ Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 4 ) ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( SiteVersion[ 0 ] as int ) > ( CurrentVersion[ 0 ] as int ) ){ Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( ( SiteVersion[ 1 ] as int ) > ( CurrentVersion[ 1 ] as int ) ){ Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( ( SiteVersion[ 2 ] as int ) > ( CurrentVersion[ 2 ] as int ) ){ Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } } } else { Logging( "${ DriverName() } is not published on drdsnell.com", 2 ) ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com" ) } break default: Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 ) break } } }