diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua index 602aa34bd4..84d787e377 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua @@ -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() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua index fd07648021..591bf78628 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -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" @@ -48,10 +48,10 @@ 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 @@ -59,6 +59,8 @@ utils.step_shade_level_handler = function(driver, device, command) 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)