Skip to content

Commit fa58718

Browse files
committed
Support multi-endpoint window covering
Signed-off-by: Hunsup Jung <hunsup.jung@samsung.com>
1 parent 53c74eb commit fa58718

1 file changed

Lines changed: 104 additions & 17 deletions

File tree

  • drivers/SmartThings/matter-window-covering/src

‎drivers/SmartThings/matter-window-covering/src/init.lua‎

Lines changed: 104 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
-- Copyright 2022 SmartThings, Inc.
22
-- Licensed under the Apache License, Version 2.0
33

4-
54
--Note: Currently only support for window shades with the PositionallyAware Feature
65
--Note: No support for setting device into calibration mode, it must be done manually
76
local capabilities = require "st.capabilities"
@@ -10,6 +9,12 @@ local clusters = require "st.matter.clusters"
109
local MatterDriver = require "st.matter.driver"
1110
local utils = require "st.utils"
1211

12+
local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device"
13+
--- If the ASSIGNED_CHILD_KEY field is populated for an endpoint, it should be
14+
--- used as the key in the get_child_by_parent_assigned_key() function. This allows
15+
--- multiple endpoints to associate with the same child device, though right now child
16+
local ASSIGNED_CHILD_KEY = "__assigned_child_key"
17+
1318
local battery_support = {
1419
NO_BATTERY = "NO_BATTERY",
1520
BATTERY_LEVEL = "BATTERY_LEVEL",
@@ -103,10 +108,14 @@ end
103108

104109
--- Tries to emit the idle window shade status if no target percentages are set.
105110
--- @param device any a Matter device object
106-
local function try_emit_idle_window_shade_status(device)
111+
local function try_emit_idle_window_shade_status(device, ep)
107112
if not device:get_field(TARGET_LIFT_PERCENT.STATE) and
108113
not device:get_field(TARGET_TILT_PERCENT.STATE) then
109-
device:emit_event(get_idle_window_shade_status(device))
114+
if ep then
115+
device:emit_event_for_endpoint(ep, get_idle_window_shade_status(device))
116+
else
117+
device:emit_event(get_idle_window_shade_status(device))
118+
end
110119
end
111120
end
112121

@@ -123,7 +132,7 @@ local function cache_data_with_timeout(device, data, op_fields)
123132
local new_timer = device.thread:call_with_delay(op_fields.TIMEOUT_DELAY_S, function()
124133
device:set_field(op_fields.STATE, nil)
125134
device:set_field(op_fields.TIMEOUT, nil)
126-
try_emit_idle_window_shade_status(device)
135+
try_emit_idle_window_shade_status(device, nil)
127136
end)
128137
device:set_field(op_fields.TIMEOUT, new_timer)
129138
end
@@ -154,6 +163,80 @@ local function component_to_endpoint(device, component_name)
154163
return find_default_endpoint(device, clusters.WindowCovering.ID)
155164
end
156165

166+
local function create_feature_table(lift_eps, tilt_eps)
167+
local result = {}
168+
local i, j = 1, 1
169+
while i <= #lift_eps or j <= #tilt_eps do
170+
local lift_ep, tilt_ep = lift_eps[i], tilt_eps[j]
171+
if tilt_ep == nil or (lift_ep ~= nil and lift_ep < tilt_ep) then
172+
result[#result + 1] = { endpoint = lift_ep, has_lift = true, has_tilt = false }
173+
i = i + 1
174+
elseif lift_ep == nil or tilt_ep < lift_ep then
175+
result[#result + 1] = { endpoint = tilt_ep, has_lift = false, has_tilt = true }
176+
j = j + 1
177+
else -- lift_ep == tilt_ep
178+
result[#result + 1] = { endpoint = lift_ep, has_lift = true, has_tilt = true }
179+
i, j = i + 1, j + 1
180+
end
181+
end
182+
return result
183+
end
184+
185+
local function update_profile_table(device)
186+
local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT})
187+
local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT})
188+
table.sort(lift_eps)
189+
table.sort(tilt_eps)
190+
local feature_table = create_feature_table(lift_eps, tilt_eps)
191+
local profile_table = {}
192+
193+
for _, entry in pairs(feature_table) do
194+
profile_table[entry.endpoint] = "window-covering"
195+
if entry.has_tilt then
196+
profile_table[entry.endpoint] = "window-covering-tilt"
197+
if not entry.has_lift then
198+
profile_table[entry.endpoint] = "window-covering-tilt-only"
199+
end
200+
end
201+
end
202+
return profile_table
203+
end
204+
205+
local function find_child(parent_device, ep_id)
206+
local assigned_key = parent_device:get_field(string.format("%s_%d", ASSIGNED_CHILD_KEY, ep_id)) or ep_id
207+
return parent_device:get_child_by_parent_assigned_key(string.format("%d", assigned_key))
208+
end
209+
210+
local function create_or_update_multi_devices(driver, device, wc_cluster_eps)
211+
local profile_table = update_profile_table(device)
212+
local default_endpoint_id = find_default_endpoint(device)
213+
table.sort(wc_cluster_eps)
214+
for device_num, ep_id in ipairs(wc_cluster_eps) do
215+
if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint
216+
local label_and_name = string.format("%s %d", device.label, device_num)
217+
local child_profile = profile_table[ep_id] or "window-covering"
218+
local existing_child_device = device:get_field(IS_PARENT_CHILD_DEVICE) and find_child(device, ep_id)
219+
if not existing_child_device then
220+
driver:try_create_device({
221+
type = "EDGE_CHILD",
222+
label = label_and_name,
223+
profile = child_profile,
224+
parent_device_id = device.id,
225+
parent_assigned_child_key = string.format("%d", ep_id),
226+
vendor_provided_label = label_and_name
227+
})
228+
else
229+
existing_child_device:try_update_metadata({profile = child_profile})
230+
end
231+
end
232+
end
233+
-- Persist so that the find_child function is always set on each driver init.
234+
device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true})
235+
device:set_find_child(find_child)
236+
-- Update parent device's profile
237+
device:try_update_metadata({profile = profile_table[default_endpoint_id] or "window-covering"})
238+
end
239+
157240
local function match_profile(device, battery_supported)
158241
local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT})
159242
local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT})
@@ -191,20 +274,25 @@ local function device_init(driver, device)
191274
end
192275

193276
local function do_configure(driver, device)
194-
local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY})
195-
if #battery_feature_eps > 0 then
196-
local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {})
197-
attribute_list_read:merge(clusters.PowerSource.attributes.AttributeList:read())
198-
device:send(attribute_list_read)
277+
local wc_cluster_eps = device:get_endpoints(clusters.WindowCovering.ID)
278+
if #wc_cluster_eps > 1 then
279+
create_or_update_multi_devices(driver, device, wc_cluster_eps)
199280
else
200-
match_profile(device, battery_support.NO_BATTERY)
281+
local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY})
282+
if #battery_feature_eps > 0 then
283+
local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {})
284+
attribute_list_read:merge(clusters.PowerSource.attributes.AttributeList:read())
285+
device:send(attribute_list_read)
286+
else
287+
match_profile(device, battery_support.NO_BATTERY)
288+
end
201289
end
202290
end
203291

204292
local function info_changed(driver, device, event, args)
205293
if device.profile.id ~= args.old_st_store.profile.id then
206294
device:subscribe()
207-
elseif device.matter_version.software ~= args.old_st_store.matter_version.software then
295+
elseif args.old_st_store.matter_version and device.matter_version.software ~= args.old_st_store.matter_version.software then
208296
local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY})
209297
if #battery_feature_eps > 0 then
210298
local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {})
@@ -323,7 +411,7 @@ local function current_position_lift_percent_100ths_handler(driver, device, ib,
323411

324412
if is_target_value_reached(lift_percent, device:get_field(TARGET_LIFT_PERCENT.STATE)) then
325413
clear_cached_data(device, TARGET_LIFT_PERCENT)
326-
try_emit_idle_window_shade_status(device)
414+
try_emit_idle_window_shade_status(device, ib.endpoint_id)
327415
end
328416
end
329417

@@ -334,7 +422,7 @@ local function current_position_tilt_percent_100ths_handler(driver, device, ib,
334422

335423
if is_target_value_reached(tilt_percent, device:get_field(TARGET_TILT_PERCENT.STATE)) then
336424
clear_cached_data(device, TARGET_TILT_PERCENT)
337-
try_emit_idle_window_shade_status(device)
425+
try_emit_idle_window_shade_status(device, ib.endpoint_id)
338426
end
339427
end
340428

@@ -348,7 +436,7 @@ local function target_position_lift_percent_100ths_handler(driver, device, ib, r
348436
)
349437
if latest_lift_percentage and is_target_value_reached(latest_lift_percentage, target_lift_percent) then
350438
clear_cached_data(device, TARGET_LIFT_PERCENT)
351-
try_emit_idle_window_shade_status(device)
439+
try_emit_idle_window_shade_status(device, ib.endpoint_id)
352440
else
353441
cache_data_with_timeout(device, target_lift_percent, TARGET_LIFT_PERCENT)
354442
end
@@ -365,7 +453,7 @@ local function target_position_tilt_percent_100ths_handler(driver, device, ib, r
365453
)
366454
if latest_tilt_percent and is_target_value_reached(latest_tilt_percent, target_tilt_percent) then
367455
clear_cached_data(device, TARGET_TILT_PERCENT)
368-
try_emit_idle_window_shade_status(device)
456+
try_emit_idle_window_shade_status(device, ib.endpoint_id)
369457
else
370458
cache_data_with_timeout(device, target_tilt_percent, TARGET_TILT_PERCENT)
371459
end
@@ -376,12 +464,11 @@ end
376464
local function operational_status_handler(driver, device, ib, response)
377465
if not ib.data.value then return end
378466
local global_op_status = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL
379-
380467
local reverse = device.preferences.reverse
381468
local windowShade = capabilities.windowShade.windowShade
382469
local status_event
383470
if global_op_status == OP_STATUS_BITMAP.idle then
384-
try_emit_idle_window_shade_status(device)
471+
try_emit_idle_window_shade_status(device, ib.endpoint_id)
385472
elseif global_op_status == OP_STATUS_BITMAP.opening then
386473
status_event = reverse and windowShade.closing() or windowShade.opening()
387474
elseif global_op_status == OP_STATUS_BITMAP.closing then

0 commit comments

Comments
 (0)