diff --git a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml index 6a73e78428..be5d62866f 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml +++ b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml @@ -63,3 +63,9 @@ zwaveManufacturer: productType: 0x0004 productId: 0x0003 deviceProfileName: co-battery + - id: 0371/0002/0032 + deviceLabel: Aeotec SmokeShield + manufacturerId: 0x0371 + productType: 0x0002 + productId: 0x0032 + deviceProfileName: aeotec-smoke-shield \ No newline at end of file diff --git a/drivers/SmartThings/zwave-smoke-alarm/profiles/aeotec-smoke-shield.yml b/drivers/SmartThings/zwave-smoke-alarm/profiles/aeotec-smoke-shield.yml new file mode 100644 index 0000000000..7b3ccadfb1 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/profiles/aeotec-smoke-shield.yml @@ -0,0 +1,14 @@ +name: aeotec-smoke-shield +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: battery + version: 1 + - id: tamperAlert + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/aeotec-smoke-shield/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/aeotec-smoke-shield/init.lua new file mode 100644 index 0000000000..ebdd2a6698 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/aeotec-smoke-shield/init.lua @@ -0,0 +1,72 @@ +-- 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" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version=1 }) +--- @type st.zwave.CommandClass.WakeUp +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) + +local AEOTEC_SMOKE_SHIELD_FINGERPRINTS = { + { manufacturerId = 0x0371, productType = 0x0002 , productId = 0x0032 } +} + +--- Determine whether the passed device is aeotec smoke shield +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @return boolean true if the device is aeotec smoke shield +local function can_handle_aeotec_smoke_shield(opts, driver, device, cmd, ...) + for _, fingerprint in ipairs(AEOTEC_SMOKE_SHIELD_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true + end + end + return false +end + +local function device_added(self, device) + device:emit_event(capabilities.smokeDetector.smoke.clear()) + device:emit_event(capabilities.tamperAlert.tamper.clear()) + device:send(Battery:Get({})) +end + +local function wakeup_notification_handler(self, device, cmd) + --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher + --This is done to help the hub correctly set the checkInterval for migrated devices. + if not device:get_field("__wakeup_interval_get_sent") then + device:send(WakeUp:IntervalGetV1({})) + device:set_field("__wakeup_interval_get_sent", true) + end + device:emit_event(capabilities.smokeDetector.smoke.clear()) + device:send(Battery:Get({})) +end + +local aeotec_smoke_shield = { + zwave_handlers = { + [cc.WAKE_UP] = { + [WakeUp.NOTIFICATION] = wakeup_notification_handler + } + }, + lifecycle_handlers = { + added = device_added + }, + NAME = "Aeotec SmokeShield", + can_handle = can_handle_aeotec_smoke_shield, + health_check = false, +} + +return aeotec_smoke_shield diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua index 0d0a5d1bfd..ae20baa5e0 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua @@ -88,6 +88,7 @@ local driver_template = { require("zwave-smoke-co-alarm-v2"), require("fibaro-smoke-sensor"), require("apiv6_bugfix"), + require("aeotec-smoke-shield"), }, lifecycle_handlers = { init = device_init, diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_aeotec_smoke_shield.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_aeotec_smoke_shield.lua new file mode 100644 index 0000000000..4eee9f29a8 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_aeotec_smoke_shield.lua @@ -0,0 +1,258 @@ +-- 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 test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" + +local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + +local AEOTEC_MANUFACTURER_ID = 0x0371 +local AEOTEC_SMOKE_SHIELD_PRODUCT_TYPE = 0x0002 +local AEOTEC_SMOKE_SHIELD_PRODUCT_ID = 0x0032 + +-- supported comand classes +local sensor_endpoints = { + { + command_classes = { + {value = zw.SENSOR_BINARY}, + {value = zw.BATTERY}, + {value = zw.NOTIFICATION}, + {value = zw.WAKE_UP } + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("aeotec-smoke-shield.yml"), + zwave_endpoints = sensor_endpoints, + zwave_manufacturer_id = AEOTEC_MANUFACTURER_ID, + zwave_product_type = AEOTEC_SMOKE_SHIELD_PRODUCT_TYPE, + zwave_product_id = AEOTEC_SMOKE_SHIELD_PRODUCT_ID + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Sensor Binary report (smoke) should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(SensorBinary:Report({ + sensor_type = SensorBinary.sensor_type.SMOKE, + sensor_value = SensorBinary.sensor_value.DETECTED_AN_EVENT + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.detected()) + } + } +) + +test.register_message_test( + "Sensor Binary report (tamper) should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(SensorBinary:Report({ + sensor_type = SensorBinary.sensor_type.TAMPER, + sensor_value = SensorBinary.sensor_value.DETECTED_AN_EVENT + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + } +) + +test.register_message_test( + "Notification report (smoke) should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.SMOKE, + event = Notification.event.smoke.DETECTED + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.detected()) + } + } +) + +test.register_message_test( + "Notification report (smoke) ALARM_TEST should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.SMOKE, + event = Notification.event.smoke.ALARM_TEST + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.tested()) + } + } +) + +test.register_message_test( + "Notification report (smoke) STATE_IDLE should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.SMOKE, + event = Notification.event.smoke.STATE_IDLE + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.clear()) + } + } +) + + +test.register_message_test( + "Notification report (tamper) TAMPERING should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.HOME_SECURITY, + event = Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + } +) + +test.register_message_test( + "Notification report (tamper) STATE_IDLE should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.HOME_SECURITY, + event = Notification.event.home_security.STATE_IDLE + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + } +) + +test.register_message_test( + "Refresh should generate the correct commands", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_device.id, "added" }, + }, + { + 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()) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Battery:Get({}) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "WakeUp notification should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(WakeUp:Notification({})) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.clear()) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Battery:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + WakeUp:IntervalGet({}) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests()