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
5 changes: 5 additions & 0 deletions drivers/SmartThings/zigbee-window-treatment/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ zigbeeManufacturer:
manufacturer: LUMI
model: lumi.curtain.agl001
deviceProfileName: window-treatment-aqara-curtain-driver-e1
- id: "LUMI/lumi.curtain.vagl02"
deviceLabel: Roller Shade Controller T1S
manufacturer: LUMI
model: lumi.curtain.vagl02
deviceProfileName: window-treatment-aqara-roller-shade-rotate
- id: "REXENSE/KG0001"
deviceLabel: Window Treatment
manufacturer: REXENSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ local FINGERPRINTS = {
{ mfr = "LUMI", model = "lumi.curtain" },
{ mfr = "LUMI", model = "lumi.curtain.v1" },
{ mfr = "LUMI", model = "lumi.curtain.aq2" },
{ mfr = "LUMI", model = "lumi.curtain.agl001" }
{ mfr = "LUMI", model = "lumi.curtain.agl001" },
{ mfr = "LUMI", model = "lumi.curtain.vagl02" }
}

return FINGERPRINTS
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
-- Copyright 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local MODELS = {
["lumi.curtain.aq2"] = true,
["lumi.curtain.vagl02"] = true
}
local function roller_shade_can_handle(opts, driver, device, ...)
if device:get_model() == "lumi.curtain.aq2" then
if MODELS[device:get_model()] then
return true, require("aqara.roller-shade")
end
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ local aqara_roller_shade_handler = {
}
},
can_handle = require("aqara.roller-shade.can_handle"),
sub_drivers = require("aqara.roller-shade.sub_drivers"),
}

return aqara_roller_shade_handler
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- 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("aqara.roller-shade.t1s"),
}
return sub_drivers
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local function t1s_can_handle(opts, driver, device, ...)
if device:get_model() == "lumi.curtain.vagl02" then
return true, require("aqara.roller-shade.t1s")
end
return false
end

return t1s_can_handle
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
-- 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 cluster_base = require "st.zigbee.cluster_base"
local FrameCtrl = require "st.zigbee.zcl.frame_ctrl"
local data_types = require "st.zigbee.data_types"
local aqara_utils = require "aqara/aqara_utils"
local window_treatment_utils = require "window_treatment_utils"

local WindowCovering = clusters.WindowCovering
local AnalogOutput = clusters.AnalogOutput

local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"]
local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"]
local shadeRotateState = capabilities["stse.shadeRotateState"]
local setRotateStateCommandName = "setRotateState"

local MULTISTATE_CLUSTER_ID = 0x0013
local MULTISTATE_ATTRIBUTE_ID = 0x0055
local INIT_STATE_ATTRIBUTE_ID = 0x0407
local DIRECTION_ATTRIBUTE_ID = 0x0400

local ROTATE_UP_VALUE = 0x0006
Comment thread
cjswedes marked this conversation as resolved.
Resolved
local ROTATE_DOWN_VALUE = 0x0005
local SHADE_STATE_STOP = 2
local SHADE_STATE_OPEN = 1
local SHADE_STATE_CLOSE = 0
local SHADE_STATE_BLOCKED = 4

local function is_not_init(device)
Comment thread
cjswedes marked this conversation as resolved.
Resolved
-- True until the device reports that its travel points have been initialized
return device:get_latest_state("main", initializedStateWithGuide.ID, initializedStateWithGuide.initializedStateWithGuide.NAME) ~=
initializedStateWithGuide.initializedStateWithGuide.initialized.NAME
end
Comment thread
cjswedes marked this conversation as resolved.
Resolved

local function window_shade_level_cmd(driver, device, command)
-- Cannot be controlled if not initialized
if is_not_init(device) then return end
aqara_utils.shade_level_cmd(driver, device, command)
end

local function window_shade_open_cmd(driver, device, command)
-- Cannot be controlled if not initialized
if is_not_init(device) then return end
device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100))
end

local function window_shade_close_cmd(driver, device, command)
-- Cannot be controlled if not initialized
if is_not_init(device) then return end
device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0))
end

local function set_rotate_command_handler(driver, device, command)
device:emit_event(shadeRotateState.rotateState.idle({ state_change = true, visibility = { displayed = false } })) -- update UI

-- Cannot be controlled if not initialized
if is_not_init(device) then return end
local state = command.args.state
if state == "rotateUp" then
local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID,
MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE)
message.body.zcl_header.frame_ctrl = FrameCtrl(0x10)
device:send(message)
elseif state == "rotateDown" then
local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID,
MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE)
message.body.zcl_header.frame_ctrl = FrameCtrl(0x10)
device:send(message)
end
end

local function do_refresh(driver, device)
device:send(AnalogOutput.attributes.PresentValue:read(device))
device:send(cluster_base.read_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, INIT_STATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE))
end

local function init_state_handler(driver, device, value, zb_rx)
local state = value.value == 0x02 and initializedStateWithGuide.initializedStateWithGuide.initialized() or
initializedStateWithGuide.initializedStateWithGuide.notInitialized()
device:emit_event(state)
end

-- The position of this device is reported through the present value of the analog output
-- cluster, where 0 is fully closed and 100 is fully open. The lift percentage of the window
-- covering cluster is reported with the opposite polarity (100% = fully closed), so it is
-- ignored here to keep the parent Aqara handler from emitting an inverted shade level.
Comment thread
cjswedes marked this conversation as resolved.
Resolved
local function current_position_lift_percentage_handler(driver, device, value, zb_rx)
end
Comment thread
cjswedes marked this conversation as resolved.
Resolved

local function shade_state_report_handler(driver, device, value, zb_rx)
local state = value.value
-- update state ui
if state == SHADE_STATE_STOP or state == SHADE_STATE_BLOCKED then
-- read shade position to update the UI
device:send(AnalogOutput.attributes.PresentValue:read(device))
-- The device does not report the init state when its travel points have been set, so it is
-- re-read once each time the shade stops while the device is still not initialized.
if is_not_init(device) then
device:send(cluster_base.read_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID,
INIT_STATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE))
end
elseif state == SHADE_STATE_OPEN then
device:emit_event(capabilities.windowShade.windowShade.opening())
elseif state == SHADE_STATE_CLOSE then
device:emit_event(capabilities.windowShade.windowShade.closing())
end
end

local function device_info_changed(driver, device, event, args)
if device.preferences ~= nil then
local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID]
if reverseRollerShadeDirPrefValue ~= nil and
reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then
device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID,
DIRECTION_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, reverseRollerShadeDirPrefValue))
end
end
end

local function device_added(driver, device)
device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" },
{ visibility = { displayed = false } }))
window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel,
capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0))
window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade,
capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed())
window_treatment_utils.emit_event_if_latest_state_missing(device, "main", initializedStateWithGuide,
initializedStateWithGuide.initializedStateWithGuide.NAME,
initializedStateWithGuide.initializedStateWithGuide.notInitialized())
device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false } }))
device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID,
aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1))

-- The direction is stored on the device and survives a re-pairing, so sync it with the preference
local reverse = device.preferences ~= nil and device.preferences[reverseRollerShadeDir.ID] == true
device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID,
DIRECTION_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, reverse))
end

local function do_configure(self, device)
device:configure()
do_refresh(self, device)
end

local aqara_roller_shade_t1s_handler = {
NAME = "Aqara T1S Blinder Controller Handler",
lifecycle_handlers = {
added = device_added,
doConfigure = do_configure,
infoChanged = device_info_changed
},
capability_handlers = {
[capabilities.windowShadeLevel.ID] = {
[capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd
},
[capabilities.windowShade.ID] = {
[capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd,
[capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd
},
[shadeRotateState.ID] = {
[setRotateStateCommandName] = set_rotate_command_handler
},
[capabilities.refresh.ID] = {
[capabilities.refresh.commands.refresh.NAME] = do_refresh
}
},
zigbee_handlers = {
attr = {
[aqara_utils.PRIVATE_CLUSTER_ID] = {
[INIT_STATE_ATTRIBUTE_ID] = init_state_handler,
},
[WindowCovering.ID] = {
[WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = current_position_lift_percentage_handler
},
[MULTISTATE_CLUSTER_ID] = {
[MULTISTATE_ATTRIBUTE_ID] = shade_state_report_handler
}
}
},
can_handle = require("aqara.roller-shade.t1s.can_handle"),
}

return aqara_roller_shade_t1s_handler
Loading
Loading