Repository navigation
Support multi-endpoint window covering #3191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| -- Copyright 2022 SmartThings, Inc. | ||
| -- Licensed under the Apache License, Version 2.0 | ||
|
|
||
|
|
||
| --Note: Currently only support for window shades with the PositionallyAware Feature | ||
| --Note: No support for setting device into calibration mode, it must be done manually | ||
| local capabilities = require "st.capabilities" | ||
|
|
@@ -10,6 +9,12 @@ local clusters = require "st.matter.clusters" | |
| local MatterDriver = require "st.matter.driver" | ||
| local utils = require "st.utils" | ||
|
|
||
| local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" | ||
| --- If the ASSIGNED_CHILD_KEY field is populated for an endpoint, it should be | ||
| --- used as the key in the get_child_by_parent_assigned_key() function. This allows | ||
| --- multiple endpoints to associate with the same child device, though right now child | ||
| local ASSIGNED_CHILD_KEY = "__assigned_child_key" | ||
|
|
||
| local battery_support = { | ||
| NO_BATTERY = "NO_BATTERY", | ||
| BATTERY_LEVEL = "BATTERY_LEVEL", | ||
|
|
@@ -103,10 +108,14 @@ end | |
|
|
||
| --- Tries to emit the idle window shade status if no target percentages are set. | ||
| --- @param device any a Matter device object | ||
| local function try_emit_idle_window_shade_status(device) | ||
| local function try_emit_idle_window_shade_status(device, ep) | ||
| if not device:get_field(TARGET_LIFT_PERCENT.STATE) and | ||
| not device:get_field(TARGET_TILT_PERCENT.STATE) then | ||
| device:emit_event(get_idle_window_shade_status(device)) | ||
| if ep then | ||
| device:emit_event_for_endpoint(ep, get_idle_window_shade_status(device)) | ||
| else | ||
| device:emit_event(get_idle_window_shade_status(device)) | ||
| end | ||
| end | ||
| end | ||
|
|
||
|
|
@@ -123,7 +132,7 @@ local function cache_data_with_timeout(device, data, op_fields) | |
| local new_timer = device.thread:call_with_delay(op_fields.TIMEOUT_DELAY_S, function() | ||
| device:set_field(op_fields.STATE, nil) | ||
| device:set_field(op_fields.TIMEOUT, nil) | ||
| try_emit_idle_window_shade_status(device) | ||
| try_emit_idle_window_shade_status(device, nil) | ||
| end) | ||
| device:set_field(op_fields.TIMEOUT, new_timer) | ||
| end | ||
|
|
@@ -173,6 +182,35 @@ local function match_profile(device, battery_supported) | |
| device:try_update_metadata({profile = profile_name}) | ||
| end | ||
|
|
||
| local function find_child(parent_device, ep_id) | ||
| local assigned_key = parent_device:get_field(string.format("%s_%d", ASSIGNED_CHILD_KEY, ep_id)) or ep_id | ||
| return parent_device:get_child_by_parent_assigned_key(string.format("%d", assigned_key)) | ||
| end | ||
|
|
||
| local function create_or_update_multi_devices(driver, device, wc_cluster_eps) | ||
| local default_endpoint_id = find_default_endpoint(device) | ||
| table.sort(wc_cluster_eps) | ||
| for device_num, ep_id in ipairs(wc_cluster_eps) do | ||
| if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint | ||
| local label_and_name = string.format("%s %d", device.label, device_num) | ||
| local existing_child_device = device:get_field(IS_PARENT_CHILD_DEVICE) and find_child(device, ep_id) | ||
| if not existing_child_device then | ||
| driver:try_create_device({ | ||
| type = "EDGE_CHILD", | ||
| label = label_and_name, | ||
| profile = "window-covering", | ||
| parent_device_id = device.id, | ||
| parent_assigned_child_key = string.format("%d", ep_id), | ||
| vendor_provided_label = label_and_name | ||
| }) | ||
| end | ||
| end | ||
| end | ||
| -- Persist so that the find_child function is always set on each driver init. | ||
| device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true}) | ||
| device:set_find_child(find_child) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You will need to call
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't know about this. Thanks for checking. |
||
| end | ||
|
|
||
| local function device_init(driver, device) | ||
| -- set unused fields to nil. These field settings can be removed after a single driver update. | ||
| device:set_field("__preset_level_key", nil) | ||
|
|
@@ -191,6 +229,10 @@ local function device_init(driver, device) | |
| end | ||
|
|
||
| local function do_configure(driver, device) | ||
| local wc_cluster_eps = device:get_endpoints(clusters.WindowCovering.ID) | ||
| if #wc_cluster_eps > 1 then | ||
| create_or_update_multi_devices(driver, device, wc_cluster_eps) | ||
| end | ||
| local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) | ||
| if #battery_feature_eps > 0 then | ||
| local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) | ||
|
|
@@ -204,7 +246,7 @@ end | |
| local function info_changed(driver, device, event, args) | ||
| if device.profile.id ~= args.old_st_store.profile.id then | ||
| device:subscribe() | ||
| elseif device.matter_version.software ~= args.old_st_store.matter_version.software then | ||
| elseif args.old_st_store.matter_version and device.matter_version.software ~= args.old_st_store.matter_version.software then | ||
| local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) | ||
| if #battery_feature_eps > 0 then | ||
| local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) | ||
|
|
@@ -323,7 +365,7 @@ local function current_position_lift_percent_100ths_handler(driver, device, ib, | |
|
|
||
| if is_target_value_reached(lift_percent, device:get_field(TARGET_LIFT_PERCENT.STATE)) then | ||
| clear_cached_data(device, TARGET_LIFT_PERCENT) | ||
| try_emit_idle_window_shade_status(device) | ||
| try_emit_idle_window_shade_status(device, ib.endpoint_id) | ||
| end | ||
| end | ||
|
|
||
|
|
@@ -334,7 +376,7 @@ local function current_position_tilt_percent_100ths_handler(driver, device, ib, | |
|
|
||
| if is_target_value_reached(tilt_percent, device:get_field(TARGET_TILT_PERCENT.STATE)) then | ||
| clear_cached_data(device, TARGET_TILT_PERCENT) | ||
| try_emit_idle_window_shade_status(device) | ||
| try_emit_idle_window_shade_status(device, ib.endpoint_id) | ||
| end | ||
| end | ||
|
|
||
|
|
@@ -348,7 +390,7 @@ local function target_position_lift_percent_100ths_handler(driver, device, ib, r | |
| ) | ||
| if latest_lift_percentage and is_target_value_reached(latest_lift_percentage, target_lift_percent) then | ||
| clear_cached_data(device, TARGET_LIFT_PERCENT) | ||
| try_emit_idle_window_shade_status(device) | ||
| try_emit_idle_window_shade_status(device, ib.endpoint_id) | ||
| else | ||
| cache_data_with_timeout(device, target_lift_percent, TARGET_LIFT_PERCENT) | ||
| end | ||
|
|
@@ -365,7 +407,7 @@ local function target_position_tilt_percent_100ths_handler(driver, device, ib, r | |
| ) | ||
| if latest_tilt_percent and is_target_value_reached(latest_tilt_percent, target_tilt_percent) then | ||
| clear_cached_data(device, TARGET_TILT_PERCENT) | ||
| try_emit_idle_window_shade_status(device) | ||
| try_emit_idle_window_shade_status(device, ib.endpoint_id) | ||
| else | ||
| cache_data_with_timeout(device, target_tilt_percent, TARGET_TILT_PERCENT) | ||
| end | ||
|
|
@@ -376,12 +418,11 @@ end | |
| local function operational_status_handler(driver, device, ib, response) | ||
| if not ib.data.value then return end | ||
| local global_op_status = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL | ||
|
|
||
| local reverse = device.preferences.reverse | ||
| local windowShade = capabilities.windowShade.windowShade | ||
| local status_event | ||
| if global_op_status == OP_STATUS_BITMAP.idle then | ||
| try_emit_idle_window_shade_status(device) | ||
| try_emit_idle_window_shade_status(device, ib.endpoint_id) | ||
| elseif global_op_status == OP_STATUS_BITMAP.opening then | ||
| status_event = reverse and windowShade.closing() or windowShade.opening() | ||
| elseif global_op_status == OP_STATUS_BITMAP.closing then | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just confirming - will all the child endpoints always have the
window-coveringprofile? For example, none of them support tilt, right?In the future we might want to make this dynamic similar to how we handle child devices in the matter-switch driver, but if this is the only multi-endpoint device we know of for window coverings, we can leave it as is for now and plan to make this dynamic in a follow up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how the child lifecycle works, but from the logs it looks like
do_configuregets called for each child once it’s created as anEDGE_CHILD. So it seems we don’t need to check the feature map here.Could you take a look at how the lifecycle is currently set up and let me know what you think?