Skip to content
Open
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
63 changes: 52 additions & 11 deletions drivers/SmartThings/matter-window-covering/src/init.lua
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"
Expand All @@ -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",
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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",

Copy link
Copy Markdown
Contributor

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-covering profile? 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.

Copy link
Copy Markdown
Collaborator Author

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_configure gets called for each child once it’s created as an EDGE_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?

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need to call set_find_child in the driver init handler or else child devices will no longer be usable after a driver/hub restart. You can add this in the init function to handle this:

if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then
    device:set_find_child(switch_utils.find_child)
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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)
Expand All @@ -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, {})
Expand All @@ -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, {})
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading