diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua index db8cfe67d4..ad6be4b873 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua @@ -23,6 +23,7 @@ local PRODUCT_ID = { SWITCH_2G = 0x0006, PIR_1_1M = 0x0007, PIR_2_2M = 0x000A, + ROTARY_DIMMER = 0x000C } if version.api < 16 then @@ -33,6 +34,12 @@ local function subscribe (device, endpoint_id, cluster_id, attr_id, event_id) device:send(cluster_base.subscribe(device, endpoint_id, cluster_id, attr_id, event_id)) end +local function is_standalone_device (device) + if switch_utils.get_product_override_field(device, "is_standalone") then + return true + end +end + local function get_parent (driver, device) return driver:get_device_info(device:get_field(PARENT_ID) or nil) end @@ -51,12 +58,12 @@ local function get_matter_device(device) return nil end -local function extract_reported_endpoints(ib) +local function extract_reported_endpoints(ib, device) local eps = {} if ib.data and ib.data.elements then for _, el in ipairs(ib.data.elements) do local ep_id = el.value - if type(ep_id) == "number" and ep_id ~= 1 and ep_id ~= 2 then + if type(ep_id) == "number" and ep_id ~= 1 and ep_id ~= 2 or is_standalone_device(device) then table.insert(eps, ep_id) end end @@ -188,11 +195,16 @@ local function info_changed(driver, device, event, args) return end + if is_standalone_device(device) then + device:subscribe() + return + end + if device.profile.id ~= args.old_st_store.profile.id or device.network_type == device_lib.NETWORK_TYPE_CHILD then - local parent = device:get_parent_device() - local matter_device = get_matter_device(parent) - local map = {} device.thread:call_with_delay(2, function() + local parent = device:get_parent_device() + local matter_device = get_matter_device(parent) + local map = {} if device:supports_capability(capabilities.button) then local button_eps = parent:get_field(BUTTON_EPS) local clean_eps = {} @@ -225,7 +237,7 @@ local function info_changed(driver, device, event, args) subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID) subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) end - if device.id == matter_device.id then + if not is_standalone_device(device) and device.id == matter_device.id then parent:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) matter_device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) end @@ -234,6 +246,13 @@ local function info_changed(driver, device, event, args) end local function device_init (driver, device) + if is_standalone_device(device) then + device:set_find_child(switch_utils.find_child) + device:set_field(PARENT_ID, device.id) + device:set_field(MATTER_DEVICE_ID, device.id) + subscribe(device, nil, clusters.Descriptor.ID, clusters.Descriptor.attributes.PartsList.ID, nil) + return + end if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then device.thread:call_with_delay(4, function() info_changed(driver, device, nil, { @@ -265,7 +284,6 @@ local function device_init (driver, device) end) end) end - device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) end @@ -274,21 +292,26 @@ local function handle_descriptor_report(driver, device, ib, response) local product_id = device.manufacturer_info and device.manufacturer_info.product_id local parent = get_parent(driver, device) - if not parent or ib.endpoint_id ~= 2 then - return + if is_standalone_device(device) then + if ib.endpoint_id ~= 0 then + return + end + else + if ib.endpoint_id ~= 2 or not parent then + return + end end - local matter_device = get_matter_device(device) + local matter_device = get_matter_device(device) or device + local new_eps = extract_reported_endpoints(ib, device) or {} - local new_eps = extract_reported_endpoints(ib) or {} table.sort(new_eps) - local removed_eps, added_eps = detect_endpoint_changes(device, new_eps) + local removed_eps, added_eps = detect_endpoint_changes(device, new_eps) device:set_field(ACTIVE_EPS, new_eps, { persist = true }) for _, ep_id in ipairs(added_eps or {}) do subscribe(parent, ep_id, clusters.Descriptor.ID, clusters.Descriptor.attributes.DeviceTypeList.ID, nil) - if product_id == PRODUCT_ID.SWITCH_1G and ep_id == 3 then matter_device:try_update_metadata({ profile = "light-binary" }) elseif product_id == PRODUCT_ID.SWITCH_2G then @@ -347,6 +370,7 @@ local function device_type_handler (driver, device, ib) end local parent = get_parent(driver, device) + if not parent then return end @@ -356,10 +380,19 @@ local function device_type_handler (driver, device, ib) local new_btn_eps = {} local ep_id = ib.endpoint_id local value = ib.data.elements + local reports_dimmable = false + + for _, element in ipairs(value) do + local device_type_field = element.elements.device_type + if device_type_field and device_type_field.value == fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE then + reports_dimmable = true + end + end for _, v in ipairs(stored_btn_eps) do table.insert(new_btn_eps, v) end + for _, element in ipairs(value) do local device_type_field = element.elements.device_type local device_type_id = device_type_field and device_type_field.value @@ -396,11 +429,17 @@ local function device_type_handler (driver, device, ib) driver:try_delete_device(ep_id) end return + elseif ep_id == 1 and device.manufacturer_info.product_id == PRODUCT_ID.ROTARY_DIMMER then + if not reports_dimmable then + device:try_update_metadata({ profile = "light-binary" }) + end end create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) elseif device_type_id == fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE then if ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then return + elseif ep_id == 1 and device.manufacturer_info.product_id == PRODUCT_ID.ROTARY_DIMMER then + device:try_update_metadata({ profile = "light-level" }) end create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) elseif device_type_id == fields.DEVICE_TYPE_ID.WINDOW_COVERING then @@ -433,7 +472,7 @@ local function do_configure (driver, device) device:try_update_metadata({ profile = "4-button" }) elseif #bt_eps == 2 then device:try_update_metadata({ profile = "2-button" }) - elseif #lvl_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then + elseif #lvl_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G or is_standalone_device(device) then device:try_update_metadata({ profile = "light-level" }) end device:set_field(BUTTON_EPS, bt_eps, { persist = true }) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index d16214d2fd..cf0483dc5c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -125,6 +125,7 @@ SwitchFields.vendor_overrides = { [0x0006] = { needs_hager_subdriver = true }, -- Hager HBnet 2g switch [0x0007] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 1.1M [0x000A] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 2.2M + [0x000C] = { needs_hager_subdriver = true, is_standalone = true }, -- Hager Rotary }, [0x130A] = { -- EVE_MANUFACTURER_ID [0x0050] = { needs_eve_energy_subdriver = true }, -- Eve Energy EU diff --git a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua index a85137d0e3..a863f3d67c 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua @@ -17,6 +17,8 @@ end local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" local PARENT_ID = "PARENT_ID" local BUTTON_EPS = "__button_eps" +local MAIN_WC_EP = "__main_wc_ep" +local ACTIVE_EPS = "__active_EPS" local function create_parent_device(product_id) return test.mock_device.build_test_matter_device({ @@ -434,6 +436,154 @@ local function create_hager_pir_device(profile_name, parent) }) end +-- Create Hager 1G device with ONLY a WindowCovering endpoint (12) - no button or occupancy endpoints +local function create_hager_1g_window_covering(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 1G Window Covering", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0005, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.WindowCovering.types.Feature.LIFT | + clusters.WindowCovering.types.Feature.POSITION_AWARE_LIFT | + clusters.WindowCovering.types.Feature.ABSOLUTE_POSITION, + attributes = { + [clusters.WindowCovering.attributes.OperationalStatus.ID] = 0x00, + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = 0x0000, + } + } + }, + device_types = { { device_type_id = 0x0202, device_type_revision = 1 } } + }, + } + }) +end + +-- Create Hager 1G Relay device with endpoint 3 (OnOff cluster) +local function create_hager_1g_relay(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 1G Relay", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0005, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + } + }, + device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } + }, + } + }) +end + +local function create_hager_rotary_dimmer(profile_name) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 Rotary Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x000C, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + } + }) +end + +local function create_hager_rotary_switch(profile_name) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 Rotary Switch", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x000C, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + } + }, + device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } + }, + } + }) +end + local function add_parent_device(parent) test.mock_device.add_test_device(parent) test.socket.device_lifecycle:__queue_receive({ parent.id, "added" }) @@ -495,8 +645,8 @@ local function button_supported_values (matter_device) end local function initiate_info_changed(device, profile) - test.socket.device_lifecycle:__queue_receive(device:generate_info_changed({ profile = { id = profile } })) test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive(device:generate_info_changed({ profile = { id = profile } })) test.mock_time.advance_time(2) end @@ -512,6 +662,63 @@ local function configure_parent(device) test.mock_time.advance_time(3) end +-- Create window covering child device on EP12 via PartsList and DeviceTypeList reports +local function announce_window_covering_child(host) + test.socket.matter:__queue_receive({ + host.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(host, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + for _, ep in ipairs({ 8, 9, 12 }) do + test.socket.matter:__expect_send({ + host.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(host, ep) + }) + end + + test.socket.matter:__queue_receive({ + host.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(host, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + host:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "window-covering", + parent_device_id = host.id, + parent_assigned_child_key = "12" + }) + + local child_wc = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("window-covering.yml"), + device_network_id = string.format("%s:12", host.id), + parent_device_id = host.id, + parent_assigned_child_key = "12" + }) + test.mock_device.add_test_device(child_wc) + return child_wc +end + +local function add_standalone_device(device, expected_profile_change) + test.mock_device.add_test_device(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ device.id, "doConfigure" }) + device:expect_metadata_update({ profile = expected_profile_change }) + device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.matter:__expect_send({ + device.id, + cluster_base.subscribe(device, nil, descriptor.ID, descriptor.attributes.PartsList.ID, nil) + }) +end + local parent = create_parent_device(0x0006) -- 2G button product local parent_1g = create_parent_device(0x0005) -- 1G button product local parent_pir = create_parent_device(0x0007) -- PIR product @@ -593,9 +800,9 @@ test.register_coroutine_test("Test: 4-Button Device Detection - Profile Changes }) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Button Event Handling - Pushed, Double Press, and Held Events on 4-Button Device", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -800,9 +1007,9 @@ test.register_coroutine_test("Test: Button Event Handling - Pushed, Double Press }) test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.held({ state_change = true }))) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Device Type Handler - Handles Button (Type 15) and OnOff (Type 256) Device Types with Child Creation", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -896,9 +1103,9 @@ test.register_coroutine_test("Test: Device Type Handler - Handles Button (Type 1 clusters.OnOff.attributes.OnOff:subscribe(parent, nil) }) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: 2G Relay - Profile Changes Between light-binary and 2-button Based On Endpoint Availability", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1141,9 +1348,9 @@ test.register_coroutine_test("Test: 2G Relay - Profile Changes Between light-bin parent_assigned_child_key = "4" }) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable Endpoint with Button Support", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1240,9 +1447,9 @@ test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable }) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Update to light-level", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1252,9 +1459,9 @@ test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Updat test.wait_for_events() configure_parent(parent) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control Capabilities", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1327,9 +1534,9 @@ test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control clusters.LevelControl.server.commands.MoveToLevelWithOnOff(dimmer, 4, 51, nil, 0, 0) }) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: PIR Device - Initialization with Motion and Illuminance Capabilities", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1340,9 +1547,9 @@ test.register_coroutine_test("Test: PIR Device - Initialization with Motion and configure_parent(parent_pir) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: PIR Device - Complete Functionality with Motion, Illuminance, and Dimmer Support", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1497,9 +1704,9 @@ test.register_coroutine_test("Test: PIR Device - Complete Functionality with Mot test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switchLevel.level(50))) parent_pir.expect_native_attr_handler_registration(parent_pir, "switchLevel", "level") end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Host with Window Covering - 2-Button Profile with Window Covering Child Device", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1638,9 +1845,9 @@ test.register_coroutine_test("Test: Host with Window Covering - 2-Button Profile test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closed())) end, - { - min_api_version = 15 - }) + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Window Covering - Preference Changes for Reverse Polarity and Preset Position", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1806,8 +2013,262 @@ test.register_coroutine_test("Test: Window Covering - Preference Changes for Rev cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) }) end, - { - min_api_version = 15 + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: 1G Window Covering - window-covering Profile and No Duplicate Child Device", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent_1g) + local wc_1g = create_hager_1g_window_covering("window-covering", parent_1g) + add_matter_device(wc_1g, parent_1g, "window-covering") + test.wait_for_events() + configure_parent(parent_1g) + test.wait_for_events() + + local main_wc_ep = wc_1g:get_field(MAIN_WC_EP) + assert(main_wc_ep == 12, "MAIN_WC_EP is set to 12") + + -- EP12 reported again → no second child created, MAIN_WC_EP already set + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_1g, 2, data_types.Array({ + data_types.Uint16(12), + })) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 12) + }) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_1g, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + test.wait_for_events() +end, + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: 1G Relay - Profile Changes Between light-binary and 2-button Based On EP3 Availability", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent_1g) + local relay_1g = create_hager_1g_relay("light-binary", parent_1g) + add_switch_matter_device(relay_1g, parent_1g) + test.wait_for_events() + configure_parent(parent_1g) + test.wait_for_events() + + -- Scenario 1: EP3 present → light-binary profile + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_1g, 2, data_types.Array({ + data_types.Uint16(3), + })) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 3) + }) + relay_1g:expect_metadata_update({ profile = "light-binary" }) + test.wait_for_events() + + -- EP3 is device type 256 (OnOff) - no child device created for 1G + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_1g, 3, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } }) + }) + test.wait_for_events() + + -- Scenario 2: EP3 removed, buttons 8 & 9 present → 2-button profile + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_1g, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 8) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 9) + }) + relay_1g:expect_metadata_update({ profile = "2-button" }) + test.wait_for_events() +end, + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: Window Covering - Position Attribute Reports With Reverse Polarity", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local window_covering_device = create_matter_device_with_window("2-button", parent) + add_matter_device(window_covering_device, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + local child_wc = announce_window_covering_child(parent) + test.wait_for_events() + + -- Attribute reports are addressed to the host, so reverse is set on the host + test.socket.device_lifecycle():__queue_receive( + parent:generate_info_changed({ preferences = { reverse = "true" } }) + ) + test.wait_for_events() + + -- Verify 50% position via attribute report - direction independent + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 5000) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.partially_open())) + test.wait_for_events() + + -- Verify 100% position via attribute report - with reverse true, this should report closed + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 0) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closed())) + test.wait_for_events() + + -- Verify 0% position via attribute report - with reverse true, this should report open + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 10000) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.open())) + test.wait_for_events() +end, + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: Window Covering - Unrecognised OperationalStatus Reports Unknown State", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local window_covering_device = create_matter_device_with_window("2-button", parent) + add_matter_device(window_covering_device, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + local child_wc = announce_window_covering_child(parent) + test.wait_for_events() + + -- OperationalStatus 0x03 is neither opening (1) nor closing (2) + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x03) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.unknown())) + test.wait_for_events() +end, + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: Rotary Dimmer - Standalone Initialization Subscribes To PartsList And Sets light-level", function() + test.socket.matter:__set_channel_ordering("relaxed") + local rotary = create_hager_rotary_dimmer("light-level") + add_standalone_device(rotary, "light-level") + test.wait_for_events() + + local button_eps = rotary:get_field(BUTTON_EPS) + assert(#button_eps == 0, "BUTTON_EPS is empty for a rotary dimmer") +end, + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: Rotary Dimmer - EP1 Dimmable Device Type Sets light-level And Creates Child", function() + test.socket.matter:__set_channel_ordering("relaxed") + local rotary = create_hager_rotary_dimmer("light-level") + add_standalone_device(rotary, "light-level") + test.wait_for_events() + + test.socket.matter:__queue_receive({ + rotary.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(rotary, 0, data_types.Array({ + data_types.Uint16(1), + })) + }) + test.socket.matter:__expect_send({ + rotary.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(rotary, 1) + }) + test.wait_for_events() + + local active_eps = rotary:get_field(ACTIVE_EPS) + assert(active_eps[1] == 1, "ACTIVE_EPS contains endpoint 1") + + test.socket.matter:__queue_receive({ + rotary.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(rotary, 1, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + rotary:expect_metadata_update({ profile = "light-level" }) + test.wait_for_events() +end, + { + min_api_version = 15 + }) + +test.register_coroutine_test("Test: Rotary Dimmer - EP1 OnOff Device Type Sets light-binary And Creates Child", function() + test.socket.matter:__set_channel_ordering("relaxed") + local rotary = create_hager_rotary_switch("light-level") + add_standalone_device(rotary, "light-level") + test.wait_for_events() + + test.socket.matter:__queue_receive({ + rotary.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(rotary, 0, data_types.Array({ + data_types.Uint16(1), + })) + }) + test.socket.matter:__expect_send({ + rotary.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(rotary, 1) + }) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + rotary.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(rotary, 1, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + rotary:expect_metadata_update({ profile = "light-binary" }) +end, + { + min_api_version = 15 + }) test.run_registered_tests()