Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1329c3f
Add Sonoff SNZB-01M Smart Scene Button into zigbee-button.
Oniums Oct 30, 2025
f1e199d
modify Copyright Date
Oniums Feb 4, 2026
c0ae8b7
modify test_sonoff.py fix PR error
Oniums Feb 4, 2026
df3c547
Update copyright year from 2022 to 2026,remove remove whitespace-only…
Oniums Feb 6, 2026
bce843b
remove log.info
Oniums Feb 6, 2026
e929ca4
Use defaults for Sonoff battery and added lifecycle
Oniums Feb 25, 2026
9aeec23
remove log
Oniums Feb 28, 2026
61d1077
Fix Sonoff multi-button sub-driver registration and test execution
Oniums Mar 13, 2026
d487ce0
update copyright sections for sonoff multi-button
Oniums Mar 27, 2026
21d7877
Merge branch 'SmartThingsCommunity:main' into main
Oniums Apr 8, 2026
f465c2f
fix: address review feedback for Sonoff SNZB-01M button support
Oniums Apr 8, 2026
70dd9ec
fix: resolve Sonoff SNZB-01M unit test failures
Oniums Apr 13, 2026
14c276c
Merge branch 'SmartThingsCommunity:main' into main
Oniums Apr 19, 2026
e025d91
Merge branch 'main' into main
Oniums May 7, 2026
51656f3
Merge branch 'main' into main
Oniums May 9, 2026
b38c373
Merge branch 'main' into main
Oniums May 18, 2026
4cb461d
Removed the SONOFF entry from the generic supported values table and …
Oniums May 18, 2026
a880b84
Apply suggestion from @KKlimczukS
Oniums May 18, 2026
dda1089
Apply suggestion from @KKlimczukS
Oniums May 18, 2026
9597339
Apply suggestion from @KKlimczukS
Oniums May 18, 2026
bc400dd
Apply suggestion from @KKlimczukS
Oniums May 18, 2026
0ba3aa0
Merge branch 'main' into main
Oniums May 19, 2026
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 @@ -267,6 +267,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: four-buttons-battery
- id: "MultIR/MIR-SO100"
deviceLabel: MultiIR Smart button MIR-SO100
manufacturer: MultIR
Expand Down
279 changes: 279 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,279 @@
-- 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 zb_const = require "st.zigbee.constants"
local messages = require "st.zigbee.messages"
local data_types = require "st.zigbee.data_types"
local zcl_messages = require "st.zigbee.zcl"
local report_attr = require "st.zigbee.zcl.global_commands.report_attribute"

local SONOFF_PRIVATE_BUTTON_CLUSTER = 0xFC12
local SONOFF_PRIVATE_ATTR = 0x0000

local mock_device = test.mock_device.build_test_zigbee_device(
{
profile = t_utils.get_profile_definition("four-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 build_test_attr_report(device, endpoint, value)
local report_body = report_attr.ReportAttribute({
report_attr.ReportAttributeAttributeRecord(SONOFF_PRIVATE_ATTR, data_types.Uint8.ID, value)
})
local zclh = zcl_messages.ZclHeader({
cmd = data_types.ZCLCommandId(report_body.ID)
})
local addrh = messages.AddressHeader(
device:get_short_address(),
endpoint,
zb_const.HUB.ADDR,
zb_const.HUB.ENDPOINT,
zb_const.HA_PROFILE_ID,
SONOFF_PRIVATE_BUTTON_CLUSTER
)
local message_body = zcl_messages.ZclMessageBody({
zcl_header = zclh,
zcl_body = report_body
})

return messages.ZigbeeMessageRx({
address_header = addrh,
body = message_body
})
end

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.capability:__expect_send(
mock_device:generate_test_message(
"main",
capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } })
)
)
test.socket.capability:__expect_send(
mock_device:generate_test_message(
"main",
capabilities.button.numberOfButtons({ value = 4 }, { visibility = { displayed = false } })
)
)

-- 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.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))
)

test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
test.wait_for_events()
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Button pushed message should generate event",
function()
-- 0xFC12, 0x0000, 0x01 = pushed
local attr_report = build_test_attr_report(mock_device, 1, 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 }))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))
)
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Button double message should generate event",
function()
-- 0xFC12, 0x0000, 0x02 = double
local attr_report = build_test_attr_report(mock_device, 1, 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 }))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.button.button.double({ state_change = true }))
)
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Button held message should generate event",
function()
-- 0xFC12, 0x0000, 0x03 = held
local attr_report = build_test_attr_report(mock_device, 1, 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 }))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.button.button.held({ state_change = true }))
)
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Button pushed_3x message should generate event",
function()
-- 0xFC12, 0x0000, 0x04 = pushed_3x
local attr_report = build_test_attr_report(mock_device, 1, 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 }))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.button.button.pushed_3x({ state_change = true }))
)
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Button 2 pushed message should generate event on button2 component",
function()
-- Endpoint 2 test
local attr_report = build_test_attr_report(mock_device, 2, data_types.Uint8(0x01))

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 }))
)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))
)
end,
{
min_api_version = 17
}
)

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,
{
min_api_version = 17
}
)

test.run_registered_tests()
Comment thread
Oniums marked this conversation as resolved.

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
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,63 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local button_utils = require "button_utils"

local SONOFF_CLUSTER_ID = 0xFC12
local SONOFF_ATTR_ID = 0x0000
local SONOFF_SUPPORTED_BUTTON_VALUES = { "pushed", "double", "held", "pushed_3x" }
local SONOFF_NUMBER_OF_BUTTONS = 4

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 added_handler(self, device)
for _, component in pairs(device.profile.components) do
local number_of_buttons = component.id == "main" and SONOFF_NUMBER_OF_BUTTONS or 1
device:emit_component_event(component,
capabilities.button.supportedButtonValues(SONOFF_SUPPORTED_BUTTON_VALUES, { visibility = { displayed = false } }))
device:emit_component_event(component,
capabilities.button.numberOfButtons({ value = number_of_buttons }, { visibility = { displayed = false } }))
end

button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button,
capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false }))
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]
if event_func then
local comp = device.profile.components[button_name]
if comp then
local event = event_func({ state_change = true })
device:emit_component_event(comp, event)
device:emit_event(event)
end
end
end

local sonoff_handler = {
NAME = "SONOFF Multi-Button Handler",
lifecycle_handlers = {
added = added_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
Comment thread
Oniums marked this conversation as resolved.

Loading
Loading