/*
* SONOFF NSPanel Pro API
*
* Description:
* This Hubitat driver allows using the SONOFF NSPanel Pro API (https://sonoff.tech/nspanelpro-api/).
*
* Overall Setup:
* 1) Add the driver as a new user driver
* 2) Add a Virtual Device for the parent
* 3) Go to the device's Preferences section and enter the Panel's IP/Hostname BUT DO NOT SAVE YET
* (OPTIONAL: You can also pre-set the Refresh Rate and other Preferences)
* 4) On the panel itself, go to Device Settings then About.
* 5) Tap the Name label (on the left, so it does not trigger renaming the device) 7 times until you see the "Allowed to generate gateway access token"
* message popup on the panel
* 6) Go back to the device's Preferences on the Hubitat screen and Save Preferences
*
* Instructions for using Tile method:
* 1) In "Preferences -> Tile Template" enter your template (example below) and click "Save Preferences"
* Ex: "[b]Temperature:[/b] @temperature@°@location.getTemperatureScale()@[/br]"
* 2) 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
* 3) Select the Add Tile button and the tile should appear
* NOTE1: Put a @ before and after variable names
* NOTE2: Should accept most HTML formatting commands with [] instead of <>
*
* Features List:
* Allows playing a built-in sound
* Allows setting certain system settings
* Allows rebooting the system
* Checks drdsnell.com for an updated driver on a daily basis
*
* Licensing:
* Copyright 2025 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):
* The set commands do not appear to work when the system settings menu is open, only when the normal screens are showing.
*
* Version Control:
* 0.1.2 - Added commands to set screen brightness, auto-off/auto-lock, system volume, and perform a reboot
* 0.1.1 - Updated to match my current driver standards and updates to changes in API.
* 0.1.0 - Initial version
*
* Thank you(s):
* @Cobra for inspiration on driver version checking.
*/
// Returns the driver name
def DriverName(){
return "NSPanelProAPI"
}
// Returns the driver version
def DriverVersion(){
return "0.1.2"
}
// Driver Metadata
metadata{
definition( name: "NSPanelProAPI", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/NSPanelProAPI.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 "Login" // Logs in to the controller to get a cookie for the session
command "PlaySound", [ // Plays one of the built-in sounds at X volume and for Y duration
[ name: "*Name", type: "ENUM", constraints: [ "Sound: alert1", "Sound: alert2", "Sound: alert3", "Sound: alert4", "Sound: alert5", "Sound: doorbell1", "Sound: doorbell2", "Sound: doorbell3", "Sound: doorbell4", "Sound: doorbell5", "Sound: alarm1", "Sound: alarm2", "Sound: alarm3", "Sound: alarm4", "Sound: alarm5", "Beep: deviceDiscovered" ], description: "Select the sound to play", required: true ],
[ name: "*Volume", type: "INTEGER", description: "Enter volume from 0-100", defaultValue: 100, required: true ],
[ name: "Countdown", type: "INTEGER", description: "Enter sound duration in seconds 0-1799 (only applies to alerts, doorbells, or alarms)", defaultValue: 10, required: false ]
]
command "SetScreenBrightness", [ // Sets the screen brightness
[ name: "*Brightness", type: "INTEGER", description: "Enter number 0-100 for manual brightness, or any other number for auto brightness", defaultValue: 100, required: true ]
]
command "SetScreenAutoOff", [ // Sets the screen auto-off/auto-lock duration
[ name: "*Duration", type: "INTEGER", description: "Enter auto-off/auto-lock duration from 15-1800 seconds, anything else turns off auto-off/auto-lock mode", defaultValue: 120, required: true ]
]
command "SetSystemVolume", [ // Sets the system volume
[ name: "*Volume", type: "INTEGER", description: "Enter volume from 0-100", defaultValue: 100, required: true ]
]
command "Reboot", [ // Reboots the system
[ name: "*Confirmation", type: "STRING", description: "Confirm desire to reboot by typing \"reboot\" into the field", required: true ],
]
/*
[ "alert1", "alert2", "alert3", "alert4", "alert5", "doorbell1", "doorbell2", "doorbell3", "doorbell4", "doorbell5", "alarm1", "alarm2", "alarm3", "alarm4", "alarm5", "bootComplete", "networkConnected", "networkDisconnected", "systemShutdown", "deviceDiscovered", "system Armed", "system Disarmed", "factoryReset" ]
// API Supported Sounds
alert1 (Alarm Sound 1)
alert2 (Alarm Sound 2)
alert3 (Alarm Sound 3)
alert4 (Alarm Sound 4)
alert5 (Alarm Sound 5)
doorbell1 (Doorbell Sound 1)
doorbell2 (Doorbell Sound 2)
doorbell3 (Doorbell Sound 3)
doorbell4 (Doorbell Sound 4)
doorbell5 (Doorbell Sound 5)
alarm1 (Alarm Sound 1)
alarm2 (Alarm Sound 2)
alarm3 (Alarm Sound 3)
alarm4 (Alarm Sound 4)
alarm5 (Alarm Sound 5)
// API Supported Beeps
bootComplete (System startup completed) - Does not make any noise so removed from command
networkConnected (Network connected) - Does not make any noise so removed from command
networkDisconnected (Network disconnected) - Does not make any noise so removed from command
systemShutdown (System shutdown) - Does not make any noise so removed from command
deviceDiscovered (Discover device)
system Armed (System armed enable) - Does not make any noise so removed from command
system Disarmed (System armed disable) - Does not make any noise so removed from command
factoryReset (Reset device) - Does not make any noise so removed from command
*/
command "GetDeviceList" //
command "GetBridgeInfo" //
// 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 "LastLogin", "string" // Shows when the last login was performed
attribute "LastRefresh", "string" // Shows when the last refresh was performed
attribute "LastUpdatedID", "string" // Shows the ID for the last update from the bootstrap
// Tile Template attribute
attribute "Tile", "string"; // Ex: "[b]Temperature:[/b] @temperature@°@location.getTemperatureScale()@[/br]"
}
preferences{
section{
if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options
input( name: "TileTemplate", type: "string", title: "Tile Template", description: "Ex: [b]Temperature:[/b] @temperature@°@location.getTemperatureScale()@[/br]", defaultValue: "");
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: "enum", name: "LogType", title: "Enable Logging?", required: false, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" )
input( type: "string", name: "PanelURL", title: "Panel IP/Hostname", 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(){
}
// updated is called whenever device parameters are saved
def updated(){
Logging( "Updating...", 2 )
if( ( state.Token == null ) || ( state.LoginRetries > 0 ) ){
Login()
pauseExecution( 1000 )
}
unschedule()
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 )
// Check what the refresh rate is set for then run it
switch( RefreshRate ){
case "1 minute": // Schedule the refresh check for every minute
schedule( "${ Second } * * ? * *", "refresh" )
break
case "5 minutes": // Schedule the refresh check for every 5 minutes
schedule( "${ Second } 0/5 * ? * *", "refresh" )
break
case "10 minutes": // Schedule the refresh check for every 10 minutes
schedule( "${ Second } 0/10 * ? * *", "refresh" )
break
case "15 minutes": // Schedule the refresh check for every 15 minutes
schedule( "${ Second } 0/15 * ? * *", "refresh" )
break
case "30 minutes": // Schedule the refresh check for every 30 minutes
schedule( "${ Second } 0/30 * ? * *", "refresh" )
break
case "1 hour": // Schedule the refresh check for every hour
schedule( "${ Second } ${ Minute } * ? * *", "refresh" )
break
case "3 hours": // Schedule the refresh check for every 3 hours
schedule( "${ Second } ${ Minute } 0/3 ? * *", "refresh" )
break
default:
RefreshRate = "Manual"
break
}
Logging( "Refresh rate: ${ RefreshRate }", 4 )
// Set the driver name and version before update checking is scheduled
ProcessEvent( "DriverName", DriverName() )
ProcessEvent( "DriverVersion", DriverVersion() )
ProcessEvent( "DriverStatus", null )
// Schedule checks that are only performed once a day
schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "DailyCheck" )
Logging( "Updated", 2 )
}
// DailyCheck is only performed once a day
def DailyCheck(){
GetDeviceList()
CheckForUpdate()
}
// refresh performs a poll of data
def refresh(){
ProcessState( "LastRefresh", new Date() )
}
//Log in attempt
def Login( Manual = true ){
def Attempt = "/open-api/v1/rest/bridge/access_token"
if( ( Manual ) || ( state.LoginRetries < 5 ) ){
asynchttpGet( "ReceiveData", GenerateParamsNoAuth( "${ Attempt }" ), [ Method: "Login" ] )
} else {
Logging( "Too many login failures. Please confirm settings and perform a manual login.", 5 )
ProcessState( "Status", "Login failure, check settings and perform manual login." )
}
}
// Command to attempt to get the device list
def GetDeviceList(){
def Attempt = "/open-api/v1/rest/devices"
asynchttpGet( "ReceiveData", GenerateParams( "${ Attempt }" ), [ Method: "DeviceList" ] )
}
// Command to attempt to get the bridge info
def GetBridgeInfo(){
def Attempt = "/open-api/v1/rest/bridge"
asynchttpGet( "ReceiveData", GenerateParamsNoAuth( "${ Attempt }" ), [ Method: "BridgeInfo" ] )
}
// Command to attempt to set the screen brightness
def SetScreenBrightness( Number Value = 100 ){
def Attempt = "/open-api/v1/rest/screen/brightness"
def Params
if( ( Value >= 0 ) && ( Value <= 100 ) ){
//asynchttpPut( "ReceiveData", GenerateParams( "${ Attempt }", "mode: manual, value: ${ Value }" ), [ Method: "SetScreenBrightness" ] )
Params = GenerateParams( "${ Attempt }", "{\"mode\":\"manual\",\"value\":${ Value }}" )
SendPut( Params, "SetScreenBrightness" )
} else {
//asynchttpPut( "ReceiveData", GenerateParams( "${ Attempt }", "mode: \"auto\"" ), [ Method: "SetScreenBrightness" ] )
Params = GenerateParams( "${ Attempt }", "{\"mode\":\"auto\"}" )
SendPut( Params, "SetScreenBrightness" )
}
}
// Command to attempt to set the screen's auto off
def SetScreenAutoOff( Number Value = 600 ){
def Attempt = "/open-api/v1/rest/screen/display"
def Params
if( ( Value >= 15 ) && ( Value <= 1800 ) ){
Params = GenerateParams( "${ Attempt }", "{\"auto_screen_off\":{\"enable\":true,\"duration\":${ Value }}}" )
SendPut( Params, "SetScreenAutoOff" )
} else {
Params = GenerateParams( "${ Attempt }", "{\"auto_screen_off\":{\"enable\":false }}" )
SendPut( Params, "SetScreenAutoOff" )
}
}
// Command to attempt to set the system's overall volume
def SetSystemVolume( Number Value = 100 ){
def Attempt = "/open-api/v1/rest/bridge/config"
def Params
def Volume = Value
if( ( Volume < 0 ) || ( Volume > 100 ) ){
Volume = 100
Logging( "Volume invalid, setting to 100", 3 )
}
Params = GenerateParams( "${ Attempt }", "{\"volume\":${ Volume }}" )
SendPut( Params, "SetSystemVolume" )
}
// Command to attempt to reboot the
def Reboot( String Confirmation ){
if( Confirmation == "reboot" ){
Logging( "Reboot confirmation correct, attempting reboot", 2 )
def Attempt = "/open-api/v1/rest/hardware/reboot"
def Params = GenerateParams( "${ Attempt }" )
SendPost( Params, "Reboot" )
} else {
Logging( "Reboot confirmation incorrect, will not reboot", 2 )
}
}
// Command to attempt to play sounds
def PlaySound( String Name, Number Volume, Number Countdown = 10 ){
def Attempt = "/open-api/v1/rest/hardware/speaker"
def Params
def TempCountdown = Countdown
if( ( TempCountdown < 0 ) || ( TempCountDown > 1799 ) ){
TempCountdown = 10
Logging( "Sound countdown for ${ Name } invalid, setting to 10 seconds", 3 )
}
if( Name.split( " " )[ 0 ].toString() == "Sound:" ){
Params = GenerateParams( "${ Attempt }", "{\"type\":\"play_sound\",\"sound\":{\"name\":\"${ Name.split( " " )[ 1 ].toString() }\",\"volume\":${ Volume },\"countdown\":${ TempCountdown }}}" )
} else {
Params = GenerateParams( "${ Attempt }", "{\"type\":\"play_beep\",\"beep\":{\"name\":\"${ Name.split( " " )[ 1 ].toString() }\",\"volume\":${ Volume }}}" )
}
//asynchttpPost( "ReceiveData", Params, [ Method: "PlaySound" ] )
SendPost( Params, "PlaySound" )
}
// GenerateParams assembles the parameters to be sent rather than repeat so much of it
def GenerateParams( String Path, String Data = null ){
def Params
if( Data != null ){
Params = [ uri: "http://${ PanelURL }:8081${ Path }", requestContentType: "application/json", headers: [ 'Content-Type': 'application/json', Authorization: "Bearer ${ state.Token }" ], body:"${ Data }" ]
} else {
Params = [ uri: "http://${ PanelURL }:8081${ Path }", requestContentType: "application/json", headers: [ 'Content-Type': 'application/json', Authorization: "Bearer ${ state.Token }" ] ]
}
Logging( "Parameters = ${ Params }", 4 )
return Params
}
// GenerateParams assembles the parameters to be sent rather than repeat so much of it
def GenerateParamsNoAuth( String Path, String Data = null ){
def Params
if( Data != null ){
Params = [ uri: "http://${ PanelURL }:8081${ Path }", requestContentType: "application/json", headers: [ 'Content-Type': 'application/json' ], data:"${ Data }" ]
} else {
Params = [ uri: "http://${ PanelURL }:8081${ Path }", requestContentType: "application/json", headers: [ 'Content-Type': 'application/json' ] ]
}
Logging( "Parameters = ${ Params }", 4 )
return Params
}
// Meant to handle the common responses from SendPost or SendPut
def SendResponses( resp, Method ){
switch( resp.getStatus() ){
case 200:
ProcessEvent( "Status", "${ Method } successful." )
break
case 408:
Logging( "${ Method } Request Timeout", 5 )
break
default:
Logging( "${ Method } Error: ${ resp.status }", 5 )
break
}
}
// Send an httpPost for the particular method provided (meant for functions with no data response)
def SendPost( Params, Method ){
try{
httpPost( Params ){ resp ->
SendResponses( resp, Method )
}
} catch( Exception e ){
if( Method != "Reboot" ){
Logging( "Exception: ${ e }", 5 )
}
}
}
// Send an httpPut for the particular method provided (meant for functions with no data response)
def SendPut( Params, Method ){
try{
httpPut( Params ){ resp ->
SendResponses( resp, Method )
}
} catch( Exception e ){
Logging( "Exception: ${ e }", 5 )
}
}
// Receive general data
def ReceiveData( resp, data ){
switch( resp.getStatus() ){
case 200:
def Json = parseJson( resp.data )
def TempError = 0
def ErrorMessage = "${ Json.message }"
switch( Json.error ){
case 0:
switch( data.Method ){
case "Login":
Logging( "${ data.Method } Data: ${ resp.data }", 4 )
ProcessState( "LoginRetries", 0 )
ProcessState( "LastLogin", new Date() )
ProcessState( "Token", Json.data.token )
break
case "PlaySound":
Logging( "${ data.Method } Data: ${ resp.data }", 4 )
break
case "BridgeInfo":
Logging( "${ data.Method } Data: ${ Json }", 4 )
Json.data.each(){
if( it.key != null ){
switch( it.key ){
case "ip":
ProcessState( "IP", it.value )
break
case "mac":
ProcessState( "MAC", it.value )
break
case "domain":
ProcessState( "Domain", it.value )
break
case "fw_version":
ProcessState( "FirmwareVersion", it.value )
break
default:
Logging( "Unhandled data for ${ Device } ${ it.key } = ${ it.value }", 3 )
break
}
}
}
break
case "DeviceList":
Logging( "${ data.Method } Data: ${ Json }", 4 )
//Json.data.each(){
//}
break
// Successful responses for following commands have no data to process
case "SetScreenBrightness":
case "SetScreenAutoOff":
case "Restart":
Logging( "${ data.Method } Data: ${ resp.data }", 4 )
break
default:
Logging( "${ data.Method } Unhandled: ${ Json }", 3 )
break
}
break
case 401:
Logging( "${ data.Method } Error: ${ ErrorMessage }", 5 )
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: // Page not found
ProcessState( "Status", "${ data.Method } Page not found error" )
Logging( "Page not found for ${ data.Method }", 5 )
break
case 408: // Timeout
ProcessState( "Status", "Request timeout for ${ data.Method }" )
if( resp.getHeaders() != null ){
Logging( "Timeout for ${ data.Method } headers = ${ resp.getHeaders() }", 4 )
} else {
Logging( "Timeout for ${ data.Method } = ${ ErrorMessage }", 4 )
}
break
default: // Other error
ProcessState( "Status", "Error ${ ErrorMessage } connecting for ${ data.Method }" )
Logging( "Error connecting to panel: ${ ErrorMessage } for ${ data.Method }", 5 )
break
}
}
// Process data coming in for a child device
def ProcessChildData( 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
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
}
}
// Tile method to produce HTML formatted string for dashboard use
private void UpdateTile( String val ){
if( TileTemplate != null ){
def TempString = ""
Parsing = TileTemplate
Parsing = Parsing.replaceAll( "\\[", "<" )
Parsing = Parsing.replaceAll( "\\]", ">" )
Count = Parsing.count( "@" )
if( Count >= 1 ){
def x = 1
while( x <= Count ){
TempName = Parsing.split( "@" )[ x ]
switch( TempName ){
case "location.latitude":
Value = location.latitude
break
case "location.longitude":
Value = location.longitude
break
case "location.getTemperatureScale()":
Value = location.getTemperatureScale()
break
default:
Value = ReturnState( "${ TempName }" )
break
}
TempString = TempString + Parsing.split( "@" )[ ( x - 1 ) ] + Value
x = ( x + 2 )
}
if( Parsing.split( "@" ).last() != Parsing.split( "@" )[ Count - 1 ] ){
TempString = TempString + Parsing.split( "@" ).last()
}
} else if( Count == 1 ){
TempName = Parsing.split( "@" )[ 1 ]
switch( TempName ){
case "location.latitude":
Value = location.latitude
break
case "location.longitude":
Value = location.longitude
break
case "location.getTemperatureScale()":
Value = location.getTemperatureScale()
break
default:
Value = ReturnState( "${ TempName }" )
break
}
TempString = TempString + Parsing.split( "@" )[ 0 ] + Value
} else {
TempString = TileTemplate
}
sendEvent( name: "Tile", value: TempString, isStateChange: true )
}
}
// 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 "Test": //
ChildType = "Test"
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 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( "PanelChild", DNI, [ name: "${ DNI }" ] )
} else {
addChildDevice( "PanelChild-${ ChildType }", DNI, [ name: "${ DNI }" ] )
}
}
catch( Exception e ){
def Temp = e as String
if( Temp.contains( "not found" ) ){
if( ( ChildType != null ) && ( ChildType != "Generic" ) ){
Logging( "PanelChild-${ 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( "PanelChild 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
}
}
}