Skip to content
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

Add Dynamic Battery Support for Base-Lock Profiled Door Lock Devices #1660

Merged
merged 8 commits into from
Oct 21, 2024
Merged
23 changes: 23 additions & 0 deletions drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
62 changes: 58 additions & 4 deletions drivers/SmartThings/matter-lock/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hcarter-775
I am confused that if device do not have attribute 0x000D BatTimeRemaining, then device will also matched "-batteryLevel". In this case, device will not report BatTimeRemaining, if plugin has problem with device status not updated error, for example like this.

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()
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
},
}

-----------------------------------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
-- 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_aqara = {
profile = t_utils.get_profile_definition("lock-lockalarm-nobattery.yml"),
manufacturer_info = {vendor_id = 0x115F, product_id = 0x2801}, -- Aqara Smart Lock U300
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_aqara = test.mock_device.build_test_matter_device(mock_device_record_aqara)

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 = clusters.DoorLock.attributes.LockState:subscribe(mock_device_aqara)
subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device_aqara))
test.socket["matter"]:__expect_send({mock_device_aqara.id, subscribe_request})
test.mock_device.add_test_device(mock_device_aqara)
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 aqara lock",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_aqara.id, "doConfigure" })
mock_device_aqara:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
},
},
Expand Down
Loading