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
76local capabilities = require " st.capabilities"
@@ -10,6 +9,12 @@ local clusters = require "st.matter.clusters"
109local MatterDriver = require " st.matter.driver"
1110local 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+
1318local 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
111120end
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 )
129138end
@@ -154,6 +163,80 @@ local function component_to_endpoint(device, component_name)
154163 return find_default_endpoint (device , clusters .WindowCovering .ID )
155164end
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+
157240local 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)
191274end
192275
193276local 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
202290end
203291
204292local 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
328416end
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
339427end
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
376464local 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