/*
* Hubitat Import URL: https://raw.githubusercontent.com/stephack/Hubitat/master/apps/Advanced%20Button%20Controller%20(ABC)/ABC_Child_Creator.groovy
*
*
*
* ABC Child Creator for Advanced Button Controller
*
* Author: SmartThings, modified by Bruce Ravenel, Dale Coffing, Stephan Hackett
*
* 01/09/22 - added support for RMv5 rules. Older versions are considered "legacy". Requires Hub v2.2.9 f/w or higher.
* - Thanks to @bertabcd1234 for guidance with the undocumented RMUtils v5 api.
*
* 09/03/21 - added new garageDoorControl capability with distinct open/close actions
* - original Garage Door section is now Legacy Garage Door
*
* 08/12/21 - added optional input to disable log.info statements to reduce Hubitat Log traffic (Dan Ogorchock)
*
* 06/17/21 - added support for stopPositionChange() and startPositionChange(direction) to comply with windowShade capability
*
* 10/02/20 - split Ramp section in 2. Original is now called Ramp(Auto Stop on Release) and there is also a new option called Ramp (Manual Stop)
* - added the ability to unlock locks since this was added as an option in built-in apps.
*
* 03/01/20 - added ability to control windowShade devices under "Shades" menu option)
* - original shade control (doorControl) moved to "Garage Doors/Legacy Shades" menu option
*
* 02/26/20 - Forced initialization on hub restart to avoid delays on "first activation". Thank you @ogiewon for the suggestion.
*
* 11/05/19 - Added previousTrack support for speakers
*
* 10/06/19 - Added Auto as option under Set Fan Speed
*
* 08/14/19 - Send Http Requests (POST or GET - simple form encoded)
*
* 05/18/19 - Speech notifications now allow random messages to be sent (Use ; to separate options)
* - cycleFan modified to no longer use numeric setSpeed values as this may be deprecated by HE for future fan devices
*
* 04/29/19 - fixed small UI bug handling '0' level values
* - updated adjustFans method
*
* 02/19/19 - rules api bug squashed
*
* 02/17/19 - updated Button Description for rules to show Rule name instead of Rule number
* - Button Descriptions will now be surrounded by [] for better visibility
* - Action details are now stored in a state value to allow for better efficiency
*
* 02/10/19 - setColor Level is no longer required (can be left blank)
*
* 02/07/19 - fixed Set Color bug (missing level option)
*
* 01/14/19 - updated logging output to appropriate type (info vs debug)
* - added input to enable/disable debug logging
* - added url to Raw code at the top of the parent/child apps
* (Thanks for the feedback and suggestions @csteeele)
* - update checking code is now done through json file (Thanks to @Cobra for his guidance)
*
* 12/15/18 - updated color scheme to match new HE theme
* - added suppot for Rules API
*
*
* 10/12/18 - adjusted "Set Mode" to comply with mode related updates in firmware 1.1.5
*
*
* 8/01/18 - added Hubitat Safety Monitor Control (created new MODES section for Set Mode and Set HSM)
* added level to setColor()
* added new detail parameter "myDetail.mul" (Mode and HSM set to multiple:false)
* removed section shallHide for sub inputs .... (section will be visible if primary input has a value...sub value no longer checked)
*
*
* 7/03/18 - code cleanup
* Added pictures enhancements and reordered options for better flow
* Corrected default child app label (previously defaulted to "ABC Button Mapping" on first save)
*
*
* 7/01/18 - added Released actions for all control sections
* Pushed/Held/DoubleTapped/Released hidden from Dimmer Ramp section based on devices capabilities
*
* 6/30/18 - adapted fan cycle to be compliant with fanControl capability (removed cycle support for custom driver)
* added ability to set specific fan speed
* added support for ramping (graceful dimming) - switch/bulb needs changeLevel capability and button device needs releaseableButton capability
*
*
* 6/02/18 - added ability to cycle custom Hampton Bay Zigbee Fan Controller
*
*
* 4/21/18 - added support for new Sonos Player devices (play/pause, next, previous, mute/unmute, volumeup/down)
*
*
* 3/28/18 - added option to set color and temp
* test code for custom commands (not yet working)
*
* 2/06/18 - converted code to hubitat format
* removed ability to hide "held options"
* removed hwspecifics section as is no longer applicable
* adjusted device list to look for "capability.pushableButton"
* adjusted buttonDevice subscription (pushed, held, doubleTapped)
* adjusted buttonEvent() to swap "name" and "value" as per new rules
* 2/08/18 - change formatting for Button Config Preview (Blue/Complete color)
* Added Double Tap inputs and edited shallHide() getDescription()
* added code for showDouble() to only display when controller support DT
* removed enableSpec and other Virtual Container Code as this is not supported in Hubitat
*2/12/18
* Updated to new detailsMap and modified Button Config/Preview pages
* hides secondary values if primary not set. When dispayed they are now "required".
*
*2/12/18
* Switched to parent/child config
* removed button pics and descriptive text (not utilized by hubitat)
*
*10/24/18
* added the ability to cycle through Scenes (done using push() command and cycles in alphabetical order only)
* minor GUI updates
*/
import hubitat.helper.RMUtils
def version(){"v0.2.220109"}
definition(
name: "ABC Button Mapping",
namespace: "stephack",
author: "Stephan Hackett",
description: "Assign tasks to your Button Controller Devices",
category: "My Apps",
parent: "stephack:Advanced Button Controller",
iconUrl: "https://cdn.rawgit.com/stephack/ABC/master/resources/images/abc2.png",
iconX2Url: "https://cdn.rawgit.com/stephack/ABC/master/resources/images/abc2.png",
iconX3Url: "https://cdn.rawgit.com/stephack/ABC/master/resources/images/abc2.png",
)
preferences {
page(name: "chooseButton")
page(name: "configButtonsPage")
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def chooseButton() {
state.details=getPrefDetails()
dynamicPage(name: "chooseButton", install: true, uninstall: true) {
section(){
def appHead = "
\n${checkForUpdate()}"
paragraph "
${appHead}
"
}
section(getFormat("header", "${getImage("Device", "45")}"+" Step 1: Select Your Button Device")) {
input "buttonDevice", "capability.pushableButton", title: getFormat("section", "Button Device"), description: "Tap to Select", multiple: false, required: true, submitOnChange: true
}
if(buttonDevice){
state.buttonType = buttonDevice.typeName
if(state.buttonType.contains("Aeon Minimote")) state.buttonType = "Aeon Minimote"
if(logEnable) log.debug "Device Type is now set to: "+state.buttonType
state.buttonCount = manualCount?: buttonDevice.currentValue('numberOfButtons')
section(getFormat("header", "${getImage("Button", "45")}"+" Step 2: Configure Your Buttons")) {
if(state.buttonCount<1) {
paragraph "The selected button device did not report the number of buttons it has. Please specify in the Advanced Config section below."
}
else {
for(i in 1..state.buttonCount){
href "configButtonsPage", title: getFormat("section", "${getImage("Button", "30")}" + " Button ${i}"), state: getDescription(i)!="Tap to configure"? "complete": null, description: getDescription(i), params: [pbutton: i]
}
}
}
}
section(getFormat("header", "${getImage("Custom", "45")}"+" Set Custom Name (Optional)")) {
label title: "Assign a name:", required: false
paragraph getFormat("line")
}
section("Advanced Config:", hideable: true, hidden: hideOptionsSection()) {
input "manualCount", "number", title: "Set/Override # of Buttons?", required: false, description: "Only set if your driver does not report", submitOnChange: true
input "collapseAll", "bool", title: "Collapse Unconfigured Sections?", defaultValue: true
input "logEnable", "bool", title: "Enable Debug Logging?", required: false
input "descTextDisable", "bool", title: "Disable Descriptive Text Logging?", required: false
}
section(title: "Only Execute When:", hideable: true, hidden: hideOptionsSection()) {
def timeLabel = timeIntervalLabel()
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
}
}
}
def configButtonsPage(params) {
if (params.pbutton != null) state.currentButton = params.pbutton.toInteger()
dynamicPage(name: "configButtonsPage", title: "CONFIGURE BUTTON ${state.currentButton}:\n${state.buttonType}", getButtonSections(state.currentButton))
}
def getButtonSections(buttonNumber) {
return {
def myDetail
section(getFormat("header", "${getImage("Switches", "45")}"+" SWITCHES")){}
//state.details=getPrefDetails()
for(i in 1..34) {//Build 1st 34 Button Config Options
myDetail = state.details.find{it.sOrder==i}
//
section(title: myDetail.secLabel, hideable: true, hidden: !(shallHide("${myDetail.id}${buttonNumber}"))) {
if(showPush(myDetail.desc)) input "${myDetail.id}${buttonNumber}_pushed", myDetail.cap, title: "When Pushed", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_pushed")) input "${myDetail.sub}${buttonNumber}_pushed", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_pushed")) input "${myDetail.sub2}${buttonNumber}_pushed", myDetail.sub2Type, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.sub2Opt
if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_pushed")) input "${myDetail.sub3}${buttonNumber}_pushed", myDetail.sub3Type, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.sub3Opt
if(showHeld(myDetail.desc)) input "${myDetail.id}${buttonNumber}_held", myDetail.cap, title: "When Held", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_held")) input "${myDetail.sub}${buttonNumber}_held", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_held")) input "${myDetail.sub2}${buttonNumber}_held", myDetail.sub2Type, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.sub2Opt
if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_held")) input "${myDetail.sub3}${buttonNumber}_held", myDetail.sub3Type, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.sub3Opt
if(showDouble(myDetail.desc)) input "${myDetail.id}${buttonNumber}_doubleTapped", myDetail.cap, title: "When Double Tapped", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_doubleTapped")) input "${myDetail.sub}${buttonNumber}_doubleTapped", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_doubleTapped")) input "${myDetail.sub2}${buttonNumber}_doubleTapped", myDetail.sub2Type, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.sub2Opt
if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_doubleTapped")) input "${myDetail.sub3}${buttonNumber}_doubleTapped", myDetail.sub3Type, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.sub3Opt
if(showRelease(myDetail.desc)) input "${myDetail.id}${buttonNumber}_released", myDetail.cap, title: "When Released", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_released")) input "${myDetail.sub}${buttonNumber}_released", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_released")) input "${myDetail.sub2}${buttonNumber}_released", myDetail.sub2Type, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.sub2Opt
if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_released")) input "${myDetail.sub3}${buttonNumber}_released", myDetail.sub3Type, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.sub3Opt
}
if(i==3) section("\n"+getFormat("header", "${getImage("Dimmers", "45")}"+" DIMMERS")){}
if(i==10) section("\n"+getFormat("header", "${getImage("Color", "45")}"+" COLOR LIGHTS")){}
if(i==12) section("\n"+getFormat("header", "${getImage("Speakers", "45")}"+" SPEAKERS")){}
if(i==19) section("\n"+getFormat("header", "${getImage("Fans", "45")}"+" FANS")){}
if(i==22) section("\n"+getFormat("header", "${getImage("Mode", "45")}"+" MODES")){}
if(i==24) section("\n"+getFormat("header", "${getImage("Rule", "45")}"+" RULE CONTROL")){}
if(i==26) section("\n"+getFormat("header", "${getImage("Other", "45")}"+" OTHER")){}
}
section(getFormat("section", "Notifications (SMS):"), hideable:true , hidden: !shallHide("notifications_${buttonNumber}")) {
input "notifications_${buttonNumber}_pushed", "text", title: "Message To Send When Pushed:", description: "Enter message to send", required: false, submitOnChange: collapseAll
input "phone_${buttonNumber}_pushed","phone" ,title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
if(showHeld()) {
paragraph getFormat("line")
input "notifications_${buttonNumber}_held", "text", title: "Message To Send When Held:", description: "Enter message to send", required: false, submitOnChange: collapseAll
input "phone_${buttonNumber}_held", "phone", title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
}
if(showDouble()) {
paragraph getFormat("line")
input "notifications_${buttonNumber}_doubleTapped", "text", title: "Message To Send When Double Tapped:", description: "Enter message to send", required: false, submitOnChange: collapseAll
input "phone_${buttonNumber}_doubleTapped", "phone", title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
}
if(showRelease()) {
paragraph getFormat("line")
input "notifications_${buttonNumber}_released", "text", title: "Message To Send When Released:", description: "Enter message to send", required: false, submitOnChange: collapseAll
input "phone_${buttonNumber}_released", "phone", title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
}
}
}
}
def getImage(type, mySize) {
def loc = "
"
if(type == "Button") return "${loc}Button.png height=${mySize} width=${mySize}> "
if(type == "Switches") return "${loc}Switches.png height=${mySize} width=${mySize}> "
if(type == "Color") return "${loc}Color.png height=${mySize} width=${mySize}> "
if(type == "Dimmers") return "${loc}Dimmers.png height=${mySize} width=${mySize}> "
if(type == "Speakers") return "${loc}Speakers.png height=${mySize} width=${mySize}> "
if(type == "Fans") return "${loc}Fans.png height=${mySize} width=${mySize}> "
if(type == "HSM") return "${loc}Mode.png height=${mySize} width=${mySize}> "
if(type == "Mode") return "${loc}Mode.png height=${mySize} width=${mySize}> "
if(type == "Other") return "${loc}Other.png height=${mySize} width=${mySize}> "
if(type == "Custom") return "${loc}Custom.png height=${mySize} width=${mySize}> "
if(type == "Locks") return "${loc}Locks.png height=30 width=30> "
if(type == "Sirens") return "${loc}Sirens.png height=30 width=30> "
if(type == "Scenes") return "${loc}Scenes.png height=30 width=30> "
if(type == "Shades") return "${loc}Shades.png height=30 width=30> "
if(type == "SMS") return "${loc}SMS.png height=30 width=30> "
if(type == "Speech") return "${loc}Audio.png height=30 width=30> "
if(type == "Rule") return "${loc}Rule.png height=${mySize} width=${mySize}> "
}
def getFormat(type, myText=""){
if(type == "section") return "${myText}
"
if(type == "command") return "${myText}
"
if(type == "header") return "${myText}
"
if(type == "line") return "\n
"
}
def shallHide(myFeature) {
if(collapseAll) return (settings["${myFeature}_pushed"]||settings["${myFeature}_held"]||settings["${myFeature}_doubleTapped"]||settings["${myFeature}_released"]||settings["${myFeature}"])
return true
}
def isReq(myFeature) {
(settings[myFeature])? true : false
}
def showPush(desc) {
if(buttonDevice.hasCapability("PushableButton")){ //is device pushable?
if(desc.contains("Ramp(Auto Stop)")){
if(buttonDevice.hasCapability("HoldableButton")) return false //if this is the Ramp section and device is also Holdable, then hide Pushed option
}
return true
}
return false
}
def showHeld(desc) {
return buttonDevice.hasCapability("HoldableButton")
}
def showDouble(desc) {
if(desc && desc.contains("Ramp(Auto Stop)")) return false //remove DoubleTapped option when setting smooth dimming button/devices
return buttonDevice.hasCapability("DoubleTapableButton")
}
def showRelease(desc) {
if(desc && desc.contains("Ramp(Auto Stop)")) return false //remove On Release option when setting smooth dimming button/devices
return buttonDevice.hasCapability("ReleasableButton")
}
def getDescription(dNumber) {
def descript = ""
if(!(settings.find{it.key.contains("_${dNumber}_")})) return "Tap to configure"
if(settings.find{it.key.contains("_${dNumber}_pushed")}) descript = "\nPUSHED:"+getDescDetails(dNumber,"_pushed")+"\n"
if(settings.find{it.key.contains("_${dNumber}_held")}) descript = descript+"\nHELD:"+getDescDetails(dNumber,"_held")+"\n"
if(settings.find{it.key.contains("_${dNumber}_doubleTapped")}) descript = descript+"\nTAPx2:"+getDescDetails(dNumber,"_doubleTapped")+"\n"
if(settings.find{it.key.contains("_${dNumber}_released")}) descript = descript+"\nRELEASED:"+getDescDetails(dNumber,"_released")+"\n"
return descript
}
def getDescDetails(bNum, type){
def numType=bNum+type
def preferenceNames = settings.findAll{it.key.contains("_${numType}")}.sort() //get all configured settings that: match button# and type, AND are not false
if(!preferenceNames){
return " **Not Configured** "
}
else {
def formattedPage =""
preferenceNames.each {eachPref->
def prefDetail = state.details.find{eachPref.key.contains(it.id)} //gets decription of action being performed(eg Turn On)
def prefDevice //name of device the action is being performed on (eg Bedroom Fan)
if(prefDetail.sub == "valRule"){
prefDevice = " : " + getRuleName(eachPref.value) //extracts rules name (instead if number) for button description
}
else if(prefDetail.sub == "valv5Rule"){
prefDevice = " : " + getRuleName5(eachPref.value) //extracts rules name (instead if number) for button description
}
else {
prefDevice = " : ${eachPref.value}"
}
def thisSub = settings[prefDetail.sub + numType]
def prefSubValue = thisSub != null? thisSub:"(!Missing!)"
def sub2Value = settings[prefDetail.sub2 + numType]
def sub3Value = settings[prefDetail.sub3 + numType]
def sub2Initial = prefDetail.s2Initial
def sub3Initial = prefDetail.s3Initial
if(sub2Initial) prefSubValue += "${sub2Initial}"
if(sub2Value) prefSubValue += "${sub2Value}"
if(sub3Initial) prefSubValue += "${sub3Initial}"
if(sub3Value) prefSubValue += "${sub3Value}"
if(prefDetail.type=="normal") formattedPage += "\n- ${prefDetail.desc}${prefDevice}"
if(prefDetail.type=="hasSub") formattedPage += "\n- ${prefDetail.desc}${prefSubValue}${prefDevice}"
if(prefDetail.type=="bool") formattedPage += "\n- ${prefDetail.desc}"
}
return formattedPage
}
}
def getRules(){
rules = RMUtils.getRuleList()
//converts rules list to easily parsed format and stores in state.rules for easy access
state.rules=[:]
rules.each{rule->
rule.each{pair->
state.rules[pair.key]=pair.value
}
}
////////////////////////////////////////////////////
return rules
}
def getRules5(){
rules = RMUtils.getRuleList('5.0')
//converts rules list to easily parsed format and stores in state.rules for easy access
state.rules5=[:]
rules.each{rule->
rule.each{pair->
state.rules5[pair.key]=pair.value
}
}
////////////////////////////////////////////////////
return rules
}
def getRuleName(num){ //allows button descriptions for RuleAPI controls to show Rule Name instead of Rule Number
getRules()
def holder=[]
num.each{ruleNum->
holder << state.rules.find{it.key==ruleNum.toInteger()}.value
}
return holder
}
def getRuleName5(num){ //allows button descriptions for RuleAPI controls to show Rule Name instead of Rule Number
getRules5()
def holder=[]
num.each{ruleNum->
holder << state.rules5.find{it.key==ruleNum.toInteger()}.value
}
return holder
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
if(logEnable) log.debug "INITIALIZED with settings: ${settings}"
if(logEnable) log.debug app.label
if(!app.label || app.label == "default")app.updateLabel(defaultLabel())
subscribe(location, "systemStart", hubRestartHandler)
subscribe(buttonDevice, "pushed", buttonEvent)
subscribe(buttonDevice, "held", buttonEvent)
subscribe(buttonDevice, "doubleTapped", buttonEvent)
subscribe(buttonDevice, "released", buttonEvent)
state.lastshadesUp = true
state.details=getPrefDetails()
}
def defaultLabel() {
return "${buttonDevice} Mapping"
}
def getPrefDetails(){
def detailMappings =
[[id:'lightOn_', sOrder:1, desc:'Turn On ', comm:turnOn, type:"normal", secLabel: getFormat("section", "Turn On"), cap: "capability.switch", mul: true],
[id:"lightOff_", sOrder:2, desc:'Turn Off', comm:turnOff, type:"normal", secLabel: getFormat("section", "Turn Off"), cap: "capability.switch", mul: true],
[id:'lights_', sOrder:3, desc:'Toggle On/Off', comm:toggle, type:"normal", secLabel: getFormat("section", "Toggle On/Off"), cap: "capability.switch", mul: true],
[id:"lightDim_", sOrder:4, desc:'Dim to ', comm:turnDim, sub:"valLight", subType:"number", type:"hasSub", secLabel: getFormat("section", "On to Level - Group 1"), cap: "capability.switchLevel", sTitle: "Bright Level", sDesc:"0 to 100%", mul: true],
[id:"lightD2m_", sOrder:5, desc:'Dim to ', comm:turnDim, sub:"valLight2", subType:"number", type:"hasSub", secLabel: getFormat("section", "On to Level - Group 2"), cap: "capability.switchLevel", sTitle: "Bright Level", sDesc:"0 to 100%", mul: true],
[id:'dimPlus_', sOrder:6, desc:'Brightness +', comm:levelUp, sub:"valDimP", subType:"number", type:"hasSub", secLabel: getFormat("section", "Increase Level By"), cap: "capability.switchLevel", sTitle: "Increase by", sDesc:"0 to 15", mul: true],
[id:'dimMinus_', sOrder:7, desc:'Brightness -', comm:levelDown, sub:"valDimM", subType:"number", type:"hasSub", secLabel: getFormat("section", "Decrease Level By"), cap: "capability.switchLevel", sTitle: "Decrease by", sDesc:"0 to 15", mul: true],
[id:'lightsDT_', sOrder:8, desc:'Toggle Off/Dim to ', comm:dimToggle, sub:"valDT", subType:"number", type:"hasSub", secLabel: getFormat("section", "Toggle OnToLevel/Off"), cap: "capability.switchLevel", sTitle: "Bright Level", sDesc:"0 to 100%", mul: true],
[id:'lightsRamp_', sOrder:9, desc:'Ramp(Auto Stop) ', comm:rampUp, sub:"valDir", subType:"enum", subOpt:['up','down'], type:"hasSub", secLabel: getFormat("section", "Ramp Up/Down (Auto Stop on Release)"), cap: "capability.changeLevel", sTitle: "Ramp Direction (Up/Down)", sDesc:"Up or Down", mul: true],
[id:'lightsRampMan_', sOrder:10, desc:'Ramp(Manual Stop) ', comm:rampMan, sub:"valRman", subType:"enum", subOpt:['up','down','stop'], type:"hasSub", secLabel: getFormat("section", "Ramp Up/Down/Stop (Manual Stop)"), cap: "capability.changeLevel", sTitle: "Ramp Command (Up/Down/Stop)", sDesc:"Up, Down or Stop", mul: true],
[id:'lightColorTemp_', sOrder:11, desc:'Set Light Color Temp to ', comm:colorSetT, sub:"valColorTemp", subType:"number", type:"hasSub", secLabel: getFormat("section", "Set Temperature"), cap: "capability.colorTemperature", sTitle: "Color Temp", sDesc:"2000 to 9000", mul: true],
[id:'lightColor_', sOrder:12, desc:'Set Light Color H:', comm:colorSet, sub:"valHue", sub2:"valSat", sub3:"valLvl", type:"hasSub", subType:"number", sub2Type:"number", sub3Type:"number", secLabel: getFormat("section", "Set Color"), cap: "capability.colorControl", sTitle: "Hue", s2Title: "Saturation", s3Title: "Lvl", sDesc:"0 to 100", s2Desc:"0 to 100", s3Desc:"0 to 100", s2Initial: ", S:", s3Initial: ", L:", mul: true, s3NotReq:true],
[id:"speakerpp_", sOrder:13, desc:'Toggle Play/Pause', comm:speakerplaystate, type:"normal", secLabel: getFormat("section", "Toggle Play/Pause"), cap: "capability.musicPlayer", mul: true],
[id:'speakervu_', sOrder:14, desc:'Volume +', comm:levelUp, sub:"valSpeakU", subType:"number", type:"hasSub", secLabel: getFormat("section", "Increase Volume By"), cap: "capability.musicPlayer", sTitle: "Increase by", sDesc:"0 to 15", mul: true],
[id:"speakervd_", sOrder:15, desc:'Volume -', comm:levelDown, sub:"valSpeakD", subType:"number", type:"hasSub", secLabel: getFormat("section", "Decrease Volume By"), cap: "capability.musicPlayer", sTitle: "Decrease by", sDesc:"0 to 15", mul: true],
[id:'speakernt_', sOrder:16, desc:'Next Track', comm:speakernexttrack, type:"normal", secLabel: getFormat("section", "Go to Next Track"), cap: "capability.musicPlayer", mul: true],
[id:'speakerpt_', sOrder:17, desc:'Previous Track', comm:speakerprevioustrack, type:"normal", secLabel: getFormat("section", "Go to Previous Track"), cap: "capability.musicPlayer", mul: true],
[id:'speakermu_', sOrder:18, desc:'Mute', comm:speakermute, type:"normal", secLabel: getFormat("section", "Speakers Toggle Mute"), cap: "capability.musicPlayer", mul: true],
[id:"musicPreset_", sOrder:19, desc:'Cycle Preset', comm:cyclePlaylist, type:"normal", secLabel: getFormat("section", "Preset to Cycle"), cap: "device.VirtualContainer", mul: true],
[id:'fanSet_', sOrder:20, desc:'Set Fan to ', comm:setFan, sub:"valSpeed", subType:"enum", subOpt:['off','low','medium-low','medium','high','auto'], type:"hasSub", secLabel: getFormat("section", "Set Speed"), cap: "capability.fanControl", sTitle: "Set Speed to", sDesc:"L/ML/M/H/A", mul: true],
[id:"fanCycle_", sOrder:21, desc:'Cycle Fan Speed', comm:cycleFan, type:"normal", secLabel: getFormat("section", "Cycle Speed"), cap: "capability.fanControl", mul: true],
[id:"fanAdjust_", sOrder:22,desc:'Adjust', comm:adjustFan, type:"normal", secLabel: getFormat("section", "Cycle Speed (Legacy)"), cap: "capability.switchLevel", mul: true],
[id:"mode_", sOrder:23, desc:'Set Mode', comm:changeMode, type:"normal", secLabel: getFormat("section", "Set Mode"), cap: "mode", mul: false],
[id:"hsm_", sOrder:24, desc:'Set HSM', comm:setHSM, type:"normal", secLabel: getFormat("section", "Set HSM"), cap: "enum", opt:['armAway','armHome','disarm','armRules','disarmRules','disarmAll','armAll','cancelAlerts'], mul: false],
[id:'rule_', sOrder:25, desc:'Rule To ', comm:ruleExec, sub:"valRule", subType:"enum", subOpt:['Run','Stop','Pause','Resume','Evaluate','Set Boolean True','Set Boolean False'], type:"hasSub", secLabel: getFormat("section", "Legacy Rule and Actions"), cap: "enum", opt: getRules(), sTitle: "Select Action Type", sDesc:"Choose Action", mul: true],
[id:'rule5_', sOrder:26, desc:'Rule v5 To ', comm:ruleExec5, sub:"valv5Rule", subType:"enum", subOpt:['Run','Stop','Pause','Resume','Evaluate','Set Boolean True','Set Boolean False'], type:"hasSub", secLabel: getFormat("section", "Rule (v5) and Actions"), cap: "enum", opt: getRules5(), sTitle: "Select Action Type", sDesc:"Choose Action", mul: true],
[id:"locks_", sOrder:27, desc:'Set Lock: ', comm:setUnlock, sub:"valLock", subType:"enum", subOpt:['lock','unlock'], type:"hasSub", secLabel: getFormat("section", "Locks"), cap: "capability.lock", sTitle: "Select Action Type", sDesc:"Choose Action", mul: true],
[id:'cycleScenes_', sOrder:28, desc:'Cycle', comm:cycle, type:"normal", secLabel: getFormat("section", "Scenes (Cycle)"), cap: "device.SceneActivator", mul: true, isCycle: true],
[id:"shadeAdjust_", sOrder:29, desc:'Adjust', comm:adjustShade, type:"normal", secLabel: getFormat("section", "Legacy Garage Doors/Legacy Shades (Up/Down/Stop)"), cap: "capability.doorControl", mul: true],
[id:'newGarageAdjust_', sOrder:30, desc:'Adjust: ', comm:adjustNewGarage, sub:"valGarageAction", type:"hasSub", subType:"enum", subOpt:['Open','Close'], secLabel: getFormat("section", "Garage Doors (Open/Close)"), cap: "capability.garageDoorControl", sTitle: "Action", sDesc:"Choose Action", mul: true],
[id:'newShadeAdjust_', sOrder:31, desc:'Adjust: ', comm:adjustNewShade, sub:"valShadeAction", sub2:"valSposition", sub3:"valSDirection", type:"hasSub", subType:"enum", sub2Type:"number", sub3Type:"enum", subOpt:['Open','Close', 'Set Position','Start Position Change','Stop Position Change'], sub3Opt:['Open','Close'], secLabel: getFormat("section", "Shades (Open/Close/Position/Start/Stop)"), cap: "capability.windowShade", sTitle: "Action", s2Title:"Position", s3Title:"Direction", sDesc:"", s2Desc:"(0 to 100) *applies to Set Position Only", s3Desc:"*applies to Start Position Change Only", s2Initial: ", Pos:", s3Initial: ", Dir:", mul: true, s2NotReq: true, s3NotReq: true],
[id:'sirens_', sOrder:32, desc:'Toggle', comm:toggle, type:"normal", secLabel: getFormat("section", "Sirens (Toggle)"), cap: "capability.alarm", mul: true],
[id:'httpRequest_', sOrder:33, desc:'Send: ', comm:hRequest, sub:"reqUrl", subType:"text", type:"hasSub", secLabel: getFormat("section", "Send Http Request"), cap: "enum", opt:['POST', 'GET'], sTitle:"HTTP URL", sDesc:"Enter complete url to send", mul: false],
[id:"speechDevice_", sOrder:34, desc:'Send Msg To', comm:speechHandle, type:"normal", secLabel: getFormat("section", "Notifications (Speech):"), sub:"speechTxt", cap: "capability.speechSynthesis", subType:"text", sTitle: "Message To Speak:", sDesc:"Enter message to speak (Random messages: Use ; to separate choices)", mul: true],///set type to normal instead of sub so message text is not displayed
[id:"notifications_", desc:'Send Push Notification', comm:messageHandle, sub:"valNotify", type:"bool"],
[id:"phone_", desc:'Send SMS', comm:smsHandle, sub:"notifications_", type:"normal"],
]
return detailMappings
}
def checkForUpdate(){
def params = [uri: "https://raw.githubusercontent.com/stephack/Hubitat/master/apps/Advanced%20Button%20Controller%20(ABC)/child.json",
contentType: "application/json"]
try {
httpGet(params) { response ->
def results = response.data
def appStatus
if(version() == results.currVersion) {
appStatus = "Child ${version()}
${results.noUpdateImg}"
}
else {
appStatus = "Child ${version()}
${results.updateImg}${results.changeLog}"
log.warn "ABC Child App does not appear to be the latest version: Please update."
}
return appStatus
}
}
catch (e) {
log.error "Error: $e"
}
}
def hubRestartHandler(evt) {
if(logEnable) log.debug "ABC [$app.label]Initialized: Hub Restart"
}
def buttonEvent(evt) {
if(allOk) {
def buttonNumber = evt.value
def pressType = evt.name
if(logEnable) log.debug "$buttonDevice: Button $buttonNumber was $pressType"
//detects if button is used for graceful hold to dim function then calls stopLevelChange()
if(pressType == "released" && settings["lightsRamp_${buttonNumber}_pushed"]){
rampEnd(settings["lightsRamp_${buttonNumber}_pushed"])
}
if(pressType == "released" && settings["lightsRamp_${buttonNumber}_held"]){
rampEnd(settings["lightsRamp_${buttonNumber}_held"])
}
def preferenceNames = settings.findAll{it.key.contains("_${buttonNumber}_${pressType}")}
preferenceNames.each{eachPref->
def prefDetail = state.details?.find{eachPref.key.contains(it.id)} //returns the detail map of id,desc,comm,sub
def PrefSubValue = settings["${prefDetail.sub}${buttonNumber}_${pressType}"] //value of subsetting (eg 100)
def PrefSub2Value = settings["${prefDetail.sub2}${buttonNumber}_${pressType}"] //value of subsetting (eg 100)
def PrefSub3Value = settings["${prefDetail.sub3}${buttonNumber}_${pressType}"] //value of subsetting (eg 100)
if(prefDetail.sub3) "$prefDetail.comm"(eachPref.value,PrefSubValue, PrefSub2Value, PrefSub3Value)
else if(prefDetail.sub2) "$prefDetail.comm"(eachPref.value,PrefSubValue, PrefSub2Value)
else if(prefDetail.sub) "$prefDetail.comm"(eachPref.value,PrefSubValue)
else if(prefDetail.isCycle) "$prefDetail.comm"(eachPref.value, "${eachPref.key}")
else "$prefDetail.comm"(eachPref.value)
}
}
}
def speechHandle(devices, msg){
if (!descTextDisable) log.info "Sending ${msg} to ${devices}"
if(msg.contains(";")) {
def myPool = msg.split(";")
def poolSize = myPool.size()
def randomItem = Math.abs(new Random().nextInt() % poolSize)
devices.speak(myPool[randomItem-1])
}
else{
devices.speak(msg)
}
}
def turnOn(devices) {
if (!descTextDisable) log.info "Turning On: $devices"
devices.on()
}
def turnOff(devices) {
if (!descTextDisable) log.info "Turning Off: $devices"
devices.off()
}
def turnDim(devices, level) {
if (!descTextDisable) log.info "Dimming (to $level): $devices"
devices.setLevel(level)
}
def colorSet(devices,hueVal,satVal,lvlVal) {
if (!descTextDisable) log.info "Setting Color (to H:$hueVal, S:$satVal, L:$lvlVal): $devices"
def myColor = [:]
myColor.hue = hueVal.toInteger()
myColor.saturation = satVal.toInteger()
if(lvlVal) myColor.level = lvlVal.toInteger()
devices.setColor(myColor)//([hue:hueVal,saturation:satVal,level:50])
}
def colorSetT(devices, temp) {
if (!descTextDisable) log.info "Setting Color Temp (to $temp): $devices"
devices.setColorTemperature(temp)
}
def adjustFan(device) {
if (!descTextDisable) log.info "Adjusting: $device"
def currentLevel = device.currentLevel[0]
if(device.currentSwitch[0] == 'off') device.setLevel(15)
else if (currentLevel < 34) device.setLevel(50)
else if (currentLevel < 67) device.setLevel(90)
else device.off()
}
def adjustShade(device) {
if (!descTextDisable) log.info "Shades: $device = ${device.currentMotor} state.lastUP = $state.lastshadesUp"
if(device.currentMotor in ["up","down"]) {
state.lastshadesUp = device.currentMotor == "up"
device.stop()
} else {
state.lastshadesUp ? device.down() : device.up()
state.lastshadesUp = !state.lastshadesUp
}
}
def adjustNewGarage(devices, action){
if (!descTextDisable) log.info "Sending ${action} to: $devices"
if(action=='Open'){devices.open()}
if(action=='Close'){devices.close()}
}
def adjustNewShade(devices, action, position, direction){
if (!descTextDisable) log.info "Sending ${action} to: $devices"
if(action=='Open'){devices.open()}
if(action=='Close'){devices.close()}
if(action=='Set Position'){devices.setPosition(position)}
if(action=='Start Position Change'){devices.startPositionChange(direction)}
if(action=='Stop Position Change'){devices.stopPositionChange()}
}
def setFan(devices, speed){
if (!descTextDisable) log.info "Setting Speed to $speed: $devices"
devices.setSpeed(speed)
}
def speakerplaystate(device) {
if (!descTextDisable) log.info "Toggling Play/Pause: $device"
device.currentStatus.contains('playing')? device.pause() : device.play()
}
def speakernexttrack(device) {
if (!descTextDisable) log.info "Next Track Sent to: $device"
device.nextTrack()
}
def speakerprevioustrack(device) {
if (!descTextDisable) log.info "Previous Track Sent to: $device"
device.previousTrack()
}
def speakermute(device) {
if (!descTextDisable) log.info "Toggling Mute/Unmute: $device"
device.currentMute.contains('unmuted')? device.mute() : device.unmute()
}
def levelUp(device, inclevel) {
if (!descTextDisable) log.info "Incrementing Level (by +$inclevel): $device"
def currentVol = device.currentLevel[0]//device.currentValue('level')[0] //currentlevel return a list...[0] is first item in list ie volume level
def newVol = currentVol + inclevel
device.setLevel(newVol)
if(logEnable) log.debug "Level increased by $inclevel to $newVol"
}
def levelDown(device, declevel) {
if (!descTextDisable) log.info "Decrementing Level (by -$declevel): $device"
def currentVol = device.currentLevel[0]//device.currentValue('level')[0]
def newVol = currentVol - declevel
device.setLevel(newVol)
if(logEnable) log.debug "Level decreased by $declevel to $newVol"
}
def rampUp(devices, dir){
if (!descTextDisable) log.info "Ramping ${dir}: $devices"
devices.startLevelChange(dir)
}
def rampEnd(device){
if (!descTextDisable) log.info "Ending Ramp: $device"
device.stopLevelChange()
}
def rampMan(devices, rCommand){
if(rCommand=='stop'){
if (!descTextDisable) log.info "Ending Ramp: $devices"
devices.stopLevelChange()
}
else{
if (!descTextDisable) log.info "Ramping ${rCommand}: $devices"
devices.startLevelChange(rCommand)
}
}
def setUnlock(devices,action) {
if(action=='unlock'){
if (!descTextDisable) log.info "Unlocking: $devices"
devices.unlock()
}
if(action=='lock'){
if (!descTextDisable) log.info "Locking: $devices"
devices.lock()
}
}
def toggle(devices) {
if (!descTextDisable) log.info "Toggling: $devices"
if (devices*.currentValue('switch').contains('on')) {
devices.off()
}
else if (devices*.currentValue('switch').contains('off')) {
devices.on()
}
else if (devices*.currentValue('alarm').contains('off')) {
devices.siren()
}
else {
devices.on()
}
}
def dimToggle(devices, dimLevel) {
if (!descTextDisable) log.info "Toggling On/Off | Dimming (to $dimLevel): $devices"
if (devices*.currentValue('switch').contains('on')) devices.off()
else devices.setLevel(dimLevel)
}
def runRout(rout){
if (!descTextDisable) log.info "Running: $rout"
location.helloHome.execute(rout)
}
def ruleExec(rules, action){
if (!descTextDisable) log.info "Performing ${action} Action on Rules: ${rules}"
def ruleAction
if(action == "Run") ruleAction = "runRuleAct"
if(action == "Stop") ruleAction = "stopRuleAct"
if(action == "Pause") ruleAction = "pauseRule"
if(action == "Resume") ruleAction = "resumeRule"
if(action == "Evaluate") ruleAction = "runRule"
if(action == "Set Boolean True") ruleAction = "setRuleBooleanTrue"
if(action == "Set Boolean False") ruleAction = "setRuleBooleanFalse"
RMUtils.sendAction(rules, ruleAction, app.label)
}
def ruleExec5(rules, action){
if (!descTextDisable) log.info "Performing ${action} Action on Rules: ${rules}"
def ruleAction
if(action == "Run") ruleAction = "runRuleAct"
if(action == "Stop") ruleAction = "stopRuleAct"
if(action == "Pause") ruleAction = "pauseRule"
if(action == "Resume") ruleAction = "resumeRule"
if(action == "Evaluate") ruleAction = "runRule"
if(action == "Set Boolean True") ruleAction = "setRuleBooleanTrue"
if(action == "Set Boolean False") ruleAction = "setRuleBooleanFalse"
log.info "RM5 test running"
RMUtils.sendAction(rules, ruleAction, app.label,'5.0')
}
def messageHandle(msg, inApp) {
if(inApp==true) {
if (!descTextDisable) log.info "Push notification sent"
sendPush(msg)
}
}
def smsHandle(phone, msg){
if (!descTextDisable) log.info "SMS sent"
sendSms(phone, msg ?:"No custom text entered on: $app.label")
}
def setHSM(hsmMode) {
sendLocationEvent(name: "hsmSetArm", value: hsmMode)
}
def changeMode(mode) {
if (!descTextDisable) log.info "Changing Mode to: $mode"
if (location.mode != mode && location.modes?.find { it.name == mode}) setLocationMode(mode)
}
def cycleFan(devices) { //all fans will sync speeds with first fan in the list
if (!descTextDisable) log.info "Cycling: $devices"
def mySpeed = devices[0].currentSpeed
if(mySpeed == "off") devices.setSpeed("low")
if(mySpeed == "low") devices.setSpeed("medium-low")
if(mySpeed == "medium-low") devices.setSpeed("medium")
if(mySpeed == "medium") devices.setSpeed("high")
if(mySpeed == "high") devices.setSpeed("off")
}
def cycle(devices, devIndex) {
if (!descTextDisable) log.info "Cycling: $devices"
def mySize = devices.size() -1
if(!state."${devIndex}" || state."${devIndex}" > mySize) state."${devIndex}" = 0
devices[state."${devIndex}"].push()
state."${devIndex}" ++
}
def cyclePlaylist(devices){
devices.cycle()
}
def hRequest(reqType, myUrl){
def params = [
uri: myUrl,
contentType: 'application/x-www-form-urlencoded'
]
if(logEnable) log.debug "${reqType} - ${params}"
if(reqType == "POST") asynchttpPost('myPostResponse', params, [type: reqType])
if(reqType == "GET") asynchttpGet('myPostResponse', params, [type: reqType])
}
def myPostResponse(response,data){
if(response.status != 200) {
log.error "HTTP ${data["type"]} Request returned error ${response.status}. Check your URL!"
}
else {
if(logEnable) log.debug "HTTP ${data["type"]} Request sent successfully"
}
}
// execution filter methods
private getAllOk() {
modeOk && daysOk && timeOk
}
private getModeOk() {
def result = !modes || modes.contains(location.mode)
if(logEnable) log.debug "modeOk = $result"
result
}
private getDaysOk() {
def result = true
if (days) {
def df = new java.text.SimpleDateFormat("EEEE")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
def day = df.format(new Date())
result = days.contains(day)
}
if(logEnable) log.debug "daysOk = $result"
result
}
private getTimeOk() {
def result = true
if (starting && ending) {
def currTime = now()
def start = timeToday(starting).time
def stop = timeToday(ending).time
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
}
if(logEnable) log.debug "timeOk = $result"
result
}
private hhmm(time, fmt = "h:mm a") {
def t = timeToday(time, location.timeZone)
def f = new java.text.SimpleDateFormat(fmt)
f.setTimeZone(location.timeZone ?: timeZone(time))
f.format(t)
}
private hideOptionsSection() {
(starting || ending || days || modes || manualCount) ? false : true
}
private timeIntervalLabel() {
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}