/* * FireBoard Cloud API * * Description: * This Hubitat driver allows polling of the FireBoard Cloud API. * * Overall Setup: * 1) Add the FireBoardCloudAPI.groovy and FireBoardChild.groovy as new user drivers * 2) Add Virtual Device as the parent * 3) Enter the account Username & Password in the Preference fields and Save Preferences * OPTIONAL: Set the Refresh Rate or Logging as desired * * 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: * Can create an HTML Template that contains values for every child device * Checks FireBoard API for devices on account * Creates child devices based on main device and channels * Checks drdsnell.com for an updated driver on a daily basis * * Licensing: * Copyright 2026 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.21 - Additional null handling, changes to Event & State Variable handling, addition of Alarms attribute and determination of Alarm(s) as well * as command to ClearAlarms, added 2 minute refresh interval * 0.1.20 - Attempting to fix for a null event, additional data handling, addition of Drivelog checking, changes to version method * 0.1.19 - Correction to ProcessEvent function and removal of old driver-specific attributes when Preferences are saved * 0.1.18 - 15 second refresh replaced with 20 seconds to not hit the API limit (200 calls per hour) & * ListAllDevices runs when preferences are saved (after login is attempted) * 0.1.17 - Added 15 and 30 second refresh rates (30 seconds was mistakenly removed) * 0.1.16 - Device no longer performs refresh ALL the time as scheduled instead it is performed when "on" * 0.1.15 - Corrected mistake in update * 0.1.14 - Improvements and additions to data handling * 0.1.13 - Switched template method so it removes HTML, updates to general code, additional attributes added * 0.1.12 - Added capability to have html template show every child device's matching attribute(s) * 0.1.11 - Correction for some nested data errors and added battery reading * 0.1.10 - Correction for channel data as part of latest temps * 0.1.9 - Correction for "GetTemperatures" data to be processed properly, changes to daily driver check method * 0.1.8 - Attempting to get device data to iterate through switch check, corrected null checks for sessions and temperature * 0.1.7 - Further refinement of receiving listalldevices data * 0.1.6 - Correction for switch statement when receiving data * 0.1.5 - Correction for missing break statement in ReceiveData * 0.1.4 - Change to ListSessions and rework how data is handled * 0.1.3 - Changes for GetTemperature and addition of ListSessions data * 0.1.2 - Better error handling for nonexistent children, corrected calls to GetTemperature * 0.1.1 - Attempting to populate things with the data returned and create child devices * 0.1.0 - Initial version * * Thank you(s): * @Cobra for inspiration on driver version checking * @mircolino for his original HTML Template method * @dbowles1975, @samiam, & @tray_e for all their patience in testing this */ import groovy.transform.Field @Field static final String DRIVER = "FireBoardCloudAPI" // Returns the driver name def DriverName(){ return "FireBoardCloudAPI" } @Field static final String VERSION = "0.1.21" // Returns the driver version public static String version(){ return "0.1.21" } // Driver Metadata metadata{ definition( name: "FireBoardCloudAPI", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/FireBoardCloudAPI.groovy" ) { // Indicate what capabilities the device should be capable of capability "Sensor" capability "Refresh" capability "Switch" capability "Actuator" // Commands //command "DoSomething" // Does something for development/testing purposes, should be commented before publishing command "Login" // Logs in to the API to get a key command "ListAllDevices" // Should list all devices for the account command "GetTemperature", [ [ name: "DNI", type: "STRING", description: "Enter FireBoard Device DNI to check (blank checks all)" ] ] command "ListSessions", [ [ name: "Session", type: "STRING", description: "Enter SessionID to check (blank checks for any)" ] ] command "GetDrivelog" command "ClearAlarms" // Attributes for the driver itself attribute "DriverName", "string" // Driver identifies the driver being used for update purposes attribute "DriverVersion", "string" // Version number of the driver attribute "DriverStatus", "string" // Status of the driver version compared to what is currently published attribute "Status", "string" // Used to provide general status of the device // Attributes for the parent device attribute "Alarms", "string" // String for happening on child devices // Tile Template attribute attribute "Tile", "string"; // Ex: "[font size='2'][b]Temperature:[/b] ${ temperature }°${ location.getTemperatureScale() }[/br][/font]" } preferences{ section{ if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options input( type: "string", name: "TileTemplate", title: "Tile Template", description: "Ex: [b]Temperature:[/b] \${ state.temperature }°${ location.getTemperatureScale() }[/br]", defaultValue: ""); input( type: "enum", name: "RefreshRate", title: "Refresh Rate", required: false, multiple: false, options: [ "1 minute", "2 minutes", "5 minutes", "10 minutes", "30 minutes", "1 hour", "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: "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 ) } } } } // Just a command to be put fixes or other oddities during development def DoSomething(){ } // 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" ) } ProcessState( "DriverName", "${ DriverName() }" ) ProcessState( "DriverVersion", "${ version() }" ) ProcessState( "DriverStatus", null ) ClearAlarms() if( LogType == null ){ LogType = "Info" } // Daily version checking schedule unschedule( "CheckForUpdate" ) def Hour = ( new Date().format( "h" ) as int ) def Minute = ( new Date().format( "m" ) as int ) def Second = ( new Date().format( "s" ) as int ) // Schedule daily check for updated driver // Set the Version and Driver states schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "CheckForUpdate" ) // Attempt to login Login() pauseExecution( 2000 ) // Check what the refresh rate is set for then run it unschedule( "refresh" ) switch( RefreshRate ){ case "1 minute": // Schedule the refresh check for every minute schedule( "${ Second } * * ? * *", "refresh" ) break case "2 minutes": // Schedule the refresh check for every 2 minutes schedule( "${ Second } 0/2 * ? * *", "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 default: RefreshRate = "Manual" break } Logging( "Updated", 2 ) } // refresh performs a poll of data def refresh(){ GetTemperature() GetDrivelog() ListAllDevices() //ListSessions() ProcessEvent( "LastRefresh", new Date() ) } //Log in def Login(){ def Params Params = [ uri: "https://fireboard.io/api/rest-auth/login/", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", body: "{ \"username\":\"${ Username }\", \"password\":\"${ Password }\" }" ] try{ httpPost( Params ){ resp -> switch( resp.getStatus() ){ case 200: Logging( "Login response = ${ resp.data }", 4 ) ProcessEvent( "Status", "Login successful." ) ProcessEvent( "LastLogin", new Date() ) ProcessState( "Key", "${ resp.data.key }" ) break case 408: Logging( "Request Timeout", 5 ) break default: Logging( "Error logging in: ${ resp.status }", 5 ) break } } } catch( Exception e ){ Logging( "Exception when performing Login: ${ e }", 5 ) } } // When the device is turned "on" it starts refreshing the data def on(){ unschedule( "refresh" ) def Hour = ( new Date().format( "h" ) as int ) def Minute = ( new Date().format( "m" ) as int ) def Second = ( new Date().format( "s" ) as int ) switch( RefreshRate ){ case "20 seconds": schedule( "0/20 * * ? * *", "refresh" ) break case "30 seconds": schedule( "0/30 * * ? * *", "refresh" ) break case "1 minute": schedule( "${ Second } * * ? * *", "refresh" ) break case "5 minutes": schedule( "${ Second } 0/5 * ? * *", "refresh" ) break case "10 minutes": schedule( "${ Second } 0/10 * ? * *", "refresh" ) break case "15 minutes": schedule( "${ Second } 0/15 * ? * *", "refresh" ) break case "30 minutes": schedule( "${ Second } 0/30 * ? * *", "refresh" ) break case "1 hour": schedule( "${ Second } ${ Minute } * ? * *", "refresh" ) break default: RefreshRate = "Manual" break } Logging( "Refresh rate: ${ RefreshRate }", 4 ) ProcessEvent( "switch", "on", true ) } // When the device is turned "off" it unschedules the refresh def off(){ unschedule( "refresh" ) ProcessEvent( "switch", "off", true ) } // Generate Params 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: "https://fireboard.io/api/v1/${ Path }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Authorization: "Token ${ state.Key }" ], data:"${ Data }" ] } else { Params = [ uri: "https://fireboard.io/api/v1/${ Path }", ignoreSSLIssues: true, requestContentType: "application/json", contentType: "application/json", headers: [ Authorization: "Token ${ state.Key }" ] ] } Logging( "Parameters = ${ Params }", 4 ) return Params } // CheckPoll makes sure all basic items are needed before doing a Get/Post besides Login boolean CheckPoll(){ def TempBool = false if( state.Key != null ){ TempBool = true } else { Logging( "Key must be populated, please login first.", 5 ) } return TempBool } // Lists all devices on the account def ListAllDevices(){ if( CheckPoll ){ Logging( "Attempting to ListAllDevices.", 4 ) asynchttpGet( "ReceiveData", GenerateParams( "devices.json" ), [ Method: "ListAllDevices", Device: "All" ] ) } } // Attempts to get the temperature of a FireBoard (API returns one from the last 60 seconds only) def GetTemperature( DNI = null ){ if( DNI == null ){ Logging( "No specific device entered, attempting to GetTemperature for all known.", 4 ) getChildDevices().each{ if( getChildDevice( it.deviceNetworkId ).ReturnState( "UUID" ) != null ){ if( CheckPoll ){ Logging( "Attempting to GetTemperature for device= ${ it.deviceNetworkId } UUID= ${ getChildDevice( it.deviceNetworkId ).ReturnState( "UUID" ) }", 4 ) asynchttpGet( "ReceiveData", GenerateParams( "devices/${ getChildDevice( it.deviceNetworkId ).ReturnState( "UUID" ) }/temps.json" ), [ Method: "GetTemperature", Device: "${ it.deviceNetworkId }" ] ) } } } } else { if( getChildDevice( DNI ) != null ){ if( getChildDevice( DNI ).ReturnState( "UUID" ) != null ){ if( CheckPoll ){ Logging( "Attempting to GetTemperature for device= ${ DNI } UUID= ${ getChildDevice( it.deviceNetworkId ).ReturnState( "UUID" ) }", 4 ) asynchttpGet( "ReceiveData", GenerateParams( "devices/${ getChildDevice( DNI ).ReturnState( "UUID" ) }/temps.json" ), [ Method: "GetTemperature", Device: "${ DNI }" ] ) } } } } } // Attempts to get the drivelog of a FireBoard (API returns one from the last 60 seconds only) def GetDrivelog(){ if( state.FireBoardUUID != null ){ if( CheckPoll ){ Logging( "Attempting to GetDrivelog", 4 ) asynchttpGet( "ReceiveData", GenerateParams( "devices/${ state.FireBoardUUID }/drivelog.json" ), [ Method: "GetDrivelog", Device: "${ state.FireBoardUUID }" ] ) } } } // Attempts to get sessions data def ListSessions( Session = null ){ if( Session == null ){ Logging( "No specific Session entered, attempting to get all sessions.", 4 ) if( CheckPoll ){ asynchttpGet( "ReceiveData", GenerateParams( "sessions.json" ), [ Method: "ListSessions", Session: "All" ] ) } } else { Logging( "Attempting to get data for Session ${ Session }.", 4 ) if( CheckPoll ){ asynchttpGet( "ReceiveData", GenerateParams( "sessions/${ Session }.json" ), [ Method: "ListSessions", Session: "${ Session }" ] ) } } } // Meant to clear out any alarm information on the parent and any children def ClearAlarms(){ ProcessEvent( "Alarms", "None" ) getChildDevices().each{ PostEventToChild( it.deviceNetworkId, "Alarm", "None" ) } } // Meant to check if the child device's temperature is outside the configured min/max Alert values def CheckAlarms( Child ){ ChildTemp = getChildDevice( Child ).ReturnState( "temperature" ) ChildAlarm = getChildDevice( Child ).ReturnState( "Alarm" ) ChildAlertMin = getChildDevice( Child ).ReturnState( "AlertMinTemp" ) ChildAlertMax = getChildDevice( Child ).ReturnState( "AlertMaxTemp" ) ParentAlarms = state.Alarms Channel = Child.split( "CH" )[ 1 ] // Below Minimum Alarm if( ChildAlertMin != null ){ if( ChildTemp != null ){ if( ChildTemp <= ChildAlertMin ){ if( ( ChildAlarm == null ) || ( ChildAlarm == "None" ) ){ ChildAlarm = "Temperature is below minimum." } else if( !ChildAlarm.contains( "below" ) ){ ChildAlarm = "${ ChildAlarm } & Temperature is below minimum." } if( ( ParentAlarms == null ) || ( ParentAlarms == "None" ) ){ ParentAlarms = "CH${ Channel } temperature is below minumum." } else if( !state.Alarms.contains( "CH${ Channel } temperature is below minumum" ) ){ ParentAlarms = "${ ParentAlarms } & CH${ Channel } temperature is below minumum." } } else { if( ChildAlarm != null ){ if( ChildAlarm.contains( "below" ) ){ ChildAlarm = "None" } } if( ParentAlarms != null ){ if( ParentAlarms.contains( "CH${ Channel } temperature is below minumum." ) ){ if( ParentAlarms.contains( " & CH${ Channel } temperature is below minumum." ) ){ ParentAlarms = "${ ParentAlarms.minus( " & CH${ Channel } temperature is below minumum." ) }" } else if( state.Alarms.contains( "CH${ Channel } temperature is below. & " ) ){ ParentAlarms = "${ ParentAlarms.minus( "CH${ Channel } temperature is below minumum. & " ) }" } else { ParentAlarms = "None" } } } } } } else { if( ChildAlarm != null ){ if( ChildAlarm.contains( "below" ) ){ ChildAlarm = "None" } } if( ParentAlarms != null ){ if( ParentAlarms.contains( "CH${ Channel } temperature is below minumum." ) ){ if( ParentAlarms.contains( " & CH${ Channel } temperature is below minumum." ) ){ ParentAlarms = "${ ParentAlarms.minus( " & CH${ Channel } temperature is below minumum." ) }" } else if( state.Alarms.contains( "CH${ Channel } temperature is below. & " ) ){ ParentAlarms = "${ ParentAlarms.minus( "CH${ Channel } temperature is below minumum. & " ) }" } else { ParentAlarms = "None" } } } } // Above Maximum Alarm if( ChildAlertMax != null ){ if( ChildTemp != null ){ if( ChildTemp >= ChildAlertMax ){ if( ( ChildAlarm == null ) || ( ChildAlarm == "None" ) ){ ChildAlarm = "Temperature is above maximum." } else if( !ChildAlarm.contains( "above" ) ){ ChildAlarm = "${ ChildAlarm } & Temperature is above maximum." } if( ( ParentAlarms == null ) || ( ParentAlarms == "None" ) ){ ParentAlarms = "CH${ Channel } temperature is above maximum." } else if( !state.Alarms.contains( "CH${ Channel } temperature is above" ) ){ ParentAlarms = "${ state.Alarms } & CH${ Channel } temperature is above maximum." } } else { if( ChildAlarm != null ){ if( ChildAlarm.contains( "above" ) ){ PostEventToChild( "${ FireBoardID }_CH${ Channel }", "Alarm", "None" ) ChildAlarm = "None" } } if( ParentAlarms != null ){ if( ParentAlarms.contains( "CH${ Channel } temperature is above maximum." ) ){ if( ParentAlarms.contains( " & CH${ Channel } temperature is above maximum." ) ){ ParentAlarms = "${ state.Alarms.minus( " & CH${ Channel } temperature is above maximum." ) }" } else if( state.Alarms.contains( "CH${ Channel } temperature is above maximum. & " ) ){ ParentAlarms = "${ state.Alarms.minus( "CH${ Channel } temperature is above maximum. & " ) }" } else { ParentAlarms = "None" } } } } } } else { if( ChildAlarm != null ){ if( ChildAlarm.contains( "above" ) ){ PostEventToChild( "${ FireBoardID }_CH${ Channel }", "Alarm", "None" ) ChildAlarm = "None" } } if( ParentAlarms != null ){ if( ParentAlarms.contains( "CH${ Channel } temperature is above maximum." ) ){ if( ParentAlarms.contains( " & CH${ Channel } temperature is above maximum." ) ){ ParentAlarms = "${ state.Alarms.minus( " & CH${ Channel } temperature is above maximum." ) }" } else if( state.Alarms.contains( "CH${ Channel } temperature is above maximum. & " ) ){ ParentAlarms = "${ state.Alarms.minus( "CH${ Channel } temperature is above maximum. & " ) }" } else { ParentAlarms = "None" } } } } if( ChildAlarm == null ){ ChildAlarm = "None" } if( ParentAlarms == null ){ ParentAlarms = "None" } PostEventToChild( Child, "Alarm", ChildAlarm ) ProcessEvent( "Alarms", ParentAlarms ) //Logging( "${ Channel } ChildAlarm = ${ ChildAlarm }", 3 ) //Logging( "ParentAlarms = ${ ParentAlarms }", 3 ) } // Handles receiving the data from various commands def ReceiveData( resp, data ){ switch( resp.getStatus() ){ case 200: def Json = parseJson( resp.data ) ProcessEvent( "Status", "${ data.Method } successful." ) Logging( "${ data.Method } for ${ data.Device } Data: ${ resp.data }", 4 ) switch( data.Method ){ case "ListAllDevices": Json.each{ Logging( "Specific Device data = ${ it }", 4 ) if( it.hardware_id != null ){ def FireBoardID = "${ it.hardware_id }" PostEventToChild( "${ FireBoardID }", "Status", "${ FireBoardID } created." ) def TempType // Temporary variable to hold the temperature type (F or C) the Fireboard is using if( it.degreetype == 1 ){ // 1 = C, 2 = F PostStateToChild( "${ FireBoardID }", "DegreeType", "Celsius" ) TempType = "C" } else { PostStateToChild( "${ FireBoardID }", "DegreeType", "Fahrenheit" ) TempType = "F" } it.each{ switch( it.key ){ case "created": PostStateToChild( "${ FireBoardID }", "Created", "${ it.value }" ) break case "title": getChildDevice( "${ FireBoardID }" ).label = "${ it.value }" break case "last_templog": PostStateToChild( "${ FireBoardID }", "LastTempLog", "${ it.value }" ) break case "fbj_version": PostStateToChild( "${ FireBoardID }", "FireBoardVersionJ", "${ it.value }" ) break case "fbn_version": PostStateToChild( "${ FireBoardID }", "FireBoardVersionN", "${ it.value }" ) break case "fbu_version": PostStateToChild( "${ FireBoardID }", "FireBoardVersionU", "${ it.value }" ) break case "version": PostStateToChild( "${ FireBoardID }", "FireBoardVersion", "${ it.value }" ) break case "channel_count": PostStateToChild( "${ FireBoardID }", "ChannelCount", it.value ) break case "model": PostStateToChild( "${ FireBoardID }", "Model", "${ it.value }" ) break case "active": PostStateToChild( "${ FireBoardID }", "Active", it.value ) break case "uuid": PostStateToChild( "${ FireBoardID }", "UUID", "${ it.value }" ) ProcessEvent( "FireBoardUUID", "${ it.value }" ) break case "id": PostStateToChild( "${ FireBoardID }", "ID", "${ it.value }" ) break case "model_name": PostStateToChild( "${ FireBoardID }", "ModelName", "${ it.value }" ) break case "latest_temps": Logging( "latest_temps = ${ it.value }", 4 ) if( it != null ){ def DegreeTemp = "" def ChannelTemp if( it.value.size() >= 1 ){ for( i = 0; i < it.value.size; i++ ){ ChannelTemp = it.value[ i ].channel if( it.value[ i ].degreetype == 1 ){ DegreeTemp = "C" PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "DegreeType", "Celcius" ) } else { DegreeTemp = "F" PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "DegreeType", "Fahrenheit" ) } PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "temperature", ConvertTemperature( DegreeTemp, it.value[ i ].temp ), "°${ DegreeTemp }" ) CheckAlarms( "${ FireBoardID }_CH${ ChannelTemp }" ) PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "DataAsOf", "${ it.value[ i ].created }" ) } } } break case "channels": def ChannelValue Logging( "Channel data = ${ it.value }", 4 ) if( it != null ){ def DegreeTemp = "" def ChannelTemp if( it.value.size() >= 1 ){ for( i = 0; i < it.value.size; i++ ){ ChannelTemp = it.value[ i ].channel if( it.value[ i ].degreetype == 1 ){ DegreeTemp = "C" PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "DegreeType", "Celcius" ) } else { DegreeTemp = "F" PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "DegreeType", "Fahrenheit" ) } it.value[ i ].each{ switch( it.key ){ case "channel_label": if( ( it.value != null ) && ( it.value != "null" ) ){ getChildDevice( "${ FireBoardID }_CH${ ChannelTemp }" ).label = "${ it.value }" } break case "current_temp": case "temp": if( ( it.value != null ) && ( it.value != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "temperature", ConvertTemperature( DegreeTemp, it.value ), "°${ DegreeTemp }" ) CheckAlarms( "${ FireBoardID }_CH${ ChannelTemp }" ) } else { PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "temperature", null ) } CheckAlarms( "${ FireBoardID }_CH${ ChannelTemp }" ) break case "created": if( ( it.value != null ) && ( it.value != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "DataAsOf", "${ it.value }" ) } break case "range_min_temp": if( ( it.value != null ) && ( it.value != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "RangeMinTemp", ConvertTemperature( DegreeTemp, it.value ), "°${ DegreeTemp }" ) } else { PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "RangeMinTemp", null ) } break case "range_max_temp": if( ( it.value != null ) && ( it.value != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "RangeMaxTemp", ConvertTemperature( DegreeTemp, it.value ), "°${ DegreeTemp }" ) } else { PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "RangeMaxTemp", null ) } break case "range_average_temp": if( ( it.value != null ) && ( it.value != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "RangeAverageTemp", ConvertTemperature( DegreeTemp, it.value ), "°${ DegreeTemp }" ) } else { PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "RangeAverageTemp", null ) } break case "enabled": if( ( it.value != null ) && ( it.value != "null" ) ){ PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "Enabled", it.value ) } break case "sessionid": if( ( it.value != null ) && ( it.value != "null" ) ){ PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "SessionID", it.value ) } break case "id": if( ( it.value != null ) && ( it.value != "null" ) ){ PostStateToChild( "${ FireBoardID }_CH${ ChannelTemp }", "ID", it.value ) } break case "alerts": if( it.value != null ){ if( it.value[ 0 ] != null ){ if( ( it.value[ 0 ].temp_min != null ) && ( it.value[ 0 ].temp_min != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "AlertMinTemp", ConvertTemperature( DegreeTemp, it.value[ 0 ].temp_min ), "°${ DegreeTemp }" ) CheckAlarms( "${ FireBoardID }_CH${ ChannelTemp }" ) } else { PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "AlertMinTemp", null ) } if( ( it.value[ 0 ].temp_max != null ) && ( it.value[ 0 ].temp_max != "null" ) ){ PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "AlertMaxTemp", ConvertTemperature( DegreeTemp, it.value[ 0 ].temp_max ), "°${ DegreeTemp }" ) CheckAlarms( "${ FireBoardID }_CH${ ChannelTemp }" ) } else { PostEventToChild( "${ FireBoardID }_CH${ ChannelTemp }", "AlertMaxTemp", null ) } } } break // Things to ignore or already handled case "color_hex": case "color_id": case "channel": // Already dealt with case "degreetype": // Already dealt with case "state": case "last_templog": break default: Logging( "Unhandled channels data for ${ FireBoardID }_CH${ ChannelTemp }: ${ it }", 3 ) break } } } } } break case "last_battery_reading": if( it.value != null ){ PostEventToChild( "${ FireBoardID }", "battery", Math.round( it.value * 100 ), "%" ) } break case "device_log": Logging( "device_log data = ${ it.value }", 4 ) it.value.each{ switch( it.key ){ case "vBattPer": PostEventToChild( "${ FireBoardID }", "battery", Math.round( it.value * 100 ), "%" ) break case "uptime": PostEventToChild( "${ FireBoardID }", "Uptime", it.value ) break case "cpuUsage": PostEventToChild( "${ FireBoardID }", "CPUUsage", it.value ) break case "diskUsage": PostEventToChild( "${ FireBoardID }", "DiskUsage", it.value ) break case "memUsage": PostEventToChild( "${ FireBoardID }", "MemoryUsage", it.value ) break case "model": PostEventToChild( "${ FireBoardID }", "Model", it.value ) break case "onboardTemp": PostEventToChild( "${ FireBoardID }", "OnboardTemp", ConvertTemperature( TempType, it.value ), "°${ TempType }" ) break // Data points to ignore at this time case "auxPort": case "band": case "bleClientMAC": case "bleSignalLevel": case "boardID": case "commercialMode": case "contrast": case "date": case "deviceID": case "drivesettings": case "frequency": case "internalIP": case "linkquality": case "macAP": case "macNIC": case "mode": case "nightmode": case "publicIP": case "signallevel": case "ssid": case "tempFilter": case "timeZoneBT": case "txpower": case "vBatt": case "vBattPerRaw": case "version": case "versionEspHal": case "versionImage": case "versionJava": case "versionNode": case "versionUtils": case "fwd": break default: Logging( "Unhandled device_log data ${ it.key } = ${ it.value }", 3 ) break } } break // Cases to ignore including data from because irrelevant or already handled otherwise case "hardware_id": case "degreetype": case "last_drivelog": case "probe_config": case "auto_session": break // Log any case not seen before default: Logging( "Unhandled ListAllDevices data for ${ FireBoardID }: ${ it.key } = ${ it.value }", 3 ) break } } } } break case "GetTemperature": def Count = 0 Json.each{ def Channel = Json[ Count ].channel it.each{ switch( it.key ){ case "created": PostStateToChild( "${ data.Device }_CH${ Channel }", "Created", "${ it.value }" ) break case "temp": if( Json[ Count ].degreetype == 1 ){ // 1 = C, 2 = F PostStateToChild( "${ data.Device }_CH${ Channel }", "DegreeType", "Celcius" ) PostEventToChild( "${ data.Device }_CH${ Channel }", "temperature", ConvertTemperature( "C", it.value ), "°C" ) } else { PostStateToChild( "${ data.Device }_CH${ Channel }", "DegreeType", "Fahrenheit" ) PostEventToChild( "${ data.Device }_CH${ Channel }", "temperature", ConvertTemperature( "F", it.value ), "°F" ) } CheckAlarms( "${ data.Device }_CH${ Channel }" ) break case "latest_temps": Count = 0 if( it != null ){ it.each{ def TempChannel = Json.latest_temps[ Count ].channel if( TempChannel != null ){ switch( it.key ){ case "created": PostEventToChild( "${ data.Device }_CH${ TempChannel }", "DataAsOf", "${ it.value }" ) break case "temp": if( Json.latest_temps[ Count ].degreetype == 1 ){ // 1 = C, 2 = F PostStateToChild( "${ data.Device }_CH${ TempChannel }", "DegreeType", "Celcius" ) PostEventToChild( "${ data.Device }_CH${ TempChannel }", "temperature", ConvertTemperature( "C", it.value ), "°C" ) } else { PostStateToChild( "${ data.Device }_CH${ TempChannel }", "DegreeType", "Fahrenheit" ) PostEventToChild( "${ data.Device }_CH${ TempChannel }", "temperature", ConvertTemperature( "F", it.value ), "°F" ) } CheckAlarms( "${ data.Device }_CH${ TempChannel }" ) break // Cases to ignore including data from because irrelevant or already handled otherwise case "channel": case "degreetype": break // Log any case not seen before default: Logging( "Unhandled latest_temps data for ${ data.Device }_CH${ TempChannel }: ${ it }", 3 ) break } } Count += 1 } } break // Cases to ignore including data from because irrelevant or already handled otherwise case "channel": case "degreetype": break // Log any case not seen before default: Logging( "Unhandled data for ${ data.Device }_CH${ Json[ Count ].channel }: ${ it.key } = ${ it.value }", 3 ) break } } Count += 1 } break case "GetDrivelog": if( ( resp.data != "{}" ) && ( Json != null ) ){ Logging( "Drivelog = ${ resp.data }", 3 ) } else { Logging( "Drivelog empty, likely no activity in last minute", 4 ) } break case "ListSessions": // Have captured samples... but there does not seem to be any useful data within it to process Logging( "ListSessions for ${ data.Session } Data: ${ resp.data }", 4 ) break default: Logging( "Response for ${ data.Method } Data: ${ resp.data }", 3 ) break } break case 400: // Bad request Logging( "Bad request for ${ data.Method }", 5 ) break case 401: // Unauthorized Logging( "Unauthorized for ${ data.Method }", 5 ) break case 404: // Bad request Logging( "Page not found for ${ data.Method }", 5 ) break case 408: // Bad request Logging( "Request timeout for ${ data.Method }", 5 ) break case 503: // Service Unavailable Logging( "Fireboard API unavailable for ${ data.Method }", 5 ) break case 504: // Gateway timeout error Logging( "Server-side timeout for ${ data.Method }", 5 ) break default: Logging( "Unknown response to ${ data.Method } = ${ resp.getStatus() }", 5 ) break } } // installed is called when the device is installed def installed(){ Logging( "Installed", 2 ) } // initialize is called when the device is initialized def initialize(){ Logging( "Initialized", 2 ) } // uninstalling device so make sure to clean up children void uninstalled() { // Delete all children getChildDevices().each{ deleteChildDevice( it.deviceNetworkId ) } unschedule() Logging( "Uninstalled", 2 ) } // parse appears to be one of those "special" methods for when data is returned def parse( String description ){ Logging( "Parse = ${ description }", 3 ) } // Used to convert epoch values to text dates def String ConvertEpochToDate( Number Epoch ){ def date = use( groovy.time.TimeCategory ) { new Date( 0 ) + Epoch.seconds } 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 } } // Post data to child device def PostEventToChild( Child, Variable, Value, Unit = null ){ if( Child != null ){ if( getChildDevice( "${ Child }" ) == null ){ addChild( "${ Child }" ) } if( getChildDevice( "${ Child }" ) != null ){ if( Unit != null ){ getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }" ) Logging( "${ Child } Event: ${ Variable } = ${ Value }${ Unit }", 3 ) } else { getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value ) Logging( "${ Child } Event: ${ Variable } = ${ Value }", 3 ) } } 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 ){ addChild( "${ Child }" ) } if( getChildDevice( "${ Child }" ) != null ){ Logging( "${ Child } State: ${ Variable } = ${ Value }", 4 ) getChildDevice( "${ Child }" ).ProcessState( "${ Variable }", Value ) } else { Logging( "Failure to add ${ ChildParent } 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 ){ try{ Logging( "addChild(${ DNI })", 3 ) addChildDevice( "FireBoardChild", DNI, [ name: "${ DNI }" ] ) } catch( Exception e ){ def Temp = e as String if( Temp.contains( "not found" ) ){ Logging( "FireBoardChild driver is not loaded, this is required for child devices.", 5 ) } else { Logging( "addChild Error, likely child already exists: ${ Temp }", 5 ) } } } // Tile Template method based on @mircolino's HTML Template method private void UpdateTile( String val ){ if( settings.TileTemplate ){ // Create special compund/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 ){ ProcessState( "${ 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 ) } } // Process data to check against current state value and update if it has changed def ProcessState( 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", version() ) 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 = version().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", 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" ) } 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" ) } 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" ) } 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 } } }