diff --git a/drivers/SmartThings/zigbee-siren/fingerprints.yml b/drivers/SmartThings/zigbee-siren/fingerprints.yml index 57b192b294..48c6512613 100644 --- a/drivers/SmartThings/zigbee-siren/fingerprints.yml +++ b/drivers/SmartThings/zigbee-siren/fingerprints.yml @@ -29,3 +29,8 @@ zigbeeManufacturer : manufacturer: Sercomm Corp. model: SZ-SRN12N deviceProfileName: basic-alarm + - id: "MultIR/MIR-SR100" + deviceLabel: MultiIR Siren MIR-SR100 + manufacturer: MultIR + model: MIR-SR100 + deviceProfileName: switch-alarm-tamper-warningduration-volume diff --git a/drivers/SmartThings/zigbee-siren/profiles/switch-alarm-tamper-warningduration-volume.yml b/drivers/SmartThings/zigbee-siren/profiles/switch-alarm-tamper-warningduration-volume.yml new file mode 100755 index 0000000000..77000579d8 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/profiles/switch-alarm-tamper-warningduration-volume.yml @@ -0,0 +1,38 @@ +name: switch-alarm-tamper-warningduration-volume +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: alarm + version: 1 + - id: tamperAlert + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Siren +preferences: + - title: "警报时长/秒(warning duration/sec)" + name: warningDuration + description: "警报持续时间(warning duration unit:seconds)" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 1800 + default: 1800 + - title: "警报音量(siren volume)" + name: sirenVolume + description: "警报音量大小(siren volume)" + required: false + preferenceType: enumeration + definition: + options: + 0: "低(low)" + 1: "中(medium)" + 2: "高(high)" + 3: "最大(max)" + default: 3 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/MultiIR/can_handle.lua b/drivers/SmartThings/zigbee-siren/src/MultiIR/can_handle.lua new file mode 100755 index 0000000000..d389619c78 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/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-siren/src/MultiIR/fingerprints.lua b/drivers/SmartThings/zigbee-siren/src/MultiIR/fingerprints.lua new file mode 100755 index 0000000000..ef955eecc6 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/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-SR100" } +} diff --git a/drivers/SmartThings/zigbee-siren/src/MultiIR/init.lua b/drivers/SmartThings/zigbee-siren/src/MultiIR/init.lua new file mode 100755 index 0000000000..8c481f1c12 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/MultiIR/init.lua @@ -0,0 +1,135 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local data_types = require "st.zigbee.data_types" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local IASWD = zcl_clusters.IASWD +local IASZone = zcl_clusters.IASZone +local IaswdLevel = IASWD.types.IaswdLevel +local SirenConfiguration = IASWD.types.SirenConfiguration +local WarningMode = IASWD.types.WarningMode +local Strobe = IASWD.types.Strobe +local capabilities = require "st.capabilities" +local ALARM_COMMAND = "alarmCommand" +local DEFAULT_MAX_WARNING_DURATION = 1800 +local ALARM_STROBE_DUTY_CYCLE = 40 + +local alarm_command = { + OFF = 0, + SIREN = 1, + STROBE = 2, + BOTH = 3 +} + +local function device_added (driver, device) + device:emit_event(capabilities.switch.switch.off()) + device:emit_event(capabilities.alarm.alarm.off()) + if(device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end + device:send(IASWD.attributes.MaxDuration:read(device)) +end + +local function do_refresh(driver, device) + device:refresh() +end + +local function do_configure(driver, device) + device:configure() +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + 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 send_siren_command(device, warning_mode, warning_siren_level, warning_duration, strobe_active, strobe_level) + local siren_configuration + + siren_configuration = SirenConfiguration(0x00) + siren_configuration:set_warning_mode(warning_mode) + siren_configuration:set_siren_level(warning_siren_level) + siren_configuration:set_strobe(strobe_active) + + device:send( + IASWD.server.commands.StartWarning( + device, + siren_configuration, + data_types.Uint16(warning_duration), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(strobe_level) + ) + ) +end + +local function siren_switch_off_handler(driver, device, command) + device:set_field(ALARM_COMMAND, alarm_command.OFF, {persist = true}) + send_siren_command(device, WarningMode.STOP, IaswdLevel.LOW_LEVEL, DEFAULT_MAX_WARNING_DURATION, Strobe.NO_STROBE, IaswdLevel.LOW_LEVEL) +end + +local function siren_alarm_siren_handler(alarm_cmd, WarningMode, Strobe, strobe_level) + return function(driver, device, command) + device:set_field(ALARM_COMMAND, alarm_cmd, {persist = true}) + + local sirenVolume_msg = tonumber(device.preferences.sirenVolume) + local warning_duration = tonumber(device.preferences.warningDuration) + + send_siren_command(device, WarningMode , sirenVolume_msg == nil or IaswdLevel.VERY_HIGH_LEVEL ,warning_duration == nil or DEFAULT_MAX_WARNING_DURATION, Strobe, strobe_level) + + device.thread:call_with_delay(warning_duration, function() -- Send command to switch from siren to off in the app when the siren is done + if(device:get_field(ALARM_COMMAND) ~= alarm_command.OFF) then + siren_switch_off_handler(driver, device, alarm_cmd) + end + end) + end +end + +local MultiIR_siren_driver = { + NAME = "MultiIR siren", + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure, + }, + capability_handlers = { + [capabilities.alarm.ID] = { + [capabilities.alarm.commands.off.NAME] = siren_switch_off_handler, + [capabilities.alarm.commands.siren.NAME] = siren_alarm_siren_handler(alarm_command.SIREN, WarningMode.BURGLAR, Strobe.NO_STROBE, IaswdLevel.LOW_LEVEL), + [capabilities.alarm.commands.both.NAME] = siren_alarm_siren_handler(alarm_command.BOTH, WarningMode.BURGLAR, Strobe.USE_STROBE , IaswdLevel.VERY_HIGH_LEVEL), + [capabilities.alarm.commands.strobe.NAME] = siren_alarm_siren_handler(alarm_command.STROBE, WarningMode.STOP, Strobe.USE_STROBE, IaswdLevel.VERY_HIGH_LEVEL) + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = siren_alarm_siren_handler(alarm_command.BOTH, WarningMode.BURGLAR, Strobe.USE_STROBE , IaswdLevel.VERY_HIGH_LEVEL), + [capabilities.switch.commands.off.NAME] = siren_switch_off_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + 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_siren_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua b/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua index 05d186b87e..5f5953b04e 100644 --- a/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua @@ -6,4 +6,5 @@ local lazy_load_if_possible = require "lazy_load_subdriver" return { lazy_load_if_possible("frient"), lazy_load_if_possible("ozom"), + lazy_load_if_possible("MultiIR"), } diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_multiir_zigbee_siren_tamper.lua b/drivers/SmartThings/zigbee-siren/src/test/test_multiir_zigbee_siren_tamper.lua new file mode 100755 index 0000000000..4297bf90a6 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/test/test_multiir_zigbee_siren_tamper.lua @@ -0,0 +1,358 @@ +-- Copyright 2026 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. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local OnOff = clusters.OnOff +local IASZone = clusters.IASZone +local IASWD = clusters.IASWD +local SirenConfiguration = IASWD.types.SirenConfiguration +local DEFAULT_MAX_WARNING_DURATION = 1800 +local ALARM_STROBE_DUTY_CYCLE = 40 +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local t_utils = require "integration_test.utils" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("switch-alarm-tamper-warningduration-volume.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "MultIR", + model = "MIR-SR100", + server_clusters = { IASWD.ID, IASZone.ID,OnOff.ID} + } + } + } +) + +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( + "doConifigure lifecycle should configure device", + function () + test.socket.zigbee:__set_channel_ordering("relaxed") + 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, IASZone.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 0, 180, 0) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse(mock_device, 0x00, 0x00) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 19 + } +) + +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.switch.switch.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tamperAlert.tamper.clear())) + test.socket.zigbee:__expect_send({ mock_device.id, + IASWD.attributes.MaxDuration:read(mock_device)}) + end, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Capability(switch) command(on) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x17), + data_types.Uint16(DEFAULT_MAX_WARNING_DURATION), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(3)) } + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Capability(switch) command(off) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x00), + data_types.Uint16(DEFAULT_MAX_WARNING_DURATION), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(0)) } + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Capability(alarm) command(both) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "alarm", component = "main", command = "both", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x17), + data_types.Uint16(DEFAULT_MAX_WARNING_DURATION), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(3)) } + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Capability(alarm) command(off) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "alarm", component = "main", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x00), + data_types.Uint16(DEFAULT_MAX_WARNING_DURATION), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(0)) } + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Capability(alarm) command(siren) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "alarm", component = "main", command = "siren", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x13), + data_types.Uint16(DEFAULT_MAX_WARNING_DURATION), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(0)) } + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Capability(alarm) command(strobe) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "alarm", component = "main", command = "strobe", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x07), + data_types.Uint16(DEFAULT_MAX_WARNING_DURATION), + data_types.Uint8(ALARM_STROBE_DUTY_CYCLE), + data_types.Enum8(3)) } + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 19 + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: tamper/detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0004, 0x00) } + }, + { + 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: 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.tamperAlert.tamper.clear()) + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: 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.tamperAlert.tamper.clear()) + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: tamper/detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0004) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + }, + { + min_api_version = 19 + } +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index c03098c37f..01c995dd6e 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 Siren MIR-SR100",麦乐克声光报警器MIR-SR100