diff --git a/drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml b/drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml new file mode 100644 index 0000000000..bbab3c16ad --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml @@ -0,0 +1,14 @@ +name: garage-door-battery +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: GarageDoor diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 1a409f0787..63a95a06ad 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -344,6 +344,7 @@ local matter_driver_template = { switch_utils.lazy_load("sub_drivers.camera"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), + switch_utils.lazy_load_if_possible("sub_drivers.third_reality_garage_door"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") }, shared_device_thread_enabled = true, diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua new file mode 100644 index 0000000000..2e1b734406 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local utils = require "switch_utils.utils" + +return function(opts, driver, device) + if utils.get_product_override_field(device, "is_third_reality_garage_door") then + return true, require("sub_drivers.third_reality_garage_door") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua new file mode 100644 index 0000000000..20834fb793 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua @@ -0,0 +1,91 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +------------------------------------------------------------------------------------- +-- Third Reality Garage Door Opener specifics +-- +-- This device uses the OnOff cluster to control the door: +-- OnOff = true -> door open +-- OnOff = false -> door closed +-- Commands are mapped from doorControl capability to OnOff cluster commands. +------------------------------------------------------------------------------------- + +local function device_init(driver, device) + -- Force a subscription to the OnOff cluster, since doorControl does not explicitly map to it in the default driver. + device:add_subscribed_attribute(clusters.OnOff.attributes.OnOff) + device:add_subscribed_attribute(clusters.PowerSource.attributes.BatPercentRemaining) + device:subscribe() +end + +local function match_profile(driver, device) + device:try_update_metadata({profile = "garage-door-battery"}) +end + +-- Prevent any of the main driver's logic from running +local function device_added(driver, device) end + +-- Prevent any of the main driver's logic from running +local function info_changed(driver, device, event, args) end + +local function do_configure(driver, device) + match_profile(driver, device) +end + +local function driver_switched(driver, device) + match_profile(driver, device) +end + + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.doorControl.door.open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.doorControl.door.closed()) + end +end + +local function handle_door_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:emit_event_for_endpoint(endpoint_id, capabilities.doorControl.door.opening()) + device:send(clusters.OnOff.server.commands.On(device, endpoint_id)) +end + +local function handle_door_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:emit_event_for_endpoint(endpoint_id, capabilities.doorControl.door.closing()) + device:send(clusters.OnOff.server.commands.Off(device, endpoint_id)) +end + +local third_reality_garage_door_handler = { + NAME = "ThirdReality Garage Door Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + driverSwitched = driver_switched, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + }, + }, + capability_handlers = { + [capabilities.doorControl.ID] = { + [capabilities.doorControl.commands.open.NAME] = handle_door_open, + [capabilities.doorControl.commands.close.NAME] = handle_door_close, + }, + }, + supported_capabilities = { + capabilities.doorControl, + capabilities.battery, + }, + can_handle = require("sub_drivers.third_reality_garage_door.can_handle") +} + +return third_reality_garage_door_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua index f3f5342989..f68e568639 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua @@ -1,13 +1,10 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local device_lib = require "st.device" +local utils = require "switch_utils.utils" return function(opts, driver, device) - local THIRD_REALITY_MK1_FINGERPRINT = { vendor_id = 0x1407, product_id = 0x1388 } - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == THIRD_REALITY_MK1_FINGERPRINT.vendor_id and - device.manufacturer_info.product_id == THIRD_REALITY_MK1_FINGERPRINT.product_id then + if utils.get_product_override_field(device, "is_third_reality_mk1") then return true, require("sub_drivers.third_reality_mk1") end return false diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index cb93b6331a..f31fb6e081 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -112,6 +112,10 @@ SwitchFields.vendor_overrides = { [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, + [0x1407] = { -- THIRD_REALITY_MANUFACTURER_ID + [0x1098] = { is_third_reality_garage_door = true }, + [0x1388] = { is_third_reality_mk1 = true}, + }, } SwitchFields.switch_category_vendor_overrides = { diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua deleted file mode 100644 index 82a8d4d641..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ /dev/null @@ -1,242 +0,0 @@ --- Copyright © 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local dkjson = require "dkjson" -local t_utils = require "integration_test.utils" -local test = require "integration_test" -local utils = require "st.utils" - -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("12-button-keyboard.yml"), - manufacturer_info = {vendor_id = 0x1407, product_id = 0x1388}, - 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 = 1, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 2, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 3, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 4, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 5, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 6, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 7, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 8, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 9, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 10, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 11, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 12, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - } - } -}) - -local function configure_buttons() - for key = 1, 12 do - local component = "F" .. key - if key == 1 then component = "main" end - test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.button.pushed({state_change = false}))) - end -end - -local function test_init() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device) - local cluster_subscribe_list = { - clusters.Switch.events.InitialPress - } - 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.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "12-button-keyboard" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - configure_buttons() - - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "12-buttons-keyboard" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - configure_buttons() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Handle single press sequence", - function() - for key = 1, 12 do - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report(mock_device, key, {new_position = 1}) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message(key == 1 and "main" or "F" .. key, capabilities.button.button.pushed({state_change = true})) - ) - end - end, - { - min_api_version = 17 - } -) - --- run the tests -test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua new file mode 100644 index 0000000000..ce7c7a5ddc --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua @@ -0,0 +1,533 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local dkjson = require "dkjson" +local utils = require "st.utils" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("garage-door-battery.yml"), + manufacturer_info = {vendor_id = 0x1407, product_id = 0x1098}, + 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 = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.Feature.BATTERY, + }, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- On/Off Plug-in Unit + } + } + } +}) + +-- The subscribe list matches the profile capabilities: +-- doorControl -> OnOff.attributes.OnOff +-- battery -> PowerSource.attributes.BatPercentRemaining +-- The subdriver overrides device_init, so the main driver's extend_device("subscribe", ...) +-- is not called; the default device:subscribe() is used, which does not add AttributeList. +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.PowerSource.attributes.BatPercentRemaining, +} + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + + 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 + + -- added lifecycle: subdriver overrides device_added to a no-op so no subscribe here + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + + -- init lifecycle: device_init subscribes + test.socket.device_lifecycle:__queue_receive({mock_device.id, "init"}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- doConfigure: sets battery support field and updates profile metadata + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({profile = "garage-door-battery"}) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) +end + +test.set_test_init_function(test_init) + +-- ── Attribute handler tests ────────────────────────────────────────────────── + +test.register_message_test( + "OnOff true should emit door.open", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.open()) + } + } +) + +test.register_message_test( + "OnOff false should emit door.closed", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.closed()) + } + } +) + +-- ── Capability command tests ───────────────────────────────────────────────── + +test.register_message_test( + "doorControl open command should emit opening then send OnOff.On", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "doorControl", component = "main", command = "open", args = {}} + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.opening()) + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 1) + } + } + } +) + +test.register_message_test( + "doorControl close command should emit closing then send OnOff.Off", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "doorControl", component = "main", command = "close", args = {}} + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.closing()) + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 1) + } + } + } +) + +-- ── Battery attribute tests ─────────────────────────────────────────────────── + +test.register_message_test( + "BatPercentRemaining report should emit correct battery percentage", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 1, 200) + } + }, + { + channel = "capability", + direction = "send", + -- BatPercentRemaining is in units of 0.5%, so 200 = 100% + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_message_test( + "BatPercentRemaining report of 150 should emit 75% battery", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 1, 150) + } + }, + { + channel = "capability", + direction = "send", + -- 150 * 0.5 = 75% + message = mock_device:generate_test_message("main", capabilities.battery.battery(75)) + } + } +) + +-- ── Profile / driverSwitched tests ──────────────────────────────────────────── + +test.register_coroutine_test( + "doConfigure should set garage-door-battery profile", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({profile = "garage-door-battery"}) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) + end, + {min_api_version = 17} +) + +test.register_coroutine_test( + "driverSwitched should restore garage-door-battery profile", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "driverSwitched"}) + mock_device:expect_metadata_update({profile = "garage-door-battery"}) + end, + {min_api_version = 17} +) + + +local mock_device_misprofiled = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-binary.yml"), + manufacturer_info = {vendor_id = 0x1407, product_id = 0x1098}, + 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 = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.Feature.BATTERY, + }, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- On/Off Plug-in Unit + } + } + } +}) + +test.register_coroutine_test( + "doConfigure should correct profile if misprofiled", + function() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_misprofiled) + + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_misprofiled) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_misprofiled)) end + end + + -- added lifecycle: subdriver overrides device_added to a no-op so no subscribe here + test.socket.device_lifecycle:__queue_receive({mock_device_misprofiled.id, "added"}) + + -- init lifecycle: device_init subscribes + test.socket.device_lifecycle:__queue_receive({mock_device_misprofiled.id, "init"}) + test.socket.matter:__expect_send({mock_device_misprofiled.id, subscribe_request}) + + -- doConfigure: sets battery support field and updates profile metadata + test.socket.device_lifecycle:__queue_receive({mock_device_misprofiled.id, "doConfigure"}) + mock_device_misprofiled:expect_metadata_update({profile = "garage-door-battery"}) + mock_device_misprofiled:expect_metadata_update({provisioning_state = "PROVISIONED"}) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_misprofiled) end, + } +) + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("12-button-keyboard.yml"), + manufacturer_info = {vendor_id = 0x1407, product_id = 0x1388}, + 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 = 1, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 2, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 4, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 5, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 6, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 7, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } + } +}) + +local function configure_buttons() + for key = 1, 12 do + local component = "F" .. key + if key == 1 then component = "main" end + test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.button.pushed({state_change = false}))) + end +end + +local function test_init_mk1() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + local cluster_subscribe_list = { + clusters.Switch.events.InitialPress + } + 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.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "12-button-keyboard" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + configure_buttons() + + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "12-buttons-keyboard" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + configure_buttons() + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +test.register_coroutine_test( + "Handle single press sequence", + function() + for key = 1, 12 do + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report(mock_device, key, {new_position = 1}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(key == 1 and "main" or "F" .. key, capabilities.button.button.pushed({state_change = true})) + ) + end + end, + { + test_init = test_init_mk1, + min_api_version = 17 + } +) + +-- run the tests +test.run_registered_tests()