diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 61285448f7..c70cae5eac 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -24,6 +24,16 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.remote.b286acn03 deviceProfileName: aqara-double-buttons + - id: "LUMI/lumi.remote.b18ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Single Rocker) + manufacturer: LUMI + model: lumi.remote.b18ac1 + deviceProfileName: aqara-single-button-mode + - id: "LUMI/lumi.remote.b28ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Double Rocker) + manufacturer: LUMI + model: lumi.remote.b28ac1 + deviceProfileName: aqara-double-buttons-mode - id: "HEIMAN/SOS-EM" deviceLabel: HEIMAN Button manufacturer: HEIMAN diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml new file mode 100644 index 0000000000..f19f19da57 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml @@ -0,0 +1,35 @@ +name: aqara-double-buttons-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + 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: all + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml new file mode 100644 index 0000000000..904de72281 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml @@ -0,0 +1,17 @@ +name: aqara-single-button-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 41dfdbf467..80147bef04 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -19,11 +19,15 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local button_utils = require "button_utils" +local MODE = "devicemode" +local MODE_CHANGE = "stse.allowOperationModeChange" +local SUPPORTED_BUTTON = { { "pushed" }, { "pushed", "held", "double" } } local PowerConfiguration = clusters.PowerConfiguration local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 local MFG_CODE = 0x115F local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 @@ -34,7 +38,9 @@ local FINGERPRINTS = { ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1 }, ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1 }, ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 } + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 }, + ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1 }, + ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3 } } local configuration = { @@ -101,6 +107,36 @@ local function battery_level_handler(driver, device, value, zb_rx) end end +local function mode_switching_handler(driver, device, value, zb_rx) + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local allow = device.preferences[MODE_CHANGE] or false + if allow then + local mode = device:get_field(MODE) or 1 + mode = 3 - mode + device:set_field(MODE, mode, { persist = true }) + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode)) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, + capabilities.button.button.pushed({ state_change = false })) + if btn_evt_cnt > 1 then + for i = 1, btn_evt_cnt do + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.numberOfButtons({ value = 1 })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, COMP_LIST[i], capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) + end + end + end +end + local is_aqara_products = function(opts, driver, device) local isAqaraProducts = false if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then @@ -120,8 +156,18 @@ end local function added_handler(self, device) local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 - - device:emit_event(capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + local mode = device:get_field(MODE) or 0 + local model = device:get_model() + + if mode == 0 then + if model == "lumi.remote.b18ac1" or model == "lumi.remote.b28ac1" then + mode = 1 + else + mode = 2 + end + end + device:set_field(MODE, mode, { persist = true }) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], { visibility = { displayed = false } })) device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, @@ -133,7 +179,7 @@ local function added_handler(self, device) if btn_evt_cnt > 1 then for i = 1, btn_evt_cnt do device:emit_component_event(device.profile.components[COMP_LIST[i]], - capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], { visibility = { displayed = false } })) device:emit_component_event(device.profile.components[COMP_LIST[i]], capabilities.button.numberOfButtons({ value = 1 })) @@ -177,6 +223,9 @@ local aqara_wireless_switch_handler = { }, [PowerConfiguration.ID] = { [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_level_handler + }, + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_ATTRIBUTE_ID_ALIVE] = mode_switching_handler } } }, diff --git a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua index 1d7b6d090c..6996f05096 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -28,6 +28,8 @@ 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_ALIVE = 0x00F7 +local MODE_CHANGE = "stse.allowOperationModeChange" local COMP_LIST = { "button1", "button2", "all" } local mock_device_e1 = test.mock_device.build_test_zigbee_device( @@ -44,9 +46,9 @@ local mock_device_e1 = test.mock_device.build_test_zigbee_device( } ) -local mock_device_t1_double_rocker = test.mock_device.build_test_zigbee_device( +local mock_device_h1_double_rocker = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("aqara-double-buttons.yml"), + profile = t_utils.get_profile_definition("aqara-double-buttons-mode.yml"), zigbee_endpoints = { [1] = { id = 1, @@ -61,7 +63,7 @@ local mock_device_t1_double_rocker = test.mock_device.build_test_zigbee_device( 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_double_rocker) + test.mock_device.add_test_device(mock_device_h1_double_rocker) end test.set_test_init_function(test_init) @@ -69,28 +71,27 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle - T1 double rocker", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "added" }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "added" }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.battery.normal())) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.type("CR2032"))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.quantity(1))) for i = 1, 3 do - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.numberOfButtons({ value = 1 }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.button.pushed({ state_change = false }))) - end - + end end ) @@ -118,8 +119,8 @@ test.register_coroutine_test( 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, - data_types.Uint8, 2) }) - mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + data_types.Uint8, 2) }) + mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -127,31 +128,31 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle doConfigure lifecycle -- t1", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "doConfigure" }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_t1_double_rocker, 30, 3600, 1) + mock_device_h1_double_rocker.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_h1_double_rocker, 30, 3600, 1) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, MULTISTATE_INPUT_CLUSTER_ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attr_config(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attr_config(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) }) - test.socket.zigbee:__expect_send({ mock_device_t1_double_rocker.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_t1_double_rocker, PRIVATE_CLUSTER_ID, + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, + cluster_base.write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, - data_types.Uint8, 1) }) - mock_device_t1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + data_types.Uint8, 1) }) + mock_device_h1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -162,14 +163,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", - capabilities.button.button.pushed({state_change = true}))) end ) @@ -180,14 +181,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.double({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.double({ state_change = true }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", - capabilities.button.button.double({state_change = true}))) end ) @@ -198,13 +199,13 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", - capabilities.button.button.held({state_change = true}))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.held({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.held({ state_change = true }))) end ) @@ -254,4 +255,45 @@ test.register_message_test( } } ) + +test.register_coroutine_test( + "Wireless Remote Switch H1 Mode Change", + function() + local mode = 2 + local updates = { + preferences = { + [MODE_CHANGE] = true + } + } + test.socket.device_lifecycle:__queue_receive(mock_device_h1_double_rocker:generate_info_changed(updates)) + mock_device_h1_double_rocker:set_field("devicemode", 1, { persist = true }) + local attr_report_data = { + { PRIVATE_ATTRIBUTE_ID_ALIVE, data_types.OctetString.ID, "\x01\x21\xB8\x0B\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x45\x08\x06\x24\x07\x00\x00\x00\x00\x08\x21\x15\x01\x0A\x21\xF5\x65\x0C\x20\x01\x64\x20\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, attr_report_data, + MFG_CODE) + }) + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, cluster_base + .write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode) }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + + for i = 1, 3 do + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.button.pushed({ state_change = false }))) + end + end +) + test.run_registered_tests()