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