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 4ea6ba1e0d..3d9e68b44c 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml index 576695f873..3a8a53bf70 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index 5d92c55afa..816ca446f2 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -40,7 +40,17 @@ local lock_utils = { SCHEDULE_END_HOUR = "scheduleEndHour", SCHEDULE_END_MINUTE = "scheduleEndMinute", SCHEDULE_LOCAL_START_TIME = "scheduleLocalStartTime", - SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime" + SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime", + VERIFICATION_KEY = "verificationKey", + GROUP_ID = "groupId", + GROUP_RESOLVING_KEY = "groupResolvingKey", + ISSUER_KEY = "issuerKey", + ISSUER_KEY_INDEX = "issuerKeyIndex", + ENDPOINT_KEY = "endpointKey", + ENDPOINT_KEY_INDEX = "endpointKeyIndex", + ENDPOINT_KEY_TYPE = "endpointKeyType", + DEVICE_KEY_ID = "deviceKeyId", + COMMAND_REQUEST_ID = "commandRequestId" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index aad7046a3d..f9c06f193c 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -27,12 +27,37 @@ end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource -local INITIAL_COTA_INDEX = 1 +local INITIAL_CREDENTIAL_INDEX = 1 local ALL_INDEX = 0xFFFE local MIN_EPOCH_S = 0 local MAX_EPOCH_S = 0xffffffff local THIRTY_YEARS_S = 946684800 -- 1970-01-01T00:00:00 ~ 2000-01-01T00:00:00 +local RESPONSE_STATUS_MAP = { + [DoorLock.types.DlStatus.SUCCESS] = "success", + [DoorLock.types.DlStatus.FAILURE] = "failure", + [DoorLock.types.DlStatus.DUPLICATE] = "duplicate", + [DoorLock.types.DlStatus.OCCUPIED] = "occupied", + [DoorLock.types.DlStatus.INVALID_FIELD] = "invalidCommand", + [DoorLock.types.DlStatus.RESOURCE_EXHAUSTED] = "resourceExhausted", + [DoorLock.types.DlStatus.NOT_FOUND] = "failure" +} + +local WEEK_DAY_MAP = { + ["Sunday"] = 1, + ["Monday"] = 2, + ["Tuesday"] = 4, + ["Wednesday"] = 8, + ["Thursday"] = 16, + ["Friday"] = 32, + ["Saturday"] = 64, +} + +local ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP = { + ["evictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_EVICTABLE_ENDPOINT_KEY, + ["nonEvictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY +} + local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 @@ -86,6 +111,17 @@ local subscribed_attributes = { DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser, DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser }, + [capabilities.lockAliro.ID] = { + DoorLock.attributes.AliroReaderVerificationKey, + DoorLock.attributes.AliroReaderGroupIdentifier, + DoorLock.attributes.AliroReaderGroupSubIdentifier, + DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions, + DoorLock.attributes.AliroGroupResolvingKey, + DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions, + DoorLock.attributes.AliroBLEAdvertisingVersion, + DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported, + DoorLock.attributes.NumberOfAliroEndpointKeysSupported, + }, [capabilities.battery.ID] = { PowerSource.attributes.BatPercentRemaining }, @@ -193,6 +229,9 @@ local function match_profile_modular(driver, 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}})) end + if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then + table.insert(main_component_capabilities, capabilities.lockAliro.ID) + end break end end @@ -295,7 +334,7 @@ local function driver_switched(driver, device) end -- This function check busy_state and if busy_state is false, set it to true(current time) -local function check_busy_state(device) +local function is_busy_state_set(device) local c_time = os.time() local busy_state = device:get_field(lock_utils.BUSY_STATE) or false if busy_state == false or c_time - busy_state > 10 then @@ -404,7 +443,7 @@ local function set_cota_credential(device, credential_index) end -- Check Busy State - if check_busy_state(device) == true then + if is_busy_state_set(device) then device.log.debug("delaying setting COTA credential since a credential is currently being set") device.thread:call_with_delay(2, function(t) set_cota_credential(device, credential_index) @@ -451,7 +490,7 @@ local function apply_cota_credentials_if_absent(device) -- delay needed to allow test to override the random credential data device.thread:call_with_delay(0, function(t) -- Attempt to set cota credential at the lowest index - set_cota_credential(device, INITIAL_COTA_INDEX) + set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) end) end) end @@ -479,6 +518,113 @@ local function max_year_schedule_of_user_handler(driver, device, ib, response) device:emit_event(capabilities.lockSchedules.yearDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) end +---------------- +-- Aliro Util -- +---------------- +local function hex_string_to_octet_string(hex_string) + if hex_string == nil then + return nil + end + local octet_string = "" + for i = 1, #hex_string, 2 do + local hex = hex_string:sub(i, i + 1) + octet_string = octet_string .. string.char(tonumber(hex, 16)) + end + return octet_string +end + +----------------------------------- +-- Aliro Reader Verification Key -- +----------------------------------- +local function aliro_reader_verification_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + utils.bytes_to_hex_string(ib.data.value), {visibility = {displayed = false}} + )) + end +end + +----------------------------------- +-- Aliro Reader Group Identifier -- +----------------------------------- +local function aliro_reader_group_id_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + utils.bytes_to_hex_string(ib.data.value), + {visibility = {displayed = false}} + )) + end +end + +------------------------------------------------------------- +-- Aliro Expedited Transaction Supported Protocol Versions -- +------------------------------------------------------------- +local function aliro_group_resolving_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + utils.bytes_to_hex_string(ib.data.value), + {visibility = {displayed = false}} + )) + end +end + +------------------------------- +-- Aliro Group Resolving Key -- +------------------------------- +local function aliro_protocol_versions_handler(driver, device, ib, response) + if ib.data.elements == nil then + return + end + local protocol_versions = {} + for i, element in ipairs(ib.data.elements) do + local version = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) + table.insert(protocol_versions, version); + end + device:emit_event(capabilities.lockAliro.expeditedTransactionProtocolVersions(protocol_versions, {visibility = {displayed = false}})) +end + +----------------------------------------------- +-- Aliro Supported BLE UWB Protocol Versions -- +----------------------------------------------- +local function aliro_supported_ble_uwb_protocol_versions_handler(driver, device, ib, response) + if ib.data.elements == nil then + return + end + local protocol_versions = {} + for i, element in ipairs(ib.data.elements) do + local version = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) + table.insert(protocol_versions, version); + end + device:emit_event(capabilities.lockAliro.bleUWBProtocolVersions(protocol_versions, {visibility = {displayed = false}})) +end + +----------------------------------- +-- Aliro BLE Advertising Version -- +----------------------------------- +local function aliro_ble_advertising_version_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.bleAdvertisingVersion(string.format("%s", ib.data.value), {visibility = {displayed = false}})) + end +end + +------------------------------------------------------ +-- Number Of Aliro Credential Issuer Keys Supported -- +------------------------------------------------------ +local function max_aliro_credential_issuer_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.maxCredentialIssuerKeys(ib.data.value, {visibility = {displayed = false}})) + end +end + +--------------------------------------------- +-- Number Of Aliro Endpoint Keys Supported -- +--------------------------------------------- +local function max_aliro_endpoint_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.maxEndpointKeys(ib.data.value, {visibility = {displayed = false}})) + end +end + --------------------------------- -- Power Source Attribute List -- --------------------------------- @@ -579,7 +725,7 @@ end ---------------- -- User Table -- ---------------- -local function add_user_to_table(device, userIdx, usrType) +local function add_user_to_table(device, userIdx, userName, userType) -- Get latest user table local user_table = utils.deep_copy(device:get_latest_state( "main", @@ -589,11 +735,11 @@ local function add_user_to_table(device, userIdx, usrType) )) -- Add new entry to table - table.insert(user_table, {userIndex = userIdx, userType = usrType}) + table.insert(user_table, {userIndex = userIdx, userName = userName, userType = userType}) device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end -local function update_user_in_table(device, userIdx, usrType) +local function update_user_in_table(device, userIdx, userName, userType) -- Get latest user table local user_table = utils.deep_copy(device:get_latest_state( "main", @@ -613,7 +759,8 @@ local function update_user_in_table(device, userIdx, usrType) -- Update user entry if i ~= 0 then - user_table[i].userType = usrType + user_table[i].userType = userType + user_table[i].userName = userName device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end end @@ -646,6 +793,40 @@ end ---------------------- -- Credential Table -- ---------------------- +local function has_credentials(device, userIdx) + -- Get latest credential table + local cred_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockCredentials.ID, + capabilities.lockCredentials.credentials.NAME, + {} + )) + + -- Find credential + for index, entry in pairs(cred_table) do + if entry.userIndex == userIdx then + return true + end + end + + -- Get latest Aliro credential table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find Aliro credential + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx then + return true + end + end + + return false +end + local function add_credential_to_table(device, userIdx, credIdx, credType) -- Get latest credential table local cred_table = utils.deep_copy(device:get_latest_state( @@ -664,7 +845,7 @@ local function delete_credential_from_table(device, credIdx) -- If Credential Index is ALL_INDEX, remove all entries from the table if credIdx == ALL_INDEX then device:emit_event(capabilities.lockCredentials.credentials({}, {visibility = {displayed = false}})) - return + return ALL_INDEX end -- Get latest credential table @@ -676,7 +857,7 @@ local function delete_credential_from_table(device, credIdx) )) -- Delete an entry from credential table - local userIdx = 0 + local userIdx = nil for index, entry in pairs(cred_table) do if entry.credentialIndex == credIdx then table.remove(cred_table, index) @@ -717,16 +898,6 @@ end ----------------------------- -- Week Day Schedule Table -- ----------------------------- -local WEEK_DAY_MAP = { - ["Sunday"] = 1, - ["Monday"] = 2, - ["Tuesday"] = 4, - ["Wednesday"] = 8, - ["Thursday"] = 16, - ["Friday"] = 32, - ["Saturday"] = 64, -} - local function add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule) -- Get latest week day schedule table local week_schedule_table = utils.deep_copy(device:get_latest_state( @@ -980,6 +1151,76 @@ local function delete_year_schedule_from_table_as_user(device, userIdx) device:emit_event(capabilities.lockSchedules.yearDaySchedules(new_year_schedule_table, {visibility = {displayed = false}})) end +---------------------------- +-- Aliro Credential Table -- +---------------------------- +local function add_aliro_to_table(device, userIdx, keyIdx, keyType, keyId) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Add new entry to table + table.insert(aliro_table, {userIndex = userIdx, keyIndex = keyIdx, keyType = keyType, keyId = keyId}) + device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) +end + +local function delete_aliro_from_table(device, userIdx, keyType, keyId) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Delete an entry from aliro table + if keyType == "issuerKey" then + for i, entry in pairs(aliro_table) do + if entry.userIndex == userIdx then + table.remove(aliro_table, i) + break + end + end + else + for i, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyId == keyId then + table.remove(aliro_table, i) + break + end + end + end + device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) +end + +local function delete_aliro_from_table_as_user(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockAliro.credentials({}, {visibility = {displayed = false}})) + return + end + + -- Get latest credential table + local aliro_table = device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME + ) or {} + local new_aliro_table = {} + + -- Re-create credential table + for index, entry in pairs(aliro_table) do + if entry.userIndex ~= userIdx then + table.insert(new_aliro_table, entry) + end + end + + device:emit_event(capabilities.lockAliro.credentials(new_aliro_table, {visibility = {displayed = false}})) +end + -------------- -- Add User -- -------------- @@ -990,32 +1231,26 @@ local function handle_add_user(driver, device, command) local userType = command.args.userType -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end -- Save values to field device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, INITIAL_COTA_INDEX, {persist = true}) + device:set_field(lock_utils.USER_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) device:set_field(lock_utils.USER_NAME, userName, {persist = true}) device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) -- Get available user index local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_COTA_INDEX)) + device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) end ----------------- @@ -1026,33 +1261,28 @@ local function handle_update_user(driver, device, command) local cmdName = "updateUser" local userIdx = command.args.userIndex local userName = command.args.userName - local userType = command.args.lockUserType + local userType = command.args.userType local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER if userType == "guest" then userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER end -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end -- Save values to field device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.USER_NAME, userName, {persist = true}) device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) -- Send command @@ -1086,19 +1316,14 @@ local function get_user_response_handler(driver, device, ib, response) end if status ~= "success" then -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1137,19 +1362,14 @@ local function get_user_response_handler(driver, device, ib, response) ) elseif userIdx >= maxUser then -- There's no available user index -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = "resourceExhausted" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) else -- Check next user index device:send(DoorLock.server.commands.GetUser(device, ep, userIdx + 1)) @@ -1163,6 +1383,7 @@ local function set_user_response_handler(driver, device, ib, response) -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) local userIdx = device:get_field(lock_utils.USER_INDEX) + local userName = device:get_field(lock_utils.USER_NAME) local userType = device:get_field(lock_utils.USER_TYPE) local status = "success" if ib.status == DoorLock.types.DlStatus.FAILURE then @@ -1176,28 +1397,23 @@ local function set_user_response_handler(driver, device, ib, response) -- Update User in table if status == "success" then if cmdName == "addUser" then - add_user_to_table(device, userIdx, userType) + add_user_to_table(device, userIdx, userName, userType) elseif cmdName == "updateUser" then - update_user_in_table(device, userIdx, userType) + update_user_in_table(device, userIdx, userName, userType) end else device.log.warn(string.format("Failed to set user: %s", status)) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1210,20 +1426,14 @@ local function handle_delete_user(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1244,20 +1454,14 @@ local function handle_delete_all_users(driver, device, command) local cmdName = "deleteAllUsers" -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1288,6 +1492,7 @@ local function clear_user_response_handler(driver, device, ib, response) if status == "success" then delete_user_from_table(device, userIdx) delete_credential_from_table_as_user(device, userIdx) + delete_aliro_from_table_as_user(device, userIdx) delete_week_schedule_from_table_as_user(device, userIdx) delete_year_schedule_from_table_as_user(device, userIdx) else @@ -1295,18 +1500,14 @@ local function clear_user_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - }) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1327,25 +1528,19 @@ local function handle_add_credential(driver, device, command) end local credential = { credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = INITIAL_COTA_INDEX + credential_index = INITIAL_CREDENTIAL_INDEX } local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1353,7 +1548,7 @@ local function handle_add_credential(driver, device, command) device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, INITIAL_COTA_INDEX, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) device:set_field(lock_utils.CRED_DATA, credData, {persist = true}) -- Send command @@ -1386,20 +1581,14 @@ local function handle_update_credential(driver, device, command) local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1423,19 +1612,10 @@ local function handle_update_credential(driver, device, command) ) end ------------------------------ --- Set Credential Response -- ------------------------------ -local RESPONSE_STATUS_MAP = { - [DoorLock.types.DlStatus.FAILURE] = "failure", - [DoorLock.types.DlStatus.DUPLICATE] = "duplicate", - [DoorLock.types.DlStatus.OCCUPIED] = "occupied", - [DoorLock.types.DlStatus.INVALID_FIELD] = "invalidCommand", - [DoorLock.types.DlStatus.RESOURCE_EXHAUSTED] = "resourceExhausted", - [DoorLock.types.DlStatus.NOT_FOUND] = "failure" -} - -local function set_credential_response_handler(driver, device, ib, response) +--------------------------------- +-- Set Pin Credential Response -- +--------------------------------- +local function set_pin_response_handler(driver, device, ib, response) if ib.status ~= im.InteractionResponse.Status.SUCCESS then device.log.error("Failed to set credential for device") return @@ -1449,9 +1629,10 @@ local function set_credential_response_handler(driver, device, ib, response) local userIdx = device:get_field(lock_utils.USER_INDEX) local userType = device:get_field(lock_utils.USER_TYPE) local credIdx = device:get_field(lock_utils.CRED_INDEX) - local status = "success" local elements = ib.info_block.data.elements - if elements.status.value == DoorLock.types.DlStatus.SUCCESS then + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then -- Don't save user and credential for COTA if cmdName == "addCota" then device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) @@ -1460,7 +1641,7 @@ local function set_credential_response_handler(driver, device, ib, response) -- If user is added also, update User table if userIdx == nil then - add_user_to_table(device, elements.user_index.value, userType) + add_user_to_table(device, elements.user_index.value, nil, userType) end -- Update Credential table @@ -1470,20 +1651,15 @@ local function set_credential_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, credentialIndex = credIdx, statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) -- If User Type is Guest and device support schedule, add default schedule local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) @@ -1513,33 +1689,20 @@ local function set_credential_response_handler(driver, device, ib, response) return end - -- Update commandResult - status = RESPONSE_STATUS_MAP[elements.status.value] + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. device.log.warn(string.format("Failed to set credential: %s", status)) - - -- Set commandResult to error status - if status == "duplicate" and cmdName == "addCota" then - generate_cota_cred_for_device(device) - device.thread:call_with_delay(0, function(t) set_cota_credential(device, credIdx) end) - return - elseif status ~= "occupied" then - local result = { + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { commandName = cmdName, - statusCode = status + statusCode = "resourceExhausted" -- No more available credential index } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - return - end - - if elements.next_credential_index.value ~= nil then + elseif status == "occupied" then -- Get parameters local credIdx = elements.next_credential_index.value local credential = { @@ -1572,60 +1735,264 @@ local function set_credential_response_handler(driver, device, ib, response) userTypeMatter -- User Type ) ) + elseif status == "duplicate" and cmdName == "addCota" then + generate_cota_cred_for_device(device) + device.thread:call_with_delay(0, function(t) set_cota_credential(device, credIdx) end) else - local result = { + local command_result_info = { commandName = cmdName, - statusCode = "resourceExhausted" -- No more available credential index + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end end ------------------------ --- Delete Credential -- ------------------------ -local function handle_delete_credential(driver, device, command) - -- Get parameters - local cmdName = "deleteCredential" - local credIdx = command.args.credentialIndex - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = credIdx, - } +----------------------------------- +-- Set Aliro Credential Response -- +----------------------------------- +local function set_issuer_key_response_handler(driver, device, ib, response) + local cmdName = "setIssuerKey" + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local issuerKeyIndex = device:get_field(lock_utils.ISSUER_KEY_INDEX) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] - -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if status == "success" then + -- Delete field data + device:set_field(lock_utils.ISSUER_KEY, nil, {persist = true}) + + -- If user is added also, update User table + if userIdx == nil then + userIdx = elements.user_index.value + add_user_to_table(device, userIdx, nil, "adminMember") + end + + -- Update Aliro table + add_aliro_to_table(device, userIdx, issuerKeyIndex, "issuerKey", nil) + + -- Update commandResult + local command_result_info = { commandName = cmdName, - statusCode = "busy" + userIndex = userIdx, + requestId = reqId, + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) return end - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + if userIdx ~= nil then + userType = nil + end + local credIdx = elements.next_credential_index.value + local credType = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY + local credData = device:get_field(lock_utils.ISSUER_KEY) + local credential = { + credential_type = credType, + credential_index = credIdx + } - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + -- Save values to field + device:set_field(lock_utils.ISSUER_KEY_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + credential, -- Credential + hex_string_to_octet_string(credData), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_endpoint_key_response_handler(driver, device, ib, response) + local cmdName = "setEndpointKey" + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local keyId = device:get_field(lock_utils.DEVICE_KEY_ID) + local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) + local endpointKeyIndex = device:get_field(lock_utils.ENDPOINT_KEY_INDEX) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then + -- Delete field data + device:set_field(lock_utils.ENDPOINT_KEY, nil, {persist = true}) + + -- If user is added also, update User table + if userIdx == nil then + userIdx = elements.user_index.value + add_user_to_table(device, userIdx, nil, "adminMember") + end + + -- Update Aliro table + add_aliro_to_table(device, userIdx, endpointKeyIndex, keyType, keyId) + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + if userIdx ~= nil then + userType = nil + end + local credIdx = elements.next_credential_index.value + local credType = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType] + local credData = device:get_field(lock_utils.ENDPOINT_KEY) + local credential = { + credential_type = credType, + credential_index = credIdx + } + + -- Save values to field + device:set_field(lock_utils.ENDPOINT_KEY_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + credential, -- Credential + hex_string_to_octet_string(credData), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_credential_response_handler(driver, device, ib, response) + if ib.status ~= im.InteractionResponse.Status.SUCCESS then + device.log.error("Failed to set credential for device") + return + end + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + if cmdName == "addCredential" or cmdName == "updateCredential" or cmdName == "addCota" then + set_pin_response_handler(driver, device, ib, response) + elseif cmdName == "setIssuerKey" then + set_issuer_key_response_handler(driver, device, ib, response) + elseif cmdName == "setEndpointKey" then + set_endpoint_key_response_handler(driver, device, ib, response) + end +end + +----------------------- +-- Delete Credential -- +----------------------- +local function handle_delete_credential(driver, device, command) + -- Get parameters + local cmdName = "deleteCredential" + local credIdx = command.args.credentialIndex + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = credIdx, + } + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) end ---------------------------- @@ -1640,20 +2007,14 @@ local function handle_delete_all_credentials(driver, device, command) } -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1670,42 +2031,71 @@ end -- Clear Credential Response -- ------------------------------- local function clear_credential_response_handler(driver, device, ib, response) - -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local credIdx = device:get_field(lock_utils.CRED_INDEX) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" + if cmdName ~= "deleteCredential" and cmdName ~= "clearEndpointKey" and + cmdName ~= "clearIssuerKey" and cmdName ~= "deleteAllCredentials" then + return end + local status = RESPONSE_STATUS_MAP[ib.status] or "success" + local command_result_info = { commandName = cmdName, statusCode = status } -- default command result + local userIdx = device:get_field(lock_utils.USER_INDEX) + local all_user_credentials_removed = false - -- Delete User in table - local userIdx = 0 - if status == "success" then + if (cmdName == "deleteCredential" or cmdName == "deleteAllCredentials") and status == "success" then + -- Get result from data saved in relevant, associated fields + local credIdx = device:get_field(lock_utils.CRED_INDEX) + + -- find userIdx associated with credIdx, don't use lock utils field in this case userIdx = delete_credential_from_table(device, credIdx) - if userIdx == 0 then - userIdx = nil + if userIdx ~= nil then + all_user_credentials_removed = not has_credentials(device, userIdx) end - else - device.log.warn(string.format("Failed to clear credential: %s", status)) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.credentialIndex = credIdx + elseif cmdName == "clearIssuerKey" and status == "success" then + -- Get result from data saved in relevant, associated fields + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + + delete_aliro_from_table(device, userIdx, "issuerKey", nil) + all_user_credentials_removed = not has_credentials(device, userIdx) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.requestId = reqId + elseif cmdName == "clearEndpointKey" and status == "success" then + -- Get result from data saved in relevant, associated fields + local deviceKeyId = device:get_field(lock_utils.DEVICE_KEY_ID) + local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + + delete_aliro_from_table(device, userIdx, keyType, deviceKeyId) + all_user_credentials_removed = not has_credentials(device, userIdx) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.keyId = deviceKeyId + command_result_info.requestId = reqId + end + + -- user data if credentials were removed + if all_user_credentials_removed then + delete_user_from_table(device, userIdx) + delete_week_schedule_from_table_as_user(device, userIdx) + delete_year_schedule_from_table_as_user(device, userIdx) end -- Update commandResult - local result = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + if cmdName == "deleteCredential" or cmdName == "deleteAllCredentials" then + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + else + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + end device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1730,20 +2120,14 @@ local function handle_set_week_day_schedule(driver, device, command) local endMinute = schedule.endMinute -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1808,20 +2192,15 @@ local function set_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1835,20 +2214,14 @@ local function handle_clear_week_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1885,20 +2258,15 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1942,20 +2310,14 @@ local function handle_set_year_day_schedule(driver, device, command) local localEndTime = command.args.schedule.localEndTime -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2011,20 +2373,15 @@ local function set_year_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -2038,20 +2395,14 @@ local function handle_clear_year_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2160,6 +2511,354 @@ local function handle_refresh(driver, device, command) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end +local function handle_set_reader_config(driver, device, command) + local cmdName = "setReaderConfig" + local signingKey = command.args.signingKey + local verificationKey = command.args.verificationKey + local groupId = command.args.groupId + local groupResolvingKey = nil + local aliro_ble_uwb_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.ALIROBLEUWB}) + if #aliro_ble_uwb_eps > 0 then + groupResolvingKey = command.args.groupResolvingKey + end + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.VERIFICATION_KEY, verificationKey, {persist = true}) + device:set_field(lock_utils.GROUP_ID, groupId, {persist = true}) + device:set_field(lock_utils.GROUP_RESOLVING_KEY, groupResolvingKey, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetAliroReaderConfig( + device, ep, + hex_string_to_octet_string(signingKey), + hex_string_to_octet_string(verificationKey), + hex_string_to_octet_string(groupId), -- Group identification + hex_string_to_octet_string(groupResolvingKey) -- Group resolving key + ) + ) +end + +local function set_aliro_reader_config_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local verificationKey = device:get_field(lock_utils.VERIFICATION_KEY) + local groupId = device:get_field(lock_utils.GROUP_ID) + local groupResolvingKey = device:get_field(lock_utils.GROUP_RESOLVING_KEY) + + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + elseif ib.status == DoorLock.types.DlStatus.SUCCESS then + if verificationKey ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + verificationKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupId ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + groupId, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupResolvingKey ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + groupResolvingKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + end + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) +end + +local function handle_set_card_id(driver, device, command) + if command.args.cardId ~= nil then + device:emit_event(capabilities.lockAliro.cardId(command.args.cardId, {visibility = {displayed = false}})) + end +end + +local function handle_set_issuer_key(driver, device, command) + -- Get parameters + local cmdName = "setIssuerKey" + local userIdx = command.args.userIndex + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local issuerKey = command.args.issuerKey + local reqId = command.args.requestId + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = INITIAL_CREDENTIAL_INDEX + } + + -- Adjustment + if userIdx == 0 then + userIdx = nil + else + userType = nil + end + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.ISSUER_KEY, issuerKey, {persist = true}) + device:set_field(lock_utils.ISSUER_KEY_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + hex_string_to_octet_string(issuerKey), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) +end + +local function handle_clear_issuer_key(driver, device, command) + -- Get parameters + local cmdName = "clearIssuerKey" + local userIdx = command.args.userIndex + local reqId = command.args.requestId + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find issuer key index + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyType == "issuerKey" then + -- Set parameters + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = entry.keyIndex, + } + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + break + end + end +end + +local function handle_set_endpoint_key(driver, device, command) + -- Get parameters + local cmdName = "setEndpointKey" + local userIdx = command.args.userIndex + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local keyId = command.args.keyId + local keyType = command.args.keyType + local endpointKey = command.args.endpointKey + local reqId = command.args.requestId + local dataOpType = DoorLock.types.DataOperationTypeEnum.ADD -- Data Operation Type: Add(0), Modify(2) + local endpointKeyIndex = INITIAL_CREDENTIAL_INDEX + + -- Min user index of commandResult is 1 + -- 0 should convert to nil before busy check + if userIdx == 0 then + userIdx = nil + end + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Adjustment + if userIdx ~= nil then + userType = nil + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find existing endpoint key + for index, entry in pairs(aliro_table) do + if (entry.keyType == "evictableEndpointKey" or entry.keyType == "nonEvictableEndpointKey") and entry.keyId == keyId then + dataOpType = DoorLock.types.DataOperationTypeEnum.MODIFY + endpointKeyIndex = entry.keyIndex + delete_aliro_from_table(device, userIdx, keyType, keyId) + break + end + end + end + + local credential = { + credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], + credential_index = endpointKeyIndex + } + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY, endpointKey, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_INDEX, endpointKeyIndex, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + dataOpType, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + hex_string_to_octet_string(endpointKey), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) +end + +local function handle_clear_endpoint_key(driver, device, command) + -- Get parameters + local cmdName = "clearEndpointKey" + local userIdx = command.args.userIndex + local keyId = command.args.keyId + local keyType = command.args.keyType + local reqId = command.args.requestId + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + local ep = device:component_to_endpoint(command.component) + if keyId == nil then + return + else + -- Find aliro credential + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyId == keyId and entry.keyType == keyType then + -- Set parameters + local credential = { + credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], + credential_index = entry.keyIndex, + } + -- Send command + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + break + end + end + end +end + local new_matter_lock_handler = { NAME = "New Matter Lock Handler", lifecycle_handlers = { @@ -2181,6 +2880,14 @@ local new_matter_lock_handler = { [DoorLock.attributes.RequirePINforRemoteOperation.ID] = require_remote_pin_handler, [DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser.ID] = max_week_schedule_of_user_handler, [DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser.ID] = max_year_schedule_of_user_handler, + [DoorLock.attributes.AliroReaderVerificationKey.ID] = aliro_reader_verification_key_handler, + [DoorLock.attributes.AliroReaderGroupIdentifier.ID] = aliro_reader_group_id_handler, + [DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions.ID] = aliro_protocol_versions_handler, + [DoorLock.attributes.AliroGroupResolvingKey.ID] = aliro_group_resolving_key_handler, + [DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions.ID] = aliro_supported_ble_uwb_protocol_versions_handler, + [DoorLock.attributes.AliroBLEAdvertisingVersion.ID] = aliro_ble_advertising_version_handler, + [DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported.ID] = max_aliro_credential_issuer_key_handler, + [DoorLock.attributes.NumberOfAliroEndpointKeysSupported.ID] = max_aliro_endpoint_key_handler, }, [PowerSource.ID] = { [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, @@ -2204,6 +2911,7 @@ local new_matter_lock_handler = { [DoorLock.server.commands.SetWeekDaySchedule.ID] = set_week_day_schedule_handler, [DoorLock.server.commands.ClearWeekDaySchedule.ID] = clear_week_day_schedule_handler, [DoorLock.server.commands.SetYearDaySchedule.ID] = set_year_day_schedule_handler, + [DoorLock.server.commands.SetAliroReaderConfig.ID] = set_aliro_reader_config_handler, }, }, }, @@ -2233,6 +2941,14 @@ local new_matter_lock_handler = { [capabilities.lockSchedules.commands.setYearDaySchedule.NAME] = handle_set_year_day_schedule, [capabilities.lockSchedules.commands.clearYearDaySchedules.NAME] = handle_clear_year_day_schedule, }, + [capabilities.lockAliro.ID] = { + [capabilities.lockAliro.commands.setReaderConfig.NAME] = handle_set_reader_config, + [capabilities.lockAliro.commands.setCardId.NAME] = handle_set_card_id, + [capabilities.lockAliro.commands.setIssuerKey.NAME] = handle_set_issuer_key, + [capabilities.lockAliro.commands.clearIssuerKey.NAME] = handle_clear_issuer_key, + [capabilities.lockAliro.commands.setEndpointKey.NAME] = handle_set_endpoint_key, + [capabilities.lockAliro.commands.clearEndpointKey.NAME] = handle_clear_endpoint_key, + }, [capabilities.refresh.ID] = {[capabilities.refresh.commands.refresh.NAME] = handle_refresh} }, supported_capabilities = { diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua index 6067d056ff..ab9ac68c88 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua @@ -971,7 +971,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users({{userIndex = 1, userType = "adminMember"}}, {visibility={displayed=false}}) + capabilities.lockUsers.users({{userIndex = 1, userName="Guest1", userType = "adminMember"}}, {visibility={displayed=false}}) ) ) test.socket.capability:__expect_send( @@ -1493,7 +1493,6 @@ test.register_coroutine_test( ), } ) - -- test.wait_for_events() end ) @@ -1745,11 +1744,29 @@ test.register_coroutine_test( capabilities.lockCredentials.credentials({}, {visibility={displayed=false}}) ) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockCredentials.commandResult( - {commandName="deleteAllCredentials", credentialIndex=65534, statusCode="success"}, + {commandName="deleteAllCredentials", userIndex=65534, credentialIndex=65534, statusCode="success"}, {state_change=true, visibility={displayed=false}} ) )