diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index ba03737a73..c0aa9340fa 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -1,15 +1,21 @@ -- 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" local im = require "st.matter.interaction_model" local clusters = require "st.matter.clusters" +local device_lib = require "st.device" 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", @@ -36,6 +42,27 @@ local TARGET_TILT_PERCENT = { local TARGET_REACH_TOLERANCE = 0.5 +local function get_field_for_endpoint(device, field, endpoint) + local field_device = device + if device.network_type == device_lib.NETWORK_TYPE_CHILD then + field_device = device:get_parent_device() + end + return field_device:get_field(string.format("%s_%d", field, endpoint)) +end + +local function set_field_for_endpoint(device, field, endpoint, value, additional_params) + local field_device = device + if device.network_type == device_lib.NETWORK_TYPE_CHILD then + field_device = device:get_parent_device() + end + field_device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +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 + --- Gets the current window shade status based on the lift and tilt positions. ---@param device any a Matter device object ---@return any status the current window shade status @@ -92,21 +119,23 @@ end --- Clears the cached target percentage and cancels any associated timeout. --- @param device any a Matter device object --- @param op_fields table a table containing operational field constants used for caching target percentages and timeouts. -local function clear_cached_data(device, op_fields) - local current_timer = device:get_field(op_fields.TIMEOUT) +local function clear_cached_data(device, op_fields, ep) + local current_timer = get_field_for_endpoint(device, op_fields.TIMEOUT, ep) if current_timer then device.thread:cancel_timer(current_timer) end - device:set_field(op_fields.STATE, nil) - device:set_field(op_fields.TIMEOUT, nil) + set_field_for_endpoint(device, op_fields.STATE, ep, nil) + set_field_for_endpoint(device, op_fields.TIMEOUT, ep, nil) 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) - 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)) +local function try_emit_idle_window_shade_status(device, ep) + if not get_field_for_endpoint(device, TARGET_LIFT_PERCENT.STATE, ep) and + not get_field_for_endpoint(device, TARGET_TILT_PERCENT.STATE, ep) then + --- gets child device if one exists for this endpoint + local status_device = find_child(device, ep) or device + status_device:emit_event(get_idle_window_shade_status(status_device)) end end @@ -114,18 +143,18 @@ end --- @param device any a Matter device object --- @param data number the target percentage to cache --- @param op_fields table a table containing operational field constants used for caching target percentages and timeouts. -local function cache_data_with_timeout(device, data, op_fields) - device:set_field(op_fields.STATE, data) - local previous_timer = device:get_field(op_fields.TIMEOUT) +local function cache_data_with_timeout(device, data, op_fields, ep) + set_field_for_endpoint(device, op_fields.STATE, ep, data) + local previous_timer = get_field_for_endpoint(device, op_fields.TIMEOUT, ep) if previous_timer then device.thread:cancel_timer(previous_timer) end 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) + set_field_for_endpoint(device, op_fields.STATE, ep, nil) + set_field_for_endpoint(device, op_fields.TIMEOUT, ep, nil) + try_emit_idle_window_shade_status(device, ep) end) - device:set_field(op_fields.TIMEOUT, new_timer) + set_field_for_endpoint(device, op_fields.TIMEOUT, ep, new_timer) end local function is_target_value_reached(current_value, target_value) @@ -173,11 +202,37 @@ local function match_profile(device, battery_supported) device:try_update_metadata({profile = profile_name}) end +local function create_or_update_multi_devices(driver, device, wc_cluster_eps) + local default_endpoint_id = find_default_endpoint(device, clusters.WindowCovering.ID) + 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 or "Window Covering", 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) +end + local function device_init(driver, device) + if device:get_field(IS_PARENT_CHILD_DEVICE) then + device:set_find_child(find_child) + end -- set unused fields to nil. These field settings can be removed after a single driver update. device:set_field("__preset_level_key", nil) device:set_field("__reverse_polarity", nil) - device:set_component_to_endpoint_fn(component_to_endpoint) if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then @@ -191,6 +246,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 +263,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, {}) @@ -224,15 +283,15 @@ end -- capability handlers local function handle_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) local preset_target_percent = device:get_latest_state( "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME, DEFAULT_PRESET_LEVEL ) - cache_data_with_timeout(device, preset_target_percent, TARGET_LIFT_PERCENT) + cache_data_with_timeout(device, preset_target_percent, TARGET_LIFT_PERCENT, endpoint_id) local hundredths_lift_percent = (100 - preset_target_percent) * 100 - local endpoint_id = device:component_to_endpoint(cmd.component) device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( device, endpoint_id, hundredths_lift_percent )) @@ -246,10 +305,10 @@ end local function handle_close(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) > 0 then - cache_data_with_timeout(device, device.preferences.reverse and 100 or 0, TARGET_LIFT_PERCENT) + cache_data_with_timeout(device, device.preferences.reverse and 100 or 0, TARGET_LIFT_PERCENT, endpoint_id) end if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) > 0 then - cache_data_with_timeout(device, device.preferences.reverse and 100 or 0, TARGET_TILT_PERCENT) + cache_data_with_timeout(device, device.preferences.reverse and 100 or 0, TARGET_TILT_PERCENT, endpoint_id) end device:send(device.preferences.reverse and clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) or @@ -260,10 +319,10 @@ end local function handle_open(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) > 0 then - cache_data_with_timeout(device, device.preferences.reverse and 0 or 100, TARGET_LIFT_PERCENT) + cache_data_with_timeout(device, device.preferences.reverse and 0 or 100, TARGET_LIFT_PERCENT, endpoint_id) end if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) > 0 then - cache_data_with_timeout(device, device.preferences.reverse and 0 or 100, TARGET_TILT_PERCENT) + cache_data_with_timeout(device, device.preferences.reverse and 0 or 100, TARGET_TILT_PERCENT, endpoint_id) end device:send(device.preferences.reverse and clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) or @@ -280,13 +339,14 @@ end local function handle_shade_level(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) local hundredths_lift_percentage = (100 - cmd.args.shadeLevel) * 100 - cache_data_with_timeout(device, cmd.args.shadeLevel, TARGET_LIFT_PERCENT) + cache_data_with_timeout(device, cmd.args.shadeLevel, TARGET_LIFT_PERCENT, endpoint_id) device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( device, endpoint_id, hundredths_lift_percentage )) end local function handle_step_shade_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) local step = cmd.args.stepSize local latest_lift_percentage = device:get_latest_state( "main", @@ -294,9 +354,9 @@ local function handle_step_shade_level(driver, device, cmd) capabilities.windowShadeLevel.shadeLevel.NAME, 0 ) - local target_lift_percent = device:get_field(TARGET_LIFT_PERCENT.STATE) or latest_lift_percentage + local target_lift_percent = get_field_for_endpoint(device, TARGET_LIFT_PERCENT.STATE, endpoint_id) or latest_lift_percentage local updated_target_lift = utils.clamp_value(target_lift_percent + step, 0, 100) - cache_data_with_timeout(device, updated_target_lift, TARGET_LIFT_PERCENT) + cache_data_with_timeout(device, updated_target_lift, TARGET_LIFT_PERCENT, endpoint_id) driver:inject_capability_command(device, { capability = capabilities.windowShadeLevel.ID, component = cmd.component, @@ -309,7 +369,7 @@ end local function handle_shade_tilt_level(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) local hundredths_tilt_percentage = (100 - cmd.args.level) * 100 - cache_data_with_timeout(device, cmd.args.level, TARGET_TILT_PERCENT) + cache_data_with_timeout(device, cmd.args.level, TARGET_TILT_PERCENT, endpoint_id) device:send(clusters.WindowCovering.server.commands.GoToTiltPercentage( device, endpoint_id, hundredths_tilt_percentage )) @@ -320,10 +380,9 @@ local function current_position_lift_percent_100ths_handler(driver, device, ib, if not ib.data.value then return end local lift_percent = 100 - math.floor(ib.data.value / 100) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(lift_percent)) - - 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) + if is_target_value_reached(lift_percent, get_field_for_endpoint(device, TARGET_LIFT_PERCENT.STATE, ib.endpoint_id)) then + clear_cached_data(device, TARGET_LIFT_PERCENT, ib.endpoint_id) + try_emit_idle_window_shade_status(device, ib.endpoint_id) end end @@ -331,10 +390,9 @@ local function current_position_tilt_percent_100ths_handler(driver, device, ib, if not ib.data.value then return end local tilt_percent = 100 - math.floor(ib.data.value / 100) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeTiltLevel.shadeTiltLevel(tilt_percent)) - - 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) + if is_target_value_reached(tilt_percent, get_field_for_endpoint(device, TARGET_TILT_PERCENT.STATE, ib.endpoint_id)) then + clear_cached_data(device, TARGET_TILT_PERCENT, ib.endpoint_id) + try_emit_idle_window_shade_status(device, ib.endpoint_id) end end @@ -347,10 +405,10 @@ local function target_position_lift_percent_100ths_handler(driver, device, ib, r capabilities.windowShadeLevel.shadeLevel.NAME ) 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) + clear_cached_data(device, TARGET_LIFT_PERCENT, ib.endpoint_id) + try_emit_idle_window_shade_status(device, ib.endpoint_id) else - cache_data_with_timeout(device, target_lift_percent, TARGET_LIFT_PERCENT) + cache_data_with_timeout(device, target_lift_percent, TARGET_LIFT_PERCENT, ib.endpoint_id) end end end @@ -364,10 +422,10 @@ local function target_position_tilt_percent_100ths_handler(driver, device, ib, r capabilities.windowShadeTiltLevel.shadeTiltLevel.NAME ) 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) + clear_cached_data(device, TARGET_TILT_PERCENT, ib.endpoint_id) + try_emit_idle_window_shade_status(device, ib.endpoint_id) else - cache_data_with_timeout(device, target_tilt_percent, TARGET_TILT_PERCENT) + cache_data_with_timeout(device, target_tilt_percent, TARGET_TILT_PERCENT, ib.endpoint_id) end end end @@ -376,12 +434,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 diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_multi_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_multi_window_covering.lua new file mode 100644 index 0000000000..b482fa4c93 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_multi_window_covering.lua @@ -0,0 +1,1380 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local clusters = require "st.matter.clusters" +local version = require "version" +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local matter_child = require "st.matter.child" +local matter_device = require "st.matter.device" +matter_child.MatterChildDevice.endpoint_to_component = matter_device.MatterDevice.endpoint_to_component + +local WindowCovering = clusters.WindowCovering + +test.disable_startup_messages() + +local CHILD_DEVICE_MAP = { + {key = 2, ep = 6}, + {key = 3, ep = 7}, + {key = 4, ep = 8}, + {key = 5, ep = 9}, + {key = 6, ep = 10}, + {key = 7, ep = 11} +} + +local mock_parent_device = test.mock_device.build_test_matter_device( + { + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {software = 1}, + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 5, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + { + endpoint_id = CHILD_DEVICE_MAP[1].ep, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + { + endpoint_id = CHILD_DEVICE_MAP[2].ep, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + { + endpoint_id = CHILD_DEVICE_MAP[3].ep, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + { + endpoint_id = CHILD_DEVICE_MAP[4].ep, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + { + endpoint_id = CHILD_DEVICE_MAP[5].ep, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + { + endpoint_id = CHILD_DEVICE_MAP[6].ep, + clusters = { + { + cluster_id = WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + }, + }, + } +) + +local mock_child_device = { + test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", CHILD_DEVICE_MAP[1].key), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + device_network_id = string.format("%s:%02X", mock_parent_device.id, CHILD_DEVICE_MAP[1].key), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", CHILD_DEVICE_MAP[1].ep) + }), + test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", CHILD_DEVICE_MAP[2].key), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + device_network_id = string.format("%s:%02X", mock_parent_device.id, CHILD_DEVICE_MAP[2].key), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", CHILD_DEVICE_MAP[2].ep) + }), + test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", CHILD_DEVICE_MAP[3].key), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + device_network_id = string.format("%s:%02X", mock_parent_device.id, CHILD_DEVICE_MAP[3].key), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", CHILD_DEVICE_MAP[3].ep) + }), + test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", CHILD_DEVICE_MAP[4].key), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + device_network_id = string.format("%s:%02X", mock_parent_device.id, CHILD_DEVICE_MAP[4].key), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", CHILD_DEVICE_MAP[4].ep) + }), + test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", CHILD_DEVICE_MAP[5].key), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + device_network_id = string.format("%s:%02X", mock_parent_device.id, CHILD_DEVICE_MAP[5].key), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", CHILD_DEVICE_MAP[5].ep) + }), + test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", CHILD_DEVICE_MAP[6].key), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), + device_network_id = string.format("%s:%02X", mock_parent_device.id, CHILD_DEVICE_MAP[6].key), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", CHILD_DEVICE_MAP[6].ep) + }) +} + +local CLUSTER_SUBSCRIBE_LIST = { + clusters.LevelControl.server.attributes.CurrentLevel, + WindowCovering.server.attributes.CurrentPositionLiftPercent100ths, + WindowCovering.server.attributes.CurrentPositionTiltPercent100ths, + WindowCovering.server.attributes.TargetPositionLiftPercent100ths, + WindowCovering.server.attributes.TargetPositionTiltPercent100ths, + WindowCovering.server.attributes.OperationalStatus, + clusters.PowerSource.server.attributes.BatPercentRemaining +} + +local function set_preset(device) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.position(50, {visibility = {displayed = false}}) + ) + ) +end + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + for i = 1, #mock_child_device do + test.mock_device.add_test_device(mock_child_device[i]) + end + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "added" }) + test.socket.capability:__expect_send( + mock_parent_device:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "init" }) + set_preset(mock_parent_device) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_parent_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_parent_device)) end + end + test.socket.matter:__expect_send({mock_parent_device.id, subscribe_request}) + for _, child in ipairs(CHILD_DEVICE_MAP) do + mock_parent_device:expect_device_create({ + type = "EDGE_CHILD", + label = string.format("%s %d", "Window Covering", child.key), + profile = "window-covering", + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%d", child.ep) + }) + end + + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "doConfigure" }) + mock_parent_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_parent_device.id, clusters.PowerSource.attributes.AttributeList:read()}) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Idle WindowCovering OperationalStatus state triggers 'open' capability emission if lift position is 100", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(100) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Idle WindowCovering OperationalStatus state triggers 'closed' capability emission if lift position is 0", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Idle WindowCovering OperationalStatus state triggers 'partially open' capability emission if lift position is 50", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Idle WindowCovering OperationalStatus state triggers 'partially open' capability emission if tilt position is between 1-100", + function() + for i = 1, #mock_child_device do + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, ((100 - 15) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(15) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Unknown WindowCovering OperationalStatus state triggers 'unknown' capability emission", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 99), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.unknown() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Opening WindowCovering OperationalStatus state triggers 'closing' capability emission", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 2), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.closing() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Closing WindowCovering OperationalStatus state triggers 'opening' capability emission", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 1), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.opening() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercent100ths triggers appropriate capability emission", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(100) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionTiltPercent100ths triggers appropriate capability emission", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, ((100 - 15) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(15) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, ((100 - 65) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(65) + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering TargetPositionLiftPercent100ths sets field appropriately", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.TargetPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.wait_for_events() + assert(mock_parent_device:get_field(string.format("__target_lift_percent_%d", CHILD_DEVICE_MAP[i].ep)) == 50) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering TargetPositionTiltPercent100ths sets field appropriately", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.TargetPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.wait_for_events() + assert(mock_parent_device:get_field(string.format("__target_tilt_percent_%d", CHILD_DEVICE_MAP[i].ep)) == 50) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering TargetPositionLiftPercent100ths triggers operational status update when appropriate", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, ((100 - 65) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(65) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering TargetPositionTiltPercent100ths triggers operational status update when appropriate", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, ((100 - 65) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(65) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Open capability command triggers appropriate Matter command", + function() + for i = 1, #mock_child_device do + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.UpOrOpen(mock_parent_device, CHILD_DEVICE_MAP[i].ep)} + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Close capability command triggers appropriate Matter command", + function() + for i = 1, #mock_child_device do + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShade", component = "main", command = "close", args = {}}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.DownOrClose(mock_parent_device, CHILD_DEVICE_MAP[i].ep)} + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Pause capability command triggers appropriate Matter command", + function() + for i = 1, #mock_child_device do + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShade", component = "main", command = "pause", args = {}}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.StopMotion(mock_parent_device, CHILD_DEVICE_MAP[i].ep)} + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Cached target lift/tilt position timeouts trigger one operational status update", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 50 * 100 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(50) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + } + ) + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.UpOrOpen(mock_parent_device, CHILD_DEVICE_MAP[i].ep)} + ) + + test.wait_for_events() + test.mock_time.advance_time(120) + + test.socket.capability:__expect_send({ + mock_child_device[i].id, { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } }, + } + ) + test.wait_for_events() + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Cached target lift timeout triggers one operational status update if tilt target and idle state are first reached appropriately", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(50) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + } + ) + -- two timers are made when open is called + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.UpOrOpen(mock_parent_device, CHILD_DEVICE_MAP[i].ep)} + ) + + -- ensure target state is cached before queueing responses + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ), + } + ) + + -- advance time to trigger the single remaining timer + test.wait_for_events() + test.mock_time.advance_time(120) + test.socket.capability:__expect_send({ + mock_child_device[i].id, { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } }, + } + ) + test.wait_for_events() + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test("SetShadeLevel capability command triggers appropriate Matter command", +function() + for i = 1, #mock_child_device do + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 20 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 8000)} + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test("SetShadeTiltLevel capability command triggers appropriate Matter command", +function() + for i = 1, #mock_child_device do + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 60 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 4000)} + ) + end + end, + { + min_api_version = 17 + } +) + +--test battery +test.register_coroutine_test( + "Battery percent reports should generate correct messages", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 150 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.battery.battery(math.floor(150/2.0+0.5)) + ) + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle preset commands", + function() + for i = 1, #mock_child_device do + local PRESET_LEVEL = 30 + test.socket.capability:__queue_receive({ + mock_child_device[i].id, + {capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = { PRESET_LEVEL }}, + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadePreset.position(PRESET_LEVEL) + ) + ) + test.socket.capability:__queue_receive({ + mock_child_device[i].id, + {capability = "windowShadePreset", component = "main", command = "presetPosition", args = {}}, + }) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, (100 - PRESET_LEVEL) * 100)} + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Test profile change to window-covering-battery when battery percent remaining attribute (attribute ID 12) is available", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, {uint32(12)}) + } + ) + mock_child_device[i]:expect_metadata_update({ profile = "window-covering-tilt-battery" }) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Test that profile is not changed to window-covering-battery when battery percent remaining attribute (attribute ID 12) is not available", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_child_device[i], CHILD_DEVICE_MAP[i].ep, {uint32(10)}) + } + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Check that preference updates to reverse polarity after being set to true and that the shade lift operates as expected when opening and closing", + function() + for i = 1, #mock_child_device do + test.socket.device_lifecycle():__queue_receive(mock_child_device[i]:generate_info_changed({ preferences = { reverse = "true" } })) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 100 * 100 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(100) + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_child_device[i].id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 85 }}, + }) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 1500)} + ) + test.socket.capability:__queue_receive({ + mock_child_device[i].id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 100 }}, + }) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 0)} + ) + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Check that preference updates to reverse polarity after being set to true and that the shade tilt operates as expected when opening and closing", + function() + for i = 1, #mock_child_device do + test.socket.device_lifecycle():__queue_receive(mock_child_device[i]:generate_info_changed({ preferences = { reverse = "true" } })) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 100 * 100 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) + ) + ) + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ) + }) + test.socket.matter:__queue_receive({ + mock_child_device[i].id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 0 + ) + }) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_child_device[i].id, + {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 15 }}, + }) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 8500)} + ) + test.socket.capability:__queue_receive({ + mock_child_device[i].id, + {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 0 }}, + }) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 10000)} + ) + end + end, + { + min_api_version = 17 + } +) + +-- -- stepShadeLevel tests + +test.register_coroutine_test( + "WindowShade stepShadeLevel cmd handler - step up", + function() + for i = 1, #mock_child_device do + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 9000)} + ) + end + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel cmd handler - step down", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -20 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 7000)} + ) + end + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel cmd handler - continuous step with target tracking", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 7000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(30) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 6000)} + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 5000)} + ) + end + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel - target reached clears target marker", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 4000)} + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 4000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(60) + ) + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel - step up to maximum (100)", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 500 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(95) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 0)} + ) + end + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel - step down to minimum (0)", + function() + for i = 1, #mock_child_device do + test.socket.matter:__queue_receive( + { + mock_child_device[i].id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child_device[i], CHILD_DEVICE_MAP[i].ep, 9500 + ), + } + ) + test.socket.capability:__expect_send( + mock_child_device[i]:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(5) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_child_device[i].id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_parent_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_parent_device, CHILD_DEVICE_MAP[i].ep, 10000)} + ) + end + end, + { + min_api_version = 19 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 7199e3704d..f2e2f3530e 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -465,7 +465,7 @@ test.register_coroutine_test( } ) test.wait_for_events() - assert(mock_device:get_field("__target_lift_percent") == 50) + assert(mock_device:get_field("__target_lift_percent_10") == 50) end, { min_api_version = 17 @@ -484,7 +484,7 @@ test.register_coroutine_test( } ) test.wait_for_events() - assert(mock_device:get_field("__target_tilt_percent") == 50) + assert(mock_device:get_field("__target_tilt_percent_10") == 50) end, { min_api_version = 17