From 1329c3f372b7ab50b17f39242dfad6106491cabb Mon Sep 17 00:00:00 2001 From: West_Zhao <237791949@qq.com> Date: Thu, 30 Oct 2025 09:39:45 +0800 Subject: [PATCH 1/8] Add Sonoff SNZB-01M Smart Scene Button into zigbee-button. --- .../zigbee-button/fingerprints.yml | 6 ++ .../profiles/sonoff-buttons-battery.yml | 36 ++++++++ .../src/test/test_sonoff_button.lua | 26 ++++++ .../src/zigbee-multi-button/init.lua | 1 - .../src/zigbee-multi-button/sonoff/init.lua | 87 +++++++++++++++++++ .../zigbee-multi-button/supported_values.lua | 7 ++ 6 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-button/profiles/sonoff-buttons-battery.yml create mode 100644 drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index c73109ebca..a30c3dd291 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -262,6 +262,12 @@ zigbeeManufacturer: manufacturer: WALL HERO model: ACL-401SCA4 deviceProfileName: thirty-buttons + # SONOFF + - id: "SONOFF/SNZB-01M" + deviceLabel: SNZB-01M + manufacturer: SONOFF + model: SNZB-01M + deviceProfileName: sonoff-buttons-battery zigbeeGeneric: - id: "generic-button-sensor" deviceLabel: "Zigbee Generic Button" diff --git a/drivers/SmartThings/zigbee-button/profiles/sonoff-buttons-battery.yml b/drivers/SmartThings/zigbee-button/profiles/sonoff-buttons-battery.yml new file mode 100644 index 0000000000..13a99912e9 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/sonoff-buttons-battery.yml @@ -0,0 +1,36 @@ +name: sonoff-buttons-battery +components: + - id: main + capabilities: + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua new file mode 100644 index 0000000000..be7c44daa8 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -0,0 +1,26 @@ +-- Test file for SONOFF SNZB-01M integration +-- This file can be used to verify the SONOFF driver integration + +local test = {} + +-- Test that the SONOFF fingerprint is correctly added +test.fingerprint_test = function() + local fingerprints = { + { mfr = "SONOFF", model = "SNZB-01M" } + } + + for _, fp in ipairs(fingerprints) do + print("Testing fingerprint: " .. fp.mfr .. "/" .. fp.model) + end +end + +-- Test supported button values +test.supported_values_test = function() + local supported_values = { "pushed", "double", "held", "pushed_3x" } + print("Supported button values for SONOFF:") + for _, value in ipairs(supported_values) do + print(" - " .. value) + end +end + +return test \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index 84dc2af26e..daaa24a8b6 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -8,7 +8,6 @@ local supported_values = require "zigbee-multi-button.supported_values" local button_utils = require "button_utils" - local function added_handler(self, device) local config = supported_values.get_device_parameters(device) for _, component in pairs(device.profile.components) do diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua new file mode 100644 index 0000000000..63d7d9d408 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -0,0 +1,87 @@ +-- Copyright 2024 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" +local clusters = require "st.zigbee.zcl.clusters" +local log = require "log" + +local SONOFF_CLUSTER_ID = 0xFC12 +local SONOFF_ATTR_ID = 0x0000 +local BatteryPercentageRemaining = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining + +local EVENT_MAP = { + [0x01] = capabilities.button.button.pushed, + [0x02] = capabilities.button.button.double, + [0x03] = capabilities.button.button.held, + [0x04] = capabilities.button.button.pushed_3x +} + +local function can_handle(opts, driver, device, ...) + return device:get_manufacturer() == "SONOFF" +end + +local function battery_attr_handler(driver, device, value, zb_rx) + local percent = math.floor((value.value or 0) / 2) + device:emit_event(capabilities.battery.battery(percent)) + log.info(string.format("Battery percentage remaining: %d%%", percent)) +end + +local function sonoff_attr_handler(driver, device, value, zb_rx) + local attr_val = value.value + local endpoint = zb_rx.address_header.src_endpoint.value + local button_name = "button" .. tostring(endpoint) + local event_func = EVENT_MAP[attr_val] + log.info(string.format("SONOFF attr: endpoint=%s, value=%s, button=%s", endpoint, attr_val, button_name)) + if event_func then + local comp = device.profile.components[button_name] + if comp then + device:emit_component_event(comp, event_func({state_change = true})) + else + log.warn("Unknown button component: " .. button_name) + end + else + log.warn("Unknown event value: " .. tostring(attr_val)) + end +end + +local function added_handler(self, device) + device:configure() + for _, comp in pairs(device.profile.components) do + if comp.id ~= "main" then + device:emit_component_event(comp, capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = { displayed = false }})) + device:emit_component_event(comp, capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) + device:emit_component_event(comp, capabilities.button.button.pushed({state_change = false})) + end + end +end + +local sonoff_handler = { + NAME = "SONOFF Multi-Button Handler", + zigbee_handlers = { + attr = { + [SONOFF_CLUSTER_ID] = { + [SONOFF_ATTR_ID] = sonoff_attr_handler + }, + [clusters.PowerConfiguration.ID] = { + [BatteryPercentageRemaining.ID] = battery_attr_handler + }, + } + }, + lifecycle_handlers = { + added = added_handler + }, + can_handle = can_handle +} + +return sonoff_handler \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua index 813859f891..83a66abb65 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua @@ -119,6 +119,13 @@ local devices = { }, SUPPORTED_BUTTON_VALUES = { "pushed", "down_hold", "up" }, NUMBER_OF_BUTTONS = 2 + }, + SONOFF_BUTTON_4 = { + MATCHING_MATRIX = { + { mfr = "SONOFF", model = "SNZB-01M" } + }, + SUPPORTED_BUTTON_VALUES = { "pushed", "double", "held", "pushed_3x" }, + NUMBER_OF_BUTTONS = 4 } } From f1e199d030ea972c3a743c9ed5033b1ad96aaec0 Mon Sep 17 00:00:00 2001 From: West Zhao <55424074+Oniums@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:22:39 +0800 Subject: [PATCH 2/8] modify Copyright Date --- .../zigbee-button/src/zigbee-multi-button/sonoff/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua index 63d7d9d408..c7ed8c4564 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -1,4 +1,4 @@ --- Copyright 2024 SmartThings +-- 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. @@ -84,4 +84,4 @@ local sonoff_handler = { can_handle = can_handle } -return sonoff_handler \ No newline at end of file +return sonoff_handler From c0ae8b73882335d01a15f89a0394ec6ecf6a54a0 Mon Sep 17 00:00:00 2001 From: West Zhao <55424074+Oniums@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:32:35 +0800 Subject: [PATCH 3/8] modify test_sonoff.py fix PR error --- .../src/test/test_sonoff_button.lua | 273 ++++++++++++++++-- 1 file changed, 254 insertions(+), 19 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua index be7c44daa8..e4d2495416 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -1,26 +1,261 @@ --- Test file for SONOFF SNZB-01M integration --- This file can be used to verify the SONOFF driver integration +-- 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 test = {} +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 data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" --- Test that the SONOFF fingerprint is correctly added -test.fingerprint_test = function() - local fingerprints = { - { mfr = "SONOFF", model = "SNZB-01M" } +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("sonoff-buttons-battery.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + }, + [2] = { + id = 2, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + }, + [3] = { + id = 3, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + }, + [4] = { + id = 4, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + } + } } - - for _, fp in ipairs(fingerprints) do - print("Testing fingerprint: " .. fp.mfr .. "/" .. fp.model) - end +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) end --- Test supported button values -test.supported_values_test = function() - local supported_values = { "pushed", "double", "held", "pushed_3x" } - print("Supported button values for SONOFF:") - for _, value in ipairs(supported_values) do - print(" - " .. value) +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "added lifecycle event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- Check initial events for button 1 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.pushed({ state_change = false })) + ) + + -- Check initial events for button 2 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = false })) + ) + + -- Check initial events for button 3 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button3", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button3", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button3", capabilities.button.button.pushed({ state_change = false })) + ) + + -- Check initial events for button 4 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button4", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button4", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button4", capabilities.button.button.pushed({ state_change = false })) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.PowerConfiguration.attributes.BatteryPercentageRemaining: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, clusters.PowerConfiguration.ID) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -end +) + +test.register_coroutine_test( + "Button pushed message should generate event", + function() + -- 0xFC12, 0x0000, 0x01 = pushed + local attr_report = cluster_base.build_custom_report_attribute( + mock_device, + 0xFC12, + 0x0000, + 0x20, -- Uint8 + data_types.Uint8(0x01) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + end +) + +test.register_coroutine_test( + "Button double message should generate event", + function() + -- 0xFC12, 0x0000, 0x02 = double + local attr_report = cluster_base.build_custom_report_attribute( + mock_device, + 0xFC12, + 0x0000, + 0x20, -- Uint8 + data_types.Uint8(0x02) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.double({ state_change = true })) + ) + end +) + +test.register_coroutine_test( + "Button held message should generate event", + function() + -- 0xFC12, 0x0000, 0x03 = held + local attr_report = cluster_base.build_custom_report_attribute( + mock_device, + 0xFC12, + 0x0000, + 0x20, -- Uint8 + data_types.Uint8(0x03) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.held({ state_change = true })) + ) + end +) + +test.register_coroutine_test( + "Button pushed_3x message should generate event", + function() + -- 0xFC12, 0x0000, 0x04 = pushed_3x + local attr_report = cluster_base.build_custom_report_attribute( + mock_device, + 0xFC12, + 0x0000, + 0x20, -- Uint8 + data_types.Uint8(0x04) + ) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.pushed_3x({ state_change = true })) + ) + end +) + +test.register_coroutine_test( + "Button 2 pushed message should generate event on button2 component", + function() + -- Endpoint 2 test + local attr_report = cluster_base.build_custom_report_attribute( + mock_device, + 0xFC12, + 0x0000, + 0x20, -- Uint8 + data_types.Uint8(0x01) + ) + -- Modify endpoint to 2 + attr_report.address_header.src_endpoint.value = 2 + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true })) + ) + end +) + +test.register_coroutine_test( + "Battery percentage report should generate event", + function() + -- 0x0001 PowerConfiguration, 0x0021 BatteryPercentageRemaining + -- Driver logic: math.floor(value / 2) + local battery_report = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 180) -- 180/2 = 90% + + test.socket.zigbee:__queue_receive({ mock_device.id, battery_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(90)) + ) + end +) -return test \ No newline at end of file +return test From df3c5470cff1e90fa4b0bbb9308126f85633e8c8 Mon Sep 17 00:00:00 2001 From: West Zhao <55424074+Oniums@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:34:39 +0800 Subject: [PATCH 4/8] Update copyright year from 2022 to 2026,remove remove whitespace-only lines Update copyright year from 2022 to 2026,remove remove whitespace-only lines --- .../src/test/test_sonoff_button.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua index e4d2495416..b6277e9908 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- 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. @@ -64,7 +64,7 @@ test.register_coroutine_test( "added lifecycle event", function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - + -- Check initial events for button 1 test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -142,7 +142,7 @@ test.register_coroutine_test( mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, clusters.PowerConfiguration.ID) }) - + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -158,7 +158,7 @@ test.register_coroutine_test( 0x20, -- Uint8 data_types.Uint8(0x01) ) - + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) test.socket.capability:__expect_send( mock_device:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) @@ -177,7 +177,7 @@ test.register_coroutine_test( 0x20, -- Uint8 data_types.Uint8(0x02) ) - + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) test.socket.capability:__expect_send( mock_device:generate_test_message("button1", capabilities.button.button.double({ state_change = true })) @@ -196,7 +196,7 @@ test.register_coroutine_test( 0x20, -- Uint8 data_types.Uint8(0x03) ) - + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) test.socket.capability:__expect_send( mock_device:generate_test_message("button1", capabilities.button.button.held({ state_change = true })) @@ -215,7 +215,7 @@ test.register_coroutine_test( 0x20, -- Uint8 data_types.Uint8(0x04) ) - + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) test.socket.capability:__expect_send( mock_device:generate_test_message("button1", capabilities.button.button.pushed_3x({ state_change = true })) @@ -250,7 +250,7 @@ test.register_coroutine_test( -- 0x0001 PowerConfiguration, 0x0021 BatteryPercentageRemaining -- Driver logic: math.floor(value / 2) local battery_report = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 180) -- 180/2 = 90% - + test.socket.zigbee:__queue_receive({ mock_device.id, battery_report }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(90)) From bce843b0f7cef5ad84a26bbc8899704e80da9067 Mon Sep 17 00:00:00 2001 From: West Zhao <55424074+Oniums@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:39:56 +0800 Subject: [PATCH 5/8] remove log.info remove log.info --- .../zigbee-button/src/zigbee-multi-button/sonoff/init.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua index c7ed8c4564..a3ce72ebbb 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -34,7 +34,6 @@ end local function battery_attr_handler(driver, device, value, zb_rx) local percent = math.floor((value.value or 0) / 2) device:emit_event(capabilities.battery.battery(percent)) - log.info(string.format("Battery percentage remaining: %d%%", percent)) end local function sonoff_attr_handler(driver, device, value, zb_rx) @@ -42,7 +41,6 @@ local function sonoff_attr_handler(driver, device, value, zb_rx) local endpoint = zb_rx.address_header.src_endpoint.value local button_name = "button" .. tostring(endpoint) local event_func = EVENT_MAP[attr_val] - log.info(string.format("SONOFF attr: endpoint=%s, value=%s, button=%s", endpoint, attr_val, button_name)) if event_func then local comp = device.profile.components[button_name] if comp then From e929ca4c4d8e370092565d2e728ecb145266bbfa Mon Sep 17 00:00:00 2001 From: Oniums <237791949@qq.com> Date: Wed, 25 Feb 2026 18:38:17 +0800 Subject: [PATCH 6/8] Use defaults for Sonoff battery and added lifecycle - Remove custom battery attribute handler and added lifecycle in Sonoff subdriver - Align Sonoff tests with default added behavior and keep battery report coverage --- .../src/test/test_sonoff_button.lua | 30 ++----------------- .../src/zigbee-multi-button/sonoff/init.lua | 26 +--------------- 2 files changed, 4 insertions(+), 52 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua index b6277e9908..aa8567e2ba 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -63,6 +63,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "added lifecycle event", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) -- Check initial events for button 1 @@ -78,9 +79,6 @@ test.register_coroutine_test( capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("button1", capabilities.button.button.pushed({ state_change = false })) - ) -- Check initial events for button 2 test.socket.capability:__expect_send( @@ -95,9 +93,6 @@ test.register_coroutine_test( capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = false })) - ) -- Check initial events for button 3 test.socket.capability:__expect_send( @@ -112,9 +107,6 @@ test.register_coroutine_test( capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("button3", capabilities.button.button.pushed({ state_change = false })) - ) -- Check initial events for button 4 test.socket.capability:__expect_send( @@ -129,21 +121,7 @@ test.register_coroutine_test( capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("button4", capabilities.button.button.pushed({ state_change = false })) - ) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zigbee:__expect_send({ - mock_device.id, - clusters.PowerConfiguration.attributes.BatteryPercentageRemaining: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, clusters.PowerConfiguration.ID) - }) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() end ) @@ -247,9 +225,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Battery percentage report should generate event", function() - -- 0x0001 PowerConfiguration, 0x0021 BatteryPercentageRemaining - -- Driver logic: math.floor(value / 2) - local battery_report = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 180) -- 180/2 = 90% + local battery_report = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 180) test.socket.zigbee:__queue_receive({ mock_device.id, battery_report }) test.socket.capability:__expect_send( diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua index a3ce72ebbb..d73b125f38 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -13,12 +13,10 @@ -- limitations under the License. local capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" local log = require "log" local SONOFF_CLUSTER_ID = 0xFC12 local SONOFF_ATTR_ID = 0x0000 -local BatteryPercentageRemaining = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining local EVENT_MAP = { [0x01] = capabilities.button.button.pushed, @@ -31,11 +29,6 @@ local function can_handle(opts, driver, device, ...) return device:get_manufacturer() == "SONOFF" end -local function battery_attr_handler(driver, device, value, zb_rx) - local percent = math.floor((value.value or 0) / 2) - device:emit_event(capabilities.battery.battery(percent)) -end - local function sonoff_attr_handler(driver, device, value, zb_rx) local attr_val = value.value local endpoint = zb_rx.address_header.src_endpoint.value @@ -53,32 +46,15 @@ local function sonoff_attr_handler(driver, device, value, zb_rx) end end -local function added_handler(self, device) - device:configure() - for _, comp in pairs(device.profile.components) do - if comp.id ~= "main" then - device:emit_component_event(comp, capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = { displayed = false }})) - device:emit_component_event(comp, capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_component_event(comp, capabilities.button.button.pushed({state_change = false})) - end - end -end - local sonoff_handler = { NAME = "SONOFF Multi-Button Handler", zigbee_handlers = { attr = { [SONOFF_CLUSTER_ID] = { [SONOFF_ATTR_ID] = sonoff_attr_handler - }, - [clusters.PowerConfiguration.ID] = { - [BatteryPercentageRemaining.ID] = battery_attr_handler - }, + } } }, - lifecycle_handlers = { - added = added_handler - }, can_handle = can_handle } From 9aeec232b06d8dd043fe649a6f3503a2bd7964c1 Mon Sep 17 00:00:00 2001 From: Oniums <237791949@qq.com> Date: Sat, 28 Feb 2026 10:22:37 +0800 Subject: [PATCH 7/8] remove log --- .../zigbee-button/src/zigbee-multi-button/sonoff/init.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua index d73b125f38..e3972264cf 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -13,7 +13,6 @@ -- limitations under the License. local capabilities = require "st.capabilities" -local log = require "log" local SONOFF_CLUSTER_ID = 0xFC12 local SONOFF_ATTR_ID = 0x0000 @@ -38,11 +37,7 @@ local function sonoff_attr_handler(driver, device, value, zb_rx) local comp = device.profile.components[button_name] if comp then device:emit_component_event(comp, event_func({state_change = true})) - else - log.warn("Unknown button component: " .. button_name) end - else - log.warn("Unknown event value: " .. tostring(attr_val)) end end From 61d1077966acc1116db41408ce58d00715964065 Mon Sep 17 00:00:00 2001 From: Oniums <237791949@qq.com> Date: Fri, 13 Mar 2026 13:44:43 +0800 Subject: [PATCH 8/8] Fix Sonoff multi-button sub-driver registration and test execution --- .../src/test/test_sonoff_button.lua | 2 +- .../src/zigbee-multi-button/fingerprints.lua | 1 + .../zigbee-multi-button/sonoff/can_handle.lua | 27 +++++++++++++++++++ .../sonoff/fingerprints.lua | 19 +++++++++++++ .../src/zigbee-multi-button/sonoff/init.lua | 6 +---- .../src/zigbee-multi-button/sub_drivers.lua | 1 + 6 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua index aa8567e2ba..61334d3c39 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -234,4 +234,4 @@ test.register_coroutine_test( end ) -return test +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua index cf3903152d..05ef834306 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua @@ -36,6 +36,7 @@ local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { { mfr = "Vimar", model = "RemoteControl_v1.0" }, { mfr = "Linxura", model = "Smart Controller" }, { mfr = "Linxura", model = "Aura Smart Button" }, + { mfr = "SONOFF", model = "SNZB-01M" }, { mfr = "zunzunbee", model = "SSWZ8T" } } diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua new file mode 100644 index 0000000000..e71cc32170 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua @@ -0,0 +1,27 @@ +-- 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. + +local function sonoff_can_handle(opts, driver, device, ...) + local fingerprints = require("zigbee-multi-button.sonoff.fingerprints") + + for _, fingerprint in ipairs(fingerprints) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.sonoff") + end + end + + return false +end + +return sonoff_can_handle \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua new file mode 100644 index 0000000000..0b3332dcff --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua @@ -0,0 +1,19 @@ +-- 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. + +local SONOFF_FINGERPRINTS = { + { mfr = "SONOFF", model = "SNZB-01M" } +} + +return SONOFF_FINGERPRINTS \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua index e3972264cf..4384f23ea4 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -24,10 +24,6 @@ local EVENT_MAP = { [0x04] = capabilities.button.button.pushed_3x } -local function can_handle(opts, driver, device, ...) - return device:get_manufacturer() == "SONOFF" -end - local function sonoff_attr_handler(driver, device, value, zb_rx) local attr_val = value.value local endpoint = zb_rx.address_header.src_endpoint.value @@ -50,7 +46,7 @@ local sonoff_handler = { } } }, - can_handle = can_handle + can_handle = require("zigbee-multi-button.sonoff.can_handle") } return sonoff_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua index d8d3611ba3..bd7828c466 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua @@ -6,6 +6,7 @@ local sub_drivers = { lazy_load_if_possible("zigbee-multi-button.ikea"), lazy_load_if_possible("zigbee-multi-button.somfy"), lazy_load_if_possible("zigbee-multi-button.ecosmart"), + lazy_load_if_possible("zigbee-multi-button.sonoff"), lazy_load_if_possible("zigbee-multi-button.centralite"), lazy_load_if_possible("zigbee-multi-button.adurosmart"), lazy_load_if_possible("zigbee-multi-button.heiman"),