-
Notifications
You must be signed in to change notification settings - Fork 2
Add Lutron Aurora driver #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
079f06a
af57732
ab87c58
3ec3ba2
69ef9c3
bc11192
63fe303
c9c4441
b73bc74
55a4006
5f5d7ab
abc2e4c
b036fa3
e70b18a
30f1db0
6a22d95
ce9cc33
9ff9ffa
7fe5f5d
22f61e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| name: Lutron Aurora | ||
| packageKey: lutron_aurora | ||
| permissions: | ||
| zigbee: {} |
| 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 |
| 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 | ||
| 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 | ||
| 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. |
| 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() |
| 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) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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