/*
* UnifiProtectChild-Sensor
*
* Description:
* This Hubitat driver provides a spot to put data from Unifi Protect Sensor-related devices. It does not belong on it's own and requires
* the UnifiProtectAPI driver as a parent device.
*
* Instructions for using Tile Template method (originally based on @mircolino's HTML Templates):
* 1) In "Hubitat -> Devices" select the child/sensor (not the parent) you would like to "templetize"
* 2) In "Preferences -> Tile Template" enter your template (example below) and click "Save Preferences"
* Ex: "[font size='2'][b]Temperature:[/b] ${ temperature }°${ location.getTemperatureScale() }[/br][/font]"
* 3) In a Hubitat dashboard, add a new tile, and select the child/sensor, in the center select "Attribute", and on the right select the "Tile" attribute
* 4) Select the Add Tile button and the tile should appear
* NOTE: Should accept most HTML formatting commands with [] instead of <>
*
* Features List:
* Ability to control general device settings
* Ability to trigger device's locate function
* Ability to check a website (mine) to notify user if there is a newer version of the driver available
*
* Known Issue:
* Due to rounding (when converting Temperature Low/High Thresholds from F to C) the temperature set on the Unifi Protect may not be exactly as desired
*
* 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.
*
* Version Control:
* 0.1.5 - Correction of isStateChange and driver-specific attributes
* 0.1.4 - Add preference for Mounting Type/Placement of sensor
* 0.1.3 - Major rework of preferences to handle new settings methods as well as changes for locate and refesh commands
* 0.1.2 - Removed Auto Shutoff preference
* 0.1.1 - Addition of signal strength capability
* 0.1.0 - Initial version
*
* Thank you(s):
* @Cobra for inspiration of how I perform driver version checking
* @mircolino for HTML Template method for dashboard use
*/
// Returns the driver name
def DriverName(){
return "UnifiProtectChild-Sensor"
}
// Returns the driver version
def DriverVersion(){
return "0.1.5"
}
// Driver Metadata
metadata{
definition( name: "UnifiProtectChild-Sensor", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/UnifiProtectChild-Sensor.groovy" ) {
capability "Sensor"
capability "Actuator"
//capability "Switch"
capability "MotionSensor"
//capability "Switch Level"
capability "Refresh"
capability "Battery"
capability "TemperatureMeasurement"
capability "RelativeHumidityMeasurement"
capability "IlluminanceMeasurement"
capability "WaterSensor"
capability "ContactSensor"
capability "SignalStrength"
// Commands
//command "SetBrightness", [ [ name: "Brightness*", type: "ENUM", defaultValue: "3", constraints: [ "0", "1", "2", "3", "4", "5", "6" ], description: "REQUIRED: Brightness level of the floodlight from 0 (off) to 6 (full brightness)" ] ]
command "Locate" // Meant to help identify/locate the particular device by flashing the light
//command "DoSomething" // For testing and development purposes only, it should not be uncommented for normal use
// 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
// General Device Attributes
attribute "Type", "string" // The type of device, per Ubiquiti data
// Attributes - Device Related
attribute "Status", "string" // Show success/failure of commands performed
attribute "Device Status", "string" // Show the current state of the device as reported by the controller
attribute "Device Type", "string" // What type of device the child is respresenting
// Sensor-Specific Attributes
attribute "MountType", "enum", [ "Door", "Window", "Garage", "Leak", "None" ] //
attribute "Alarm_Enabled", "string" //
attribute "Motion_Enabled", "string" //
attribute "Motion_Sensitivity", "string" //
attribute "Temperature_Enabled", "string" //
attribute "Temperature_Low_Threshold", "string" //
attribute "Temperature_High_Threshold", "string" //
attribute "Humidity_Enabled", "string" //
attribute "Humidity_Low_Threshold", "string" //
attribute "Humidity_High_Threshold", "string" //
attribute "Light_Enabled", "string" //
attribute "Light_Low_Threshold", "string" //
attribute "Light_High_Threshold", "string" //
attribute "LightAlert", "string" //
attribute "HumidityAlert", "string" //
attribute "TemperatureAlert", "string" //
// Tile Template attribute
attribute "Tile", "string"; // Ex: "[font size='2'][b]Temperature:[/b] ${ temperature }°${ location.getTemperatureScale() }[/br][/font]"
}
preferences{
//section{
if( ShowAllPreferences ){
input( type: "string", name: "DeviceName", title: "Device Name", description: "If set it will change the device's name on the controller.", defaultValue: "${ device.label }")
input( type: "enum", name: "MountType", title: "Mount Type/Placement", required: true, multiple: false, options: [ "Door", "Window", "Garage", "Leak", "None" ], defaultValue: "${ state.MountType }" )
input( type: "bool", name: "StatusLED", title: "Status LED On/Off", defaultValue: false)
input( type: "bool", name: "AlarmDetection", title: "Detect Alarm On/Off", defaultValue: state.Alarm_Enabled )
input( type: "bool", name: "LightDetection", title: "Detect Light On/Off", defaultValue: state.Light_Enabled)
input( type: "number", name: "LightLowThreshold", title: "Light Low Threshold", description: "in Lux", required: false, defaultValue: 1 )
input( type: "number", name: "LightHighThreshold", title: "Light High Threshold", description: "in Lux", required: false, defaultValue: 100 )
input( type: "bool", name: "TemperatureDetection", title: "Detect Temperature On/Off", defaultValue: state.Temperature_Enabled)
input( type: "number", name: "TemperatureLowThreshold", title: "Temperature Low Threshold", description: "in ${ location.getTemperatureScale() }", required: false )
input( type: "number", name: "TemperatureHighThreshold", title: "Temperature High Threshold", description: "in ${ location.getTemperatureScale() }", required: false )
input( type: "bool", name: "HumidityDetection", title: "Detect Humidity On/Off", defaultValue: state.Humidity_Enabled)
input( type: "number", name: "HumidityLowThreshold", title: "Humidity Low Threshold", description: "in %", required: false, defaultValue: 30 )
input( type: "number", name: "HumidityHighThreshold", title: "Humidity High Threshold", description: "in %", required: false, defaultValue: 70 )
input( type: "bool", name: "MotionDetection", title: "Detect Motion On/Off", defaultValue: state.Motion_Enabled)
input( type: "number", name: "MotionSensitivity", title: "Motion Sensitivity", description: "in %", required: false, defaultValue: 75 )
input( name: "TileTemplate", type: "string", title: "Tile Template", description: "Ex: [b]Temperature:[/b] \${ state.temperature }°${ location.getTemperatureScale() }[/br]", defaultValue: "");
input( type: "enum", name: "LogType", title: "Enable Logging?", required: false, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" )
input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true )
} else {
input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true )
}
//}
}
}
// updated
def updated( boolean NewDevice = false ){
if( LogType == null ){
LogType = "Info"
}
if( NewDevice != true ){
SendSettings()
}
// Schedule daily check for driver updates to notify user
def Hour = ( new Date().format( "h" ) as int )
def Minute = ( new Date().format( "m" ) as int )
def Second = ( new Date().format( "s" ) as int )
// Set the driver name and version before update checking is scheduled
if( state."Driver Name" != null ){
state.remove( "Driver Name" )
state.remove( "Driver Version" )
device.deleteCurrentState( "Driver Name" )
device.deleteCurrentState( "Driver Version" )
}
ProcessEvent( "DriverName", DriverName() )
ProcessEvent( "DriverVersion", DriverVersion() )
// Schedule checks that are only performed once a day
schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "CheckForUpdate" )
Logging( "Updated", 2 )
}
// DoSomething is for testing and development purposes. It should not be uncommented for normal usage.
def DoSomething(){
}
// Configure device settings based on Preferences
def SendSettings(){
if( state.ID != null ){
def ShutoffValue = 15000
switch( AutoShutoff ){
case "15 Seconds":
ShutoffValue = 15000
break
case "30 Seconds":
ShutoffValue = 30000
break
case "1 Minute":
ShutoffValue = 60000
break
case "5 Minutes":
ShutoffValue = 300000
break
case "15 Minutes":
ShutoffValue = 900000
break
}
def TempMounting = "none"
def TempValue = "None"
if( MountType != null ){
TempValue = MountType
} else if( state.MountType != null ){
TempValue = state.MountType
}
switch( TempValue ){
case "Door":
TempMounting = "door"
break
case "Window":
TempMounting = "window"
break
case "Garage":
TempMounting = "garage"
break
case "Leak":
TempMounting = "leak"
break
case "None":
case "null":
default:
TempMounting = "none"
break
}
if( DeviceName != null && DeviceName != device.label ){
parent.SendSensorSettings( device.getDeviceNetworkId(), state.ID, "{\"name\":\"${ DeviceName }\",\"ledSettings\":{\"isEnabled\":${ StatusLED }},\"alarmSettings\":{\"isEnabled\":${ AlarmDetection }},\"lightSettings\":{\"isEnabled\":${ LightDetection },\"lowThreshold\":${ LightLowThreshold },\"highThreshold\":${ LightHighThreshold },\"margin\":10},\"motionSettings\":{\"isEnabled\":${ MotionDetection },\"sensitivity\":${ MotionSensitivity }},\"temperatureSettings\":{\"isEnabled\":${ TemperatureDetection },\"lowThreshold\":${ ConvertTemperatureToTarget( "C", location.getTemperatureScale(), TemperatureLowThreshold ) },\"highThreshold\":${ ConvertTemperatureToTarget( "C", location.getTemperatureScale(), TemperatureHighThreshold ) },\"margin\":0.1},\"humiditySettings\":{\"isEnabled\":${ HumidityDetection },\"lowThreshold\":${ HumidityLowThreshold },\"highThreshold\":${ HumidityHighThreshold },\"margin\":1},\"mountType\":\"${ TempMounting }\"}" )
//parent.SendSensorSettings( device.getDeviceNetworkId(), state.ID, "{\"name\":\"${ DeviceName }\",\"lightDeviceSettings\":{\"isIndicatorEnabled\":${ StatusLED },\"pirDuration\":${ ShutoffValue },\"pirSensitivity\":${ MotionSensitivity }}}" )
} else {
parent.SendSensorSettings( device.getDeviceNetworkId(), state.ID, "{\"name\":\"${ device.label }\",\"ledSettings\":{\"isEnabled\":${ StatusLED }},\"alarmSettings\":{\"isEnabled\":${ AlarmDetection }},\"lightSettings\":{\"isEnabled\":${ LightDetection },\"lowThreshold\":${ LightLowThreshold },\"highThreshold\":${ LightHighThreshold },\"margin\":10},\"motionSettings\":{\"isEnabled\":${ MotionDetection },\"sensitivity\":${ MotionSensitivity }},\"temperatureSettings\":{\"isEnabled\":${ TemperatureDetection },\"lowThreshold\":${ ConvertTemperatureToTarget( "C", location.getTemperatureScale(), TemperatureLowThreshold ) },\"highThreshold\":${ ConvertTemperatureToTarget( "C", location.getTemperatureScale(), TemperatureHighThreshold ) },\"margin\":0.1},\"humiditySettings\":{\"isEnabled\":${ HumidityDetection },\"lowThreshold\":${ HumidityLowThreshold },\"highThreshold\":${ HumidityHighThreshold },\"margin\":1},\"mountType\":\"${ TempMounting }\"}" )
//parent.SendSensorSettings( device.getDeviceNetworkId(), state.ID, "{\"name\":\"${ device.label }\",\"lightDeviceSettings\":{\"isIndicatorEnabled\":${ StatusLED },\"pirDuration\":${ ShutoffValue },\"pirSensitivity\":${ MotionSensitivity }}}" )
}
} else {
Logging( "No ID for ${ device.getDeviceNetworkId() }, cannot send settings", 5 )
}
}
// refresh information on the specific child
def refresh(){
if( state.ID != null ){
parent.GetSensorStatus( device.getDeviceNetworkId(), state.ID )
} else {
Logging( "No ID for ${ device.getDeviceNetworkId() }, cannot refresh", 5 )
}
}
// Turn on the device's locate function to help locate/identify it
def Locate(){
if( state.ID != null ){
parent.LocateSensor( device.getDeviceNetworkId(), state.ID )
} else {
Logging( "No ID for ${ device.getDeviceNetworkId() }, cannot activate identify function", 5 )
}
}
// Checks the location.getTemperatureScale() to convert temperature values
def ConvertTemperatureToTarget( String Target, String Origin, Number Value ){
if( Value != null ){
def ReturnValue = Value as double
if( Target.toUpperCase() == "C" && Origin.toUpperCase() == "F" ){
ReturnValue = ( ( ( Value - 32 ) * 5 ) / 9 )
Logging( "Temperature Conversion ${ Value }°F to ${ ReturnValue }°C", 4 )
} else if( Target.toUpperCase() == "F" && Origin.toUpperCase() == "C" ) {
ReturnValue = ( ( ( Value * 9 ) / 5 ) + 32 )
Logging( "Temperature Conversion ${ Value }°C to ${ ReturnValue }°F", 4 )
} else if( ( Target.toUpperCase()== "C" && Origin.toUpperCase() == "C" ) || ( Target.toUpperCase() == "F" && Origin.toUpperCase() == "F" ) ){
ReturnValue = Value
}
def TempInt = ( ReturnValue * 100 ) as int
ReturnValue = ( TempInt / 100 )
return ReturnValue
}
}
// installed is called when the device is installed, all it really does is run updated
def installed(){
Logging( "Installed", 2 )
updated( true )
}
// initialize is called when the device is initialized, all it really does is run updated
def initialize(){
Logging( "Initialized", 2 )
updated( true )
}
// Return a state value
def ReturnState( Variable ){
return state."${ Variable }"
}
// Tile Template method based on @mircolino's HTML Template method
private void UpdateTile( String val ){
if( settings.TileTemplate ){
// Create special compound/html tile
val = settings.TileTemplate.toString().replaceAll( "\\[", "<" )
val = val.replaceAll( "\\]", ">" )
val = val.replaceAll( ~/\$\{\s*([A-Za-z][A-Za-z0-9_]*)\s*\}/ ) { java.util.ArrayList m -> device.currentValue("${ m [ 1 ] }").toString() }
if( device.currentValue( "Tile" ).toString() != val ){
sendEvent( name: "Tile", value: val )
}
}
}
// 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 }" )
}
}
// Handles whether logging is enabled and thus what to put there.
def Logging( LogMessage, LogLevel ){
// Add all messages as info logging
if( ( LogLevel == 2 ) && ( LogType != "None" ) ){
log.info( "${ device.displayName } - ${ LogMessage }" )
} else if( ( LogLevel == 3 ) && ( ( LogType == "Debug" ) || ( LogType == "Trace" ) ) ){
log.debug( "${ device.displayName } - ${ LogMessage }" )
} else if( ( LogLevel == 4 ) && ( LogType == "Trace" ) ){
log.trace( "${ device.displayName } - ${ LogMessage }" )
} else if( LogLevel == 5 ){
log.error( "${ device.displayName } - ${ LogMessage }" )
}
}
// Checks drdsnell.com for the latest version of the driver
// Original inspiration from @cobra's version checking
def CheckForUpdate(){
ProcessEvent( "DriverName", DriverName() )
ProcessEvent( "DriverVersion", DriverVersion() )
httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp ->
switch( resp.status ){
case 200:
if( resp.data."${ DriverName() }" ){
CurrentVersion = DriverVersion().split( /\./ )
if( resp.data."${ DriverName() }".version == "REPLACED" ){
ProcessEvent( "DriverStatus", "Driver replaced, please use ${ resp.data."${ state.DriverName }".file }" )
} else if( resp.data."${ DriverName() }".version == "REMOVED" ){
ProcessEvent( "DriverStatus", "Driver removed and no longer supported." )
} else {
SiteVersion = resp.data."${ DriverName() }".version.split( /\./ )
if( CurrentVersion == SiteVersion ){
Logging( "Driver version up to date", 2 )
ProcessEvent( "DriverStatus", "Up to date" )
} else if( ( CurrentVersion[ 0 ] as int ) > ( SiteVersion [ 0 ] as int ) ){
Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 4 )
ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){
Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 4 )
ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){
Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 4 )
ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( SiteVersion[ 0 ] as int ) > ( CurrentVersion[ 0 ] as int ) ){
Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
} else if( ( SiteVersion[ 1 ] as int ) > ( CurrentVersion[ 1 ] as int ) ){
Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
} else if( ( SiteVersion[ 2 ] as int ) > ( CurrentVersion[ 2 ] as int ) ){
Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
}
}
} else {
Logging( "${ DriverName() } is not published on drdsnell.com", 2 )
ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com" )
}
break
default:
Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 )
break
}
}
}