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
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,66 @@ test.register_coroutine_test(
{ min_api_version = 17 }
)

-- Test: a step after the accumulation window expires starts from the reported level again
test.register_coroutine_test(
"statelessWindowShadeLevelStep with Level cluster - step after timeout starts from current shade level",
function()
test.timer.__create_and_queue_test_time_advance_timer(121, "oneshot")
test.socket.zigbee:__set_channel_ordering("relaxed")
test.socket.zigbee:__queue_receive({
axis_device.id,
Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 127)
})
test.socket.capability:__expect_send({
axis_device.id,
{ capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } }
})
test.socket.capability:__expect_send({
axis_device.id,
{ capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } }
})
test.wait_for_events()

test.socket.capability:__queue_receive({
axis_device.id,
{ capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } }
})
test.socket.capability:__expect_send({
axis_device.id,
{ capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 60 } }
})
test.socket.capability:__expect_send({
axis_device.id,
{ capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } }
})
test.socket.zigbee:__expect_send({
axis_device.id,
Level.server.commands.MoveToLevelWithOnOff(axis_device, 152)
})
test.wait_for_events()

-- The 120 second window has elapsed, so the next step is computed from the current shade level (60)
test.mock_time.advance_time(121)
test.wait_for_events()

test.socket.capability:__queue_receive({
axis_device.id,
{ capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } }
})
test.socket.capability:__expect_send({
axis_device.id,
{ capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 70 } }
})
test.socket.capability:__expect_send({
axis_device.id,
{ capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } }
})
test.socket.zigbee:__expect_send({
axis_device.id,
Level.server.commands.MoveToLevelWithOnOff(axis_device, 178)
})
end,
{ min_api_version = 17 }
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ utils.PRESET_LEVEL_KEY = "_presetLevel"
-- within a short time frame while preventing indefinite accumulation.
local LATEST_TARGET_LEVEL = "_latestTargetLevel"
-- Timeout in seconds for the stateless step accumulation window
local TARGET_LEVEL_TIME_OUT_SECONDS = 15
local TARGET_LEVEL_TIME_OUT_SECONDS = 120
-- Field name for the timer that clears LATEST_TARGET_LEVEL after timeout
local TARGET_LEVEL_TIME_OUT = "_targetLevelTimeOut"

Expand Down Expand Up @@ -48,17 +48,19 @@ utils.step_shade_level_handler = function(driver, device, command)

-- Step from the latest target level if it exists, or from the current shade level
local latest_target_level = device:get_field(LATEST_TARGET_LEVEL) or
device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME, 0)
device:get_latest_state(command.component, capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME, 0)
local new_target_level = st_utils.clamp_value(latest_target_level + step, 0, 100)

-- Cancel any previous timer and set a 15 second timeout timer to ensure LATEST_TARGET_LEVEL is cleared
-- Cancel any previous timer and set a 120 second timeout timer to ensure LATEST_TARGET_LEVEL is cleared
-- This is to prevent the stateless step from accumulating indefinitely, while ensuring that a single
-- "scroll" action can accumulate multiple steps within a short time frame
if device:get_field(TARGET_LEVEL_TIME_OUT) then
device.thread:cancel_timer(device:get_field(TARGET_LEVEL_TIME_OUT))
end
local timer = device.thread:call_with_delay(TARGET_LEVEL_TIME_OUT_SECONDS, function(d)
device:set_field(LATEST_TARGET_LEVEL, nil)
-- A completed timer cannot be cancelled, so drop the reference as well
device:set_field(TARGET_LEVEL_TIME_OUT, nil)
end)
device:set_field(TARGET_LEVEL_TIME_OUT, timer)
device:set_field(LATEST_TARGET_LEVEL, new_target_level)
Expand Down
Loading