Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions drivers/Aqara/aqara-lock/src/credential_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ local lockCredentialInfo = capabilities["stse.lockCredentialInfo"]

local credential_utils = {}
local HOST_COUNT = "__host_count"
local PERSIST_DATA = "__persist_area"

credential_utils.eventResource = function(table)
local credentialResource = {}
for key, value in pairs(table) do
credentialResource[key] = value
end
return credentialResource
end

credential_utils.backup_data = function(device) -- Back up data the persistent
local credentialInfoTable = utils.deep_copy(device:get_latest_state("main", lockCredentialInfo.ID,
lockCredentialInfo.credentialInfo.NAME, {}))
device:set_field(PERSIST_DATA, credentialInfoTable, { persist = true })
end

credential_utils.sync = function(driver, device)
local table = device:get_field(PERSIST_DATA) or nil
if table ~= nil then
device:emit_event(lockCredentialInfo.credentialInfo(credential_utils.eventResource(table),
{ visibility = { displayed = false } }))
else
credential_utils.backup_data(device)
end
end

credential_utils.save_data = function(driver)
driver.datastore:save()
Expand All @@ -28,6 +53,7 @@ credential_utils.update_remote_control_status = function(driver, device, added)
end

device:set_field(HOST_COUNT, host_cnt, { persist = true })
credential_utils.backup_data(device)
credential_utils.save_data(driver)
end

Expand All @@ -38,6 +64,7 @@ credential_utils.sync_all_credential_info = function(driver, device, command)
end
end
device:emit_event(lockCredentialInfo.credentialInfo(command.args.credentialInfo, { visibility = { displayed = false } }))
credential_utils.backup_data(device)
credential_utils.save_data(driver)
end

Expand Down Expand Up @@ -73,6 +100,7 @@ credential_utils.upsert_credential_info = function(driver, device, command)
end

device:emit_event(lockCredentialInfo.credentialInfo(credentialInfoTable, { visibility = { displayed = false } }))
credential_utils.backup_data(device)
credential_utils.save_data(driver)
end

Expand All @@ -95,6 +123,7 @@ credential_utils.delete_user = function(driver, device, command)
end

device:emit_event(lockCredentialInfo.credentialInfo(credentialInfoTable, { visibility = { displayed = false } }))
credential_utils.backup_data(device)
credential_utils.save_data(driver)
end

Expand All @@ -116,6 +145,7 @@ credential_utils.delete_credential = function(driver, device, command)
end

device:emit_event(lockCredentialInfo.credentialInfo(credentialInfoTable, { visibility = { displayed = false } }))
credential_utils.backup_data(device)
credential_utils.save_data(driver)
end

Expand Down
1 change: 1 addition & 0 deletions drivers/Aqara/aqara-lock/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ local function device_init(self, device)
end
device:emit_event(capabilities.battery.quantity(battery_quantity))
device:emit_event(capabilities.batteryLevel.quantity(battery_quantity))
credential_utils.sync(self, device)
end

local function device_added(self, device)
Expand Down
77 changes: 77 additions & 0 deletions drivers/Aqara/aqara-lock/src/test/test_aqara_lock_L100.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
local test = require "integration_test"
local t_utils = require "integration_test.utils"
local capabilities = require "st.capabilities"
local zigbee_test_utils = require "integration_test.zigbee_test_utils"

local remoteControlStatus = capabilities.remoteControlStatus
local antiLockStatus = capabilities["stse.antiLockStatus"]
test.add_package_capability("antiLockStatus.yaml")
local lockCredentialInfo = capabilities["stse.lockCredentialInfo"]
test.add_package_capability("lockCredentialInfo.yaml")
local lockAlarm = capabilities["lockAlarm"]
test.add_package_capability("lockAlarm.yaml")
local Battery = capabilities.battery
local BatteryLevel = capabilities.batteryLevel
local Lock = capabilities.lock

local PRI_CLU = 0xFCC0

local HOST_COUNT = "__host_count"
local PERSIST_DATA = "__persist_area"

local mock_device = test.mock_device.build_test_zigbee_device(
{
profile = t_utils.get_profile_definition("aqara-lock-battery.yml"),
fingerprinted_endpoint_id = 0x01,
zigbee_endpoints = {
[1] = {
id = 1,
manufacturer = "Lumi",
model = "aqara.lock.akr001",
server_clusters = { PRI_CLU }
}
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()
local function test_init()
local SUPPORTED_ALARM_VALUES = { "damaged", "forcedOpeningAttempt", "unableToLockTheDoor", "notClosedForALongTime",
"highTemperature", "attemptsExceeded" }
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
lockAlarm.supportedAlarmValues(SUPPORTED_ALARM_VALUES, { visibility = { displayed = false } })))
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
Lock.supportedUnlockDirections({"fromInside", "fromOutside"}, { visibility = { displayed = false } })))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.type("AA")))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.type("AA")))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.quantity(6)))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.quantity(6)))
local credentialInfoData = {
{ credentialId = 1, credentialType = "keypad", userId = "1", userLabel = "june", userType = "host" }
}
mock_device:set_field(PERSIST_DATA, credentialInfoData, { persist = true })
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
lockCredentialInfo.credentialInfo(credentialInfoData, { visibility = { displayed = false } })))
test.mock_device.add_test_device(mock_device)
end
test.set_test_init_function(test_init)

test.register_coroutine_test(
"Handle added lifecycle - only regular user",
function()
mock_device:set_field(HOST_COUNT, 1, { persist = true })
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })

test.socket.capability:__expect_send(mock_device:generate_test_message("main",
remoteControlStatus.remoteControlEnabled('true', { visibility = { displayed = false } })))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.battery(100)))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.battery("normal")))
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
lockAlarm.alarm.clear({ visibility = { displayed = false } })))
test.socket.capability:__expect_send(mock_device:generate_test_message("main",
antiLockStatus.antiLockStatus('unknown', { visibility = { displayed = false } })))
test.socket.capability:__expect_send(mock_device:generate_test_message("main", Lock.lock("locked")))
end
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ components:
version: 1
- id: lockAlarm
version: 1
- id: doorState
version: 1
optional: true
- id: remoteControlStatus
version: 1
- id: lockUsers
Expand Down
3 changes: 3 additions & 0 deletions drivers/SmartThings/matter-lock/profiles/lock-modular.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ components:
capabilities:
- id: lock
version: 1
- id: doorState
version: 1
optional: true
- id: lockAlarm
version: 1
- id: remoteControlStatus
Expand Down
175 changes: 174 additions & 1 deletion drivers/SmartThings/matter-lock/src/lock_utils.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
-- Copyright 2022 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local security = require "st.security"
local PUB_KEY_PREFIX = "04"

local lock_utils = {
-- Lock device field names
LOCK_CODES = "lockCodes",
Expand Down Expand Up @@ -39,7 +42,10 @@ local lock_utils = {
ENDPOINT_KEY_INDEX = "endpointKeyIndex",
ENDPOINT_KEY_TYPE = "endpointKeyType",
DEVICE_KEY_ID = "deviceKeyId",
COMMAND_REQUEST_ID = "commandRequestId"
COMMAND_REQUEST_ID = "commandRequestId",
MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED",
ALIRO_READER_CONFIG_UPDATED = "aliroReaderConfigUpdated",
LATEST_DOOR_LOCK_FEATURE_MAP = "latestDoorLockFeatureMap"
}
local capabilities = require "st.capabilities"
local json = require "st.json"
Expand Down Expand Up @@ -102,4 +108,171 @@ end
-- keys are the code slots that ST uses
-- user_index and credential_index are used in the matter commands
--
function lock_utils.get_field_for_endpoint(device, field, endpoint)
return device:get_field(string.format("%s_%d", field, endpoint))
end

function lock_utils.set_field_for_endpoint(device, field, endpoint, value, additional_params)
device:set_field(string.format("%s_%d", field, endpoint), value, additional_params)
end

function lock_utils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list)
local previous_capability_map = {}
local component_sizes = {}
local previous_component_count = 0
for component_name, component in pairs(previous_component_capability_list or {}) do
previous_capability_map[component_name] = {}
component_sizes[component_name] = 0
for _, capability in pairs(component.capabilities or {}) do
if capability.id ~= "lock" and capability.id ~= "lockAlarm" and capability.id ~= "remoteControlStatus" and
capability.id ~= "firmwareUpdate" and capability.id ~= "refresh" then
previous_capability_map[component_name][capability.id] = true
component_sizes[component_name] = component_sizes[component_name] + 1
end
end
previous_component_count = previous_component_count + 1
end

local number_of_components_counted = 0
for _, new_component_capabilities in pairs(new_component_capability_list or {}) do
local component_name = new_component_capabilities[1]
local capability_list = new_component_capabilities[2]
number_of_components_counted = number_of_components_counted + 1
if previous_capability_map[component_name] == nil then
return true
end
for _, capability in ipairs(capability_list) do
if previous_capability_map[component_name][capability] == nil then
return true
end
end
if #capability_list ~= component_sizes[component_name] then
return true
end
end

if number_of_components_counted ~= previous_component_count then
return true
end

return false
end

-- This function check busy_state and if busy_state is false, set it to true(current time)
function lock_utils.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
device:set_field(lock_utils.BUSY_STATE, c_time, {persist = true})
return false
else
return true
end
end

function lock_utils.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

function lock_utils.create_group_id_resolving_key()
math.randomseed(os.time())
local result = string.format("%02x", math.random(0, 255))
for i = 1, 15 do
result = result .. string.format("%02x", math.random(0, 255))
end
return result
end

function lock_utils.generate_keypair(device)
local request_opts = {
key_algorithm = {
type = "ec",
curve = "prime256v1"
},
signature_algorithm = "sha256",
return_formats = {
pem = true,
der = true
},
subject = {
common_name = "reader config"
},
validity_days = 36500,
x509_extensions = {
key_usage = {
critical = true,
digital_signature = true
},
certificate_policies = {
critical = true,
policy_2030_5_self_signed_client = true
}
}
}
local status = security.generate_self_signed_cert(request_opts)
if not status or not status.key_der then
device.log.error("generate_self_signed_cert returned no data")
return nil, nil
end

local der = status.key_der
local privKey, pubKey = nil, nil
-- Helper: Parse ASN.1 length (handles 1-byte and multi-byte lengths)
local function get_length(data, start_pos)
local b = string.byte(data, start_pos)
if not b then return nil, start_pos end

if b < 0x80 then
return b, start_pos + 1
else
local num_bytes = b - 0x80
local len = 0
for i = 1, num_bytes do
len = (len * 256) + string.byte(data, start_pos + i)
end
return len, start_pos + 1 + num_bytes
end
end
-- Start parsing after the initial SEQUENCE tag (0x30)
-- Most keys start: [0x30][Length]. We find the first length to find the start of content.
local _, pos = get_length(der, 2)

while pos < #der do
local tag = string.byte(der, pos)
local len, content_start = get_length(der, pos + 1)
if not len then break end
if tag == 0x04 then
-- PRIVATE KEY: Octet String
privKey = utils.bytes_to_hex_string(string.sub(der, content_start, content_start + len - 1))
elseif tag == 0xA1 then
-- PUBLIC KEY Wrapper: Explicit Tag [1]
-- Inside 0xA1 is a BIT STRING (0x03)
local inner_tag = string.byte(der, content_start)
if inner_tag == 0x03 then
local bit_len, bit_start = get_length(der, content_start + 1)
-- BIT STRINGS have a "leading null byte" (unused bits indicator)
-- We skip that byte (bit_start) and the 0x04 EC prefix to get the raw X/Y coordinates
local actual_key_start = bit_start + 2
local actual_key_len = bit_len - 2
pubKey = PUB_KEY_PREFIX .. utils.bytes_to_hex_string(string.sub(der, actual_key_start, actual_key_start + actual_key_len - 1))
end
end
-- Move pointer to the next tag
pos = content_start + len
end

if not privKey or not pubKey then
device.log.error("Failed to extract keys from DER")
end
return privKey, pubKey
end

return lock_utils
Loading
Loading