From a8c85d4df2986c146c3fb1cefe9566adfa086d8f Mon Sep 17 00:00:00 2001 From: Zhongpei Ge Date: Mon, 16 Mar 2026 19:41:04 +0800 Subject: [PATCH 1/2] knob for blind --- .../profiles/window-treatment-reverse.yml | 2 ++ .../tuya-zigbee/src/curtain/init.lua | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml index 9712eb6849..a6bbc35a10 100644 --- a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml +++ b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml @@ -4,6 +4,8 @@ components: capabilities: - id: windowShade version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: windowShadeLevel version: 1 - id: windowShadePreset diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 03c34cac65..76446005d7 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -19,6 +19,7 @@ local device_management = require "st.zigbee.device_management" local tuya_utils = require "tuya_utils" local Basic = clusters.Basic local packet_id = 0 +local log = require "log" local PRESET_LEVEL = 50 local PRESET_LEVEL_KEY = "_presetLevel" @@ -109,6 +110,7 @@ local function window_shade_level(driver, device, command) if level > 100 then level = 100 end + log.info("capability handler level ------------->", level) level = utils.round(level) if device:get_manufacturer() == "_TZE284_nladmfvf" then level = 100 - level -- specific for _TZE284_nladmfvf @@ -153,6 +155,28 @@ local function tuya_cluster_handler(driver, device, zb_rx) end end +local function knob_to_window_shade_step_cmd(driver, device, command) + -- step1: get the rotateAmount + local step = command.args.stepSize + -- step2: get the current_level + local current_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + -- calcultate the target_level + -- Tuya curtain devices use INVERTED position logic, + -- if we want to set to "target level" = "current_level" + "step" + -- we should send (100 - target level) to device + local target_level = 100-(current_level + step) + if target_level > 100 then + target_level = 100 + elseif target_level < 0 then + target_level = 0 + end + target_level = utils.round(target_level) + tuya_utils.send_tuya_command(device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00'..string.pack(">I2", target_level), packet_id) + packet_id = increase_packet_id(packet_id) + log.info("-------------------target_level", 100-target_level) + device:emit_event(capabilities.windowShadeLevel.shadeLevel(100-target_level)) +end + local tuya_curtain_driver = { NAME = "tuya curtain", lifecycle_handlers = { @@ -173,6 +197,9 @@ local tuya_curtain_driver = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset, [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = set_preset_position_cmd + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = knob_to_window_shade_step_cmd } }, zigbee_handlers = { From 7cad4d8614f8ed4699759a7f562af1f58d2ae6ed Mon Sep 17 00:00:00 2001 From: Zhongpei Ge Date: Mon, 23 Mar 2026 17:38:00 +0800 Subject: [PATCH 2/2] generic blind devices logic for ikea and tuya --- .../profiles/window-treatment-battery.yml | 2 + .../src/invert-lift-percentage/init.lua | 50 +++++++++++++++++++ .../tuya-zigbee/src/curtain/init.lua | 50 +++++++++++++------ 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml index be0ae73d76..8a04f2afbd 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: battery version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua index b586459b9a..b4f48130a5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua @@ -5,13 +5,26 @@ local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local window_shade_utils = require "window_shade_utils" +local utils = require "st.utils" +local log = require "log" local WindowCovering = zcl_clusters.WindowCovering local SHADE_SET_STATUS = "shade_set_status" +local TARGET_REACH_TOLERANCE = 1 -- ±1 degree tolerance for reaching target local function current_position_attr_handler(driver, device, value, zb_rx) local level = 100 - value.value + + local last_target_level = device:get_field("last_target_level") + log.info("---------->IKEA curtain report level:", level, "last_target_level:", last_target_level) + if last_target_level then + if math.abs(level - last_target_level) <= TARGET_REACH_TOLERANCE then + device:set_field("last_target_level", nil) + log.info("----------->IKEA curtain reached target, clearing last_target_level") + end + end + local current_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) local windowShade = capabilities.windowShade.windowShade if level == -155 then -- unknown position @@ -57,6 +70,7 @@ local function current_position_attr_handler(driver, device, value, zb_rx) end local function set_shade_level(device, value, command) + device:set_field("last_target_level", nil) local level = 100 - value device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level)) end @@ -70,6 +84,39 @@ local function window_shade_preset_cmd(driver, device, command) set_shade_level(device, level, command) end +local function window_shade_step_level_cmd(driver, device, command) + local step = command.args.stepSize + log.info("------------->IKEA step size:", step) + + -- Priority: use last_target_level if exists + local last_target_level = device:get_field("last_target_level") + local current_level = last_target_level or + device:get_latest_state("main", capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + + log.info("------------->IKEA current_level:", current_level, "from last_target_level:", last_target_level ~= nil) + + -- Calculate new target (user level: 0-100, 0=closed, 100=open) + local target_level = current_level + step + if target_level > 100 then target_level = 100 + elseif target_level < 0 then target_level = 0 + end + target_level = utils.round(target_level) + + log.info("------------->IKEA target_level:", target_level) + + -- Update tracking state + device:set_field("last_target_level", target_level) + + -- Invert for IKEA: user level → device level + local device_level = 100 - target_level + + log.info("------------->IKEA sending device_level:", device_level) + + device:send_to_component(command.component, + WindowCovering.server.commands.GoToLiftPercentage(device, device_level)) +end + local ikea_window_treatment = { NAME = "inverted lift percentage", zigbee_handlers = { @@ -85,6 +132,9 @@ local ikea_window_treatment = { }, [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset_cmd + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = window_shade_step_level_cmd } }, can_handle = require("invert-lift-percentage.can_handle"), diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 76446005d7..1667c0d998 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -139,7 +139,19 @@ local function tuya_cluster_handler(driver, device, zb_rx) -- dp means data point in tuya payload format local dp = raw:byte(3) local dp_data = raw:byte(10) - if dp == 0x03 then + if dp == 0x03 then + local knob_target = device:get_field("knob_target_level") + log.info("---------->tuya curtain report dp_data:", dp_data) + log.info("---------->knob_target:", knob_target) + if knob_target then + -- Allow ±1 degree tolerance for reaching target + if math.abs(knob_target - dp_data) <= 1 then + device:set_field("knob_target_level", nil) + log.info("tuya curtain reached target, clearing knob_target") + end + end + + -- Emit events with device-reported level (dp_data) window_shade_level_event = capabilities.windowShadeLevel.shadeLevel(dp_data) if dp_data == 0 then window_shade_val_event = capabilities.windowShade.windowShade("open") @@ -156,25 +168,31 @@ local function tuya_cluster_handler(driver, device, zb_rx) end local function knob_to_window_shade_step_cmd(driver, device, command) - -- step1: get the rotateAmount local step = command.args.stepSize - -- step2: get the current_level - local current_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 - -- calcultate the target_level - -- Tuya curtain devices use INVERTED position logic, - -- if we want to set to "target level" = "current_level" + "step" - -- we should send (100 - target level) to device - local target_level = 100-(current_level + step) - if target_level > 100 then - target_level = 100 - elseif target_level < 0 then - target_level = 0 + log.info("------------->knob step size:", step) + + -- Priority: use knob_target_level if exists + local knob_target = device:get_field("knob_target_level") + local current_level = knob_target or + device:get_latest_state("main", capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + + -- Calculate new target (user level: 0-100, 0=closed, 100=open) + log.info("------------->current_level:", current_level) + local target_level = current_level + step + if target_level > 100 then target_level = 100 + elseif target_level < 0 then target_level = 0 end - target_level = utils.round(target_level) + log.info("------------->target_level:", target_level) + + -- Update tracking state + device:set_field("knob_target_level", target_level) + + target_level = utils.round(100 - target_level) + log.info("------------->sending level:", target_level) + tuya_utils.send_tuya_command(device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00'..string.pack(">I2", target_level), packet_id) packet_id = increase_packet_id(packet_id) - log.info("-------------------target_level", 100-target_level) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(100-target_level)) end local tuya_curtain_driver = {