/*
* 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 MonitoringRequired 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
}
}
}