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