From 0e0fdccff258f50d9423732fbe2954e8660e0296 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Wed, 26 Mar 2025 07:58:40 +0100 Subject: [PATCH 1/7] Adding support for frient Intelligent Smoke Alarm --- .../zigbee-smoke-detector/config.yml | 4 +- .../zigbee-smoke-detector/fingerprints.yml | 2 +- .../profiles/smoke-temp-battery-alarm.yml | 50 +++++ .../zigbee-smoke-detector/src/frient/init.lua | 193 +++++++++++++++--- .../zigbee-smoke-detector/src/init.lua | 10 +- 5 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-temp-battery-alarm.yml diff --git a/drivers/SmartThings/zigbee-smoke-detector/config.yml b/drivers/SmartThings/zigbee-smoke-detector/config.yml index 6e4f64c653..5607b709c1 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/config.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/config.yml @@ -1,5 +1,5 @@ -name: 'Zigbee Smoke Detector' -packageKey: 'zigbee-smoke-detector' +name: 'Zigbee Smoke Detector by Gabriel' +packageKey: 'zigbee-smoke-detector by Gabriel' permissions: zigbee: {} description: "SmartThings driver for Zigbee smoke detector devices" diff --git a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml index a8384b886c..76b105fdb8 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml @@ -33,7 +33,7 @@ zigbeeManufacturer: deviceLabel: frient Smoke Detector manufacturer: frient A/S model: SMSZB-120 - deviceProfileName: smoke-battery + deviceProfileName: smoke-temp-battery-alarm - id: "Heiman/Orvibo/Gas3" deviceLabel: Orvibo Gas Detector manufacturer: Heiman diff --git a/drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-temp-battery-alarm.yml b/drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-temp-battery-alarm.yml new file mode 100644 index 0000000000..c1134baa02 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/profiles/smoke-temp-battery-alarm.yml @@ -0,0 +1,50 @@ +name: smoke-temp-battery-alarm +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: alarm + version: 1 + config: + values: + - key: "alarm.value" + enabledValues: + - off + - siren + - key: "{{enumCommands}}" + enabledValues: + - off + - siren + categories: + - name: SmokeDetector +preferences: + - preferenceId: tempOffset + explicit: true + - name: "tempSensitivity" + title: "Temperature Sensitivity (18.0°)" + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - name: "warningDuration" + title: "Alarm duration (s)" + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 + \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index 0f1dee8e23..f1a7f5f169 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -1,32 +1,177 @@ --- Copyright 2022 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 battery_defaults = require "st.zigbee.defaults.battery_defaults" +local capabilities = require "st.capabilities" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local zcl_global_commands = require "st.zigbee.zcl.global_commands" +local data_types = require "st.zigbee.data_types" +local device_management = require "st.zigbee.device_management" +local alarm = capabilities.alarm +local smokeDetector = capabilities.smokeDetector + +local IASWD = zcl_clusters.IASWD +local IASZone = zcl_clusters.IASZone +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement + +local ALARM_COMMAND = "alarmCommand" +local ALARM_LAST_DURATION = "Custom_Alarm_Duration" +local ALARM_DEFAULT_MAX_DURATION = 0x00B4 +local DEFAULT_WARNING_DURATION = 240 +local BATTERY_MIN_VOLTAGE = 2.3 +local BATTERY_MAX_VOLTAGE = 3.0 + + +local alarm_command = { + OFF = 0, + SIREN = 1 +} + +local function device_init(driver, device) + device:send(IASZone.attributes.ZoneStatus:read(device)) -- read the initial status of the smoke detector + device:emit_event(alarm.alarm.off()) + battery_defaults.build_linear_voltage_init(BATTERY_MIN_VOLTAGE, BATTERY_MAX_VOLTAGE)(driver, device) +end + +local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) + print("Received ZoneStatus:", zone_status.value) + + if zone_status:is_test_set() then + print("Test mode detected!") + device:emit_event(smokeDetector.smoke.tested()) + elseif zone_status:is_alarm1_set() then + print("Smoke detected!") + device:emit_event(smokeDetector.smoke.detected()) + else + print("Smoke cleared!") + device:emit_event(smokeDetector.smoke.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) + local warning_duration = device:get_field(ALARM_LAST_DURATION) or DEFAULT_WARNING_DURATION + local sirenConfiguration = IASWD.types.SirenConfiguration(0x00) + + sirenConfiguration:set_warning_mode(0x01) + + device:send( + IASWD.server.commands.StartWarning( + device, + sirenConfiguration, + data_types.Uint16(warning_duration) + ) + ) + +end + +local emit_alarm_event = function(device, cmd) + if cmd == alarm_command.OFF then + device:emit_event(alarm.alarm.off()) + else + if cmd == alarm_command.SIREN then + device:emit_event(alarm.alarm.siren()) + end + end +end -local is_frient_smoke_detector = function(opts, driver, device) - if device:get_manufacturer() == "frient A/S" then - return true +local default_response_handler = function(driver, device, zigbee_message) + local command = zigbee_message.body.zcl_body.cmd.value + local alarm_ev = device:get_field(ALARM_COMMAND) + if command == IASWD.server.commands.StartWarning.ID then + if alarm_ev ~= alarm_command.OFF then + emit_alarm_event(device, alarm_ev) + local lastDuration = device:get_field(ALARM_LAST_DURATION) or ALARM_DEFAULT_MAX_DURATION + device.thread:call_with_delay(lastDuration, function(d) + device:emit_event(alarm.alarm.off()) + end) + else + emit_alarm_event(device,alarm_command.OFF) + end end - return false end -local frient_smoke_detector = { - NAME = "Freint Smoke Detector", +local function do_configure(self, device) + device:configure() + device:send(device_management.build_bind_request(device, zcl_clusters.IASZone.ID, self.environment_info.hub_zigbee_eui )) + device:send(TemperatureMeasurement.server.attributes.MeasuredValue:configure_reporting(device, 60, 600, 100):to_endpoint(0x26)) + +end + +local info_changed = function (driver, device, event, args) + for name, info in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local input = device.preferences[name] + local payload + if (name == "tempSensitivity") then + payload = (input * 100) + 0.5 + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, data_types.Int16(payload))) + elseif (name == "warningDuration") then + device:set_field(ALARM_LAST_DURATION, input, {persist = true}) + end + end + end +end + +local siren_alarm_siren_handler = function(driver, device, command) + device:set_field(ALARM_COMMAND, alarm_command.SIREN, {persist = true}) + send_siren_command(device) +end + + +local siren_switch_off_handler = function(driver, device, command) + local sirenConfiguration = IASWD.types.SirenConfiguration(0x00) + sirenConfiguration:set_warning_mode(0x00) + device:set_field(ALARM_COMMAND, alarm_command.OFF, {persist = true}) + + device:send( + IASWD.server.commands.StartWarning( + device, + sirenConfiguration + ) + ) +end + +local frient_smoke_sensor = { + NAME = "frient smoke sensor", lifecycle_handlers = { - init = battery_defaults.build_linear_voltage_init(2.3, 3.0) + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed + }, + supported_capabilities = { + alarm }, - can_handle = is_frient_smoke_detector + capability_handlers = { + [alarm.ID] = { + [alarm.commands.off.NAME] = siren_switch_off_handler, + [alarm.commands.siren.NAME] = siren_alarm_siren_handler, + }, + }, + zigbee_handlers = { + global = { + [IASWD.ID] = { + [zcl_global_commands.DEFAULT_RESPONSE_ID] = default_response_handler + }, + }, + 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 = function(opts, driver, device, ...) + return device:get_manufacturer() == "frient A/S" and device:get_model() == "SMSZB-120" + end } - -return frient_smoke_detector +return frient_smoke_sensor \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index 2d46a5dc8c..269063677b 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -16,21 +16,23 @@ local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" +local TemperatureMeasurement = (require "st.zigbee.zcl.clusters").TemperatureMeasurement local zigbee_smoke_driver_template = { supported_capabilities = { capabilities.smokeDetector, - capabilities.battery + capabilities.battery, + capabilities.alarm, + capabilities.temperatureMeasurement }, sub_drivers = { require("frient"), require("aqara-gas"), - require("aqara"), - require("frient") + require("aqara") }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, } defaults.register_for_default_handlers(zigbee_smoke_driver_template, zigbee_smoke_driver_template.supported_capabilities) local zigbee_smoke_driver = ZigbeeDriver("zigbee-smoke-detector", zigbee_smoke_driver_template) -zigbee_smoke_driver:run() +zigbee_smoke_driver:run() \ No newline at end of file From 812e12fa1478f1e60819ba6a2313f05cfbd8ca63 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Thu, 27 Mar 2025 08:32:48 +0100 Subject: [PATCH 2/7] Update init.lua --- .../zigbee-smoke-detector/src/frient/init.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index f1a7f5f169..e119e2c47f 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -36,12 +36,17 @@ local function generate_event_from_zone_status(driver, device, zone_status, zigb if zone_status:is_test_set() then print("Test mode detected!") device:emit_event(smokeDetector.smoke.tested()) + + elseif zone_status:is_alarm1_set() then print("Smoke detected!") device:emit_event(smokeDetector.smoke.detected()) else - print("Smoke cleared!") + device.thread:call_with_delay(6, function () + print("Smoke cleared!") device:emit_event(smokeDetector.smoke.clear()) + end) + end end @@ -98,9 +103,7 @@ end local function do_configure(self, device) device:configure() - device:send(device_management.build_bind_request(device, zcl_clusters.IASZone.ID, self.environment_info.hub_zigbee_eui )) device:send(TemperatureMeasurement.server.attributes.MeasuredValue:configure_reporting(device, 60, 600, 100):to_endpoint(0x26)) - end local info_changed = function (driver, device, event, args) From e3a3373602213aeb5179590fd62994b7084581c1 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Fri, 28 Mar 2025 07:57:52 +0100 Subject: [PATCH 3/7] According to @lelandblue comment. --- drivers/SmartThings/zigbee-smoke-detector/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-smoke-detector/config.yml b/drivers/SmartThings/zigbee-smoke-detector/config.yml index 5607b709c1..6e4f64c653 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/config.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/config.yml @@ -1,5 +1,5 @@ -name: 'Zigbee Smoke Detector by Gabriel' -packageKey: 'zigbee-smoke-detector by Gabriel' +name: 'Zigbee Smoke Detector' +packageKey: 'zigbee-smoke-detector' permissions: zigbee: {} description: "SmartThings driver for Zigbee smoke detector devices" From 788e9914ec0f425e590d6e15d091a87182d524f4 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Fri, 11 Apr 2025 08:01:13 +0200 Subject: [PATCH 4/7] v.1.2 - updated according to feedback --- .../zigbee-smoke-detector/src/frient/init.lua | 114 ++-- .../src/test/test_frient_smoke_detector.lua | 578 +++++++++++++----- 2 files changed, 504 insertions(+), 188 deletions(-) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index e119e2c47f..295013d1ef 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -1,9 +1,22 @@ +-- 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 battery_defaults = require "st.zigbee.defaults.battery_defaults" local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local zcl_global_commands = require "st.zigbee.zcl.global_commands" local data_types = require "st.zigbee.data_types" -local device_management = require "st.zigbee.device_management" local alarm = capabilities.alarm local smokeDetector = capabilities.smokeDetector @@ -12,32 +25,80 @@ local IASZone = zcl_clusters.IASZone local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement local ALARM_COMMAND = "alarmCommand" -local ALARM_LAST_DURATION = "Custom_Alarm_Duration" -local ALARM_DEFAULT_MAX_DURATION = 0x00B4 -local DEFAULT_WARNING_DURATION = 240 +local ALARM_LAST_DURATION = "Custom_Alarm_Duration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local DEFAULT_WARNING_DURATION = 240 local BATTERY_MIN_VOLTAGE = 2.3 local BATTERY_MAX_VOLTAGE = 3.0 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 local alarm_command = { OFF = 0, SIREN = 1 } -local function device_init(driver, device) - device:send(IASZone.attributes.ZoneStatus:read(device)) -- read the initial status of the smoke detector +local CONFIGURATIONS = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 30, + maximum_interval = 300, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1 + }, + { + cluster = TemperatureMeasurement.ID, + attribute = TemperatureMeasurement.attributes.MeasuredValue.ID, + minimum_interval = 60, + maximum_interval = 600, + data_type = TemperatureMeasurement.attributes.MeasuredValue.base_type, + reportable_change = 100 + } +} + +local function device_added(driver, device) device:emit_event(alarm.alarm.off()) + device:emit_event(smokeDetector.smoke.clear()) +end + +local function device_init(driver, device) battery_defaults.build_linear_voltage_init(BATTERY_MIN_VOLTAGE, BATTERY_MAX_VOLTAGE)(driver, device) + if CONFIGURATIONS ~= nil then + for _, attribute in ipairs(CONFIGURATIONS) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + end +end + +local function do_configure(self, device) + device:configure() + device:send(IASWD.attributes.MaxDuration:write(device, ALARM_DEFAULT_MAX_DURATION)) +end + +local info_changed = function (driver, device, event, args) + for name, info in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local input = device.preferences[name] + local payload + if (name == "tempSensitivity") then + local sensitivity = math.floor((device.preferences.tempSensitivity or 0.1)*100 + 0.5) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 60, 600, sensitivity):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT)) + elseif (name == "warningDuration") then + device:set_field(ALARM_LAST_DURATION, input, {persist = true}) + device:send(IASWD.attributes.MaxDuration:write(device, tonumber(input))) + end + end + end end local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) print("Received ZoneStatus:", zone_status.value) - + if zone_status:is_test_set() then print("Test mode detected!") device:emit_event(smokeDetector.smoke.tested()) - - elseif zone_status:is_alarm1_set() then print("Smoke detected!") device:emit_event(smokeDetector.smoke.detected()) @@ -46,7 +107,6 @@ local function generate_event_from_zone_status(driver, device, zone_status, zigb print("Smoke cleared!") device:emit_event(smokeDetector.smoke.clear()) end) - end end @@ -60,11 +120,11 @@ local function ias_zone_status_change_handler(driver, device, zb_rx) end local function send_siren_command(device) - local warning_duration = device:get_field(ALARM_LAST_DURATION) or DEFAULT_WARNING_DURATION + local warning_duration = device:get_field(ALARM_LAST_DURATION) or DEFAULT_WARNING_DURATION local sirenConfiguration = IASWD.types.SirenConfiguration(0x00) - + sirenConfiguration:set_warning_mode(0x01) - + device:send( IASWD.server.commands.StartWarning( device, @@ -101,26 +161,6 @@ local default_response_handler = function(driver, device, zigbee_message) end end -local function do_configure(self, device) - device:configure() - device:send(TemperatureMeasurement.server.attributes.MeasuredValue:configure_reporting(device, 60, 600, 100):to_endpoint(0x26)) -end - -local info_changed = function (driver, device, event, args) - for name, info in pairs(device.preferences) do - if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then - local input = device.preferences[name] - local payload - if (name == "tempSensitivity") then - payload = (input * 100) + 0.5 - device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, data_types.Int16(payload))) - elseif (name == "warningDuration") then - device:set_field(ALARM_LAST_DURATION, input, {persist = true}) - end - end - end -end - local siren_alarm_siren_handler = function(driver, device, command) device:set_field(ALARM_COMMAND, alarm_command.SIREN, {persist = true}) send_siren_command(device) @@ -143,13 +183,11 @@ end local frient_smoke_sensor = { NAME = "frient smoke sensor", lifecycle_handlers = { - init = device_init, + added = device_added, doConfigure = do_configure, + init = device_init, infoChanged = info_changed }, - supported_capabilities = { - alarm - }, capability_handlers = { [alarm.ID] = { [alarm.commands.off.NAME] = siren_switch_off_handler, @@ -177,4 +215,4 @@ local frient_smoke_sensor = { return device:get_manufacturer() == "frient A/S" and device:get_model() == "SMSZB-120" end } -return frient_smoke_sensor \ No newline at end of file +return frient_smoke_sensor diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua index fae04f8963..188b12c591 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. @@ -15,182 +15,460 @@ -- Mock out globals local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" +local IASWD = clusters.IASWD local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement local capabilities = require "st.capabilities" +local alarm = capabilities.alarm +local smokeDetector = capabilities.smokeDetector local zigbee_test_utils = require "integration_test.zigbee_test_utils" local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" local t_utils = require "integration_test.utils" +local data_types = require "st.zigbee.data_types" +local SirenConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SirenConfiguration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local IASZONE_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 +local base64 = require "base64" + local mock_device = test.mock_device.build_test_zigbee_device( - { profile = t_utils.get_profile_definition("smoke-battery.yml"), - zigbee_endpoints = { - [1] = { - id = 1, - manufacturer = "frient A/S", - model = "SMSZB-120", - server_clusters = {0x0000,0x0001,0x0003,0x000F,0x0020,0x0500,0x0502} - } - } - } + { profile = t_utils.get_profile_definition("smoke-temp-battery-alarm.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SMSZB-120", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x000f, 0x0020, 0x0500, 0x0502 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + } + } + } ) zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device) - zigbee_test_utils.init_noop_health_check_timer() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() end test.set_test_init_function(test_init) -test.register_message_test( - "Battery voltage report should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) - } - } +test.register_coroutine_test( + "Clear alarm and smokeDetector states when the device is added", function() + test.socket.matter:__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", alarm.alarm.off()) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", smokeDetector.smoke.clear()) + ) + + test.wait_for_events() + end ) test.register_coroutine_test( - "Health check should check all relevant attributes", - function() - test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) - test.wait_for_events() - - test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.zigbee:__expect_send( - { - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:read(mock_device) - } - ) - test.socket.zigbee:__expect_send( - { - mock_device.id, - IASZone.attributes.ZoneStatus:read(mock_device) - } - ) - end, - { - test_init = function() - test.mock_device.add_test_device(mock_device) - test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") - end - } + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", alarm.alarm.off()) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", smokeDetector.smoke.clear()) + ) + + test.wait_for_events() + 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, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 600, + 100 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 0 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, ALARM_DEFAULT_MAX_DURATION) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit0 Alarm1 set to 1 + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.detected()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: tested", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit8: Test set to 1 + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x100, 0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.tested()) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + } ) test.register_coroutine_test( - "Configure should configure all necessary attributes", - function () - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) - test.socket.zigbee:__expect_send({ - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.attributes.ZoneStatus:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting( - mock_device, - 30, - 21600, - 1 - ) - }) - 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, - 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, - IasEnrollResponseCode.SUCCESS, - 0x00 - ) - }) - 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, - zigbee_test_utils.build_bind_request( - mock_device, - zigbee_test_utils.mock_hub_eui, - IASZone.ID - ) - }) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "Health check should check all relevant attributes", + function() + test.wait_for_events() + + test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device) + test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") + end + } +) + +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, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } ) test.register_message_test( - "Refresh should read all necessary attributes", - { - { - channel = "device_lifecycle", - direction = "receive", - message = {mock_device.id, "added"} - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "refresh", component = "main", command = "refresh", args = {} } + "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, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" } - }, - { - channel = "zigbee", - direction = "send", - message = { - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:read(mock_device) +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: tempSensitivity, warningDuration", + function() + local updates = { + preferences = { + tempSensitivity = 1.3, + warningDuration = 100 + } + } + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 600, + 130 + )--:to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, 0x0064)--:to_endpoint(IASZONE_ENDPOINT) + }) + + + test.socket.zigbee:__set_channel_ordering("relaxed") + + end +) + +local sirenConfiguration = SirenConfiguration(0x00) +sirenConfiguration:set_warning_mode(0x01) +local defaultWarningDuration = 240 + +test.register_message_test( + "Capability command Alarm - siren 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, + data_types.Uint16(defaultWarningDuration), + data_types.Uint8(00), + data_types.Enum8(00)) + } + } + }, + { + inner_block_ordering = "relaxed" } - }, - { - channel = "zigbee", - direction = "send", - message = { - mock_device.id, - IASZone.attributes.ZoneStatus:read(mock_device) +) + +local sirenConfiguration = SirenConfiguration(0x00) +sirenConfiguration:set_warning_mode(0x00) + +test.register_message_test( + "Capability command Alarm - OFF 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, + data_types.Uint16(0x00), + data_types.Uint8(00), + data_types.Enum8(00)) + } + } + }, + { + inner_block_ordering = "relaxed" } - }, - }, - { - inner_block_ordering = "relaxed" - } ) + test.run_registered_tests() From a54d9c442b74f1d2fb40e2683f04883e2c94a0c5 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Fri, 11 Apr 2025 09:00:11 +0200 Subject: [PATCH 5/7] v1.3 - fixed according to test results --- drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua | 1 - drivers/SmartThings/zigbee-smoke-detector/src/init.lua | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index 295013d1ef..f258d85426 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -81,7 +81,6 @@ local info_changed = function (driver, device, event, args) for name, info in pairs(device.preferences) do if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then local input = device.preferences[name] - local payload if (name == "tempSensitivity") then local sensitivity = math.floor((device.preferences.tempSensitivity or 0.1)*100 + 0.5) device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 60, 600, sensitivity):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT)) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index 269063677b..6f4f44f492 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -16,13 +16,12 @@ local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" -local TemperatureMeasurement = (require "st.zigbee.zcl.clusters").TemperatureMeasurement local zigbee_smoke_driver_template = { supported_capabilities = { capabilities.smokeDetector, capabilities.battery, - capabilities.alarm, + capabilities.alarm, capabilities.temperatureMeasurement }, sub_drivers = { From 1f57bf3a43aa9b17f7915903550ea5e46a91a876 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Tue, 15 Apr 2025 09:50:05 +0200 Subject: [PATCH 6/7] v1.4 update based on review feedback --- .../SmartThings/zigbee-smoke-detector/src/frient/init.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index f258d85426..16008ecf52 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -1,4 +1,4 @@ --- Copyright 2025 SmartThings +-- Copyright 2022 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -93,17 +93,13 @@ local info_changed = function (driver, device, event, args) end local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) - print("Received ZoneStatus:", zone_status.value) if zone_status:is_test_set() then - print("Test mode detected!") device:emit_event(smokeDetector.smoke.tested()) elseif zone_status:is_alarm1_set() then - print("Smoke detected!") device:emit_event(smokeDetector.smoke.detected()) else device.thread:call_with_delay(6, function () - print("Smoke cleared!") device:emit_event(smokeDetector.smoke.clear()) end) end From ec01237b8215b77b5a13b8de3c8a36e97140e0b2 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Wed, 16 Apr 2025 07:55:43 +0200 Subject: [PATCH 7/7] V1.5, according to feedback --- .../src/test/test_frient_smoke_detector.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua index 188b12c591..e8453260fa 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua @@ -1,4 +1,4 @@ --- Copyright 2025 SmartThings +-- Copyright 2022 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License.