Skip to content

Commit 917bdad

Browse files
authored
Zigbee Switch: Add Stateless Step ZLL Refresh (#3006)
1 parent 5b6e514 commit 917bdad

4 files changed

Lines changed: 136 additions & 7 deletions

File tree

drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
local capabilities = require "st.capabilities"
55
local st_utils = require "st.utils"
6+
local constants = require "st.zigbee.constants"
67
local clusters = require "st.zigbee.zcl.clusters"
78
local switch_utils = require "switch_utils"
89

@@ -11,19 +12,47 @@ local DEFAULT_MIRED_MAX_BOUND = 370 -- 2700 Kelvin (Mireds are the inverse of Ke
1112
local DEFAULT_MIRED_MIN_BOUND = 154 -- 6500 Kelvin (Mireds are the inverse of Kelvin)
1213

1314
-- Transition Time: The time that shall be taken to perform the step change, in units of 1/10ths of a second.
15+
-- Specific fields can store custom transition times for stateless capabilities
16+
local SWITCH_LEVEL_STEP_TRANSITION_TIME = "__switch_level_step_transition_time"
17+
local COLOR_TEMP_STEP_TRANSITION_TIME = "__color_temp_step_transition_time"
1418
local DEFAULT_STEP_TRANSITION_TIME = 3 -- 0.3 seconds
1519

1620
-- Options Mask & Override: Indicates which options are being overridden by the Level/ColorControl cluster commands
1721
local OPTIONS_MASK = 0x01 -- default: The `ExecuteIfOff` option is overriden
1822
local IGNORE_COMMAND_IF_OFF = 0x00 -- default: the command will not be executed if the device is off
1923

24+
-- Indicates whether a delayed refresh for ZLL devices is in progress, to prevent multiple refreshes in a quick series of step commands
25+
local IS_REFRESH_CALLBACK_QUEUED = "__is_refresh_callback_queued"
26+
-- Stores a timer object, which is required to cancel a timer early
27+
local REFRESH_CALLBACK_TIMER = "__refresh_callback_timer"
28+
29+
-- Note: These commands' native handlers do not match the driver's ZLL behavior 1-1.
30+
-- Instead, they will queue a 2s timer and read refresh for each command, in all cases.
31+
local function trigger_delayed_refresh_if_zll(device)
32+
if device:get_profile_id() ~= constants.ZLL_PROFILE_ID then
33+
return
34+
end
35+
36+
-- If a refresh callback is already queued, cancel it and create a new one with the updated time
37+
if device:get_field(IS_REFRESH_CALLBACK_QUEUED) then
38+
device.thread:cancel_timer(device:get_field(REFRESH_CALLBACK_TIMER))
39+
end
40+
local delay_s = 2
41+
local new_timer = device.thread:call_with_delay(delay_s, function()
42+
device:refresh()
43+
device:set_field(IS_REFRESH_CALLBACK_QUEUED, nil)
44+
end)
45+
device:set_field(REFRESH_CALLBACK_TIMER, new_timer)
46+
device:set_field(IS_REFRESH_CALLBACK_QUEUED, true)
47+
end
48+
2049
local function step_color_temperature_by_percent_handler(driver, device, cmd)
2150
if type(device.register_native_capability_cmd_handler) == "function" then
2251
device:register_native_capability_cmd_handler(cmd.capability, cmd.command)
2352
end
2453
local step_percent_change = cmd.args and cmd.args.stepSize or 0
2554
if step_percent_change == 0 then return end
26-
local transition_time = device:get_field(switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
55+
local transition_time = device:get_field(COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
2756
-- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP
2857
local step_mode = (step_percent_change > 0) and clusters.ColorControl.types.CcStepMode.DOWN or clusters.ColorControl.types.CcStepMode.UP
2958
-- note: the field containing the color temp bounds will be associated with a parent device
@@ -37,6 +66,7 @@ local function step_color_temperature_by_percent_handler(driver, device, cmd)
3766
end
3867
local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0))
3968
device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, step_mode, step_size_in_mireds, transition_time, min_mireds, max_mireds, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF))
69+
trigger_delayed_refresh_if_zll(device)
4070
end
4171

4272
local function step_level_handler(driver, device, cmd)
@@ -45,9 +75,10 @@ local function step_level_handler(driver, device, cmd)
4575
end
4676
local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254)
4777
if step_size == 0 then return end
48-
local transition_time = device:get_field(switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
78+
local transition_time = device:get_field(SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
4979
local step_mode = (step_size > 0) and clusters.Level.types.MoveStepMode.UP or clusters.Level.types.MoveStepMode.DOWN
5080
device:send(clusters.Level.server.commands.Step(device, step_mode, math.abs(step_size), transition_time, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF))
81+
trigger_delayed_refresh_if_zll(device)
5182
end
5283

5384
local stateless_handlers = {

drivers/SmartThings/zigbee-switch/src/switch_utils.lua

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ local switch_utils = {}
88
switch_utils.MIRED_MAX_BOUND = "__max_mired_bound"
99
switch_utils.MIRED_MIN_BOUND = "__min_mired_bound"
1010

11-
-- Fields to store the transition times for the stateless capabilities,
12-
-- in case native handler implementations need to be re-configured in the future
13-
switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME = "__switch_level_step_transition_time"
14-
switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME = "__color_temp_step_transition_time"
15-
1611
switch_utils.MIREDS_CONVERSION_CONSTANT = 1000000
1712

1813
switch_utils.convert_mired_to_kelvin = function(mired)

drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ local OnOff = clusters.OnOff
1111
local Level = clusters.Level
1212
local ColorControl = clusters.ColorControl
1313

14+
local TRANSITION_TIME = 3
15+
local OPTIONS_MASK = 0x01
16+
local IGNORE_COMMAND_IF_OFF = 0x00
17+
local DEFAULT_MIRED_MIN = 154
18+
local DEFAULT_MIRED_MAX = 370
19+
1420
local mock_device = test.mock_device.build_test_zigbee_device(
1521
{ profile = t_utils.get_profile_definition("color-temp-bulb.yml"),
1622
fingerprinted_endpoint_id = 0x01,
@@ -279,4 +285,74 @@ test.register_coroutine_test(
279285
max_api_version = 19
280286
}
281287
)
288+
289+
test.register_coroutine_test(
290+
"StatelessColorTemperatureStep stepColorTemperatureByPercent should trigger delayed refresh on ZLL device",
291+
function()
292+
test.socket.zigbee:__set_channel_ordering("relaxed")
293+
test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot")
294+
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } })
295+
mock_device:expect_native_cmd_handler_registration("statelessColorTemperatureStep", "stepColorTemperatureByPercent")
296+
test.socket.zigbee:__expect_send({
297+
mock_device.id,
298+
ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 43, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
299+
})
300+
301+
test.wait_for_events()
302+
test.mock_time.advance_time(2)
303+
304+
test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
305+
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
306+
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
307+
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
308+
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
309+
end,
310+
{
311+
min_api_version = 20
312+
}
313+
)
314+
315+
test.register_coroutine_test(
316+
"Rapid StatelessSwitchLevelStep stepLevel commands should cancel and recreate delayed refresh timer",
317+
function()
318+
test.socket.zigbee:__set_channel_ordering("relaxed")
319+
320+
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } })
321+
mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel")
322+
test.socket.zigbee:__expect_send({
323+
mock_device.id,
324+
Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
325+
})
326+
test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot")
327+
328+
test.wait_for_events()
329+
test.mock_time.advance_time(1)
330+
331+
-- Second step command: cancels timer #1, creates timer #2
332+
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } })
333+
mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel")
334+
test.socket.zigbee:__expect_send({
335+
mock_device.id,
336+
Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
337+
})
338+
test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot")
339+
340+
test.wait_for_events()
341+
test.mock_time.advance_time(1)
342+
-- now, nothing should happen since the first timer was cancelled and the second timer has not yet reached its 2s delay
343+
344+
test.wait_for_events()
345+
test.mock_time.advance_time(1)
346+
347+
test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
348+
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
349+
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
350+
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
351+
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
352+
end,
353+
{
354+
min_api_version = 20
355+
}
356+
)
357+
282358
test.run_registered_tests()

drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils"
1010
local OnOff = clusters.OnOff
1111
local Level = clusters.Level
1212

13+
local TRANSITION_TIME = 3
14+
local OPTIONS_MASK = 0x01
15+
local IGNORE_COMMAND_IF_OFF = 0x00
16+
1317
local mock_device = test.mock_device.build_test_zigbee_device(
1418
{ profile = t_utils.get_profile_definition("on-off-level.yml"),
1519
fingerprinted_endpoint_id = 0x01,
@@ -178,4 +182,27 @@ test.register_coroutine_test(
178182
}
179183
)
180184

185+
test.register_coroutine_test(
186+
"StatelessSwitchLevelStep stepLevel should trigger delayed refresh on ZLL device",
187+
function()
188+
test.socket.zigbee:__set_channel_ordering("relaxed")
189+
test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot")
190+
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } })
191+
mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel")
192+
test.socket.zigbee:__expect_send({
193+
mock_device.id,
194+
Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
195+
})
196+
197+
test.wait_for_events()
198+
test.mock_time.advance_time(2)
199+
200+
test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
201+
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
202+
end,
203+
{
204+
min_api_version = 19
205+
}
206+
)
207+
181208
test.run_registered_tests()

0 commit comments

Comments
 (0)