Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: batteryLevel
version: 1
- id: firmwareUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ components:
version: 1
- id: windowShadeTiltLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ components:
version: 1
- id: windowShadeTiltLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ components:
version: 1
- id: windowShadeTiltLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ components:
version: 1
- id: windowShadeTiltLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
Expand Down
49 changes: 49 additions & 0 deletions drivers/SmartThings/matter-window-covering/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ local REVERSE_POLARITY = "__reverse_polarity"
local PRESET_LEVEL_KEY = "__preset_level_key"
local DEFAULT_PRESET_LEVEL = 50

local TARGET_REACH_TOLERANCE = 1

local function find_default_endpoint(device, cluster)
local res = device.MATTER_DEFAULT_ENDPOINT
local eps = device:get_endpoints(cluster)
Expand Down Expand Up @@ -185,6 +187,38 @@ local function handle_shade_level(driver, device, cmd)
device:send(req)
end

-- knob control handler
local function knob_to_window_shade_step_cmd(driver, device, cmd)
local step = cmd.args.stepSize

-- 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)
local target_level = current_level + step
if target_level > 100 then target_level = 100
elseif target_level < 0 then target_level = 0
end

-- Update tracking state
device:set_field("knob_target_level", target_level)

-- Matter uses inverted logic (like IKEA)
-- User level: 0=closed, 100=open
-- Matter level: 10000=open, 0=closed (in percent100ths)
local lift_percentage_value = 100 - target_level
local hundredths_lift_percentage = lift_percentage_value * 100

local endpoint_id = device:component_to_endpoint(cmd.component)
local req = clusters.WindowCovering.server.commands.GoToLiftPercentage(
device, endpoint_id, hundredths_lift_percentage
)
device:send(req)
end

-- move to shade tilt level between 0-100
local function handle_shade_tilt_level(driver, device, cmd)
local endpoint_id = device:component_to_endpoint(cmd.component)
Expand All @@ -204,6 +238,17 @@ local current_pos_handler = function(attribute)
end
local windowShade = capabilities.windowShade.windowShade
local position = 100 - math.floor(ib.data.value / 100)

-- Knob control logic
local knob_target = device:get_field("knob_target_level")

if knob_target and attribute == capabilities.windowShadeLevel.shadeLevel then
-- Allow ±1 degree tolerance for reaching target
if math.abs(position - knob_target) <= TARGET_REACH_TOLERANCE then
device:set_field("knob_target_level", nil)
end
end

local reverse = device:get_field(REVERSE_POLARITY)
device:emit_event_for_endpoint(ib.endpoint_id, attribute(position))

Expand Down Expand Up @@ -264,6 +309,7 @@ local function current_status_handler(driver, device, ib, response)
elseif state == 2 then -- closing
device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing())
elseif state ~= 0 then -- unknown
device:set_field("knob_target_level", nil) -- clean this field once the window covering stop
device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown())
end
end
Expand Down Expand Up @@ -364,6 +410,9 @@ local matter_driver_template = {
[capabilities.windowShadeLevel.ID] = {
[capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = handle_shade_level,
},
[capabilities.statelessSwitchLevelStep.ID] = {
[capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = knob_to_window_shade_step_cmd
},
[capabilities.windowShadeTiltLevel.ID] = {
[capabilities.windowShadeTiltLevel.commands.setShadeTiltLevel.NAME] = handle_shade_tilt_level,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ components:
version: 1
- id: stse.deviceInitialization
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand Down
77 changes: 77 additions & 0 deletions drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ local cluster_base = require "st.zigbee.cluster_base"
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 Basic = clusters.Basic
local WindowCovering = clusters.WindowCovering
Expand All @@ -24,6 +25,8 @@ local INIT_STATE_INIT = "init"
local INIT_STATE_OPEN = "open"
local INIT_STATE_CLOSE = "close"
local INIT_STATE_REVERSE = "reverse"
local TARGET_LEVEL_TIME_OUT = "_target_level_timeout"
local TARGET_LEVEL_TIME_OUT_SECONDS = 30

local PREF_INITIALIZE = "\x00\x01\x00\x00\x00\x00\x00"
local PREF_SOFT_TOUCH_OFF = "\x00\x08\x00\x00\x00\x01\x00"
Expand All @@ -37,6 +40,44 @@ local function window_shade_level_cmd(driver, device, command)
aqara_utils.shade_level_cmd(driver, device, command)
end

local function window_shade_step_level_cmd(driver, device, command)
local step = command.args.stepSize

-- Priority: use target_level if exists, otherwise use latest state
local target_level_field = device:get_field("target_level")
local current_level = target_level_field or
device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0

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)

-- Set target_level for tracking
device:set_field("target_level", target_level)

-- Cancel previous timeout timer if exists
local old_timer = device:get_field(TARGET_LEVEL_TIME_OUT)
if old_timer ~= nil then
device.thread:cancel_timer(old_timer)
end

-- Set 30 second timeout timer to ensure target_level is cleared
local timer = device.thread:call_with_delay(TARGET_LEVEL_TIME_OUT_SECONDS, function(d)
device:set_field("target_level", nil)
device:set_field(TARGET_LEVEL_TIME_OUT, nil)
end)
device:set_field(TARGET_LEVEL_TIME_OUT, timer)

-- Don't emit to cloud, let device reports drive UI
-- device:emit_event(capabilities.windowShadeLevel.shadeLevel(target_level))

device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, target_level))
end

local function set_initialized_state_handler(driver, device, command)
-- update ui
device:emit_event(deviceInitialization.initializedState.initializing())
Expand All @@ -61,12 +102,45 @@ local function set_initialized_state_handler(driver, device, command)
end

local function shade_level_report_legacy_handler(driver, device, value, zb_rx)
local reported_level = value.value
local target_level_field = device:get_field("target_level")

if target_level_field then
-- Active step control
if utils.round(reported_level) == utils.round(target_level_field) then
-- Device reached target position, clear target marker and timeout timer
device:set_field("target_level", nil)
local timer = device:get_field(TARGET_LEVEL_TIME_OUT)
if timer ~= nil then
device.thread:cancel_timer(timer)
device:set_field(TARGET_LEVEL_TIME_OUT, nil)
end
end
-- Always emit to update UI with actual device position
end

-- for version 34
aqara_utils.emit_shade_level_event(device, value)
aqara_utils.emit_shade_event(device, value)
end

local function shade_level_report_handler(driver, device, value, zb_rx)
local reported_level = value.value
local target_level_field = device:get_field("target_level")

if target_level_field then
-- Active step control
if utils.round(reported_level) == utils.round(target_level_field) then
-- Device reached target position, clear target marker and timeout timer
device:set_field("target_level", nil)
local timer = device:get_field(TARGET_LEVEL_TIME_OUT)
if timer ~= nil then
device.thread:cancel_timer(timer)
device:set_field(TARGET_LEVEL_TIME_OUT, nil)
end
end
-- Always emit to update UI with actual device position
end
aqara_utils.emit_shade_level_event(device, value)
aqara_utils.emit_shade_event(device, value)
end
Expand Down Expand Up @@ -190,6 +264,9 @@ local aqara_window_treatment_handler = {
},
[capabilities.refresh.ID] = {
[capabilities.refresh.commands.refresh.NAME] = do_refresh
},
[capabilities.statelessSwitchLevelStep.ID] = {
[capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = window_shade_step_level_cmd
}
},
zigbee_handlers = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing this

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
Expand Down Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ components:
capabilities:
- id: windowShade
version: 1
- id: statelessSwitchLevelStep
version: 1
- id: windowShadeLevel
version: 1
- id: windowShadePreset
Expand Down
Loading
Loading