Driver
Drivers are the replacement for DTHs (Device Type Handlers) but are more specific in their responsibilities. A driver represents
the code necessary to provide the needed behavior for a set of devices. Within the Lua code base, a “driver”
represents a table containing the context necessary for executing device behaviors. Unlike DTHs, a separate
driver instance will not run for each device; instead a single “driver” will handle all devices that are
set to use it. Unlike DTHs, drivers behave functionally as if they are long running. That is,
the script will behave as if it is always running and waiting for input on the devices it is registered to
handle. See Driver
for the description of what a “driver” table will
contain. There are additional functions and structures provided on a per-protocol basis that are specific to those
protocols.
For most use cases you can create a template that has some basic behavior defined for a driver and then pass it to
Driver(<driver_name>, <driver_template>)
to handle setting up most of the defaults. Below is an
example of a driver setup:
local driver = Driver("my fancy bulb", {
capability_handlers = {
[capabilities.switch.ID] = {
[capabilities.switch.commands.on.NAME] = command_handlers.handle_switch_on,
[capabilities.switch.commands.off.NAME] = command_handlers.handle_switch_off,
},
[capabilities.switchLevel.ID] = {
[capabilities.switchLevel.commands.setLevel.NAME] = command_handlers.handle_set_level,
},
[capabilities.colorControl.ID] = {
[capabilities.colorControl.commands.setColor.NAME] = command_handlers.handle_set_color,
[capabilities.colorControl.commands.setHue.NAME] = command_handlers.handle_set_hue,
[capabilities.colorControl.commands.setSaturation.NAME] = command_handlers.handle_set_saturation,
},
[capabilities.colorTemperature.ID] = {
[capabilities.colorTemperature.commands.setColorTemperature.NAME] = command_handlers.handle_set_color_temp,
}
}
})
In this case, the only pieces of the template being specified are the command handlers for capability commands for these devices. Also note that some specific protocols may have more tailored options, and a more tailored init function for the driver contexts. See the individual protocols for examples.
Driver Class Documentation
- class Driver
This is a template class to define the various parts of a driver table. The Driver object represents all of the state necessary for running and supporting the operation of this class of devices. This can be as specific as a single model of device, or if there is much shared functionality can manage several different models and manufacturers.
Drivers go through initial set up on hub boot, or initial install, but after that the Drivers are considered long running. That is, they will behave as if they run forever. As a result, they should have a main run loop that continues to check for work/things to process and handles it when available. For MOST uses the provided run function should work, and there should be no reason to overwrite the existing run loop.
- NAME: str
a name used for debug and error output
- capability_channel: message_channel
the communication channel for capability commands/events
- lifecycle_channel: message_channel
the communication channel for device lifecycle events
- timers: table
this will contain a list of in progress timers running for the driver
- capability_dispatcher: CapabilityCommandDispatcher
dispatcher for routing capability commands
- lifecycle_dispatcher: DeviceLifecycleDispatcher
dispatcher for routing lifecycle events
- secret_data_dispatcher: SecretDataDispatcher
dispatcher for routing secret data events
- sub_drivers: list[SubDriver]
A list of sub_drivers that contain more specific behavior behind a can_handle function
- call_with_delay(self, delay_s, callback, name)
Set up a one shot timer to hit the callback after delay_s seconds
- Parameters
self (
Driver
) – the driver setting up the timerdelay_s (
number
) – the number of seconds to wait before hitting the callbackcallback (
function
) – the function to call when the timer expires. @see Driver.timer_callback_templatename (
str
) – an optional name for the timer
- Returns
the created timer
- Return type
timer
- call_on_schedule(self, interval_s, callback, name)
Set up a periodic timer to hit the callback every interval_s seconds
- Parameters
self (
Driver
) – the driver setting up the timerinterval_s (
number
) – the number of seconds to wait between hitting the callbackcallback (
function
) – the function to call when the timer expires. @see Driver.timer_callback_templatename (
str
) – an optional name for the timer
- Returns
the created timer
- Return type
timer
- cancel_timer(self, t)
Cancel a timer set up on this driver
- Parameters
self (
Driver
) – the driver with the timert (
Timer
) – the timer to cancel
- capability_message_handler(self, capability_channel)
Default handler that can be registered for the capability message channel
- Parameters
self (
Driver
) – the driver to handle the capability commandscapability_channel (
message_channel
) – the capability message channel with data to be read
- handle_capability_command(self, device, cap_command, quiet)
Default capability command handler. This takes the parsed command and will look up the command handler and call it
- Parameters
self (
Driver
) – the driver to handle the capability commandsdevice (
st.Device
) – the device that this command was sent tocap_command (
table
) – the capability command table including the capability, command, component and argsquiet (
boolean
) – if true, suppress logging; useful if the driver is injecting a capability command itself
- inject_capability_command(self, device, cap_command)
Inject a capability command into the capability command dispatcher.
- lifecycle_message_handler(self, lifecycle_channel)
Default handler that can be registered for the device lifecycle events
- Parameters
self (
Driver
) – the driver to handle the device lifecycle eventslifecycle_channel (
message_channel
) – the lifecycle message channel with data to be read
- driver_lifecycle_message_handler(self, ch)
Default handler that can be registered for the driver lifecycle events
- Parameters
self (
Driver
) – the driver to handle the device lifecycle eventsch (
message_channel
) – the lifecycle message channel with data to be read
- discovery_message_handler(self, discovery_channel)
Default handler that can be registered for the device discovery events
- Parameters
self (
Driver
) – the driver to handle the device discovery eventsdiscovery_channel (
message_channel
) – the discovery message channel with data to be read
- environment_info_handler(self, environment_channel)
Default handler that can be registered for the environment info messages
- Parameters
self (
Driver
) – the driver to handle the device lifecycle eventsenvironment_channel (
message_channel
) – the environment update message channel
- secret_data_handler(self, secret_data)
Driver secret data handler
This function handles the “secretData” security message that is returned asynchronously as a response to a request from a driver. The secret data security message contains sensitive information (like keys or auth material) obtained by the hub on behalf of the driver.
- Parameters
self (
Driver
) – This driversecret_data (
Table
) – Secret data returned as the result from the get_secret request
- security_handler(self, security_channel)
Default handler for security event messages
Security event messages are currently limited to secret_data requests (see secret_data_handler) which are keys or auth material obtained by the hub on behalf of the driver.
- Parameters
self (
Driver
) – the driver to handle the security eventssecurity_channel (
message_channel
) – the security message channel
- get_devices()
Get a list of all devices known to this driver.
- Returns
of Device objects
- Return type
List
- build_child_device(raw_device_table)
- Parameters
raw_device_table (
any
) –
- get_device_info(self, device_uuid, force_refresh)
Default function for getting and caching device info on a driver
By default this will use the devices api to request information about the device id provided it will then cache that information on the driver. The information will be stored as a table after being decoded from the JSON sent across.
- Parameters
self (
Driver
) – the driver runningdevice_uuid (
str
) – the uuid of the device to get info forforce_refresh (
boolean
) – if true, re-request from the driver api instead of returning cached value
- try_create_device(device_metadata)
Send a request to create a new device.
Note
At this time, only LAN type devices can be created via this api.
Example usage:
local metadata = { type = "LAN", device_network_id = "24FD5B0001044502", label = "Kitchen Smart Bulb", profile = "bulb.rgb.v1", manufacturer = "WiFi Smart Bulb Co.", model = "WiFi Bulb 9000", vendor_provided_label = "Kitchen Smart Bulb" }) driver:try_create_device(metadata))
All metadata fields are type string. Valid metadata fields are:
type - network type of the device. Must be “LAN” or “EDGE_CHILD”. (required)
device_network_id - unique identifier specific for this device (required)
label - label for the device (required)
profile - profile name defined in the profile .yaml file (required)
parent_device_id - device id of a parent device
manufacturer - device manufacturer
model - model name of the device
vendor_provided_label - device label provided by the manufacturer/vendor (typically the same as label during device creation)
external_id - The unique identifier of the device on its host platform.
- Parameters
device_metadata (
table
) – A table of device metadata
- try_delete_device(device_uuid)
Send a request to delete an existing device
At this time, only LAN and EDGE_CHILD type devices can be deleted via this api.
Example usage:: local device_uuid = “aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee”
driver:try_delete_device(device_uuid)
device_uuid is expected to be of type string, otherwise a warn-level message will be logged and two values will be returned: nil and a string message
- Parameters
device_uuid (
str
) – A string of the device UUID
- register_channel_handler(self, message_channel, callback, name)
Function to register a message_channel handler
- Parameters
self (
Driver
) – the driver to handle message eventsmessage_channel (
message_channel
) – the message channel to listen to input oncallback (
function
) – the callback function to call when there is data on the message channelname (
str
) – Optional name for the channel handler, used for logging
- unregister_channel_handler(self, message_channel)
Function to unregister a message_channel handler
- Parameters
self (
Driver
) – the driver to handle the message eventsmessage_channel (
message_channel
) – the message channel to stop listening for input on
- static standardize_sub_drivers(driver)
Standardize the structure of the sub driver structure of this driver
The handlers registered as a part of the base driver file (or capability defaults) are assumed to be the default behavior of the driver. However, if there is need for a subset of devices to override the base behavior for one reason or another (e.g. manufacturer or model specific behavior), a value can be added to the “sub_drivers”. Each sub_driver must contain a can_handle function of the signature can_handle(opts, driver, device, …) where opts can be used to provide context specific information necessary to determine if the sub_driver should be responsible for some type of work. The most common use for the sub drivers will be to provide capabiltiy/zigbee/zwave/matter handlers that need to override the default for the driver. It may optionally also contain its own sub_drivers containing further subservient sets.
- Parameters
driver (
Driver
) – the driver
- static lazy_load_sub_driver(sub_driver)
Load reduced version of sub driver for memory savings
This function takes a sub driver and removes the handlers so that they can be “lazy loaded” later when needed. All that is saved is the sub driver name and the can_handle function. This allows the handlers to be garbage collected for memory savings. The handlers will be loaded again later if the dispatcher recieves a message that can be handled by this sub driver.
- Parameters
sub_driver (
Driver
) – the sub driver
- static populate_capability_dispatcher_from_sub_drivers(driver)
Recursively build the capability dispatcher structure from sub_drivers
This will recursively follow the sub_drivers defined on the driver and build a structure that will correctly find and execute a handler that matches. It should be noted that a child handler will always be preferred over a handler at the same level, but that if multiple child handlers report that they can handle a message, it will be sent to each handler that reports it can handle the message.
- Parameters
driver (
Driver
) – the driver
- static populate_lifecycle_dispatcher_from_sub_drivers(driver)
Recursively build the lifecycle dispatcher structure from sub_drivers
- Parameters
driver (
Driver
) – the driver
- static populate_secret_data_dispatcher_from_sub_drivers(driver)
Recursively build the secret data dispatcher structure from sub-drivers
- Parameters
driver (
Driver
) – the driver
- static init(cls, name, template)
Given a driver template and name initialize the context
This is used to build the driver context that will be passed around to provide access to various state necessary for operation
- run(self)
Function to run a driver
This will run an “infinite” loop for this driver. It will wait for input on any message channel that has a handler registered for it through the register_channel_handler function. In addition it will wait for any registered timers to expire and trigger as well. Whenever data becomes available on one of the message channels the callback will be called and then it will go back to waiting for input.
- Parameters
self (
Driver
) – the driver to run
Driver Template Options
To create a driver, use the Driver(<driver_name>, <driver_template>)
call to
create the driver object. There are a number of things that can be included within the driver template that is passed
in.
- lifecycle_handlers
A structure of registered handlers for a variety of different events pertaining to the lifecycle of devices
- capability_handlers
The structure mentioned in the capabilities handlers section used to handle SmartThings capability commands
- sub_drivers
The list of sub drivers that are under this driver.
- driver_lifecycle
A function used to respond to driver lifecycle events. Contains two arguments. The first argument is the
Driver
the second argument is the string name of the event (currently"shutdown"
is the only option)
Lifecycle Event Handlers
All drivers, regardless of the underlying protocol, will by default support the definition of a lifecycle_handlers
structure that will allow you to provide a set of functions that will be called when any of the following device events
happen
- init
This device init function will be called any time a device object needs to be instantiated within the driver. There are 2 main cases where this happens: 1) the driver just started up and needs to create the objects for existing devices and 2) a device was newly added to the driver.
- added
A device was newly added to this driver. This represents when the device is, for the first time, assigned to run with this driver. For example, when it is first joined to the network and fingerprinted to this driver.
- doConfigure
This is an event that will be sent when the platform believes the device needs to go through provisioning for it to work as expected. The most common situation for this is when the device is first added to the platform, but there are other protocol specific cases that this may be triggered as well.
- infoChanged
This represents a change that has happened in the data representing the device on the SmartThings platform. An example could be a change to the name of the device.
- driverSwitched
This represents a device being switched from using a different driver, to using the current driver. This will be sent after an
added
event and it can be used to determine if this device can properly be supported by the driver or not. SeeDriver.default_capability_match_driverSwitched_handler
as an example. Updating the devices metadata fieldprovisioning_state
to eitherNONFUNCTIONAL
orPROVISIONED
can be used to indicate that the driver won’t or will, respectively, function within this driver.- removed
This represents a device being removed from this driver.
Each of these can be defined in the lifecycle_handlers
table as a key with the corresponding function that will be
called when the event happens. These functions will have the signature of
event_handler(driver, device, event, args)
where event
is the string matching the above event and args is
a table with key-values specific to the event. The only event that currently uses the args
is the infoChanged
event which provides the key arg old_st_store
which is the table representation of the device info from before this
event happened with the device
arg containing the new up to date information.
There is also some default behavior that will always happen for certain events. added
will also trigger an init
after it has completed the added
behavior. removed
will remove the device from the device cache and make
the device inoperable.
In addition there are 2 extra keys that can be added the fallback
and error
fields can be set to be used as
the special handlers for the Dispatchers to be handlers that get called in the case of no other
matching handler or an error is encountered respectively.
Further these lifecycle handlers can be defined in SubDrivers if there is some differntiation needed between devices within a driver.
example
Here is a simple example in which a device needs to do some specific configuration and setup.
local capabilities = require "st.capabilities"
local ZigbeeDriver = require "st.zigbee"
local defaults = require "st.zigbee.defaults"
local function configure_device(self, device)
device:configure()
-- some driver specific behavior
self.arbitrary_value = self.arbitrary_value + 1
end
local device_init = function(self, device)
self.device_tracker[device.id] = "device initted"
end
local zigbee_outlet_driver_template = {
supported_capabilities = {
capabilities.switch,
},
lifecycle_handlers = {
init = device_init,
doConfigure = configure_device
},
device_tracker = {}
}
defaults.register_for_default_handlers(zigbee_outlet_driver_template, zigbee_outlet_driver_template.supported_capabilities)
local zigbee_outlet = ZigbeeDriver("zigbee_bulb", zigbee_outlet_driver_template)
zigbee_outlet:run()
Capability Command Handlers
All drivers, regardless of the underlying protocol, will by default support the definition of a capability_handlers
structure that will allow you to map the capability commands to a function meant to support that command. For more
information about the use of capabilities within drivers, including the capability_handlers
structure, see
capabilities.
SubDrivers
Because drivers are no longer a separate instance for each device, it becomes much easier to support
multiple different-but-similar devices (e.g. both a Zigbee on/off/dim bulb and a Zigbee RGBW bulb) in the same driver
to maximize similar behavior between devices; a driver can support any number of different profiles. However,
there still exists the problem of handling abnormal devices, for example a device that is exactly the same in handling
and behavior except for one command which has a slightly different handling. You could register a single
capability handler (or other protocol specific handler) with branching within the handler
for each device variation. However, this can quickly get out of control with long lists of if/else blocks. This is the
problem that SubDrivers
helps solve. With this solution, each subdriver will provide a single can_handle
function that can be used to determine if the given subdriver should handle _something_. Below is the function
definition:
- can_handle(opts, driver, device, ...)
Check if this subdriver can handle a given context
- Parameters
opts (
table
) – A table containing optional arguments that can be used to determine if something is handleabledriver (
Driver
) – the driver contextdevice (
Device
) – the device we are checking handling against... (
vararg
) – The additional context values, typically the message that is being checked for handling
As you can see, some of the args are a bit nebulous including a vararg option at the end. This is to allow for the flexibility of using the same function in many contexts. However, the most common example would be to handle devices of a specific model (or manufacturer) in which case the function can be very simple:
local my_subdriver = {
can_handle = function(opts, driver, device, ...)
return device:get_model() == "modelOne"
end
}
In this case, this is for a ZigbeeDriver
so we are using the function to get the model of the
device and branching on that, and we will use that in all situations for the subdriver.
In addition to the can_handle
function, you can incorporate capability handlers or any
other protocol specific handlers. These will be built into the dispatcher
for these handlers, and follow the corresponding rules for dispatching to the correct handler in the context of that
dispatcher. Additionally, this subdriver could be used for whatever driver-specific uses are needed, specific to your use case.
Example
Below is an example showing a base driver utilizing subdrivers. It has a base driver that provides the
Zigbee standard support for switch and switchLevel capabilities. It registers 2 subdrivers, split into separate files
to make organization and understanding the blocks of behavior better. First, it has a manufacturer_one.lua
subdriver
whose can_handle
verifies that the device has the correct manufacturer. If the device does match that manufacturer,
then instead of using the standard switchLevel support it will arbitrarily generate an event 15 less than it would
otherwise when the device reports, and it will set it to 15 less than the commands sent to it. Similarly, the
manufacturer_two.lua
works functionally the same but with a different manufacturer and different level offset. The
complete driver could now support any standard Zigbee bulb that supported switch and switchLevel as well as two specific
manufacturers bulbs who arbitrarily offset the levels.
init.lua
local capabilities = require "st.capabilities"
local ZigbeeDriver = require "st.zigbee"
local defaults = require "st.zigbee.defaults"
local zigbee_man_model_driver_template = {
NAME = "man-model-example",
supported_capabilities = {
capabilities.switch,
capabilities.switchLevel,
},
sub_drivers = { require("manufacturer_one"), require("manufacturer_two") }
}
defaults.register_for_default_handlers(zigbee_man_model_driver_template,
zigbee_man_model_driver_template.supported_capabilities)
local zigbee_man_model_driver = ZigbeeDriver(zigbee_man_model_driver_template)
zigbee_man_model_driver:run()
manufacturer_one.lua
local zcl_clusters = require "st.zigbee.zcl.clusters"
local Level = zcl_clusters.Level
local capabilities = require "st.capabilities"
local manufacturer_one_handler = {
NAME = "ManufacturerOne",
zigbee_handlers = {
attr = {
[Level.ID] = {
[Level.attributes.CurrentLevel.ID] = function(driver, device, value)
-- Arbitrarily subtract 15 for example purposes
device:emit_event(capabilities.switchLevel.level(math.floor((value.value / 254.0 * 100) + 0.5) - 15))
end
}
}
},
capability_handlers = {
[capabilities.switchLevel.ID] = {
[capabilities.switchLevel.commands.setLevel.ID] = function(driver, device, command)
-- Arbitrarily subtract 15 for example purposes
local level = math.floor((command.args.level - 15)/100.0 * 254)
device:send(Level.commands.client.MoveToLevelWithOnOff(device, level, command.args.rate or 0xFFFF))
end,
}
},
sub_drivers = {}, -- could optionally nest further. The can_handles would be chained
can_handle = function(opts, driver, device, ...)
return device:get_manufacturer() == "manufacturer_one"
end,
}
return manufacturer_one_handler
manufacturer_two.lua
local capabilities = require "st.capabilities"
local zcl_clusters = require "st.zigbee.zcl.clusters"
local Level = zcl_clusters.Level
local manufacturer_two = {
NAME = "ManufacturerTwo",
zigbee_handlers = {
attr = {
[Level.ID] = {
[Level.attributes.CurrentLevel.ID] = function(driver, device, value)
-- Arbitrarily subtract 5 for example purposes
device:emit_event(capabilities.switchLevel.level(math.floor((value.value / 254.0 * 100) + 0.5) - 5))
end
}
}
},
capability_handlers = {
[capabilities.switchLevel.ID] = {
[capabilities.switchLevel.commands.setLevel.ID] = function(driver, device, command)
-- Arbitrarily subtract 5 for example purposes
local level = math.floor((command.args.level - 5)/100.0 * 254)
device:send(Level.commands.client.MoveToLevelWithOnOff(device, level, command.args.rate or 0xFFFF))
end,
}
},
sub_drivers = {},
can_handle = function(opts, driver, device, ...)
return device:get_manufacturer() == "manufacturer_two"
end,
}
SubDriver Class Documentation
- class SubDriver
A SubDriver is a way to bundle groups of functionality that overrides the basic behavior of a given driver by gating it behind a can_handle function.
- can_handle: function
(type: Driver, type: Device, …):boolean whether or not this sub driver, if it has a matching handler, should handle a message
- zigbee_handlers: table
the same zigbee handlers that a driver would have
- zwave_handlers: table
the same zwave handlers that a driver would have
- capability_handlers: table
the same capability handlers that a driver would have
- secret_data_handlers: table
the same secret_data handlers that a driver would have
Driver tests
It is useful to have a set of unit tests for the functionality of your driver. You can see documentation on how to set up driver tests here here