/* * Unifi Connect API * * Description: * This Hubitat driver allows polling of the Unifi Connect API, initially geared around a Unifi Dream Machine Pro. * * Overall Setup: * 1) Add both the UnifiConnectAPI.groovy and the related child drivers as new user drivers * NOTE: Connect 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: * Recognize and create child devices for the following Connect products * Display Cast * Display 7 * Display 13 * Display 21 * Display 27 * Display SE 7 * Display SE 13 * Display SE 21 * Display SE 27 * ULED Panel AT * ULED Panel AC * Dimmer Switch AC * UC EV Station * 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.1.8 - Change to support new authorization Token name in newer Unifi OS versions and replacement of driver-specific attribute names * 0.1.7 - Handling for additional data returned by the API * 0.1.6 - Change to obtaining CSRF to account for change made in Unifi OS 3.2.8 and updated websocket portion * 0.1.5 - Cleanup of copy-paste error in login section * 0.1.4 - Change to error logging for login * 0.1.3 - Added support for the following devices: Display 7, Display 21, Display 27, Display SE 7, Display SE 21, Display SE 27, * ULED Panel AT, ULED Panel AC, Dimmer Switch AC, UC EV Station, plus overhauled the way actions are handled * 0.1.2 - Support for Display SE 13 added * 0.1.1 - Additional logging for device identification section * 0.1.0 - Initial version * * Thank you(s): * @tomw for the WebSocket data parsing code * @Cobra for inspiration on driver version checking. */ // Returns the driver name def DriverName(){ return "UnifiConnectAPI" } // Returns the driver version def DriverVersion(){ return "0.1.8" } // Driver Metadata metadata{ definition( name: "UnifiConnectAPI", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/UnifiConnectAPI.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 Connect related commands command "GetConnectInfo" // Checks for general data on the Connect controller, same as refresh command "GetConnectDeviceTypes" command "GetConnectDevices" // 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(){ state.CSRF = null } // Create a WebSocket connection to monitor for events def ConnectWebSocket(){ try{ if( Controller == "Unifi Dream Machine (inc Pro)" ){ interfaces.webSocket.connect( "wss://${ UnifiURL }/proxy/connect/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 }", 4 ) switch( it.value ){ 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 } 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 "sensorMotion": PostEventToChild( "${ Device }", "motion", "inactive" ) 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 Status" != null ){ state.remove( "Driver Name" ) state.remove( "Driver Version" ) state.remove( "Driver Status" ) device.deleteCurrentState( "Driver Status" ) device.deleteCurrentState( "Driver Name" ) device.deleteCurrentState( "Driver Version" ) } ProcessEvent( "DriverName", "${ DriverName() }" ) ProcessEvent( "DriverVersion", "${ DriverVersion() }" ) ProcessEvent( "DriverStatus", null ) 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 ) 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" ) Login() pauseExecution( 1000 ) GetConnectDevices() // 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 ) // Schedule checks that are only performed once a day schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "DailyCheck" ) if( EnableWebSocket ){ ProcessState( "WebSocket Failures", 0 ) } Logging( "Updated", 2 ) } // DailyCheck is only performed once a day def DailyCheck(){ CheckForUpdate() //GetConnectDeviceTypes() GetConnectDevices() } // refresh performs a poll of data def refresh(){ if( EnableWebSocket ){ CloseWebSocket() } GetConnectInfo() if( EnableWebSocket && ( state.'WebSocket Failures' < 5 ) ){ ConnectWebSocket() } ProcessState( "Last Refresh", new Date() ) } //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 -> switch( resp.getStatus() ){ case 200: if( state.LoginRetries > 0 ){ ProcessState( "LoginRetries", 0 ) } if( Manual && EnableWebSocket ){ ProcessState( "WebSocket Failures", 0 ) } //Logging( "Login response = ${ resp.data }", 4 ) 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" ) || ( it.value.split( '=' )[ 0 ].toString() == "UOS_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", GenerateConnectParams( "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( "Error logging in to controller: ${ e }", 5 ) ProcessState( "Status", "Login failure" ) ProcessState( "LoginRetries", ( state.LoginRetries + 1 ) ) } } } 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 Connect info def GetConnectInfo(){ def Attempt = "api/v2/configs/info" asynchttpGet( "ReceiveData", GenerateConnectParams( "${ Attempt }" ), [ Method: "Connect Info" ] ) } // Command to attempt to perform a device action def PerformAction( Device, DeviceID, CommandID, State, Args = null ){ def Attempt = "api/v2/devices" if( Args == null ){ asynchttpPatch( "ReceiveData", GenerateConnectStatusParams( "${ Attempt }", DeviceID, "{\"id\": \"${ CommandID }\", \"name\": \"${ State }\"}" ), [ Method: "${ State }", Device: "${ Device }" ] ) } else { asynchttpPatch( "ReceiveData", GenerateConnectStatusParams( "${ Attempt }", DeviceID, "{\"id\": \"${ CommandID }\", \"name\": \"${ State }\", \"args\": {${ Args }}}" ), [ Method: "${ State }", Device: "${ Device }", Value: "${ Args }" ] ) } } // Command to attempt to get Connect Device Types def GetConnectDeviceTypes(){ def Attempt = "api/v2/devices/types" asynchttpGet( "ReceiveData", GenerateConnectParams( "${ Attempt }" ), [ Method: "Device Types" ] ) } // Command to attempt to get Connect Devices def GetConnectDevices(){ def Attempt = "api/v2/devices?shadow=true" asynchttpGet( "ReceiveData", GenerateConnectParams( "${ Attempt }" ), [ Method: "Devices" ] ) } // Get a specific child's status def GetChildStatus( String DNI, String ChildID ){ def Attempt = "api/lights/${ ChildID }" asynchttpGet( "ReceiveData", GenerateConnectParams( "${ Attempt }" ), [ Method: "GetChildStatus", DNI: "${ DNI }", ChildID: "${ ChildID }" ] ) } // Set a child device's name def SetChildName( String DNI, String ChildID, String Value ){ def Attempt = "api/lights/" asynchttpPatch( "ReceiveData", GenerateConnectManageParams( "${ Attempt }", "${ ChildID }", "${ Value }" ), [ Method: "SetChildName", DNI: "${ DNI }", ChildID: "${ ChildID }", Value: "${ Value }" ] ) } // GenerateConnectStatusParams assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateConnectStatusParams( String Path, String ChildID, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/connect/${ Path }/${ ChildID }/status", ignoreSSLIssues: true, contentType: "application/json", headers: [ referer: "https://${ UnifiURL }/connect/devices/all/${ ChildID }", origin: "https://${ UnifiURL }", 'cookie': "${ state.Cookie }", 'x-csrf-token': "${ state.CSRF }" ], body:"${ Data }" ] } else { Params = [ uri: "https://${ UnifiURL }/proxy/connect/${ Path }/${ ChildID }/status", ignoreSSLIssues: true, contentType: "application/json", headers: [ referer: "https://${ UnifiURL }/connect/devices/all/${ ChildID }", origin: "https://${ UnifiURL }", 'cookie': "${ state.Cookie }", 'x-csrf-token': "${ state.CSRF }" ] ] } } else { if( Data != null ){ Params = [ uri: "https://${ UnifiURL }:${ ControllerPort }/${ Path }/${ ChildID }/status", ignoreSSLIssues: true, 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 }/status", ignoreSSLIssues: true, 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 } // GenerateConnectParams assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateConnectParams( String Path, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/connect/${ 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/connect/${ 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 } // GenerateConnectImageParams assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateConnectImageParams( String Path, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/connect/${ 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/connect/${ 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 } // GenerateConnectCommandParams assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateConnectCommandParams( String Path, String ChildID, String Data = null ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/connect/${ 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/connect/${ 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 } // GenerateConnectManageParams assembles the parameters to be sent to the controller rather than repeat so much of it def GenerateConnectManageParams( String Path, String ChildID, String Data ){ def Params if( Controller == "Unifi Dream Machine (inc Pro)" ){ if( Data != null ){ Params = [ uri: "https://${ UnifiURL }/proxy/connect/${ 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/connect/${ 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 } // 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 "Connect Info": Logging( "${ data.Method } Data: ${ resp.data }", 4 ) break case "Devices": Logging( "${ data.Method } Data: ${ resp.data }", 4 ) Json.data.each(){ def DeviceType switch( it.type.platform ){ case "UC-Cast": // Display Cast DeviceType = "UC-Cast" break case "UC-Display-7": // Display 7 DeviceType = "UC-Display-7" break case "UC-Display-13": // Display 13 DeviceType = "UC-Display-13" break case "UC-Display-21": // Display 21 DeviceType = "UC-Display-21" break case "UC-Display-27": // Display 27 DeviceType = "UC-Display-27" break case "UC-Display-SE-7": // Display SE 7 DeviceType = "UC-Display-SE-7" break case "UC-Display-SE-13": // Display SE 13 DeviceType = "UC-Display-SE-13" break case "UC-Display-SE-21": // Display SE 21 DeviceType = "UC-Display-SE-21" break case "UC-Display-SE-27": // Display SE 27 DeviceType = "UC-Display-SE-27" break case "ULP3PE": // ULED Panel AT DeviceType = "ULP3PE" break case "ULP3AC": // ULED Panel AC DeviceType = "ULP3AC" break case "ULD3AC": // Dimmer Switch AC DeviceType = "ULD3AC" break case "UC-EV-Station": // UC EV Station DeviceType = "UC-EV-Station" break default: Logging( "Unknown Type ${ it.type.platform }, using Generic", 3 ) DeviceType = "Generic" break } PostEventToChild( "${ DeviceType } ${ it.mac }", "Type", "${ DeviceType }" ) ProcessData( "${ DeviceType } ${ it.mac }", it ) } break case "Device Types": Logging( "${ data.Method } Data: ${ resp.data }", 4 ) break case "play": case "stop": case "switch": case "start_locating": case "stop_locating": case "reboot": case "enable_auto_rotate": case "fw_update": case "enable_sleep": case "disable_sleep": case "enable_memorize_playlist": case "disable_memorize_playlist": case "disable_auto_rotate": case "load_website": case "refresh_website": case "enable_auto_reload": case "disable_auto_reload": case "upgrade_mode": case "launch_app": case "stop_app": if( ( Json.err == null ) && ( Json.data == "OK" ) ){ Logging( "${ data.Method } Successful: ${ resp.data }", 2 ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "off": case "display_off": if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "switch", "off" ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "on": case "display_on": if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "switch", "on" ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "ble_off": if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "BLE", "off" ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "ble_on": if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "BLE", "on" ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "volume": def Temp = data.Value.split( ":" ) if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "Volume", ( Temp[ 1 ] as int ) ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "dimming": def Temp = data.Value.split( ":" ) if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "level", ( Temp[ 1 ] as int ) ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "rotate": if( ( Json.err == null ) && ( Json.data == "OK" ) ){ switch( data.Value ){ case "\"scale\":\"portraitPrim\"": PostEventToChild( data.Device, "Rotation", "Portrait" ) break case "\"scale\":\"landscapePrim\"": PostEventToChild( data.Device, "Rotation", "Landscape" ) break case "\"scale\":\"portraitSec\"": PostEventToChild( data.Device, "Rotation", "Portrait (flipped)" ) break case "\"scale\":\"landscapeSec\"": PostEventToChild( data.Device, "Rotation", "Landscape (flipped)" ) break default: Logging( "Unknown rotation setting ${ data.Value }", 4 ) break } } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "brightness": def Temp = data.Value.split( ":" ) if( ( Json.err == null ) && ( Json.data == "OK" ) ){ PostEventToChild( data.Device, "Brightness", ( Temp[ 1 ] as int ) ) } else { Logging( "${ data.Method } Not Successful?: ${ resp.data }", 2 ) } break case "GetChildStatus": Logging( "GetChildStatus 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 ) } 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 } } // 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 "id": PostStateToChild( "${ Device }", "ID", 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 "state": PostEventToChild( "${ Device }", "Device Status", 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 "ip": PostStateToChild( "${ Device }", "IP", it.value ) break case "firmwareVersion": PostStateToChild( "${ Device }", "Firmware_Version", it.value ) break // Things being ignored for now case "type": case "shadow": case "extraInfo": case "featureFlags": case "availableHVAC": case "displayLabel": case "parent": case "groups": case "adoptedAt": case "hostname": case "wiFiUplink": case "lastBootTimestamp": case "online": case "enableDisplayLabel": case "relayShadow": 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 ){ Long Temp = Epoch.toLong() def date if( Temp <= 9999999999 ){ date = new Date( ( Temp * 1000 ) ).toString() } else { date = new Date( Temp ).toString() } 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 }" ) } } // Consolidate the child type checking def GetChildType( Child ){ def ChildType = "" switch( Child ){ case "UC-Cast": // Display Cast ChildType = "UC-Cast" break /* case "UC-Display-7": // Display 7 ChildType = "UC-Display-7" break case "UC-Display-13": // Display 13 ChildType = "UC-Display-13" break case "UC-Display-21": // Display 21 ChildType = "UC-Display-21" break case "UC-Display-27": // Display 27 ChildType = "UC-Display-27" break case "UC-Display-SE-7": // Display SE 7 ChildType = "UC-Display-SE-7" break case "UC-Display-SE-13": // Display SE 13 ChildType = "UC-Display-SE-13" break case "UC-Display-SE-21": // Display SE 21 ChildType = "UC-Display-SE-21" break case "UC-Display-SE-27": // Display SE 27 ChildType = "UC-Display-SE-27" break */ case "UC-Display-7": // Display 7 case "UC-Display-13": // Display 13 case "UC-Display-21": // Display 21 case "UC-Display-27": // Display 27 case "UC-Display-SE-7": // Display SE 7 case "UC-Display-SE-13": // Display SE 13 case "UC-Display-SE-21": // Display SE 21 case "UC-Display-SE-27": // Display SE 27 ChildType = "UC-Display" break case "ULP3PE": // ULED Panel AT ChildType = "ULP3PE" break case "ULP3AC": // ULED Panel AC ChildType = "ULP3AC" break case "ULD3AC": // Dimmer Switch AC ChildType = "ULD3AC" break case "UC-EV-Station": // UC EV Station ChildType = "UC-EV-Station" break default: ChildType = "Generic" break } return ChildType } // Post data to child device def PostEventToChild( Child, Variable, Value, Unit = null, ForceEvent = null ){ if( "${ Child }" != null ){ if( getChildDevice( "${ Child }" ) == null ){ TempChild = Child.split( " " ) addChild( "${ Child }", GetChildType( TempChild[ 0 ] ) ) } 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( " " ) addChild( "${ Child }", GetChildType( TempChild[ 0 ] ) ) } 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 UnifiConnectChild child device // Based on @mircolino's method for child sensors def addChild( String DNI, String ChildType ){ try{ Logging( "addChild(${ DNI })", 3 ) if( ChildType == "Generic" ){ addChildDevice( "UnifiConnectChild", DNI, [ name: "${ DNI }" ] ) } else { addChildDevice( "UnifiConnectChild-${ ChildType }", DNI, [ name: "${ DNI }" ] ) } } catch( Exception e ){ def Temp = e as String if( Temp.contains( "not found" ) ){ if( ( ChildType != null ) && ( ChildType != "Generic" ) ){ Logging( "UnifiConnectChild-${ ChildType } driver is not loaded, this is required for the child device. /n Attempting to use generic child driver instead.", 5 ) addChild( DNI, "Generic" ) } else { Logging( "UnifiConnectChild 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(), null, true ) ProcessEvent( "DriverVersion", DriverVersion(), null, true ) ProcessEvent( "DriverStatus", null, null, true ) 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 }", null, true ) } else if( resp.data."${ DriverName() }".version == "REMOVED" ){ ProcessEvent( "DriverStatus", "Driver removed and no longer supported.", null, true ) } else { SiteVersion = resp.data."${ DriverName() }".version.split( /\./ ) if( CurrentVersion == SiteVersion ){ Logging( "Driver version up to date", 3 ) 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", 3 ) ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", null, true ) } else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){ Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", null, true ) } else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){ Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", null, true ) } 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", null, true ) } 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", null, true ) } 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", null, true ) } } } else { Logging( "${ DriverName() } is not published on drdsnell.com", 2 ) ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com", null, true ) } break default: Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 ) break } } }