Skip to content
Closed
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
@@ -0,0 +1,16 @@
id: stse.statelessWindowShadeLevelStep
version: 1
status: proposed
name: stateless window shade level step
ephemeral: false
attributes: {}
commands:
stepShadeLevel:
name: stepShadeLevel
arguments:
- name: stepSize
optional: false
schema:
type: integer
minimum: -100
maximum: 100
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessWindowShadeLevelStep
version: 1
- id: stse.initializedStateWithGuide
version: 1
- id: stse.shadeRotateState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessWindowShadeLevelStep
version: 1
- id: stse.deviceInitialization
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: statelessWindowShadeLevelStep
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: statelessWindowShadeLevelStep
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessWindowShadeLevelStep
version: 1
- id: windowShadePreset
version: 1
- id: powerSource
Expand All @@ -17,4 +19,4 @@ components:
- id: firmwareUpdate
version: 1
categories:
- name: Blind
- name: Blind
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessWindowShadeLevelStep
version: 1
- id: refresh
version: 1
categories:
- name: Blind
- name: Blind
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessWindowShadeLevelStep
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ components:
version: 1
- id: windowShadeLevel
version: 1
- id: statelessWindowShadeLevelStep
version: 1
- id: windowShadePreset
version: 1
- id: firmwareUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ local function shade_level_cmd(driver, device, command)
end
level = utils.round(level)

-- update ui to the new level
-- Invert percentage: 100-level, so user's 100% means closed, 0% means open
local inverted_level = 100 - level

-- update ui to the new level
device:emit_event(capabilities.windowShadeLevel.shadeLevel(level))

-- send
device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level))
-- send (send inverted value to device)
device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, inverted_level))
end

local function emit_shade_event_by_state(device, value)
Expand Down Expand Up @@ -82,8 +85,11 @@ local function emit_shade_level_event(device, value)
end
level = utils.round(level)

-- update level ui
device:emit_event(capabilities.windowShadeLevel.shadeLevel(level))
-- Invert percentage: 100-level, invert device-reported value for UI display
local inverted_level = 100 - level

-- update level ui (display inverted value)
device:emit_event(capabilities.windowShadeLevel.shadeLevel(inverted_level))
end

aqara_utils.PRIVATE_CLUSTER_ID = PRIVATE_CLUSTER_ID
Expand Down
13 changes: 9 additions & 4 deletions drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ local TUYA_CLUSTER = 0xEF00
local DP_TYPE_VALUE = "\x02"
local DP_TYPE_ENUM = "\x04"

local SeqNum = 0

-------- Send Command Function for Tuya Zigbee device -------------
-- ZigbeeMessageTx:
-- Uint16: 0x0000
Expand All @@ -45,6 +43,14 @@ local SeqNum = 0
-- ReadAttribute:
-- AttributeId: 0x0000

-- 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

local function SendCommand(device, DpId, Type, Value)
local addrh = Messages.AddressHeader(
ZigbeeConstants.HUB.ADDR, -- Source Address
Expand All @@ -57,8 +63,7 @@ local function SendCommand(device, DpId, Type, Value)
local zclh = ZigbeeZcl.ZclHeader({cmd = data_types.ZCLCommandId(0x00)})
zclh.frame_ctrl:set_cluster_specific() -- sets this frame control field to be cluster specific
-- Make a payload body
SeqNum = (SeqNum + 1) % 65536
local strSeqNum = string.pack(">I2", SeqNum) -- Pack the Sequence number to 2 bytes unsigned integer type with big endian.
local strSeqNum = string.pack(">I2", get_next_tuya_seq_num(device)) -- Pack the Sequence number to 2 bytes unsigned integer type with big endian.
local LenOfValue = string.pack(">I2",string.len(Value)) -- Pack length of Value to 2 bytes unsigned integer type wiht big endian.
local PayloadBody = generic_body.GenericBody(strSeqNum .. DpId .. Type .. LenOfValue .. Value)
local MsgBody = ZigbeeZcl.ZclMessageBody({zcl_header = zclh, zcl_body = PayloadBody})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

-- Brand-specific configuration for window treatment devices
-- Supports statelessWindowShadeLevelStep capability

local BRAND_CONFIGS = {
-- invert_level = true: Device reports inverted values (0 = open, 100 = closed)
-- When sending command: device_target_level = 100 - ui_target_level
-- When receiving report: ui_reported_level = 100 - reported_level
{
name = "aqara",
mfr = "LUMI",
models = { "lumi.curtain", "lumi.curtain.v1", "lumi.curtain.aq2", "lumi.curtain.agl001", "lumi.curtain.acn002" },
invert_level = true,
},
{
name = "somfy",
mfr = "Somfy",
models = {}, -- Empty models = match any model from this manufacturer
invert_level = true,
},
{
name = "vimar",
mfr = "VIMAR",
models = {},
invert_level = true,
},
{
name = "invert-lift-percentage",
mfr = "IKEA of Sweden",
models = {},
invert_level = true,
},
{
name = "invert-lift-percentage",
mfr = "Smartwings",
models = {},
invert_level = true,
},
{
name = "invert-lift-percentage",
mfr = "Insta GmbH",
models = {},
invert_level = true,
},
{
name = "yoolax",
mfr = "Yookee",
models = { "D10110" },
invert_level = true,
},
{
name = "yoolax",
mfr = "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 = "hanssem",
mfr = "",
models = { "TS0601" },
match_by_model = true, -- Match by model only, not manufacturer
use_tuya_cluster = true, -- Use Tuya custom cluster 0xEF00
},
{
name = "rooms-beautiful",
mfr = "Rooms Beautiful",
models = { "C001" },
},
{
name = "screen-innovations",
mfr = "",
models = { "WM25/L-Z" },
match_by_model = true,
},
-- Note: HOPOsmart and VIVIDSTORM are NOT supported (use custom clusters)
}

return BRAND_CONFIGS
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- 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
Loading
Loading