بنقرة واحدة
understanding-lua-libraries
// Understanding the SmartThings Edge Driver Lua libraries - driver lifecycle, message dispatchers, default handlers, and protocol message objects
// Understanding the SmartThings Edge Driver Lua libraries - driver lifecycle, message dispatchers, default handlers, and protocol message objects
Setting up the development environment, deploying Edge Drivers to hubs, and sharing drivers with other users via channels and invites
Running luacheck for Lua linting and following code style conventions in Edge Driver development
Running and writing integration tests for SmartThings Edge Drivers using the Python test harness and Lua integration test framework
Understanding and defining SmartThings capabilities, device profiles, preferences, and embedded device configurations for Edge Drivers
| name | understanding-lua-libraries |
| description | Understanding the SmartThings Edge Driver Lua libraries - driver lifecycle, message dispatchers, default handlers, and protocol message objects |
A driver is created by calling Driver("name", template) (or a protocol-specific variant like ZigbeeDriver("name", template)). The template is a Lua table containing handler tables and configuration.
The base Driver.init (in lua_libs/st/driver.lua) does the following:
out_driver.NAME from the name argumentcapability_handlers, lifecycle_handlers, message_handlerscapability_channel, environment_channel, lifecycle_channel, driver_lifecycle_channel, and optionally discovery_channelDriver.standardize_sub_drivers() to normalize the sub_drivers listlifecycle_dispatcher and capability_dispatcher from handlers + sub_driversThe driver:run() call starts the cosock event loop, which runs forever processing messages from all registered channels.
The dispatcher system (lua_libs/st/dispatcher.lua) is a hierarchical message routing tree. The base class MessageDispatcher provides:
default_handlers - handlers at this level of the hierarchy.child_dispatchers - sub-dispatchers (from sub_drivers) that may override defaultscan_handle(driver, device, ...) - returns true if this dispatcher or a child can handle the messagedispatch(driver, device, ...) - finds and executes the matching handlercan_handle on each child dispatcher| Dispatcher | Class | Handles |
|---|---|---|
capability_dispatcher | CapabilityCommandDispatcher | Capability commands from the platform (on, off, setLevel, etc.) |
lifecycle_dispatcher | DeviceLifecycleDispatcher | Device lifecycle events (added, init, removed, etc.) |
zigbee_message_dispatcher | ZigbeeMessageDispatcher | Incoming Zigbee messages (attribute reports, cluster commands, ZDO) |
zwave_dispatcher | ZwaveDispatcher | Incoming Z-Wave commands |
matter_dispatcher | MatterMessageDispatcher | Incoming Matter interaction responses |
secret_data_dispatcher | SecretDataDispatcher | Security/secret data events |
Each protocol-specific driver (ZigbeeDriver, ZwaveDriver, MatterDriver) adds its own dispatcher on top of the base Driver's capability and lifecycle dispatchers.
Zigbee handler structure:
zigbee_handlers = {
attr = { -- attribute reports / read responses
[ClusterID] = {
[AttributeID] = handler_function,
}
},
global = { -- global ZCL commands
[ClusterID] = {
[CommandID] = handler_function,
}
},
cluster = { -- cluster-specific commands
[ClusterID] = {
[CommandID] = handler_function,
}
},
zdo = { -- ZDO commands
[ClusterID] = handler_function,
}
}
Z-Wave handler structure:
zwave_handlers = {
[cc.SWITCH_BINARY] = { -- command class
[SwitchBinary.REPORT] = handler_function, -- command ID
},
}
Matter handler structure:
matter_handlers = {
attr = {
[ClusterID] = {
[AttributeID] = handler_function,
}
},
cmd_response = { ... },
event = { ... },
fallback = handler_function,
}
Capability handler structure:
capability_handlers = {
[capabilities.switch.ID] = {
[capabilities.switch.commands.on.NAME] = handle_on,
[capabilities.switch.commands.off.NAME] = handle_off,
},
[capabilities.switchLevel.ID] = {
[capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level,
},
}
Sub-drivers allow device-specific behavior overrides gated by a can_handle function. A sub-driver is a table with:
NAME (string)can_handle(opts, driver, device, ...) -> booleancapability_handlers, lifecycle_handlerssub_driversIn practice, sub-drivers are often organized as separate files under src/sub_drivers/ for clarity, and required in the main driver template.
can_handle on each child dispatcherSub-drivers support lazy loading for memory optimization:
Driver.lazy_load_sub_driver(sub_driver): Strips handlers, keeps only can_handle and NAMEDriver.lazy_load_sub_driver_v2(require_path): Even more efficient; only requires can_handle and sub_drivers modules separatelyNew sub-drivers must be:
sub_drivers.lua (or the equivalent sub_drivers table)can_handle.lua that correctly identifies the target devicesinit.lua that returns the sub-driver tableIf any of these are missing, the sub-driver will not be loaded.
Device lifecycle events are dispatched through the DeviceLifecycleDispatcher. The key events:
init -- Called for every device on driver startup (existing devices) and after added for new devices. Used for setting up component/endpoint mappings and device fields.added -- Called only when a device is first paired. NOT called for existing devices when a driver is updated. After added, a synthetic init is automatically dispatched.doConfigure -- Called when the device needs configuration (typically after pairing).infoChanged -- Called when device metadata changes (e.g., preferences updated). Receives args.old_st_store for comparison.removed -- Called when device is removed.driverSwitched -- Called when device switches to this driver.Register lifecycle handlers in the template:
lifecycle_handlers = {
init = device_init,
added = device_added,
removed = device_removed,
doConfigure = device_do_configure,
infoChanged = info_changed_handler,
}
Handler signature: function(driver, device, event, args)
Default behaviors provided by the framework:
driverSwitched: Base Driver marks device as NONFUNCTIONAL. ZigbeeDriver overrides this to check capability matching and marks as PROVISIONED if all capabilities match.doConfigure: ZigbeeDriver defaults to device_management.configure which sends attribute reporting configuration.added: After a successful added callback, the framework automatically queues a synthetic init event.doConfigure: After success, the framework transitions the device to PROVISIONED state.Critical timing knowledge for lifecycle events
init on driver startup. After hub restart the radio may not be ready and sending Zigbee/Z-Wave commands in init can fail.added is NOT called for existing devices on driver update. Only called on first pair. Code that must run for existing devices should go in init (for non-radio operations) or use driverSwitched.doConfigure is called any time a device is added with the TYPED provisioning state and is the right place for device-specific configuration commands.infoChanged receives args.old_st_store for comparing old vs new preferences. Drivers should check if a preference actually changed before acting on it.-- Base driver (for virtual/LAN devices)
local Driver = require "st.driver"
-- Protocol-specific drivers
local ZigbeeDriver = require "st.zigbee"
local ZwaveDriver = require "st.zwave.driver"
local MatterDriver = require "st.matter.driver"
-- Capabilities
local capabilities = require "st.capabilities"
-- Zigbee defaults (pre-built handlers for common capabilities)
local defaults = require "st.zigbee.defaults"
-- Zigbee clusters (for building commands/reading attributes)
local zcl_clusters = require "st.zigbee.zcl"
-- Z-Wave command classes
local cc = require "st.zwave.CommandClass"
local SwitchBinary = require "st.zwave.CommandClass.SwitchBinary"
-- Matter clusters
local clusters = require "st.matter.clusters"
-- Utilities
local utils = require "st.utils"
local json = require "st.json"
local log = require "log"
-- Coroutine runtime
local cosock = require "cosock"
-- LAN utils
local socket = cosock.socket
local luncheon = require "luncheon"
local luxure = require "luxure"
local lustre = require "lustre"
local capabilities = require "st.capabilities"
local ZigbeeDriver = require "st.zigbee"
local defaults = require "st.zigbee.defaults"
local template = {
supported_capabilities = {
capabilities.switch,
capabilities.switchLevel,
capabilities.colorControl,
capabilities.colorTemperature,
},
sub_drivers = require("sub_drivers"),
lifecycle_handlers = {
init = device_init,
added = device_added,
},
}
-- Register default Zigbee handlers for all supported capabilities
defaults.register_for_default_handlers(template,
template.supported_capabilities,
{native_capability_cmds_enabled = true, native_capability_attrs_enabled = true}
)
local driver = ZigbeeDriver("zigbee_switch", template)
driver:run()
This pattern - declare supported capabilities, register defaults, add overrides via sub_drivers and lifecycle_handlers, then construct and run - is the standard structure for all protocol-based Edge drivers.
When a driver declares supported_capabilities in its template, the framework automatically registers default handlers for each capability. The registration uses or-merge
logic: driver-defined handlers always take precedence over defaults. If the driver already registered a handler for a given cluster/attribute/command slot, the default
is silently skipped.
Registration happens in st.{zigbee,zwave,matter}.defaults.init.lua via register_for_default_handlers(driver, capabilities, opts):
supported_capabilitieszigbee_handlers, zwave_handlers, or matter_handlers (only where driver hasn't defined one)attribute_configurations (Zigbee), get_refresh_commands (Z-Wave), or subscribed_attributes (Matter)The default doConfigure handler (device_management.configure):
refresh command (reads all configured attributes)device:configure() which iterates all configured attributes and for each:
0x0500doConfigure calls device:default_configure() which calls device:refresh(). The default refresh iterates get_refresh_commands from all default capability modules and sends Get commands for each supported CC.
Refresh collects get_refresh_commands from all default modules, sends Get commands
TODO
Load the testing-edge-drivers skill for details on the built in unit test framework for to test Zigbee, Z-Wave, and Matter drivers.