Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
ids_env
.luarocks/config-5.3.lua
.luarocks/default-lua-version.lua
lua
luarocks
smartthingsedgedrivers-dev-1.rockspec
test-api-key
lua_libs-api*
4 changes: 4 additions & 0 deletions lutron-aurora/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: Lutron Aurora
packageKey: lutron_aurora
permissions:
zigbee: {}
6 changes: 6 additions & 0 deletions lutron-aurora/fingerprints.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
zigbeeManufacturer:
- id: Lutron/Aurora
deviceLabel: Lutron Aurora
manufacturer: Lutron
model: Z3-1BRL
deviceProfileName: lutron-aurora.v1
28 changes: 28 additions & 0 deletions lutron-aurora/profiles/lutron-aurora-profile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: lutron-aurora.v1
components:
- id: main
capabilities:
- id: switch
version: 1
- id: switchLevel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you also want to emulate "switch"?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is sort of up in the air, this is currently setup to just be a button and a dimmer but it could be a switch instead

version: 1
config:
values:
- key: "level.value"
range: [1, 100]
- id: refresh
version: 1
- id: battery
version: 1
categories:
- name: Switch
preferences:
- preferenceType: integer
name: stepSize
title: Step Size
required: true
description: The % step size to use for dimmer events
definition:
min: 1
max: 50
default: 5
14 changes: 14 additions & 0 deletions lutron-aurora/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Lutron Aurora

Adds preliminary support for the Lutron Aurora button/dimmer.

- button press
- As apposed to using the switch capability for on/off, this driver will emit a button press
- No double-press or hold events are available
- dimmer
- Dimmer events are interpreted as either increase or decrease based on the direction they
are turned, this maps to the preference for `stepSize` which will determine what % an increase
or decrease represents
- I have tested this with 3 different Aurora's from the factory (one of which was joined to hue)
and observed that the decrease event always sends 2 and increase always sends > 3 but I
wouldn't be surprised if other devices act differently.
189 changes: 189 additions & 0 deletions lutron-aurora/src/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
local zcl_clusters = require "st.zigbee.zcl.clusters"
local Level = zcl_clusters.Level
local OnOff = zcl_clusters.OnOff
local ZigbeeDriver = require "st.zigbee"
local capabilities = require "st.capabilities"
local device_management = require "st.zigbee.device_management"
local utils = require "st.utils"
local zcl_commands = require "st.zigbee.zcl.global_commands"
local read_responder = require "read_responder"

local PowerConfiguration = zcl_clusters.PowerConfiguration
local Groups = zcl_clusters.Groups

local LAST_LEVEL_EVENT = "LAST_LEVEL_EVENT"
local DEVICE_GROUP = "DEVICE_GROUP"
--server: Basic, PowerConfiguration, Identify, TouchlinkCommissioning, 0xFC00
--client: Identify, Groups, OnOff, Level, OTAUpgrade, TouchlinkCommissioning

local function do_configure(driver, device)
if not driver.environment_info.hub_zigbee_eui then
return driver:call_with_delay(1, function()
do_configure(driver, device)
end)
end
device:send(device_management.build_bind_request(device, PowerConfiguration.ID, driver.environment_info.hub_zigbee_eui))
device:send(device_management.build_bind_request(device, Level.ID, driver.environment_info.hub_zigbee_eui))
device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 30,
21600, 1))
end

--- The shared logic for both `init` and `added` events.
local function start_device(driver, device)
if not driver.environment_info.hub_zigbee_eui then
return driver:call_with_delay(1, function()
start_device(driver, device)
end)
end
device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device))
end

local function added_handler(driver, device)
start_device(driver, device)
device:emit_event(capabilities.switch.switch.on({ state_change = false }))
device:emit_event(capabilities.switchLevel.level(0, { state_change = false }))
end

local function init_handler(driver, device)
start_device(driver, device)
do_configure(driver, device)
end

local function battery_perc_attr_handler(_, device, value, _)
device:emit_event(capabilities.battery.battery(utils.clamp_value(math.floor(value.value / 2), 0,
100)))
end

local function handle_on_off(device, on)
print("handle_on_off", device.label, level)
if on then
device:emit_event(capabilities.switch.switch.on())
else
device:emit_event(capabilities.switch.switch.off())
end
end

local function handle_level(device, level)
print("handle_level", device.label, level)
level = utils.clamp_value(level, 0, 100)
level = math.floor(level)
device:emit_event(capabilities.switchLevel.level(level))
end

local function level_event_handler(driver, device, cmd)
-- The device will always send events with a transition_time
-- of 7 for on/off events and the value will be either 255 or 0, the memory on the device
-- seems to reset after about 1 second so 0 is a bit rare. If the transition_time is 2 then
-- the event is a dimmer event, the value with either will be 2 for a reduction or > 3
-- for an increase. We are converting these dimmer events into individual steps based on the
-- device preference
local level_step = device.preferences.stepSize or 5
local value = cmd.body.zcl_body.level.value
local time = cmd.body.zcl_body.transition_time.value
print(device.label, "level_event_handler", value, time)
if time == 7 then
local current = device:get_latest_state("main", "switch", "switch", "off")
print(device.label, "switch event", current)
if current == "off" then
handle_on_off(device, true)
else
handle_on_off(device, false)
end
elseif time == 2 then
local current = device:get_latest_state("main", "switchLevel", "level", 0)
print(device.label, "dimmer event", current)
-- look up the last event which will either be 3 or some larger value
local last_event = device:get_field(LAST_LEVEL_EVENT) or 3
-- if the event is > than the last (or 3) then increase the switchLevel by
-- level_step, otherwise reduce it by level_step
local added = value > last_event and level_step or -level_step
handle_level(device, current + added, 0, 100)
device:set_field(LAST_LEVEL_EVENT, value)
-- to guard against quick increase to decrease transitions we wait for 1 second and
-- reset the last event level to 3
driver:call_with_delay(1, function()
device:set_field(LAST_LEVEL_EVENT, 3)
end)
end
print("level_event_handler exit", device:get_latest_state("main", "switchLevel", "level", 0))
end

local function group_handler(driver, device, info)
print("group_handler", device.label, info.body.zcl_body.group_id.value)
-- shortly after onboarding the device will send a GroupAdd event so we tell
-- the hub to join that group. I am saving this in the device data because I feel
-- like we should be leaving the group on device remove but that API doesn't seem to exist
device:set_field(DEVICE_GROUP, info.body.zcl_body.group_id.value, {persist = true})
driver:add_hub_to_zigbee_group(info.body.zcl_body.group_id.value)
end

local driver_template = {
supported_capabilities = {
capabilities.battery,
capabilities.switchLevel,
capabilities.switch,
capabilities.refresh,
},
lifecycle_handlers = {
doConfigure = do_configure,
added = added_handler,
init = init_handler,
},
driver_lifecycle = function()
os.exit(0)
end,
zigbee_handlers = {
global = {
[Level.ID] = {
[zcl_commands.ReportAttribute.ID] = function(_driver, device, info)
return read_responder(device)
end
},
},
attr = {
[PowerConfiguration.ID] = {
[PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_perc_attr_handler
},
[Level.ID] = {
[Level.attributes.CurrentLevel.ID] = function(_, _, info)
print(utils.stringify_table(info, "CurrentLevel", true))
end
}
},
cluster = {
[Level.ID] = {
[Level.server.commands.MoveToLevelWithOnOff.ID] = level_event_handler,
},
[Groups.ID] = {
[Groups.server.commands.AddGroup.ID] = group_handler,
},
[OnOff.ID] = {
[OnOff.server.commands.On.ID] = function(...)
print("OnOff->On", ...)
end,
[OnOff.server.commands.Off.ID] = function(...)
print("OnOff->OFF", ...)
end,
}
},
},
capability_handlers = {
[capabilities.switch.ID] = {
[capabilities.switch.commands.on.NAME] = function(_, device)
handle_on_off(device, true)
end,
[capabilities.switch.commands.off.NAME] = function(_, device)
handle_on_off(device, false)
end
},
[capabilities.switchLevel.ID] = {
[capabilities.switchLevel.commands.setLevel.NAME] = function (_, device, cmd)
print("level:", utils.stringify_table(cmd))
handle_level(device, cmd.args.level)
end,
},
},
}

local driver = ZigbeeDriver("Lutron Aurora", driver_template)
driver:run()
46 changes: 46 additions & 0 deletions lutron-aurora/src/read_responder.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
local zb_messages = require "st.zigbee.messages"
local zcl_messages = require "st.zigbee.zcl"
local zb_const = require "st.zigbee.constants"
local data_types = require "st.zigbee.data_types"
local read_attr_response = require "st.zigbee.zcl.global_commands.read_attribute_response"
local Level = require "st.zigbee.zcl.clusters".Level
local Status = require "st.zigbee.generated.types.ZclStatus"
local Uint8 = require "st.zigbee.data_types.Uint8"
local utils = require "st.utils"
--- The goal here is to respond to the device's `Read` message with the current
--- value of the switchLevel propery.
---
--- note: this seems to not really help with the device losing its own state
return function(device, info)
print(utils.stringify_table(info, "read_responder", true))
local current = device:get_latest_state("main", "switchLevel", "level", 0)
local adjusted = math.floor((255 - 2) * (current / 100)) + 2
local response = read_attr_response.ReadAttributeResponse({
read_attr_response.ReadAttributeResponseAttributeRecord(
Level.attributes.CurrentLevel.ID,
Status.SUCCESS,
Uint8.ID,
adjusted
)
})
local zclh = zcl_messages.ZclHeader({
cmd = data_types.ZCLCommandId(read_attr_response.ReadAttributeResponse.ID)
})
local addrh = zb_messages.AddressHeader(
zb_const.HUB.ADDR,
zb_const.HUB.ENDPOINT,
device:get_short_address(),
device:get_endpoint(Level.ID),
zb_const.HA_PROFILE_ID,
Level.ID
)
local message_body = zcl_messages.ZclMessageBody({
zcl_header = zclh,
zcl_body = response,
})
local tx = zb_messages.ZigbeeMessageTx({
address_header = addrh,
body = message_body,
})
device:send(tx)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that doing this would allow the device itself to send an accurate state, instead of treating it like a stateless dimmer where events are either < or > 3. I am now seeing this go out and maybe get acknowledged but not change in the device.

# received read request
< ZigbeeMessageRx || type: 0x00, < AddressHeader || src_addr: 0xF298, src_endpoint: 0x01, dest_addr: 0x0000, dest_endpoint: 0xFF, profile: 0x0104, cluster: Level >, lqi: 0xC4, rssi: -61, body_length: 0x0005, < ZCLMessageBody || < ZCLHeader || frame_ctrl: 0x10, seqno: 0x75, ZCLCommandId: 0x00 >, < ReadAttribute || AttributeId: 0x0000 > > >
# sent response
< ZigbeeMessageTx || Uint16: 0x0000, < AddressHeader || src_addr: 0x0000, src_endpoint: 0x01, dest_addr: 0xF298, dest_endpoint: 0x01, profile: 0x0104, cluster: Level >, < ZCLMessageBody || < ZCLHeader || frame_ctrl: 0x00, seqno: 0x00, ZCLCommandId: 0x01 >, < ReadAttributeResponse || < AttributeRecord || attr_id: 0x0000, ZclStatus: SUCCESS, DataType: Uint8, data: 0xE5 > > > >
# Level
< ZigbeeMessageRx || type: 0x00, < AddressHeader || src_addr: 0xF298, src_endpoint: 0x01, dest_addr: 0x0000, dest_endpoint: 0xFF, profile: 0x0104, cluster: Level >, lqi: 0xD4, rssi: -64, body_length: 0x0006, < ZCLMessageBody || < ZCLHeader || frame_ctrl: 0x11, seqno: 0x77, ZCLCommandId: 0x04 >, < MoveToLevelWithOnOff || level: 0x02, transition_time: 0x0002 > > >
# ack?
< ZigbeeMessageRx || type: 0x00, < AddressHeader || src_addr: 0xF298, src_endpoint: 0x01, dest_addr: 0x0000, dest_endpoint: 0x01, profile: 0x0104, cluster: Level >, lqi: 0xD6, rssi: -64, body_length: 0x0005, < ZCLMessageBody || < ZCLHeader || frame_ctrl: 0x08, seqno: 0x14, ZCLCommandId: 0x0B >, < DefaultResponse || cmd: 0x01, ZclStatus: SUCCESS > > >

It seems odd that we get the MoveToLevel event before the ack (assuming that it is an ack?)

end