From d9070212d9c56569aad441ba8dfda046d18d856f Mon Sep 17 00:00:00 2001 From: thinkaName <962679819@qq.com> Date: Fri, 3 Apr 2026 11:13:38 +0800 Subject: [PATCH] add MultiIR Smoke Detector MIR-SM200 --- .../zigbee-smoke-detector/fingerprints.yml | 5 + .../profiles/smoke-battery-tamper.yml | 16 ++ .../src/MultiIR/can_handle.lua | 13 ++ .../src/MultiIR/fingerprints.lua | 6 + .../src/MultiIR/init.lua | 62 +++++ .../zigbee-smoke-detector/src/sub_drivers.lua | 1 + .../src/test/test_multiir_smoke_detector.lua | 215 ++++++++++++++++++ tools/localizations/cn.csv | 1 + 8 files changed, 319 insertions(+) create mode 100755 drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-battery-tamper.yml create mode 100755 drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/can_handle.lua create mode 100755 drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/fingerprints.lua create mode 100755 drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/init.lua create mode 100755 drivers/SmartThings/zigbee-smoke-detector/src/test/test_multiir_smoke_detector.lua diff --git a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml index ddd1135cbd..20de6d1432 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml @@ -54,3 +54,8 @@ zigbeeManufacturer: manufacturer: HEIMAN model: GASSensor-N deviceProfileName: smoke-detector + - id: "MultIR/MIR-SM200" + deviceLabel: MultiIR Smoke Detector MIR-SM200 + manufacturer: MultIR + model: MIR-SM200 + deviceProfileName: smoke-battery-tamper diff --git a/drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-battery-tamper.yml b/drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-battery-tamper.yml new file mode 100755 index 0000000000..8c5d8b4b8f --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-battery-tamper.yml @@ -0,0 +1,16 @@ +name: smoke-battery-tamper +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: tamperAlert + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/can_handle.lua new file mode 100755 index 0000000000..d389619c78 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/can_handle.lua @@ -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 diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/fingerprints.lua b/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/fingerprints.lua new file mode 100755 index 0000000000..3d845bdf7e --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/fingerprints.lua @@ -0,0 +1,6 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "MultIR", model = "MIR-SM200" } +} diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/init.lua new file mode 100755 index 0000000000..b53e79287b --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/MultiIR/init.lua @@ -0,0 +1,62 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local zcl_clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local IASZone = zcl_clusters.IASZone + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + if zone_status:is_alarm1_set() then + device:emit_event(capabilities.smokeDetector.smoke.detected()) + elseif zone_status:is_alarm2_set() then + device:emit_event(capabilities.smokeDetector.smoke.tested()) + else + device:emit_event(capabilities.smokeDetector.smoke.clear()) + end + if device:supports_capability(capabilities.tamperAlert) 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, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + local zone_status = zb_rx.body.zcl_body.zone_status + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function added_handler(self, device) + device:emit_event(capabilities.battery.battery(100)) + device:emit_event(capabilities.smokeDetector.smoke.clear()) + device:emit_event(capabilities.tamperAlert.tamper.clear()) +end + +local function do_configure(self, device) + device:configure() +end + +local MultiIR_smoke_detector_handler = { + NAME = "MultiIR Smoke Detector Handler", + lifecycle_handlers = { + 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_smoke_detector_handler diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua index e6c5e004f3..2b917b85dc 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua @@ -6,5 +6,6 @@ local sub_drivers = { lazy_load_if_possible("frient"), lazy_load_if_possible("aqara-gas"), lazy_load_if_possible("aqara"), + lazy_load_if_possible("MultiIR"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_multiir_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_multiir_smoke_detector.lua new file mode 100755 index 0000000000..99d95717ad --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_multiir_smoke_detector.lua @@ -0,0 +1,215 @@ +-- 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("smoke-battery-tamper.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "MultIR", + model = "MIR-SM200", + server_clusters = { 0x0001,0x0003, 0x0005, 0x0006 } + } + } + } +) + +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.smokeDetector.smoke.clear())) + 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: smoke/clear 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.smokeDetector.smoke.clear()) + }, + { + 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: smoke/detected 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.smokeDetector.smoke.detected()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: smoke/tested tamper/detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0006) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.tested()) + }, + { + 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: smoke/detected 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.smokeDetector.smoke.detected()) + }, + { + 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: smoke/tested tamper/detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0006, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.tested()) + }, + { + 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: smoke/clear 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.smokeDetector.smoke.clear()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + }, + { + min_api_version = 19 + } +) + +test.run_registered_tests() \ No newline at end of file diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index c03098c37f..faeee2a3f7 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -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 Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200