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
33 changes: 31 additions & 2 deletions drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

local capabilities = require "st.capabilities"
local st_utils = require "st.utils"
local constants = require "st.zigbee.constants"
local clusters = require "st.zigbee.zcl.clusters"
local switch_utils = require "switch_utils"

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

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

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

-- Indicates whether a delayed refresh for ZLL devices is in progress, to prevent multiple refreshes in a quick series of step commands
local IS_REFRESH_CALLBACK_QUEUED = "__is_refresh_callback_queued"
-- Stores a timer object, which is required to cancel a timer early
local REFRESH_CALLBACK_TIMER = "__refresh_callback_timer"

local function trigger_delayed_refresh_if_zll(device)
if device:get_profile_id() ~= constants.ZLL_PROFILE_ID then
return
end

-- If a refresh callback is already queued, cancel it and create a new one with the updated time
if device:get_field(IS_REFRESH_CALLBACK_QUEUED) then
device.thread:cancel_timer(device:get_field(REFRESH_CALLBACK_TIMER))
end
local delay_s = 2
local new_timer = device.thread:call_with_delay(delay_s, function()
device:refresh()
device:set_field(IS_REFRESH_CALLBACK_QUEUED, nil)
end)
device:set_field(REFRESH_CALLBACK_TIMER, new_timer)
device:set_field(IS_REFRESH_CALLBACK_QUEUED, true)
end

local function step_color_temperature_by_percent_handler(driver, device, cmd)
if type(device.register_native_capability_cmd_handler) == "function" then
device:register_native_capability_cmd_handler(cmd.capability, cmd.command)
end
local step_percent_change = cmd.args and cmd.args.stepSize or 0
if step_percent_change == 0 then return end
local transition_time = device:get_field(switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
local transition_time = device:get_field(COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
-- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP
local step_mode = (step_percent_change > 0) and clusters.ColorControl.types.CcStepMode.DOWN or clusters.ColorControl.types.CcStepMode.UP
-- note: the field containing the color temp bounds will be associated with a parent device
Expand All @@ -37,6 +64,7 @@ local function step_color_temperature_by_percent_handler(driver, device, cmd)
end
local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0))
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))
trigger_delayed_refresh_if_zll(device)
end

local function step_level_handler(driver, device, cmd)
Expand All @@ -45,9 +73,10 @@ local function step_level_handler(driver, device, cmd)
end
local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254)
if step_size == 0 then return end
local transition_time = device:get_field(switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
local transition_time = device:get_field(SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
local step_mode = (step_size > 0) and clusters.Level.types.MoveStepMode.UP or clusters.Level.types.MoveStepMode.DOWN
device:send(clusters.Level.server.commands.Step(device, step_mode, math.abs(step_size), transition_time, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF))
trigger_delayed_refresh_if_zll(device)
end

local stateless_handlers = {
Expand Down
5 changes: 0 additions & 5 deletions drivers/SmartThings/zigbee-switch/src/switch_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ local switch_utils = {}
switch_utils.MIRED_MAX_BOUND = "__max_mired_bound"
switch_utils.MIRED_MIN_BOUND = "__min_mired_bound"

-- Fields to store the transition times for the stateless capabilities,
-- in case native handler implementations need to be re-configured in the future
switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME = "__switch_level_step_transition_time"
switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME = "__color_temp_step_transition_time"

switch_utils.MIREDS_CONVERSION_CONSTANT = 1000000

switch_utils.convert_mired_to_kelvin = function(mired)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ local OnOff = clusters.OnOff
local Level = clusters.Level
local ColorControl = clusters.ColorControl

local TRANSITION_TIME = 3
local OPTIONS_MASK = 0x01
local IGNORE_COMMAND_IF_OFF = 0x00
local DEFAULT_MIRED_MIN = 154
local DEFAULT_MIRED_MAX = 370

local mock_device = test.mock_device.build_test_zigbee_device(
{ profile = t_utils.get_profile_definition("color-temp-bulb.yml"),
fingerprinted_endpoint_id = 0x01,
Expand Down Expand Up @@ -279,4 +285,74 @@ test.register_coroutine_test(
max_api_version = 19
}
)

test.register_coroutine_test(
"StatelessColorTemperatureStep stepColorTemperatureByPercent should trigger delayed refresh on ZLL device",
function()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot")
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } })
mock_device:expect_native_cmd_handler_registration("statelessColorTemperatureStep", "stepColorTemperatureByPercent")
test.socket.zigbee:__expect_send({
mock_device.id,
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)
})

test.wait_for_events()
test.mock_time.advance_time(2)

test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
end,
{
min_api_version = 20
}
)

test.register_coroutine_test(
"Rapid StatelessSwitchLevelStep stepLevel commands should cancel and recreate delayed refresh timer",
function()
test.socket.zigbee:__set_channel_ordering("relaxed")

test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } })
mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel")
test.socket.zigbee:__expect_send({
mock_device.id,
Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
})
test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot")

test.wait_for_events()
test.mock_time.advance_time(1)

-- Second step command: cancels timer #1, creates timer #2
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } })
mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel")
test.socket.zigbee:__expect_send({
mock_device.id,
Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
})
test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot")

test.wait_for_events()
test.mock_time.advance_time(1)
-- now, nothing should happen since the first timer was cancelled and the second timer has not yet reached its 2s delay

test.wait_for_events()
test.mock_time.advance_time(1)

test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
end,
{
min_api_version = 20
}
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils"
local OnOff = clusters.OnOff
local Level = clusters.Level

local TRANSITION_TIME = 3
local OPTIONS_MASK = 0x01
local IGNORE_COMMAND_IF_OFF = 0x00

local mock_device = test.mock_device.build_test_zigbee_device(
{ profile = t_utils.get_profile_definition("on-off-level.yml"),
fingerprinted_endpoint_id = 0x01,
Expand Down Expand Up @@ -178,4 +182,27 @@ test.register_coroutine_test(
}
)

test.register_coroutine_test(
"StatelessSwitchLevelStep stepLevel should trigger delayed refresh on ZLL device",
function()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot")
test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } })
mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel")
test.socket.zigbee:__expect_send({
mock_device.id,
Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)
})

test.wait_for_events()
test.mock_time.advance_time(2)

test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
end,
{
min_api_version = 19
}
)

test.run_registered_tests()
Loading