-
Notifications
You must be signed in to change notification settings - Fork 529
WWSTCERT-10701 Add Sonoff SNZB-01M Smart Scene Button into zigbee-button. #2510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1329c3f
f1e199d
c0ae8b7
df3c547
bce843b
e929ca4
9aeec23
61d1077
d487ce0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 |
|---|---|---|
| @@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you overwrote
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I didn't notice 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. |
||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.