diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-battery.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-battery.yml new file mode 100644 index 0000000000..2401c4119e --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-battery.yml @@ -0,0 +1,29 @@ +name: lock-user-pin-battery +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-batteryLevel.yml new file mode 100644 index 0000000000..582fcd3975 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-batteryLevel.yml @@ -0,0 +1,29 @@ +name: lock-user-pin-batteryLevel +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-battery.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-battery.yml new file mode 100644 index 0000000000..ac52c839fa --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-battery.yml @@ -0,0 +1,31 @@ +name: lock-user-pin-schedule-battery +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: lockSchedules + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-batteryLevel.yml new file mode 100644 index 0000000000..d1b8ce8a13 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-batteryLevel.yml @@ -0,0 +1,31 @@ +name: lock-user-pin-schedule-batteryLevel +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: lockSchedules + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 83db28279c..f7adcf40c6 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -25,15 +25,19 @@ if version.api < 10 then end local DoorLock = clusters.DoorLock +local PowerSource = clusters.PowerSource local INITIAL_COTA_INDEX = 1 local ALL_INDEX = 0xFFFE local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 - {0x10E1, 0x2002} -- VDA + {0x147F, 0x0001}, -- U-tec + {0x10E1, 0x2002} -- VDA } +local PROFILE_BASE_NAME = "__profile_base_name" + local subscribed_attributes = { [capabilities.lock.ID] = { DoorLock.attributes.LockState @@ -53,6 +57,12 @@ local subscribed_attributes = { [capabilities.lockSchedules.ID] = { DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser, DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser + }, + [capabilities.battery.ID] = { + PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + PowerSource.attributes.BatChargeLevel } } @@ -127,6 +137,7 @@ local function do_configure(driver, device) local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) + local battery_eps = device:get_endpoints(PowerSource.ID, {feature_bitmap = PowerSource.types.PowerSourceFeature.BATTERY}) local profile_name = "lock" if #user_eps > 0 then @@ -144,8 +155,15 @@ local function do_configure(driver, device) else device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) end - device.log.info(string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) + if #battery_eps > 0 then + device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.PowerSource.attributes.AttributeList:read()) + device:send(req) + else + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end end local function info_changed(driver, device, event, args) @@ -353,6 +371,55 @@ local function max_year_schedule_of_user_handler(driver, device, ib, response) device:emit_event(capabilities.lockSchedules.yearDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) end +--------------------------------- +-- Power Source Attribute List -- +--------------------------------- +local function handle_power_source_attribute_list(driver, device, ib, response) + local support_battery_percentage = false + local support_battery_level = false + for _, attr in ipairs(ib.data.elements) do + -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) is present. + if attr.value == 0x0C then + support_battery_percentage = true + end + if attr.value == 0x0E then + support_battery_level = true + end + end + local profile_name = device:get_field(PROFILE_BASE_NAME) + if profile_name ~= nil then + if support_battery_percentage then + profile_name = profile_name .. "-battery" + elseif support_battery_level then + profile_name = profile_name .. "-batteryLevel" + end + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end +end + +------------------------------- +-- Battery Percent Remaining -- +------------------------------- +local function handle_battery_percent_remaining(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +-------------------------- +-- Battery Charge Level -- +-------------------------- +local function handle_battery_charge_level(driver, device, ib, response) + if ib.data.value == PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + -- Capability Handler ----------------- -- Lock/Unlock -- @@ -1671,6 +1738,11 @@ local new_matter_lock_handler = { [DoorLock.attributes.RequirePINforRemoteOperation.ID] = require_remote_pin_handler, [DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser.ID] = max_week_schedule_of_user_handler, [DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser.ID] = max_year_schedule_of_user_handler, + }, + [PowerSource.ID] = { + [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, + [PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, + [PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, } }, event = { @@ -1723,7 +1795,9 @@ local new_matter_lock_handler = { capabilities.lock, capabilities.lockUsers, capabilities.lockCredentials, - capabilities.lockSchedules + capabilities.lockSchedules, + capabilities.battery, + capabilities.batteryLevel }, can_handle = is_new_matter_lock_products } diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua new file mode 100644 index 0000000000..1565b37799 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua @@ -0,0 +1,189 @@ +-- Copyright 2023 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +test.add_package_capability("lockAlarm.yml") +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" + +local DoorLock = clusters.DoorLock + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-user-pin.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0181, -- PIN & USR & COTA + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local function test_init() + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test profile change when attributes related to BAT feature is not available.", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:read() + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin" }) + end +) + +test.register_coroutine_test( + "Test profile change when BatChargeLevel attribute is available", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:read() + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test profile change when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:read() + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin-battery" }) + end +) + +test.run_registered_tests()