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 timer

  • delay_s (number) – the number of seconds to wait before hitting the callback

  • callback (function) – the function to call when the timer expires. @see Driver.timer_callback_template

  • name (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 timer

  • interval_s (number) – the number of seconds to wait between hitting the callback

  • callback (function) – the function to call when the timer expires. @see Driver.timer_callback_template

  • name (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 timer

  • t (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 commands

  • capability_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 commands

  • device (st.Device) – the device that this command was sent to

  • cap_command (table) – the capability command table including the capability, command, component and args

  • quiet (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.

Parameters
  • self (Driver) – the driver to handle the capability command

  • device (st.Device) – the device for which this command is injected

  • cap_command (table) – the capability command table including the capability, command, component and args

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 events

  • lifecycle_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 events

  • ch (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 events

  • discovery_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 events

  • environment_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 driver

  • secret_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 events

  • security_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 running

  • device_uuid (str) – the uuid of the device to get info for

  • force_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 events

  • message_channel (message_channel) – the message channel to listen to input on

  • callback (function) – the callback function to call when there is data on the message channel

  • name (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 events

  • message_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

Parameters
  • cls (Driver) – class to be instantiated

  • name (str) – the name of the driver used for logging

  • template (table) – a template with any override or necessary driver information

Returns

the constructed driver context

Return type

Driver

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. See Driver.default_capability_match_driverSwitched_handler as an example. Updating the devices metadata field provisioning_state to either NONFUNCTIONAL or PROVISIONED 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 handleable

  • driver (Driver) – the driver context

  • device (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