diff --git a/drivers/Aqara/aqara-cube/capabilities/cubeAction.yaml b/drivers/Aqara/aqara-cube/capabilities/cubeAction.yaml new file mode 100644 index 0000000000..fa2890e992 --- /dev/null +++ b/drivers/Aqara/aqara-cube/capabilities/cubeAction.yaml @@ -0,0 +1,29 @@ +id: stse.cubeAction +version: 1 +status: proposed +name: Cube Action +ephemeral: false +attributes: + cubeAction: + schema: + type: object + properties: + value: + title: ActionType + type: string + enum: + - noAction + - shake + - rotate + - pickUpAndHold + - flipToSide1 + - flipToSide2 + - flipToSide3 + - flipToSide4 + - flipToSide5 + - flipToSide6 + additionalProperties: false + required: + - value + enumCommands: [] +commands: {} diff --git a/drivers/Aqara/aqara-cube/capabilities/cubeFace.yaml b/drivers/Aqara/aqara-cube/capabilities/cubeFace.yaml new file mode 100644 index 0000000000..af7e7301f8 --- /dev/null +++ b/drivers/Aqara/aqara-cube/capabilities/cubeFace.yaml @@ -0,0 +1,25 @@ +id: stse.cubeFace +version: 1 +status: proposed +name: Cube Face +ephemeral: false +attributes: + cubeFace: + schema: + type: object + properties: + value: + title: CubeFace + type: string + enum: + - face1Up + - face2Up + - face3Up + - face4Up + - face5Up + - face6Up + additionalProperties: false + required: + - value + enumCommands: [] +commands: {} diff --git a/drivers/Aqara/aqara-cube/config.yml b/drivers/Aqara/aqara-cube/config.yml new file mode 100644 index 0000000000..bed52eb695 --- /dev/null +++ b/drivers/Aqara/aqara-cube/config.yml @@ -0,0 +1,6 @@ +name: 'Aqara Cube' +packageKey: 'aqara-cube' +permissions: + zigbee: {} +description: "SmartThings driver for Aqara Cube devices" +vendorSupportInformation: "https://www.aqara.com/en/support/" diff --git a/drivers/Aqara/aqara-cube/fingerprints.yml b/drivers/Aqara/aqara-cube/fingerprints.yml new file mode 100644 index 0000000000..33b530fc0e --- /dev/null +++ b/drivers/Aqara/aqara-cube/fingerprints.yml @@ -0,0 +1,6 @@ +zigbeeManufacturer: + - id: "Lumi/lumi.remote.cagl02" + deviceLabel: Aqara Cube T1 Pro + manufacturer: LUMI + model: lumi.remote.cagl02 + deviceProfileName: cube-t1-pro diff --git a/drivers/Aqara/aqara-cube/profiles/cube-t1-pro.yml b/drivers/Aqara/aqara-cube/profiles/cube-t1-pro.yml new file mode 100644 index 0000000000..de111cf939 --- /dev/null +++ b/drivers/Aqara/aqara-cube/profiles/cube-t1-pro.yml @@ -0,0 +1,19 @@ +name: cube-t1-pro +components: + - id: main + capabilities: + - id: stse.cubeAction + version: 1 + - id: stse.cubeFace + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController +metadata: + mnmn: SolutionsEngineering + vid: SmartThings-smartthings-Aqara_CubeT1Pro \ No newline at end of file diff --git a/drivers/Aqara/aqara-cube/src/init.lua b/drivers/Aqara/aqara-cube/src/init.lua new file mode 100644 index 0000000000..cde24f8f23 --- /dev/null +++ b/drivers/Aqara/aqara-cube/src/init.lua @@ -0,0 +1,133 @@ +local ZigbeeDriver = require "st.zigbee" +local capabilities = require "st.capabilities" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local clusters = require "st.zigbee.zcl.clusters" +local PowerConfiguration = clusters.PowerConfiguration + +local PRI_CLU = 0xFCC0 +local PRI_ATTR = 0x0009 +local MFG_CODE = 0x115F + +local ROTATE_CLU = 0x000C +local EVENT_CLU = 0x0012 +local FACE_ATTR = 0x0149 +local ACTION_ATTR = 0x0055 +local CUBE_MODE = 0x0148 + +local cubeAction = capabilities["stse.cubeAction"] +local cubeFace = capabilities["stse.cubeFace"] +local cubeFaceVal = { "face1Up", "face2Up", "face3Up", "face4Up", "face5Up", "face6Up" } +local cubeFlipToSideVal = { "flipToSide1", "flipToSide2", "flipToSide3", "flipToSide4", "flipToSide5", "flipToSide6" } + +local CUBEACTION_TIMER = "cubeAction_timer" +local CUBEACTION_TIME = 3 + +local callback_timer = function(device) + return function() + device:emit_event(cubeAction.cubeAction("noAction")) + end +end + +local function reset_thread(device) + local timer = device:get_field(CUBEACTION_TIMER) + if timer then + device.thread:cancel_timer(timer) + device:set_field(CUBEACTION_TIMER, nil) + end + device:set_field(CUBEACTION_TIMER, device.thread:call_with_delay(CUBEACTION_TIME, callback_timer(device))) +end + +local function data_handler(driver, device, value, zb_rx) + local val = value.value + if val == 0x0000 then -- Shake + reset_thread(device) + device:emit_event(cubeAction.cubeAction("shake")) + elseif val == 0x0004 then -- hold + reset_thread(device) + device:emit_event(cubeAction.cubeAction("pickUpAndHold")) + elseif val & 0x0400 == 0x0400 then -- Flip to side + local faceNum = val & 0x0007 + reset_thread(device) + device:emit_event(cubeAction.cubeAction(cubeFlipToSideVal[faceNum + 0x1])) + end +end + +local function rotate_handler(driver, device, value, zb_rx) + -- Rotation + reset_thread(device) + device:emit_event(cubeAction.cubeAction("rotate")) +end + +local function face_handler(driver, device, value, zb_rx) + local faceNum = value.value + device:emit_event(cubeFace.cubeFace(cubeFaceVal[faceNum + 1])) +end + +local function do_refresh(driver, device) + -- refresh + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) +end + +local function device_init(driver, device) + local power_configuration = { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30, + maximum_interval = 3600, + data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, + reportable_change = 1 + } + + battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) + + device:add_configured_attribute(power_configuration) + device:add_monitored_attribute(power_configuration) +end + +local function device_added(self, device) + -- Set private attribute + device:send(cluster_base.write_manufacturer_specific_attribute(device, + PRI_CLU, PRI_ATTR, MFG_CODE, data_types.Uint8, 1)) + + device:send(cluster_base.write_manufacturer_specific_attribute(device, + PRI_CLU, CUBE_MODE, MFG_CODE, data_types.Uint8, 1)) + device:emit_event(cubeAction.cubeAction("noAction")) + device:emit_event(cubeFace.cubeFace("face1Up")) + do_refresh(self, device) +end + +-- [[ register ]] +local aqara_cube_t1_pro_handler = { + NAME = "Aqara Cube T1 Pro", + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + attr = { + [EVENT_CLU] = { + [ACTION_ATTR] = data_handler + }, + [ROTATE_CLU] = { + [ACTION_ATTR] = rotate_handler, + }, + [PRI_CLU] = { + [FACE_ATTR] = face_handler + }, + [PowerConfiguration.ID] = { + [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_defaults.battery_volt_attr_handler + } + } + }, + lifecycle_handlers = { + init = device_init, + added = device_added + } +} + +local aqara_cube_t1_pro_driver = ZigbeeDriver("aqara_cube_t1_pro", aqara_cube_t1_pro_handler) +aqara_cube_t1_pro_driver:run() + diff --git a/drivers/Aqara/aqara-cube/src/test/test_aqara_cube_t1_pro.lua b/drivers/Aqara/aqara-cube/src/test/test_aqara_cube_t1_pro.lua new file mode 100644 index 0000000000..f574cd0e3e --- /dev/null +++ b/drivers/Aqara/aqara-cube/src/test/test_aqara_cube_t1_pro.lua @@ -0,0 +1,170 @@ +local test = require "integration_test" +local cluster_base = require "st.zigbee.cluster_base" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local capabilities = require "st.capabilities" +local data_types = require "st.zigbee.data_types" +local clusters = require "st.zigbee.zcl.clusters" +local PowerConfiguration = clusters.PowerConfiguration + +local PRI_CLU = 0xFCC0 +local PRI_ATTR = 0x0009 +local MFG_CODE = 0x115F + +local ROTATE_CLU = 0x000C +local EVENT_CLU = 0x0012 +local FACE_ATTR = 0x0149 +local ACTION_ATTR = 0x0055 +local CUBE_MODE = 0x0148 + +local cubeAction = capabilities["stse.cubeAction"] +local cubeFace = capabilities["stse.cubeFace"] +test.add_package_capability("cubeAction.yaml") +test.add_package_capability("cubeFace.yaml") + +local CUBEACTION_TIME = 3 + + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("cube-t1-pro.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.remote.cagl02", + server_clusters = { PRI_CLU } + } + } + } +) + +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( + "lifecycle - added test", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + -- private protocol enable + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRI_CLU, PRI_ATTR, MFG_CODE, + data_types.Uint8, 1) }) + -- init + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRI_CLU, CUBE_MODE, MFG_CODE, + data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", cubeAction.cubeAction("noAction"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", cubeFace.cubeFace("face1Up"))) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) + end +) + +test.register_coroutine_test( + "capability - refresh", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) + end +) + +test.register_coroutine_test( + "data_handler test - shake", + function() + local attr_report_data = { + { ACTION_ATTR, data_types.Uint16.ID, 0x0000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, EVENT_CLU, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("shake"))) + test.timer.__create_and_queue_test_time_advance_timer(CUBEACTION_TIME, "oneshot") + test.mock_time.advance_time(CUBEACTION_TIME) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("noAction"))) + end +) + +test.register_coroutine_test( + "data_handler test - pick up and hold", + function() + local attr_report_data = { + { ACTION_ATTR, data_types.Uint16.ID, 0x0004 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, EVENT_CLU, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("pickUpAndHold"))) + test.timer.__create_and_queue_test_time_advance_timer(CUBEACTION_TIME, "oneshot") + test.mock_time.advance_time(CUBEACTION_TIME) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("noAction"))) + end +) + +test.register_coroutine_test( + "data_handler test - flip to side 6", + function() + local attr_report_data = { + { ACTION_ATTR, data_types.Uint16.ID, 0x0405 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, EVENT_CLU, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("flipToSide6"))) + test.timer.__create_and_queue_test_time_advance_timer(CUBEACTION_TIME, "oneshot") + test.mock_time.advance_time(CUBEACTION_TIME) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("noAction"))) + end +) + +test.register_coroutine_test( + "rotate_handler test - rotation", + function() + local attr_report_data = { + { ACTION_ATTR, data_types.Uint16.ID, 0x0000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, ROTATE_CLU, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("rotate"))) + test.timer.__create_and_queue_test_time_advance_timer(CUBEACTION_TIME, "oneshot") + test.mock_time.advance_time(CUBEACTION_TIME) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeAction.cubeAction("noAction"))) + end +) + +test.register_coroutine_test( + "face_handler test - face up 1", + function() + local attr_report_data = { + { FACE_ATTR, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRI_CLU, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + cubeFace.cubeFace("face1Up"))) + end +) + +test.run_registered_tests()