Skip to content

Create modular profiles subdriver #2142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: modular-profiles-test-matter-switch
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions drivers/SmartThings/matter-switch/src/button-utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
-- Copyright 2025 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 capabilities = require "st.capabilities"
local common_utils = require "common-utils"
local clusters = require "st.matter.clusters"
local log = require "log"
local lua_socket = require "socket"

local START_BUTTON_PRESS = "__start_button_press"
-- 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"
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 TIMEOUT_THRESHOLD = 10 -- arbitrary timeout
local HELD_THRESHOLD = 1

local button_utils = {}

button_utils.STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8}

--- create_multi_press_values_list helper function to create list of multi press values
local function create_multi_press_values_list(size, supportsHeld)
local list = {"pushed", "double"}
if supportsHeld then table.insert(list, "held") end
-- add multi press values of 3 or greater to the list
for i=3, size do
table.insert(list, string.format("pushed_%dx", i))
end
return list
end

local function init_press(device, endpoint)
common_utils.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 = common_utils.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
common_utils.set_field_for_endpoint(device, START_BUTTON_PRESS, ep, nil, {persist = false})
end

function button_utils.configure_buttons(device)
local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE})
local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS})
local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS})

for _, ep in ipairs(ms_eps) do
if device.profile.components[common_utils.endpoint_to_component(device, ep)] then
device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep))
local supportedButtonValues_event
-- this ordering is important, since MSM & MSL devices must also support MSR
if common_utils.tbl_contains(msm_eps, ep) then
supportedButtonValues_event = nil -- deferred to the max press handler
device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep))
common_utils.set_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ep, true, {persist = true})
elseif common_utils.tbl_contains(msl_eps, ep) then
supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}})
elseif common_utils.tbl_contains(msr_eps, ep) then
supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}})
common_utils.set_field_for_endpoint(device, EMULATE_HELD, ep, true, {persist = true})
else -- this switch endpoint only supports momentary switch, no release events
supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}})
common_utils.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}))
else
device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep))
end
end
end

function button_utils.build_button_component_map(device, main_endpoint, button_eps)
-- create component mapping on the main profile button endpoints
table.sort(button_eps)
local component_map = {}
component_map["main"] = main_endpoint
for component_num, ep in ipairs(button_eps) do
if ep ~= main_endpoint then
local button_component = "button"
if #button_eps > 1 then
button_component = button_component .. component_num
end
component_map[button_component] = ep
end
end
device:set_field(common_utils.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true})
end

function button_utils.build_button_profile(device, main_endpoint, num_button_eps)
local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep
if common_utils.device_type_supports_button_switch_combination(device, main_endpoint) then
profile_name = "light-level-" .. profile_name
end
if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then
device:try_update_metadata({profile = profile_name})
else
device:send(clusters.PowerSource.attributes.AttributeList:read(device)) -- battery profiles are configured later, in power_source_attribute_list_handler
end
end

function button_utils.initial_press_event_handler(driver, device, ib, response)
if common_utils.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
common_utils.set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil)
elseif common_utils.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 common_utils.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

-- 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
function button_utils.long_press_event_handler(driver, device, ib, response)
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true}))
if common_utils.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
common_utils.set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true)
end
end

function button_utils.short_release_event_handler(driver, device, ib, response)
if not common_utils.get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then
if common_utils.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

function button_utils.multi_press_complete_event_handler(driver, device, ib, response)
-- in the case of multiple button presses
-- emit number of times, multiple presses have been completed
if ib.data and not common_utils.get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) then
local press_value = ib.data.elements.total_number_of_presses_counted.value
--capability only supports up to 6 presses
if press_value < 7 then
local button_event = capabilities.button.button.pushed({state_change = true})
if press_value == 2 then
button_event = capabilities.button.button.double({state_change = true})
elseif press_value > 2 then
button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true})
end

device:emit_event_for_endpoint(ib.endpoint_id, button_event)
else
log.info(string.format("Number of presses (%d) not supported by capability", press_value))
end
end
common_utils.set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil)
end

function button_utils.max_press_handler(driver, device, ib, response)
local max = ib.data.value or 1 -- get max number of presses
device.log.debug("Device supports "..max.." presses")
-- capability only supports up to 6 presses
if max > 6 then
log.info("Device supports more than 6 presses")
max = 6
end
local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS})
local supportsHeld = common_utils.tbl_contains(MSL, ib.endpoint_id)
local values = create_multi_press_values_list(max, supportsHeld)
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}}))
end

return button_utils
Loading
Loading