Drivers on Hubitat Elevation are the means by which users and apps communicate with devices. Hubitat comes with a variety of built-in drivers for various Zigbee, Z-Wave, LAN, and cloud devices, but it is also possible to write user drivers (also known as custom drivers) — the focus of this document.
Most drivers correspond to real-world devices. For example, the Generic Zigbee RGBW Light driver is compatible with many Zigbee RGBW bulbs, lightstrips, etc. However, some drivers are "virtual," meaning that they do not (necessarily) represent or communicate with a real-world device, generally just simulating device behavior on the hub (e.g., reporting a "switch: on" event immediately after running the on()
command – rather than waiting for this information to come back from the device, as is typically done with real devices).
To add a custom driver, navigate to Drivers Code in the Hubitat web interface (expand the Developer Tools section if you do not see this in the sidebar). Select New Driver to create a new driver, or select an existing custom driver in the list to modify that driver. Select Save to commit your changes. Note that only valid code will save; any errors will appear in a a yellow bar at the top of the editor.
For an overview of the Hubitat development environment in general, see: Developer Overview.
The code below is a fully complete Hubitat driver, though it is only virtual and represents a simple virtual switch. Still, it demonstrates all components necessary for a basic virtual device. Try adding it to Drivers Code and adding a device using it under Devices > Add Device > Virtual, and choosing your driver name for Type on the new device to see how it works!
metadata {
definition (name: "Custom Virtual Switch", namespace: "MyNamespace", author: "My Name") {
capability "Actuator"
capability "Switch"
}
preferences {
// none in this driver
}
}
def installed() {
log.debug "installed()"
}
def updated() {
log.debug "updated()"
}
def on() {
// With a real device, you would normally send a Z-Wave/Zigbee/etc. command
// to the device here. For a virtual device, we are simply generating an
// event to make the "switch" attribute "on". With a real device, you would
// typically wait to hear back from the device in parse() before doing so.
sendEvent(name: "switch", value: "on", descriptionText: "${device.displayName} switch is on")
}
def off() {
// Same notes as for on(), above, apply here...
sendEvent(name: "switch", value: "off", descriptionText: "${device.displayName} switch is off")
}
An explanation of these components is provided below.
Note that this driver demonstrates use of the built-in logger, log
, available to all apps and drivers. This outputs the provided text to Logs, and you will see this output when these methods are called (installed()
when the device is first added, and updated()
whenever the user selects the Save Preferences button). Logging methods availalbe are the same as those for apps. For now, we are simply logging when ceratin driver methods are called; generally, you will need to do something in one or more of these methods.
Unlike an app, the definition
and preferences
for a driver are inside a metadata
block.
The definition()
in a driver contains data about the app, including the name and author. It also specifies capabilities, commands, attributes, and/or fingerprints applicable to the driver. (Technically, this is a method that takes two parameters, a Map
with the former information and a Closure
with the latter; see above for an example of how this is typically written.)
name
: The name of the driver as will be displayed in the Type (driver list) on the device detail page and other placesnamespace
: A unique identifier for the developer (driver name plus namespace combinations must be unique), typically a username associated with that developer (often their GitHub username)author
: A string identifying the developer, sometimes a real name or company name (unlike the name and namespace, this is used only for display purposes)importUrl
- The URL where the Groovy code for this driver can be found (will auto-populate URL field when user selects Import button on driver code page)singleThreaded
- If true
(default is false
), simultaneous execution of a particular driver instance is prevented. The hub will load driver data (including state
), run the called method, and save the data (including state
) completely before moving on to any additional calls that may have been queued in the meantime. This applies to "top level" methods only. More details: https://community.hubitat.com/t/2-2-9-singlethreaded-option-for-apps-drivers/80969The preferences
block defines what appears under Preferences on the device detail page. These often correspond to options that the device itself offers (e.g., Zigbee or Z-Wave configuration options) or how the driver handles certain device behavior. (Unlike in an app, there is no page
or section
block available to drivers.)
The input()
method method allows you to display various types of inputs (drop-down lists, string or numeric inputs, etc.). The general format is:
input(String name: elementName, String type: elementType, String title: elementTitle, /* ... additional options */ )
Groovy allows omitting the parentheses, and the name
and type
labels can be omitted, allowing for simpler calls like this:
input "myName", "myType", title: "My Input"
Each input
creates a setting with the name
specified in the input. These can be accessed in the app directly with the name, e.g., myName
(as if it were a field/variable name), or with settings["myName"]
. Thesettings
object is a Map with key being the input name, and value being the input value from the user. Settings names accessed by the input name directly have scope across the entire driver.
Input types include:
text
: Stringnumber
: Integerdecimal
: Doubleenum
: List (this type requires a parameter options of type List
with the options); this is a pull-down menu.bool
: Boolean; this is an on/off slider.The options of an enum
can be a List of Maps, where the key of the map element is returned while the String value is displayed in the pull-down menu.
A defaultValue: value
can be specified for an input, which will display in the UI on load but will not be saved to the corresponding setting until the preferences are saved. Optional required: true
forces an input to be completed before saving the page.
Drivers should implement all commands required by the capabilities it specifies. The Hubitat platform will look for a regular Groovy method in the driver with a name that matches the command. For example, capability "Switch"
requires an on()
and off()
command, which are implemented as methods in the example driver above.
Some driver methods will also be called at at specific times and should be provided in your driver code:
installed()
: will be called when device is first addedupdated()
: will be called when user selects Save Preferencesinitialize()
: called on hub startup if driver specifies capability "Initialize"
(otherwise is not required or automatically called if present)parse(String desc)
: handles raw incoming data from Zigbee, Z-Wave, or LAN devices and generally creates events as needed; see example drivers for more (virtual devices generally do not demonstrate this behavior, as there is no data coming in from a real device)To get started developing Z-Wave, Zigbee, LAN/cloud, or Matter drivers, continue with:
For more examples of virtual, Zigbee, Z-Wave, and Matter drivers, consult the HubitatPublic example repository (see below).
To store data between executions, drivers have a built-in state
object available. This object behaves like a Map
and allows storing most common data types (strings, numbers, Lists or Maps of such objects, etc. — anything that can be serialized to/from JSON). Each device has its own state
object.
A similar object named atomicState
is also available. They work similarly, except that state
writes data just before the driver goes to sleep again, whereas atomicState
commits the changes as soon as they are made. We suggest beginning with state
and changing to atomicState
only if there are concerns for simultaneous execution and state data related to this. Use of state
is more efficient. Another more efficient alternative to atomicState
is to use specify singleThreaded: true
in definition
, preventing more than one overlapping wake of the driver code for the same device.
The atomicState
object also provides a convenience method:
atomicState.updateMapValue(Object stateKey, Object key, Object value)
: gets a Map
from atomicState
, updates a value inside that map, and writes it backThe
state
andatomicState
objects refer to the same data and can be mixed in the same driver if needed, but most developers choose one or the other based on the requirements of the driver.
Example: state.foo = "bar"
See the remaining developer documentation. For drivers, the following may be particularly helpful:
device
referenceIt may also be helpful to look at examples of Hubitat drivers. Some can be found in the the HubitatPublic GitHub repository: https://github.com/hubitat/HubitatPublic/tree/master/examples/drivers.