From c387d3fd6004a54ea3de51987b9a4ea08206927d Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Mon, 26 Oct 2026 12:58:24 +0900 Subject: [PATCH 1/7] Add Schlage Sense Pro UWB subdriver and lock-modular-uwb-settings profile - Introduce subdriver for Schlage Sense Pro (Aliro) door lock with UWB support - Add lock-modular-uwb-settings profile supporting lockUwbApproachSettings and lockUwbModeSettings - Implement scaffold for Matter Manufacturer-Specific Cluster communication - Link subdriver lifecycle and modular profile matching in matter-lock - Add unit and integration tests covering subdriver matcher, lifecycle, commands, and attribute reports --- .../SmartThings/matter-lock/fingerprints.yml | 2 +- .../profiles/lock-modular-uwb-settings.yml | 40 ++ drivers/SmartThings/matter-lock/src/init.lua | 13 +- .../src/schlage-sense-pro/can_handle.lua | 32 + .../src/schlage-sense-pro/constants.lua | 261 +++++++++ .../src/schlage-sense-pro/init.lua | 382 ++++++++++++ .../matter-lock/src/sub_drivers.lua | 1 + .../src/test/test_schlage_sense_pro_uwb.lua | 554 ++++++++++++++++++ 8 files changed, 1282 insertions(+), 3 deletions(-) create mode 100644 drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml create mode 100644 drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua create mode 100644 drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua create mode 100644 drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua create mode 100644 drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index b81ae4c48f..d0452d2817 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -202,7 +202,7 @@ matterManufacturer: deviceLabel: Schlage Sense Pro vendorId: 0x1236 productId: 0x3800 - deviceProfileName: lock-modular + deviceProfileName: lock-modular-uwb-settings #Lockly - id: "5212/2" deviceLabel: Lockly Smart Lock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml new file mode 100644 index 0000000000..a5e633b815 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml @@ -0,0 +1,40 @@ +name: lock-modular-uwb-settings +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + optional: true + - id: lockCredentials + version: 1 + optional: true + - id: lockSchedules + version: 1 + optional: true + - id: lockAliro + version: 1 + optional: true + - id: lockUwbApproachSettings + version: 1 + optional: true + - id: lockUwbModeSettings + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - 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 72bfaae425..4634f49341 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -212,7 +212,9 @@ end local function match_profile_modular(driver, device) local enabled_optional_component_capability_pairs = {} local main_component_capabilities = {} - local modular_profile_name = "lock-modular" + local is_uwb = (device.profile.id == "lock-modular-uwb-settings") or + (device.manufacturer_info and device.manufacturer_info.vendor_id == 0x1236 and device.manufacturer_info.product_id == 0x3800) + local modular_profile_name = is_uwb and "lock-modular-uwb-settings" or "lock-modular" for _, device_ep in pairs(device.endpoints) do for _, ep_cluster in pairs(device_ep.clusters) do if ep_cluster.cluster_id == DoorLock.ID then @@ -236,7 +238,9 @@ local function match_profile_modular(driver, device) if clus_has_feature(DoorLock.types.Feature.UNBOLT) then device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" + if not is_uwb then + modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" + end else device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) @@ -256,6 +260,11 @@ local function match_profile_modular(driver, device) table.insert(main_component_capabilities, capabilities.battery.ID) end + if is_uwb then + table.insert(main_component_capabilities, "lockUwbApproachSettings") + table.insert(main_component_capabilities, "lockUwbModeSettings") + end + table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison or device:supports_capability(capabilities.lockCodes) -- always re-profile a device using the legacy code storage methods diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua new file mode 100644 index 0000000000..e7f739c5da --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua @@ -0,0 +1,32 @@ +-- Copyright 2026 SmartThings, Inc. +-- +-- 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 constants = require "schlage-sense-pro.constants" + +--- Matcher for Schlage Sense Pro (Aliro) UWB Door Lock. +--- [PLACEHOLDER - OPTIONAL / VERIFICATION]: +--- Default implementation identifies the device by Vendor ID (0x1236 / Allegion) and Product ID (0x3800). +--- If product IDs vary across models or dynamic matching by custom cluster presence is preferred, +--- the condition below can be updated to check for the cluster endpoint: +--- #device:get_endpoints(constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID) > 0 +local function is_schlage_sense_pro(opts, driver, device) + if device.manufacturer_info + and device.manufacturer_info.vendor_id == constants.SCHLAGE_VENDOR_ID + and device.manufacturer_info.product_id == constants.SCHLAGE_SENSE_PRO_PRODUCT_ID then + return true, require("schlage-sense-pro") + end + return false +end + +return is_schlage_sense_pro diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua new file mode 100644 index 0000000000..42cea60aaf --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua @@ -0,0 +1,261 @@ +-- Copyright 2026 SmartThings, Inc. +-- +-- 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. + +--- Constants and Specification Scaffolding for Allegion / Schlage Sense Pro UWB Door Lock +--- +--- Reference Specifications: +--- 1. Confluence: "Schlage Sense Pro (Aliro) - UWB Configuration and Feature Support" (Page 5350097136) +--- 2. Confluence: "Action plan for Schlage UWB configuration" (Page 5392597723) +--- +--- Feature Overview: +--- 1. Field of View (Detection Area): +--- Allowed values per Allegion spec: +--- 0: Front + Left + Right (all directions) +--- 1: Front + Left +--- 2: Front + Right +--- 3: Front only +--- SmartThings Capability: lockUwbApproachSettings +--- +--- 2. Intent Mode (Unlock Behavior): +--- Allowed values per Allegion spec: +--- 0: Unlock on Approach +--- 1: Unlock on Arrival +--- 2: Unlock on Lock Interaction +--- SmartThings Capability: lockUwbModeSettings +--- +--- NOTE FOR ALLEGION ENGINEERS: +--- Update the manufacturer-specific Matter cluster ID, attribute IDs, command IDs, +--- and data types below to match your final Matter cluster specification. + +local capabilities = require "st.capabilities" + +local constants = {} + +-- Device Identification +constants.SCHLAGE_VENDOR_ID = 0x1236 -- Allegion / Schlage Vendor ID +constants.SCHLAGE_SENSE_PRO_PRODUCT_ID = 0x3800 -- Schlage Sense Pro Product ID + +------------------------------------------------------------------------------------- +-- Matter Manufacturer-Specific Cluster Definitions +-- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] +------------------------------------------------------------------------------------- + +--- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] +--- Manufacturer-specific cluster ID for Schlage UWB Configuration. +--- Allegion engineers MUST replace this placeholder with the official cluster ID +--- allocated for the Schlage Sense Pro UWB configuration cluster. +--- (Typically formatted as (VendorID << 16) | MEI, e.g. 0x1236FC01). +constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID = 0x1236FC01 + +--- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] +--- Attribute ID for Field of View / Approach Direction. +--- Allegion engineers MUST replace this placeholder with the actual attribute ID +--- defined in the custom cluster specification. +constants.ATTR_FIELD_OF_VIEW_ID = 0x0001 + +--- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] +--- Attribute ID for Intent Mode / Unlock Behavior. +--- Allegion engineers MUST replace this placeholder with the actual attribute ID +--- defined in the custom cluster specification. +constants.ATTR_INTENT_MODE_ID = 0x0002 + +--- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] +--- Command IDs (used only if interaction requires Matter Invoke Commands instead of Attribute Writes). +--- If commands are used, Allegion engineers MUST replace these placeholders with the actual command IDs. +constants.CMD_SET_FIELD_OF_VIEW_ID = 0x0001 +constants.CMD_SET_INTENT_MODE_ID = 0x0002 + +------------------------------------------------------------------------------------- +-- Capability Definitions and Value Mappings +-- [VERIFICATION REQUIRED BY ALLEGION] +------------------------------------------------------------------------------------- + +constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS = "lockUwbApproachSettings" +constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS = "lockUwbModeSettings" + +--- Field of View (lockUwbApproachSettings) mappings +--- [PLACEHOLDER / VERIFY]: Update this mapping table if the device's raw attribute values differ from the defaults below. +--- Default per Confluence 5350097136 / 5392597723: +--- 0: Front+Left+Right, 1: Front+Left, 2: Front+Right, 3: Front only +constants.APPROACH_DIRECTION = { + FRONT_LEFT_RIGHT = "frontLeftRight", + FRONT_LEFT = "frontLeft", + FRONT_RIGHT = "frontRight", + FRONT_ONLY = "frontOnly", +} + +constants.RAW_TO_APPROACH_DIRECTION = { + [0] = constants.APPROACH_DIRECTION.FRONT_LEFT_RIGHT, + [1] = constants.APPROACH_DIRECTION.FRONT_LEFT, + [2] = constants.APPROACH_DIRECTION.FRONT_RIGHT, + [3] = constants.APPROACH_DIRECTION.FRONT_ONLY, +} + +constants.APPROACH_DIRECTION_TO_RAW = { + [constants.APPROACH_DIRECTION.FRONT_LEFT_RIGHT] = 0, + [constants.APPROACH_DIRECTION.FRONT_LEFT] = 1, + [constants.APPROACH_DIRECTION.FRONT_RIGHT] = 2, + [constants.APPROACH_DIRECTION.FRONT_ONLY] = 3, +} + +constants.SUPPORTED_APPROACH_DIRECTIONS = { + constants.APPROACH_DIRECTION.FRONT_LEFT_RIGHT, + constants.APPROACH_DIRECTION.FRONT_LEFT, + constants.APPROACH_DIRECTION.FRONT_RIGHT, + constants.APPROACH_DIRECTION.FRONT_ONLY, +} + +--- Intent Mode (lockUwbModeSettings) mappings +--- [PLACEHOLDER / VERIFY]: Update this mapping table if the device's raw attribute values differ from the defaults below. +--- Default per Confluence 5350097136 / 5392597723: +--- 0: Unlock on Approach, 1: Unlock on Arrival, 2: Unlock on Lock Interaction +constants.INTENT_MODE = { + UNLOCK_ON_APPROACH = "unlockOnApproach", + UNLOCK_ON_ARRIVAL = "unlockOnArrival", + UNLOCK_ON_INTERACTION = "unlockOnInteraction", +} + +constants.RAW_TO_INTENT_MODE = { + [0] = constants.INTENT_MODE.UNLOCK_ON_APPROACH, + [1] = constants.INTENT_MODE.UNLOCK_ON_ARRIVAL, + [2] = constants.INTENT_MODE.UNLOCK_ON_INTERACTION, +} + +constants.INTENT_MODE_TO_RAW = { + [constants.INTENT_MODE.UNLOCK_ON_APPROACH] = 0, + [constants.INTENT_MODE.UNLOCK_ON_ARRIVAL] = 1, + [constants.INTENT_MODE.UNLOCK_ON_INTERACTION] = 2, +} + +constants.SUPPORTED_INTENT_MODES = { + constants.INTENT_MODE.UNLOCK_ON_APPROACH, + constants.INTENT_MODE.UNLOCK_ON_ARRIVAL, + constants.INTENT_MODE.UNLOCK_ON_INTERACTION, +} + +------------------------------------------------------------------------------------- +-- Fallback Capability Builders (Pre-27R1 Compatibility) +------------------------------------------------------------------------------------- + +constants.APPROACH_CAP_JSON = [[{ + "name": "Lock UWB Approach Settings", + "id": "lockUwbApproachSettings", + "version": 1, + "attributes": { + "approachDirection": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] + } + }, + "required": ["value"] + } + }, + "supportedApproachDirections": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["value"] + } + } + }, + "commands": { + "setApproachDirection": { + "name": "setApproachDirection", + "arguments": [ + { + "name": "approachDirection", + "schema": { + "type": "string", + "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] + }, + "optional": false + } + ] + } + } +}]] + +constants.MODE_CAP_JSON = [[{ + "name": "Lock UWB Mode Settings", + "id": "lockUwbModeSettings", + "version": 1, + "attributes": { + "intentMode": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] + } + }, + "required": ["value"] + } + }, + "supportedIntentModes": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["value"] + } + } + }, + "commands": { + "setIntentMode": { + "name": "setIntentMode", + "arguments": [ + { + "name": "intentMode", + "schema": { + "type": "string", + "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] + }, + "optional": false + } + ] + } + } +}]] + +local function load_or_build(cap_id, cap_json) + local status, cap = pcall(function() return capabilities[cap_id] end) + if status and cap and pcall(function() return cap.commands end) and cap.commands ~= nil then + return cap + end + return capabilities.build_cap_from_json_string(cap_json) +end + +function constants.get_lock_uwb_approach_settings_capability() + return load_or_build(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, constants.APPROACH_CAP_JSON) +end + +function constants.get_lock_uwb_mode_settings_capability() + return load_or_build(constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, constants.MODE_CAP_JSON) +end + +return constants diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua new file mode 100644 index 0000000000..084e598ba2 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua @@ -0,0 +1,382 @@ +-- Copyright 2026 SmartThings, Inc. +-- +-- 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. + +--- Subdriver for Allegion / Schlage Sense Pro UWB Door Lock +--- +--- Provides integration for: +--- - lockUwbApproachSettings (Field of View: Front+Left+Right, Front+Left, Front+Right, Front only) +--- - lockUwbModeSettings (Intent Mode: Unlock on Approach, Unlock on Arrival, Unlock on Lock Interaction) +--- - Profile management with lock-uwb-modular +--- - Communication via Matter Manufacturer-Specific Cluster (to be detailed by Allegion) + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local lock_utils = require "lock_utils" +local constants = require "schlage-sense-pro.constants" + +local DoorLock = clusters.DoorLock + +-- Resolve or build capability representations +local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() +local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() + +------------------------------------------------------------------------------------- +-- Helper: Endpoint Resolution +-- [VERIFICATION REQUIRED BY ALLEGION] +------------------------------------------------------------------------------------- + +--- Find the target endpoint for Schlage UWB custom cluster communication. +--- [PLACEHOLDER - VERIFICATION REQUIRED BY ALLEGION]: +--- Verify the endpoint hosting the manufacturer custom cluster on the physical lock. +--- By default, it searches for an endpoint supporting the custom cluster ID, +--- or falls back to the DoorLock cluster endpoint (typically 1). +local function find_schlage_endpoint(device) + local custom_eps = device:get_endpoints(constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID) + if #custom_eps > 0 then + return custom_eps[1] + end + local lock_eps = device:get_endpoints(DoorLock.ID) + if #lock_eps > 0 then + return lock_eps[1] + end + return device.MATTER_DEFAULT_ENDPOINT or 1 +end + +------------------------------------------------------------------------------------- +-- Manufacturer-Specific Cluster Communication (Scaffold for Allegion) +-- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] +------------------------------------------------------------------------------------- + +--- Read Field of View attribute from Schlage Custom Cluster. +--- @param device st.matter.Device +local function read_field_of_view(device) + local ep = find_schlage_endpoint(device) + device.log.info(string.format("Reading Schlage UWB Field of View (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, ep)) + device:send(cluster_base.read(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, nil)) +end + +--- Write Field of View attribute to Schlage Custom Cluster. +--- [PLACEHOLDER - DATA TYPE]: +--- Scaffold uses data_types.Uint8 by default. If the cluster attribute is defined as +--- data_types.Enum8 or another type, update data_types.Uint8 accordingly. +--- @param device st.matter.Device +--- @param raw_value number (0: Front+Left+Right, 1: Front+Left, 2: Front+Right, 3: Front only) +local function write_field_of_view(device, raw_value) + local ep = find_schlage_endpoint(device) + device.log.info(string.format("Writing Schlage UWB Field of View: %d (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + raw_value, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, ep)) + -- [PLACEHOLDER - DATA TYPE]: Update to data_types.Enum8 or matching cluster data type if needed + local data = data_types.validate_or_build_type(raw_value, data_types.Uint8) + device:send(cluster_base.write(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, nil, data)) +end + +--- Read Intent Mode attribute from Schlage Custom Cluster. +--- @param device st.matter.Device +local function read_intent_mode(device) + local ep = find_schlage_endpoint(device) + device.log.info(string.format("Reading Schlage UWB Intent Mode (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, ep)) + device:send(cluster_base.read(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, nil)) +end + +--- Write Intent Mode attribute to Schlage Custom Cluster. +--- [PLACEHOLDER - DATA TYPE]: +--- Scaffold uses data_types.Uint8 by default. If the cluster attribute is defined as +--- data_types.Enum8 or another type, update data_types.Uint8 accordingly. +--- @param device st.matter.Device +--- @param raw_value number (0: Unlock on Approach, 1: Unlock on Arrival, 2: Unlock on Lock Interaction) +local function write_intent_mode(device, raw_value) + local ep = find_schlage_endpoint(device) + device.log.info(string.format("Writing Schlage UWB Intent Mode: %d (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + raw_value, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, ep)) + -- [PLACEHOLDER - DATA TYPE]: Update to data_types.Enum8 or matching cluster data type if needed + local data = data_types.validate_or_build_type(raw_value, data_types.Uint8) + device:send(cluster_base.write(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, nil, data)) +end + +--- [PLACEHOLDER - COMMAND INVOKE ALTERNATIVE]: +--- If the cluster uses Matter Invoke Commands instead of Attribute Writes, +--- use the template functions below and invoke them from handle_set_* instead: +--[[ +local function invoke_set_field_of_view(device, raw_value) + local ep = find_schlage_endpoint(device) + local cmd_params = { [0] = raw_value } + local req = cluster_base.build_cluster_command(nil, device, cmd_params, ep, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.CMD_SET_FIELD_OF_VIEW_ID, false, false) + device:send(req) +end + +local function invoke_set_intent_mode(device, raw_value) + local ep = find_schlage_endpoint(device) + local cmd_params = { [0] = raw_value } + local req = cluster_base.build_cluster_command(nil, device, cmd_params, ep, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.CMD_SET_INTENT_MODE_ID, false, false) + device:send(req) +end +--]] + +------------------------------------------------------------------------------------- +-- Capability Event Helpers +------------------------------------------------------------------------------------- + +local function emit_supported_values(device) + if device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) then + device:emit_event(lockUwbApproachSettings.supportedApproachDirections( + constants.SUPPORTED_APPROACH_DIRECTIONS, { visibility = { displayed = false } } + )) + end + if device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS) then + device:emit_event(lockUwbModeSettings.supportedIntentModes( + constants.SUPPORTED_INTENT_MODES, { visibility = { displayed = false } } + )) + end +end + +------------------------------------------------------------------------------------- +-- Matter Attribute Handlers (Device -> SmartThings Hub) +------------------------------------------------------------------------------------- + +--- Handles Field of View reports from the Schlage Custom Cluster +local function field_of_view_attr_handler(driver, device, ib, response) + if ib.data.value ~= nil then + local raw_val = ib.data.value + local enum_val = constants.RAW_TO_APPROACH_DIRECTION[raw_val] + device.log.info(string.format("Schlage UWB Field of View report: raw=%s, enum=%s", tostring(raw_val), tostring(enum_val))) + if enum_val then + device:emit_event(lockUwbApproachSettings.approachDirection(enum_val)) + else + device.log.warn(string.format("Unknown Schlage UWB Field of View value: %s", tostring(raw_val))) + end + end +end + +--- Handles Intent Mode reports from the Schlage Custom Cluster +local function intent_mode_attr_handler(driver, device, ib, response) + if ib.data.value ~= nil then + local raw_val = ib.data.value + local enum_val = constants.RAW_TO_INTENT_MODE[raw_val] + device.log.info(string.format("Schlage UWB Intent Mode report: raw=%s, enum=%s", tostring(raw_val), tostring(enum_val))) + if enum_val then + device:emit_event(lockUwbModeSettings.intentMode(enum_val)) + else + device.log.warn(string.format("Unknown Schlage UWB Intent Mode value: %s", tostring(raw_val))) + end + end +end + +------------------------------------------------------------------------------------- +-- Capability Command Handlers (SmartThings App -> Device) +------------------------------------------------------------------------------------- + +--- Handler for lockUwbApproachSettings:setApproachDirection command +local function handle_set_approach_direction(driver, device, command) + local val = command.args.approachDirection + device.log.info(string.format("Handling setApproachDirection command: %s", tostring(val))) + + local raw_val = nil + if type(val) == "number" then + raw_val = val + elseif type(val) == "string" then + raw_val = constants.APPROACH_DIRECTION_TO_RAW[val] + end + + if raw_val ~= nil and constants.RAW_TO_APPROACH_DIRECTION[raw_val] ~= nil then + write_field_of_view(device, raw_val) + -- Optimistic event update for immediate UX feedback + local enum_val = constants.RAW_TO_APPROACH_DIRECTION[raw_val] + device:emit_event(lockUwbApproachSettings.approachDirection(enum_val)) + else + device.log.error(string.format("Invalid approachDirection argument: %s", tostring(val))) + end +end + +--- Handler for lockUwbModeSettings:setIntentMode command +local function handle_set_intent_mode(driver, device, command) + local val = command.args.intentMode + device.log.info(string.format("Handling setIntentMode command: %s", tostring(val))) + + local raw_val = nil + if type(val) == "number" then + raw_val = val + elseif type(val) == "string" then + raw_val = constants.INTENT_MODE_TO_RAW[val] + end + + if raw_val ~= nil and constants.RAW_TO_INTENT_MODE[raw_val] ~= nil then + write_intent_mode(device, raw_val) + -- Optimistic event update for immediate UX feedback + local enum_val = constants.RAW_TO_INTENT_MODE[raw_val] + device:emit_event(lockUwbModeSettings.intentMode(enum_val)) + else + device.log.error(string.format("Invalid intentMode argument: %s", tostring(val))) + end +end + +--- Handler for refresh capability +local function handle_refresh(driver, device, command) + device.log.info("Refreshing Schlage Sense Pro UWB and Lock State") + -- 1. Standard Door Lock state refresh + device:send(DoorLock.attributes.LockState:read(device)) + device:set_field(lock_utils.BUSY_STATE, false, { persist = true }) + + -- 2. Schlage UWB settings refresh + read_field_of_view(device) + read_intent_mode(device) +end + +------------------------------------------------------------------------------------- +-- Device Lifecycle Handlers +------------------------------------------------------------------------------------- + +local function configure_profile(device) + local modular_profile_name = "lock-modular-uwb-settings" + local main_component_capabilities = { + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + } + + -- Check DoorLock features + for _, device_ep in pairs(device.endpoints) do + for _, ep_cluster in pairs(device_ep.clusters) do + if ep_cluster.cluster_id == DoorLock.ID then + local clus_has_feature = function(feature_bitmap) + return DoorLock.are_features_supported( + feature_bitmap, + lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, device_ep.endpoint_id) or + ep_cluster.feature_map + ) + end + if clus_has_feature(DoorLock.types.Feature.USER) then + table.insert(main_component_capabilities, capabilities.lockUsers.ID) + end + if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then + table.insert(main_component_capabilities, capabilities.lockCredentials.ID) + end + if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or + clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then + table.insert(main_component_capabilities, capabilities.lockSchedules.ID) + end + if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then + table.insert(main_component_capabilities, capabilities.lockAliro.ID) + end + break + end + end + end + + local supported_battery_type = device:get_field("__BATTERY_SUPPORT") + if supported_battery_type == "BATTERY_LEVEL" then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif supported_battery_type == "BATTERY_PERCENTAGE" then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + local enabled_optional = { { "main", main_component_capabilities } } + device:try_update_metadata({ + profile = modular_profile_name, + optional_component_capabilities = enabled_optional + }) +end + +local function device_init(driver, device) + device.log.info("Schlage Sense Pro UWB subdriver device_init") + -- Chain base driver initialization if available + if driver.lifecycle_handlers and driver.lifecycle_handlers.init then + driver.lifecycle_handlers.init(driver, device) + end +end + +local function device_added(driver, device) + device.log.info("Schlage Sense Pro UWB subdriver device_added") + if driver.lifecycle_handlers and driver.lifecycle_handlers.added then + driver.lifecycle_handlers.added(driver, device) + end + + emit_supported_values(device) + read_field_of_view(device) + read_intent_mode(device) +end + +local function do_configure(driver, device) + device.log.info("Schlage Sense Pro UWB subdriver do_configure") + configure_profile(device) + + emit_supported_values(device) + read_field_of_view(device) + read_intent_mode(device) + + if driver.lifecycle_handlers and driver.lifecycle_handlers.doConfigure then + driver.lifecycle_handlers.doConfigure(driver, device) + end +end + +local function driver_switched(driver, device) + device.log.info("Schlage Sense Pro UWB subdriver driver_switched") + configure_profile(device) + emit_supported_values(device) + read_field_of_view(device) + read_intent_mode(device) + device:try_update_metadata({ provisioning_state = "PROVISIONED" }) +end + +local function info_changed(driver, device, event, args) + device.log.info("Schlage Sense Pro UWB subdriver info_changed") + if driver.lifecycle_handlers and driver.lifecycle_handlers.infoChanged then + driver.lifecycle_handlers.infoChanged(driver, device, event, args) + end +end + +------------------------------------------------------------------------------------- +-- Subdriver Template Definition +------------------------------------------------------------------------------------- + +local schlage_sense_pro_subdriver = { + NAME = "schlage-sense-pro", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + driverSwitched = driver_switched, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID] = { + [constants.ATTR_FIELD_OF_VIEW_ID] = field_of_view_attr_handler, + [constants.ATTR_INTENT_MODE_ID] = intent_mode_attr_handler, + } + }, + }, + capability_handlers = { + [constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS] = { + [lockUwbApproachSettings.commands.setApproachDirection.NAME] = handle_set_approach_direction, + }, + [constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS] = { + [lockUwbModeSettings.commands.setIntentMode.NAME] = handle_set_intent_mode, + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = handle_refresh, + }, + }, + supported_capabilities = { + lockUwbApproachSettings, + lockUwbModeSettings, + }, + can_handle = require("schlage-sense-pro.can_handle"), +} + +return schlage_sense_pro_subdriver diff --git a/drivers/SmartThings/matter-lock/src/sub_drivers.lua b/drivers/SmartThings/matter-lock/src/sub_drivers.lua index 563c428945..44d68b7ec1 100644 --- a/drivers/SmartThings/matter-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/matter-lock/src/sub_drivers.lua @@ -3,6 +3,7 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { + lazy_load_if_possible("schlage-sense-pro"), lazy_load_if_possible("unmigrated-matter-lock"), } return sub_drivers diff --git a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua new file mode 100644 index 0000000000..1e17b4c41f --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua @@ -0,0 +1,554 @@ +-- Copyright 2026 SmartThings, Inc. +-- +-- 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" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local constants = require "schlage-sense-pro.constants" +local can_handle = require "schlage-sense-pro.can_handle" + +local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() +local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() + +local enabled_optional_capabilities = { + { + "main", + { + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + } + } +} + +local mock_schlage_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition( + "lock-modular-uwb-settings.yml", + { enabled_optional_capabilities = enabled_optional_capabilities } + ), + manufacturer_info = { + vendor_id = constants.SCHLAGE_VENDOR_ID, + product_id = constants.SCHLAGE_SENSE_PRO_PRODUCT_ID, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + { + cluster_id = constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + cluster_type = "SERVER", + cluster_revision = 1, + } + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local DoorLockFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.DoorLock.ID } + +local function test_init() + test.socket.capability:__load_capability_definition( + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + 1, + constants.APPROACH_CAP_JSON + ) + test.socket.capability:__load_capability_definition( + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + 1, + constants.MODE_CAP_JSON + ) + + local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_schlage_device) + subscribe_request:merge(clusters.DoorLock.attributes.OperatingMode:subscribe(mock_schlage_device)) + subscribe_request:merge(cluster_base.subscribe(mock_schlage_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) + subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_schlage_device)) + subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_schlage_device)) + + test.mock_device.add_test_device(mock_schlage_device) + test.socket.matter:__expect_send({ mock_schlage_device.id, subscribe_request }) +end +test.set_test_init_function(test_init) + +------------------------------------------------------------------------------------- +-- Subdriver Matching Tests +------------------------------------------------------------------------------------- + +test.register_coroutine_test( + "can_handle matches Schlage Sense Pro and rejects other devices", + function() + local matched, subdriver = can_handle(nil, nil, mock_schlage_device) + assert(matched == true, "Expected can_handle to return true for Schlage Sense Pro") + assert(subdriver ~= nil, "Expected subdriver table to be returned") + assert(subdriver.NAME == "schlage-sense-pro", "Expected subdriver NAME to be schlage-sense-pro") + + local other_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-modular.yml"), + manufacturer_info = { + vendor_id = 0x115F, + product_id = 0x2802, + } + }) + local other_matched = can_handle(nil, nil, other_device) + assert(other_matched == false, "Expected can_handle to return false for non-Schlage device") + end +) + +------------------------------------------------------------------------------------- +-- Capability Command Tests (Cloud/App -> Device) +------------------------------------------------------------------------------------- + +test.register_message_test( + "Handle lockUwbApproachSettings setApproachDirection('frontOnly')", + { + { + channel = "capability", + direction = "receive", + message = { + mock_schlage_device.id, + { + capability = constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + component = "main", + command = "setApproachDirection", + args = { "frontOnly" } + } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + cluster_base.write( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + nil, + data_types.validate_or_build_type(3, data_types.Uint8) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_schlage_device:generate_test_message( + "main", + lockUwbApproachSettings.approachDirection("frontOnly") + ) + } + } +) + +test.register_message_test( + "Handle lockUwbApproachSettings setApproachDirection('frontLeftRight')", + { + { + channel = "capability", + direction = "receive", + message = { + mock_schlage_device.id, + { + capability = constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + component = "main", + command = "setApproachDirection", + args = { "frontLeftRight" } + } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + cluster_base.write( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + nil, + data_types.validate_or_build_type(0, data_types.Uint8) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_schlage_device:generate_test_message( + "main", + lockUwbApproachSettings.approachDirection("frontLeftRight") + ) + } + } +) + +test.register_message_test( + "Handle lockUwbModeSettings setIntentMode('unlockOnArrival')", + { + { + channel = "capability", + direction = "receive", + message = { + mock_schlage_device.id, + { + capability = constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + component = "main", + command = "setIntentMode", + args = { "unlockOnArrival" } + } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + cluster_base.write( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + nil, + data_types.validate_or_build_type(1, data_types.Uint8) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_schlage_device:generate_test_message( + "main", + lockUwbModeSettings.intentMode("unlockOnArrival") + ) + } + } +) + +test.register_message_test( + "Handle lockUwbModeSettings setIntentMode('unlockOnInteraction')", + { + { + channel = "capability", + direction = "receive", + message = { + mock_schlage_device.id, + { + capability = constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + component = "main", + command = "setIntentMode", + args = { "unlockOnInteraction" } + } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + cluster_base.write( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + nil, + data_types.validate_or_build_type(2, data_types.Uint8) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_schlage_device:generate_test_message( + "main", + lockUwbModeSettings.intentMode("unlockOnInteraction") + ) + } + } +) + +------------------------------------------------------------------------------------- +-- Matter Attribute Report Tests (Device -> Hub) +------------------------------------------------------------------------------------- + +test.register_message_test( + "Handle Field of View attribute report from device (raw=2 -> frontRight)", + { + { + channel = "matter", + direction = "receive", + message = { + mock_schlage_device.id, + cluster_base.build_test_report_data( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + data_types.Uint8(2) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_schlage_device:generate_test_message( + "main", + lockUwbApproachSettings.approachDirection("frontRight") + ) + } + } +) + +test.register_message_test( + "Handle Intent Mode attribute report from device (raw=0 -> unlockOnApproach)", + { + { + channel = "matter", + direction = "receive", + message = { + mock_schlage_device.id, + cluster_base.build_test_report_data( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + data_types.Uint8(0) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_schlage_device:generate_test_message( + "main", + lockUwbModeSettings.intentMode("unlockOnApproach") + ) + } + } +) + +------------------------------------------------------------------------------------- +-- Refresh Command Tests +------------------------------------------------------------------------------------- + +test.register_message_test( + "Handle refresh command queries both standard lock state and UWB settings", + { + { + channel = "capability", + direction = "receive", + message = { + mock_schlage_device.id, + { + capability = capabilities.refresh.ID, + component = "main", + command = "refresh", + args = {} + } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + clusters.DoorLock.attributes.LockState:read(mock_schlage_device) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + cluster_base.read( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + nil + ) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_schlage_device.id, + cluster_base.read( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + nil + ) + } + } + } +) + +------------------------------------------------------------------------------------- +-- Lifecycle Event Tests +------------------------------------------------------------------------------------- + +test.register_coroutine_test( + "Handle doConfigure lifecycle event for Schlage Sense Pro UWB lock", + function() + mock_schlage_device:expect_metadata_update({ + profile = "lock-modular-uwb-settings", + optional_component_capabilities = { + { + "main", + { + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + } + } + } + }) + mock_schlage_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_schlage_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + lockUwbApproachSettings.supportedApproachDirections( + constants.SUPPORTED_APPROACH_DIRECTIONS, { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + lockUwbModeSettings.supportedIntentModes( + constants.SUPPORTED_INTENT_MODES, { visibility = { displayed = false } } + ) + ) + ) + test.socket.matter:__expect_send({ + mock_schlage_device.id, + cluster_base.read( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + nil + ) + }) + test.socket.matter:__expect_send({ + mock_schlage_device.id, + cluster_base.read( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + nil + ) + }) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + capabilities.lock.supportedLockValues( + { "locked", "unlocked", "not fully locked" }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + capabilities.lock.supportedLockCommands( + { "lock", "unlock" }, + { visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Handle driverSwitched lifecycle event for Schlage Sense Pro UWB lock", + function() + mock_schlage_device:expect_metadata_update({ + profile = "lock-modular-uwb-settings", + optional_component_capabilities = { + { + "main", + { + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + } + } + } + }) + mock_schlage_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_schlage_device.id, "driverSwitched" }) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + lockUwbApproachSettings.supportedApproachDirections( + constants.SUPPORTED_APPROACH_DIRECTIONS, { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + lockUwbModeSettings.supportedIntentModes( + constants.SUPPORTED_INTENT_MODES, { visibility = { displayed = false } } + ) + ) + ) + test.socket.matter:__expect_send({ + mock_schlage_device.id, + cluster_base.read( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + nil + ) + }) + test.socket.matter:__expect_send({ + mock_schlage_device.id, + cluster_base.read( + mock_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + nil + ) + }) + test.wait_for_events() + end +) + +test.run_registered_tests() From 7f7640575cbe9a4a3b430eafb2e2e46d7d4d4c84 Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Mon, 26 Oct 2026 13:47:32 +0900 Subject: [PATCH 2/7] Support OTA driver update migration for existing Schlage locks - In schlage-sense-pro device_init, automatically upgrade previously onboarded locks missing lockUwbApproachSettings to lock-modular-uwb-settings - In match_profile_modular, check supports_capability_by_id for lockUwbApproachSettings - In can_handle, support hybrid matching on Vendor ID alongside Product ID or custom cluster - Clean up comment references to match lock-modular-uwb-settings profile name - Add unit test covering device_init migration on driver update --- drivers/SmartThings/matter-lock/src/init.lua | 3 +- .../src/schlage-sense-pro/can_handle.lua | 19 ++-- .../src/schlage-sense-pro/init.lua | 12 ++- .../src/test/test_schlage_sense_pro_uwb.lua | 102 ++++++++++++++++++ 4 files changed, 125 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 4634f49341..480a00ae1a 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -212,7 +212,8 @@ end local function match_profile_modular(driver, device) local enabled_optional_component_capability_pairs = {} local main_component_capabilities = {} - local is_uwb = (device.profile.id == "lock-modular-uwb-settings") or + local is_uwb = device:supports_capability_by_id("lockUwbApproachSettings") or + (device.profile.id == "lock-modular-uwb-settings") or (device.manufacturer_info and device.manufacturer_info.vendor_id == 0x1236 and device.manufacturer_info.product_id == 0x3800) local modular_profile_name = is_uwb and "lock-modular-uwb-settings" or "lock-modular" for _, device_ep in pairs(device.endpoints) do diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua index e7f739c5da..95db3de46d 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua @@ -15,16 +15,17 @@ local constants = require "schlage-sense-pro.constants" --- Matcher for Schlage Sense Pro (Aliro) UWB Door Lock. ---- [PLACEHOLDER - OPTIONAL / VERIFICATION]: ---- Default implementation identifies the device by Vendor ID (0x1236 / Allegion) and Product ID (0x3800). ---- If product IDs vary across models or dynamic matching by custom cluster presence is preferred, ---- the condition below can be updated to check for the cluster endpoint: ---- #device:get_endpoints(constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID) > 0 +--- +--- Supports both initial onboarding and OTA driver updates for previously onboarded locks. +--- [PLACEHOLDER - OPTIONAL / VERIFICATION FOR ALLEGION]: +--- Matches Allegion Vendor ID (0x1236) with either the known Schlage Sense Pro Product ID (0x3800) +--- or the presence of the custom UWB cluster. local function is_schlage_sense_pro(opts, driver, device) - if device.manufacturer_info - and device.manufacturer_info.vendor_id == constants.SCHLAGE_VENDOR_ID - and device.manufacturer_info.product_id == constants.SCHLAGE_SENSE_PRO_PRODUCT_ID then - return true, require("schlage-sense-pro") + if device.manufacturer_info and device.manufacturer_info.vendor_id == constants.SCHLAGE_VENDOR_ID then + if device.manufacturer_info.product_id == constants.SCHLAGE_SENSE_PRO_PRODUCT_ID or + #device:get_endpoints(constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID) > 0 then + return true, require("schlage-sense-pro") + end end return false end diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua index 084e598ba2..3098197589 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua @@ -17,7 +17,7 @@ --- Provides integration for: --- - lockUwbApproachSettings (Field of View: Front+Left+Right, Front+Left, Front+Right, Front only) --- - lockUwbModeSettings (Intent Mode: Unlock on Approach, Unlock on Arrival, Unlock on Lock Interaction) ---- - Profile management with lock-uwb-modular +--- - Profile management with lock-modular-uwb-settings --- - Communication via Matter Manufacturer-Specific Cluster (to be detailed by Allegion) local capabilities = require "st.capabilities" @@ -298,6 +298,16 @@ local function device_init(driver, device) if driver.lifecycle_handlers and driver.lifecycle_handlers.init then driver.lifecycle_handlers.init(driver, device) end + + -- Automatically migrate previously onboarded devices (e.g. paired prior to driver update + -- under lock-modular) that do not yet have lockUwbApproachSettings to lock-modular-uwb-settings. + if not device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) then + device.log.info("Migrating existing Schlage Sense Pro device profile to lock-modular-uwb-settings") + configure_profile(device) + emit_supported_values(device) + read_field_of_view(device) + read_intent_mode(device) + end end local function device_added(driver, device) diff --git a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua index 1e17b4c41f..54800e9d0d 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua @@ -551,4 +551,106 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle driver update migration for existing lock-modular Schlage device", + function() + local mock_existing_schlage_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-modular.yml"), + manufacturer_info = { + vendor_id = constants.SCHLAGE_VENDOR_ID, + product_id = constants.SCHLAGE_SENSE_PRO_PRODUCT_ID, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + { + cluster_id = constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + cluster_type = "SERVER", + cluster_revision = 1, + } + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } + } + } + } + }) + + test.socket.capability:__load_capability_definition( + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + 1, + constants.APPROACH_CAP_JSON + ) + test.socket.capability:__load_capability_definition( + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + 1, + constants.MODE_CAP_JSON + ) + + local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_existing_schlage_device) + subscribe_request:merge(clusters.DoorLock.attributes.OperatingMode:subscribe(mock_existing_schlage_device)) + subscribe_request:merge(cluster_base.subscribe(mock_existing_schlage_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) + subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_existing_schlage_device)) + subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_existing_schlage_device)) + + test.mock_device.add_test_device(mock_existing_schlage_device) + test.socket.matter:__expect_send({ mock_existing_schlage_device.id, subscribe_request }) + + mock_existing_schlage_device:expect_metadata_update({ + profile = "lock-modular-uwb-settings", + optional_component_capabilities = { + { + "main", + { + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + } + } + } + }) + test.socket.matter:__expect_send({ + mock_existing_schlage_device.id, + cluster_base.read( + mock_existing_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_FIELD_OF_VIEW_ID, + nil + ) + }) + test.socket.matter:__expect_send({ + mock_existing_schlage_device.id, + cluster_base.read( + mock_existing_schlage_device, + 1, + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, + constants.ATTR_INTENT_MODE_ID, + nil + ) + }) + + test.socket.device_lifecycle:__queue_receive({ mock_existing_schlage_device.id, "init" }) + test.wait_for_events() + end, + { + test_init = function() end + } +) + test.run_registered_tests() From f981d40c8b9f8fd8751ffe7b0c6f496bf333509c Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Mon, 26 Oct 2026 14:03:11 +0900 Subject: [PATCH 3/7] Remove circular supports_capability_by_id check from match_profile_modular --- drivers/SmartThings/matter-lock/src/init.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 480a00ae1a..4634f49341 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -212,8 +212,7 @@ end local function match_profile_modular(driver, device) local enabled_optional_component_capability_pairs = {} local main_component_capabilities = {} - local is_uwb = device:supports_capability_by_id("lockUwbApproachSettings") or - (device.profile.id == "lock-modular-uwb-settings") or + local is_uwb = (device.profile.id == "lock-modular-uwb-settings") or (device.manufacturer_info and device.manufacturer_info.vendor_id == 0x1236 and device.manufacturer_info.product_id == 0x3800) local modular_profile_name = is_uwb and "lock-modular-uwb-settings" or "lock-modular" for _, device_ep in pairs(device.endpoints) do From ea77f732dde50ece31d7417180369eb8e88931ad Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Mon, 26 Oct 2026 14:35:30 +0900 Subject: [PATCH 4/7] Decouple main driver and make Schlage UWB subdriver fully self-contained - Revert changes to src/init.lua, keeping the base driver completely generic - Move all profiling, doConfigure shadowing, and battery/feature attribute handling into schlage-sense-pro subdriver - Follows the self-contained subdriver architecture from matter-sensor and matter-switch - Verify all 177 matter-lock unit tests pass --- drivers/SmartThings/matter-lock/src/init.lua | 13 +---- .../src/schlage-sense-pro/init.lua | 54 ++++++++++++++++--- .../src/test/test_schlage_sense_pro_uwb.lua | 15 ++++++ 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 4634f49341..72bfaae425 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -212,9 +212,7 @@ end local function match_profile_modular(driver, device) local enabled_optional_component_capability_pairs = {} local main_component_capabilities = {} - local is_uwb = (device.profile.id == "lock-modular-uwb-settings") or - (device.manufacturer_info and device.manufacturer_info.vendor_id == 0x1236 and device.manufacturer_info.product_id == 0x3800) - local modular_profile_name = is_uwb and "lock-modular-uwb-settings" or "lock-modular" + local modular_profile_name = "lock-modular" for _, device_ep in pairs(device.endpoints) do for _, ep_cluster in pairs(device_ep.clusters) do if ep_cluster.cluster_id == DoorLock.ID then @@ -238,9 +236,7 @@ local function match_profile_modular(driver, device) if clus_has_feature(DoorLock.types.Feature.UNBOLT) then device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - if not is_uwb then - modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" - end + modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" else device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) @@ -260,11 +256,6 @@ local function match_profile_modular(driver, device) table.insert(main_component_capabilities, capabilities.battery.ID) end - if is_uwb then - table.insert(main_component_capabilities, "lockUwbApproachSettings") - table.insert(main_component_capabilities, "lockUwbModeSettings") - end - table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison or device:supports_capability(capabilities.lockCodes) -- always re-profile a device using the legacy code storage methods diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua index 3098197589..6f38ee385d 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua @@ -14,10 +14,10 @@ --- Subdriver for Allegion / Schlage Sense Pro UWB Door Lock --- ---- Provides integration for: +--- Provides self-contained integration for: --- - lockUwbApproachSettings (Field of View: Front+Left+Right, Front+Left, Front+Right, Front only) --- - lockUwbModeSettings (Intent Mode: Unlock on Approach, Unlock on Arrival, Unlock on Lock Interaction) ---- - Profile management with lock-modular-uwb-settings +--- - Self-contained profile management with lock-modular-uwb-settings --- - Communication via Matter Manufacturer-Specific Cluster (to be detailed by Allegion) local capabilities = require "st.capabilities" @@ -28,6 +28,8 @@ local lock_utils = require "lock_utils" local constants = require "schlage-sense-pro.constants" local DoorLock = clusters.DoorLock +local PowerSource = clusters.PowerSource +local DoorLockFeatureMapAttr = { ID = 0xFFFC, cluster = DoorLock.ID } -- Resolve or build capability representations local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() @@ -239,7 +241,7 @@ local function handle_refresh(driver, device, command) end ------------------------------------------------------------------------------------- --- Device Lifecycle Handlers +-- Device Profiling and Lifecycle Handlers (Self-Contained) ------------------------------------------------------------------------------------- local function configure_profile(device) @@ -292,6 +294,35 @@ local function configure_profile(device) }) end +--- Handles DoorLock FeatureMap attribute updates within the subdriver +local function handle_door_lock_feature_map(driver, device, ib, response) + if ib.data.value == nil then return end + local feature_map = lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id) or nil + if feature_map ~= ib.data.value then + lock_utils.set_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id, ib.data.value, { persist = true }) + configure_profile(device) + end +end + +--- Handles PowerSource AttributeList updates within the subdriver to prevent main driver profile overwrite +local function handle_power_source_attribute_list(driver, device, ib, response) + local latest_battery_support = device:get_field("__BATTERY_SUPPORT") + for _, attr in ipairs(ib.data.elements or {}) do + if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then + device:set_field("__BATTERY_SUPPORT", "BATTERY_PERCENTAGE", { persist = true }) + break + elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID then + device:set_field("__BATTERY_SUPPORT", "BATTERY_LEVEL", { persist = true }) + end + end + if latest_battery_support == nil and not device:get_field("__BATTERY_SUPPORT") then + device:set_field("__BATTERY_SUPPORT", "NO_BATTERY", { persist = true }) + end + if latest_battery_support == nil or latest_battery_support ~= device:get_field("__BATTERY_SUPPORT") then + configure_profile(device) + end +end + local function device_init(driver, device) device.log.info("Schlage Sense Pro UWB subdriver device_init") -- Chain base driver initialization if available @@ -304,7 +335,6 @@ local function device_init(driver, device) if not device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) then device.log.info("Migrating existing Schlage Sense Pro device profile to lock-modular-uwb-settings") configure_profile(device) - emit_supported_values(device) read_field_of_view(device) read_intent_mode(device) end @@ -321,6 +351,7 @@ local function device_added(driver, device) read_intent_mode(device) end +--- Fully self-contained do_configure handler (prevents main driver generic match_profile) local function do_configure(driver, device) device.log.info("Schlage Sense Pro UWB subdriver do_configure") configure_profile(device) @@ -329,9 +360,10 @@ local function do_configure(driver, device) read_field_of_view(device) read_intent_mode(device) - if driver.lifecycle_handlers and driver.lifecycle_handlers.doConfigure then - driver.lifecycle_handlers.doConfigure(driver, device) - end + device:emit_event(capabilities.lock.supportedLockValues({ "locked", "unlocked", "not fully locked" }, { visibility = { displayed = false } })) + device:emit_event(capabilities.lock.supportedLockCommands({ "lock", "unlock" }, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockAlarm.alarm.clear({ state_change = true })) + device:emit_event(capabilities.lockAlarm.supportedAlarmValues({ "unableToLockTheDoor" }, { visibility = { displayed = false } })) end local function driver_switched(driver, device) @@ -368,7 +400,13 @@ local schlage_sense_pro_subdriver = { [constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID] = { [constants.ATTR_FIELD_OF_VIEW_ID] = field_of_view_attr_handler, [constants.ATTR_INTENT_MODE_ID] = intent_mode_attr_handler, - } + }, + [PowerSource.ID] = { + [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, + }, + [DoorLock.ID] = { + [DoorLockFeatureMapAttr.ID] = handle_door_lock_feature_map, + }, }, }, capability_handlers = { diff --git a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua index 54800e9d0d..1faa7c7d43 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua @@ -490,6 +490,21 @@ test.register_coroutine_test( ) ) ) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + capabilities.lockAlarm.alarm.clear({ state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + capabilities.lockAlarm.supportedAlarmValues( + { "unableToLockTheDoor" }, + { visibility = { displayed = false } } + ) + ) + ) test.wait_for_events() end ) From 34b47b70e8415a679c34917731f5dd0fab53dc99 Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Wed, 28 Oct 2026 13:46:48 +0900 Subject: [PATCH 5/7] Enable Schlage UWB settings on the existing modular lock profiles Sharing match_profile_modular avoids a second profile and keeps battery, unlatch, and capability-change checks on one path. Co-authored-by: Cursor --- .../SmartThings/matter-lock/fingerprints.yml | 2 +- .../lock-modular-embedded-unlatch.yml | 6 + .../profiles/lock-modular-uwb-settings.yml | 40 --- .../matter-lock/profiles/lock-modular.yml | 6 + .../matter-lock/src/device_configuration.lua | 163 +++++++++ drivers/SmartThings/matter-lock/src/init.lua | 118 +------ .../src/schlage-sense-pro/can_handle.lua | 13 +- .../src/schlage-sense-pro/constants.lua | 13 +- .../src/schlage-sense-pro/init.lua | 320 ++---------------- .../src/schlage-sense-pro/schlage_utils.lua | 103 ++++++ .../src/test/test_schlage_sense_pro_uwb.lua | 167 +++++---- 11 files changed, 426 insertions(+), 525 deletions(-) delete mode 100644 drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml create mode 100644 drivers/SmartThings/matter-lock/src/device_configuration.lua create mode 100644 drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index d0452d2817..b81ae4c48f 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -202,7 +202,7 @@ matterManufacturer: deviceLabel: Schlage Sense Pro vendorId: 0x1236 productId: 0x3800 - deviceProfileName: lock-modular-uwb-settings + deviceProfileName: lock-modular #Lockly - id: "5212/2" deviceLabel: Lockly Smart Lock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml index 3d9e68b44c..0647b1518c 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -20,6 +20,12 @@ components: - id: lockAliro version: 1 optional: true + - id: lockUwbApproachSettings + version: 1 + optional: true + - id: lockUwbModeSettings + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml deleted file mode 100644 index a5e633b815..0000000000 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular-uwb-settings.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: lock-modular-uwb-settings -components: -- id: main - capabilities: - - id: lock - version: 1 - - id: lockAlarm - version: 1 - - id: remoteControlStatus - version: 1 - - id: lockUsers - version: 1 - optional: true - - id: lockCredentials - version: 1 - optional: true - - id: lockSchedules - version: 1 - optional: true - - id: lockAliro - version: 1 - optional: true - - id: lockUwbApproachSettings - version: 1 - optional: true - - id: lockUwbModeSettings - version: 1 - optional: true - - id: battery - version: 1 - optional: true - - id: batteryLevel - version: 1 - optional: true - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml index 3a8a53bf70..b2fcdc4db1 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -20,6 +20,12 @@ components: - id: lockAliro version: 1 optional: true + - id: lockUwbApproachSettings + version: 1 + optional: true + - id: lockUwbModeSettings + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/src/device_configuration.lua b/drivers/SmartThings/matter-lock/src/device_configuration.lua new file mode 100644 index 0000000000..0e2aa85364 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/device_configuration.lua @@ -0,0 +1,163 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" +local lock_utils = require "lock_utils" + +if version.api < 10 then + clusters.DoorLock = require "DoorLock" +end + +local DoorLock = clusters.DoorLock + +local device_configuration = {} + +device_configuration.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +device_configuration.profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", +} + +local optional_capability_providers = {} + +--- Register extra optional capabilities contributed by a subdriver. +--- The provider is called with the device and returns a list of capability ids, or nil. +--- @param provider fun(device: Device): string[]|nil +function device_configuration.register_optional_capabilities(provider) + table.insert(optional_capability_providers, provider) +end + +local function extra_optional_capabilities(device) + local extras = {} + local seen = {} + for _, provider in ipairs(optional_capability_providers) do + local caps = provider(device) + for _, cap_id in ipairs(caps or {}) do + if not seen[cap_id] then + seen[cap_id] = true + table.insert(extras, cap_id) + end + end + end + return extras +end + +local function profiling_data_still_required(device) + for _, field in pairs(device_configuration.profiling_data) do + if device:get_field(field) == nil then + return true + end + end + return false +end + +function device_configuration.match_profile_modular(driver, device) + local enabled_optional_component_capability_pairs = {} + local main_component_capabilities = {} + local modular_profile_name = "lock-modular" + for _, device_ep in pairs(device.endpoints) do + for _, ep_cluster in pairs(device_ep.clusters) do + if ep_cluster.cluster_id == DoorLock.ID then + local clus_has_feature = function(feature_bitmap) + return DoorLock.are_features_supported( + feature_bitmap, + lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, device_ep.endpoint_id) or + ep_cluster.feature_map + ) + end + if clus_has_feature(DoorLock.types.Feature.USER) then + table.insert(main_component_capabilities, capabilities.lockUsers.ID) + end + if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then + table.insert(main_component_capabilities, capabilities.lockCredentials.ID) + end + if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or + clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then + table.insert(main_component_capabilities, capabilities.lockSchedules.ID) + end + if clus_has_feature(DoorLock.types.Feature.UNBOLT) then + device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + modular_profile_name = "lock-modular-embedded-unlatch" + else + device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end + if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then + table.insert(main_component_capabilities, capabilities.lockAliro.ID) + end + break + end + end + end + + for _, cap_id in ipairs(extra_optional_capabilities(device)) do + table.insert(main_component_capabilities, cap_id) + end + + local supported_battery_type = device:get_field(device_configuration.profiling_data.BATTERY_SUPPORT) + if supported_battery_type == device_configuration.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif supported_battery_type == device_configuration.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) + if modular_profile_name == "lock-modular-embedded-unlatch" + or device:supports_capability(capabilities.lockCodes) + or lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then + device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) + end +end + +local function match_profile_switch(driver, device) + local user_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.USER}) + local pin_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.PIN_CREDENTIAL}) + 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 profile_name = "lock" + if #user_eps > 0 then + profile_name = profile_name .. "-user" + if #pin_eps > 0 then + profile_name = profile_name .. "-pin" + end + if #week_schedule_eps + #year_schedule_eps > 0 then + profile_name = profile_name .. "-schedule" + end + end + if #unbolt_eps > 0 then + profile_name = profile_name .. "-unlatch" + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + else + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end + + local supported_battery_type = device:get_field(device_configuration.profiling_data.BATTERY_SUPPORT) + if supported_battery_type == device_configuration.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif supported_battery_type == device_configuration.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + 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 + +function device_configuration.match_profile(driver, device, ignore_static_profiling) + if profiling_data_still_required(device) then return end + if version.api >= 15 and version.rpc >= 9 then + device_configuration.match_profile_modular(driver, device) + elseif ignore_static_profiling ~= true then + match_profile_switch(driver, device) + end +end + +return device_configuration diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 72bfaae425..23499ca557 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -12,6 +12,7 @@ local version = require "version" if version.api < 10 then clusters.DoorLock = require "DoorLock" end +local device_configuration = require "device_configuration" local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource @@ -46,15 +47,8 @@ local ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP = { ["nonEvictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY } -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local profiling_data = { - BATTERY_SUPPORT = "__BATTERY_SUPPORT", -} +local battery_support = device_configuration.battery_support +local profiling_data = device_configuration.profiling_data local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} local subscribed_attributes = { @@ -209,96 +203,6 @@ local function set_reader_config(device) device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "IN_PROGRESS", {persist = true}) end -local function match_profile_modular(driver, device) - local enabled_optional_component_capability_pairs = {} - local main_component_capabilities = {} - local modular_profile_name = "lock-modular" - for _, device_ep in pairs(device.endpoints) do - for _, ep_cluster in pairs(device_ep.clusters) do - if ep_cluster.cluster_id == DoorLock.ID then - local clus_has_feature = function(feature_bitmap) - return DoorLock.are_features_supported( - feature_bitmap, - lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, device_ep.endpoint_id) or - ep_cluster.feature_map - ) - end - if clus_has_feature(DoorLock.types.Feature.USER) then - table.insert(main_component_capabilities, capabilities.lockUsers.ID) - end - if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then - table.insert(main_component_capabilities, capabilities.lockCredentials.ID) - end - if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or - clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then - table.insert(main_component_capabilities, capabilities.lockSchedules.ID) - end - if clus_has_feature(DoorLock.types.Feature.UNBOLT) then - device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" - else - device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - end - if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then - table.insert(main_component_capabilities, capabilities.lockAliro.ID) - end - break - end - end - end - - local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) - if supported_battery_type == battery_support.BATTERY_LEVEL then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - - table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) - if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison - or device:supports_capability(capabilities.lockCodes) -- always re-profile a device using the legacy code storage methods - or lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then - device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) - end -end - -local function match_profile_switch(driver, device) - local user_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.USER}) - local pin_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.PIN_CREDENTIAL}) - 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 profile_name = "lock" - if #user_eps > 0 then - profile_name = profile_name .. "-user" - if #pin_eps > 0 then - profile_name = profile_name .. "-pin" - end - if #week_schedule_eps + #year_schedule_eps > 0 then - profile_name = profile_name .. "-schedule" - end - end - if #unbolt_eps > 0 then - profile_name = profile_name .. "-unlatch" - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - else - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - end - - local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) - if supported_battery_type == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then - profile_name = profile_name .. "-battery" - 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 - --- Deeply compare two values. --- Handles metatables. Can optionally ignore cycle checking and/or function differences. --- @@ -390,22 +294,8 @@ local function info_changed(driver, device, event, args) end end -local function profiling_data_still_required(device) - for _, field in pairs(profiling_data) do - if device:get_field(field) == nil then - return true -- data still required if a field is nil - end - end - return false -end - local function match_profile(driver, device, ignore_static_profiling) - if profiling_data_still_required(device) then return end - if version.api >= 15 and version.rpc >= 9 then - match_profile_modular(driver, device) - elseif ignore_static_profiling ~= true then - match_profile_switch(driver, device) - end + device_configuration.match_profile(driver, device, ignore_static_profiling) end local function do_configure(driver, device) diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua index 95db3de46d..4ea8be744e 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua @@ -1,16 +1,5 @@ -- Copyright 2026 SmartThings, Inc. --- --- 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. +-- Licensed under the Apache License, Version 2.0 local constants = require "schlage-sense-pro.constants" diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua index 42cea60aaf..ba1ea4afd9 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua @@ -1,16 +1,5 @@ -- Copyright 2026 SmartThings, Inc. --- --- 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. +-- Licensed under the Apache License, Version 2.0 --- Constants and Specification Scaffolding for Allegion / Schlage Sense Pro UWB Door Lock --- diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua index 6f38ee385d..75b4f0e705 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua @@ -1,158 +1,42 @@ -- Copyright 2026 SmartThings, Inc. --- --- 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. +-- Licensed under the Apache License, Version 2.0 --- Subdriver for Allegion / Schlage Sense Pro UWB Door Lock --- ---- Provides self-contained integration for: ---- - lockUwbApproachSettings (Field of View: Front+Left+Right, Front+Left, Front+Right, Front only) ---- - lockUwbModeSettings (Intent Mode: Unlock on Approach, Unlock on Arrival, Unlock on Lock Interaction) ---- - Self-contained profile management with lock-modular-uwb-settings ---- - Communication via Matter Manufacturer-Specific Cluster (to be detailed by Allegion) +--- Enables lockUwbApproachSettings and lockUwbModeSettings as optional capabilities +--- on the shared modular lock profiles, and maps those capabilities onto the +--- Matter manufacturer-specific cluster. local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" +local device_configuration = require "device_configuration" local lock_utils = require "lock_utils" local constants = require "schlage-sense-pro.constants" +local schlage_utils = require "schlage-sense-pro.schlage_utils" local DoorLock = clusters.DoorLock -local PowerSource = clusters.PowerSource -local DoorLockFeatureMapAttr = { ID = 0xFFFC, cluster = DoorLock.ID } - --- Resolve or build capability representations local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() -------------------------------------------------------------------------------------- --- Helper: Endpoint Resolution --- [VERIFICATION REQUIRED BY ALLEGION] -------------------------------------------------------------------------------------- - ---- Find the target endpoint for Schlage UWB custom cluster communication. ---- [PLACEHOLDER - VERIFICATION REQUIRED BY ALLEGION]: ---- Verify the endpoint hosting the manufacturer custom cluster on the physical lock. ---- By default, it searches for an endpoint supporting the custom cluster ID, ---- or falls back to the DoorLock cluster endpoint (typically 1). -local function find_schlage_endpoint(device) - local custom_eps = device:get_endpoints(constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID) - if #custom_eps > 0 then - return custom_eps[1] +device_configuration.register_optional_capabilities(function(device) + local matched = require("schlage-sense-pro.can_handle")(nil, nil, device) + if matched then + return { + constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, + } end - local lock_eps = device:get_endpoints(DoorLock.ID) - if #lock_eps > 0 then - return lock_eps[1] - end - return device.MATTER_DEFAULT_ENDPOINT or 1 -end - -------------------------------------------------------------------------------------- --- Manufacturer-Specific Cluster Communication (Scaffold for Allegion) --- [PLACEHOLDER - ACTION REQUIRED BY ALLEGION] -------------------------------------------------------------------------------------- - ---- Read Field of View attribute from Schlage Custom Cluster. ---- @param device st.matter.Device -local function read_field_of_view(device) - local ep = find_schlage_endpoint(device) - device.log.info(string.format("Reading Schlage UWB Field of View (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", - constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, ep)) - device:send(cluster_base.read(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, nil)) -end - ---- Write Field of View attribute to Schlage Custom Cluster. ---- [PLACEHOLDER - DATA TYPE]: ---- Scaffold uses data_types.Uint8 by default. If the cluster attribute is defined as ---- data_types.Enum8 or another type, update data_types.Uint8 accordingly. ---- @param device st.matter.Device ---- @param raw_value number (0: Front+Left+Right, 1: Front+Left, 2: Front+Right, 3: Front only) -local function write_field_of_view(device, raw_value) - local ep = find_schlage_endpoint(device) - device.log.info(string.format("Writing Schlage UWB Field of View: %d (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", - raw_value, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, ep)) - -- [PLACEHOLDER - DATA TYPE]: Update to data_types.Enum8 or matching cluster data type if needed - local data = data_types.validate_or_build_type(raw_value, data_types.Uint8) - device:send(cluster_base.write(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, nil, data)) -end - ---- Read Intent Mode attribute from Schlage Custom Cluster. ---- @param device st.matter.Device -local function read_intent_mode(device) - local ep = find_schlage_endpoint(device) - device.log.info(string.format("Reading Schlage UWB Intent Mode (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", - constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, ep)) - device:send(cluster_base.read(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, nil)) -end +end) ---- Write Intent Mode attribute to Schlage Custom Cluster. ---- [PLACEHOLDER - DATA TYPE]: ---- Scaffold uses data_types.Uint8 by default. If the cluster attribute is defined as ---- data_types.Enum8 or another type, update data_types.Uint8 accordingly. ---- @param device st.matter.Device ---- @param raw_value number (0: Unlock on Approach, 1: Unlock on Arrival, 2: Unlock on Lock Interaction) -local function write_intent_mode(device, raw_value) - local ep = find_schlage_endpoint(device) - device.log.info(string.format("Writing Schlage UWB Intent Mode: %d (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", - raw_value, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, ep)) - -- [PLACEHOLDER - DATA TYPE]: Update to data_types.Enum8 or matching cluster data type if needed - local data = data_types.validate_or_build_type(raw_value, data_types.Uint8) - device:send(cluster_base.write(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, nil, data)) -end - ---- [PLACEHOLDER - COMMAND INVOKE ALTERNATIVE]: ---- If the cluster uses Matter Invoke Commands instead of Attribute Writes, ---- use the template functions below and invoke them from handle_set_* instead: ---[[ -local function invoke_set_field_of_view(device, raw_value) - local ep = find_schlage_endpoint(device) - local cmd_params = { [0] = raw_value } - local req = cluster_base.build_cluster_command(nil, device, cmd_params, ep, - constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.CMD_SET_FIELD_OF_VIEW_ID, false, false) - device:send(req) -end - -local function invoke_set_intent_mode(device, raw_value) - local ep = find_schlage_endpoint(device) - local cmd_params = { [0] = raw_value } - local req = cluster_base.build_cluster_command(nil, device, cmd_params, ep, - constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.CMD_SET_INTENT_MODE_ID, false, false) - device:send(req) -end ---]] - -------------------------------------------------------------------------------------- --- Capability Event Helpers -------------------------------------------------------------------------------------- - -local function emit_supported_values(device) - if device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) then - device:emit_event(lockUwbApproachSettings.supportedApproachDirections( - constants.SUPPORTED_APPROACH_DIRECTIONS, { visibility = { displayed = false } } - )) - end - if device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS) then - device:emit_event(lockUwbModeSettings.supportedIntentModes( - constants.SUPPORTED_INTENT_MODES, { visibility = { displayed = false } } - )) - end +local function uwb_capabilities_missing(device) + return not device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) + or not device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS) end ------------------------------------------------------------------------------------- -- Matter Attribute Handlers (Device -> SmartThings Hub) ------------------------------------------------------------------------------------- ---- Handles Field of View reports from the Schlage Custom Cluster local function field_of_view_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then local raw_val = ib.data.value @@ -166,7 +50,6 @@ local function field_of_view_attr_handler(driver, device, ib, response) end end ---- Handles Intent Mode reports from the Schlage Custom Cluster local function intent_mode_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then local raw_val = ib.data.value @@ -184,7 +67,6 @@ end -- Capability Command Handlers (SmartThings App -> Device) ------------------------------------------------------------------------------------- ---- Handler for lockUwbApproachSettings:setApproachDirection command local function handle_set_approach_direction(driver, device, command) local val = command.args.approachDirection device.log.info(string.format("Handling setApproachDirection command: %s", tostring(val))) @@ -197,16 +79,14 @@ local function handle_set_approach_direction(driver, device, command) end if raw_val ~= nil and constants.RAW_TO_APPROACH_DIRECTION[raw_val] ~= nil then - write_field_of_view(device, raw_val) - -- Optimistic event update for immediate UX feedback - local enum_val = constants.RAW_TO_APPROACH_DIRECTION[raw_val] - device:emit_event(lockUwbApproachSettings.approachDirection(enum_val)) + if schlage_utils.write_field_of_view(device, raw_val) then + device:emit_event(lockUwbApproachSettings.approachDirection(constants.RAW_TO_APPROACH_DIRECTION[raw_val])) + end else device.log.error(string.format("Invalid approachDirection argument: %s", tostring(val))) end end ---- Handler for lockUwbModeSettings:setIntentMode command local function handle_set_intent_mode(driver, device, command) local val = command.args.intentMode device.log.info(string.format("Handling setIntentMode command: %s", tostring(val))) @@ -219,181 +99,61 @@ local function handle_set_intent_mode(driver, device, command) end if raw_val ~= nil and constants.RAW_TO_INTENT_MODE[raw_val] ~= nil then - write_intent_mode(device, raw_val) - -- Optimistic event update for immediate UX feedback - local enum_val = constants.RAW_TO_INTENT_MODE[raw_val] - device:emit_event(lockUwbModeSettings.intentMode(enum_val)) + if schlage_utils.write_intent_mode(device, raw_val) then + device:emit_event(lockUwbModeSettings.intentMode(constants.RAW_TO_INTENT_MODE[raw_val])) + end else device.log.error(string.format("Invalid intentMode argument: %s", tostring(val))) end end ---- Handler for refresh capability local function handle_refresh(driver, device, command) device.log.info("Refreshing Schlage Sense Pro UWB and Lock State") - -- 1. Standard Door Lock state refresh device:send(DoorLock.attributes.LockState:read(device)) device:set_field(lock_utils.BUSY_STATE, false, { persist = true }) - - -- 2. Schlage UWB settings refresh - read_field_of_view(device) - read_intent_mode(device) + schlage_utils.read_field_of_view(device) + schlage_utils.read_intent_mode(device) end ------------------------------------------------------------------------------------- --- Device Profiling and Lifecycle Handlers (Self-Contained) +-- Lifecycle Handlers ------------------------------------------------------------------------------------- -local function configure_profile(device) - local modular_profile_name = "lock-modular-uwb-settings" - local main_component_capabilities = { - constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, - constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, - } - - -- Check DoorLock features - for _, device_ep in pairs(device.endpoints) do - for _, ep_cluster in pairs(device_ep.clusters) do - if ep_cluster.cluster_id == DoorLock.ID then - local clus_has_feature = function(feature_bitmap) - return DoorLock.are_features_supported( - feature_bitmap, - lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, device_ep.endpoint_id) or - ep_cluster.feature_map - ) - end - if clus_has_feature(DoorLock.types.Feature.USER) then - table.insert(main_component_capabilities, capabilities.lockUsers.ID) - end - if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then - table.insert(main_component_capabilities, capabilities.lockCredentials.ID) - end - if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or - clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then - table.insert(main_component_capabilities, capabilities.lockSchedules.ID) - end - if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then - table.insert(main_component_capabilities, capabilities.lockAliro.ID) - end - break - end - end - end - - local supported_battery_type = device:get_field("__BATTERY_SUPPORT") - if supported_battery_type == "BATTERY_LEVEL" then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif supported_battery_type == "BATTERY_PERCENTAGE" then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - - local enabled_optional = { { "main", main_component_capabilities } } - device:try_update_metadata({ - profile = modular_profile_name, - optional_component_capabilities = enabled_optional - }) -end - ---- Handles DoorLock FeatureMap attribute updates within the subdriver -local function handle_door_lock_feature_map(driver, device, ib, response) - if ib.data.value == nil then return end - local feature_map = lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id) or nil - if feature_map ~= ib.data.value then - lock_utils.set_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id, ib.data.value, { persist = true }) - configure_profile(device) - end -end - ---- Handles PowerSource AttributeList updates within the subdriver to prevent main driver profile overwrite -local function handle_power_source_attribute_list(driver, device, ib, response) - local latest_battery_support = device:get_field("__BATTERY_SUPPORT") - for _, attr in ipairs(ib.data.elements or {}) do - if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then - device:set_field("__BATTERY_SUPPORT", "BATTERY_PERCENTAGE", { persist = true }) - break - elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID then - device:set_field("__BATTERY_SUPPORT", "BATTERY_LEVEL", { persist = true }) - end - end - if latest_battery_support == nil and not device:get_field("__BATTERY_SUPPORT") then - device:set_field("__BATTERY_SUPPORT", "NO_BATTERY", { persist = true }) - end - if latest_battery_support == nil or latest_battery_support ~= device:get_field("__BATTERY_SUPPORT") then - configure_profile(device) - end -end - local function device_init(driver, device) device.log.info("Schlage Sense Pro UWB subdriver device_init") - -- Chain base driver initialization if available if driver.lifecycle_handlers and driver.lifecycle_handlers.init then driver.lifecycle_handlers.init(driver, device) end - -- Automatically migrate previously onboarded devices (e.g. paired prior to driver update - -- under lock-modular) that do not yet have lockUwbApproachSettings to lock-modular-uwb-settings. - if not device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) then - device.log.info("Migrating existing Schlage Sense Pro device profile to lock-modular-uwb-settings") - configure_profile(device) - read_field_of_view(device) - read_intent_mode(device) - end -end - -local function device_added(driver, device) - device.log.info("Schlage Sense Pro UWB subdriver device_added") - if driver.lifecycle_handlers and driver.lifecycle_handlers.added then - driver.lifecycle_handlers.added(driver, device) + if uwb_capabilities_missing(device) then + device.log.info("Enabling Schlage Sense Pro UWB capabilities on the modular lock profile") + device_configuration.match_profile(driver, device, false) + schlage_utils.sync_uwb_settings(device) end - - emit_supported_values(device) - read_field_of_view(device) - read_intent_mode(device) end ---- Fully self-contained do_configure handler (prevents main driver generic match_profile) local function do_configure(driver, device) device.log.info("Schlage Sense Pro UWB subdriver do_configure") - configure_profile(device) - - emit_supported_values(device) - read_field_of_view(device) - read_intent_mode(device) - - device:emit_event(capabilities.lock.supportedLockValues({ "locked", "unlocked", "not fully locked" }, { visibility = { displayed = false } })) - device:emit_event(capabilities.lock.supportedLockCommands({ "lock", "unlock" }, { visibility = { displayed = false } })) - device:emit_event(capabilities.lockAlarm.alarm.clear({ state_change = true })) - device:emit_event(capabilities.lockAlarm.supportedAlarmValues({ "unableToLockTheDoor" }, { visibility = { displayed = false } })) + if driver.lifecycle_handlers and driver.lifecycle_handlers.doConfigure then + driver.lifecycle_handlers.doConfigure(driver, device) + end + schlage_utils.sync_uwb_settings(device) end local function driver_switched(driver, device) device.log.info("Schlage Sense Pro UWB subdriver driver_switched") - configure_profile(device) - emit_supported_values(device) - read_field_of_view(device) - read_intent_mode(device) - device:try_update_metadata({ provisioning_state = "PROVISIONED" }) -end - -local function info_changed(driver, device, event, args) - device.log.info("Schlage Sense Pro UWB subdriver info_changed") - if driver.lifecycle_handlers and driver.lifecycle_handlers.infoChanged then - driver.lifecycle_handlers.infoChanged(driver, device, event, args) + if driver.lifecycle_handlers and driver.lifecycle_handlers.driverSwitched then + driver.lifecycle_handlers.driverSwitched(driver, device) end + schlage_utils.sync_uwb_settings(device) end -------------------------------------------------------------------------------------- --- Subdriver Template Definition -------------------------------------------------------------------------------------- - local schlage_sense_pro_subdriver = { NAME = "schlage-sense-pro", lifecycle_handlers = { init = device_init, - added = device_added, doConfigure = do_configure, driverSwitched = driver_switched, - infoChanged = info_changed, }, matter_handlers = { attr = { @@ -401,12 +161,6 @@ local schlage_sense_pro_subdriver = { [constants.ATTR_FIELD_OF_VIEW_ID] = field_of_view_attr_handler, [constants.ATTR_INTENT_MODE_ID] = intent_mode_attr_handler, }, - [PowerSource.ID] = { - [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, - }, - [DoorLock.ID] = { - [DoorLockFeatureMapAttr.ID] = handle_door_lock_feature_map, - }, }, }, capability_handlers = { diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua new file mode 100644 index 0000000000..6e0e2eac0c --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua @@ -0,0 +1,103 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local constants = require "schlage-sense-pro.constants" + +local DoorLock = clusters.DoorLock +local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() +local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() + +local schlage_utils = {} + +--- Find the endpoint that hosts the Schlage UWB custom cluster. +--- Returns nil when neither the custom cluster nor DoorLock is present. +--- @param device st.matter.Device +--- @return number|nil +function schlage_utils.find_endpoint(device) + local custom_eps = device:get_endpoints(constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID) + if #custom_eps > 0 then + return custom_eps[1] + end + local lock_eps = device:get_endpoints(DoorLock.ID) + if #lock_eps > 0 then + return lock_eps[1] + end + return nil +end + +local function endpoint_or_warn(device, action) + local ep = schlage_utils.find_endpoint(device) + if ep == nil then + device.log.warn(string.format("Schlage UWB endpoint not found; skipping %s", action)) + end + return ep +end + +--- @param device st.matter.Device +function schlage_utils.read_field_of_view(device) + local ep = endpoint_or_warn(device, "Field of View read") + if ep == nil then return end + device.log.info(string.format("Reading Schlage UWB Field of View (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, ep)) + device:send(cluster_base.read(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, nil)) +end + +--- @param device st.matter.Device +--- @param raw_value number +--- @return boolean true when the write was sent +function schlage_utils.write_field_of_view(device, raw_value) + local ep = endpoint_or_warn(device, "Field of View write") + if ep == nil then return false end + device.log.info(string.format("Writing Schlage UWB Field of View: %d (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + raw_value, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, ep)) + local data = data_types.validate_or_build_type(raw_value, data_types.Uint8) + device:send(cluster_base.write(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_FIELD_OF_VIEW_ID, nil, data)) + return true +end + +--- @param device st.matter.Device +function schlage_utils.read_intent_mode(device) + local ep = endpoint_or_warn(device, "Intent Mode read") + if ep == nil then return end + device.log.info(string.format("Reading Schlage UWB Intent Mode (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, ep)) + device:send(cluster_base.read(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, nil)) +end + +--- @param device st.matter.Device +--- @param raw_value number +--- @return boolean true when the write was sent +function schlage_utils.write_intent_mode(device, raw_value) + local ep = endpoint_or_warn(device, "Intent Mode write") + if ep == nil then return false end + device.log.info(string.format("Writing Schlage UWB Intent Mode: %d (Cluster: 0x%08X, Attr: 0x%04X, EP: %d)", + raw_value, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, ep)) + local data = data_types.validate_or_build_type(raw_value, data_types.Uint8) + device:send(cluster_base.write(device, ep, constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID, constants.ATTR_INTENT_MODE_ID, nil, data)) + return true +end + +function schlage_utils.emit_supported_values(device) + if device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS) then + device:emit_event(lockUwbApproachSettings.supportedApproachDirections( + constants.SUPPORTED_APPROACH_DIRECTIONS, { visibility = { displayed = false } } + )) + end + if device:supports_capability_by_id(constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS) then + device:emit_event(lockUwbModeSettings.supportedIntentModes( + constants.SUPPORTED_INTENT_MODES, { visibility = { displayed = false } } + )) + end +end + +--- Emit supported UWB values and read both attributes. +function schlage_utils.sync_uwb_settings(device) + schlage_utils.emit_supported_values(device) + schlage_utils.read_field_of_view(device) + schlage_utils.read_intent_mode(device) +end + +return schlage_utils diff --git a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua index 1faa7c7d43..760f9bcf95 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua @@ -1,16 +1,5 @@ -- Copyright 2026 SmartThings, Inc. --- --- 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. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -36,7 +25,7 @@ local enabled_optional_capabilities = { local mock_schlage_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition( - "lock-modular-uwb-settings.yml", + "lock-modular.yml", { enabled_optional_capabilities = enabled_optional_capabilities } ), manufacturer_info = { @@ -121,6 +110,21 @@ test.register_coroutine_test( }) local other_matched = can_handle(nil, nil, other_device) assert(other_matched == false, "Expected can_handle to return false for non-Schlage device") + + local cluster_device = { + manufacturer_info = { + vendor_id = constants.SCHLAGE_VENDOR_ID, + product_id = 0x0001, + }, + get_endpoints = function(_, cluster_id) + if cluster_id == constants.SCHLAGE_UWB_CUSTOM_CLUSTER_ID then + return { 1 } + end + return {} + end, + } + local cluster_matched = can_handle(nil, nil, cluster_device) + assert(cluster_matched == true, "Expected can_handle to match Allegion vendor plus custom cluster") end ) @@ -422,20 +426,25 @@ test.register_message_test( test.register_coroutine_test( "Handle doConfigure lifecycle event for Schlage Sense Pro UWB lock", function() - mock_schlage_device:expect_metadata_update({ - profile = "lock-modular-uwb-settings", - optional_component_capabilities = { - { - "main", - { - constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, - constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, - } - } - } - }) mock_schlage_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.device_lifecycle:__queue_receive({ mock_schlage_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + capabilities.lock.supportedLockValues( + { "locked", "unlocked", "not fully locked" }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_schlage_device:generate_test_message( + "main", + capabilities.lock.supportedLockCommands( + { "lock", "unlock" }, + { visibility = { displayed = false } } + ) + ) + ) test.socket.capability:__expect_send( mock_schlage_device:generate_test_message( "main", @@ -472,6 +481,15 @@ test.register_coroutine_test( nil ) }) + test.socket.device_lifecycle:__queue_receive({ mock_schlage_device.id, "doConfigure" }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Handle driverSwitched lifecycle event for Schlage Sense Pro UWB lock", + function() + mock_schlage_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.socket.capability:__expect_send( mock_schlage_device:generate_test_message( "main", @@ -490,42 +508,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_schlage_device:generate_test_message( - "main", - capabilities.lockAlarm.alarm.clear({ state_change = true }) - ) - ) - test.socket.capability:__expect_send( - mock_schlage_device:generate_test_message( - "main", - capabilities.lockAlarm.supportedAlarmValues( - { "unableToLockTheDoor" }, - { visibility = { displayed = false } } - ) - ) - ) - test.wait_for_events() - end -) - -test.register_coroutine_test( - "Handle driverSwitched lifecycle event for Schlage Sense Pro UWB lock", - function() - mock_schlage_device:expect_metadata_update({ - profile = "lock-modular-uwb-settings", - optional_component_capabilities = { - { - "main", - { - constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, - constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, - } - } - } - }) - mock_schlage_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.device_lifecycle:__queue_receive({ mock_schlage_device.id, "driverSwitched" }) test.socket.capability:__expect_send( mock_schlage_device:generate_test_message( "main", @@ -562,6 +544,7 @@ test.register_coroutine_test( nil ) }) + test.socket.device_lifecycle:__queue_receive({ mock_schlage_device.id, "driverSwitched" }) test.wait_for_events() end ) @@ -628,7 +611,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({ mock_existing_schlage_device.id, subscribe_request }) mock_existing_schlage_device:expect_metadata_update({ - profile = "lock-modular-uwb-settings", + profile = "lock-modular", optional_component_capabilities = { { "main", @@ -639,6 +622,24 @@ test.register_coroutine_test( } } }) + test.socket.capability:__expect_send( + mock_existing_schlage_device:generate_test_message( + "main", + capabilities.lock.supportedLockValues( + { "locked", "unlocked", "not fully locked" }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_existing_schlage_device:generate_test_message( + "main", + capabilities.lock.supportedLockCommands( + { "lock", "unlock" }, + { visibility = { displayed = false } } + ) + ) + ) test.socket.matter:__expect_send({ mock_existing_schlage_device.id, cluster_base.read( @@ -668,4 +669,44 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "setApproachDirection does not write when no UWB endpoint exists", + function() + local device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition( + "lock-modular.yml", + { enabled_optional_capabilities = enabled_optional_capabilities } + ), + manufacturer_info = { + vendor_id = constants.SCHLAGE_VENDOR_ID, + product_id = constants.SCHLAGE_SENSE_PRO_PRODUCT_ID, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + } + } + }) + + test.mock_device.add_test_device(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.capability:__queue_receive({ + device.id, + { + capability = constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, + component = "main", + command = "setApproachDirection", + args = { "frontOnly" } + } + }) + test.wait_for_events() + end, + { + test_init = function() end + } +) + test.run_registered_tests() From c91147fc8865fab856b345b260fb2333f9f10bfb Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Mon, 2 Nov 2026 11:29:08 +0900 Subject: [PATCH 6/7] Use published UWB capabilities and call the shared profile matcher directly Removes the local capability fallback and calls device_configuration.match_profile from each profiling site. The Schlage optional capabilities are still applied inside that shared match. Co-authored-by: Cursor --- .../matter-lock/src/device_configuration.lua | 1 + drivers/SmartThings/matter-lock/src/init.lua | 14 +- .../src/schlage-sense-pro/can_handle.lua | 7 +- .../src/schlage-sense-pro/constants.lua | 156 +----------------- .../src/schlage-sense-pro/init.lua | 4 +- .../src/schlage-sense-pro/schlage_utils.lua | 5 +- .../src/test/test_schlage_sense_pro_uwb.lua | 109 +++++++++++- 7 files changed, 123 insertions(+), 173 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/device_configuration.lua b/drivers/SmartThings/matter-lock/src/device_configuration.lua index 0e2aa85364..45d87e56c7 100644 --- a/drivers/SmartThings/matter-lock/src/device_configuration.lua +++ b/drivers/SmartThings/matter-lock/src/device_configuration.lua @@ -97,6 +97,7 @@ function device_configuration.match_profile_modular(driver, device) end end + -- Subdriver registrations, including Schlage UWB settings, are applied on every profile match. for _, cap_id in ipairs(extra_optional_capabilities(device)) do table.insert(main_component_capabilities, cap_id) end diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 23499ca557..e1d26d394c 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -294,12 +294,8 @@ local function info_changed(driver, device, event, args) end end -local function match_profile(driver, device, ignore_static_profiling) - device_configuration.match_profile(driver, device, ignore_static_profiling) -end - local function do_configure(driver, device) - match_profile(driver, device, false) + device_configuration.match_profile(driver, device, false) device.thread:call_with_delay(5, function() device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is mandatory @@ -307,7 +303,7 @@ local function do_configure(driver, device) end local function driver_switched(driver, device) - match_profile(driver, device, false) + device_configuration.match_profile(driver, device, false) device:try_update_metadata({provisioning_state = "PROVISIONED"}) end @@ -531,7 +527,7 @@ local function door_lock_feature_map_handler(driver, device, ib, response) local feature_map = lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id) or nil if feature_map ~= ib.data.value then lock_utils.set_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id, ib.data.value, { persist = true }) - match_profile(driver, device, true) + device_configuration.match_profile(driver, device, true) end end @@ -550,7 +546,7 @@ local function handle_power_source_attribute_list(driver, device, ib, response) device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist=true}) end if latest_battery_support == nil or latest_battery_support ~= device:get_field(profiling_data.BATTERY_SUPPORT) then - match_profile(driver, device, false) + device_configuration.match_profile(driver, device, false) end end @@ -2342,7 +2338,7 @@ local function handle_refresh(driver, device, command) else device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist = true}) end - match_profile(driver, device, true) + device_configuration.match_profile(driver, device, true) else device:send(DoorLock.attributes.LockState:read(device)) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua index 4ea8be744e..5c84112b57 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/can_handle.lua @@ -3,12 +3,7 @@ local constants = require "schlage-sense-pro.constants" ---- Matcher for Schlage Sense Pro (Aliro) UWB Door Lock. ---- ---- Supports both initial onboarding and OTA driver updates for previously onboarded locks. ---- [PLACEHOLDER - OPTIONAL / VERIFICATION FOR ALLEGION]: ---- Matches Allegion Vendor ID (0x1236) with either the known Schlage Sense Pro Product ID (0x3800) ---- or the presence of the custom UWB cluster. +--- Matches Allegion vendor ID 0x1236 with product ID 0x3800 or the custom UWB cluster. local function is_schlage_sense_pro(opts, driver, device) if device.manufacturer_info and device.manufacturer_info.vendor_id == constants.SCHLAGE_VENDOR_ID then if device.manufacturer_info.product_id == constants.SCHLAGE_SENSE_PRO_PRODUCT_ID or diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua index ba1ea4afd9..27683c23b1 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/constants.lua @@ -1,33 +1,11 @@ -- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 ---- Constants and Specification Scaffolding for Allegion / Schlage Sense Pro UWB Door Lock +--- Constants for the Schlage Sense Pro UWB door lock. --- ---- Reference Specifications: ---- 1. Confluence: "Schlage Sense Pro (Aliro) - UWB Configuration and Feature Support" (Page 5350097136) ---- 2. Confluence: "Action plan for Schlage UWB configuration" (Page 5392597723) ---- ---- Feature Overview: ---- 1. Field of View (Detection Area): ---- Allowed values per Allegion spec: ---- 0: Front + Left + Right (all directions) ---- 1: Front + Left ---- 2: Front + Right ---- 3: Front only ---- SmartThings Capability: lockUwbApproachSettings ---- ---- 2. Intent Mode (Unlock Behavior): ---- Allowed values per Allegion spec: ---- 0: Unlock on Approach ---- 1: Unlock on Arrival ---- 2: Unlock on Lock Interaction ---- SmartThings Capability: lockUwbModeSettings ---- ---- NOTE FOR ALLEGION ENGINEERS: ---- Update the manufacturer-specific Matter cluster ID, attribute IDs, command IDs, ---- and data types below to match your final Matter cluster specification. - -local capabilities = require "st.capabilities" +--- Field of view values: 0 front+left+right, 1 front+left, 2 front+right, 3 front only. +--- Intent mode values: 0 unlock on approach, 1 unlock on arrival, 2 unlock on lock interaction. +--- Cluster and attribute IDs below are provisional until the manufacturer cluster is finalized. local constants = {} @@ -73,10 +51,8 @@ constants.CMD_SET_INTENT_MODE_ID = 0x0002 constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS = "lockUwbApproachSettings" constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS = "lockUwbModeSettings" ---- Field of View (lockUwbApproachSettings) mappings ---- [PLACEHOLDER / VERIFY]: Update this mapping table if the device's raw attribute values differ from the defaults below. ---- Default per Confluence 5350097136 / 5392597723: ---- 0: Front+Left+Right, 1: Front+Left, 2: Front+Right, 3: Front only +--- Field of view (lockUwbApproachSettings) mappings. +--- 0: front+left+right, 1: front+left, 2: front+right, 3: front only. constants.APPROACH_DIRECTION = { FRONT_LEFT_RIGHT = "frontLeftRight", FRONT_LEFT = "frontLeft", @@ -105,10 +81,8 @@ constants.SUPPORTED_APPROACH_DIRECTIONS = { constants.APPROACH_DIRECTION.FRONT_ONLY, } ---- Intent Mode (lockUwbModeSettings) mappings ---- [PLACEHOLDER / VERIFY]: Update this mapping table if the device's raw attribute values differ from the defaults below. ---- Default per Confluence 5350097136 / 5392597723: ---- 0: Unlock on Approach, 1: Unlock on Arrival, 2: Unlock on Lock Interaction +--- Intent mode (lockUwbModeSettings) mappings. +--- 0: unlock on approach, 1: unlock on arrival, 2: unlock on lock interaction. constants.INTENT_MODE = { UNLOCK_ON_APPROACH = "unlockOnApproach", UNLOCK_ON_ARRIVAL = "unlockOnArrival", @@ -133,118 +107,4 @@ constants.SUPPORTED_INTENT_MODES = { constants.INTENT_MODE.UNLOCK_ON_INTERACTION, } -------------------------------------------------------------------------------------- --- Fallback Capability Builders (Pre-27R1 Compatibility) -------------------------------------------------------------------------------------- - -constants.APPROACH_CAP_JSON = [[{ - "name": "Lock UWB Approach Settings", - "id": "lockUwbApproachSettings", - "version": 1, - "attributes": { - "approachDirection": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "string", - "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] - } - }, - "required": ["value"] - } - }, - "supportedApproachDirections": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "array", - "items": { "type": "string" } - } - }, - "required": ["value"] - } - } - }, - "commands": { - "setApproachDirection": { - "name": "setApproachDirection", - "arguments": [ - { - "name": "approachDirection", - "schema": { - "type": "string", - "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] - }, - "optional": false - } - ] - } - } -}]] - -constants.MODE_CAP_JSON = [[{ - "name": "Lock UWB Mode Settings", - "id": "lockUwbModeSettings", - "version": 1, - "attributes": { - "intentMode": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "string", - "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] - } - }, - "required": ["value"] - } - }, - "supportedIntentModes": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "array", - "items": { "type": "string" } - } - }, - "required": ["value"] - } - } - }, - "commands": { - "setIntentMode": { - "name": "setIntentMode", - "arguments": [ - { - "name": "intentMode", - "schema": { - "type": "string", - "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] - }, - "optional": false - } - ] - } - } -}]] - -local function load_or_build(cap_id, cap_json) - local status, cap = pcall(function() return capabilities[cap_id] end) - if status and cap and pcall(function() return cap.commands end) and cap.commands ~= nil then - return cap - end - return capabilities.build_cap_from_json_string(cap_json) -end - -function constants.get_lock_uwb_approach_settings_capability() - return load_or_build(constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, constants.APPROACH_CAP_JSON) -end - -function constants.get_lock_uwb_mode_settings_capability() - return load_or_build(constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, constants.MODE_CAP_JSON) -end - return constants diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua index 75b4f0e705..6fe3fc31ed 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/init.lua @@ -15,8 +15,8 @@ local constants = require "schlage-sense-pro.constants" local schlage_utils = require "schlage-sense-pro.schlage_utils" local DoorLock = clusters.DoorLock -local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() -local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() +local lockUwbApproachSettings = capabilities.lockUwbApproachSettings +local lockUwbModeSettings = capabilities.lockUwbModeSettings device_configuration.register_optional_capabilities(function(device) local matched = require("schlage-sense-pro.can_handle")(nil, nil, device) diff --git a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua index 6e0e2eac0c..2334548ac2 100644 --- a/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua +++ b/drivers/SmartThings/matter-lock/src/schlage-sense-pro/schlage_utils.lua @@ -1,14 +1,15 @@ -- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local constants = require "schlage-sense-pro.constants" local DoorLock = clusters.DoorLock -local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() -local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() +local lockUwbApproachSettings = capabilities.lockUwbApproachSettings +local lockUwbModeSettings = capabilities.lockUwbModeSettings local schlage_utils = {} diff --git a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua index 760f9bcf95..1404b6e0da 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua @@ -10,8 +10,105 @@ local data_types = require "st.matter.data_types" local constants = require "schlage-sense-pro.constants" local can_handle = require "schlage-sense-pro.can_handle" -local lockUwbApproachSettings = constants.get_lock_uwb_approach_settings_capability() -local lockUwbModeSettings = constants.get_lock_uwb_mode_settings_capability() +local APPROACH_CAP_JSON = [[{ + "name": "Lock UWB Approach Settings", + "id": "lockUwbApproachSettings", + "version": 1, + "attributes": { + "approachDirection": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] + } + }, + "required": ["value"] + } + }, + "supportedApproachDirections": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["value"] + } + } + }, + "commands": { + "setApproachDirection": { + "name": "setApproachDirection", + "arguments": [ + { + "name": "approachDirection", + "schema": { + "type": "string", + "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] + }, + "optional": false + } + ] + } + } +}]] + +local MODE_CAP_JSON = [[{ + "name": "Lock UWB Mode Settings", + "id": "lockUwbModeSettings", + "version": 1, + "attributes": { + "intentMode": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] + } + }, + "required": ["value"] + } + }, + "supportedIntentModes": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["value"] + } + } + }, + "commands": { + "setIntentMode": { + "name": "setIntentMode", + "arguments": [ + { + "name": "intentMode", + "schema": { + "type": "string", + "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] + }, + "optional": false + } + ] + } + } +}]] + +test.socket.capability:__load_capability_definition("lockUwbApproachSettings", 1, APPROACH_CAP_JSON) +test.socket.capability:__load_capability_definition("lockUwbModeSettings", 1, MODE_CAP_JSON) + +local lockUwbApproachSettings = capabilities.lockUwbApproachSettings +local lockUwbModeSettings = capabilities.lockUwbModeSettings local enabled_optional_capabilities = { { @@ -70,12 +167,12 @@ local function test_init() test.socket.capability:__load_capability_definition( constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, 1, - constants.APPROACH_CAP_JSON + APPROACH_CAP_JSON ) test.socket.capability:__load_capability_definition( constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, 1, - constants.MODE_CAP_JSON + MODE_CAP_JSON ) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_schlage_device) @@ -593,12 +690,12 @@ test.register_coroutine_test( test.socket.capability:__load_capability_definition( constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, 1, - constants.APPROACH_CAP_JSON + APPROACH_CAP_JSON ) test.socket.capability:__load_capability_definition( constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, 1, - constants.MODE_CAP_JSON + MODE_CAP_JSON ) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_existing_schlage_device) From 9718d88328aabfb10178b798d916d387e1408ccd Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Mon, 2 Nov 2026 13:16:07 +0900 Subject: [PATCH 7/7] Drop the embedded UWB capability schemas from the Schlage tests. The test harness loads the published definitions, so a second copy in the test is unnecessary. Co-authored-by: Cursor --- .../src/test/test_schlage_sense_pro_uwb.lua | 119 ------------------ 1 file changed, 119 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua index 1404b6e0da..e801051bb1 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_schlage_sense_pro_uwb.lua @@ -10,103 +10,6 @@ local data_types = require "st.matter.data_types" local constants = require "schlage-sense-pro.constants" local can_handle = require "schlage-sense-pro.can_handle" -local APPROACH_CAP_JSON = [[{ - "name": "Lock UWB Approach Settings", - "id": "lockUwbApproachSettings", - "version": 1, - "attributes": { - "approachDirection": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "string", - "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] - } - }, - "required": ["value"] - } - }, - "supportedApproachDirections": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "array", - "items": { "type": "string" } - } - }, - "required": ["value"] - } - } - }, - "commands": { - "setApproachDirection": { - "name": "setApproachDirection", - "arguments": [ - { - "name": "approachDirection", - "schema": { - "type": "string", - "enum": ["frontLeftRight", "frontLeft", "frontRight", "frontOnly"] - }, - "optional": false - } - ] - } - } -}]] - -local MODE_CAP_JSON = [[{ - "name": "Lock UWB Mode Settings", - "id": "lockUwbModeSettings", - "version": 1, - "attributes": { - "intentMode": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "string", - "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] - } - }, - "required": ["value"] - } - }, - "supportedIntentModes": { - "schema": { - "type": "object", - "properties": { - "value": { - "type": "array", - "items": { "type": "string" } - } - }, - "required": ["value"] - } - } - }, - "commands": { - "setIntentMode": { - "name": "setIntentMode", - "arguments": [ - { - "name": "intentMode", - "schema": { - "type": "string", - "enum": ["unlockOnApproach", "unlockOnArrival", "unlockOnInteraction"] - }, - "optional": false - } - ] - } - } -}]] - -test.socket.capability:__load_capability_definition("lockUwbApproachSettings", 1, APPROACH_CAP_JSON) -test.socket.capability:__load_capability_definition("lockUwbModeSettings", 1, MODE_CAP_JSON) - local lockUwbApproachSettings = capabilities.lockUwbApproachSettings local lockUwbModeSettings = capabilities.lockUwbModeSettings @@ -164,17 +67,6 @@ local mock_schlage_device = test.mock_device.build_test_matter_device({ local DoorLockFeatureMapAttr = { ID = 0xFFFC, cluster = clusters.DoorLock.ID } local function test_init() - test.socket.capability:__load_capability_definition( - constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, - 1, - APPROACH_CAP_JSON - ) - test.socket.capability:__load_capability_definition( - constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, - 1, - MODE_CAP_JSON - ) - local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_schlage_device) subscribe_request:merge(clusters.DoorLock.attributes.OperatingMode:subscribe(mock_schlage_device)) subscribe_request:merge(cluster_base.subscribe(mock_schlage_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) @@ -687,17 +579,6 @@ test.register_coroutine_test( } }) - test.socket.capability:__load_capability_definition( - constants.CAPABILITY_LOCK_UWB_APPROACH_SETTINGS, - 1, - APPROACH_CAP_JSON - ) - test.socket.capability:__load_capability_definition( - constants.CAPABILITY_LOCK_UWB_MODE_SETTINGS, - 1, - MODE_CAP_JSON - ) - local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_existing_schlage_device) subscribe_request:merge(clusters.DoorLock.attributes.OperatingMode:subscribe(mock_existing_schlage_device)) subscribe_request:merge(cluster_base.subscribe(mock_existing_schlage_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID))