From 598c5f7a2e8cf1a7e3e89050075a8f690a2f56d1 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 18 Sep 2024 15:16:08 -0500 Subject: [PATCH 01/17] Support for combination switch/button devices This change adds support for devices containing both switch and button endpoints. --- .../profiles/2-button-battery-switch.yml | 26 +++ .../SmartThings/matter-switch/src/init.lua | 114 ++++++---- ...test_matter_button_parent_child_switch.lua | 199 ++++++++++++++++++ 3 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua diff --git a/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml new file mode 100644 index 0000000000..30dd1e6fc7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml @@ -0,0 +1,26 @@ +name: 2-button-battery-switch +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: switch + capabilities: + - id: switch + version: 1 + categories: + - name: Switch diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 9b90cfb1d7..4229bdf796 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -140,6 +140,13 @@ local device_type_attribute_map = { clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY + }, + [GENERIC_SWITCH_ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete } } @@ -250,17 +257,14 @@ end local function find_default_endpoint(device, component) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) - local all_eps = {} - for _,ep in ipairs(switch_eps) do - table.insert(all_eps, ep) - end + -- use first button endpoint as main endpoint if one exists for _,ep in ipairs(button_eps) do - table.insert(all_eps, ep) + if ep ~= 0 then --0 is the matter RootNode endpoint + return ep + end end - table.sort(all_eps) - - for _,ep in ipairs(all_eps) do + for _,ep in ipairs(switch_eps) do if ep ~= 0 then --0 is the matter RootNode endpoint return ep end @@ -343,6 +347,7 @@ end local function initialize_switch(driver, device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + local all_eps = {} local profile_name = nil @@ -355,12 +360,34 @@ local function initialize_switch(driver, device) return end + for _,v in ipairs(switch_eps) do + table.insert(all_eps, v) + end + for _,v in ipairs(button_eps) do + table.insert(all_eps, v) + end + table.sort(all_eps) + -- Since we do not support bindings at the moment, we only want to count clusters -- that have been implemented as server. This can be removed when we have -- support for bindings. local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) + if #button_eps > 0 then + for _, ep in ipairs(button_eps) do + -- Configure MCD for button endpoints + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if ep ~= main_endpoint then + component_map[string.format("button%d", current_component_number)] = ep + current_component_number = current_component_number + 1 + else + component_map["main"] = ep + end + component_map_used = true + end + end + end if #switch_eps > 0 then for _, ep in ipairs(switch_eps) do -- Create child devices for non-main switch endpoints @@ -381,19 +408,6 @@ local function initialize_switch(driver, device) parent_child_device = true end end - elseif #button_eps > 0 then - for _, ep in ipairs(button_eps) do - -- Configure MCD for button endpoints - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if ep ~= main_endpoint then - component_map[string.format("button%d", current_component_number)] = ep - current_component_number = current_component_number + 1 - else - component_map["main"] = ep - end - component_map_used = true - end - end end if parent_child_device then @@ -409,7 +423,30 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if #switch_eps > 0 then + if #button_eps > 0 then + local battery_support = false + if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and + #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then + battery_support = true + end + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if battery_support then + profile_name = string.format("%d-button-battery", #button_eps) + else + profile_name = string.format("%d-button", #button_eps) + end + elseif not battery_support then + -- a battery-less button/remote (either single or will use parent/child) + profile_name = "button" + end + + if profile_name then + device:try_update_metadata({profile = profile_name}) + device:set_field(DEFERRED_CONFIGURE, true) + else + configure_buttons(device) + end + elseif #switch_eps > 0 then -- The case where num_switch_server_eps > 0 is a workaround for devices that have a -- Light Switch device type but implement the On Off cluster as server (which is against the spec -- for this device type). By default, we do not support Light Switch device types because by spec these @@ -436,29 +473,6 @@ local function initialize_switch(driver, device) device:try_update_metadata({profile = device_type_profile_map[id]}) end end - elseif #button_eps > 0 then - local battery_support = false - if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and - #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then - battery_support = true - end - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if battery_support then - profile_name = string.format("%d-button-battery", #button_eps) - else - profile_name = string.format("%d-button", #button_eps) - end - elseif not battery_support then - -- a battery-less button/remote (either single or will use parent/child) - profile_name = "button" - end - - if profile_name then - device:try_update_metadata({profile = profile_name}) - device:set_field(DEFERRED_CONFIGURE, true) - else - configure_buttons(device) - end end end @@ -516,7 +530,15 @@ local function device_init(driver, device) id = math.max(id, dt.device_type_id) end for _, attr in pairs(device_type_attribute_map[id] or {}) do - device:add_subscribed_attribute(attr) + if id == GENERIC_SWITCH_ID then + if attr == clusters.PowerSource.attributes.BatPercentRemaining then + device:add_subscribed_attribute(attr) + else + device:add_subscribed_event(attr) + end + else + device:add_subscribed_attribute(attr) + end end end end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua new file mode 100644 index 0000000000..7f87e89977 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua @@ -0,0 +1,199 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local OPTIONS_OVERRIDE = 0x01 + +local parent_ep = 10 +local child_ep = 20 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("button-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = parent_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = child_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + } + } +}) + +local child_profiles = { + [child_ep] = t_utils.get_profile_definition("light-color-level.yml") +} + +local mock_children = {} +for i, endpoint in ipairs(mock_device.endpoints) do + if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then + local child_data = { + profile = child_profiles[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child_ep) + }) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Parent device: Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) + } + } +) + +test.register_message_test( + "Child device: set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child_ep].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child_ep, 370, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child_ep, 370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2700)) + }, + } +) + +test.run_registered_tests() From 711fd079eaf43ad33a58b5f41181006e2e4c2657 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 18 Sep 2024 15:16:08 -0500 Subject: [PATCH 02/17] Support for combination switch/button devices This change adds support for devices containing both switch and button endpoints. --- .../profiles/2-button-battery-switch.yml | 26 ++ .../SmartThings/matter-switch/src/init.lua | 105 ++++---- ...test_matter_button_parent_child_switch.lua | 199 +++++++++++++++ .../test_matter_multi_button_child_switch.lua | 239 ++++++++++++++++++ .../src/test_matter_button_switch_mcd.lua | 199 +++++++++++++++ 5 files changed, 721 insertions(+), 47 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua create mode 100644 drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua diff --git a/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml new file mode 100644 index 0000000000..30dd1e6fc7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml @@ -0,0 +1,26 @@ +name: 2-button-battery-switch +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: switch + capabilities: + - id: switch + version: 1 + categories: + - name: Switch diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 9b90cfb1d7..bfa33fb613 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -140,6 +140,13 @@ local device_type_attribute_map = { clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY + }, + [GENERIC_SWITCH_ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete } } @@ -250,17 +257,14 @@ end local function find_default_endpoint(device, component) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) - local all_eps = {} - for _,ep in ipairs(switch_eps) do - table.insert(all_eps, ep) - end + -- use first button endpoint as main endpoint if one exists for _,ep in ipairs(button_eps) do - table.insert(all_eps, ep) + if ep ~= 0 then --0 is the matter RootNode endpoint + return ep + end end - table.sort(all_eps) - - for _,ep in ipairs(all_eps) do + for _,ep in ipairs(switch_eps) do if ep ~= 0 then --0 is the matter RootNode endpoint return ep end @@ -361,6 +365,20 @@ local function initialize_switch(driver, device) local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) + if #button_eps > 0 then + for _, ep in ipairs(button_eps) do + -- Configure MCD for button endpoints + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if ep ~= main_endpoint then + component_map[string.format("button%d", current_component_number)] = ep + current_component_number = current_component_number + 1 + else + component_map["main"] = ep + end + component_map_used = true + end + end + end if #switch_eps > 0 then for _, ep in ipairs(switch_eps) do -- Create child devices for non-main switch endpoints @@ -381,19 +399,6 @@ local function initialize_switch(driver, device) parent_child_device = true end end - elseif #button_eps > 0 then - for _, ep in ipairs(button_eps) do - -- Configure MCD for button endpoints - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if ep ~= main_endpoint then - component_map[string.format("button%d", current_component_number)] = ep - current_component_number = current_component_number + 1 - else - component_map["main"] = ep - end - component_map_used = true - end - end end if parent_child_device then @@ -409,7 +414,29 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if #switch_eps > 0 then + if #button_eps > 0 then + local battery_support = false + if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and + #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then + battery_support = true + end + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if battery_support then + profile_name = string.format("%d-button-battery", #button_eps) + else + profile_name = string.format("%d-button", #button_eps) + end + elseif not battery_support then + -- a battery-less button/remote (either single or will use parent/child) + profile_name = "button" + end + if profile_name then + device:try_update_metadata({profile = profile_name}) + device:set_field(DEFERRED_CONFIGURE, true) + else + configure_buttons(device) + end + elseif #switch_eps > 0 then -- The case where num_switch_server_eps > 0 is a workaround for devices that have a -- Light Switch device type but implement the On Off cluster as server (which is against the spec -- for this device type). By default, we do not support Light Switch device types because by spec these @@ -431,34 +458,10 @@ local function initialize_switch(driver, device) break end end - if device_type_profile_map[id] ~= nil then device:try_update_metadata({profile = device_type_profile_map[id]}) end end - elseif #button_eps > 0 then - local battery_support = false - if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and - #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then - battery_support = true - end - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if battery_support then - profile_name = string.format("%d-button-battery", #button_eps) - else - profile_name = string.format("%d-button", #button_eps) - end - elseif not battery_support then - -- a battery-less button/remote (either single or will use parent/child) - profile_name = "button" - end - - if profile_name then - device:try_update_metadata({profile = profile_name}) - device:set_field(DEFERRED_CONFIGURE, true) - else - configure_buttons(device) - end end end @@ -516,7 +519,15 @@ local function device_init(driver, device) id = math.max(id, dt.device_type_id) end for _, attr in pairs(device_type_attribute_map[id] or {}) do - device:add_subscribed_attribute(attr) + if id == GENERIC_SWITCH_ID then + if attr == clusters.PowerSource.attributes.BatPercentRemaining then + device:add_subscribed_attribute(attr) + else + device:add_subscribed_event(attr) + end + else + device:add_subscribed_attribute(attr) + end end end end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua new file mode 100644 index 0000000000..7f87e89977 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua @@ -0,0 +1,199 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local OPTIONS_OVERRIDE = 0x01 + +local parent_ep = 10 +local child_ep = 20 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("button-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = parent_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = child_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + } + } +}) + +local child_profiles = { + [child_ep] = t_utils.get_profile_definition("light-color-level.yml") +} + +local mock_children = {} +for i, endpoint in ipairs(mock_device.endpoints) do + if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then + local child_data = { + profile = child_profiles[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child_ep) + }) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Parent device: Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) + } + } +) + +test.register_message_test( + "Child device: set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child_ep].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child_ep, 370, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child_ep, 370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2700)) + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua new file mode 100644 index 0000000000..b9a8acbbc4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua @@ -0,0 +1,239 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local utils = require "st.utils" +local dkjson = require "dkjson" + +local clusters = require "st.matter.clusters" + +local light_ep = 10 +local button1_ep = 20 +local button2_ep = 30 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("2-button-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = light_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = button1_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = button2_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } + } +}) + +local child_data = { + profile = t_utils.get_profile_definition("light-color-level.yml"), + device_network_id = string.format("%s:%d", mock_device.id, light_ep), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", light_ep) +} +local mock_child = test.mock_device.build_test_child_device(child_data) + +local function test_init() + --test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device) + mock_device:expect_metadata_update({ profile = "2-button-battery" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + test.mock_device.add_test_device(mock_child) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", light_ep) + }) + --local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + --for i, cluster in ipairs(cluster_subscribe_list) do + -- if i > 1 then + -- subscribe_request:merge(cluster:subscribe(mock_device)) + -- end + --end + --test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + --test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + --test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 20)}) + --test.mock_device.add_test_device(mock_device) + --mock_device:expect_metadata_update({ profile = "2-button-battery-switch" }) + --local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + --device_info_copy.profile.id = "2-buttons-battery-switch" + --local device_info_json = dkjson.encode(device_info_copy) + --test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + --test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + --test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + --test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button1_ep, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) --should send initial press + } + } +) + +test.register_coroutine_test( + "Handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, button2_ep, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device, button2_ep, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) + end +) + +test.register_message_test( + "On command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "switch3", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, light_ep) + } + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua new file mode 100644 index 0000000000..eb4f370043 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua @@ -0,0 +1,199 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local utils = require "st.utils" +local dkjson = require "dkjson" + +local clusters = require "st.matter.clusters" + +local button1_ep = 10 +local button2_ep = 20 +local light_ep = 30 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("2-button-battery-switch.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = button1_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = button2_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = light_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light + } + } + } +}) + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 20)}) + test.mock_device.add_test_device(mock_device) + mock_device:expect_metadata_update({ profile = "2-button-battery-switch" }) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "2-buttons-battery-switch" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button1_ep, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) --should send initial press + } + } +) + +test.register_coroutine_test( + "Handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, button2_ep, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device, button2_ep, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) + end +) + +test.register_message_test( + "On command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "switch3", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, light_ep) + } + } + } +) + +test.run_registered_tests() From 850652cca255de8c5777f0cb357768a190cc736b Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 1 Oct 2024 15:21:38 -0500 Subject: [PATCH 03/17] fix and expand test cases --- .../SmartThings/matter-switch/src/init.lua | 2 + .../test_matter_multi_button_child_switch.lua | 154 ++++++++++++++---- 2 files changed, 121 insertions(+), 35 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 1e787bfc7e..170e4c0a4c 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -348,6 +348,8 @@ local function initialize_switch(driver, device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) local all_eps = {} + table.sort(switch_eps) + table.sort(button_eps) local profile_name = nil diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua index b9a8acbbc4..4aee55eae9 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua @@ -20,9 +20,10 @@ local dkjson = require "dkjson" local clusters = require "st.matter.clusters" -local light_ep = 10 +local light1_ep = 10 local button1_ep = 20 local button2_ep = 30 +local light2_ep = 40 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -42,7 +43,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = light_ep, + endpoint_id = light1_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, @@ -80,20 +81,40 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch } + }, + { + endpoint_id = light2_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } } } }) -local child_data = { - profile = t_utils.get_profile_definition("light-color-level.yml"), - device_network_id = string.format("%s:%d", mock_device.id, light_ep), - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", light_ep) +local child_profiles = { + [light1_ep] = t_utils.get_profile_definition("light-color-level.yml"), + [light2_ep] = t_utils.get_profile_definition("light-level.yml") } -local mock_child = test.mock_device.build_test_child_device(child_data) + +local mock_children = {} +for i, endpoint in ipairs(mock_device.endpoints) do + if endpoint.endpoint_id ~= button1_ep and endpoint.endpoint_id ~= button2_ep and endpoint.endpoint_id ~= 0 then + local child_data = { + profile = child_profiles[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end local function test_init() - --test.socket.matter:__set_channel_ordering("relaxed") local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -125,40 +146,40 @@ local function test_init() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - test.mock_device.add_test_device(mock_child) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 1", profile = "light-color-level", parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", light_ep) + parent_assigned_child_key = string.format("%d", light1_ep) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", light2_ep) }) - --local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - --for i, cluster in ipairs(cluster_subscribe_list) do - -- if i > 1 then - -- subscribe_request:merge(cluster:subscribe(mock_device)) - -- end - --end - --test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - --test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - --test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 20)}) - --test.mock_device.add_test_device(mock_device) - --mock_device:expect_metadata_update({ profile = "2-button-battery-switch" }) - --local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - --device_info_copy.profile.id = "2-buttons-battery-switch" - --local device_info_json = dkjson.encode(device_info_copy) - --test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - --test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - --test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - --test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) + + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, button2_ep)}) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "2-buttons-battery-switch" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) end test.set_test_init_function(test_init) test.register_message_test( - "Handle single press sequence, no hold", { + "Parent MCD device: handle single press sequence, no hold", { { channel = "matter", direction = "receive", @@ -178,7 +199,7 @@ test.register_message_test( ) test.register_coroutine_test( - "Handle single press sequence for a multi press on multi button", + "Parent MCD device: handle single press sequence for a multi press on multi button", function () test.socket.matter:__queue_receive({ mock_device.id, @@ -215,14 +236,64 @@ test.register_coroutine_test( ) test.register_message_test( - "On command should send the appropriate commands", + "First child device: switch capability should send the appropriate commands", { { channel = "capability", direction = "receive", + message = { + mock_children[light1_ep].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, light1_ep) + } + }, + { + channel = "matter", + direction = "receive", message = { mock_device.id, - { capability = "switch", component = "switch3", command = "on", args = { } } + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, light1_ep, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[light1_ep]:generate_test_message("main", capabilities.switch.switch.on()) + } + } +) +test.register_message_test( + "Second child device: switch capability should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[light2_ep].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[light2_ep].id, capability_id = "switch", capability_cmd_id = "on" } } }, { @@ -230,8 +301,21 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.OnOff.server.commands.On(mock_device, light_ep) + clusters.OnOff.server.commands.On(mock_device, light2_ep) } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, light2_ep, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[light2_ep]:generate_test_message("main", capabilities.switch.switch.on()) } } ) From 1d5aa89404cb91000ed1547e0d32849f508e3d70 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 1 Oct 2024 15:45:42 -0500 Subject: [PATCH 04/17] fixup: remove unneeded profile 2-button-battery-switch.yml was created during the initial work of this PR, when different configurations for combo button/switch devices were being tested, but should be removed now. --- .../profiles/2-button-battery-switch.yml | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml diff --git a/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml deleted file mode 100644 index 30dd1e6fc7..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: 2-button-battery-switch -components: - - id: main - capabilities: - - id: button - version: 1 - - id: battery - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: RemoteController - - id: button2 - capabilities: - - id: button - version: 1 - categories: - - name: RemoteController - - id: switch - capabilities: - - id: switch - version: 1 - categories: - - name: Switch From fd8d2ccd02e5ec606164ede7ec4f9b968bf19994 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 2 Oct 2024 09:21:26 -0500 Subject: [PATCH 05/17] fixup: address review comments * Sort endpoints before returning the default endpoint * Add comment on why the main endpoint should be a button endpoint if one exists --- drivers/SmartThings/matter-switch/src/init.lua | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 170e4c0a4c..9932fed238 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -257,8 +257,12 @@ end local function find_default_endpoint(device, component) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + table.sort(switch_eps) + table.sort(button_eps) - -- use first button endpoint as main endpoint if one exists + -- If button endpoints are present, use the first one as the main endpoint to + -- ensure that if switch endpoints are also present, the parent device will be + -- a multi-component device composed of all the button endpoints. for _,ep in ipairs(button_eps) do if ep ~= 0 then --0 is the matter RootNode endpoint return ep @@ -347,7 +351,6 @@ end local function initialize_switch(driver, device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) - local all_eps = {} table.sort(switch_eps) table.sort(button_eps) @@ -362,14 +365,6 @@ local function initialize_switch(driver, device) return end - for _,v in ipairs(switch_eps) do - table.insert(all_eps, v) - end - for _,v in ipairs(button_eps) do - table.insert(all_eps, v) - end - table.sort(all_eps) - -- Since we do not support bindings at the moment, we only want to count clusters -- that have been implemented as server. This can be removed when we have -- support for bindings. From a546d866e4ac7b1fb6335babe0372b1e32215160 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 2 Oct 2024 09:23:41 -0500 Subject: [PATCH 06/17] remove unused test file --- .../src/test_matter_button_switch_mcd.lua | 199 ------------------ 1 file changed, 199 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua diff --git a/drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua deleted file mode 100644 index eb4f370043..0000000000 --- a/drivers/SmartThings/matter-switch/src/test_matter_button_switch_mcd.lua +++ /dev/null @@ -1,199 +0,0 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" -local utils = require "st.utils" -local dkjson = require "dkjson" - -local clusters = require "st.matter.clusters" - -local button1_ep = 10 -local button2_ep = 20 -local light_ep = 30 - -local mock_device = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("2-button-battery-switch.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = button1_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = button2_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, - cluster_type = "SERVER" - }, - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = light_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light - } - } - } -}) - -local function test_init() - test.socket.matter:__set_channel_ordering("relaxed") - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 20)}) - test.mock_device.add_test_device(mock_device) - mock_device:expect_metadata_update({ profile = "2-button-battery-switch" }) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "2-buttons-battery-switch" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, button1_ep, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) --should send initial press - } - } -) - -test.register_coroutine_test( - "Handle single press sequence for a multi press on multi button", - function () - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, button2_ep, {previous_position = 0} - ) - }) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.MultiPressOngoing:build_test_event_report( - mock_device, button2_ep, {new_position = 1, current_number_of_presses_counted = 2} - ) - }) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} - ) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) - end -) - -test.register_message_test( - "On command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "switch3", command = "on", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, light_ep) - } - } - } -) - -test.run_registered_tests() From f2e5dfb0ce44cde1e3aa85654caaa29b8ce6d995 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 2 Oct 2024 10:36:21 -0500 Subject: [PATCH 07/17] use the type of main endpoint to determine which configuration to use --- drivers/SmartThings/matter-switch/src/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index ff415a83d4..729d7d7635 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -536,7 +536,7 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if #button_eps > 0 then + if tbl_contains(button_eps, main_endpoint) then local battery_support = false if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then @@ -559,7 +559,7 @@ local function initialize_switch(driver, device) else configure_buttons(device) end - elseif #switch_eps > 0 then + else -- The case where num_switch_server_eps > 0 is a workaround for devices that have a -- Light Switch device type but implement the On Off cluster as server (which is against the spec -- for this device type). By default, we do not support Light Switch device types because by spec these From 7e4a17724218c22e33c9ee18a4f48da83e625c95 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 2 Oct 2024 12:05:44 -0500 Subject: [PATCH 08/17] fixup: make switch endpoints check explicit --- drivers/SmartThings/matter-switch/src/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 729d7d7635..b1e069ee96 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -559,7 +559,7 @@ local function initialize_switch(driver, device) else configure_buttons(device) end - else + elseif #switch_eps > 0 then -- The case where num_switch_server_eps > 0 is a workaround for devices that have a -- Light Switch device type but implement the On Off cluster as server (which is against the spec -- for this device type). By default, we do not support Light Switch device types because by spec these From 39d5bdc6499a26fff67477a835967fb03ac62941 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Thu, 3 Oct 2024 11:27:14 -0500 Subject: [PATCH 09/17] remove unused argument and unneeded logic * remove the component argument from find_default_endpoint * remove checks for button and switch endpoints in initialize_switch as they are not necessary --- .../SmartThings/matter-switch/src/init.lua | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b1e069ee96..28dd320c06 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -345,7 +345,7 @@ end --- In this case the function returns the lowest endpoint value that isn't 0 --- and supports the OnOff or Switch cluster. This is done to bypass the --- BRIDGED_NODE_DEVICE_TYPE on bridged devices. -local function find_default_endpoint(device, component) +local function find_default_endpoint(device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) table.sort(switch_eps) @@ -486,39 +486,35 @@ local function initialize_switch(driver, device) local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) - if #button_eps > 0 then - for _, ep in ipairs(button_eps) do - -- Configure MCD for button endpoints - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if ep ~= main_endpoint then - component_map[string.format("button%d", current_component_number)] = ep - current_component_number = current_component_number + 1 - else - component_map["main"] = ep - end - component_map_used = true + for _, ep in ipairs(button_eps) do + -- Configure MCD for button endpoints + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if ep ~= main_endpoint then + component_map[string.format("button%d", current_component_number)] = ep + current_component_number = current_component_number + 1 + else + component_map["main"] = ep end + component_map_used = true end end - if #switch_eps > 0 then - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 - local name = string.format("%s %d", device.label, num_switch_server_eps) - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true - end + for _, ep in ipairs(switch_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + num_switch_server_eps = num_switch_server_eps + 1 + local name = string.format("%s %d", device.label, num_switch_server_eps) + if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint + local child_profile = assign_child_profile(device, ep) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep), + vendor_provided_label = name + } + ) + parent_child_device = true end end end @@ -593,7 +589,7 @@ local function component_to_endpoint(device, component) if map[component] then return map[component] end - return find_default_endpoint(device, component) + return find_default_endpoint(device) end local function endpoint_to_component(device, ep) From 0d3de4232cc0654cd61f4469aea03752983066ea Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Fri, 4 Oct 2024 13:44:13 -0500 Subject: [PATCH 10/17] add more test cases to verify alternate endpoint configurations --- ...test_matter_button_parent_child_switch.lua | 61 +- .../test_matter_multi_button_child_switch.lua | 777 ++++++++++++++---- 2 files changed, 668 insertions(+), 170 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua index 7f87e89977..42177e8627 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua @@ -22,7 +22,8 @@ local OPTIONS_MASK = 0x01 local OPTIONS_OVERRIDE = 0x01 local parent_ep = 10 -local child_ep = 20 +local child1_ep = 20 +local child2_ep = 30 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -56,7 +57,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = child_ep, + endpoint_id = child1_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, @@ -65,12 +66,24 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light } + }, + { + endpoint_id = child2_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } } } }) local child_profiles = { - [child_ep] = t_utils.get_profile_definition("light-color-level.yml") + [child1_ep] = t_utils.get_profile_definition("light-color-level.yml"), + [child2_ep] = t_utils.get_profile_definition("light-level.yml") } local mock_children = {} @@ -127,7 +140,15 @@ local function test_init() label = "Matter Switch 1", profile = "light-color-level", parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child_ep) + parent_assigned_child_key = string.format("%d", child1_ep) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child2_ep) }) end @@ -154,13 +175,13 @@ test.register_message_test( ) test.register_message_test( - "Child device: set color temperature should send the appropriate commands", + "First child device: set color temperature should send the appropriate commands", { { channel = "capability", direction = "receive", message = { - mock_children[child_ep].id, + mock_children[child1_ep].id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } } }, @@ -169,7 +190,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child_ep, 370, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child1_ep, 370, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) } }, { @@ -177,7 +198,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child_ep) + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child1_ep) } }, { @@ -185,13 +206,33 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child_ep, 370) + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child1_ep, 370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2700)) + }, + } +) + + +test.register_message_test( + "Second child device: current level reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child2_ep, 50) } }, { channel = "capability", direction = "send", - message = mock_children[child_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2700)) + message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) }, } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua index 4aee55eae9..c914737cfd 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua @@ -20,12 +20,18 @@ local dkjson = require "dkjson" local clusters = require "st.matter.clusters" -local light1_ep = 10 -local button1_ep = 20 -local button2_ep = 30 -local light2_ep = 40 +-- Three mock devices are used to test the functionality of three different endpoint configurations: +-- (1) single switch endpoint and multiple button endpoints, +-- (2) multiple switch endpoints and multiple button endpoints (with the button enpoints lower than the switch endpoints), and +-- (3) multiple switch endpoints and multiple button endpoints (with the switch enpoints lower than the button endpoints) -local mock_device = test.mock_device.build_test_matter_device({ +-- Configuration 1: Single switch endpoint and multiple button endpoints + +local configuration_1_button1_ep = 10 +local configuration_1_button2_ep = 20 +local configuration_1_light_ep = 30 + +local mock_device_configuration_1 = test.mock_device.build_test_matter_device({ label = "Matter Switch", profile = t_utils.get_profile_definition("2-button-battery.yml"), manufacturer_info = { @@ -43,7 +49,36 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = light1_ep, + endpoint_id = configuration_1_button1_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = configuration_1_button2_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = configuration_1_light_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, @@ -52,9 +87,46 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light } + } + } +}) + +local child_profile = t_utils.get_profile_definition("light-color-level.yml") + +local child_data = { + profile = child_profile, + device_network_id = string.format("%s:%d", mock_device_configuration_1.id, configuration_1_light_ep), + parent_device_id = mock_device_configuration_1.id, + parent_assigned_child_key = string.format("%d", configuration_1_light_ep) +} +local mock_device_configuration_1_child = test.mock_device.build_test_child_device(child_data) + +-- Configuration 2: Multiple switch endpoints and multiple button endpoints; button endpoints lower than the switch endpoints + +local configuration_2_button1_ep = 10 +local configuration_2_button2_ep = 20 +local configuration_2_light1_ep = 30 +local configuration_2_light2_ep = 40 + +local mock_device_configuration_2 = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("2-button-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } }, { - endpoint_id = button1_ep, + endpoint_id = configuration_2_button1_ep, clusters = { { cluster_id = clusters.Switch.ID, @@ -68,13 +140,13 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = button2_ep, + endpoint_id = configuration_2_button2_ep, clusters = { { cluster_id = clusters.Switch.ID, feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, cluster_type = "SERVER" }, }, @@ -83,7 +155,18 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = light2_ep, + endpoint_id = configuration_2_light1_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = configuration_2_light2_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} @@ -96,228 +179,602 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) -local child_profiles = { - [light1_ep] = t_utils.get_profile_definition("light-color-level.yml"), - [light2_ep] = t_utils.get_profile_definition("light-level.yml") +local child_profiles_configuration_2 = { + [configuration_2_light1_ep] = t_utils.get_profile_definition("light-color-level.yml"), + [configuration_2_light2_ep] = t_utils.get_profile_definition("light-level.yml") } -local mock_children = {} -for i, endpoint in ipairs(mock_device.endpoints) do - if endpoint.endpoint_id ~= button1_ep and endpoint.endpoint_id ~= button2_ep and endpoint.endpoint_id ~= 0 then - local child_data = { - profile = child_profiles[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), - parent_device_id = mock_device.id, +local mock_device_configuration_2_children = {} +for i, endpoint in ipairs(mock_device_configuration_2.endpoints) do + if endpoint.endpoint_id ~= configuration_2_button1_ep and endpoint.endpoint_id ~= configuration_2_button2_ep and endpoint.endpoint_id ~= 0 then + local child_data_configuration2 = { + profile = child_profiles_configuration_2[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device_configuration_2.id, endpoint.endpoint_id), + parent_device_id = mock_device_configuration_2.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + mock_device_configuration_2_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data_configuration2) end end -local function test_init() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete +-- Configuration 3: Multiple switch endpoints and multiple button endpoints; switch endpoints lower than the button endpoints + +local configuration_3_light1_ep = 10 +local configuration_3_light2_ep = 20 +local configuration_3_button1_ep = 30 +local configuration_3_button2_ep = 40 + +local mock_device_configuration_3 = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("2-button-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = configuration_3_light1_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = configuration_3_light2_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } + }, + { + endpoint_id = configuration_3_button1_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = configuration_3_button2_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) +}) + +local child_profiles_configuration_3 = { + [configuration_3_light1_ep] = t_utils.get_profile_definition("light-color-level.yml"), + [configuration_3_light2_ep] = t_utils.get_profile_definition("light-level.yml") +} + +local mock_device_configuration_3_children = {} +for i, endpoint in ipairs(mock_device_configuration_3.endpoints) do + if endpoint.endpoint_id ~= configuration_3_button1_ep and endpoint.endpoint_id ~= configuration_3_button2_ep and endpoint.endpoint_id ~= 0 then + local child_data_configuration3 = { + profile = child_profiles_configuration_3[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device_configuration_3.id, endpoint.endpoint_id), + parent_device_id = mock_device_configuration_3.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_device_configuration_3_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data_configuration3) + end +end + +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete +} + +local function test_init_configuration_1() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_configuration_1) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(mock_device_configuration_1)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device_configuration_1.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device_configuration_1) + mock_device_configuration_1:expect_metadata_update({ profile = "2-button-battery" }) + test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + test.mock_device.add_test_device(mock_device_configuration_1_child) - test.mock_device.add_test_device(mock_device) - mock_device:expect_metadata_update({ profile = "2-button-battery" }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + mock_device_configuration_1:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "light-color-level", + parent_device_id = mock_device_configuration_1.id, + parent_assigned_child_key = string.format("%d", configuration_1_light_ep) + }) + + test.socket.matter:__expect_send({mock_device_configuration_1.id, subscribe_request}) + + test.socket.matter:__expect_send({mock_device_configuration_1.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_configuration_1, configuration_1_button2_ep)}) + local device_info_copy = utils.deep_copy(mock_device_configuration_1.raw_st_data) + device_info_copy.profile.id = "2-buttons-battery-switch" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_configuration_1.id, "infoChanged", device_info_json }) + test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) +end - for _, child in pairs(mock_children) do +local function test_init_configuration_2() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_configuration_2) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_configuration_2)) + end + end + test.socket.matter:__expect_send({mock_device_configuration_2.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device_configuration_2) + mock_device_configuration_2:expect_metadata_update({ profile = "2-button-battery" }) + test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + for _, child in pairs(mock_device_configuration_2_children) do test.mock_device.add_test_device(child) end - mock_device:expect_device_create({ + mock_device_configuration_2:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 1", profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", light1_ep) + parent_device_id = mock_device_configuration_2.id, + parent_assigned_child_key = string.format("%d", configuration_2_light1_ep) }) - mock_device:expect_device_create({ + mock_device_configuration_2:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", profile = "light-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", light2_ep) + parent_device_id = mock_device_configuration_2.id, + parent_assigned_child_key = string.format("%d", configuration_2_light2_ep) }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device_configuration_2.id, subscribe_request}) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, button2_ep)}) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + test.socket.matter:__expect_send({mock_device_configuration_2.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_configuration_2, configuration_2_button2_ep)}) + local device_info_copy = utils.deep_copy(mock_device_configuration_2.raw_st_data) device_info_copy.profile.id = "2-buttons-battery-switch" local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) + test.socket.device_lifecycle:__queue_receive({ mock_device_configuration_2.id, "infoChanged", device_info_json }) + test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) end -test.set_test_init_function(test_init) +local function test_init_configuration_3() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_configuration_3) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_configuration_3)) + end + end + test.socket.matter:__expect_send({mock_device_configuration_3.id, subscribe_request}) -test.register_message_test( - "Parent MCD device: handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, button1_ep, {new_position = 1} --move to position 1? - ), + test.mock_device.add_test_device(mock_device_configuration_3) + mock_device_configuration_3:expect_metadata_update({ profile = "2-button-battery" }) + test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + for _, child in pairs(mock_device_configuration_3_children) do + test.mock_device.add_test_device(child) + end + + mock_device_configuration_3:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "light-color-level", + parent_device_id = mock_device_configuration_3.id, + parent_assigned_child_key = string.format("%d", configuration_3_light1_ep) + }) + + mock_device_configuration_3:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-level", + parent_device_id = mock_device_configuration_3.id, + parent_assigned_child_key = string.format("%d", configuration_3_light2_ep) + }) + + test.socket.matter:__expect_send({mock_device_configuration_3.id, subscribe_request}) + + test.socket.matter:__expect_send({mock_device_configuration_3.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_configuration_3, configuration_3_button2_ep)}) + local device_info_copy = utils.deep_copy(mock_device_configuration_3.raw_st_data) + device_info_copy.profile.id = "2-buttons-battery-switch" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_configuration_3.id, "infoChanged", device_info_json }) + test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) +end + +test.register_coroutine_test( + "Configuration 1: Parent device: handle single press sequence", + function() + test.socket.matter:__queue_receive( + { + mock_device_configuration_1.id, + clusters.Switch.events.InitialPress:build_test_event_report(mock_device_configuration_1, configuration_1_button1_ep, {new_position = 1}), } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) --should send initial press - } - } + ) + test.socket.capability:__expect_send( + mock_device_configuration_1:generate_test_message( + "main", capabilities.button.button.pushed({state_change = true}) + ) + ) + end, + { test_init = test_init_configuration_1 } ) test.register_coroutine_test( - "Parent MCD device: handle single press sequence for a multi press on multi button", + "Configuration 1: Parent device: handle single press sequence for a multi press on multi button", function () test.socket.matter:__queue_receive({ - mock_device.id, + mock_device_configuration_1.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, button2_ep, {new_position = 1} + mock_device_configuration_1, configuration_1_button2_ep, {new_position = 1} ) }) test.socket.matter:__queue_receive({ - mock_device.id, + mock_device_configuration_1.id, clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, button2_ep, {previous_position = 0} + mock_device_configuration_1, configuration_1_button2_ep, {previous_position = 0} ) }) test.socket.matter:__queue_receive({ - mock_device.id, + mock_device_configuration_1.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, button2_ep, {new_position = 1} + mock_device_configuration_1, configuration_1_button2_ep, {new_position = 1} ) }) test.socket.matter:__queue_receive({ - mock_device.id, + mock_device_configuration_1.id, clusters.Switch.events.MultiPressOngoing:build_test_event_report( - mock_device, button2_ep, {new_position = 1, current_number_of_presses_counted = 2} + mock_device_configuration_1, configuration_1_button2_ep, {new_position = 1, current_number_of_presses_counted = 2} ) }) test.socket.matter:__queue_receive({ - mock_device.id, + mock_device_configuration_1.id, clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + mock_device_configuration_1, configuration_1_button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} ) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) - end + test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) + end, + { test_init = test_init_configuration_1 } ) -test.register_message_test( - "First child device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[light1_ep].id, +test.register_coroutine_test( + "Configuration 2: First child device: switch capability should send the appropriate commands", + function() + test.socket.capability:__queue_receive( + { + mock_device_configuration_2_children[configuration_2_light1_ep].id, + { capability = "switch", component = "main", command = "on", args = { } }, + } + ) + test.socket.devices:__expect_send( + { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_configuration_2_children[configuration_2_light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } + } + ) + test.socket.matter:__expect_send( + { + mock_device_configuration_2.id, clusters.OnOff.server.commands.On(mock_device_configuration_2, configuration_2_light1_ep) + } + ) + test.socket.matter:__queue_receive( + { + mock_device_configuration_2.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_2, configuration_2_light1_ep, false) + } + ) + test.socket.capability:__expect_send( + mock_device_configuration_2_children[configuration_2_light1_ep]:generate_test_message( + "main", capabilities.switch.switch.off() + ) + ) + end, + { test_init = test_init_configuration_2 } +) + +test.register_coroutine_test( + "Configuration 2: Parent device: handle single press sequence", + function() + test.socket.matter:__queue_receive( + { + mock_device_configuration_2.id, + clusters.Switch.events.InitialPress:build_test_event_report(mock_device_configuration_2, configuration_2_button1_ep, {new_position = 1}), + } + ) + test.socket.capability:__expect_send( + mock_device_configuration_2:generate_test_message( + "main", capabilities.button.button.pushed({state_change = true}) + ) + ) + end, + { test_init = test_init_configuration_2 } +) + +test.register_coroutine_test( + "Configuration 2: Parent device: handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device_configuration_2.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device_configuration_2, configuration_2_button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_2.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device_configuration_2, configuration_2_button2_ep, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_2.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device_configuration_2, configuration_2_button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_2.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device_configuration_2, configuration_2_button2_ep, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_2.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device_configuration_2, configuration_2_button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) + end, + { test_init = test_init_configuration_2 } +) + +test.register_coroutine_test( + "Configuration 2: First child device: switch capability should send the appropriate commands", + function() + test.socket.capability:__queue_receive( + { + mock_device_configuration_2_children[configuration_2_light1_ep].id, + { capability = "switch", component = "main", command = "on", args = { } }, + } + ) + test.socket.devices:__expect_send( + { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_configuration_2_children[configuration_2_light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } + } + ) + test.socket.matter:__expect_send( + { + mock_device_configuration_2.id, clusters.OnOff.server.commands.On(mock_device_configuration_2, configuration_2_light1_ep) + } + ) + test.socket.matter:__queue_receive( + { + mock_device_configuration_2.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_2, configuration_2_light1_ep, false) + } + ) + test.socket.capability:__expect_send( + mock_device_configuration_2_children[configuration_2_light1_ep]:generate_test_message( + "main", capabilities.switch.switch.off() + ) + ) + end, + { test_init = test_init_configuration_2 } +) + +test.register_coroutine_test( + "Configuration 2: Second child device: switch capability should send the appropriate commands", + function() + test.socket.capability:__queue_receive( + { + mock_device_configuration_2_children[configuration_2_light2_ep].id, { capability = "switch", component = "main", command = "on", args = { } } } - }, - { - channel = "devices", - direction = "send", - message = { + ) + test.socket.devices:__expect_send( + { "register_native_capability_cmd_handler", - { device_uuid = mock_children[light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } + { device_uuid = mock_device_configuration_2_children[configuration_2_light2_ep].id, capability_id = "switch", capability_cmd_id = "on" } } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, light1_ep) + ) + test.socket.matter:__expect_send( + { + mock_device_configuration_2.id, clusters.OnOff.server.commands.On(mock_device_configuration_2, configuration_2_light2_ep) } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, light1_ep, true) + ) + test.socket.matter:__queue_receive( + { + mock_device_configuration_2.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_2, configuration_2_light2_ep, false) } - }, - { - channel = "capability", - direction = "send", - message = mock_children[light1_ep]:generate_test_message("main", capabilities.switch.switch.on()) - } - } + ) + test.socket.capability:__expect_send( + mock_device_configuration_2_children[configuration_2_light2_ep]:generate_test_message( + "main", capabilities.switch.switch.off() + ) + ) + end, + { test_init = test_init_configuration_2 } ) -test.register_message_test( - "Second child device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[light2_ep].id, + +test.register_coroutine_test( + "Configuration 3: Parent device: handle single press sequence", + function() + test.socket.matter:__queue_receive( + { + mock_device_configuration_3.id, + clusters.Switch.events.InitialPress:build_test_event_report(mock_device_configuration_3, configuration_3_button1_ep, {new_position = 1}), + } + ) + test.socket.capability:__expect_send( + mock_device_configuration_3:generate_test_message( + "main", capabilities.button.button.pushed({state_change = true}) + ) + ) + end, + { test_init = test_init_configuration_3 } +) + +test.register_coroutine_test( + "Configuration 3: Parent device: handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device_configuration_3.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device_configuration_3, configuration_3_button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_3.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device_configuration_3, configuration_3_button2_ep, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_3.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device_configuration_3, configuration_3_button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_3.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device_configuration_3, configuration_3_button2_ep, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device_configuration_3.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device_configuration_3, configuration_3_button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) + end, + { test_init = test_init_configuration_3 } +) + +test.register_coroutine_test( + "Configuration 3: First child device: switch capability should send the appropriate commands", + function() + test.socket.capability:__queue_receive( + { + mock_device_configuration_3_children[configuration_3_light1_ep].id, + { capability = "switch", component = "main", command = "on", args = { } }, + } + ) + test.socket.devices:__expect_send( + { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_configuration_3_children[configuration_3_light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } + } + ) + test.socket.matter:__expect_send( + { + mock_device_configuration_3.id, clusters.OnOff.server.commands.On(mock_device_configuration_3, configuration_3_light1_ep) + } + ) + test.socket.matter:__queue_receive( + { + mock_device_configuration_3.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_3, configuration_3_light1_ep, false) + } + ) + test.socket.capability:__expect_send( + mock_device_configuration_3_children[configuration_3_light1_ep]:generate_test_message( + "main", capabilities.switch.switch.off() + ) + ) + end, + { test_init = test_init_configuration_3 } +) + +test.register_coroutine_test( + "Configuration 3: Second child device: switch capability should send the appropriate commands", + function() + test.socket.capability:__queue_receive( + { + mock_device_configuration_3_children[configuration_3_light2_ep].id, { capability = "switch", component = "main", command = "on", args = { } } } - }, - { - channel = "devices", - direction = "send", - message = { + ) + test.socket.devices:__expect_send( + { "register_native_capability_cmd_handler", - { device_uuid = mock_children[light2_ep].id, capability_id = "switch", capability_cmd_id = "on" } + { device_uuid = mock_device_configuration_3_children[configuration_3_light2_ep].id, capability_id = "switch", capability_cmd_id = "on" } } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, light2_ep) + ) + test.socket.matter:__expect_send( + { + mock_device_configuration_3.id, clusters.OnOff.server.commands.On(mock_device_configuration_3, configuration_3_light2_ep) } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, light2_ep, true) + ) + test.socket.matter:__queue_receive( + { + mock_device_configuration_3.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_3, configuration_3_light2_ep, false) } - }, - { - channel = "capability", - direction = "send", - message = mock_children[light2_ep]:generate_test_message("main", capabilities.switch.switch.on()) - } - } + ) + test.socket.capability:__expect_send( + mock_device_configuration_3_children[configuration_3_light2_ep]:generate_test_message( + "main", capabilities.switch.switch.off() + ) + ) + end, + { test_init = test_init_configuration_3 } ) test.run_registered_tests() From c5a9b6fcba31b98f074cc08dd3e3493af8724856 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 29 Oct 2024 12:02:15 -0500 Subject: [PATCH 11/17] Use MCD for button/switch devices --- .../matter-switch/fingerprints.yml | 6 + .../profiles/light-level-2-button.yml | 30 + .../profiles/light-level-3-button.yml | 36 + .../profiles/light-level-4-button.yml | 42 + .../profiles/light-level-5-button.yml | 48 ++ .../profiles/light-level-6-button.yml | 54 ++ .../profiles/light-level-7-button.yml | 60 ++ .../profiles/light-level-8-button.yml | 66 ++ .../profiles/light-level-button.yml | 24 + .../SmartThings/matter-switch/src/init.lua | 148 ++-- ...test_matter_button_parent_child_switch.lua | 240 ------ .../test_matter_multi_button_child_switch.lua | 780 ------------------ .../test_matter_multi_button_switch_mcd.lua | 338 ++++++++ 13 files changed, 803 insertions(+), 1069 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-button.yml delete mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua delete mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 5afd027dcb..53e55eaadd 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2411,6 +2411,12 @@ matterGeneric: deviceTypes: - id: 0x000F deviceProfileName: button-battery # err on the side of buttons having batteries, it'll get fixed in the driver + - id: "matter/dimmable/light/button" + deviceLabel: Matter Dimmable Light/Button + deviceTypes: + - id: 0x0101 # Dimmable Light + - id: 0x000F # Generic Switch + deviceProfileName: light-level-button matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml new file mode 100644 index 0000000000..7ffb29f8c3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml @@ -0,0 +1,30 @@ +name: light-level-2-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml new file mode 100644 index 0000000000..59600efd72 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml @@ -0,0 +1,36 @@ +name: light-level-3-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml new file mode 100644 index 0000000000..b49b7f2254 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml @@ -0,0 +1,42 @@ +name: light-level-4-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml new file mode 100644 index 0000000000..ee55a6a394 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml @@ -0,0 +1,48 @@ +name: light-level-5-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml new file mode 100644 index 0000000000..805c97763e --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml @@ -0,0 +1,54 @@ +name: light-level-6-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml new file mode 100644 index 0000000000..5cd1666a5f --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml @@ -0,0 +1,60 @@ +name: light-level-7-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml new file mode 100644 index 0000000000..4636359e92 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml @@ -0,0 +1,66 @@ +name: light-level-8-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml new file mode 100644 index 0000000000..d9c6b6fe46 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml @@ -0,0 +1,24 @@ +name: light-level-button +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 28dd320c06..c510813fad 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -348,18 +348,17 @@ end local function find_default_endpoint(device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) - table.sort(switch_eps) - table.sort(button_eps) + local all_eps = {} - -- If button endpoints are present, use the first one as the main endpoint to - -- ensure that if switch endpoints are also present, the parent device will be - -- a multi-component device composed of all the button endpoints. + for _,ep in ipairs(switch_eps) do + table.insert(all_eps, ep) + end for _,ep in ipairs(button_eps) do - if ep ~= 0 then --0 is the matter RootNode endpoint - return ep - end + table.insert(all_eps, ep) end - for _,ep in ipairs(switch_eps) do + table.sort(all_eps) + + for _, ep in ipairs(all_eps) do if ep ~= 0 then --0 is the matter RootNode endpoint return ep end @@ -472,7 +471,6 @@ local function initialize_switch(driver, device) local profile_name = nil local component_map = {} - local current_component_number = 2 local component_map_used = false local parent_child_device = false @@ -485,36 +483,65 @@ local function initialize_switch(driver, device) -- support for bindings. local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) - - for _, ep in ipairs(button_eps) do - -- Configure MCD for button endpoints - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if ep ~= main_endpoint then + if #switch_eps > 0 and #button_eps > 0 then + if #button_eps == 1 or tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + -- For switch devices, the profile components follow the naming convention "switch%d", + -- with the exception of "main" being the first component. Each component will then map + -- to the next lowest endpoint that hasn't been mapped yet. + -- Additionally, since we do not support bindings at the moment, we only want to count + -- On/Off clusters that have been implemented as server. This can be removed when we have + -- support for bindings. + local current_component_number = 1 + for _, ep in ipairs(switch_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + if current_component_number == 1 then + component_map["main"] = ep + else + component_map[string.format("switch%d", current_component_number)] = ep + end + current_component_number = current_component_number + 1; + end + end + current_component_number = 1 + for _, ep in ipairs(button_eps) do component_map[string.format("button%d", current_component_number)] = ep current_component_number = current_component_number + 1 - else - component_map["main"] = ep end component_map_used = true end - end - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 - local name = string.format("%s %d", device.label, num_switch_server_eps) - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true + elseif #switch_eps > 0 then + for _, ep in ipairs(switch_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + num_switch_server_eps = num_switch_server_eps + 1 + local name = string.format("%s %d", device.label, num_switch_server_eps) + if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint + local child_profile = assign_child_profile(device, ep) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep), + vendor_provided_label = name + } + ) + parent_child_device = true + end + end + end + elseif #button_eps > 0 then + local current_component_number = 2 + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + -- Configure MCD for button endpoints + for _, ep in ipairs(button_eps) do + if ep ~= main_endpoint then + component_map[string.format("button%d", current_component_number)] = ep + current_component_number = current_component_number + 1 + else + component_map["main"] = ep + end + component_map_used = true end end end @@ -532,24 +559,23 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if tbl_contains(button_eps, main_endpoint) then - local battery_support = false - if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and - #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then - battery_support = true - end + if #switch_eps > 0 and #button_eps > 0 then if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if battery_support then - profile_name = string.format("%d-button-battery", #button_eps) - else - profile_name = string.format("%d-button", #button_eps) + local dimmable_light = false + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then + dimmable_light = true + end + end + end + if dimmable_light then + profile_name = "light-level" end - elseif not battery_support then - -- a battery-less button/remote (either single or will use parent/child) - profile_name = "button" end if profile_name then + profile_name = profile_name .. string.format("-%d-button", #button_eps) device:try_update_metadata({profile = profile_name}) device:set_field(DEFERRED_CONFIGURE, true) else @@ -563,7 +589,7 @@ local function initialize_switch(driver, device) -- do not have a generic fingerprint and will join as a matter-thing. However, we have seen some devices -- claim to be Light Switch device types and still implement their clusters as server, so this is a -- workaround for those devices. - if num_switch_server_eps > 0 and detect_matter_thing(device) then + if detect_matter_thing(device) and num_switch_server_eps > 0 then local id = 0 for _, ep in ipairs(device.endpoints) do -- main_endpoint only supports server cluster by definition of get_endpoints() @@ -577,10 +603,34 @@ local function initialize_switch(driver, device) break end end + if device_type_profile_map[id] ~= nil then device:try_update_metadata({profile = device_type_profile_map[id]}) end end + elseif #button_eps > 0 then + local battery_support = false + if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and + #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then + battery_support = true + end + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if battery_support then + profile_name = string.format("%d-button-battery", #button_eps) + else + profile_name = string.format("%d-button", #button_eps) + end + elseif not battery_support then + -- a battery-less button/remote (either single or will use parent/child) + profile_name = "button" + end + + if profile_name then + device:try_update_metadata({profile = profile_name}) + device:set_field(DEFERRED_CONFIGURE, true) + else + configure_buttons(device) + end end end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua deleted file mode 100644 index 42177e8627..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua +++ /dev/null @@ -1,240 +0,0 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" - -local clusters = require "st.matter.clusters" -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 - -local parent_ep = 10 -local child1_ep = 20 -local child2_ep = 30 - -local mock_device = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("button-battery.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = parent_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = child1_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - { - endpoint_id = child2_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - } - } -}) - -local child_profiles = { - [child1_ep] = t_utils.get_profile_definition("light-color-level.yml"), - [child2_ep] = t_utils.get_profile_definition("light-level.yml") -} - -local mock_children = {} -for i, endpoint in ipairs(mock_device.endpoints) do - if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then - local child_data = { - profile = child_profiles[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init() - test.socket.matter:__set_channel_ordering("relaxed") - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.mock_device.add_test_device(mock_device) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - - for _, child in pairs(mock_children) do - test.mock_device.add_test_device(child) - end - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep) - }) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Parent device: Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) - } - } -) - -test.register_message_test( - "First child device: set color temperature should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child1_ep].id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child1_ep, 370, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child1_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child1_ep, 370) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2700)) - }, - } -) - - -test.register_message_test( - "Second child device: current level reports should generate appropriate events", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child2_ep, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) - }, - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua deleted file mode 100644 index c914737cfd..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_child_switch.lua +++ /dev/null @@ -1,780 +0,0 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" -local utils = require "st.utils" -local dkjson = require "dkjson" - -local clusters = require "st.matter.clusters" - --- Three mock devices are used to test the functionality of three different endpoint configurations: --- (1) single switch endpoint and multiple button endpoints, --- (2) multiple switch endpoints and multiple button endpoints (with the button enpoints lower than the switch endpoints), and --- (3) multiple switch endpoints and multiple button endpoints (with the switch enpoints lower than the button endpoints) - --- Configuration 1: Single switch endpoint and multiple button endpoints - -local configuration_1_button1_ep = 10 -local configuration_1_button2_ep = 20 -local configuration_1_light_ep = 30 - -local mock_device_configuration_1 = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("2-button-battery.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = configuration_1_button1_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = configuration_1_button2_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, - cluster_type = "SERVER" - }, - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = configuration_1_light_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - } - } -}) - -local child_profile = t_utils.get_profile_definition("light-color-level.yml") - -local child_data = { - profile = child_profile, - device_network_id = string.format("%s:%d", mock_device_configuration_1.id, configuration_1_light_ep), - parent_device_id = mock_device_configuration_1.id, - parent_assigned_child_key = string.format("%d", configuration_1_light_ep) -} -local mock_device_configuration_1_child = test.mock_device.build_test_child_device(child_data) - --- Configuration 2: Multiple switch endpoints and multiple button endpoints; button endpoints lower than the switch endpoints - -local configuration_2_button1_ep = 10 -local configuration_2_button2_ep = 20 -local configuration_2_light1_ep = 30 -local configuration_2_light2_ep = 40 - -local mock_device_configuration_2 = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("2-button-battery.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = configuration_2_button1_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = configuration_2_button2_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, - cluster_type = "SERVER" - }, - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = configuration_2_light1_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - { - endpoint_id = configuration_2_light2_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - } - } -}) - -local child_profiles_configuration_2 = { - [configuration_2_light1_ep] = t_utils.get_profile_definition("light-color-level.yml"), - [configuration_2_light2_ep] = t_utils.get_profile_definition("light-level.yml") -} - -local mock_device_configuration_2_children = {} -for i, endpoint in ipairs(mock_device_configuration_2.endpoints) do - if endpoint.endpoint_id ~= configuration_2_button1_ep and endpoint.endpoint_id ~= configuration_2_button2_ep and endpoint.endpoint_id ~= 0 then - local child_data_configuration2 = { - profile = child_profiles_configuration_2[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device_configuration_2.id, endpoint.endpoint_id), - parent_device_id = mock_device_configuration_2.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_device_configuration_2_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data_configuration2) - end -end - --- Configuration 3: Multiple switch endpoints and multiple button endpoints; switch endpoints lower than the button endpoints - -local configuration_3_light1_ep = 10 -local configuration_3_light2_ep = 20 -local configuration_3_button1_ep = 30 -local configuration_3_button2_ep = 40 - -local mock_device_configuration_3 = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("2-button-battery.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = configuration_3_light1_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - { - endpoint_id = configuration_3_light2_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - endpoint_id = configuration_3_button1_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = configuration_3_button2_ep, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | - clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, - cluster_type = "SERVER" - }, - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - } - } -}) - -local child_profiles_configuration_3 = { - [configuration_3_light1_ep] = t_utils.get_profile_definition("light-color-level.yml"), - [configuration_3_light2_ep] = t_utils.get_profile_definition("light-level.yml") -} - -local mock_device_configuration_3_children = {} -for i, endpoint in ipairs(mock_device_configuration_3.endpoints) do - if endpoint.endpoint_id ~= configuration_3_button1_ep and endpoint.endpoint_id ~= configuration_3_button2_ep and endpoint.endpoint_id ~= 0 then - local child_data_configuration3 = { - profile = child_profiles_configuration_3[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device_configuration_3.id, endpoint.endpoint_id), - parent_device_id = mock_device_configuration_3.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_device_configuration_3_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data_configuration3) - end -end - -local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete -} - -local function test_init_configuration_1() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_configuration_1) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_configuration_1)) - end - end - test.socket.matter:__expect_send({mock_device_configuration_1.id, subscribe_request}) - - test.mock_device.add_test_device(mock_device_configuration_1) - mock_device_configuration_1:expect_metadata_update({ profile = "2-button-battery" }) - test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - - test.mock_device.add_test_device(mock_device_configuration_1_child) - - mock_device_configuration_1:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "light-color-level", - parent_device_id = mock_device_configuration_1.id, - parent_assigned_child_key = string.format("%d", configuration_1_light_ep) - }) - - test.socket.matter:__expect_send({mock_device_configuration_1.id, subscribe_request}) - - test.socket.matter:__expect_send({mock_device_configuration_1.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_configuration_1, configuration_1_button2_ep)}) - local device_info_copy = utils.deep_copy(mock_device_configuration_1.raw_st_data) - device_info_copy.profile.id = "2-buttons-battery-switch" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_configuration_1.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) -end - -local function test_init_configuration_2() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_configuration_2) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_configuration_2)) - end - end - test.socket.matter:__expect_send({mock_device_configuration_2.id, subscribe_request}) - - test.mock_device.add_test_device(mock_device_configuration_2) - mock_device_configuration_2:expect_metadata_update({ profile = "2-button-battery" }) - test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - - for _, child in pairs(mock_device_configuration_2_children) do - test.mock_device.add_test_device(child) - end - - mock_device_configuration_2:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "light-color-level", - parent_device_id = mock_device_configuration_2.id, - parent_assigned_child_key = string.format("%d", configuration_2_light1_ep) - }) - - mock_device_configuration_2:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device_configuration_2.id, - parent_assigned_child_key = string.format("%d", configuration_2_light2_ep) - }) - - test.socket.matter:__expect_send({mock_device_configuration_2.id, subscribe_request}) - - test.socket.matter:__expect_send({mock_device_configuration_2.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_configuration_2, configuration_2_button2_ep)}) - local device_info_copy = utils.deep_copy(mock_device_configuration_2.raw_st_data) - device_info_copy.profile.id = "2-buttons-battery-switch" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_configuration_2.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) -end - -local function test_init_configuration_3() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_configuration_3) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_configuration_3)) - end - end - test.socket.matter:__expect_send({mock_device_configuration_3.id, subscribe_request}) - - test.mock_device.add_test_device(mock_device_configuration_3) - mock_device_configuration_3:expect_metadata_update({ profile = "2-button-battery" }) - test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - - for _, child in pairs(mock_device_configuration_3_children) do - test.mock_device.add_test_device(child) - end - - mock_device_configuration_3:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "light-color-level", - parent_device_id = mock_device_configuration_3.id, - parent_assigned_child_key = string.format("%d", configuration_3_light1_ep) - }) - - mock_device_configuration_3:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device_configuration_3.id, - parent_assigned_child_key = string.format("%d", configuration_3_light2_ep) - }) - - test.socket.matter:__expect_send({mock_device_configuration_3.id, subscribe_request}) - - test.socket.matter:__expect_send({mock_device_configuration_3.id, clusters.Switch.attributes.MultiPressMax:read(mock_device_configuration_3, configuration_3_button2_ep)}) - local device_info_copy = utils.deep_copy(mock_device_configuration_3.raw_st_data) - device_info_copy.profile.id = "2-buttons-battery-switch" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_configuration_3.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) -end - -test.register_coroutine_test( - "Configuration 1: Parent device: handle single press sequence", - function() - test.socket.matter:__queue_receive( - { - mock_device_configuration_1.id, - clusters.Switch.events.InitialPress:build_test_event_report(mock_device_configuration_1, configuration_1_button1_ep, {new_position = 1}), - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_1:generate_test_message( - "main", capabilities.button.button.pushed({state_change = true}) - ) - ) - end, - { test_init = test_init_configuration_1 } -) - -test.register_coroutine_test( - "Configuration 1: Parent device: handle single press sequence for a multi press on multi button", - function () - test.socket.matter:__queue_receive({ - mock_device_configuration_1.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device_configuration_1, configuration_1_button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_1.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device_configuration_1, configuration_1_button2_ep, {previous_position = 0} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_1.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device_configuration_1, configuration_1_button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_1.id, - clusters.Switch.events.MultiPressOngoing:build_test_event_report( - mock_device_configuration_1, configuration_1_button2_ep, {new_position = 1, current_number_of_presses_counted = 2} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_1.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device_configuration_1, configuration_1_button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} - ) - }) - test.socket.capability:__expect_send(mock_device_configuration_1:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) - end, - { test_init = test_init_configuration_1 } -) - -test.register_coroutine_test( - "Configuration 2: First child device: switch capability should send the appropriate commands", - function() - test.socket.capability:__queue_receive( - { - mock_device_configuration_2_children[configuration_2_light1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } }, - } - ) - test.socket.devices:__expect_send( - { - "register_native_capability_cmd_handler", - { device_uuid = mock_device_configuration_2_children[configuration_2_light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - ) - test.socket.matter:__expect_send( - { - mock_device_configuration_2.id, clusters.OnOff.server.commands.On(mock_device_configuration_2, configuration_2_light1_ep) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_configuration_2.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_2, configuration_2_light1_ep, false) - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_2_children[configuration_2_light1_ep]:generate_test_message( - "main", capabilities.switch.switch.off() - ) - ) - end, - { test_init = test_init_configuration_2 } -) - -test.register_coroutine_test( - "Configuration 2: Parent device: handle single press sequence", - function() - test.socket.matter:__queue_receive( - { - mock_device_configuration_2.id, - clusters.Switch.events.InitialPress:build_test_event_report(mock_device_configuration_2, configuration_2_button1_ep, {new_position = 1}), - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_2:generate_test_message( - "main", capabilities.button.button.pushed({state_change = true}) - ) - ) - end, - { test_init = test_init_configuration_2 } -) - -test.register_coroutine_test( - "Configuration 2: Parent device: handle single press sequence for a multi press on multi button", - function () - test.socket.matter:__queue_receive({ - mock_device_configuration_2.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device_configuration_2, configuration_2_button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_2.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device_configuration_2, configuration_2_button2_ep, {previous_position = 0} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_2.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device_configuration_2, configuration_2_button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_2.id, - clusters.Switch.events.MultiPressOngoing:build_test_event_report( - mock_device_configuration_2, configuration_2_button2_ep, {new_position = 1, current_number_of_presses_counted = 2} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_2.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device_configuration_2, configuration_2_button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} - ) - }) - test.socket.capability:__expect_send(mock_device_configuration_2:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) - end, - { test_init = test_init_configuration_2 } -) - -test.register_coroutine_test( - "Configuration 2: First child device: switch capability should send the appropriate commands", - function() - test.socket.capability:__queue_receive( - { - mock_device_configuration_2_children[configuration_2_light1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } }, - } - ) - test.socket.devices:__expect_send( - { - "register_native_capability_cmd_handler", - { device_uuid = mock_device_configuration_2_children[configuration_2_light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - ) - test.socket.matter:__expect_send( - { - mock_device_configuration_2.id, clusters.OnOff.server.commands.On(mock_device_configuration_2, configuration_2_light1_ep) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_configuration_2.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_2, configuration_2_light1_ep, false) - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_2_children[configuration_2_light1_ep]:generate_test_message( - "main", capabilities.switch.switch.off() - ) - ) - end, - { test_init = test_init_configuration_2 } -) - -test.register_coroutine_test( - "Configuration 2: Second child device: switch capability should send the appropriate commands", - function() - test.socket.capability:__queue_receive( - { - mock_device_configuration_2_children[configuration_2_light2_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - ) - test.socket.devices:__expect_send( - { - "register_native_capability_cmd_handler", - { device_uuid = mock_device_configuration_2_children[configuration_2_light2_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - ) - test.socket.matter:__expect_send( - { - mock_device_configuration_2.id, clusters.OnOff.server.commands.On(mock_device_configuration_2, configuration_2_light2_ep) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_configuration_2.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_2, configuration_2_light2_ep, false) - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_2_children[configuration_2_light2_ep]:generate_test_message( - "main", capabilities.switch.switch.off() - ) - ) - end, - { test_init = test_init_configuration_2 } -) - -test.register_coroutine_test( - "Configuration 3: Parent device: handle single press sequence", - function() - test.socket.matter:__queue_receive( - { - mock_device_configuration_3.id, - clusters.Switch.events.InitialPress:build_test_event_report(mock_device_configuration_3, configuration_3_button1_ep, {new_position = 1}), - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_3:generate_test_message( - "main", capabilities.button.button.pushed({state_change = true}) - ) - ) - end, - { test_init = test_init_configuration_3 } -) - -test.register_coroutine_test( - "Configuration 3: Parent device: handle single press sequence for a multi press on multi button", - function () - test.socket.matter:__queue_receive({ - mock_device_configuration_3.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device_configuration_3, configuration_3_button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_3.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device_configuration_3, configuration_3_button2_ep, {previous_position = 0} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_3.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device_configuration_3, configuration_3_button2_ep, {new_position = 1} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_3.id, - clusters.Switch.events.MultiPressOngoing:build_test_event_report( - mock_device_configuration_3, configuration_3_button2_ep, {new_position = 1, current_number_of_presses_counted = 2} - ) - }) - test.socket.matter:__queue_receive({ - mock_device_configuration_3.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device_configuration_3, configuration_3_button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} - ) - }) - test.socket.capability:__expect_send(mock_device_configuration_3:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) - end, - { test_init = test_init_configuration_3 } -) - -test.register_coroutine_test( - "Configuration 3: First child device: switch capability should send the appropriate commands", - function() - test.socket.capability:__queue_receive( - { - mock_device_configuration_3_children[configuration_3_light1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } }, - } - ) - test.socket.devices:__expect_send( - { - "register_native_capability_cmd_handler", - { device_uuid = mock_device_configuration_3_children[configuration_3_light1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - ) - test.socket.matter:__expect_send( - { - mock_device_configuration_3.id, clusters.OnOff.server.commands.On(mock_device_configuration_3, configuration_3_light1_ep) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_configuration_3.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_3, configuration_3_light1_ep, false) - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_3_children[configuration_3_light1_ep]:generate_test_message( - "main", capabilities.switch.switch.off() - ) - ) - end, - { test_init = test_init_configuration_3 } -) - -test.register_coroutine_test( - "Configuration 3: Second child device: switch capability should send the appropriate commands", - function() - test.socket.capability:__queue_receive( - { - mock_device_configuration_3_children[configuration_3_light2_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - ) - test.socket.devices:__expect_send( - { - "register_native_capability_cmd_handler", - { device_uuid = mock_device_configuration_3_children[configuration_3_light2_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - ) - test.socket.matter:__expect_send( - { - mock_device_configuration_3.id, clusters.OnOff.server.commands.On(mock_device_configuration_3, configuration_3_light2_ep) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_configuration_3.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device_configuration_3, configuration_3_light2_ep, false) - } - ) - test.socket.capability:__expect_send( - mock_device_configuration_3_children[configuration_3_light2_ep]:generate_test_message( - "main", capabilities.switch.switch.off() - ) - ) - end, - { test_init = test_init_configuration_3 } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua new file mode 100644 index 0000000000..26645ed5da --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -0,0 +1,338 @@ +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" +local dkjson = require "dkjson" + +local clusters = require "st.matter.generated.zap_clusters" + +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local OPTIONS_OVERRIDE = 0x01 +local button_attr = capabilities.button.button + + +local mock_device1_ep1 = 1 +local mock_device1_ep2 = 2 +local mock_device1_ep3 = 3 +local mock_device1_ep4 = 4 +local mock_device1_ep5 = 5 +local mock_device1_ep6 = 6 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("light-level-switch-level-light-colorTemperature-3-button.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = mock_device1_ep1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } + }, + { + endpoint_id = mock_device1_ep2, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch + } + }, + { + endpoint_id = mock_device1_ep3, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = mock_device1_ep4, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = mock_device1_ep5, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = mock_device1_ep6, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + } +}) + +-- add device for each mock device +local CLUSTER_SUBSCRIBE_LIST ={ + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + mock_device:expect_metadata_update({ profile = "light-level-switch-level-light-colorTemperature-3-button" }) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "3-button" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "First switch component: switch capability should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, mock_device1_ep1) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, mock_device1_ep1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + } +) + +test.register_message_test( + "Second switch component: Current level reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, mock_device1_ep2, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("switch2", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + }, + } +) + +test.register_message_test( + "First button component: Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, mock_device1_ep3, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button1", button_attr.pushed({state_change = true})) --should send initial press + } +} +) + +test.register_message_test( + "Second button component: Handle single press sequence for short release-supported button", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, mock_device1_ep4, {new_position = 1} + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, mock_device1_ep4, {previous_position = 0} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) + } +} +) + +test.register_coroutine_test( + "Third button component: Handle single press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, mock_device1_ep5, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, mock_device1_ep5, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) + end +) + +test.register_message_test( + "Third switch component: Set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "colorTemperature", component = "switch3", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device1_ep6, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device1_ep6) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device1_ep6, 556) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("switch3", capabilities.colorTemperature.colorTemperature(1800)) + }, + } +) + +-- run the tests +test.run_registered_tests() From e7278a1620927833627b7a6423e65ebf326f8a7b Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 5 Nov 2024 11:48:57 -0600 Subject: [PATCH 12/17] Use first switch endpoint as main endpoint --- drivers/SmartThings/matter-switch/src/init.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c510813fad..0c8d135942 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -348,17 +348,16 @@ end local function find_default_endpoint(device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) - local all_eps = {} + table.sort(switch_eps) + table.sort(button_eps) + -- Use the first switch endpoint as the main endpoint if one is present. for _,ep in ipairs(switch_eps) do - table.insert(all_eps, ep) + if ep ~= 0 then --0 is the matter RootNode endpoint + return ep + end end for _,ep in ipairs(button_eps) do - table.insert(all_eps, ep) - end - table.sort(all_eps) - - for _, ep in ipairs(all_eps) do if ep ~= 0 then --0 is the matter RootNode endpoint return ep end From a12f7e1f8ee446aee5a42af21f7ad17216a85a61 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Fri, 8 Nov 2024 09:39:28 -0600 Subject: [PATCH 13/17] Use parent-child for additional switch endpoints --- .../SmartThings/matter-switch/src/init.lua | 89 ++++++--------- .../test_matter_multi_button_switch_mcd.lua | 101 ++++++++---------- 2 files changed, 74 insertions(+), 116 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 0c8d135942..ca39e3953e 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -471,76 +471,47 @@ local function initialize_switch(driver, device) local component_map = {} local component_map_used = false + local current_component_number = 1 local parent_child_device = false - if #switch_eps == 0 and #button_eps == 0 then - return - end - -- Since we do not support bindings at the moment, we only want to count clusters -- that have been implemented as server. This can be removed when we have -- support for bindings. local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) - if #switch_eps > 0 and #button_eps > 0 then - if #button_eps == 1 or tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - -- For switch devices, the profile components follow the naming convention "switch%d", - -- with the exception of "main" being the first component. Each component will then map - -- to the next lowest endpoint that hasn't been mapped yet. - -- Additionally, since we do not support bindings at the moment, we only want to count - -- On/Off clusters that have been implemented as server. This can be removed when we have - -- support for bindings. - local current_component_number = 1 - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - if current_component_number == 1 then - component_map["main"] = ep - else - component_map[string.format("switch%d", current_component_number)] = ep - end - current_component_number = current_component_number + 1; - end - end - current_component_number = 1 - for _, ep in ipairs(button_eps) do + + -- If button endpoints are present, use MCD for the main endpoint and all button + -- endpoints. Note that if switch endpoints are present, the first switch + -- endpoint will be considered the main endpoint. Otherwise, the first button + -- endpoint will be considered the main endpoint. + if #button_eps == 1 or tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + component_map["main"] = main_endpoint + for _, ep in ipairs(button_eps) do + if ep ~= main_endpoint then component_map[string.format("button%d", current_component_number)] = ep - current_component_number = current_component_number + 1 end - component_map_used = true + current_component_number = current_component_number + 1 end - elseif #switch_eps > 0 then - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 + component_map_used = true + end + + for _, ep in ipairs(switch_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + num_switch_server_eps = num_switch_server_eps + 1 + if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint local name = string.format("%s %d", device.label, num_switch_server_eps) - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true - end - end - end - elseif #button_eps > 0 then - local current_component_number = 2 - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - -- Configure MCD for button endpoints - for _, ep in ipairs(button_eps) do - if ep ~= main_endpoint then - component_map[string.format("button%d", current_component_number)] = ep - current_component_number = current_component_number + 1 - else - component_map["main"] = ep - end - component_map_used = true + local child_profile = assign_child_profile(device, ep) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep), + vendor_provided_label = name + } + ) + parent_child_device = true end end end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index 26645ed5da..a276ff76e7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -12,16 +12,15 @@ local OPTIONS_OVERRIDE = 0x01 local button_attr = capabilities.button.button -local mock_device1_ep1 = 1 -local mock_device1_ep2 = 2 -local mock_device1_ep3 = 3 -local mock_device1_ep4 = 4 -local mock_device1_ep5 = 5 -local mock_device1_ep6 = 6 +local mock_device_ep1 = 1 +local mock_device_ep2 = 2 +local mock_device_ep3 = 3 +local mock_device_ep4 = 4 +local mock_device_ep5 = 5 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("light-level-switch-level-light-colorTemperature-3-button.yml"), + profile = t_utils.get_profile_definition("light-level-3-button.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -37,7 +36,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = mock_device1_ep1, + endpoint_id = mock_device_ep1, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} @@ -47,17 +46,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = mock_device1_ep2, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch - } - }, - { - endpoint_id = mock_device1_ep3, + endpoint_id = mock_device_ep2, clusters = { { cluster_id = clusters.Switch.ID, @@ -70,7 +59,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = mock_device1_ep4, + endpoint_id = mock_device_ep3, clusters = { { cluster_id = clusters.Switch.ID, @@ -83,7 +72,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = mock_device1_ep5, + endpoint_id = mock_device_ep4, clusters = { { cluster_id = clusters.Switch.ID, @@ -96,7 +85,7 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = mock_device1_ep6, + endpoint_id = mock_device_ep5, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, @@ -109,6 +98,15 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local child_profile = t_utils.get_profile_definition("light-color-level.yml") +local child_data = { + profile = child_profile, + device_network_id = string.format("%s:%d", mock_device.id, mock_device_ep5), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", mock_device_ep5) +} +local mock_child = test.mock_device.build_test_child_device(child_data) + -- add device for each mock device local CLUSTER_SUBSCRIBE_LIST ={ clusters.OnOff.attributes.OnOff, @@ -136,8 +134,16 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", mock_device_ep5) + }) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - mock_device:expect_metadata_update({ profile = "light-level-switch-level-light-colorTemperature-3-button" }) + mock_device:expect_metadata_update({ profile = "light-level-3-button" }) local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "3-button" local device_info_json = dkjson.encode(device_info_copy) @@ -158,7 +164,7 @@ end test.set_test_init_function(test_init) test.register_message_test( - "First switch component: switch capability should send the appropriate commands", + "Main switch component: switch capability should send the appropriate commands", { { channel = "capability", @@ -181,7 +187,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.OnOff.server.commands.On(mock_device, mock_device1_ep1) + clusters.OnOff.server.commands.On(mock_device, mock_device_ep1) }, }, { @@ -189,7 +195,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, mock_device1_ep1, true) + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, mock_device_ep1, true) } }, { @@ -200,25 +206,6 @@ test.register_message_test( } ) -test.register_message_test( - "Second switch component: Current level reports should generate appropriate events", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, mock_device1_ep2, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("switch2", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) - }, - } -) - test.register_message_test( "First button component: Handle single press sequence, no hold", { { @@ -227,7 +214,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, mock_device1_ep3, {new_position = 1} + mock_device, mock_device_ep2, {new_position = 1} ), } }, @@ -247,7 +234,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, mock_device1_ep4, {new_position = 1} + mock_device, mock_device_ep3, {new_position = 1} ), } }, @@ -257,7 +244,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, mock_device1_ep4, {previous_position = 0} + mock_device, mock_device_ep3, {previous_position = 0} ), } }, @@ -276,7 +263,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, mock_device1_ep5, {new_position = 1} + mock_device, mock_device_ep4, {new_position = 1} ) }) test.wait_for_events() @@ -284,7 +271,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, mock_device1_ep5, {previous_position = 0} + mock_device, mock_device_ep4, {previous_position = 0} ) }) test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) @@ -292,14 +279,14 @@ test.register_coroutine_test( ) test.register_message_test( - "Third switch component: Set color temperature should send the appropriate commands", + "Switch child device: Set color temperature should send the appropriate commands", { { channel = "capability", direction = "receive", message = { - mock_device.id, - { capability = "colorTemperature", component = "switch3", command = "setColorTemperature", args = {1800} } + mock_child.id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } } }, { @@ -307,7 +294,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device1_ep6, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) } }, { @@ -315,7 +302,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device1_ep6) + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep5) } }, { @@ -323,13 +310,13 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device1_ep6, 556) + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("switch3", capabilities.colorTemperature.colorTemperature(1800)) + message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) }, } ) From 926b8b499d9d5fc939093088c49f38986e50b52e Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Mon, 11 Nov 2024 10:14:24 -0600 Subject: [PATCH 14/17] Addressing review feedback --- drivers/SmartThings/matter-switch/src/init.lua | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 436e5aef6c..ad1a80b52b 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -481,10 +481,9 @@ local function initialize_switch(driver, device) local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) - -- If button endpoints are present, use MCD for the main endpoint and all button - -- endpoints. Note that if switch endpoints are present, the first switch - -- endpoint will be considered the main endpoint. Otherwise, the first button - -- endpoint will be considered the main endpoint. + -- If a switch endpoint is present, it will be the main endpoint and therefore the + -- main component. If button endpoints are present, they will be added as + -- additional components in a MCD profile. if #button_eps == 1 or tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then component_map["main"] = main_endpoint for _, ep in ipairs(button_eps) do @@ -532,17 +531,13 @@ local function initialize_switch(driver, device) if #switch_eps > 0 and #button_eps > 0 then if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - local dimmable_light = false for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then - dimmable_light = true + profile_name = "light-level" end end end - if dimmable_light then - profile_name = "light-level" - end end if profile_name then @@ -552,7 +547,7 @@ local function initialize_switch(driver, device) else configure_buttons(device) end - elseif #switch_eps > 0 then + elseif num_switch_server_eps > 0 then -- The case where num_switch_server_eps > 0 is a workaround for devices that have a -- Light Switch device type but implement the On Off cluster as server (which is against the spec -- for this device type). By default, we do not support Light Switch device types because by spec these @@ -560,7 +555,7 @@ local function initialize_switch(driver, device) -- do not have a generic fingerprint and will join as a matter-thing. However, we have seen some devices -- claim to be Light Switch device types and still implement their clusters as server, so this is a -- workaround for those devices. - if detect_matter_thing(device) and num_switch_server_eps > 0 then + if detect_matter_thing(device) then local id = 0 for _, ep in ipairs(device.endpoints) do -- main_endpoint only supports server cluster by definition of get_endpoints() From 25cf9d3d30e4bfd062ee4b21b26303b624bc0682 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Mon, 11 Nov 2024 13:09:21 -0600 Subject: [PATCH 15/17] Addressing review comments --- .../SmartThings/matter-switch/src/init.lua | 103 +++++++++++------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index ad1a80b52b..655cf4ebe8 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -252,7 +252,7 @@ local START_BUTTON_PRESS = "__start_button_press" local TIMEOUT_THRESHOLD = 10 --arbitrary timeout local HELD_THRESHOLD = 1 -- this is the number of buttons for which we have a static profile already made -local STATIC_BUTTON_PROFILE_SUPPORTED = {2, 3, 4, 5, 6, 7, 8} +local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8} local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" @@ -341,6 +341,28 @@ local function mired_to_kelvin(value, minOrMax) end end +local function is_supported_combination_button_switch_device_type(device, endpoint_id) + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then + return true + end + end + end + end + return false +end + +local function get_first_non_zero_endpoint(endpoints) + for _,ep in ipairs(endpoints) do + if ep ~= 0 then -- 0 is the matter RootNode endpoint + return ep + end + end + return nil +end + --- find_default_endpoint helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 --- In this case the function returns the lowest endpoint value that isn't 0 @@ -352,17 +374,28 @@ local function find_default_endpoint(device) table.sort(switch_eps) table.sort(button_eps) - -- Use the first switch endpoint as the main endpoint if one is present. - for _,ep in ipairs(switch_eps) do - if ep ~= 0 then --0 is the matter RootNode endpoint - return ep - end + -- Return the first switch endpoint as the default endpoint if no button endpoints are available + if #button_eps == 0 and #switch_eps > 0 then + return get_first_non_zero_endpoint(switch_eps) end - for _,ep in ipairs(button_eps) do - if ep ~= 0 then --0 is the matter RootNode endpoint - return ep + + -- Return the first button endpoint as the default endpoint if no switch endpoints are available + if #switch_eps == 0 and #button_eps > 0 then + return get_first_non_zero_endpoint(button_eps) + end + + -- If both switch and button endpoints are present, check the device type on the main switch + -- endpoint. If it is not a supported device type, return the first button endpoint as the + -- default endpoint. + if #switch_eps > 0 and #button_eps > 0 then + local main_endpoint = get_first_non_zero_endpoint(switch_eps) + if is_supported_combination_button_switch_device_type(device, main_endpoint) then + return main_endpoint + else + return get_first_non_zero_endpoint(button_eps) end end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) return device.MATTER_DEFAULT_ENDPOINT end @@ -484,7 +517,7 @@ local function initialize_switch(driver, device) -- If a switch endpoint is present, it will be the main endpoint and therefore the -- main component. If button endpoints are present, they will be added as -- additional components in a MCD profile. - if #button_eps == 1 or tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then component_map["main"] = main_endpoint for _, ep in ipairs(button_eps) do if ep ~= main_endpoint then @@ -529,19 +562,28 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if #switch_eps > 0 and #button_eps > 0 then - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then - profile_name = "light-level" - end - end + if #button_eps > 0 and is_supported_combination_button_switch_device_type(device, main_endpoint) then + profile_name = "light-level" .. string.format("-%d-button", #button_eps) + device:try_update_metadata({profile = profile_name}) + device:set_field(DEFERRED_CONFIGURE, true) + elseif #button_eps > 0 then + local battery_support = false + if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and + #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 then + battery_support = true + end + if #button_eps > 1 and tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + if battery_support then + profile_name = string.format("%d-button-battery", #button_eps) + else + profile_name = string.format("%d-button", #button_eps) end + elseif not battery_support then + -- a battery-less button/remote (either single or will use parent/child) + profile_name = "button" end if profile_name then - profile_name = profile_name .. string.format("-%d-button", #button_eps) device:try_update_metadata({profile = profile_name}) device:set_field(DEFERRED_CONFIGURE, true) else @@ -574,29 +616,6 @@ local function initialize_switch(driver, device) device:try_update_metadata({profile = device_type_profile_map[id]}) end end - elseif #button_eps > 0 then - local battery_support = false - if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and - #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 then - battery_support = true - end - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - if battery_support then - profile_name = string.format("%d-button-battery", #button_eps) - else - profile_name = string.format("%d-button", #button_eps) - end - elseif not battery_support then - -- a battery-less button/remote (either single or will use parent/child) - profile_name = "button" - end - - if profile_name then - device:try_update_metadata({profile = profile_name}) - device:set_field(DEFERRED_CONFIGURE, true) - else - configure_buttons(device) - end end end From b94a57ba5d3669677546833346b4665b4fe79fed Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Mon, 11 Nov 2024 14:22:19 -0600 Subject: [PATCH 16/17] Add test case for unsupported switch device for MCD --- .../test_matter_multi_button_switch_mcd.lua | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index a276ff76e7..a5f2420557 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -98,6 +98,61 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_mcd_unsupported_switch_device_type = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 7, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch + } + }, + { + endpoint_id = 20, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 30, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + } +}) + local child_profile = t_utils.get_profile_definition("light-color-level.yml") local child_data = { profile = child_profile, @@ -161,6 +216,33 @@ local function test_init() test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end +local function test_init_mcd_unsupported_switch_device_type() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) + end + end + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) + mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) + + mock_device_mcd_unsupported_switch_device_type:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "switch-binary", + parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, + parent_assigned_child_key = string.format("%d", 7) + }) +end + test.set_test_init_function(test_init) test.register_message_test( @@ -321,5 +403,12 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Test MCD configuration not including switch for unsupported switch device type, create child device instead", + function() + end, + { test_init = test_init_mcd_unsupported_switch_device_type } +) + -- run the tests test.run_registered_tests() From d217740eddeacb87523f636f2254125cad217b8e Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 13 Nov 2024 10:21:32 -0600 Subject: [PATCH 17/17] Use button as component name for devices with one button and other minor changes --- .../profiles/light-level-button.yml | 2 +- .../SmartThings/matter-switch/src/init.lua | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml index d9c6b6fe46..9fc53f642b 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml @@ -16,7 +16,7 @@ components: version: 1 categories: - name: Light - - id: button1 + - id: button capabilities: - id: button version: 1 diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 655cf4ebe8..bb19b6a5fd 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -341,6 +341,9 @@ local function mired_to_kelvin(value, minOrMax) end end +--- is_supported_combination_button_switch_device_type helper function used to check +--- whether the device type for an endpoint is currently supported by a profile for +--- combination button/switch devices. local function is_supported_combination_button_switch_device_type(device, endpoint_id) for _, ep in ipairs(device.endpoints) do if ep.endpoint_id == endpoint_id then @@ -392,6 +395,7 @@ local function find_default_endpoint(device) if is_supported_combination_button_switch_device_type(device, main_endpoint) then return main_endpoint else + device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") return get_first_non_zero_endpoint(button_eps) end end @@ -521,7 +525,11 @@ local function initialize_switch(driver, device) component_map["main"] = main_endpoint for _, ep in ipairs(button_eps) do if ep ~= main_endpoint then - component_map[string.format("button%d", current_component_number)] = ep + if #button_eps == 1 then + component_map[string.format("button", current_component_number)] = ep + else + component_map[string.format("button%d", current_component_number)] = ep + end end current_component_number = current_component_number + 1 end @@ -563,7 +571,11 @@ local function initialize_switch(driver, device) end if #button_eps > 0 and is_supported_combination_button_switch_device_type(device, main_endpoint) then - profile_name = "light-level" .. string.format("-%d-button", #button_eps) + if #button_eps == 1 then + profile_name = "light-level-button" + else + profile_name = "light-level" .. string.format("-%d-button", #button_eps) + end device:try_update_metadata({profile = profile_name}) device:set_field(DEFERRED_CONFIGURE, true) elseif #button_eps > 0 then @@ -579,7 +591,7 @@ local function initialize_switch(driver, device) profile_name = string.format("%d-button", #button_eps) end elseif not battery_support then - -- a battery-less button/remote (either single or will use parent/child) + -- a battery-less button/remote profile_name = "button" end @@ -673,12 +685,8 @@ local function device_init(driver, device) id = math.max(id, dt.device_type_id) end for _, attr in pairs(device_type_attribute_map[id] or {}) do - if id == GENERIC_SWITCH_ID then - if attr == clusters.PowerSource.attributes.BatPercentRemaining then - device:add_subscribed_attribute(attr) - else - device:add_subscribed_event(attr) - end + if id == GENERIC_SWITCH_ID and attr ~= clusters.PowerSource.attributes.BatPercentRemaining then + device:add_subscribed_event(attr) else device:add_subscribed_attribute(attr) end