Skip to content

Required changes in the driver for Door/Window Contact II [M] device #2061

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

Merged
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
6 changes: 6 additions & 0 deletions drivers/SmartThings/matter-sensor/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ matterManufacturer:
vendorId: 0x115F
productId: 0x2003
deviceProfileName: motion-illuminance-battery
#Bosch
- id: 4617/12309
deviceLabel: "Door/window contact II [M]"
vendorId: 0x1209
productId: 0x3015
deviceProfileName: contact-button-battery
#Elko
- id: "5170/4098"
deviceLabel: RFWD-100/MT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: contact-button-battery
components:
- id: main
capabilities:
- id: contactSensor
version: 1
- id: button
version: 1
- id: battery
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories:
- name: ContactSensor
162 changes: 162 additions & 0 deletions drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
-- 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 clusters = require "st.matter.clusters"
local device_lib = require "st.device"
local lua_socket = require "socket"
local log = require "log"

local START_BUTTON_PRESS = "__start_button_press"

local BOSCH_VENDOR_ID = 0x1209
local BOSCH_PRODUCT_ID = 0x3015

local function is_bosch_button_contact(opts, driver, device)
if device.network_type == device_lib.NETWORK_TYPE_MATTER and
device.manufacturer_info.vendor_id == BOSCH_VENDOR_ID and
device.manufacturer_info.product_id == BOSCH_PRODUCT_ID then
return true
end
return false
end

local function get_field_for_endpoint(device, field, endpoint)
return device:get_field(string.format("%s_%d", field, endpoint))
end

local function set_field_for_endpoint(device, field, endpoint, value, additional_params)
device:set_field(string.format("%s_%d", field, endpoint), value, additional_params)
end

local function init_press(device, endpoint)
set_field_for_endpoint(device, START_BUTTON_PRESS, endpoint, lua_socket.gettime(), {persist = false})
end

-- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a
-- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because
-- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used
-- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event.
local IGNORE_NEXT_MPC = "__ignore_next_mpc"
-- These are essentially storing the supported features of a given endpoint
-- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint
local EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side
local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete
local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease)

local function initial_press_event_handler(driver, device, ib, response)
if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then
-- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event
-- or else we would potentially not create the expected button capability event
set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil)
elseif get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true}))
elseif get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then
-- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time
init_press(device, ib.endpoint_id)
end
end

local function long_press_event_handler(driver, device, ib, response)
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true}))
if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then
-- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence
set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true)
end
end

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

local function tbl_contains(array, value)
for _, element in ipairs(array) do
if element == value then
return true
end
end
return false
end

local function device_init (driver, device)
device:subscribe()
device:send(clusters.Switch.attributes.MultiPressMax:read(device))
end

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

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

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

local Bosch_Button_Contact_Sensor = {
NAME = "Bosch_Button_Contact_Sensor",
lifecycle_handlers = {
init = device_init
},
matter_handlers = {
attr = {
[clusters.Switch.ID] = {
[clusters.Switch.attributes.MultiPressMax.ID] = max_press_handler
}
},
event = {
[clusters.Switch.ID] = {
[clusters.Switch.events.InitialPress.ID] = initial_press_event_handler,
[clusters.Switch.events.LongPress.ID] = long_press_event_handler,
[clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler
}
},
},
can_handle = is_bosch_button_contact,
}

return Bosch_Button_Contact_Sensor
17 changes: 15 additions & 2 deletions drivers/SmartThings/matter-sensor/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ local function match_profile(driver, device, battery_supported)
profile_name = profile_name .. "-flow"
end

if device:supports_capability(capabilities.button) then
profile_name = profile_name .. "-button"
end

if battery_supported == battery_support.BATTERY_PERCENTAGE then
profile_name = profile_name .. "-battery"
elseif battery_supported == battery_support.BATTERY_LEVEL then
Expand Down Expand Up @@ -574,12 +578,20 @@ local matter_driver_template = {
clusters.FlowMeasurement.attributes.MaxMeasuredValue
},
},
subscribed_events = {
[capabilities.button.ID] = {
clusters.Switch.events.InitialPress,
clusters.Switch.events.LongPress,
clusters.Switch.events.MultiPressComplete,
}
},
capability_handlers = {
},
supported_capabilities = {
capabilities.temperatureMeasurement,
capabilities.contactSensor,
capabilities.motionSensor,
capabilities.button,
capabilities.battery,
capabilities.batteryLevel,
capabilities.relativeHumidityMeasurement,
Expand All @@ -589,11 +601,12 @@ local matter_driver_template = {
capabilities.temperatureAlarm,
capabilities.rainSensor,
capabilities.hardwareFault,
capabilities.flowMeasurement
capabilities.flowMeasurement,
},
sub_drivers = {
require("air-quality-sensor"),
require("smoke-co-alarm")
require("smoke-co-alarm"),
require("bosch-button-contact")
}
}

Expand Down
Loading