This guide helps you through writing your first completely functional custom Hubitat Elevation app. Some basic familiarity with the Hubitat development environment is assumed, and therefore it is recommended that you have read the following guides first:
In this guide, we build on these foundations and build on them to create a typical custom app that performs a common type of automation: turning lights on and off in response to events from motion sensors.
Of course, you don't need to write a a custom app to do this; the built-in Basic Rule or Room Lighting apps can do this for you with a lot less work. The goal here is to demonstrate how to write a complete, usable app, perhaps one you may use as a base for further customization – though it is certainly usable as-is.
We will start with a basic app template as one might take from the App Overview guide:
definition(
name: "Custom Motion Lights",
namespace: "Example",
author: "Hubitat Example",
description: "Turns lights on or off with motion",
category: "Convenience",
iconUrl: "",
iconX2Url: "")
preferences {
}
def installed() {
log.debug "installed()"
}
def updated() {
log.debug "updated()"
}
def uninstalled() {}
You may wish to start by adding this to Apps Code and following along as we discuss additions and changes. You can even install an instance of the app (from Apps > Add User App) and watch how the interface changes as the code changes.
We need to add a way for the user to select the motion sensors and the lights to turn on. Our goal is for the lights to turn on when motion is detected. We will also add a feature to turn the lights off after motion remains inactive for some user-selectable number of minutes. We will create three input
elements to get information from the user needed to perform these actions (remember that an input
must be contained a section
and that a section
normally resides inside a page
, though an explicit page
is optional for single-page apps).
Putting all of this together, we might create preferences
section that looks like this:
preferences {
section("Sensors and Lights") {
input name: "motionSensor", type: "capability.motionSensor", title: "When motion is detected on...", required: true
input name: "lightDevices", type: "capability.switch", title: "Then turn on these lights...", multiple: true
input name: "offAfter", type: "number", title: "And turn off after motion remains inactive for this many minutes:", defaultValue: 1
}
}
The user interface for our app is now complete! Now, we just need to make it actually do something.
Recall that the updated()
method is called when the user selects the Done button in an installed app. The first time an app is opened, it is not yet installed, and in this case, installed()
will be called the first time the user selects Done (unless installOnOpen: true
is specified, which it is not for this app). Ultimately, in both of these methods, we will want to make sure to tell the app to listen for events from the motion sensor. When these events happen, we will then want to perform the appropriate actions (turning on or off the lights).
Listening for an event is done by creating an event subscription using the subscribe()
method. In this case, we would call:
subscribe(motionSensor, "motion", "motionHandler")
The "motionHandler"
parameter value here specifies the name of the callback method, which is a method that must be defined in your app code and will be executed when the app is notified of an event matching your subscription (which in this case will be any event for the motion
attribute on the device selected in the input
with name: "motionSensor"
). This method will be passed a single parameter, an event object, which will contain some information about the event.
For now, we will define a basic handler method to handle the above subscription:
def motionHandler(evt) {
log.debug "motionHandler() called: ${evt.name} ${evt.value}"
}
Eventually, we will want to use this method to turn on or off the lights; for now, we simply log that the method was called, along with some information about the event (the event/attribute name and value, two properties you can access on the event object parameter).
At this point, our whole app may look something like:
definition(
name: "Custom Motion Lights",
namespace: "Example",
author: "Hubitat Example",
description: "Turns lights on or off with motion",
category: "Convenience",
iconUrl: "",
iconX2Url: "")
preferences {
section("Sensors and Lights") {
input name: "motionSensor", type: "capability.motionSensor", title: "When motion is detected on...", required: true
input name: "lightDevices", type: "capability.switch", title: "Then turn on these lights...", multiple: true
input name: "offAfter", type: "number", title: "And turn off after motion remains inactive for this many minutes:", defaultValue: 1
}
}
def installed() {
log.debug "installed()"
updated() // since installed() rather than updated() will run the first time the user selects "Done"
}
def updated() {
log.debug "updated()"
subscribe(motionSensor, "motion", "motionHandler")
}
def uninstalled() {}
def motionHandler(evt) {
log.debug "motionHandler() called: ${evt.name} ${evt.value}"
}
Now, we will add the actual functionality to the app.
Recall that our motionHandler
method will be run, per our call to subscribe
, whenever there is an event on the motion
attribute for the sensor the user selected. The goal of our app is to turn the lights on when motion becomes active and turn them off a certain amount of time after motion becomes inactive.
Let's start by turning the lights on when motion becomes inactive. Recall that lightDevices
is the name we chose for the input
where the user selects the lights (or switches) to turn on. The name
for all inputs becomes available as an app-wide field variable name we can use. So, to turn on the selected devices, all we have to write is:
lightDevices.on()
All device commands are implemented by a Groovy method of the same name, so we can simply call on()
on this object that refers to the device(s), and the device driver will execute the "on" command. (This command takes no parameters, though other commands may require or permit one or more. The device detail page or capability list will show you more for other devices.) So far, our motionHandler
method might look like:
def motionHandler(evt) {
log.debug "motionHandler() called: ${evt.name} ${evt.value}"
if (evt.value == "active") {
lightDevices.on()
}
else {
// TODO
}
}
What about when motion goes inactive? If we didn't care about the delay, we could simply call lightDevices.off()
. But we really want to schedule this command for some time in the future. The Hubitat-provided runIn()
method provides the solution. Per the common methods documentation, the signature is:
void runIn(Long delayInSeconds, String handlerMethod, Map options = null)
This means a call like the following will do what we want:
runIn(offAfter*60, "delayedOffHandler")
We just need to define a delayedOffHandler()
method to do the work, which will be called when this scheduled job is executed:
def delayedOffHandler() {
lightDevices.off()
}
Now, our motionHandler
method and the new handler might look like the following:
def motionHandler(evt) {
log.debug "motionHandler() called: ${evt.name} ${evt.value}"
if (evt.value == "active") {
lightDevices.on()
}
else { // the "motion" attribute can be only either "active" or "inactive", so this is "inactive":
runIn(offAfter*60, "delayedOffHandler")
}
}
def delayedOffHandler() {
lightDevices.off()
}
Our app is almost complete! If you try it once, it might even work as you expect. But what happens if motion becomes active again during the "countdown" while we are waiting for delayedOffHandler()
to execute? The goal we set out to do was turn off the lights if motion stays inactive for the specified time. Right now, we simply turn off the lights the specified number of minutes after motion goes inactive, regardless of what happens in the meantime.
The solution is to cancel this scheduled job when motion becomes active again. This is made possible by the unschedule()
method, which is again documented in the common methods documentation:
void unschedule() // cancels all scheduled jobs
void unschedule(handlerMethod) // cancels jobs scheduled with specified handler method
In our app, we have only a single scheduled job, and thus either call would work. In general, it is probably best to be specific about what you intend to cancel (e.g., in case your app expands in functionality in the future), so we will use:
unschedule("delayedOffHandler")
Now, our motionHandler()
method might look like:
def motionHandler(evt) {
log.debug "motionHandler() called: ${evt.name} ${evt.value}"
if (evt.value == "active") {
lightDevices.on()
unschedule("delayedOffHandler")
}
else {
runIn(offAfter*60, "delayedOffHandler")
}
}
Tip: You can see event subscriptions, schedules, and the values for all inputs (the
settings
map) on the App Status page for any installed app. This can be particularly helpful during development.
Also, what if the user provides 0
as the value for offAfter
, meaning turn off instantly instead of after a delay? In that case, we would want to simply turn the lights off immediately when motion goes inactive instead of scheduling it with runIn()
. In our else
, instead of simply calling runIn()
, we can perform a simple check and either perform the action or schedule it for later. So this:
runIn(offAfter*60, "delayedOffHandler")
becomes:
if (offAfter) {
runIn(offAfter*60, "delayedOffHandler")
}
else {
lightDevices.off()
}
Groovy tip: this snippet demonstrates Groovy truth with
if (offAfter)
. This is a "shortcut" way to evaluate whether an expression is true or false without having to type out a fuller expression like you may be used to in Java or other languages. For this numeric value, the result will befalse
if it is eithernull
(not provided by the user; we didn't make it a required field) or0
. This can be a handy shortcut, as demonstrated above — but also an easy way to introduce bugs if you want to, for example, treat zero, an empty string, an empty List, etc. (any value that may evaluate asfalse
) differently fromnull
.
Finally, what if the user chooses a different motion sensor? Our app will remain subscribed to the first one and add a subscription to the second. This is undesirable — the user no longer wants to use the first sensor with our app. To solve this problem, Hubitat provides an unsubscribe()
method that an app can use to remove a subscription to a certain device, event, or even all subscriptions entirely if called with no parameters.
That last option provides an easy solution for this app (though more complex apps may require something more nuanced): simply remove all subscriptions and then re-create any that are needed, as we are already doing in updated()
. This method may then look like:
def updated() {
log.trace "updated()"
unsubscribe() // adding this line fixes the problem mentioned above
subscribe(motionSensor, "motion", "motionHandler")
}
With all of these modifications, our final app might look like:
definition(
name: "Custom Motion Lights",
namespace: "Example",
author: "Hubitat Example",
description: "Turns lights on or off with motion",
category: "Convenience",
iconUrl: "",
iconX2Url: "")
preferences {
section("Sensors and Lights") {
input name: "motionSensor", type: "capability.motionSensor", title: "When motion is detected on...", required: true
input name: "lightDevices", type: "capability.switch", title: "Then turn on these lights...", multiple: true
input name: "offAfter", type: "number", title: "And turn off after motion remains inactive for this many minutes:", defaultValue: 1
}
}
def installed() {
log.debug "installed()"
updated()
}
def updated() {
log.debug "updated()"
unsubscribe()
subscribe(motionSensor, "motion", "motionHandler")
}
def uninstalled() {}
def motionHandler(evt) {
log.debug "motionHandler() called: ${evt.name} ${evt.value}"
if (evt.value == "active") {
lightDevices.on()
unschedule("delayedOffHandler")
}
else {
if (offAfter) {
runIn(offAfter*60, "delayedOffHandler")
}
else {
lightDevices.off()
}
}
}
def delayedOffHandler() {
lightDevices.off()
}
Right now, the lightDevices
input is not required. If the user leaves no devices selected, you will get an error (in Logs) when executing lightDevices.on()
inside the motionHandler
method because lightDevices
will be null
. In one sense, this is unlikely to matter since the app would not have anything to do in this case regardless, but a better user experience might prevent this error from happening. Two possible solutions:
lightDevices.on()
to lightDevices?.on()
, utilizing the Groovy safe navigation operatorrequired: true
for lightDevices
, ensuring the app cannot be saved if no value is providedIf a user is trying to troubleshoot app behavior (or unexpected behavior with a device in use by an app), they are likely to turn to Logs. Many Hubitat apps offer the ability to enable or disable logging to help with this when enabled but also keep logs quieter for easier troubleshooting with other apps or devices when disabled. The exact logging options an app offers may vary, depending on the complexity of the app; for this simple app, a simple option to enable logging should work. This input
can be added to preferences
:
input name: "logEnable", type: "bool", title: "Enable logging?"
Unlike with certain types of driver logging, the convention for most apps in Hubitat is to keep whatever logging enabled until the user manually disables it. Most Hubitat users expect to have the ability to enable or disable most or all app (and driver) logging, depending on their preferences and troubleshooting needs. You do not have to follow any of these conventions in custom apps (or drivers), particularly if you are writing them only for your own use. However, if you share the app, many Hubitat users expect this behavior, similar to what most built-in apps offer.
Now, we can check if logEnable
is true
and write log entries that help describe app behavior and may be useful for users to troubleshoot (or when developing the app yourself or helping a user troubleshoot). For example:
definition(
name: "Custom Motion Lights",
namespace: "Example",
author: "Hubitat Example",
description: "Turns lights on or off with motion",
category: "Convenience",
iconUrl: "",
iconX2Url: "")
preferences {
section("Sensors and Lights") {
input name: "motionSensor", type: "capability.motionSensor", title: "When motion is detected on...", required: true
input name: "lightDevices", type: "capability.switch", title: "Then turn on these lights...", multiple: true
input name: "offAfter", type: "number", title: "And turn off after motion remains inactive for this many minutes:", defaultValue: 1
}
section("Logging") {
input name: "logEnable", type: "bool", title: "Enable logging?"
}
}
def installed() {
log.debug "installed()" // not checking if logging is enabled here -- this will only happen once per app and may be good to know regardless of value of preference
updated()
}
def updated() {
if (logEnable) log.debug "updated()"
unsubscribe()
subscribe(motionSensor, "motion", "motionHandler")
}
def uninstalled() {
log.debug "uninstalled()" // also not checking for preference; will happen at most once in lifetime of app and may be good to know regardless of setting
}
def motionHandler(evt) {
if (logEnable) log.debug "motionHandler() called: ${evt.name} ${evt.value}" // only log if user has option enabled
if (evt.value == "active") {
if (logEnable) log.debug "Motion active; turning on lights"
lightDevices?.on()
unschedule("delayedOffHandler")
}
else {
if (offAfter) {
if (logEnable) log.debug "Motion inactive; scheduling off in ${offAfter} minutes"
runIn(offAfter*60, "delayedOffHandler")
}
else {
if (logEnable) log.debug "Motion inactive; turning off lights now"
lightDevices.off()
}
}
}
def delayedOffHandler() {
if (logEnable) log.debug "Delay over; turning off lights now"
lightDevices.off()
}
By now, you should be able to create a basic, single-page Hubitat app. Congratulations!
It may be helpful to continue by looking at example apps, many of which can be found in the HubitatPublic GitHub repository: https://github.com/hubitat/HubitatPublic/tree/master/example-apps.
The remaining developer documentation provides more information on what methods are available for app (and driver) code and what methods and properties are available on Hubitat-provided objects or utilities. For apps, the following may be particularly helpful: