diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index ee36659670..7f46daba6a 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -83,6 +83,11 @@ zigbeeManufacturer: manufacturer: eWeLink model: SNZB-02P deviceProfileName: humidity-temp-battery + - id: "SONOFF/SNZB-02M" + deviceLabel: "SONOFF SNZB-02M" + manufacturer: SONOFF + model: SNZB-02M + deviceProfileName: sonoff-humidity-temp-press-battery - id: "SONOFF/SNZB-02DR2" deviceLabel: "SONOFF SNZB-02DR2" manufacturer: SONOFF diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/sonoff-humidity-temp-press-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/sonoff-humidity-temp-press-battery.yml new file mode 100644 index 0000000000..b6f48ef8e5 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/sonoff-humidity-temp-press-battery.yml @@ -0,0 +1,18 @@ +name: sonoff-humidity-temp-press-battery +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: atmosphericPressureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MultiFunctionalSensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/sonoff/SNZB-02M/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/sonoff/SNZB-02M/can_handle.lua new file mode 100644 index 0000000000..3989d7bc96 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/sonoff/SNZB-02M/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + if device:get_model() == "SNZB-02M" then + return true, require("sonoff.SNZB-02M") + end + + return false +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/sonoff/SNZB-02M/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/sonoff/SNZB-02M/init.lua new file mode 100644 index 0000000000..194f9b2aa0 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/sonoff/SNZB-02M/init.lua @@ -0,0 +1,58 @@ +--[[ +Description: SNZB-02M pressure/humidity/temperature sensor driver +Version: 1.0 +Author: GitHub Copilot +--]] + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local log = require "log" + +local function convert_pressure_value(raw_value) + if raw_value == nil then + return nil + end + + if raw_value > 2000 then + return raw_value / 1000.0 + end + + return raw_value / 10.0 +end + +local function pressure_report_handler(driver, device, value, zb_rx) + local pressure_value = convert_pressure_value(value.value) + if pressure_value == nil then + log.warn(string.format("SNZB-02M pressure report has no value: %s", tostring(value))) + return + end + + log.debug(string.format("SNZB-02M pressure raw: %s, converted: %s kPa", tostring(value.value), tostring(pressure_value))) + device:emit_event(capabilities.atmosphericPressureMeasurement.atmosphericPressure({value = pressure_value, unit = "kPa"})) +end + +local function refresh_handler(driver, device, command) + device:send(clusters.TemperatureMeasurement.attributes.MeasuredValue:read(device)) + device:send(clusters.RelativeHumidity.attributes.MeasuredValue:read(device)) + device:send(clusters.PressureMeasurement.attributes.MeasuredValue:read(device)) + device:send(clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) +end + +local can_handle = require "sonoff.SNZB-02M.can_handle" + +return { + NAME = "Sonoff SNZB-02M Sensor", + zigbee_handlers = { + attr = { + [clusters.PressureMeasurement.ID] = { + [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = pressure_report_handler + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = can_handle +} diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua index 6f49a5f18c..4ec183d6cb 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua @@ -9,5 +9,6 @@ local sub_drivers = { lazy_load_if_possible("centralite-sensor"), lazy_load_if_possible("heiman-sensor"), lazy_load_if_possible("frient-sensor"), + lazy_load_if_possible("sonoff.SNZB-02M"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_sonoff_snzb02m.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_sonoff_snzb02m.lua new file mode 100644 index 0000000000..c48b01f3ae --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_sonoff_snzb02m.lua @@ -0,0 +1,117 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity +local PressureMeasurement = clusters.PressureMeasurement + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("sonoff-humidity-temp-press-battery.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SONOFF", + model = "SNZB-02M", + server_clusters = { 0x0001, 0x0402, 0x0405, 0x0403 } + } + } + } +) + +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_message_test( + "Pressure report above 2000 should be divided by 1000", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + PressureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 10000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.atmosphericPressureMeasurement.atmosphericPressure({ value = 10.0, unit = "kPa" })) + } + }, + { + min_api_version = 14 + } +) + +test.register_message_test( + "Pressure report at or below 2000 should be divided by 10", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + PressureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 960) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.atmosphericPressureMeasurement.atmosphericPressure({ value = 96.0, unit = "kPa" })) + } + }, + { + min_api_version = 14 + } +) + +test.register_message_test( + "Refresh should read temperature, humidity, pressure and battery", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, RelativeHumidity.attributes.MeasuredValue:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, PressureMeasurement.attributes.MeasuredValue:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) } + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 14 + } +) + +test.run_registered_tests() diff --git a/tools/run_driver_tests_p.py b/tools/run_driver_tests_p.py index c8387dcb1c..dac1721e86 100644 --- a/tools/run_driver_tests_p.py +++ b/tools/run_driver_tests_p.py @@ -10,6 +10,8 @@ DRIVER_DIRS = Path(os.path.abspath(__file__)).parents[1].joinpath("drivers") DRIVERS = [driver for driver in DRIVER_DIRS.glob("*/*") if driver.is_dir()] # this gets all the children of the children of the drivers directory CHANGED_DRIVERS = [Path(driver).name for driver in sys.argv[1:]] +if CHANGED_DRIVERS: + DRIVERS = [driver for driver in DRIVERS if driver.name in CHANGED_DRIVERS] def per_driver_task(driver_dir): os.chdir(driver_dir.joinpath('src'))