Skip to content
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
5 changes: 5 additions & 0 deletions drivers/SmartThings/zigbee-contact/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ zigbeeManufacturer:
manufacturer: Aug. Winkhaus SE
model: FM.V.ZB
deviceProfileName: contact-battery-profile
- id: "MultIR/MIR-MC100"
deviceLabel: MultiIR Contact Sensor MIR-MC100
manufacturer: MultIR
model: MIR-MC100
deviceProfileName: contact-battery-tamper-profile-no-fw-update
zigbeeGeneric:
- id: "contact-generic"
deviceLabel: "Zigbee Contact Sensor"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: contact-battery-tamper-profile-no-fw-update
components:
- id: main
capabilities:
- id: contactSensor
version: 1
- id: battery
version: 1
- id: tamperAlert
version: 1
- id: refresh
version: 1
categories:
- name: ContactSensor
13 changes: 13 additions & 0 deletions drivers/SmartThings/zigbee-contact/src/MultiIR/can_handle.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

return function(opts, driver, device, ...)
local FINGERPRINTS = require "MultiIR.fingerprints"
for _, fingerprint in ipairs(FINGERPRINTS) do
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
local subdriver = require("MultiIR")
return true, subdriver
end
end
return false
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

return {
{ mfr = "MultIR", model = "MIR-MC100" }
}
71 changes: 71 additions & 0 deletions drivers/SmartThings/zigbee-contact/src/MultiIR/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0


local clusters = require "st.zigbee.zcl.clusters"
local configurationMap = require "configurations"
local capabilities = require "st.capabilities"

local IASZone = clusters.IASZone

local function generate_event_from_zone_status(driver, device, zone_status, zb_rx)
device:emit_event(zone_status:is_alarm1_set() and capabilities.contactSensor.contact.open() or capabilities.contactSensor.contact.closed())
if device:supports_capability_by_id(capabilities.tamperAlert.ID) then
device:emit_event(zone_status:is_tamper_set() and capabilities.tamperAlert.tamper.detected() or capabilities.tamperAlert.tamper.clear())
end
end

local function ias_zone_status_attr_handler(driver, device, attr_val, zb_rx)
generate_event_from_zone_status(driver, device, attr_val, zb_rx)
end

local function ias_zone_status_change_handler(driver, device, zb_rx)
generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx)
end

local function device_init(driver, device)
device:remove_configured_attribute(IASZone.ID, IASZone.attributes.ZoneStatus.ID)
device:remove_monitored_attribute(IASZone.ID, IASZone.attributes.ZoneStatus.ID)
local configuration = configurationMap.get_device_configuration(device)
if configuration ~= nil then
for _, attribute in ipairs(configuration) do
device:add_configured_attribute(attribute)
end
end
end
Comment on lines +26 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think you want to do this. Removing the ZoneStatus attribute from the configured attributes means device:configure() will not actually send the bind and ConfigureReporting messages to setup the attribute reporting; this mean the hub will not be notified of state changes via this attribute in the contact sensor and the device will be more likely to fall offline.

Also, the configuration map does not have an entry for the device so configuration will always be nil, and not have attributes added to the configuration. Other subdrivers use this pattern because they use different clusters for contact sensing.

Do you expect the device to report the ZoneStatus attribute?


local function added_handler(driver, device)
device:emit_event(capabilities.battery.battery(100))
device:emit_event(capabilities.contactSensor.contact.closed())
if device:supports_capability_by_id(capabilities.tamperAlert.ID) then
device:emit_event(capabilities.tamperAlert.tamper.clear())
end
end

local function do_configure(driver, device)
device:configure()
end
Comment on lines +45 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the default handler. It calls device:configure() which takes care of ias zone configuration as well as the other attributes, and injects a refresh command.


local MultiIR_sensor = {
NAME = "MultiIR Contact Tamper",
lifecycle_handlers = {
init = device_init,
added = added_handler,
doConfigure = do_configure
},
zigbee_handlers = {
cluster = {
[IASZone.ID] = {
[IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler,
}
},
attr = {
[IASZone.ID] = {
[IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler,
}
}
},
can_handle = require("MultiIR.can_handle"),
}

return MultiIR_sensor
1 change: 1 addition & 0 deletions drivers/SmartThings/zigbee-contact/src/sub_drivers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ local sub_drivers = {
lazy_load_if_possible("smartsense-multi"),
lazy_load_if_possible("sengled"),
lazy_load_if_possible("frient"),
lazy_load_if_possible("MultiIR"),
}
return sub_drivers
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local test = require "integration_test"
local clusters = require "st.zigbee.zcl.clusters"
local capabilities = require "st.capabilities"
local t_utils = require "integration_test.utils"
local zigbee_test_utils = require "integration_test.zigbee_test_utils"

local IASZone = clusters.IASZone
local PowerConfiguration = clusters.PowerConfiguration

local mock_device = test.mock_device.build_test_zigbee_device(
{ profile = t_utils.get_profile_definition("contact-battery-tamper-profile-no-fw-update.yml"),
zigbee_endpoints = {
[0x01] = {
id = 0x01,
manufacturer = "MultIR",
model = "MIR-MC100",
server_clusters = { 0x0001,0x0003, 0x0005, 0x0006 }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not include ias zone cluster, but the driver is written like the device would include it. Make sure your mock device is setup with the same endpoints/clusters as the real device.

}
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()

local function test_init()
test.mock_device.add_test_device(mock_device)
end

test.set_test_init_function(test_init)

test.register_coroutine_test(
"Handle added lifecycle",
function()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.socket.capability:__set_channel_ordering("relaxed")
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
capabilities.battery.battery(100)))
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
capabilities.contactSensor.contact.closed()))
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
capabilities.tamperAlert.tamper.clear()))
end,
{
min_api_version = 19
}
)

test.register_coroutine_test(
"Handle doConfigure lifecycle",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
test.socket.zigbee:__expect_send({
mock_device.id,
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID)
})
test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 0x001e, 0x5460, 1)
})
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end,
{
min_api_version = 19
}
)

test.register_message_test(
"Reported ZoneStatus should be handled: contact/closed tamper/clear",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed())
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())
}
},
{
min_api_version = 19
}
)

test.register_message_test(
"Reported ZoneStatus should be handled: contact/open tamper/detected",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0005) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected())
}
},
{
min_api_version = 19
}
)

test.register_message_test(
"ZoneStatusChangeNotification should be handled: contact/open tamper/detected",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0005, 0x00) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected())
}
},
{
min_api_version = 19
}
)

test.register_message_test(
"ZoneStatusChangeNotification should be handled: contact/closed tamper/clear",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed())
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())
}
},
{
min_api_version = 19
}
)

test.run_registered_tests()
1 change: 1 addition & 0 deletions tools/localizations/cn.csv
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1
"WISTAR WSCMXJ Smart Curtain Motor",威仕达智能开合帘电机 WSCMXJ
"HAOJAI Smart Switch 3-key",好家智能三键开关
"HAOJAI Smart Switch 6-key",好家智能六键开关
"MultiIR Contact Sensor MIR-MC100",麦乐克门窗开关传感器MIR-MC100
Loading