Button Controller with Fan Support

Thought I would share a modification I made to the SmartThings button controller app.

I have added a “Fan” section that allows the user to cycle through fan modes by pushing the button. Using this in conjunction with the “hold” feature and I have programmed my Aeon Minimote to do the following:

  1. Push Button: Turn on Fan to Low
  2. Push Button Again: Change Fan Speed to Medium
  3. Push Button Again: Change Fan Speed to High
  4. Push Button Again: Turn off Fan
  5. Any time I Hold the Button, Fan turns Off/On
/**
 *  Copyright 2015
 *
 *  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.
 *
 *	Button Controller
 *
 *	Author: Thomas Howard based on SmartThings Original Button Controller
 *	Date: 2015-11-09
 */
definition(
    name: "Smarter Button Controller",
    namespace: "Button Controller",
    author: "Thomas Howard",
    description: "Control devices with buttons like the Aeon Labs Minimote; Based on SmartThings button controller.",
    category: "Convenience",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png"
)
preferences {
	page(name: "selectButton")
	page(name: "configureButton1")
	page(name: "configureButton2")
	page(name: "configureButton3")
	page(name: "configureButton4")
	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 selectButton() {
	dynamicPage(name: "selectButton", title: "First, select your button device", nextPage: "configureButton1", uninstall: configured()) {
		section {
			input "buttonDevice", "capability.button", title: "Button", multiple: false, required: true
		}
		section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
			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 configureButton1() {
	dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button",
		nextPage: "configureButton2", uninstall: configured(), getButtonSections(1))
}
def configureButton2() {
	dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here",
		nextPage: "configureButton3", uninstall: configured(), getButtonSections(2))
}
def configureButton3() {
	dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here",
		nextPage: "configureButton4", uninstall: configured(), getButtonSections(3))
}
def configureButton4() {
	dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here",
		install: true, uninstall: true, getButtonSections(4))
}
def getButtonSections(buttonNumber) {
	return {
		section("Lights") {
			input "lights_${buttonNumber}_pushed", "capability.switch", title: "Pushed", multiple: true, required: false
			input "lights_${buttonNumber}_held", "capability.switch", title: "Held", multiple: true, required: false
		}
        section("Fans") {
        	input "fans_${buttonNumber}_pushed", "capability.switchLevel", title: "Pushed", multiple: true, required: false
			input "fans_${buttonNumber}_held", "capability.switchLevel", title: "Held", multiple: true, required: false
        }
		section("Locks") {
			input "locks_${buttonNumber}_pushed", "capability.lock", title: "Pushed", multiple: true, required: false
			input "locks_${buttonNumber}_held", "capability.lock", title: "Held", multiple: true, required: false
		}
		section("Sonos") {
			input "sonos_${buttonNumber}_pushed", "capability.musicPlayer", title: "Pushed", multiple: true, required: false
			input "sonos_${buttonNumber}_held", "capability.musicPlayer", title: "Held", multiple: true, required: false
		}
		section("Modes") {
			input "mode_${buttonNumber}_pushed", "mode", title: "Pushed", required: false
			input "mode_${buttonNumber}_held", "mode", title: "Held", required: false
		}
		def phrases = location.helloHome?.getPhrases()*.label
		if (phrases) {
			section("Hello Home Actions") {
				log.trace phrases
				input "phrase_${buttonNumber}_pushed", "enum", title: "Pushed", required: false, options: phrases
				input "phrase_${buttonNumber}_held", "enum", title: "Held", required: false, options: phrases
			}
		}
        section("Sirens") {
            input "sirens_${buttonNumber}_pushed","capability.alarm" ,title: "Pushed", multiple: true, required: false
            input "sirens_${buttonNumber}_held", "capability.alarm", title: "Held", multiple: true, required: false
        }
		section("Custom Message") {
			input "textMessage_${buttonNumber}", "text", title: "Message", required: false
		}
        section("Push Notifications") {
            input "notifications_${buttonNumber}_pushed","bool" ,title: "Pushed", required: false, defaultValue: false
            input "notifications_${buttonNumber}_held", "bool", title: "Held", required: false, defaultValue: false
        }
        section("Sms Notifications") {
            input "phone_${buttonNumber}_pushed","phone" ,title: "Pushed", required: false
            input "phone_${buttonNumber}_held", "phone", title: "Held", required: false
        }
	}
}
def installed() {
	initialize()
}
def updated() {
	unsubscribe()
	initialize()
}
def initialize() {
	subscribe(buttonDevice, "button", buttonEvent)
}
def configured() {
	return buttonDevice || buttonConfigured(1) || buttonConfigured(2) || buttonConfigured(3) || buttonConfigured(4)
}
def buttonConfigured(idx) {
	return settings["lights_$idx_pushed"] ||
        settings["fans_$idx_pushed"] ||
		settings["locks_$idx_pushed"] ||
		settings["sonos_$idx_pushed"] ||
		settings["mode_$idx_pushed"] ||
        settings["notifications_$idx_pushed"] ||
        settings["sirens_$idx_pushed"] ||
        settings["notifications_$idx_pushed"]   ||
        settings["phone_$idx_pushed"]
}
def buttonEvent(evt){
	if(allOk) {
		def buttonNumber = evt.data // why doesn't jsonData work? always returning [:]
		def value = evt.value
		log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)"
		log.debug "button: $buttonNumber, value: $value"
		def recentEvents = buttonDevice.eventsSince(new Date(now() - 3000)).findAll{it.value == evt.value && it.data == evt.data}
		log.debug "Found ${recentEvents.size()?:0} events in past 3 seconds"
		if(recentEvents.size <= 1){
			switch(buttonNumber) {
				case ~/.*1.*/:
					executeHandlers(1, value)
					break
				case ~/.*2.*/:
					executeHandlers(2, value)
					break
				case ~/.*3.*/:
					executeHandlers(3, value)
					break
				case ~/.*4.*/:
					executeHandlers(4, value)
					break
			}
		} else {
			log.debug "Found recent button press events for $buttonNumber with value $value"
		}
	}
}
def executeHandlers(buttonNumber, value) {
	log.debug "executeHandlers: $buttonNumber - $value"
	def lights = find('lights', buttonNumber, value)
	if (lights != null) toggle(lights)
    def fans=find('fans', buttonNumber, value)
    if (fans != null) changeFanSpeed(fans)
	def locks = find('locks', buttonNumber, value)
	if (locks != null) toggle(locks)
	def sonos = find('sonos', buttonNumber, value)
	if (sonos != null) toggle(sonos)
	def mode = find('mode', buttonNumber, value)
	if (mode != null) changeMode(mode)
	def phrase = find('phrase', buttonNumber, value)
	if (phrase != null) location.helloHome.execute(phrase)
	def textMessage = findMsg('textMessage', buttonNumber)
	def notifications = find('notifications', buttonNumber, value)
	if (notifications?.toBoolean()) sendPush(textMessage ?: "Button $buttonNumber was pressed" )
	def phone = find('phone', buttonNumber, value)
	if (phone != null) sendSms(phone, textMessage ?:"Button $buttonNumber was pressed")
    def sirens = find('sirens', buttonNumber, value)
    if (sirens != null) toggle(sirens)
}
def changeFanSpeed(fans){
	def i;
    def fan;
    def level;
    for (i=0; i<fans.size(); i++){
    	fan = fans.get(i);
        if (fan.currentValue("switch").contains("off")) level = 0;
        else level = fan.currentValue("level").toInteger();
        log.debug "CURRENT FAN LEVEL $level"
        if (level == 0) level = 30;
        else if (level <= 32) level = 60;
        else if (level <=65) level = 100;
        else 
        	level = 0;
        log.debug "SETTING LEVEL $level"
        fan.setLevel(level);
    }
}
def find(type, buttonNumber, value) {
	def preferenceName = type + "_" + buttonNumber + "_" + value
	def pref = settings[preferenceName]
	if(pref != null) {
		log.debug "Found: $pref for $preferenceName"
	}
	return pref
}
def findMsg(type, buttonNumber) {
	def preferenceName = type + "_" + buttonNumber
	def pref = settings[preferenceName]
	if(pref != null) {
		log.debug "Found: $pref for $preferenceName"
	}
	return pref
}
def toggle(devices) {
	log.debug "toggle: $devices = ${devices*.currentValue('switch')}"
	if (devices*.currentValue('switch').contains('on')) {
		devices.off()
	}
	else if (devices*.currentValue('switch').contains('off')) {
		devices.on()
	}
	else if (devices*.currentValue('lock').contains('locked')) {
		devices.unlock()
	}
	else if (devices*.currentValue('alarm').contains('off')) {
        devices.siren()
    }
	else {
		devices.on()
	}
}
def changeMode(mode) {
	log.debug "changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes"
	if (location.mode != mode && location.modes?.find { it.name == mode }) {
		setLocationMode(mode)
	}
}
// execution filter methods
private getAllOk() {
	modeOk && daysOk && timeOk
}
private getModeOk() {
	def result = !modes || modes.contains(location.mode)
	log.trace "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)
	}
	log.trace "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
	}
	log.trace "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) ? false : true
}
private timeIntervalLabel() {
	(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}
2 Likes

Thoward,

What kind of fan or device are you controlling with the button controller? What is the controlle talking to to switch the fan speeds?

Thanks,

Bill

I am using a GE fan switch

I like the concept, but my minimotes are so unreliable I rarely use them anymore. I often have to press multiple times to get the command to happen. My favorite is when I think it needs another push and I push again only to make my lights go on/off/on/off/on/off :smile:

Sorry to hear about the issues. I have no issues at all with the minimotes. Of course I have 5 Zwave switches within line-of-sight and no more than 10 feet between devices until I get to ST. That being said, I a virtually worthless Zigbee remote since I only have 3 Zigbee devices in the house. It only works within range of the router.

Range isn’t an issue I have an average of 5 z-wave devices per room and my house is square and my hub is center of the house. The “button” controller app actually worked almost flawless it’s the new smart lighting I’m using now that I believe to be the issue. Like I said, I barely use the remotes or even switches anymore. I have a “echo” in my main two spaces and between the two of them cover almost the entire house.

Understood. I actually don’t use the Minimote as much as my wife does. Most of my interaction with the system is using Homebridge with Homekit. Siri does a fairly good job for me :slight_smile:

1 Like

Great work. I have my minimote next to the bed to turn the fan on and off. Thanks for making something that I can use to control the speed.

Funny thing is, this app already exists, called Button Controller+. Here on Github:

3 Likes