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/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..1667c0d998 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 @@ -137,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") @@ -153,6 +167,34 @@ local function tuya_cluster_handler(driver, device, zb_rx) end end +local function knob_to_window_shade_step_cmd(driver, device, command) + local step = command.args.stepSize + 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 + 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) +end + local tuya_curtain_driver = { NAME = "tuya curtain", lifecycle_handlers = { @@ -173,6 +215,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 = {