From b0f3dcaa50684b99ab0a1e77c3ebca86abb5a0a3 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 3 Nov 2025 17:33:54 -0600 Subject: [PATCH 1/2] move Bosch Contact Button support to Matter Switch --- .../matter-sensor/fingerprints.yml | 6 - .../SmartThings/matter-sensor/src/init.lua | 1 - .../sub_drivers/bosch_button_contact/init.lua | 151 ---------- .../test/test_matter_bosch_button_contact.lua | 278 ------------------ .../matter-switch/fingerprints.yml | 5 + .../profiles/contact-button-battery.yml | 16 + .../SmartThings/matter-switch/src/init.lua | 6 + .../switch_handlers/attribute_handlers.lua | 15 + .../matter-switch/src/switch_utils/fields.lua | 15 +- .../src/test/test_bosch_button_contact.lua | 267 +++++++++++++++++ 10 files changed, 322 insertions(+), 438 deletions(-) delete mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua create mode 100644 drivers/SmartThings/matter-switch/profiles/contact-button-battery.yml create mode 100644 drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index af05f7bafe..924f308de1 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -10,12 +10,6 @@ matterManufacturer: vendorId: 0x115F productId: 0x2003 deviceProfileName: motion-illuminance-battery - #Bosch - - id: 4617/12309 - deviceLabel: "Door/window contact II [M]" - vendorId: 0x1209 - productId: 0x3015 - deviceProfileName: contact-button-battery #Elko - id: "5170/4098" deviceLabel: RFWD-100/MT diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 4f6b204aea..73051de4c8 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -298,7 +298,6 @@ local matter_driver_template = { sub_drivers = { require("sub_drivers.air_quality_sensor"), require("sub_drivers.smoke_co_alarm"), - require("sub_drivers.bosch_button_contact") } } diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua deleted file mode 100644 index 9b4635d87c..0000000000 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua +++ /dev/null @@ -1,151 +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 device_lib = require "st.device" -local lua_socket = require "socket" -local log = require "log" - -local START_BUTTON_PRESS = "__start_button_press" - -local BOSCH_VENDOR_ID = 0x1209 -local BOSCH_PRODUCT_ID = 0x3015 - -local function is_bosch_button_contact(opts, driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == BOSCH_VENDOR_ID and - device.manufacturer_info.product_id == BOSCH_PRODUCT_ID then - return true - end - return false -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function init_press(device, endpoint) - set_field_for_endpoint(device, START_BUTTON_PRESS, endpoint, lua_socket.gettime(), {persist = false}) -end - --- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a --- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because --- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used --- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. -local IGNORE_NEXT_MPC = "__ignore_next_mpc" --- These are essentially storing the supported features of a given endpoint --- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint -local EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side -local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete -local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) - -local function initial_press_event_handler(driver, device, ib, response) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event - -- or else we would potentially not create the expected button capability event - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) - elseif get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) - elseif get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then - -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time - init_press(device, ib.endpoint_id) - end -end - -local function long_press_event_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true) - end -end - ---helper function to create list of multi press values -local function create_multi_press_values_list(size, supportsHeld) - local list = {"pushed", "double"} - if supportsHeld then table.insert(list, "held") end - -- add multi press values of 3 or greater to the list - for i=3, size do - table.insert(list, string.format("pushed_%dx", i)) - end - return list -end - -local function tbl_contains(array, value) - for _, element in ipairs(array) do - if element == value then - return true - end - end - return false -end - -local function device_init (driver, device) - device:subscribe() - device:send(clusters.Switch.attributes.MultiPressMax:read(device)) -end - -local function max_press_handler(driver, device, ib, response) - local max = ib.data.value or 1 --get max number of presses - device.log.debug("Device supports "..max.." presses") - -- capability only supports up to 6 presses - if max > 6 then - log.info("Device supports more than 6 presses") - max = 6 - end - local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) - local supportsHeld = tbl_contains(MSL, ib.endpoint_id) - local values = create_multi_press_values_list(max, supportsHeld) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) -end - -local function multi_press_complete_event_handler(driver, device, ib, response) - -- in the case of multiple button presses - -- emit number of times, multiple presses have been completed - if ib.data and not get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) then - local press_value = ib.data.elements.total_number_of_presses_counted.value - --capability only supports up to 6 presses - if press_value < 7 then - local button_event = capabilities.button.button.pushed({state_change = true}) - if press_value == 2 then - button_event = capabilities.button.button.double({state_change = true}) - elseif press_value > 2 then - button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) - end - - device:emit_event_for_endpoint(ib.endpoint_id, button_event) - else - log.info(string.format("Number of presses (%d) not supported by capability", press_value)) - end - end - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) -end - -local Bosch_Button_Contact_Sensor = { - NAME = "Bosch_Button_Contact_Sensor", - lifecycle_handlers = { - init = device_init - }, - matter_handlers = { - attr = { - [clusters.Switch.ID] = { - [clusters.Switch.attributes.MultiPressMax.ID] = max_press_handler - } - }, - event = { - [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler, - [clusters.Switch.events.LongPress.ID] = long_press_event_handler, - [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler - } - }, - }, - can_handle = is_bosch_button_contact, -} - -return Bosch_Button_Contact_Sensor diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua deleted file mode 100644 index 1ca2add2ff..0000000000 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua +++ /dev/null @@ -1,278 +0,0 @@ --- Copyright © 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 --- package.path = package.path .. ";./?lua" --- package.loaded["path"] = dofile("mock_path.lua") -local test = require "integration_test" -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local clusters = require "st.matter.generated.zap_clusters" -local button_attr = capabilities.button.button - - -local mock_device = test.mock_device.build_test_matter_device( - { - label = "Bosch_Button_Contact_Sensor", - profile = t_utils.get_profile_definition("contact-button-battery.yml"), - manufacturer_info = { - vendor_id = 0x1209, - product_id = 0x3015 - }, - endpoints = { - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY}, - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER"} - }, - - }, - { - endpoint_id = 2, - 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", - }, - } - } - } - }) - -local CLUSTER_SUBSCRIBE_LIST = { - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.MultiPressComplete, - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.BooleanState.attributes.StateValue -} - -local function test_init() - 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:set_field("__initial_press_only_2", true, {persist = true}) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device)}) -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, 2, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - } - } -) - -test.register_message_test( - "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 2, {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 - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 2, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.held({state_change = true})) - } - } -) - -test.register_message_test( - "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 2, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 2, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 2, {previous_position = 1} - ) - } - }, - } -) - -test.register_message_test( - "Receiving a max press attribute of 2 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.attributes.MultiPressMax:build_test_report_data( - mock_device, 1, 2 - ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", - capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) - }, - } -) - -test.register_message_test( - "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 2, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 2, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.button.button.double({state_change = true})) - }, - - } -) - -test.register_message_test( - "Handle received BatPercentRemaining from device.", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( - mock_device, 1, 150 - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message( - "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), - }, - } -) - -test.register_message_test( - "Boolean state reports should generate correct messages", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device, 1, false) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device, 1, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) - } - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 6030dac5d8..175d650feb 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -137,6 +137,11 @@ matterManufacturer: vendorId: 0x1209 productId: 0x3016 deviceProfileName: plug-power-energy-powerConsumption + - id: 4617/12309 + deviceLabel: "Door/window contact II [M]" + vendorId: 0x1209 + productId: 0x3015 + deviceProfileName: contact-button-battery #Chengdu - id: "5218/8197" deviceLabel: Magic Cube DS001 diff --git a/drivers/SmartThings/matter-switch/profiles/contact-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/contact-button-battery.yml new file mode 100644 index 0000000000..f4e1d1ddae --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/contact-button-battery.yml @@ -0,0 +1,16 @@ +name: contact-button-battery +components: +- id: main + capabilities: + - id: contactSensor + version: 1 + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 2302ffc4dc..cf3f42fdba 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -125,6 +125,9 @@ local matter_driver_template = { }, matter_handlers = { attr = { + [clusters.BooleanState.ID] = { + [clusters.BooleanState.attributes.StateValue.ID] = attribute_handlers.boolean_state_value_handler + }, [clusters.ColorControl.ID] = { [clusters.ColorControl.attributes.ColorCapabilities.ID] = attribute_handlers.color_capabilities_handler, [clusters.ColorControl.attributes.ColorMode.ID] = attribute_handlers.color_mode_handler, @@ -194,6 +197,9 @@ local matter_driver_template = { fallback = switch_utils.matter_handler, }, subscribed_attributes = { + [capabilities.contactSensor.ID] = { + clusters.BooleanState.attributes.StateValue + }, [capabilities.battery.ID] = { clusters.PowerSource.attributes.BatPercentRemaining, }, diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 58310ef56b..8718f7307a 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -351,6 +351,8 @@ function AttributeHandlers.power_source_attribute_list_handler(driver, device, i if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then profile_name = profile_name .. "-temperature-humidity" + elseif switch_utils.get_product_override_field(device, "is_bosch_contact_button") then + profile_name = "contact-" .. profile_name end device:try_update_metadata({ profile = profile_name }) end @@ -492,4 +494,17 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) end + +-- [[ BOOLEAN STATE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.boolean_state_value_handler(driver, device, ib, response) + local ep_info = switch_utils.get_endpoint_info(device, ib.endpoint_id) + local ep_device_type = ep_info.device_types[1].device_type_id + if fields.BOOLEAN_STATE_TO_CAPABILITY_STATE[ep_device_type] then + device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_STATE_TO_CAPABILITY_STATE[ep_device_type][ib.data.value]) + else + device.log.error(string.format("Unsupported device type %d found on endpoint %d.", ep_device_type, ib.endpoint_id)) + end +end + return AttributeHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index f66773d77f..0acf96de4c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -42,6 +42,7 @@ SwitchFields.DEVICE_TYPE_ID = { MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, + CONTACT_SENSOR = 0x0015, LIGHT = { ON_OFF = 0x0100, DIMMABLE = 0x0101, @@ -113,7 +114,10 @@ SwitchFields.vendor_overrides = { [0x1006] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) [0x100A] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile - } + }, + [0x1209]= { -- BOSCH_MANUFACTURER_ID + [0x3015] = { is_bosch_contact_button = true }, -- Bosch Contact Button, requires unique profile + }, } SwitchFields.switch_category_vendor_overrides = { @@ -274,4 +278,11 @@ SwitchFields.device_type_attribute_map = { } } -return SwitchFields \ No newline at end of file +SwitchFields.BOOLEAN_STATE_TO_CAPABILITY_STATE = { + [SwitchFields.DEVICE_TYPE_ID.CONTACT_SENSOR] = { + [true] = capabilities.contactSensor.contact.closed(), + [false] = capabilities.contactSensor.contact.open(), + } +} + +return SwitchFields diff --git a/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua b/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua new file mode 100644 index 0000000000..d5cb8d64cb --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua @@ -0,0 +1,267 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.generated.zap_clusters" +local button_attr = capabilities.button.button +local uint32 = require "st.matter.data_types.Uint32" + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Bosch_Button_Contact_Sensor", + profile = t_utils.get_profile_definition("contact-button-battery.yml"), + manufacturer_info = { + vendor_id = 0x1209, + product_id = 0x3015 + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY}, + {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0015, device_type_revision = 1} -- CONTACT SENSOR + } + }, + { + endpoint_id = 2, + 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 CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.BooleanState.attributes.StateValue +} + +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 + + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 2)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed())) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Ensure doConfigure and the following handling works as expected", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data(mock_device, 2, 2) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double", "held"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 6, {uint32(0x0C)})}) + mock_device:expect_metadata_update({ profile = "contact-button-battery" }) + end +) + + +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, 2, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 2, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 2, {new_position = 0, total_number_of_presses_counted = 1, previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = true}))) + end +) + +test.register_message_test( + "Handle release after long press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 2, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 2, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.held({state_change=true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 2, {previous_position = 1} + ) + } + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of 2 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 1, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Handle double press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 2, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 2, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.double({state_change=true})) + }, + } +) + +test.register_message_test( + "Handle received BatPercentRemaining from device.", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device, 1, 150 + ), + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message( + "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) + ), + }, + } +) + +test.register_message_test( + "Boolean state reports should generate correct messages", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.run_registered_tests() From ad38bd97241159673ba85cc2d89afca9ae7d887c Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 10 Nov 2025 13:10:30 -0600 Subject: [PATCH 2/2] remove added whitespace --- .../matter-switch/src/test/test_bosch_button_contact.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua b/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua index d5cb8d64cb..996cdd65b0 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_bosch_button_contact.lua @@ -88,7 +88,7 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double", "held"}, {visibility = {displayed = false}})) ) test.socket.matter:__queue_receive({ - mock_device.id, + mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 6, {uint32(0x0C)})}) mock_device:expect_metadata_update({ profile = "contact-button-battery" }) end