diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 0a093645f8..43a46ad71d 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -40,7 +40,10 @@ local zigbee_window_treatment_driver_template = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = window_shade_utils.set_preset_position_cmd, [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_utils.window_shade_preset_cmd, - } + }, + [capabilities.statelessWindowShadeLevelStep.ID] = { + [capabilities.statelessWindowShadeLevelStep.commands.stepShadeLevel.NAME] = window_shade_utils.step_shade_level_handler() + }, }, lifecycle_handlers = { init = init_handler, diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua deleted file mode 100644 index 69cfd4393a..0000000000 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function invert_lift_percentage_can_handle(opts, driver, device, ...) - if device:get_manufacturer() == "IKEA of Sweden" or - device:get_manufacturer() == "Smartwings" or - device:get_manufacturer() == "Insta GmbH" - then - return true, require("invert-lift-percentage") - end - return false -end - -return invert_lift_percentage_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/can_handle.lua new file mode 100644 index 0000000000..fe37292be4 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/can_handle.lua @@ -0,0 +1,22 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function invert_lift_percentage_can_handle(opts, driver, device, ...) + local somfy_can_handle = require("inverted-lift.somfy.can_handle") + local yoolax_can_handle = require("inverted-lift.yoolax.can_handle") + local rooms_beautiful_can_handle = require("inverted-lift.rooms-beautiful.can_handle") + local vimar_can_handle = require("inverted-lift.vimar.can_handle") + if somfy_can_handle(opts, driver, device) + or yoolax_can_handle(opts, driver, device) + or rooms_beautiful_can_handle(opts, driver, device) + or vimar_can_handle(opts, driver, device) + or device:get_manufacturer() == "IKEA of Sweden" + or device:get_manufacturer() == "Smartwings" + or device:get_manufacturer() == "Insta GmbH" + then + return true, require("inverted-lift") + end + return false +end + +return invert_lift_percentage_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/init.lua similarity index 78% rename from drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/init.lua index b586459b9a..d90c8c0eae 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/init.lua @@ -56,21 +56,21 @@ local function current_position_attr_handler(driver, device, value, zb_rx) end end -local function set_shade_level(device, value, command) +local function set_shade_level_helper(device, value, command) local level = 100 - value device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level)) end -local function window_shade_level_cmd(driver, device, command) - set_shade_level(device, command.args.shadeLevel, command) +local function set_shade_level_handler(driver, device, command) + set_shade_level_helper(device, command.args.shadeLevel, command) end -local function window_shade_preset_cmd(driver, device, command) +local function preset_position_handler(driver, device, command) local level = window_shade_utils.get_preset_level(device, command.component) - set_shade_level(device, level, command) + set_shade_level_helper(device, level, command) end -local ikea_window_treatment = { +local inverted_window_treatment = { NAME = "inverted lift percentage", zigbee_handlers = { attr = { @@ -80,14 +80,19 @@ local ikea_window_treatment = { } }, capability_handlers = { + [capabilities.statelessWindowShadeLevelStep.ID] = { + [capabilities.statelessWindowShadeLevelStep.commands.stepShadeLevel.NAME] = + window_shade_utils.step_shade_level_handler(true) -- invert_shade_level is true for devices under this subdriver + }, [capabilities.windowShadeLevel.ID] = { - [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = set_shade_level_handler }, [capabilities.windowShadePreset.ID] = { - [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset_cmd - } + [capabilities.windowShadePreset.commands.presetPosition.NAME] = preset_position_handler + }, }, - can_handle = require("invert-lift-percentage.can_handle"), + can_handle = require("inverted-lift.can_handle"), + sub_drivers = require("inverted-lift.sub_drivers") } -return ikea_window_treatment +return inverted_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/can_handle.lua similarity index 72% rename from drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/can_handle.lua index 6bc25d2f91..2fea4af0f5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/can_handle.lua @@ -2,10 +2,10 @@ -- Licensed under the Apache License, Version 2.0 local is_zigbee_window_shade = function(opts, driver, device) - local FINGERPRINTS = require("rooms-beautiful.fingerprints") + local FINGERPRINTS = require("inverted-lift.rooms-beautiful.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("rooms-beautiful") + return true, require("inverted-lift.rooms-beautiful") end end return false diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/init.lua similarity index 98% rename from drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/init.lua index bb868a8716..1bbf8c8f33 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/rooms-beautiful/init.lua @@ -108,7 +108,7 @@ local rooms_beautiful_handler = { init = battery_defaults.build_linear_voltage_init(2.5, 3.0), infoChanged = info_changed }, - can_handle = require("rooms-beautiful.can_handle"), + can_handle = require("inverted-lift.rooms-beautiful.can_handle"), } return rooms_beautiful_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/can_handle.lua similarity index 75% rename from drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/can_handle.lua index 27a6c83ca3..3b470dfae1 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/can_handle.lua @@ -2,10 +2,10 @@ -- Licensed under the Apache License, Version 2.0 local is_zigbee_window_shade = function(opts, driver, device) - local FINGERPRINTS = require("somfy.fingerprints") + local FINGERPRINTS = require("inverted-lift.somfy.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("somfy") + return true, require("inverted-lift.somfy") end end return false diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/init.lua similarity index 98% rename from drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/init.lua index da416ba9ea..ba9a1644ad 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/somfy/init.lua @@ -109,7 +109,7 @@ local somfy_handler = { } } }, - can_handle = require("somfy.can_handle"), + can_handle = require("inverted-lift.somfy.can_handle"), } return somfy_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/sub_drivers.lua new file mode 100644 index 0000000000..745fb6a203 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/sub_drivers.lua @@ -0,0 +1,11 @@ +-- 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("inverted-lift.vimar"), + lazy_load_if_possible("inverted-lift.yoolax"), + lazy_load_if_possible("inverted-lift.rooms-beautiful"), + lazy_load_if_possible("inverted-lift.somfy"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/can_handle.lua similarity index 75% rename from drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/can_handle.lua index 1d72817eae..80c5406f1e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/can_handle.lua @@ -2,10 +2,10 @@ -- Licensed under the Apache License, Version 2.0 local is_zigbee_window_shade = function(opts, driver, device) - local FINGERPRINTS = require("vimar.fingerprints") + local FINGERPRINTS = require("inverted-lift.vimar.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("vimar") + return true, require("inverted-lift.vimar") end end return false diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/init.lua similarity index 99% rename from drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/init.lua index dd5ea15aed..c56b8e09f7 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/vimar/init.lua @@ -154,7 +154,7 @@ local vimar_handler = { lifecycle_handlers = { init = device_init }, - can_handle = require("vimar.can_handle"), + can_handle = require("inverted-lift.vimar.can_handle"), } return vimar_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/can_handle.lua similarity index 75% rename from drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/can_handle.lua index 006fa3e1bb..2de8928c14 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/can_handle.lua @@ -2,10 +2,10 @@ -- Licensed under the Apache License, Version 2.0 local function is_yoolax_window_shade(opts, driver, device) - local FINGERPRINTS = require("yoolax.fingerprints") + local FINGERPRINTS = require("inverted-lift.yoolax.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("yoolax") + return true, require("inverted-lift.yoolax") end end return false diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/init.lua similarity index 99% rename from drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua rename to drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/init.lua index 5a593cdf2c..a705b94b44 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/inverted-lift/yoolax/init.lua @@ -138,7 +138,7 @@ local yoolax_window_shade = { } }, }, - can_handle = require("yoolax.can_handle"), + can_handle = require("inverted-lift.yoolax.can_handle"), } return yoolax_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/can_handle.lua deleted file mode 100644 index 209af9f887..0000000000 --- a/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/can_handle.lua +++ /dev/null @@ -1,13 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" - -return function(opts, driver, device) - local can_handle = device:supports_capability(capabilities.statelessWindowShadeLevelStep) - if can_handle then - local subdriver = require("stateless_handler") - return true, subdriver - end - return false -end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/fingerprints.lua deleted file mode 100644 index 583cf82ef9..0000000000 --- a/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/fingerprints.lua +++ /dev/null @@ -1,79 +0,0 @@ --- Copyright 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- Brand-specific configuration for window treatment devices --- Supports statelessWindowShadeLevelStep capability - -local FINGERPRINTS = { - -- invert_level = true:The level values on the device side and the driver side need to be inverted. - { - name = "aqara", - mfr = "LUMI", - models = { "lumi.curtain", "lumi.curtain.v1", "lumi.curtain.aq2" }, - invert_level = false, - }, - { - name = "vimar", - mfr = "Vimar", - models = { "Window_Cov_v1.0", "Window_Cov_Module_v1.0" }, - invert_level = true, - }, - { - name = "somfy", - mfr = "SOMFY", - models = { "Glydea Ultra Curtain", "Sonesse 30 WF Roller", "Sonesse 40 Roller" }, - invert_level = true, - }, - { - name = "invert-lift-percentage", - mfrs = { "IKEA of Sweden", "Smartwings", "Insta GmbH" }, - models = {}, - invert_level = true, - }, - { - name = "yoolax", - mfrs = { "Yookee", "yooksmart" }, - models = { "D10110" }, - invert_level = true, - }, - -- use_level_cluster = true: Use Level cluster instead of WindowCovering - -- Level cluster uses 0-254 range, converted from percentage: level_value = math.floor(percentage / 100.0 * 254) - { - name = "feibit", - mfr = "Feibit Co.Ltd", - models = { "FTB56-ZT218AK1.6", "FTB56-ZT218AK1.8" }, - use_level_cluster = true, - }, - { - name = "axis", - mfr = "AXIS", - models = {}, - use_level_cluster = true, - }, - -- Standard devices (no special handling needed) - -- Use WindowCovering.GoToLiftPercentage with 0-100 percentage - { - name = "rooms-beautiful", - mfr = "Rooms Beautiful", - models = { "C001" }, - invert_level = true, - }, - { - name = "hanssem", - mfr = "_TZE204_fzbskaga", - models = { "TS0601" }, - use_tuya_cluster = true, - }, - { - name = "screen-innovations", - mfr = "Screen Innovations", - models = { "WM25/L-Z" }, - }, - { - name = "sombra-shades", - mfr = "Sombra Shades", - models = { "SOMBRA/Z-M" }, - }, -} - -return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/init.lua deleted file mode 100644 index 9e37a37bdc..0000000000 --- a/drivers/SmartThings/zigbee-window-treatment/src/stateless_handler/init.lua +++ /dev/null @@ -1,178 +0,0 @@ --- 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 utils = require "st.utils" -local FINGERPRINTS = require "stateless_handler.fingerprints" -local ZigbeeZcl = require "st.zigbee.zcl" -local Messages = require "st.zigbee.messages" -local data_types = require "st.zigbee.data_types" -local ZigbeeConstants = require "st.zigbee.constants" -local generic_body = require "st.zigbee.generic_body" - --- Tuya cluster constants (for hanssem) -local TUYA_CLUSTER = 0xEF00 -local DP_TYPE_VALUE = "\x02" -local DP_ID_SET_POSITION = "\x02" - --- Get next sequence number for Tuya commands (per-device storage to avoid counter conflicts) -local function get_next_tuya_seq_num(device) - local seq = device:get_field("tuya_seq_num") or 0 - seq = (seq + 1) % 65536 - device:set_field("tuya_seq_num", seq) - return seq -end - --- When the curtain is moving, LATEST_TARGET_LEVEL is used to store the latest target position value, --- which will be cleared when the curtain status is updated. -local LATEST_TARGET_LEVEL = "_latestTargetLevel" - --- Get fingerprint for a device -local function get_fingerprint(device) - local manufacturer = device:get_manufacturer() or "" - local model = device:get_model() or "" - - for _, config in ipairs(FINGERPRINTS) do - -- Match manufacturer (case-insensitive) - -- Support both single mfr string and mfrs array - local mfr_match = false - if config.mfrs then - -- Check if manufacturer is in the mfrs array - for _, mfr in ipairs(config.mfrs) do - if string.lower(manufacturer) == string.lower(mfr) then - mfr_match = true - break - end - end - elseif config.mfr then - mfr_match = string.lower(manufacturer) == string.lower(config.mfr) - end - - if mfr_match then - -- If models list is empty, match any model from this manufacturer - if #config.models == 0 then - return config - end - -- Check if model matches any in the list (case-insensitive exact match) - for _, model_pattern in ipairs(config.models) do - if string.lower(model) == string.lower(model_pattern) then - return config - end - end - end - end - return nil -- No matching fingerprint, use default behavior -end - --- Step shade level handler for statelessWindowShadeLevelStep capability -local function step_shade_level_handler(driver, device, command) - -- Get fingerprint-specific configuration - local fingerprint = get_fingerprint(device) - - -- Support both args.stepSize (named) and args[1] (array) formats - local step = command.args.stepSize or command.args[1] - - if not step or step == 0 then - return - end - - -- When LATEST_TARGET_LEVEL is empty, it means the curtain motor is not moving, - -- and capabilities.windowShadeLevel.shadeLevel is used as the reference for position offset calculation; - -- when LATEST_TARGET_LEVEL is not empty, it means the curtain motor is moving, - -- and LATEST_TARGET_LEVEL is used as the reference for position offset calculation to obtain a more accurate position calculation. - local latest_target_level = device:get_field(LATEST_TARGET_LEVEL) - local current_level = latest_target_level or - device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 - - -- Calculate command level (user's expected percentage) - local command_level = utils.clamp_value(current_level + step, 0, 100) - - command_level = utils.round(command_level) - - -- Apply fingerprint-specific inversion if needed - local device_target_level = command_level - if fingerprint and fingerprint.invert_level then - device_target_level = 100 - command_level - end - - -- Set target_level for tracking (store UI value) - device:set_field(LATEST_TARGET_LEVEL, command_level) - - -- Send command based on fingerprint configuration - if fingerprint and fingerprint.use_level_cluster then - -- Feibit/Axis uses Level cluster - local level_value = math.floor(device_target_level / 100.0 * 254) - device:send_to_component(command.component, clusters.Level.server.commands.MoveToLevelWithOnOff(device, level_value)) - elseif fingerprint and fingerprint.use_tuya_cluster then - -- Hanssem uses Tuya custom cluster 0xEF00 - local strSeqNum = string.pack(">I2", get_next_tuya_seq_num(device)) - local value = string.pack(">I4", device_target_level) - local LenOfValue = string.pack(">I2", string.len(value)) - local PayloadBody = generic_body.GenericBody(strSeqNum .. DP_ID_SET_POSITION .. DP_TYPE_VALUE .. LenOfValue .. value) - local zclh = ZigbeeZcl.ZclHeader({cmd = data_types.ZCLCommandId(0x00)}) - zclh.frame_ctrl:set_cluster_specific() - local addrh = Messages.AddressHeader( - ZigbeeConstants.HUB.ADDR, - ZigbeeConstants.HUB.ENDPOINT, - device:get_short_address(), - device:get_endpoint(TUYA_CLUSTER), - ZigbeeConstants.HA_PROFILE_ID, - TUYA_CLUSTER - ) - local MsgBody = ZigbeeZcl.ZclMessageBody({zcl_header = zclh, zcl_body = PayloadBody}) - local TxMsg = Messages.ZigbeeMessageTx({address_header = addrh, body = MsgBody}) - device:send(TxMsg) - else - -- Standard: use WindowCovering.GoToLiftPercentage - device:send_to_component(command.component, clusters.WindowCovering.server.commands.GoToLiftPercentage(device, device_target_level)) - end -end - -local function shade_level_report_handler(driver, device, value, zb_rx) - -- Since the curtain position status has been updated, - -- the target value is no longer used as a reference for position offset calculation, - -- so LATEST_TARGET_LEVEL needs to be set to nil. - device:set_field(LATEST_TARGET_LEVEL, nil) - -- Get fingerprint configuration - local fingerprint = get_fingerprint(device) - - -- If no fingerprint configuration exists, call the main driver's default handler - if fingerprint == nil then - local windowShade_defaults = require "st.zigbee.defaults.windowShade_defaults" - windowShade_defaults.default_current_lift_percentage_handler(driver, device, value, zb_rx) - end -end - -local stateless_handler = { - NAME = "Zigbee Window Treatment Stateless Step Handlers", - capability_handlers = { - [capabilities.statelessWindowShadeLevelStep.ID] = { - [capabilities.statelessWindowShadeLevelStep.commands.stepShadeLevel.NAME] = step_shade_level_handler, - }, - }, - zigbee_handlers = { - attr = { - -- Window Covering cluster: handles standard curtain/blind devices - -- (e.g., Vimar: Window_Cov_v1.0/Window_Cov_Module_v1.0, SOMFY: Glydea Ultra Curtain/Sonesse series, - -- IKEA: KADRILJ/FYRTUR, Smartwings: WM25/L-Z, Insta GmbH: NEXENTRO Blinds Actuator, - -- Yookee: D10110, Rooms Beautiful: C001, Screen Innovations: WM25/L-Z) - [clusters.WindowCovering.ID] = { - [clusters.WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = shade_level_report_handler, - }, - -- Level cluster: handles devices that use Level cluster for position control - -- (e.g., Feibit: FTB56-ZT218AK1.6/FTB56-ZT218AK1.8, Axis: Gear) - [clusters.Level.ID] = { - [clusters.Level.attributes.CurrentLevel.ID] = shade_level_report_handler, - }, - -- Analog Output cluster: handles Aqara devices that report position via AnalogOutput - -- (e.g., lumi.curtain, lumi.curtain.v1, lumi.curtain.aq2, lumi.curtain.agl001) - [clusters.AnalogOutput.ID] = { - [clusters.AnalogOutput.attributes.PresentValue.ID] = shade_level_report_handler, - }, - }, - }, - can_handle = require("stateless_handler.can_handle") -} - -return stateless_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua index 4487a31030..5d1ed84adb 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua @@ -3,18 +3,13 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("vimar"), lazy_load_if_possible("aqara"), lazy_load_if_possible("feibit"), - lazy_load_if_possible("somfy"), - lazy_load_if_possible("invert-lift-percentage"), - lazy_load_if_possible("rooms-beautiful"), + lazy_load_if_possible("inverted-lift"), lazy_load_if_possible("axis"), - lazy_load_if_possible("yoolax"), lazy_load_if_possible("hanssem"), lazy_load_if_possible("screen-innovations"), lazy_load_if_possible("VIVIDSTORM"), lazy_load_if_possible("HOPOsmart"), - lazy_load_if_possible("stateless_handler"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_stateless_handler.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_stateless_handler.lua index 894cc1f81c..81a8fd5de5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_stateless_handler.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_stateless_handler.lua @@ -417,6 +417,9 @@ test.register_coroutine_test( aqara_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } }) + test.socket.capability:__expect_send( + aqara_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(60)) + ) test.socket.zigbee:__expect_send({ aqara_device.id, WindowCovering.server.commands.GoToLiftPercentage(aqara_device, 60) @@ -431,6 +434,9 @@ test.register_coroutine_test( aqara_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } }) + test.socket.capability:__expect_send( + aqara_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(70)) + ) test.socket.zigbee:__expect_send({ aqara_device.id, WindowCovering.server.commands.GoToLiftPercentage(aqara_device, 70) @@ -445,6 +451,9 @@ test.register_coroutine_test( aqara_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -20 } } }) + test.socket.capability:__expect_send( + aqara_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) test.socket.zigbee:__expect_send({ aqara_device.id, WindowCovering.server.commands.GoToLiftPercentage(aqara_device, 50) @@ -459,6 +468,9 @@ test.register_coroutine_test( aqara_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 15 } } }) + test.socket.capability:__expect_send( + aqara_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(65)) + ) test.socket.zigbee:__expect_send({ aqara_device.id, WindowCovering.server.commands.GoToLiftPercentage(aqara_device, 65) @@ -473,6 +485,9 @@ test.register_coroutine_test( aqara_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -5 } } }) + test.socket.capability:__expect_send( + aqara_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(60)) + ) test.socket.zigbee:__expect_send({ aqara_device.id, WindowCovering.server.commands.GoToLiftPercentage(aqara_device, 60) 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 f3e09c20a6..9fae7f8f7a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -1,6 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local st_utils = require "st.utils" local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -9,6 +10,14 @@ local utils = {} utils.PRESET_LEVEL = 50 utils.PRESET_LEVEL_KEY = "_presetLevel" +-- When the curtain is moving, LATEST_TARGET_LEVEL is used to store the latest +-- target position value, which will be cleared when the curtain status is updated. +local LATEST_TARGET_LEVEL = "_latestTargetLevel" +-- Timeout for the stateless step to accumulate +local TARGET_LEVEL_TIME_OUT_SECONDS = 3 +-- Field name for the timer that clears the target position after timeout +local TARGET_LEVEL_TIME_OUT = "_targetLevelTimeOut" + utils.get_preset_level = function(device, component) local level = device:get_latest_state(component, "windowShadePreset", "position") or device:get_field(utils.PRESET_LEVEL_KEY) or @@ -31,4 +40,37 @@ utils.set_preset_position_cmd = function(driver, device, command) device:set_field(utils.PRESET_LEVEL_KEY, command.args.position, {persist = true}) end +-- Step shade level handler for statelessWindowShadeLevelStep capability +utils.step_shade_level_handler = function(invert_shade_level) + return function(driver, device, command) + local step = command.args.stepSize or 0 + if step == 0 then return + elseif invert_shade_level then step = -step end -- invert step's direction if invert_shade_level is true + + -- 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) + local new_target_level = st_utils.clamp_value(latest_target_level + step, 0, 100) + + -- Cancel any previous timer and set a 3 second timeout timer to ensure 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) + end) + device:set_field(TARGET_LEVEL_TIME_OUT, timer) + device:set_field(LATEST_TARGET_LEVEL, new_target_level) + + driver:inject_capability_command(device, { + capability = capabilities.windowShadeLevel.ID, + component = command.component, + command = capabilities.windowShadeLevel.commands.setShadeLevel.NAME, + named_args = { shadeLevel = new_target_level } + }) + end +end + return utils