/**
* Copyright 2020 Markus Liljergren
*
* 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.
*/
/* Default imports */
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
metadata {
definition (name: "Tasmota - RF/IR Smoke Detector (Child)", namespace: "tasmota", author: "Markus Liljergren", importURL: "https://raw.githubusercontent.com/markus-li/Hubitat/release/drivers/expanded/tasmota-rf-ir-smoke-detector-child-expanded.groovy") {
capability "SmokeDetector"
capability "Sensor"
// Attributes used for Learning Mode
attribute "status", "string"
attribute "actionSeen", "number"
attribute "actionData", "json_object"
// Commands used for Learning Mode
command("actionStartLearning")
command("actionSave")
command("actionPauseUnpauseLearning")
command "clear"
command "tested"
}
preferences {
// Default Preferences
input(name: "runReset", description: "For details and guidance, see the release thread in the Hubitat Forum. For settings marked as ADVANCED, make sure you understand what they do before activating them. If settings are not reflected on the device, press the Configure button in this driver. Also make sure all settings really are saved and correct.
Type RESET and then press 'Save Preferences' to DELETE all Preferences and return to DEFAULTS.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph")
generate_preferences(configuration_model_debug())
generateLearningPreferences()
}
}
def getDeviceInfoByName(infoName) {
// DO NOT EDIT: This is generated from the metadata!
// TODO: Figure out how to get this from Hubitat instead of generating this?
deviceInfo = ['name': 'Tasmota - RF/IR Smoke Detector (Child)', 'namespace': 'tasmota', 'author': 'Markus Liljergren', 'importURL': 'https://raw.githubusercontent.com/markus-li/Hubitat/release/drivers/expanded/tasmota-rf-ir-smoke-detector-child-expanded.groovy']
return(deviceInfo[infoName])
}
// Methods for displaying the correct Learning Preferences and returning the
// current Action Name
def generateLearningPreferences() {
input(name: "learningMode", type: "bool", title: "Learning Mode", description: 'Activate this to enter Learning Mode. DO NOT ACTIVATE THIS once you have learned the codes of a device, they will have to be re-learned!', displayDuringSetup: false, required: false)
if(learningMode) {
input(name: "actionCurrentName", type: "enum", title: "Action To Learn",
description: "Select which Action to save to in Learn Mode.",
options: ["Clear", "Tested", "Detected"], defaultValue: "Detected",
displayDuringSetup: false, required: false)
input(name: "learningModeAdvanced", type: "bool", title: "Advanced Learning Mode",
description: 'Activate this to enable setting Advanced settings. Normally this is NOT needed, be careful!',
defaultValue: false, displayDuringSetup: false, required: false)
if(learningModeAdvanced) {
input(name: "actionCodeSetManual", type: "string", title: "Set Action Code Manually",
description: 'WARNING! For ADVANCED users only!',
displayDuringSetup: false, required: false)
input(name: "actionResetAll", type: "bool", title: "RESET all Saved Actions",
description: 'WARNING! This will DELETE all saved/learned Actions!',
defaultValue: false, displayDuringSetup: false, required: false)
}
}
}
def getCurrentActionName() {
if(!binding.hasVariable('actionCurrentName') ||
(binding.hasVariable('actionCurrentName') && actionCurrentName == null)) {
logging("Doesn't have the action name defined... Using Detected!", 1)
actionName = "Detected"
} else {
actionName = actionCurrentName
}
return(actionName)
}
/* These functions are unique to each driver */
void detected() {
logging("detected()", 1)
sendEvent(name: "smoke", value: "detected", isStateChange: true)
}
void clear() {
logging("clear()", 1)
sendEvent(name: "smoke", value: "clear", isStateChange: true)
}
void tested() {
logging("tested()", 1)
sendEvent(name: "smoke", value: "tested", isStateChange: true)
}
// These are called when Action occurs, called from actionHandler()
def detectedAction() {
logging("detectedAction()", 1)
detected()
}
def clearAction() {
logging("clearAction()", 1)
clear()
}
def testedAction() {
logging("testedAction()", 1)
tested()
}
/* Helper functions for Code Learning */
def actionStartLearning() {
return(actionStartLearning(true))
}
def actionStartLearning(resetActionData) {
def cmds = []
if(learningMode) {
actionName = getCurrentActionName()
cmds << sendEvent(name: "status", value: "Learning Mode: Learning Action '${actionName}'.")
logging("actionStartLearning", 1)
cmds << sendEvent(name: "actionSeen", value: 0)
if(resetActionData) cmds << sendEvent(name: "actionData", value: JsonOutput.toJson(null))
actionSeen = device.currentValue('actionSeen', true)
actionData = device.currentValue('actionData', true)
logging("actionStartLearning actionData=${actionData}", 1)
} else {
log.warn "Learning Mode not active, can't start Learning!"
}
return cmds
}
def actionPauseUnpauseLearning() {
def cmds = []
// This will pause/unpause Learning, good for Contact sensors for example...
status = device.currentValue('status', true)
if(status == "Learning Mode: Paused") {
actionName = getCurrentActionName()
cmds << sendEvent(name: "status", value: "Learning Mode: Learning Action '${actionName}'.")
} else {
cmds << sendEvent(name: "status", value: "Learning Mode: Paused")
}
return cmds
}
def actionSave() {
def cmds = []
logging("actionSave()", 1)
if(learningMode) {
actionName = getCurrentActionName()
def slurper = new JsonSlurper()
actionData = device.currentValue('actionData', true)
if(actionData != null) actionData = slurper.parseText(actionData)
if(actionData && actionData != "Saved") {
frequentData = null
maxActionNumSeen = 0
actionData.each {
logging("it=${it}", 1)
if(it.containsKey('data')) {
if(it.containsKey('seen') && it['seen'] >= maxActionNumSeen) {
maxActionNumSeen = it['seen']
frequentData = it['data']
}
}
}
if(frequentData != null) {
logging("actionSave() saving this data: '${frequentData}'", 100)
//cmds << device.clearSetting('actionCodeDefault')
//cmds << device.removeSetting('actionCodeDefault')
//cmds << device.updateSetting('actionCodeDefault', frequentData)
state.actions = state.actions ?: [:]
state.actions[actionName] = frequentData
cmds << sendEvent(name: "actionSeen", value: 0)
cmds << sendEvent(name: "actionData", value: JsonOutput.toJson('Saved'))
cmds << sendEvent(name: "status", value: "Learning Mode: Saved Action Code for Action '${actionName}'. Refresh the page to see it in State Variables.")
} else {
log.warn "No Action codes found in actionData!"
cmds << sendEvent(name: "status", value: "Learning Mode: FAILED to save Action Code for Action '${actionName}'. See the log.")
}
} else {
log.warn "No Action codes found!"
if(actionData && actionData == "Saved") {
// Do nothing...
} else {
cmds << sendEvent(name: "status", value: "Learning Mode: FAILED to save Action Code for Action '${actionName}'. See the log.")
}
}
} else {
log.warn "Learning Mode not active, can't save Action Code!"
}
return cmds
}
def actionLearn(data) {
def cmds = []
status = device.currentValue('status', true)
if(status == "Learning Mode: Paused") return cmds
actionName = getCurrentActionName()
// Can't do this inside a mutex lock, but just don't press so quickly and it will be ok...
def slurper = new JsonSlurper()
actionSeen = device.currentValue('actionSeen', true)
actionData = device.currentValue('actionData', true)
if(actionData != null) actionData = slurper.parseText(actionData)
if(actionSeen == null || actionSeen == 'null') {
actionSeen = 0
}
if(actionData == null || actionData == 'null' ||
actionData == 'Saved' || actionData == '"Saved"' ||
actionData == 'N/A' || actionData == '"N/A"') {
actionData = []
}
logging("actionSeen=${actionSeen}", 1)
logging("actionData=${actionData}", 1)
// All is same for all types, no need to have special cases...
//if(data.type == 'parsed_portisch') {
found = false
actionData.each {
logging("it=${it}", 1)
if(it.containsKey('data') && data['Data'] == it['data']) {
found = true
it['seen'] = it['seen'] + 1
}
if(it.containsKey('seen') && actionSeen < it['seen']) actionSeen = it['seen']
}
if (!found) {
actionData.add([seen: 1, data: data.Data])
if (actionSeen < 1) actionSeen = 1
}
cmds << sendEvent(name: "status", value: "Learning Mode: Learning Action '${actionName}'. The most frequent Action seen ${actionSeen} time(s)!")
cmds << sendEvent(name: "actionSeen", value: actionSeen)
cmds << sendEvent(name: "actionData", value: JsonOutput.toJson(actionData))
if (state.containsKey("events")) {
state.remove("events")
}
return cmds
}
def actionHandler(data) {
def cmds = []
logging("actionHandler(data='${data}')", 1)
actionName = getCurrentActionName()
if(data && data.containsKey('Data') && state.actions) {
// && data['Data'] == state.actions[actionName]
currentData = data['Data']
state.actions.each {
if (it.value == currentData) {
logging('Button pushed: ${it.value)', 1)
"${it.key[0].toLowerCase() + it.key.substring(1)}Action"()
}
}
}
return cmds
}
def parseParentData(parentData) {
def cmds = []
//logging("parseParentData(parentData=${parentData})", 100)
if (parentData.containsKey("type")) {
if(parentData.type == 'parsed_portisch' ||
parentData.type == 'raw_portisch' ||
parentData.type == 'rflink') {
logging("${parentData.type}=${parentData}", 100)
if(learningMode) {
cmds << actionLearn(parentData)
} else {
cmds << actionHandler(parentData)
}
} else {
log.error("Unknown Format=${parentData}")
}
} else {
log.error("Unknown parentData=${parentData}")
}
return cmds
}
void updated() {
logging('Inside updated()...', 1)
if(!learningMode) {
sendEvent(name: "actionSeen", value: 0)
sendEvent(name: "actionData", value: JsonOutput.toJson('N/A'))
sendEvent(name: "status", value: "Action Mode")
} else {
//sendEvent(name: "actionSeen", value: 0)
//sendEvent(name: "actionData", value: JsonOutput.toJson(null))
//sendEvent(name: "status", value: "Learning Mode")
actionStartLearning(false) // Do NOT reset actionData
if(learningModeAdvanced) {
if(actionResetAll) {
log.warn "ALL saved Actions have been DELETED!"
state.actions = [:]
device.clearSetting('actionResetAll')
device.removeSetting('actionResetAll')
}
if(actionCodeSetManual && actionCodeSetManual != "") {
actionName = getCurrentActionName()
state.actions = state.actions ?: [:]
state.actions[actionName] = actionCodeSetManual
sendEvent(name: "actionSeen", value: 0)
sendEvent(name: "actionData", value: JsonOutput.toJson('Saved'))
sendEvent(name: "status", value: "Learning Mode: Saved Action Code for Action '${actionName}'. Refresh the page to see it in State Variables.")
device.clearSetting('actionCodeSetManual')
device.removeSetting('actionCodeSetManual')
}
}
}
}
def calculateB0(inputStr, repeats) {
// This calculates the B0 value from the B1 for use with the Sonoff RF Bridge
logging('inputStr: ' + inputStr, 0)
inputStr = inputStr.replace(' ', '')
//logging('inputStr.substring(4,6): ' + inputStr.substring(4,6), 0)
numBuckets = Integer.parseInt(inputStr.substring(4,6), 16)
buckets = []
logging('numBuckets: ' + numBuckets.toString(), 0)
outAux = String.format(' %02X ', numBuckets.toInteger())
outAux = outAux + String.format(' %02X ', repeats.toInteger())
logging('outAux1: ' + outAux, 0)
j = 0
for(i in (0..numBuckets-1)){
outAux = outAux + inputStr.substring(6+i*4,10+i*4) + " "
j = i
}
logging('outAux2: ' + outAux, 0)
outAux = outAux + inputStr.substring(10+j*4, inputStr.length()-2)
logging('outAux3: ' + outAux, 0)
dataStr = outAux.replace(' ', '')
outAux = outAux + ' 55'
length = (dataStr.length() / 2).toInteger()
outAux = "AA B0 " + String.format(' %02X ', length.toInteger()) + outAux
logging('outAux4: ' + outAux, 0)
logging('outAux: ' + outAux.replace(' ', ''), 10)
return(outAux)
}
/* Default functions go here */
private def getDriverVersion() {
logging("getDriverVersion()", 50)
def cmds = []
comment = ""
if(comment != "") state.comment = comment
sendEvent(name: "driverVersion", value: "v0.9.3 for Tasmota 7.x/8.x (Hubitat version)")
return cmds
}
/* Helper functions included in all drivers */
def installed() {
logging("installed()", 50)
configure()
try {
// In case we have some more to run specific to this driver
installedAdditional()
} catch (MissingMethodException e) {
// ignore
}
}
/*
initialize
Purpose: initialize the driver
Note: also called from updated() in most drivers
*/
void initialize()
{
logging("initialize()", 50)
unschedule()
// disable debug logs after 30 min, unless override is in place
if (logLevel != "0") {
if(runReset != "DEBUG") {
log.warn "Debug logging will be disabled in 30 minutes..."
} else {
log.warn "Debug logging will NOT BE AUTOMATICALLY DISABLED!"
}
runIn(1800, logsOff)
}
}
def configure() {
logging("configure()", 50)
def cmds = []
cmds = update_needed_settings()
try {
// Run the getDriverVersion() command
newCmds = getDriverVersion()
if (newCmds != null && newCmds != []) cmds = cmds + newCmds
} catch (MissingMethodException e) {
// ignore
}
if (cmds != []) cmds
}
def generate_preferences(configuration_model)
{
def configuration = new XmlSlurper().parseText(configuration_model)
configuration.Value.each
{
if(it.@hidden != "true" && it.@disabled != "true"){
switch(it.@type)
{
case ["number"]:
input "${it.@index}", "number",
title:"${it.@label}\n" + "${it.Help}",
description: "${it.@description}",
range: "${it.@min}..${it.@max}",
defaultValue: "${it.@value}",
displayDuringSetup: "${it.@displayDuringSetup}"
break
case "list":
def items = []
it.Item.each { items << ["${it.@value}":"${it.@label}"] }
input "${it.@index}", "enum",
title:"${it.@label}\n" + "${it.Help}",
description: "${it.@description}",
defaultValue: "${it.@value}",
displayDuringSetup: "${it.@displayDuringSetup}",
options: items
break
case ["password"]:
input "${it.@index}", "password",
title:"${it.@label}\n" + "${it.Help}",
description: "${it.@description}",
displayDuringSetup: "${it.@displayDuringSetup}"
break
case "decimal":
input "${it.@index}", "decimal",
title:"${it.@label}\n" + "${it.Help}",
description: "${it.@description}",
range: "${it.@min}..${it.@max}",
defaultValue: "${it.@value}",
displayDuringSetup: "${it.@displayDuringSetup}"
break
case "boolean":
input "${it.@index}", "boolean",
title:"${it.@label}\n" + "${it.Help}",
description: "${it.@description}",
defaultValue: "${it.@value}",
displayDuringSetup: "${it.@displayDuringSetup}"
break
}
}
}
}
def update_current_properties(cmd)
{
def currentProperties = state.currentProperties ?: [:]
currentProperties."${cmd.name}" = cmd.value
if (state.settings?."${cmd.name}" != null)
{
if (state.settings."${cmd.name}".toString() == cmd.value)
{
sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: false)
}
else
{
sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: false)
}
}
state.currentProperties = currentProperties
}
/*
logsOff
Purpose: automatically disable debug logging after 30 mins.
Note: scheduled in Initialize()
*/
void logsOff(){
if(runReset != "DEBUG") {
log.warn "Debug logging disabled..."
// Setting logLevel to "0" doesn't seem to work, it disables logs, but does not update the UI...
//device.updateSetting("logLevel",[value:"0",type:"string"])
//app.updateSetting("logLevel",[value:"0",type:"list"])
// Not sure which ones are needed, so doing all... This works!
device.clearSetting("logLevel")
device.removeSetting("logLevel")
state.settings.remove("logLevel")
} else {
log.warn "OVERRIDE: Disabling Debug logging will not execute with 'DEBUG' set..."
if (logLevel != "0") runIn(1800, logsOff)
}
}
private def getFilteredDeviceDriverName() {
deviceDriverName = getDeviceInfoByName('name')
if(deviceDriverName.toLowerCase().endsWith(' (parent)')) {
deviceDriverName = deviceDriverName.substring(0, deviceDriverName.length()-9)
}
return deviceDriverName
}
private def getFilteredDeviceDisplayName() {
device_display_name = device.displayName.replace(' (parent)', '').replace(' (Parent)', '')
return device_display_name
}
def configuration_model_debug()
{
'''
'''
}
/* Logging function included in all drivers */
private def logging(message, level) {
if (logLevel != "0"){
switch (logLevel) {
case "-1": // Insanely verbose
if (level >= 0 && level <= 100)
log.debug "$message"
break
case "1": // Very verbose
if (level >= 1 && level < 99 || level == 100)
log.debug "$message"
break
case "10": // A little less
if (level >= 10 && level < 99 || level == 100)
log.debug "$message"
break
case "50": // Rather chatty
if (level >= 50 )
log.debug "$message"
break
case "99": // Only parsing reports
if (level >= 99 )
log.debug "$message"
break
case "100": // Only special debug messages, eg IR and RF codes
if (level == 100 )
log.debug "$message"
break
}
}
}