diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 82c5dd875f..84e61f413b 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2486,6 +2486,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-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..9fc53f642b --- /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: button + 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 f0d706354a..bb19b6a5fd 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 } } @@ -245,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" @@ -334,29 +341,65 @@ 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 + 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 --- 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.SwitchFeature.MOMENTARY_SWITCH}) - local all_eps = {} + table.sort(switch_eps) + table.sort(button_eps) - for _,ep in ipairs(switch_eps) do - table.insert(all_eps, ep) + -- 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 - table.insert(all_eps, 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 - table.sort(all_eps) - for _, ep in ipairs(all_eps) do - if ep ~= 0 then --0 is the matter RootNode endpoint - return ep + -- 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 + 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 + 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 @@ -465,51 +508,51 @@ local function initialize_switch(driver, device) local profile_name = nil local component_map = {} - local current_component_number = 2 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 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 + + -- 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 tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + component_map["main"] = main_endpoint 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 + if ep ~= main_endpoint then + if #button_eps == 1 then + component_map[string.format("button", current_component_number)] = ep else - component_map["main"] = ep + component_map[string.format("button%d", current_component_number)] = ep end - component_map_used = true + end + current_component_number = current_component_number + 1 + end + 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) + 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 @@ -527,7 +570,38 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if num_switch_server_eps > 0 then + if #button_eps > 0 and is_supported_combination_button_switch_device_type(device, main_endpoint) then + 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 + 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 + 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 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 @@ -554,29 +628,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 @@ -585,7 +636,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) @@ -634,7 +685,11 @@ 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 and attr ~= clusters.PowerSource.attributes.BatPercentRemaining then + device:add_subscribed_event(attr) + else + device:add_subscribed_attribute(attr) + end 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 new file mode 100644 index 0000000000..a5f2420557 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -0,0 +1,414 @@ +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_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-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_device_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_device_ep2, + 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_device_ep3, + 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_device_ep4, + 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_device_ep5, + 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 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, + 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, + 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.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-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 + +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( + "Main 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_device_ep1) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, mock_device_ep1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + } +) + +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_device_ep2, {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_device_ep3, {new_position = 1} + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, mock_device_ep3, {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_device_ep4, {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_device_ep4, {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( + "Switch child device: Set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_child.id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 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_device_ep5) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) + } + }, + { + channel = "capability", + direction = "send", + message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + }, + } +) + +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()