We begin by assuming some familiarity with the Hubitat Elevation development environment and the Groovy programming language that it uses, though expert proficiency is by no means required. Read the Developer Overview if you have not already.
Familiarity with the Matter protocol is necessary to write Matter drivers. Specifications are available from the Connectivity Standards Alliance (formerly the Zigbee Alliance), though significant portions of the Matter standard and Matter driver development will seem familiar to users already familiar with Zigbee driver development. We cover some basics here.
Matter groups related functionality into clusters, which contain related commands and attributes. Each cluster is assigned a numeric identifier (traditionally written in hex). Examples of common clusters and their IDs include:
0x0006
On/Off0x0008
Level Control0x0402
Temperature MeasurementClusters will differ by device, generally depending on real-world functionality. For example, a smart plug or on/off switch will typically implement On/Off (0x0006
), among others.
Clusters may include commands or attributes. Commands are generally actions that can be performed on a device. These are also assigned numeric identifiers and conventionally written in hex. For example, the 0x0006
On/Off cluster requires at least the following commands:
0x00
Off0x01
On0x02
ToggleAttributes are generally states or properties that can be read (or set via a corresponding command). Continuing our examination of the 0x0006
On/Off cluster, we see that it offers at least the following attribute:
0x0000
On/OffThis can be read to determine the current on/off state of the switch (or more commonly, subscribed to so the driver gets notified of changes as they happen; see below).
Before writing your driver, you will need to:
We might begin with a basic driver template, similar to that found in Driver Overview. However, one important difference between virtual drivers like that example and Matter drivers (and Zigbee/Z-Wave drivers and some LAN drivers) is that data can be sent to the hub from the device. For Matter, this is often from attributes we have subscribed to and were changed (e.g., the real-world device turning on or off). The parse()
method is where raw data from the device comes into drivers, so we will include one here and explain what to do with it later.
metadata {
definition (name: "My Matter Switch", namespace: "MyNamespace", author: "My Name") {
capability "Actuator"
capability "Switch"
}
preferences {
// None for now -- but for Matter devices that offer attributes that
// can be written to set preferences, they are often included here.
// Later, we will add conventional Hubitat logging preferences here.
}
}
def installed() {
log.debug "installed()"
}
def updated() {
log.debug "updated()"
}
def parse(String description) {
// This is where incoming data from the device will be sent.
// For now, just log the raw data (we will discuss ways to handle
// this later):
log.debug "parse: $description"
}
def on() {
// TO DO: Required command from Switch capability, logging for now:
log.warn "on() not yet implemented"
}
def off() {
// TO DO: Required command from Switch capability, logging for now:
log.warn "off() not yet implemented"
}
As you can see, we have written stubs for a few methods that will need to be properly written in order to build a fully functional driver — but as-is, the driver is syntactically correct (so can be saved) and will log when commands are sent or information is received from the device. To complete the driver, we will need to:
parse()
(and make sure the device sends this information to the hub in the first place)on()
and off()
commands to perform the appropriate actions, i.e., turning the switch on or off by sending Matter commandsThe parse()
method includes a single String
parameter containing the "raw" Matter data. The Hubitat environment provides a method, parseDescriptionAsMap()
, that will parse most such data into key/value pairs for you, including data such as the cluster ID and attribute ID. Typically, you will then check the cluster ID and perform appropriate actions based on the attribute and other values. We demonstrate a possible implementation for our device below:
def parse(String description) {
// Can be helpful for debugging; for now we'll just always log:
log.debug "parse description: ${description}"
def descMap = matter.parseDescriptionAsMap(description)
// Parses hex (base 16) string data to Integer -- perhaps easier to work with:
def rawValue
switch (descMap.clusterInt) {
case 0x0006: // On/Off
if (descMap.attrInt == 0) {
rawValue = Integer.parseInt(descMap.value, 16)
String switchValue
// attribute value of 0 means off, 1 (only other valid value) means on
switchValue = (rawValue == 0) ? "off" : "on"
// for now, always log -- normally we would offer a preference:
log.info "${device.displayName} switch is ${switchValue}"
// generate an event on the hub (to change the "switch" attribute
// value as seen under "Current States" in the UI:
sendEvent(name: "switch", value: switchValue, descriptionText: "${device.displayName} switch is ${switchValue}")
}
else {
// some atribute besides 0, which we don't care about but will log
// for now -- could also leave out if you know it's not needed:
log.debug "0x0006:${descMap.attrId}"
}
// In other drivers, you may have other cases here
// For example, case 0x0008 for level, etc.
default:
// Probably not needed in most drivers but might be helpful for
// debugging -- always logging for now:
log.debug "ignoring {descMap.clusterId}:${descMap.attrId}"
break
}
}
Now, we need to make the on()
and off()
methods in our driver actually work – i.e., send an appropriate Matter command to the device. Hubitat has a built-in method to generate the Matter on and off commands in the required format, making our implementations potentially very simple:
def on() {
// returns a string (in Hubitat Matter command format) representing
// the On command for the On/Off cluster:
return matter.on()
}
and:
def off() {
// returns a string (in Hubitat Matter command format) representing
// the Off command for the On/Off cluster:
return matter.off()
}
NOTE: We are taking advantage here of the fact if the Groovy method corresponding to the Hubitat command returns a string or List of strings containing properly formatted Matter commands (such as the ones generated by the built-in
matter
object methods above), that Matter command will be automatically sent upon return from the method. This is an easy way to send commands and is the method demonstrated here.Some authors prefer to use
sendHubCommand()
instead, which requires aHubAction
object to be constructed and passed, along with other information. The method would then not need to return any value, as you are "manually" sending the command. This approach is more difficult, more verbose to write, and not demonstrated here.
Tip: Recall that the return
keyword is optional in Groovy, and Groovy will by default return the value of the last statement in the method. Therefore, the above could be written more idiomatically as:
def on() {
matter.on()
}
The off()
method could be re-written similarly, except using matter.off()
.
NOTE: You can also return a List
of commands if needed. The delayBetween()
method is often used in conjunction with this approach.
With the above changes, our driver more or less works. However, unless we take the approach of "manually" reading the attribute after sending a command, we won't get any data when the device changes state — i.e., if our "on" or "off" command were successful, or if the user changes the state of the device manually or via some other Matter controller with which the device might also be paired.
To do this, use the matter.subscribe()
method, passing in a list of attribute paths to subscribe to. Commonly, this will be done in a configure()
method. If your driver uses capability "Configuration"
, this method will be called when the device is initially installed, so this is a great combination to use. (Users are also encouraged to run the "Configure" command when switching drivers for reasons including this; nothing happens on its own as a result of a driver change.)
In our driver, this might look like:
// ...
capability "Configuration"
// ...
def configure() {
// Using a List here even though we are only returning one command because in real
// drivers, you are likely to have multiple commands you could add to this List:
List<String> cmds = []
List<Map<String, String>> attributePaths = []
attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00))
// other kinds of devices will likely need to add more paths here
Integer reportingInterval = 5 // default is 0 (OK for some devices)
String subscribeCmd = matter.subscribe(reportingInterval,0xFFFF,attributePaths)
cmds.add(subscribeCmd)
return cmds // not using delayBetween(...) but often would in real drivers
}
Using everything we've discussed so far, our driver might look like:
metadata {
definition (name: "My Matter Switch", namespace: "MyNamespace", author: "My Name") {
capability "Actuator"
capability "Configuration"
capability "Switch"
}
preferences {
// None for now -- but for Matter devices that offer attributes that
// can be written to set preferences, they are often included here.
// Later, we will add conventional Hubitat logging preferences here.
}
}
def installed() {
log.debug "installed()"
}
def updated() {
log.debug "updated()"
}
def parse(String description) {
// Can be helpful for debugging; for now we'll just always log:
log.debug "parse description: ${description}"
def descMap = matter.parseDescriptionAsMap(description)
def rawValue
switch (descMap.clusterInt) {
case 0x0006: // On/Off
if (descMap.attrInt == 0) {
// Parses hex (base 16) string data to Integer -- perhaps easier to work with:
rawValue = Integer.parseInt(descMap.value, 16)
String switchValue
// attribute value of 0 means off, 1 (only other valid value) means on
switchValue = (rawValue == 0) ? "off" : "on"
// for now, always log -- normally we would offer a preference:
log.info "${device.displayName} switch is ${switchValue}"
// this is what actually generates the event:
sendEvent(name: "switch", value: switchValue, descriptionText: "${device.displayName} switch is ${switchValue}")
}
else {
// some atribute besides 0, which we don't care about but will log
// for now -- could also leave out if you know it's not needed:
log.debug "0x0006:${descMap.attrId}"
}
// In other drivers, you may have other cases here
// For example, case 0x0008 for level, etc.
default:
// Probably not needed in most drivers but might be helpful for
// debugging -- always logging for now:
log.debug "ignoring {descMap.clusterId}:${descMap.attrId}"
break
}
}
def configure() {
List<String> cmds = []
List<Map<String, String>> attributePaths = []
attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00))
// other kinds of devices will likely need to add more paths here
Integer reportingInterval = 5
String subscribeCmd = matter.subscribe(reportingInterval,0xFFFF,attributePaths)
cmds.add(subscribeCmd)
return cmds // or delayBetween(cmds)
}
def on() {
matter.on()
}
def off() {
matter.off()
}
Built-in Hubitat drivers generally follow some conventions, and we may wish to make our driver do the same so as to meet most users' expectations. These include:
parse()
, possibly parts of how it is (and isn't) handled within parse()
, and often when commands like on()
are calledparse()
Making our driver follow these conventions, we may end up with something like:
metadata {
definition (name: "My Matter Switch", namespace: "MyNamespace", author: "My Name") {
capability "Actuator"
capability "Configuration"
capability "Switch"
}
preferences {
// For some Matter devices, you may want to offer additional options for reporting configuration or some
// manufacturer-specific options, etc. here
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
}
}
def installed() {
log.debug "installed()"
}
def updated() {
log.debug "updated()"
log.warn "debug logging is: ${logEnable == true}"
log.warn "description logging is: ${txtEnable == true}"
if (logEnable) runIn(1800, "logsOff") // 1800 seconds = 30 minutes
// In drivers that offer preferences for configuration, you might also iterate over
// these here and send Matter commands as needed here (or call configure() and do it there)
}
// handler method for scheduled job to disable debug logging:
def logsOff(){
log.warn "debug logging disabled..."
device.updateSetting("logEnable", [value:"false", type:"bool"])
}
def parse(String description) {
if (logEnable) log.debug "parse description: ${description}"
def descMap = matter.parseDescriptionAsMap(description)
// Parses hex (base 16) string data to Integer -- perhaps easier to work with:
def rawValue
switch (descMap.clusterInt) {
case 0x0006: // On/Off
if (descMap.attrInt == 0) {
rawValue = Integer.parseInt(descMap.value, 16)
String switchValue
// attribute value of 0 means off, 1 (only other valid value) means on
switchValue = (rawValue == 0) ? "off" : "on"
if (txtEnable) log.info "${device.displayName} switch is ${switchValue}"
// this is what actually generates the event:
sendEvent(name: "switch", value: switchValue, descriptionText: "${device.displayName} switch is ${switchValue}")
}
else {
if (logEnable) log.debug "0x0006:${descMap.attrId}"
}
// In other drivers, you may have other cases here
// For example, case 0x0008 for level, etc.
default:
if (logEnable) log.debug "ignoring {descMap.clusterId}:${descMap.attrId}"
break
}
}
def configure() {
List<String> cmds = []
List<Map<String, String>> attributePaths = []
attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00))
// other kinds of devices will likely need to add more paths here
Integer reportingInterval = 5
String subscribeCmd = matter.subscribe(reportingInterval,0xFFFF,attributePaths)
cmds.add(subscribeCmd)
return cmds // or delayBetween(cmds)
}
def on() {
if (logEnable) log.debug "on()"
matter.on()
}
def off() {
if (logEnable) log.debug "off()"
matter.off()
}
Many drivers offer a refresh()
method (usually as part of capability "Refresh")
to fetch current states from the device. For devices that report changes back on their own, users should rarely need to call this command. However, it is often a good idea to include (and may find some internal use in your driver, e.g., to retrieve current states upon installation).
For a Matter device, implementation of this command will often include sending one or more "read attributes" commands (the readings for which should eventually come into your parse()
method where this information can be handled appropriately).
Congratulations! You have a fully functional, albeit basic, Matter switch (or outlet) driver.
For a more complicated example, see the published Matter RGBW nightlight example: https://github.com/hubitat/HubitatPublic/blob/master/examples/drivers/thirdRealityMatterNightLight.groovy (or any of the other example drivers to see other kinds of devices).
You may wish to continue reading the remaining Developer Docs, particularly those for drivers and Common Methods (available to both apps and drivers). If you are also interested in Z-Wave or Zigbee drivers, see the analogous Building a Z-Wave Driver or Building a Zigbee Driver documents.