From 98b234acde2141fb80d3bdd590ded5d5cfaffcb8 Mon Sep 17 00:00:00 2001 From: seojune Date: Tue, 20 Oct 2026 14:04:56 +0900 Subject: [PATCH 1/2] Add Aqara Roller Shade T1S --- .../zigbee-window-treatment/fingerprints.yml | 5 + .../src/aqara/fingerprints.lua | 3 +- .../src/aqara/roller-shade/can_handle.lua | 6 +- .../src/aqara/roller-shade/init.lua | 1 + .../src/aqara/roller-shade/sub_drivers.lua | 8 + .../src/aqara/roller-shade/t1s/can_handle.lua | 11 + .../src/aqara/roller-shade/t1s/init.lua | 204 ++++++ ...indow_treatment_aqara_roller_shade_t1s.lua | 663 ++++++++++++++++++ 8 files changed, 899 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index 021d59d2d4..d527eba595 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -29,6 +29,11 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.curtain.agl001 deviceProfileName: window-treatment-aqara-curtain-driver-e1 + - id: "LUMI/lumi.curtain.vagl02" + deviceLabel: Roller Shade Controller T1S + manufacturer: LUMI + model: lumi.curtain.vagl02 + deviceProfileName: window-treatment-aqara-roller-shade-rotate - id: "REXENSE/KG0001" deviceLabel: Window Treatment manufacturer: REXENSE diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua index 8ad05530a5..25e79714db 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua @@ -5,7 +5,8 @@ local FINGERPRINTS = { { mfr = "LUMI", model = "lumi.curtain" }, { mfr = "LUMI", model = "lumi.curtain.v1" }, { mfr = "LUMI", model = "lumi.curtain.aq2" }, - { mfr = "LUMI", model = "lumi.curtain.agl001" } + { mfr = "LUMI", model = "lumi.curtain.agl001" }, + { mfr = "LUMI", model = "lumi.curtain.vagl02" } } return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua index 2e91fa090d..8463153596 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua @@ -1,8 +1,12 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local MODELS = { + ["lumi.curtain.aq2"] = true, + ["lumi.curtain.vagl02"] = true +} local function roller_shade_can_handle(opts, driver, device, ...) - if device:get_model() == "lumi.curtain.aq2" then + if MODELS[device:get_model()] then return true, require("aqara.roller-shade") end return false diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index 909b65dd3e..5b4272f25e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -137,6 +137,7 @@ local aqara_roller_shade_handler = { } }, can_handle = require("aqara.roller-shade.can_handle"), + sub_drivers = require("aqara.roller-shade.sub_drivers"), } return aqara_roller_shade_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua new file mode 100644 index 0000000000..ac313df186 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara.roller-shade.t1s"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/can_handle.lua new file mode 100644 index 0000000000..9f7bb428e9 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function t1s_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.vagl02" then + return true, require("aqara.roller-shade.t1s") + end + return false +end + +return t1s_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua new file mode 100644 index 0000000000..c3b538a473 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua @@ -0,0 +1,204 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" +local data_types = require "st.zigbee.data_types" +local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" +local utils = require "st.utils" + +local Groups = clusters.Groups +local WindowCovering = clusters.WindowCovering +local AnalogOutput = clusters.AnalogOutput + +local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] +local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] +local shadeRotateState = capabilities["stse.shadeRotateState"] +local setRotateStateCommandName = "setRotateState" +local directionReversal = "directionReversal" + +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local INIT_STATE_ATTRIBUTE_ID = 0x0407 +local DIRECTION_ATTRIBUTE_ID = 0x0400 + +local ROTATE_UP_VALUE = 0x0006 +local ROTATE_DOWN_VALUE = 0x0005 +local SHADE_STATE_STOP = 2 +local SHADE_STATE_OPEN = 1 +local SHADE_STATE_CLOSE = 0 +local SHADE_STATE_BLOCKED = 4 + +local INIT_STATE_REPORTED = "initStateReported" + +local function is_not_init(device) + if not device:get_field(INIT_STATE_REPORTED) then return false end + + -- True if uninitialized; false if already initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + local ret = true + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then ret = false end + return ret +end + + +local function window_shade_level_cmd(driver, device, command) + -- Cannot be controlled if not initialized + if is_not_init(device) then return end + local level = command.args.shadeLevel > 100 and 100 or utils.round(command.args.shadeLevel) + device:emit_event(capabilities.windowShadeLevel.shadeLevel(level)) + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level)) +end + +local function window_shade_open_cmd(driver, device, command) + -- Cannot be controlled if not initialized + if is_not_init(device) then return end + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) +end + +local function window_shade_close_cmd(driver, device, command) + -- Cannot be controlled if not initialized + if is_not_init(device) then return end + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) +end + +local function set_rotate_command_handler(driver, device, command) + device:emit_event(shadeRotateState.rotateState.idle({ state_change = true, visibility = { displayed = false } })) -- update UI + + -- Cannot be controlled if not initialized + if is_not_init(device) then return end + local state = command.args.state + if state == "rotateUp" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + elseif state == "rotateDown" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + end +end + +local function do_refresh(driver, device) + device:send(AnalogOutput.attributes.PresentValue:read(device)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, INIT_STATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE)) +end + +local function init_state_handler(driver, device, value, zb_rx) + device:set_field(INIT_STATE_REPORTED, true, { persist = true }) + local state = value.value == 0x02 and initializedStateWithGuide.initializedStateWithGuide.initialized() or + initializedStateWithGuide.initializedStateWithGuide.notInitialized() + device:emit_event(state) +end + +local function shade_level_report_handler(driver, device, value, zb_rx) + local level = value.value > 100 and 100 or utils.round(value.value) + device:emit_event(capabilities.windowShadeLevel.shadeLevel(level)) + if level >= 100 then + device:emit_event(capabilities.windowShade.windowShade.open()) + elseif level <= 0 then + device:emit_event(capabilities.windowShade.windowShade.closed()) + else + device:emit_event(capabilities.windowShade.windowShade.partially_open()) + end +end + +-- The position of this device is reported through the present value of the analog output +-- cluster, where 0 is fully closed and 100 is fully open. The lift percentage of the window +-- covering cluster is reported with the opposite polarity (100% = fully closed), so it is +-- ignored here to keep the parent Aqara handler from emitting an inverted shade level. +local function current_position_lift_percentage_handler(driver, device, value, zb_rx) +end + +local function shade_state_report_handler(driver, device, value, zb_rx) + local state = value.value + -- update state ui + if state == SHADE_STATE_STOP or state == SHADE_STATE_BLOCKED then + -- read shade position to update the UI + device:send(AnalogOutput.attributes.PresentValue:read(device)) + elseif state == SHADE_STATE_OPEN then + device:emit_event(capabilities.windowShade.windowShade.opening()) + elseif state == SHADE_STATE_CLOSE then + device:emit_event(capabilities.windowShade.windowShade.closing()) + end +end + +local function device_info_changed(driver, device, event, args) + if device.preferences ~= nil then + local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] + if reverseRollerShadeDirPrefValue ~= nil and + reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then + local raw_value = reverseRollerShadeDirPrefValue and true or false + device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + DIRECTION_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, raw_value)) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, + { visibility = { displayed = false } })) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, + capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, + capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", initializedStateWithGuide, + initializedStateWithGuide.initializedStateWithGuide.NAME, + initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false } })) + device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) +end + +local function do_configure(self, device) + device:configure() + device:send(Groups.server.commands.RemoveAllGroups(device)) + do_refresh(self, device) +end + +local aqara_roller_shade_t1s_handler = { + NAME = "Aqara T1S Blinder Controller Handler", + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure, + infoChanged = device_info_changed + }, + capability_handlers = { + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, + [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, + -- [capabilities.windowShade.commands.pause.NAME] = window_shade_pause_cmd, + }, + [shadeRotateState.ID] = { + [setRotateStateCommandName] = set_rotate_command_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + attr = { + [aqara_utils.PRIVATE_CLUSTER_ID] = { + [INIT_STATE_ATTRIBUTE_ID] = init_state_handler, + }, + [AnalogOutput.ID] = { + [AnalogOutput.attributes.PresentValue.ID] = shade_level_report_handler + }, + [MULTISTATE_CLUSTER_ID] = { + [MULTISTATE_ATTRIBUTE_ID] = shade_state_report_handler + } + } + }, + can_handle = require("aqara.roller-shade.t1s.can_handle"), +} + +return aqara_roller_shade_t1s_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua new file mode 100644 index 0000000000..f67273c6ab --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua @@ -0,0 +1,663 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Test for Aqara Roller Shade Controller T1S (lumi.curtain.vagl02) +-- The T1S reports its position through AnalogOutput.PresentValue (0 = closed, 100 = open), +-- its motion state through the MultistateOutput cluster and its travel-point initialization +-- state through a manufacturer specific attribute of the Aqara private cluster. + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local SinglePrecisionFloat = require "st.zigbee.data_types".SinglePrecisionFloat +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] +local shadeRotateState = capabilities["stse.shadeRotateState"] +local shadeRotateStateId = "stse.shadeRotateState" +test.add_package_capability("initializedStateWithGuide.yaml") +test.add_package_capability("shadeRotateState.yaml") + +local Basic = clusters.Basic +local WindowCovering = clusters.WindowCovering +local AnalogOutput = clusters.AnalogOutput +local Groups = clusters.Groups + +local PRIVATE_CLUSTER_ID = 0xFCC0 +local PRIVATE_ATTRIBUTE_ID = 0x0009 +local MFG_CODE = 0x115F +local INIT_STATE_ATTRIBUTE_ID = 0x0407 +local DIRECTION_ATTRIBUTE_ID = 0x0400 + +local INIT_STATE_INITIALIZED = 0x02 +local INIT_STATE_NOT_INITIALIZED = 0x00 + +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local ROTATE_UP_VALUE = 0x0006 +local ROTATE_DOWN_VALUE = 0x0005 +local SHADE_STATE_CLOSE = 0 +local SHADE_STATE_OPEN = 1 +local SHADE_STATE_STOP = 2 +local SHADE_STATE_BLOCKED = 4 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("window-treatment-aqara-roller-shade-rotate.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.curtain.vagl02", + server_clusters = { Basic.ID, AnalogOutput.ID, MULTISTATE_CLUSTER_ID, WindowCovering.ID, PRIVATE_CLUSTER_ID } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +-- Helpers ------------------------------------------------------------------- + +local function build_init_state_report(value) + return zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, + { { INIT_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, value } }, MFG_CODE) +end + +local function build_shade_state_report(value) + return zigbee_test_utils.build_attribute_report(mock_device, MULTISTATE_CLUSTER_ID, + { { MULTISTATE_ATTRIBUTE_ID, data_types.Uint16.ID, value } }) +end + +local function build_rotate_write(value) + local message = cluster_base.write_manufacturer_specific_attribute(mock_device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint16, value) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + return message +end + +-- Mark the device as initialized (travel points set) so that control commands are accepted +local function set_initialized() + test.socket.zigbee:__queue_receive({ mock_device.id, build_init_state_report(INIT_STATE_INITIALIZED) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() +end + +-- Mark the device as not initialized so that control commands are rejected +local function set_not_initialized() + test.socket.zigbee:__queue_receive({ mock_device.id, build_init_state_report(INIT_STATE_NOT_INITIALIZED) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.notInitialized())) + test.wait_for_events() +end + +-- Lifecycle ----------------------------------------------------------------- + +test.register_coroutine_test( + "Handle added lifecycle", + function() + -- The initial window shade events should be sent during the first time onboarding of the device + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({ visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, + data_types.Uint8, 1) }) + + -- After a driver switch-over the added lifecycle is re-triggered. The stateful events + -- (shadeLevel, windowShade, initializedStateWithGuide) must not be re-emitted so that the + -- previously known state is preserved. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({ visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, + data_types.Uint8, 1) }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Handle doConfigure lifecycle", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, WindowCovering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:configure_reporting(mock_device, 0, 600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Groups.server.commands.RemoveAllGroups(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_attribute_read(mock_device, PRIVATE_CLUSTER_ID, { INIT_STATE_ATTRIBUTE_ID }, MFG_CODE) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 14 + } +) + +-- Initialization state -------------------------------------------------------- + +test.register_coroutine_test( + "Init state report 0x02 should be handled as initialized", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, build_init_state_report(INIT_STATE_INITIALIZED) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Init state report other than 0x02 should be handled as notInitialized", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, build_init_state_report(INIT_STATE_NOT_INITIALIZED) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.notInitialized())) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, build_init_state_report(0x01) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.notInitialized())) + end, + { + min_api_version = 14 + } +) + +-- Shade level (AnalogOutput.PresentValue) ------------------------------------- + +test.register_coroutine_test( + "Window shade state closed", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:build_test_attr_report(mock_device, SinglePrecisionFloat(0, -127, 0)) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade state open", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:build_test_attr_report(mock_device, SinglePrecisionFloat(0, 6, 0.5625)) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade state partially open", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:build_test_attr_report(mock_device, SinglePrecisionFloat(0, 5, 0.5625)) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade level above 100 should be clamped to 100", + function() + test.socket.capability:__set_channel_ordering("relaxed") + -- 120 = 1.875 * 2^6 + test.socket.zigbee:__queue_receive( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:build_test_attr_report(mock_device, SinglePrecisionFloat(0, 6, 0.875)) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + end, + { + min_api_version = 14 + } +) + +-- Shade motion state (MultistateOutput.PresentValue) -------------------------- + +test.register_coroutine_test( + "Shade state report open should emit opening", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, build_shade_state_report(SHADE_STATE_OPEN) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Shade state report close should emit closing", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, build_shade_state_report(SHADE_STATE_CLOSE) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Shade state report stop should read the shade position", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, build_shade_state_report(SHADE_STATE_STOP) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Shade state report blocked should read the shade position", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, build_shade_state_report(SHADE_STATE_BLOCKED) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + }) + end, + { + min_api_version = 14 + } +) + +-- Window shade commands --------------------------------------------------------- + +test.register_coroutine_test( + "Window shade open cmd handler", + function() + set_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade close cmd handler", + function() + set_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "close", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade pause cmd handler", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "pause", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.Stop(mock_device) + }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade setShadeLevel cmd handler", + function() + set_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 30 } } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(30)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 30) + }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade commands should be allowed before the init state has been reported", + function() + -- A device that has not answered the travel point read must remain controllable + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Window shade commands should be ignored when the device is not initialized", + function() + set_not_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + } + ) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "close", args = {} } + } + ) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 30 } } + } + ) + -- No zigbee message and no shadeLevel event is expected + end, + { + min_api_version = 14 + } +) + +-- Stateless step --------------------------------------------------------------- + +test.register_coroutine_test( + "statelessWindowShadeLevelStep should step from the current shade level", + function() + set_initialized() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:build_test_attr_report(mock_device, SinglePrecisionFloat(0, 5, 0.5625)) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(60)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 60) + }) + end, + { + min_api_version = 17 + } +) + +-- Rotate ------------------------------------------------------------------------ + +test.register_coroutine_test( + "Rotate up cmd handler", + function() + set_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = shadeRotateStateId, component = "main", command = "setRotateState", args = { "rotateUp" } } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({ state_change = true, visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send({ mock_device.id, build_rotate_write(ROTATE_UP_VALUE) }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Rotate down cmd handler", + function() + set_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = shadeRotateStateId, component = "main", command = "setRotateState", args = { "rotateDown" } } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({ state_change = true, visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send({ mock_device.id, build_rotate_write(ROTATE_DOWN_VALUE) }) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Rotate cmd should only reset the UI when the device is not initialized", + function() + set_not_initialized() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = shadeRotateStateId, component = "main", command = "setRotateState", args = { "rotateUp" } } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({ state_change = true, visibility = { displayed = false } })) + ) + -- No zigbee message is expected + end, + { + min_api_version = 14 + } +) + +-- Preferences -------------------------------------------------------------------- + +test.register_coroutine_test( + "Handle reverseRollerShadeDir true in infoChanged", + function() + local updates = { preferences = {} } + updates.preferences["stse.reverseRollerShadeDir"] = true + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, DIRECTION_ATTRIBUTE_ID, + MFG_CODE, data_types.Boolean, true) + } + ) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Handle reverseRollerShadeDir false in infoChanged", + function() + local updates = { preferences = {} } + updates.preferences["stse.reverseRollerShadeDir"] = false + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, DIRECTION_ATTRIBUTE_ID, + MFG_CODE, data_types.Boolean, false) + } + ) + end, + { + min_api_version = 14 + } +) + +-- Refresh --------------------------------------------------------------------------- + +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_attribute_read(mock_device, PRIVATE_CLUSTER_ID, { INIT_STATE_ATTRIBUTE_ID }, MFG_CODE) + }) + end, + { + min_api_version = 14 + } +) + +test.run_registered_tests() From 901759bd899feda77dd1be531d3a3b8440179b8b Mon Sep 17 00:00:00 2001 From: seojune Date: Fri, 30 Oct 2026 10:55:21 +0900 Subject: [PATCH 2/2] remove unnecessary code and fix minor errors --- .../src/aqara/roller-shade/sub_drivers.lua | 2 +- .../src/aqara/roller-shade/t1s/init.lua | 37 +++++----- ...indow_treatment_aqara_roller_shade_t1s.lua | 68 +++++++++++++++++-- 3 files changed, 83 insertions(+), 24 deletions(-) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua index ac313df186..9cab6945a9 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/sub_drivers.lua @@ -3,6 +3,6 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("aqara.roller-shade.t1s"), + lazy_load_if_possible("aqara.roller-shade.t1s"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua index c3b538a473..8bd5569c9f 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/t1s/init.lua @@ -18,7 +18,6 @@ local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] local shadeRotateState = capabilities["stse.shadeRotateState"] local setRotateStateCommandName = "setRotateState" -local directionReversal = "directionReversal" local MULTISTATE_CLUSTER_ID = 0x0013 local MULTISTATE_ATTRIBUTE_ID = 0x0055 @@ -32,20 +31,12 @@ local SHADE_STATE_OPEN = 1 local SHADE_STATE_CLOSE = 0 local SHADE_STATE_BLOCKED = 4 -local INIT_STATE_REPORTED = "initStateReported" - local function is_not_init(device) - if not device:get_field(INIT_STATE_REPORTED) then return false end - - -- True if uninitialized; false if already initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - local ret = true - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then ret = false end - return ret + -- True until the device reports that its travel points have been initialized + return device:get_latest_state("main", initializedStateWithGuide.ID, initializedStateWithGuide.initializedStateWithGuide.NAME) ~= + initializedStateWithGuide.initializedStateWithGuide.initialized.NAME end - local function window_shade_level_cmd(driver, device, command) -- Cannot be controlled if not initialized if is_not_init(device) then return end @@ -91,9 +82,8 @@ local function do_refresh(driver, device) end local function init_state_handler(driver, device, value, zb_rx) - device:set_field(INIT_STATE_REPORTED, true, { persist = true }) local state = value.value == 0x02 and initializedStateWithGuide.initializedStateWithGuide.initialized() or - initializedStateWithGuide.initializedStateWithGuide.notInitialized() + initializedStateWithGuide.initializedStateWithGuide.notInitialized() device:emit_event(state) end @@ -113,7 +103,13 @@ end -- cluster, where 0 is fully closed and 100 is fully open. The lift percentage of the window -- covering cluster is reported with the opposite polarity (100% = fully closed), so it is -- ignored here to keep the parent Aqara handler from emitting an inverted shade level. +-- The device does not report the init state when its travel points have been set, so it is +-- re-read whenever the shade moves while the device is still not initialized. local function current_position_lift_percentage_handler(driver, device, value, zb_rx) + if is_not_init(device) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + INIT_STATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE)) + end end local function shade_state_report_handler(driver, device, value, zb_rx) @@ -134,7 +130,7 @@ local function device_info_changed(driver, device, event, args) local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] if reverseRollerShadeDirPrefValue ~= nil and reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then - local raw_value = reverseRollerShadeDirPrefValue and true or false + local raw_value = reverseRollerShadeDirPrefValue device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, DIRECTION_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, raw_value)) end @@ -154,6 +150,11 @@ local function device_added(driver, device) device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false } })) device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) + + -- The direction is stored on the device and survives a re-pairing, so sync it with the preference + local reverse = device.preferences ~= nil and device.preferences[reverseRollerShadeDir.ID] == true + device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + DIRECTION_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, reverse)) end local function do_configure(self, device) @@ -175,8 +176,7 @@ local aqara_roller_shade_t1s_handler = { }, [capabilities.windowShade.ID] = { [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, - [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, - -- [capabilities.windowShade.commands.pause.NAME] = window_shade_pause_cmd, + [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd }, [shadeRotateState.ID] = { [setRotateStateCommandName] = set_rotate_command_handler @@ -193,6 +193,9 @@ local aqara_roller_shade_t1s_handler = { [AnalogOutput.ID] = { [AnalogOutput.attributes.PresentValue.ID] = shade_level_report_handler }, + [WindowCovering.ID] = { + [WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = current_position_lift_percentage_handler + }, [MULTISTATE_CLUSTER_ID] = { [MULTISTATE_ATTRIBUTE_ID] = shade_state_report_handler } diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua index f67273c6ab..cae1e5eb17 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_t1s.lua @@ -128,6 +128,9 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, DIRECTION_ATTRIBUTE_ID, MFG_CODE, + data_types.Boolean, false) }) -- After a driver switch-over the added lifecycle is re-triggered. The stateful events -- (shadeLevel, windowShade, initializedStateWithGuide) must not be re-emitted so that the @@ -143,6 +146,9 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, DIRECTION_ATTRIBUTE_ID, MFG_CODE, + data_types.Boolean, false) }) end, { min_api_version = 14 @@ -279,6 +285,54 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "CurrentPositionLiftPercentage report should be ignored", + function() + set_initialized() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + ) + test.wait_for_events() + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "CurrentPositionLiftPercentage report should read the init state when the device is not initialized", + function() + set_not_initialized() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_attribute_read(mock_device, PRIVATE_CLUSTER_ID, { INIT_STATE_ATTRIBUTE_ID }, MFG_CODE) + }) + test.wait_for_events() + + -- Once the device reports that it is initialized, the init state is not read anymore + set_initialized() + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + ) + test.wait_for_events() + end, + { + min_api_version = 14 + } +) + test.register_coroutine_test( "Window shade level above 100 should be clamped to 100", function() @@ -443,19 +497,21 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Window shade commands should be allowed before the init state has been reported", + "Window shade commands should be ignored before the init state has been reported", function() - -- A device that has not answered the travel point read must remain controllable test.socket.capability:__queue_receive( { mock_device.id, { capability = "windowShade", component = "main", command = "open", args = {} } } ) - test.socket.zigbee:__expect_send({ - mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) - }) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 30 } } + } + ) + -- No zigbee message and no shadeLevel event is expected end, { min_api_version = 14