diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml new file mode 100755 index 0000000000..769e955219 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml @@ -0,0 +1,23 @@ +name: base-lock-nobattery +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockCodes + version: 1 + - id: tamperAlert + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml new file mode 100755 index 0000000000..8de4bb1a5c --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml @@ -0,0 +1,21 @@ +name: lock-without-codes-nobattery +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: tamperAlert + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index ccb3110c08..ffc3709289 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -197,7 +197,15 @@ local function set_credential_response_handler(driver, device, ib, response) if device:get_field(lock_utils.NONFUNCTIONAL) and cota_cred_index == credential_index then device.log.info("Successfully set COTA credential after being non-functional") device:set_field(lock_utils.NONFUNCTIONAL, false, {persist = true}) - device:try_update_metadata({profile = "base-lock", provisioning_state = "PROVISIONED"}) + local power_source_eps = device:get_endpoints(clusters.PowerSource.ID) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + local profile_name = "base-lock" + if #power_source_eps == 0 then + profile_name = profile_name .. "-nobattery" + elseif #battery_feature_eps == 0 then + profile_name = profile_name .. "-batteryLevel" + end + device:try_update_metadata({profile = profile_name, provisioning_state = "PROVISIONED"}) end elseif device:get_field(lock_utils.COTA_CRED) and credential_index == device:get_field(lock_utils.COTA_CRED_INDEX) then -- Handle failure to set a COTA credential @@ -519,6 +527,39 @@ local function component_to_endpoint(device, component_name) return find_default_endpoint(device, clusters.DoorLock.ID) end +local function info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id then + device:subscribe() + end +end + +local function do_configure(driver, device) + -- check if the device is NOT currently profiled as base-lock + -- by ANDing a query for every capability in the base-lock profiles. + -- If it does not use base-lock, it is WWST and does not need re-profiling. + if not (device:supports_capability(capabilities.lock) and + device:supports_capability(capabilities.lockCodes) and + device:supports_capability(capabilities.tamperAlert) and + device:supports_capability(capabilities.battery)) then + return + end + + -- if not fingerprinted, dynamically configure base-lock profile based on Power Source cluster checks + local power_source_eps = device:get_endpoints(clusters.PowerSource.ID) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + local profile_name = "base-lock" + + -- check for battery type + if #power_source_eps == 0 then + profile_name = profile_name .. "-nobattery" + elseif #battery_feature_eps == 0 then + profile_name = profile_name .. "-batteryLevel" + end + + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) +end + local function device_init(driver, device) device:set_component_to_endpoint_fn(component_to_endpoint) device:subscribe() @@ -536,7 +577,7 @@ local function device_init(driver, device) device:set_field(lock_utils.COTA_READ_INITIALIZED, true, {persist = true}) end end - end +end local function device_added(driver, device) --Note: May want to write OperatingMode to NORMAL, to attempt to ensure remote operation works @@ -546,7 +587,15 @@ local function device_added(driver, device) if #eps == 0 then if device:supports_capability_by_id(capabilities.tamperAlert.ID) then device.log.debug("Device does not support lockCodes. Switching profile.") - device:try_update_metadata({profile = "lock-without-codes"}) + local power_source_eps = device:get_endpoints(clusters.PowerSource.ID) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + local profile_name = "lock-without-codes" + if #power_source_eps == 0 then + profile_name = profile_name .. "-nobattery" + elseif #battery_feature_eps == 0 then + profile_name = profile_name .. "-batteryLevel" + end + device:try_update_metadata({profile = profile_name}) else device.log.debug("Device supports neither lock codes nor tamper. Unable to switch profile.") end @@ -638,7 +687,12 @@ local matter_lock_driver = { sub_drivers = { require("new-matter-lock"), }, - lifecycle_handlers = {init = device_init, added = device_added}, + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + infoChanged = info_changed, + }, } ----------------------------------------------------------------------------------------------------------------------------- diff --git a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua new file mode 100644 index 0000000000..3c95216dc5 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua @@ -0,0 +1,98 @@ +-- Copyright 2024 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" +test.add_package_capability("lockAlarm.yml") +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" + +local mock_device_record = { + profile = t_utils.get_profile_definition("base-lock.yml"), + manufacturer_info = {vendor_id = 0, product_id = 0}, + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0101}, + }, + }, + }, +} +local mock_device = test.mock_device.build_test_matter_device(mock_device_record) + +local mock_device_record_level = { + profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), + manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, + }, + }, + }, +} + +local mock_device_level = test.mock_device.build_test_matter_device(mock_device_record_level) + +local function test_init() + local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) + subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) + + local subscribe_request_level = clusters.DoorLock.attributes.LockState:subscribe(mock_device_level) + test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) + test.mock_device.add_test_device(mock_device_level) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "doConfigure lifecycle event for base-lock-nobattery", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "doConfigure lifecycle event for Level Lock Plus profile", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) + mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua index 30e74e9a3c..7701d5a90c 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -35,7 +35,7 @@ local mock_device_record = { endpoint_id = 10, clusters = { {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 10}, }, }, }, diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua index fce2dcdc87..55eca597e6 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua @@ -56,7 +56,7 @@ local mock_device_record = { cluster_type = "SERVER", feature_map = 0x0181, -- PIN & USR & COTA }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 10}, }, }, },