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..3ffab5685d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -0,0 +1,226 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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" + +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 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "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 + 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 } }) + ) + ) + + -- 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 } }) + ) + ) + + -- 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 } }) + ) + ) + + -- 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.wait_for_events() + 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() + 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( + mock_device:generate_test_message("main", capabilities.battery.battery(90)) + ) + end +) + +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/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/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua new file mode 100644 index 0000000000..18ea8915ba --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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..8301ae9965 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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 new file mode 100644 index 0000000000..f53dbc2b3c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -0,0 +1,41 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +local SONOFF_CLUSTER_ID = 0xFC12 +local SONOFF_ATTR_ID = 0x0000 + +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 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] + if event_func then + local comp = device.profile.components[button_name] + if comp then + device:emit_component_event(comp, event_func({state_change = true})) + end + end +end + +local sonoff_handler = { + NAME = "SONOFF Multi-Button Handler", + zigbee_handlers = { + attr = { + [SONOFF_CLUSTER_ID] = { + [SONOFF_ATTR_ID] = sonoff_attr_handler + } + } + }, + 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"), 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 } }