Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions drivers/SmartThings/zigbee-button/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
226 changes: 226 additions & 0 deletions drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: newline in this file and all the other files.

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +122 to +128
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you overwrote added in your handler (because you did not include the button capability on your main component, this info is never used.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I checked the existing profiles: four-buttons-without-main-button has no battery capability, while four-buttons-battery includes button on main, which doesn’t match the SNZB-01M capability layout. Would you prefer that I: use four-buttons-without-main-button and accept no battery on main, or use the profile I added earlier.

Copy link
Copy Markdown
Contributor

@greens greens Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you've added the new profile, but I'll advise you on why our multi-button profiles include the button capability on the main component: without it (and duplicating any other buttons' events to main, the remote on the device list will not change to indicate button presses. You'll only see them within the device details page.

I didn't notice four-buttons-without-main-button didn't have battery. I definitely think you should maintain that.

So you can go with your new profile if you like, but this comment was merely pointing out that, because these values are only used in the zigbee-multi-button added handler, your additions to this file are not used.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll advise you on why our multi-button profiles include the button capability on the main component: without it (and duplicating any other buttons' events to main, the remote on the device list will not change to indicate button presses. You'll only see them within the device details page.

It seems that you are going to go forward with the profile as is. I am commenting to make sure you have tested the device on the platform to ensure it works the way you want, since this is different than what we have for our multibutton profiles. We recommend having the button capability on the main component, and duplicating events for any button press to that component also, so that in the device list view of the app, you can see the events affect the device rather than in just the details view. Doing so also gives users a hook for routines that will trigger on any button press of the remote.

}
}

Expand Down
Loading