diff --git a/drivers/SmartThings/matter-button/fingerprints.yml b/drivers/SmartThings/matter-button/fingerprints.yml deleted file mode 100644 index 7b42234604..0000000000 --- a/drivers/SmartThings/matter-button/fingerprints.yml +++ /dev/null @@ -1,13 +0,0 @@ -matterGeneric: - - id: "button" - deviceLabel: Matter Button - deviceTypes: - - id: 0x000F - deviceProfileName: button-battery # err on the side of buttons having batteries, it'll get fixed in the driver -matterManufacturer: -#TUO - - id: "5150/1" - deviceLabel: "TUO Smart Button" - vendorId: 0x141E - productId: 0x0001 - deviceProfileName: "button-battery" diff --git a/drivers/SmartThings/matter-button/src/init.lua b/drivers/SmartThings/matter-button/src/init.lua index 88379c765c..72adc7c0c7 100644 --- a/drivers/SmartThings/matter-button/src/init.lua +++ b/drivers/SmartThings/matter-button/src/init.lua @@ -1,3 +1,10 @@ +-- As of July 2024, the matter-button driver has been deprecated in favor of being integrated into the matter-switch +-- driver. All functionality and test cases present in this driver were carried over into matter-switch. Therefore, +-- we are no longer accepting bug fixes or feature enhancements such as fingerprint adds to this driver. +-- Note that this change won't affect devices using the button driver unless they are re-onboarded, in which case +-- they would onboard to the switch driver, as all button fingerprints were removed from this driver and moved to +-- the switch driver. + local capabilities = require "st.capabilities" local log = require "log" local clusters = require "st.matter.generated.zap_clusters" diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index aa674b74b7..5d7fa1d7ce 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2204,6 +2204,12 @@ matterManufacturer: vendorId: 0x139C productId: 0xAB01 deviceProfileName: switch-binary +#TUO + - id: "5150/1" + deviceLabel: "TUO Smart Button" + vendorId: 0x141E + productId: 0x0001 + deviceProfileName: "button-battery" #Bridge devices need manufacturer specific fingerprints until @@ -2320,6 +2326,11 @@ matterGeneric: deviceTypes: - id: 0x010B # Dimmable Plug-in Unit deviceProfileName: plug-level + - id: "button" + deviceLabel: Matter Button + deviceTypes: + - id: 0x000F + deviceProfileName: button-battery # err on the side of buttons having batteries, it'll get fixed in the driver matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml new file mode 100644 index 0000000000..30dd1e6fc7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/2-button-battery-switch.yml @@ -0,0 +1,26 @@ +name: 2-button-battery-switch +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: switch + capabilities: + - id: switch + version: 1 + categories: + - name: Switch diff --git a/drivers/SmartThings/matter-switch/profiles/2-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/2-button-battery.yml new file mode 100644 index 0000000000..e8340b26cc --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/2-button-battery.yml @@ -0,0 +1,20 @@ +name: 2-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/2-button.yml b/drivers/SmartThings/matter-switch/profiles/2-button.yml new file mode 100644 index 0000000000..1faf2dfbed --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/2-button.yml @@ -0,0 +1,18 @@ +name: 2-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/3-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/3-button-battery.yml new file mode 100644 index 0000000000..e73104374e --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/3-button-battery.yml @@ -0,0 +1,26 @@ +name: 3-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/3-button.yml b/drivers/SmartThings/matter-switch/profiles/3-button.yml new file mode 100644 index 0000000000..e17b159ae0 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/3-button.yml @@ -0,0 +1,24 @@ +name: 3-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + 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/4-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/4-button-battery.yml new file mode 100644 index 0000000000..000de4edf2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/4-button-battery.yml @@ -0,0 +1,32 @@ +name: 4-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: 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/4-button.yml b/drivers/SmartThings/matter-switch/profiles/4-button.yml new file mode 100644 index 0000000000..d2fb7b4410 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/4-button.yml @@ -0,0 +1,30 @@ +name: 4-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + 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/5-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/5-button-battery.yml new file mode 100644 index 0000000000..fe8efd9053 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/5-button-battery.yml @@ -0,0 +1,38 @@ +name: 5-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: 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/5-button.yml b/drivers/SmartThings/matter-switch/profiles/5-button.yml new file mode 100644 index 0000000000..0ded733b29 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/5-button.yml @@ -0,0 +1,36 @@ +name: 5-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + 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/6-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/6-button-battery.yml new file mode 100644 index 0000000000..8ec3642e81 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/6-button-battery.yml @@ -0,0 +1,44 @@ +name: 6-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: 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/6-button.yml b/drivers/SmartThings/matter-switch/profiles/6-button.yml new file mode 100644 index 0000000000..248ddd2bcb --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/6-button.yml @@ -0,0 +1,42 @@ +name: 6-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + 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/7-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/7-button-battery.yml new file mode 100644 index 0000000000..51c08463f0 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/7-button-battery.yml @@ -0,0 +1,50 @@ +name: 7-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: 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/7-button.yml b/drivers/SmartThings/matter-switch/profiles/7-button.yml new file mode 100644 index 0000000000..c9e3fc008d --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/7-button.yml @@ -0,0 +1,48 @@ +name: 7-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + 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/8-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/8-button-battery.yml new file mode 100644 index 0000000000..ec9f1a46cc --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/8-button-battery.yml @@ -0,0 +1,57 @@ +name: 8-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: 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/8-button.yml b/drivers/SmartThings/matter-switch/profiles/8-button.yml new file mode 100644 index 0000000000..5fc93a51bc --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/8-button.yml @@ -0,0 +1,55 @@ +name: 8-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + 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/button-battery.yml b/drivers/SmartThings/matter-switch/profiles/button-battery.yml new file mode 100644 index 0000000000..b614393f1e --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/button-battery.yml @@ -0,0 +1,14 @@ +name: button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button diff --git a/drivers/SmartThings/matter-switch/profiles/button.yml b/drivers/SmartThings/matter-switch/profiles/button.yml new file mode 100644 index 0000000000..6c6bced1c2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/button.yml @@ -0,0 +1,12 @@ +name: button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c588c3dbef..aaf334bdec 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -16,6 +16,7 @@ local capabilities = require "st.capabilities" local log = require "log" local clusters = require "st.matter.clusters" local MatterDriver = require "st.matter.driver" +local lua_socket = require "socket" local utils = require "st.utils" local device_lib = require "st.device" @@ -32,12 +33,18 @@ local COLOR_TEMPERATURE_MIRED_MIN = MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPE local SWITCH_LEVEL_LIGHTING_MIN = 1 local SWITCH_INITIALIZED = "__switch_intialized" --- COMPONENT_TO_ENDPOINT_MAP is here only to perserve the endpoint mapping for +-- COMPONENT_TO_ENDPOINT_MAP is here only to preserve the endpoint mapping for -- devices that were joined to this driver as MCD devices before the transition -- to join all matter-switch devices as parent-child. This value will only exist -- in the device table for devices that joined prior to this transition, and it -- will not be set for new devices. local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +-- COMPONENT_TO_ENDPOINT_MAP_BUTTON is for devices with button endpoints, to +-- preserve the MCD functionality for button devices from the matter-button +-- driver after it was merged into the matter-switch driver. Note that devices +-- containing both button endpoints and switch endpoints will use this field +-- rather than COMPONENT_TO_ENDPOINT_MAP. +local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button" local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" local COLOR_TEMP_BOUND_RECEIVED = "__colorTemp_bound_received" local COLOR_TEMP_MIN = "__color_temp_min" @@ -55,6 +62,7 @@ local DIMMABLE_PLUG_DEVICE_TYPE_ID = 0x010B local ON_OFF_SWITCH_ID = 0x0103 local ON_OFF_DIMMER_SWITCH_ID = 0x0104 local ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105 +local GENERIC_SWITCH_ID = 0x000F local device_type_profile_map = { [ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary", [DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level", @@ -65,9 +73,123 @@ local device_type_profile_map = { [ON_OFF_SWITCH_ID] = "switch-binary", [ON_OFF_DIMMER_SWITCH_ID] = "switch-level", [ON_OFF_COLOR_DIMMER_SWITCH_ID] = "switch-color-level", + [GENERIC_SWITCH_ID] = "button" +} + +local device_type_attribute_map = { + [ON_OFF_LIGHT_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff + }, + [DIMMABLE_LIGHT_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + }, + [COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = { + 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 + }, + [EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = { + 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 + }, + [ON_OFF_PLUG_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff + }, + [DIMMABLE_PLUG_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + }, + [ON_OFF_SWITCH_ID] = { + clusters.OnOff.attributes.OnOff + }, + [ON_OFF_DIMMER_SWITCH_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + }, + [ON_OFF_COLOR_DIMMER_SWITCH_ID] = { + 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 + }, + [GENERIC_SWITCH_ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete + } } local detect_matter_thing +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_PROFILE_SUPPORTED = {2, 3, 4, 5, 6, 7, 8} + +local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" + +-- 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 HUE_MANUFACTURER_ID = 0x100B + +--helper function to create list of multi press values +local function create_multi_list(size, supportsHeld) + local list = {"pushed", "double"} + if supportsHeld then table.insert(list, "held") end + 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 get_field_for_endpoint(device, field, endpoint) return device:get_field(string.format("%s_%d", field, endpoint)) end @@ -76,6 +198,23 @@ local function set_field_for_endpoint(device, field, endpoint, value, additional 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 + +local function emulate_held_event(device, ep) + local now = lua_socket.gettime() + local press_init = get_field_for_endpoint(device, START_BUTTON_PRESS, ep) or now -- if we don't have an init time, assume instant release + if (now - press_init) < TIMEOUT_THRESHOLD then + if (now - press_init) > HELD_THRESHOLD then + device:emit_event_for_endpoint(ep, capabilities.button.button.held({state_change = true})) + else + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = true})) + end + end + set_field_for_endpoint(device, START_BUTTON_PRESS, ep, nil, {persist = false}) +end + local function convert_huesat_st_to_matter(val) return math.floor((val * 0xFE) / 100.0 + 0.5) end @@ -96,9 +235,19 @@ end --- and supports the OnOff cluster. This is done to bypass the --- BRIDGED_NODE_DEVICE_TYPE on bridged devices local function find_default_endpoint(device, component) - local eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(eps) - for _, v in ipairs(eps) do + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + local all_eps = {} + + for _,v in ipairs(switch_eps) do + table.insert(all_eps, v) + end + for _,v in ipairs(button_eps) do + table.insert(all_eps, v) + end + table.sort(all_eps) + + for _, v in ipairs(all_eps) do if v ~= 0 then --0 is the matter RootNode endpoint return v end @@ -121,78 +270,213 @@ local function assign_child_profile(device, child_ep) id = math.max(id, dt.device_type_id) end profile = device_type_profile_map[id] + for _, attr in pairs(device_type_attribute_map[id]) do + if id == GENERIC_SWITCH_ID then + if attr == clusters.PowerSource.attributes.BatPercentRemaining then + device:add_subscribed_attribute(attr) + else + device:add_subscribed_event(attr) + end + else + device:add_subscribed_attribute(attr) + end + end end end -- default to "switch-binary" if no profile is found return profile or "switch-binary" end +local function configure_buttons(device) + if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then + local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + local MSR = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH_RELEASE}) + device.log.debug(#MSR.." momentary switch release endpoints") + local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH_LONG_PRESS}) + device.log.debug(#MSL.." momentary switch long press endpoints") + local MSM = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH_MULTI_PRESS}) + device.log.debug(#MSM.." momentary switch multi press endpoints") + for _, ep in ipairs(MS) do + local supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) + -- this ordering is important, as MSL & MSM devices must also support MSR + if tbl_contains(MSM, ep) then + -- ask the device to tell us its max number of presses + device.log.debug("sending multi press max read") + device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) + set_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) + supportedButtonValues_event = nil -- deferred until max press handler + elseif tbl_contains(MSL, ep) then + device.log.debug("configuring for long press device") + elseif tbl_contains(MSR, ep) then + device.log.debug("configuring for emulated held") + set_field_for_endpoint(device, EMULATE_HELD, ep, true, {persist = true}) + else -- device only supports momentary switch, no release events + device.log.debug("configuring for press event only") + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) + set_field_for_endpoint(device, INITIAL_PRESS_ONLY, ep, true, {persist = true}) + end + + if supportedButtonValues_event then + device:emit_event_for_endpoint(ep, supportedButtonValues_event) + end + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) + end + end +end + +local function find_child(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) +end + local function initialize_switch(driver, device) local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - -- Since we do not support bindings at the moment, we only want to count On/Off - -- clusters that have been implemented as server. This can be removed when we have - -- support for bindings. - local num_server_eps = 0 - local main_endpoint = find_default_endpoint(device) - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_server_eps = num_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_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 - } - ) + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + local all_eps = {} + + local new_profile = nil + + local component_map = {} + local current_component_number = 2 + local component_map_used = false + local parent_child_device = false + + if #switch_eps > 0 or #button_eps > 0 then + for _,v in ipairs(switch_eps) do + table.insert(all_eps, v) + end + for _,v in ipairs(button_eps) do + table.insert(all_eps, v) + end + table.sort(all_eps) + -- Since we do not support bindings at the moment, we only want to count clusters + -- that have been implemented as server. This can be removed when we have + -- support for bindings. + local num_switch_server_eps = 0 + local main_endpoint = find_default_endpoint(device) + + for _, ep in ipairs(all_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) or device:supports_server_cluster(clusters.Switch.ID, ep) then + -- Configure MCD for button endpoints + if tbl_contains(STATIC_PROFILE_SUPPORTED, #button_eps) then + if ep ~= main_endpoint then + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + component_map[string.format("switch%d", current_component_number)] = ep + else + component_map[string.format("button%d", current_component_number)] = ep + end + current_component_number = current_component_number + 1 + else + component_map["main"] = ep + end + component_map_used = true + else -- Create child devices for non-main switch endpoints + 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 + } + ) + current_component_number = current_component_number + 1 + parent_child_device = true + end + end end end - end - if num_server_eps > 1 then - -- If the device is a parent child device, then set the find_child function on init. - -- This is persisted because initialize switch is only run once, but find_child function should be set - -- on each driver init. - device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true}) - end - - device:set_field(SWITCH_INITIALIZED, true) - -- The case where num_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 - -- devices need bindings to work correctly (On/Off cluster is client in this case), so these device types - -- do not have a generic fingerprint and will join as a matter-thing. However, we have seen some devices - -- claim to be Light Switch device types and still implement their clusters as server, so this is a - -- workaround for those devices. - if num_server_eps > 0 and detect_matter_thing(device) == true then - local id = 0 - for _, ep in ipairs(device.endpoints) do - -- main_endpoint only supports server cluster by definition of get_endpoints() - if main_endpoint == ep.endpoint_id then - for _, dt in ipairs(ep.device_types) do - -- no device type that is not in the switch subset should be considered. - if (ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= ON_OFF_COLOR_DIMMER_SWITCH_ID) then - id = math.max(id, dt.device_type_id) + if parent_child_device then + -- If the device is a parent child device, then set the find_child function on init. + -- This is persisted because initialize switch is only run once, but find_child function should be set + -- on each driver init. + device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true}) + end + + device:set_field(SWITCH_INITIALIZED, true) + + if #button_eps > 0 then + local battery_support = false + if device.manufacturer_info.vendor_id ~= HUE_MANUFACTURER_ID and + #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.Feature.BATTERY}) > 0 then + battery_support = true + end + if tbl_contains(STATIC_PROFILE_SUPPORTED, #button_eps) then + if battery_support then + new_profile = string.format("%d-button-battery", #button_eps) + else + new_profile = string.format("%d-button", #button_eps) + end + elseif not battery_support then + -- a battery-less button/remote (either single or will use parent/child) + new_profile = "button" + end + + if new_profile then + if #switch_eps > 0 then + new_profile = new_profile .. "-switch" + end + device:try_update_metadata({profile = new_profile}) + end + else + -- The case where num_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 + -- devices need bindings to work correctly (On/Off cluster is client in this case), so these device types + -- do not have a generic fingerprint and will join as a matter-thing. However, we have seen some devices + -- claim to be Light Switch device types and still implement their clusters as server, so this is a + -- workaround for those devices. + if num_switch_server_eps > 0 and detect_matter_thing(device) == true then + local id = 0 + for _, ep in ipairs(device.endpoints) do + -- main_endpoint only supports server cluster by definition of get_endpoints() + if main_endpoint == ep.endpoint_id then + for _, dt in ipairs(ep.device_types) do + -- no device type that is not in the switch subset should be considered. + if (ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= ON_OFF_COLOR_DIMMER_SWITCH_ID) then + id = math.max(id, dt.device_type_id) + end + end + break end end - break + + if device_type_profile_map[id] ~= nil then + device:try_update_metadata({profile = device_type_profile_map[id]}) + end end end + end + if device:get_field(IS_PARENT_CHILD_DEVICE) == true then + device:set_find_child(find_child) + end + + if component_map_used then + device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) + end - if device_type_profile_map[id] ~= nil then - device:try_update_metadata({profile = device_type_profile_map[id]}) + if #button_eps > 0 then + if new_profile then + device:set_field(DEFERRED_CONFIGURE, true) + else + configure_buttons(device) end end end local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + local map + if #button_eps > 0 then + map = device:get_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON) or {} + else + map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + end if map[component] then return map[component] end @@ -200,19 +484,21 @@ local function component_to_endpoint(device, component) end local function endpoint_to_component(device, ep) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) + local map + if #button_eps > 0 then + map = device:get_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON) or {} + else + map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + end for component, endpoint in pairs(map) do if endpoint == ep then - return component + return component end end return "main" end -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) -end - local function detect_bridge(device) for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do @@ -226,6 +512,8 @@ end local function device_init(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER then + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) -- initialize_switch will create parent-child devices as needed for multi-switch devices. -- However, we want to maintain support for existing MCD devices, so do not initialize -- device if it has already been previously initialized as an MCD device. @@ -236,8 +524,6 @@ local function device_init(driver, device) -- create child devices as needed for multi-switch devices initialize_switch(driver, device) end - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) if device:get_field(IS_PARENT_CHILD_DEVICE) == true then device:set_find_child(find_child) end @@ -269,16 +555,6 @@ local function handle_set_level(driver, device, cmd) device:send(req) end ---TODO could be moved to st.utils if made more generally useful -local tbl_contains = function(t, val) - for _, v in pairs(t) do - if v == val then - return true - end - end - return false -end - local TRANSITION_TIME = 0 --1/10ths of a second -- When sent with a command, these options mask and override bitmaps cause the command -- to take effect when the switch/light is off. @@ -309,7 +585,7 @@ local function handle_set_hue(driver, device, cmd) device:send(req) else log.warn("Device does not support huesat features on its color control cluster") - end + end end local function handle_set_saturation(driver, device, cmd) @@ -491,7 +767,6 @@ local function color_cap_attr_handler(driver, device, ib, response) end end - local function illuminance_attr_handler(driver, device, ib, response) local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) @@ -501,9 +776,91 @@ local function occupancy_attr_handler(driver, device, ib, response) device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) end +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) + else + if 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 +end + +-- if the device distinguishes a long press event, it will always be a "held" +-- there's also a "long release" event, but this event is required to come first +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 + +local function short_release_event_handler(driver, device, ib, response) + if not get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + if get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then + emulate_held_event(device, ib.endpoint_id) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + end + end +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 get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) ~= true 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 function battery_percent_remaining_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +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.Feature.MOMENTARY_SWITCH_LONG_PRESS}) + local supportsHeld = tbl_contains(MSL, ib.endpoint_id) + local values = create_multi_list(max, supportsHeld) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) +end + local function info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then device:subscribe() + if device:get_field(DEFERRED_CONFIGURE) and device.network_type ~= device_lib.NETWORK_TYPE_CHILD then + -- profile has changed, and we deferred setting up our buttons, so do that now + configure_buttons(device) + device:set_field(DEFERRED_CONFIGURE, nil) + end end end @@ -550,6 +907,20 @@ local matter_driver_template = { }, [clusters.OccupancySensing.ID] = { [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler, + }, + [clusters.PowerSource.ID] = { + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, + }, + [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.ShortRelease.ID] = short_release_event_handler, + [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler } }, fallback = matter_handler, @@ -579,7 +950,18 @@ local matter_driver_template = { }, [capabilities.motionSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy - } + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + }, + }, + subscribed_events = { + [capabilities.button.ID] = { + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete, + }, }, capability_handlers = { [capabilities.switch.ID] = { @@ -607,9 +989,11 @@ local matter_driver_template = { capabilities.colorControl, capabilities.colorTemperature, capabilities.motionSensor, - capabilities.illuminanceMeasurement + capabilities.illuminanceMeasurement, + capabilities.button, + capabilities.battery }, - sub_drivers = { + sub_drivers = { require("eve-energy"), } } diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua new file mode 100644 index 0000000000..eafa0af293 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -0,0 +1,362 @@ +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local clusters = require "st.matter.clusters" +local button_attr = capabilities.button.button + +--mock the actual device +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("button-battery.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER", + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.Feature.BATTERY + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } + } +}) + +-- add device for each mock device +local CLUSTER_SUBSCRIBE_LIST ={ + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local function test_init() + 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.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" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {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, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.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, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + } +} +) + +test.register_message_test( + "Handle release after short press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({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, 1, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 1, {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( + "Receiving a max press attribute of 3 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 1, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 1, 7 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x", "pushed_4x", "pushed_5x", "pushed_6x"}, {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, 1, {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", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + +} +) + +test.register_message_test( + "Handle multi press for 4 times", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {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", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed_4x({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)) + ), + }, + } +) +-- run the tests +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua new file mode 100644 index 0000000000..ac692f415c --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button_parent_child_switch.lua @@ -0,0 +1,199 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local OPTIONS_OVERRIDE = 0x01 + +local parent_ep = 10 +local child_ep = 20 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("button-battery.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = parent_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = child_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + } + } +}) + +local child_profiles = { + [child_ep] = t_utils.get_profile_definition("light-color-level.yml") +} + +local mock_children = {} +for i, endpoint in ipairs(mock_device.endpoints) do + if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then + local child_data = { + profile = child_profiles[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child_ep) + }) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Parent device: Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) + } + } +) + +test.register_message_test( + "Child device: set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child_ep].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child_ep, 370, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child_ep, 370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2700)) + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button_switch_mcd.lua new file mode 100644 index 0000000000..eb4f370043 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button_switch_mcd.lua @@ -0,0 +1,199 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local utils = require "st.utils" +local dkjson = require "dkjson" + +local clusters = require "st.matter.clusters" + +local button1_ep = 10 +local button2_ep = 20 +local light_ep = 30 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("2-button-battery-switch.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = button1_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = button2_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = light_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light + } + } + } +}) + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 20)}) + test.mock_device.add_test_device(mock_device) + mock_device:expect_metadata_update({ profile = "2-button-battery-switch" }) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "2-buttons-battery-switch" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button1_ep, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) --should send initial press + } + } +) + +test.register_coroutine_test( + "Handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, button2_ep, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, button2_ep, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device, button2_ep, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, button2_ep, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) + end +) + +test.register_message_test( + "On command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "switch3", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, light_ep) + } + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua new file mode 100644 index 0000000000..5b333f972b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -0,0 +1,732 @@ +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 button_attr = capabilities.button.button + +-- Mock a 4-button device using endpoints non-consecutive endpoints +local mock_device = test.mock_device.build_test_matter_device( + { + profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + endpoints = { + { + endpoint_id = 0, + clusters = {}, + device_types = {} + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 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 + } + }, + { + endpoint_id = 50, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 60, + 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 + } + }, + }, +} +) + +-- add device for each mock device +local CLUSTER_SUBSCRIBE_LIST ={ + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local function test_init() + 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.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + mock_device:expect_metadata_update({ profile = "5-button-battery" }) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + 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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", 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}))) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {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 for short release-supported button", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press + } +} +) + +test.register_coroutine_test( + "Handle single press sequence for emulated hold on short-release-only button", + 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, 20, {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, 20, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.held({state_change = true}))) + end +) + +test.register_coroutine_test( + "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, 30, {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, 30, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a long hold on multi button", -- pushes should only be generated from multiPressComplete events + 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, 50, {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, 50, {previous_position = 0} + ) + }) + 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, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 60, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device, 60, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.double({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle long press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 30, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 30, {new_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.held({state_change = true}))) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 30, {previous_position = 0} + ) + }) + end +) + +test.register_coroutine_test( + "Handle long press sequence for a long hold on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.held({state_change = true}))) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 60, {previous_position = 0} + ) + }) + end +) + +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, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.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, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + } +} +) + +test.register_message_test( + "Handle release after short press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({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, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 10, {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, 10, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of 3 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 60, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 10, 7 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x", "pushed_4x", "pushed_5x", "pushed_6x"}, {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, 10, {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", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + +} +) + +test.register_message_test( + "Handle multi press for 4 times", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {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", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + }, + +} +) + +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, 50, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button4", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +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, 10, 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( + "Handle a long press including MultiPressComplete", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + } + -- no double event +} +) + +test.register_message_test( + "Handle long press followed by single press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", button_attr.pushed({state_change = true})) + } + } +) +-- run the tests +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index ba3125a6f4..7e3093a8ca 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -196,6 +196,12 @@ local mock_device_parent_child_switch_types = test.mock_device.build_test_matter }) local function test_init_parent_child_switch_types() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_switch_types) + test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_parent_child_switch_types) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_parent_child_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_parent_child_button.lua new file mode 100644 index 0000000000..84b519eed7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_parent_child_button.lua @@ -0,0 +1,235 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" + +local parent_ep = 10 +local child1_ep = 20 +local child2_ep = 30 + +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("switch-level.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = parent_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light + } + }, + { + endpoint_id = child1_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } + }, + { + endpoint_id = child2_ep, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + } +}) + +local child_profiles = { + [child1_ep] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep] = t_utils.get_profile_definition("button-battery.yml") +} + +local mock_children = {} +for i, endpoint in ipairs(mock_device.endpoints) do + if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then + local child_data = { + profile = child_profiles[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.PowerSource.attributes.BatPercentRemaining, + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child1_ep) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 3", + profile = "button", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child2_ep) + }) + test.socket.capability:__expect_send(mock_children[child2_ep]:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_children[child2_ep]:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Parent device: switch capability should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, parent_ep) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + } +) + +test.register_message_test( + "First child device: switch capability switch should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child1_ep].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, child1_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child1_ep, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switch.switch.on()) + } + } +) + +test.register_message_test( + "Second child device: Handle single press sequence, no hold", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, child2_ep, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.button.button.pushed({state_change = true})) + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 9aa2c2f72c..b6e27bfa37 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -103,6 +103,10 @@ local function test_init() 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 } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do