How to create a filter change reminder app (for AC/heating unit)

I have been reading over the documentation for apps and though I am thinking I want to write an app for the filter change reminder I'm not completely sure. I don't see how to provide a button for the user.

I have a similar "app" on my Vera hub, but it works through the dashboard, has a button and changes an icon when the filter needs to be changed. I don't know how to do these things via an app on Hubitat.

Here is the basic functionality:

-Let the user set the fan run hours before the filter needs to be changed. Show the user the current fan run hours and provide the user a reset button.
-When the fan goes on (subscribe to thermostat events), take note of the time. When the fan goes off, calculate the run time and display the run time in hours.
-When the run time equals or exceeds the user defined fan run hours, let the user know the filter needs to be changed.
-When the user presses the reset button, the filter has been changed and the fan run hours is set to 0.

Thoughts? I would like to keep it somewhat easy to start with :slight_smile:

You could use a physical button. We don't have a UI method to do this, other than if the app were opened, and a "switch" turned on. There is an example of this in Hubitat Safety Monitor, where the Cancel Alerts switch is used for a similar purpose. What happens in that UI, is that if you turn on the switch, it does cancel alerts, and immediately turns off again,

We will look into adding a "button" ui in apps as well.

I did create the filter change reminder app, and currently have an input with Yes and No for resetting the fan run hours. It’s clunky but it works. I’m just afraid that since I can’t change the user inputs in code, that next time the app is opened, I might forget to change the value back to No and mess things up :wink:

How do you do a switch in an app? And how do you make it reset immediately? Sounds like that would be better than the Yes/No dropdown. The ST documentation is not very good :slight_smile:

input "reset", "bool", title: "Reset the filters?", submitOnChange: true
if(reset) {
    resetCounters()  // or whatever you need to do
    app.updateSetting("reset", [value: "false", type: "bool"])
}
1 Like

Perfect! I haven’t seen that anywhere else and I think it will do exactly what I want. Thank you so much for posting that :smile:

update setting does not seem to work. here is the code and output:

if (hT == _Hubitat && hideAdvanced == null)     {
    ifDebug("hideAdvanced: |$hideAdvanced|")
    app.updateSetting("hideAdvanced", [value: "true", type: "bool"]);
    ifDebug("hideAdvanced: |$hideAdvanced|")
}


[app:189] 2018-06-22 21:57:47.987:debug KT room hideAdvanced: |null|
[app:189] 2018-06-22 21:57:47.976:debug KT room hideAdvanced: |null|

thank you.

Was the setting ever created?

no. first time after the setting has been defined.

on the topic ... this has a different side effect when the setting is first defined but not saved yet ... if the setting has a default value it will show the default value in the UI however in the code the setting will be undefined ie null.

this results in the peculiarity that the UI shows the default value ... however if any UI element is conditionally displayed based on that setting it will not match.

the above snippet was meant to fix that.

Are you using submitOnChange: true?

yes.

anything else i can try here? app setting looks odd with the setting showing a value of true when its actually false ... causing a bunch of UI issues.

The problem is that the setting doesn't exist when you call updateSetting(). Are are you "defining" the setting? There is no way to define a setting without an input. Has the input not been evaluated when this call is made?

think the problem is even when the input is evaluated its not persisted till the app settings are saved causing this discrepancy. so it shows the default value but returns null when evaluated in the app before its saved.

checked on ST also ... dont see the same issue there.

Yes, there is a difference between ST and Hubitat with respect to how this case is handled. This could fairly be called a bug. We are aware of it, and will address it.

What should happen is what happens in ST. When the app is loaded, inputs with default values should have their settings created with the default value set. Right now, that doesn't happen until Done is hit or some other submitOnChange event happens to that page.

1 Like

:white_check_mark: :slight_smile:

thank you.

Hi, did you ever write this app for filter reminders. I'm getting rid of an old thermostat which had that functionality built in and needs to be replaced. Might be good for other routine maintenance items around the house.

You will need to create two hub variables and a virtual switch before you install the app. I display these 3 things on my thermostat dashboard, flip the switch to off when you change the filter.

/*
 *  Thermostat Filter Change Reminder
 */

definition(name: "Thermostat Filter Change Reminder",
           namespace: "hubitat",
           author: "dagrider",
           description: "Filter change reminder",
           category: "",
           iconUrl: "",
           iconX2Url: "")

preferences {
    section("<h2>Settings</h2>") {
        input "theThermostat", "capability.thermostat", title: "Choose thermostat"
        input "fanRunMaxHrs", "number", title: "Hours to run before filter change"
        input "HVMaxRunHrs", "capability.actuator", title: "Hub Variable for max run hours"
        input "HVRunHrs", "capability.actuator", title: "Hub Variable for current run hours"
        input "filterChangeSwitch", "capability.switch", title: "Choose filter change switch (turn off after filter changed, to reset run hours)"
        //input "smsNumber", "phone", title: "Phone number to send SMS message when filter needs to be changed", required: false
        input "thePlayer", "capability.musicPlayer", title: "Choose music players to announce", multiple: true, required: false
        input "volume", "number", title: "Volume", description: "0-100", required: false, defaultValue: "50"
		input "theSpeakers", "capability.speechSynthesis", title: "Choose speakers to announce", multiple: true, required: false
		input "notification", "capability.notification", title: "Send notification to these Notification Devices:", multiple: true, required: false
        //input "resetFanRunHrs", "bool", title: "Has filter been changed? (ON=Yes, OFF=No)"
    }
    
    section("<h2>Status</h2>") {
        paragraph "Fan run time (hours): ${state.fanRunHrs}"
        paragraph "Does filter need to be changed? ${state.filterChange}"
    }
    
    section ("<h2>Logging</h2>") {
        input name: "debugLogEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}

def installed() {
    logDebug("Installed with settings: ${settings}")
    initValues()
    initialize()
}

def updated() {
    logDebug("Updated with settings: ${settings}")
    unsubscribe()
    initialize()
}

def initialize() {
    subscribe(theThermostat, "thermostatOperatingState", thermostatHandler)
    subscribe(filterChangeSwitch, "switch.off", switchHandler)
    subscribe(HVMaxRunHrs, "variable", maxRunHrsHandler)
    updateMaxRunHrsHV(settings.fanRunMaxHrs)
}

def resetValues() {
    sendEvent(name: "reset values", value: "reset values", descriptionText: "reset values")
    setFanRunTimeHR(0)
    setFanRunTimeMS(0)
    setFilterChange("No")
  
    //app.updateSetting("resetFanRunHrs", [value: "false", type: "bool"])
}

def initValues() {
    sendEvent(name: "init values", value: "init values", descriptionText: "init values")
    setFanRunTimeHR(0)
    setFanRunTimeMS(0)
    setFanOnTime(0)
    setFanOffTime(0)
    setFilterChange("No")
}

def maxRunHrsHandler(evt) {
    logDebug("Max run hrs handler evt: $evt, variable: $HVMaxRunHrs.currentVariable")
    app.updateSetting("fanRunMaxHrs", [value: HVMaxRunHrs.currentVariable, type: "number"])
}

def switchHandler(evt) {
    sendEvent(name: "switch off", value: "${filterChangeSwitch.displayName}", descriptionText: "Filter change switch off (filter was changed)")
    resetValues()
}

def thermostatHandler(evt) {
    logDebug("thermostatHandler event value ${evt.value}")
    sendEvent(name: "thermostatOperatingState", value: "event ${evt.value}", descriptionText: "thermostatOperatingState event")
    
    if (evt.value == "idle") {
        if (state.fanOffTime == 0 && state.fanOnTime != 0) {
            setFanOffTime(now())
            def elapsed = state.fanOffTime - state.fanOnTime
            setFanRunTimeMS(state.fanRunMS + elapsed)
            setFanOnTime(0)

            if (state.fanRunMS >= settings.fanRunMaxHrs * 3600000) {
                setFilterChange("Yes")
				def msg = "A/C Filter needs to be changed"
                filterChangeSwitch.on()
                //if (smsNumber) sendSMS(smsNumber, msg)
                if (thePlayer) thePlayer.playTextAndRestore(msg, volume)
				if (theSpeakers) theSpeakers.speak(msg)
				if (notification) notification.deviceNotification(msg)
            } else {
                setFilterChange("No")
            }

            setFanRunTimeHR(Math.round(state.fanRunMS / 3600000 * 100) / 100)
        }
    } else {
        if (state.fanOnTime == 0) {
            setFanOnTime(now())
            setFanOffTime(0)
        }
    }
}

private updateMaxRunHrsHV(val) {
    //app.updateSetting("resetFanRunHrs", [value: "false", type: "bool"]) fanRunMaxHrs
    logDebug("updateMaxRunHrsHV to $val")
    HVMaxRunHrs.setVariable(val)
}

private updateRunHrsHV(val) {
    logDebug("updateRunHrsHV to $val")
    HVRunHrs.setVariable(val)
}

private setFanOnTime(val) {
    state.fanOnTime = val
    logDebug("Fan on time: $state.fanOnTime")
    sendEvent(name: "Fan on time", value: "${state.fanOnTime}", descriptionText: "Fan on time")
}

private setFanOffTime(val) {
    state.fanOffTime = val
    logDebug("Fan off time: $state.fanOffTime")
    sendEvent(name: "Fan off time", value: "${state.fanOffTime}", descriptionText: "Fan off time")
}

private setFanRunTimeMS(val) {
    state.fanRunMS = val
    logDebug("Fan run time (MS): $state.fanRunMS")
    sendEvent(name: "Fan run time (MS)", value: "${state.fanRunMS}", descriptionText: "Fan run time (MS)")
}

private setFanRunTimeHR(val) {
    state.fanRunHrs = val
    updateRunHrsHV(val)
    logDebug("Fan run time (HR): $state.fanRunHrs")
    sendEvent(name: "Fan run time (HR)", value: "${state.fanRunHrs}", descriptionText: "Fan run time (HR)")
}

private setFilterChange(val) {
    state.filterChange = val
    logDebug("Filter change? $state.filterChange")
    sendEvent(name: "Filter change?", value: "${state.filterChange}", descriptionText: "Is filter change required?")
}

private logDebug(msg) {
	if (debugLogEnable) log.debug msg
}
1 Like

I have a similar app that I wrote and it doesn't require any external buttons or variables. I just use a button input in the app to handle resetting the time.

/**
 *  Switch On-time Tracker
 *
 *  Copyright 2021 Peter Miller
 */

definition(
    name: "Switch On-Time Tracker",
    namespace: "hyposphere.net",
   author: "Peter Miller",
    description: "Track how long a switch is turned on.",
    category: "My Apps",
    iconUrl: "",
    iconX2Url: "",
    importUrl: "https://raw.githubusercontent.com/pfmiller0/Hubitat/main/Switch%20On-time%20Tracker.groovy"
)


preferences {
	page(name: "mainPage")
  	page(name: "resetPage")
}

def mainPage() {
	dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) {
		section() {
			input "isPaused", "bool", title: "Pause app", defaultValue: false
		}
		
		if (state.notifyTime) {
			section("Time on: <b>" + printTime(getOnTime()) + "</b>") { }
			section("Notify after: <b>" + printTime(Math.round(state.notifyTime)) + "</b>") { }
		
			section("Reset") {
	    		if (! state.showReset) {
					input name: "btnReset", type: "button", title: "Reset counter?"
				} else {
					paragraph "Are you sure you want to reset the counter?"
					input name: "btnCancel", type: "button", title: "No", width: 6
					input name: "btnConfirm", type: "button", title: "<span style='color:red'>Yes</span>", width: 6
				}	
			}
		}	

		section("Settings") {
			input "switchDev", "capability.switch", title: "Switch", multiple: false
			input "notifyDev", "capability.notification", title: "Notification device", multiple: false, required: false
		}
	}
}

void installed() {
	initialize()
	logDebug "Installed: $settings"
}

void updated() {
	unsubscribe()
	initialize()
	logDebug "Updated: $settings"
	app.updateLabel(switchDev.getLabel() + " On-Time Tracker")
}

void initialize() {
	if (isPaused == false) {
		state.totalOnTime = state.totalOnTime ? state.totalOnTime : 0.0
		state.notifyTime = 129600 // 3 months in minutes
		state.turnOnTime = state.turnOnTime ? state.turnOnTime : now()
		
		subscribe(switchDev, "switch", switchChanged)
	}
}

void switchChanged(evt) {
	//log.debug evt.device.getId()
	
	if (switchDev.latestValue("switch") == "on") {
		state.turnOnTime = now() 
	} else {
		state.totalOnTime += (now() - state.turnOnTime)/(1000*60) 
	}
	
	if (state.totalOnTime > state.notifyTime) {
		notifyDev.deviceNotification "Time to clean the air filter!"
	}	
}

Integer getOnTime() {
	if (switchDev.latestValue("switch") == "on") {
		return Math.round(state.totalOnTime + (now() - state.turnOnTime)/(1000*60))
	} else {
		return Math.round(state.totalOnTime)
	}
}

String printTime(Long mins) {
	String out=""
	String td = '<td style="border:1px solid silver;">'
	String tdc = '</td>'
	
	Integer days
	Integer hours

	days = mins.intdiv(1440)
	hours = (mins - (days * 1440)).intdiv(60)
	mins = (mins - (hours * 60) - (days * 1440))
	
	out="${days} days, ${hours} hours, ${mins} minutes"
	
	return out
}

void appButtonHandler(String btn) {
	switch (btn) {
	case "btnReset":
		state.showReset = true
		break
	case "btnCancel":
		state.showReset = false
		break
	case "btnConfirm":
		state.totalOnTime = 0.0
		if (switchDev.latestValue("switch") == "on") {
			state.turnOnTime = now()
		}
		state.showReset = false
		break
	default:
		log.warn "Unhandled button press: $btn"
	}
}
1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.