Skip to content

Aqara Wireless Switch H1 #1993

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
10 changes: 10 additions & 0 deletions drivers/SmartThings/zigbee-button/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ zigbeeManufacturer:
manufacturer: LUMI
model: lumi.remote.acn003
deviceProfileName: one-button-battery
- id: "LUMI/lumi.remote.b18ac1"
deviceLabel: Aqara Wireless Remote Switch H1 (Single Rocker)
manufacturer: LUMI
model: lumi.remote.b18ac1
deviceProfileName: one-button-battery
- id: "LUMI/lumi.remote.b28ac1"
deviceLabel: Aqara Wireless Remote Switch H1 (Double Rocker)
manufacturer: LUMI
model: lumi.remote.b28ac1
deviceProfileName: two-buttons-battery
- id: "HEIMAN/SOS-EM"
deviceLabel: HEIMAN Button
manufacturer: HEIMAN
Expand Down
112 changes: 61 additions & 51 deletions drivers/SmartThings/zigbee-button/src/aqara/init.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- Copyright 2024 SmartThings
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
Expand All @@ -17,20 +17,22 @@ local clusters = require "st.zigbee.zcl.clusters"
local cluster_base = require "st.zigbee.cluster_base"
local data_types = require "st.zigbee.data_types"
local capabilities = require "st.capabilities"

local supported_values = require "zigbee-multi-button.supported_values"

local PowerConfiguration = clusters.PowerConfiguration
local PRIVATE_CLUSTER_ID = 0xFCC0
local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009
local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125
local MFG_CODE = 0x115F

local PRIVATE_CLUSTER_ID = 0xFCC0
local PRIVATE_ATTRIBUTE_ID = 0x0009
local MULTISTATE_INPUT_ATTRIBUTE_ID = 0x0125
local MULTISTATE_INPUT_CLUSTER_ID = 0x0012
local PRESENT_ATTRIBUTE_ID = 0x0055
local MFG_CODE = 0x115F

local FINGERPRINTS = {
{ mfr = "LUMI", model = "lumi.remote.b1acn02" },
{ mfr = "LUMI", model = "lumi.remote.acn003" }
{ mfr = "LUMI", model = "lumi.remote.acn003" },
{ mfr = "LUMI", model = "lumi.remote.b18ac1" },
{ mfr = "LUMI", model = "lumi.remote.b28ac1" }
}

local configuration = {
Expand All @@ -53,72 +55,80 @@ local configuration = {
}

local function present_value_attr_handler(driver, device, value, zb_rx)
if value.value == 1 then
device:emit_event(capabilities.button.button.pushed({state_change = true}))
elseif value.value == 2 then
device:emit_event(capabilities.button.button.double({state_change = true}))
elseif value.value == 0 then
device:emit_event(capabilities.button.button.held({state_change = true}))
end
local src_endpoint = zb_rx.address_header.src_endpoint.value
local event_map = { [1] = "pushed", [2] = "double", [0] = "held" }
local event_value = event_map[value.value]
if not event_value then return end
device:emit_component_event(device.profile.components.main, capabilities.button.button[event_value]({ state_change = true }))
if device:get_model() == "lumi.remote.b28ac1" and src_endpoint == 1 then
device:emit_component_event(device.profile.components.button1, capabilities.button.button[event_value]({ state_change = true }))
elseif src_endpoint == 2 then
device:emit_component_event(device.profile.components.button2, capabilities.button.button[event_value]({ state_change = true }))
end
end

local is_aqara_products = function(opts, driver, device)
for _, fingerprint in ipairs(FINGERPRINTS) do
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
return true
end
for _, fingerprint in ipairs(FINGERPRINTS) do
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
return true
end
return false
end
return false
end

local function device_init(driver, device)
battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device)
if configuration ~= nil then
for _, attribute in ipairs(configuration) do
device:add_configured_attribute(attribute)
device:add_monitored_attribute(attribute)
end
battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device)
if configuration ~= nil then
for _, attribute in ipairs(configuration) do
device:add_configured_attribute(attribute)
device:add_monitored_attribute(attribute)
end
end
end

local function added_handler(self, device)
device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))
device:emit_event(capabilities.button.numberOfButtons({value = 1}))
device:emit_event(capabilities.button.button.pushed({state_change = false}))
device:emit_event(capabilities.battery.battery(100))
local config = supported_values.get_device_parameters(device)
for _, component in pairs(device.profile.components) do
local number_of_buttons = component.id == "main" and config.NUMBER_OF_BUTTONS or 1
if config ~= nil then
device:emit_component_event(component, capabilities.button.supportedButtonValues(config.SUPPORTED_BUTTON_VALUES), {visibility = { displayed = false }})
else
device:emit_component_event(component, capabilities.button.supportedButtonValues({"pushed", "held", "double"}, {visibility = { displayed = false }}))
end
device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons}))
end
device:emit_event(capabilities.button.button.pushed({state_change = false}))
device:emit_event(capabilities.battery.battery(100))
end

local function do_configure(driver, device)
device:configure()
if device:get_model() == "lumi.remote.b1acn02" then
if device:get_model() == "lumi.remote.acn003" then
device:send(cluster_base.write_manufacturer_specific_attribute(device,
PRIVATE_CLUSTER_ID, MULTISTATE_INPUT_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 2))
else
device:send(cluster_base.write_manufacturer_specific_attribute(device,
PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, data_types.Uint8, 1))
elseif device:get_model() == "lumi.remote.acn003" then
PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1))
device:send(cluster_base.write_manufacturer_specific_attribute(device,
PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, data_types.Uint8, 2))
PRIVATE_CLUSTER_ID, MULTISTATE_INPUT_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 2))
end
-- when the wireless switch T1 accesses the network, the gateway sends
-- private attribute 0009 to make the device no longer distinguish
-- between the standard gateway and the aqara gateway.
-- When wireless switch E1 is connected to the network, the gateway sends
-- private attribute 0125 to enable the device to send double-click and long-press packets.
end

local aqara_wireless_switch_handler = {
NAME = "Aqara Wireless Switch Handler",
lifecycle_handlers = {
init = device_init,
added = added_handler,
doConfigure = do_configure
},
zigbee_handlers = {
attr = {
[MULTISTATE_INPUT_CLUSTER_ID] = {
[PRESENT_ATTRIBUTE_ID] = present_value_attr_handler
}
NAME = "Aqara Wireless Switch Handler",
lifecycle_handlers = {
init = device_init,
added = added_handler,
doConfigure = do_configure
},
zigbee_handlers = {
attr = {
[MULTISTATE_INPUT_CLUSTER_ID] = {
[PRESENT_ATTRIBUTE_ID] = present_value_attr_handler
}
},
can_handle = is_aqara_products
}
},
can_handle = is_aqara_products
}

return aqara_wireless_switch_handler
134 changes: 95 additions & 39 deletions drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- Copyright 2024 SmartThings
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
Expand All @@ -19,15 +19,14 @@ local clusters = require "st.zigbee.zcl.clusters"
local cluster_base = require "st.zigbee.cluster_base"
local data_types = require "st.zigbee.data_types"


local MULTISTATE_INPUT_CLUSTER_ID = 0x0012
local PRESENT_ATTRIBUTE_ID = 0x0055
local PowerConfiguration = clusters.PowerConfiguration

local MFG_CODE = 0x115F
local PRIVATE_CLUSTER_ID = 0xFCC0
local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009
local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125
local PRIVATE_ATTRIBUTE_ID = 0x0009
local MULTISTATE_INPUT_ATTRIBUTE_ID = 0x0125
local MULTISTATE_INPUT_CLUSTER_ID = 0x0012
local PRESENT_ATTRIBUTE_ID = 0x55

local mock_device_e1 = test.mock_device.build_test_zigbee_device(
{
Expand Down Expand Up @@ -57,34 +56,67 @@ local mock_device_t1 = test.mock_device.build_test_zigbee_device(
}
)

local mock_device_h1 = test.mock_device.build_test_zigbee_device(
{
profile = t_utils.get_profile_definition("two-buttons-battery.yml"),
zigbee_endpoints = {
[1] = {
id = 1,
manufacturer = "LUMI",
model = "lumi.remote.b28ac1",
server_clusters = { 0x0001, 0x0012 }
},
[2] = {
id = 2,
server_clusters = { 0x0001, 0x0012 }
}
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()
local function test_init()
test.mock_device.add_test_device(mock_device_e1)
test.mock_device.add_test_device(mock_device_t1)
test.mock_device.add_test_device(mock_device_h1)
zigbee_test_utils.init_noop_health_check_timer()
end

test.set_test_init_function(test_init)

test.register_coroutine_test(
"Handle added lifecycle -- e1",
"Handle added lifecycle -- Single Rocker Switch (E1,T1,H1)",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" })
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})))
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"})))
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1})))
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.button.pushed({state_change = false})))
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100)))
end
)

test.register_coroutine_test(
"Handle added lifecycle -- t1",
"Handle added lifecycle -- Double Rocker Switch (H1)",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" })
test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})))
test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1})))
test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.button.pushed({state_change = false})))
test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100)))
test.socket.device_lifecycle:__queue_receive({ mock_device_h1.id, "added" })

for button_name, component in pairs(mock_device_h1.profile.components) do
local number_of_buttons = component.id == "main" and 2 or 1
test.socket.capability:__expect_send(
mock_device_h1:generate_test_message(
button_name,
capabilities.button.supportedButtonValues({ "pushed", "held", "double" })
)
)
test.socket.capability:__expect_send(
mock_device_h1:generate_test_message(
button_name,
capabilities.button.numberOfButtons({ value = number_of_buttons })
)
)
end
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("main", capabilities.button.button.pushed({state_change = false})))
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("main", capabilities.battery.battery(100)))
end
)

Expand All @@ -109,15 +141,14 @@ test.register_coroutine_test(
zigbee_test_utils.build_attr_config(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001)
})
test.socket.zigbee:__expect_send({ mock_device_e1.id,
cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE,
cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, MULTISTATE_INPUT_ATTRIBUTE_ID, MFG_CODE,
data_types.Uint8, 2) })
mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)


test.register_coroutine_test(
"Handle doConfigure lifecycle -- t1",
"Handle doConfigure lifecycle -- t1 h1",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "doConfigure" })
test.socket.zigbee:__expect_send({
Expand All @@ -137,54 +168,79 @@ test.register_coroutine_test(
zigbee_test_utils.build_attr_config(mock_device_t1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001)
})
test.socket.zigbee:__expect_send({ mock_device_t1.id,
cluster_base.write_manufacturer_specific_attribute(mock_device_t1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE,
cluster_base.write_manufacturer_specific_attribute(mock_device_t1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE,
data_types.Uint8, 1) })
test.socket.zigbee:__expect_send({ mock_device_t1.id,
cluster_base.write_manufacturer_specific_attribute(mock_device_t1, PRIVATE_CLUSTER_ID, MULTISTATE_INPUT_ATTRIBUTE_ID, MFG_CODE,
data_types.Uint8, 2) })
mock_device_t1:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)

test.register_coroutine_test(
"Reported button should be handled: pushed true",
"Single Rocker Switch (E1,T1,H1) Reported button should be handled: (pushed double held) true",
function()
local attr_report_data = {
local attr_report_data_pushed = {
{ PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 }
}
test.socket.zigbee:__queue_receive({
mock_device_e1.id,
zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE)
zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data_pushed, MFG_CODE)
})
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main",
capabilities.button.button.pushed({state_change = true})))
end
)

test.register_coroutine_test(
"Reported button should be handled: double true",
function()
local attr_report_data = {
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main",capabilities.button.button.pushed({state_change = true})))
test.wait_for_events()
local attr_report_data_double = {
{ PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 }
}
test.socket.zigbee:__queue_receive({
mock_device_e1.id,
zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE)
zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data_double, MFG_CODE)
})
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main",capabilities.button.button.double({state_change = true})))
test.wait_for_events()
local attr_report_data_held = {
{ PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 }
}
test.socket.zigbee:__queue_receive({
mock_device_e1.id,
zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data_held, MFG_CODE)
})
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main",
capabilities.button.button.double({state_change = true})))
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main",capabilities.button.button.held({state_change = true})))
end
)

test.register_coroutine_test(
"Reported button should be handled: held true",
"Double Rocker Switch (H1) Reported button should be handled: (pushed double held) true",
function()
local attr_report_data = {
local attr_report_data_pushed = {
{ PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 }
}
test.socket.zigbee:__queue_receive({
mock_device_h1.id,
zigbee_test_utils.build_attribute_report(mock_device_h1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data_pushed, MFG_CODE)
})
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("main",capabilities.button.button.pushed({state_change = true})))
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("button1", capabilities.button.button.pushed({state_change = true})))
test.wait_for_events()
local attr_report_data_double = {
{ PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 }
}
test.socket.zigbee:__queue_receive({
mock_device_h1.id,
zigbee_test_utils.build_attribute_report(mock_device_h1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data_double, MFG_CODE)
})
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("main",capabilities.button.button.double({state_change = true})))
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("button1", capabilities.button.button.double({state_change = true})))
test.wait_for_events()
local attr_report_data_held = {
{ PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 }
}
test.socket.zigbee:__queue_receive({
mock_device_e1.id,
zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE)
mock_device_h1.id,
zigbee_test_utils.build_attribute_report(mock_device_h1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data_held, MFG_CODE)
})
test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main",
capabilities.button.button.held({state_change = true})))
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("main",capabilities.button.button.held({state_change = true})))
test.socket.capability:__expect_send(mock_device_h1:generate_test_message("button1", capabilities.button.button.held({state_change = true})))
end
)

Expand Down
Loading
Loading