Skip to content

Commit 85a0fc0

Browse files
committed
update profile checking logic in infoChanged handler
1 parent f343c8b commit 85a0fc0

10 files changed

Lines changed: 228 additions & 47 deletions

File tree

drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ local MIN_EPOCH_S = 0
2222
local MAX_EPOCH_S = 0xffffffff
2323
local THIRTY_YEARS_S = 946684800 -- 1970-01-01T00:00:00 ~ 2000-01-01T00:00:00
2424

25-
local MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED"
26-
2725
local RESPONSE_STATUS_MAP = {
2826
[DoorLock.types.DlStatus.SUCCESS] = "success",
2927
[DoorLock.types.DlStatus.FAILURE] = "failure",
@@ -203,7 +201,6 @@ local function match_profile_modular(driver, device)
203201

204202
table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities})
205203
device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs})
206-
device:set_field(MODULAR_PROFILE_UPDATED, true)
207204
end
208205

209206
local function match_profile_switch(driver, device)
@@ -241,11 +238,63 @@ local function match_profile_switch(driver, device)
241238
device:try_update_metadata({profile = profile_name})
242239
end
243240

241+
--- Deeply compare two values (including tables and their metatables).
242+
---
243+
--- @param a any
244+
--- @param b any
245+
--- @return boolean
246+
local function deep_equals(a, b)
247+
if a == b then return true end -- same object
248+
if type(a) ~= type(b) then return false end -- different type
249+
if type(a) ~= "table" then return false end -- same type but not table, thus was already compared
250+
-- Compare keys/values from a
251+
for k, v in pairs(a) do
252+
if not deep_equals(v, rawget(b, k)) then
253+
return false
254+
end
255+
end
256+
-- Check for keys in b that are not in a
257+
for k in pairs(b) do
258+
if rawget(a, k) == nil then
259+
return false
260+
end
261+
end
262+
-- Compare metatables
263+
local mt_a = getmetatable(a)
264+
local mt_b = getmetatable(b)
265+
return deep_equals(mt_a, mt_b)
266+
end
267+
268+
--- Check if the profile has changed by comparing the latest profile with the previous profile.
269+
---
270+
--- @param latest_profile any
271+
--- @param previous_profile any
272+
--- @return boolean
273+
local function profile_changed(latest_profile, previous_profile)
274+
-- Check if the profile id has changed
275+
if latest_profile.id ~= previous_profile.id then
276+
return true
277+
end
278+
-- Check if all component and capability info in the latest profile existed in the previous profile
279+
for _, component in pairs(latest_profile.components) do
280+
local previous_component = previous_profile.components[component.id]
281+
if not previous_component or not deep_equals(component.capabilities, previous_component.capabilities) then
282+
return true
283+
end
284+
end
285+
-- Check if any components in the previous profile no longer exist in the latest profile
286+
for _, component in pairs(previous_profile.components) do
287+
if not latest_profile.components[component.id] then
288+
return true
289+
end
290+
end
291+
return false
292+
end
293+
244294
local function info_changed(driver, device, event, args)
245-
if device.profile.id == args.old_st_store.profile.id and not device:get_field(MODULAR_PROFILE_UPDATED) then
295+
if not profile_changed(device.profile, args.old_st_store.profile) then
246296
return
247297
end
248-
device:set_field(MODULAR_PROFILE_UPDATED, nil)
249298
for cap_id, attributes in pairs(subscribed_attributes) do
250299
if device:supports_capability_by_id(cap_id) then
251300
for _, attr in ipairs(attributes) do

drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,54 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device)
7777
end
7878
end
7979

80-
function AirQualitySensorUtils.profile_changed(synced_components, prev_components)
81-
if #synced_components ~= #prev_components then
80+
--- Deeply compare two values (including tables and their metatables).
81+
---
82+
--- @param a any
83+
--- @param b any
84+
--- @return boolean
85+
function AirQualitySensorUtils.deep_equals(a, b)
86+
if a == b then return true end -- same object
87+
if type(a) ~= type(b) then return false end -- different type
88+
if type(a) ~= "table" then return false end -- same type but not table, thus was already compared
89+
-- Compare keys/values from a
90+
for k, v in pairs(a) do
91+
if not AirQualitySensorUtils.deep_equals(v, rawget(b, k)) then
92+
return false
93+
end
94+
end
95+
-- Check for keys in b that are not in a
96+
for k in pairs(b) do
97+
if rawget(a, k) == nil then
98+
return false
99+
end
100+
end
101+
-- Compare metatables
102+
local mt_a = getmetatable(a)
103+
local mt_b = getmetatable(b)
104+
return AirQualitySensorUtils.deep_equals(mt_a, mt_b)
105+
end
106+
107+
--- Check if the profile has changed by comparing the latest profile with the previous profile.
108+
---
109+
--- @param latest_profile any
110+
--- @param previous_profile any
111+
--- @return boolean
112+
function AirQualitySensorUtils.profile_changed(latest_profile, previous_profile)
113+
-- Check if the profile id has changed
114+
if latest_profile.id ~= previous_profile.id then
82115
return true
83116
end
84-
for _, component in pairs(synced_components or {}) do
85-
if (prev_components[component.id] == nil) or
86-
(#component.capabilities ~= #prev_components[component.id].capabilities) then
117+
-- Check if all component and capability info in the latest profile existed in the previous profile
118+
for _, component in pairs(latest_profile.components) do
119+
local previous_component = previous_profile.components[component.id]
120+
if not previous_component or not AirQualitySensorUtils.deep_equals(component.capabilities, previous_component.capabilities) then
87121
return true
88122
end
89-
for _, capability in pairs(component.capabilities or {}) do
90-
if prev_components[component.id][capability.id] == nil then
91-
return true
92-
end
123+
end
124+
-- Check if any components in the previous profile no longer exist in the latest profile
125+
for _, component in pairs(previous_profile.components) do
126+
if not latest_profile.components[component.id] then
127+
return true
93128
end
94129
end
95130
return false

drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device)
6666
end
6767

6868
function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args)
69-
if device.profile.id ~= args.old_st_store.profile.id or
70-
aqs_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then
69+
if aqs_utils.profile_changed(device.profile, args.old_st_store.profile) then
7170
if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then
7271
--re-up subscription with new capabilities using the modular supports_capability override
7372
device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular)

drivers/SmartThings/matter-switch/src/init.lua

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ function SwitchLifecycleHandlers.driver_switched(driver, device)
6464
end
6565

6666
function SwitchLifecycleHandlers.info_changed(driver, device, event, args)
67-
if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then
68-
device:set_field(fields.MODULAR_PROFILE_UPDATED, nil)
67+
if switch_utils.profile_changed(device.profile, args.old_st_store.profile) then
6968
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
7069
device:subscribe()
7170
button_cfg.configure_buttons(device,

drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -134,24 +134,6 @@ function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate,
134134
return resolutions
135135
end
136136

137-
function CameraUtils.profile_changed(synced_components, prev_components)
138-
if #synced_components ~= #prev_components then
139-
return true
140-
end
141-
for _, component in pairs(synced_components or {}) do
142-
if (prev_components[component.id] == nil) or
143-
(#component.capabilities ~= #prev_components[component.id].capabilities) then
144-
return true
145-
end
146-
for _, capability in pairs(component.capabilities or {}) do
147-
if prev_components[component.id][capability.id] == nil then
148-
return true
149-
end
150-
end
151-
end
152-
return false
153-
end
154-
155137
function CameraUtils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list)
156138
local previous_capability_map = {}
157139
local component_sizes = {}

drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function CameraLifecycleHandlers.driver_switched(driver, device)
4747
end
4848

4949
function CameraLifecycleHandlers.info_changed(driver, device, event, args)
50-
if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then
50+
if switch_utils.profile_changed(device.profile, args.old_st_store.profile) then
5151
camera_cfg.initialize_camera_capabilities(device)
5252
device:subscribe()
5353
if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then

drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ function DeviceConfiguration.match_profile(driver, device)
238238
local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN)
239239
if #fan_device_type_ep_ids > 0 then
240240
updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id)
241-
device:set_field(fields.MODULAR_PROFILE_UPDATED, true)
242241
end
243242

244243
-- initialize the main device card with buttons if applicable

drivers/SmartThings/matter-switch/src/switch_utils/fields.lua

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,6 @@ SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps"
147147
--- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling.
148148
SwitchFields.ELECTRICAL_TAGS = "__electrical_tags"
149149

150-
SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated"
151-
152150
SwitchFields.profiling_data = {
153151
POWER_TOPOLOGY = "__power_topology",
154152
BATTERY_SUPPORT = "__battery_support",

drivers/SmartThings/matter-switch/src/switch_utils/utils.lua

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,59 @@ function utils.create_multi_press_values_list(size, supportsHeld)
325325
return list
326326
end
327327

328+
--- Deeply compare two values (including tables and their metatables).
329+
---
330+
--- @param a any
331+
--- @param b any
332+
--- @return boolean
333+
function utils.deep_equals(a, b)
334+
if a == b then return true end -- same object
335+
if type(a) ~= type(b) then return false end -- different type
336+
if type(a) ~= "table" then return false end -- same type but not table, thus was already compared
337+
-- Compare keys/values from a
338+
for k, v in pairs(a) do
339+
if not utils.deep_equals(v, rawget(b, k)) then
340+
return false
341+
end
342+
end
343+
-- Check for keys in b that are not in a
344+
for k in pairs(b) do
345+
if rawget(a, k) == nil then
346+
return false
347+
end
348+
end
349+
-- Compare metatables
350+
local mt_a = getmetatable(a)
351+
local mt_b = getmetatable(b)
352+
return utils.deep_equals(mt_a, mt_b)
353+
end
354+
355+
--- Check if the profile has changed by comparing the latest profile with the previous profile.
356+
---
357+
--- @param latest_profile any
358+
--- @param previous_profile any
359+
--- @return boolean
360+
function utils.profile_changed(latest_profile, previous_profile)
361+
-- Check if the profile id has changed
362+
if latest_profile.id ~= previous_profile.id then
363+
return true
364+
end
365+
-- Check if all component and capability info in the latest profile existed in the previous profile
366+
for _, component in pairs(latest_profile.components) do
367+
local previous_component = previous_profile.components[component.id]
368+
if not previous_component or not utils.deep_equals(component.capabilities, previous_component.capabilities) then
369+
return true
370+
end
371+
end
372+
-- Check if any components in the previous profile no longer exist in the latest profile
373+
for _, component in pairs(previous_profile.components) do
374+
if not latest_profile.components[component.id] then
375+
return true
376+
end
377+
end
378+
return false
379+
end
380+
328381
function utils.detect_bridge(device)
329382
return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0
330383
end

drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ local mock_device_ep2 = 2
1616

1717
local mock_device = test.mock_device.build_test_matter_device({
1818
label = "Matter Fan Light",
19-
profile = t_utils.get_profile_definition("fan-modular.yml", {}),
19+
profile = t_utils.get_profile_definition("fan-modular.yml",
20+
{enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}}),
2021
manufacturer_info = {
2122
vendor_id = 0x0000,
2223
product_id = 0x0000,
@@ -58,6 +59,40 @@ local mock_device = test.mock_device.build_test_matter_device({
5859
}
5960
})
6061

62+
local mock_device_capabilities_disabled = test.mock_device.build_test_matter_device({
63+
label = "Matter Fan Light",
64+
profile = t_utils.get_profile_definition("fan-modular.yml",
65+
{enabled_optional_capabilities = {{"main", {}}}}),
66+
manufacturer_info = {
67+
vendor_id = 0x0000,
68+
product_id = 0x0000,
69+
},
70+
matter_version = {
71+
software = 1,
72+
hardware = 1,
73+
},
74+
endpoints = {
75+
{
76+
endpoint_id = 0,
77+
clusters = {
78+
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
79+
},
80+
device_types = {
81+
{device_type_id = 0x0016, device_type_revision = 1} -- RootNode
82+
}
83+
},
84+
{
85+
endpoint_id = mock_device_ep2,
86+
clusters = {
87+
{cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15},
88+
},
89+
device_types = {
90+
{device_type_id = 0x002B, device_type_revision = 1,} -- Fan
91+
}
92+
}
93+
}
94+
})
95+
6196
local CLUSTER_SUBSCRIBE_LIST ={
6297
clusters.OnOff.attributes.OnOff,
6398
clusters.LevelControl.attributes.CurrentLevel,
@@ -110,16 +145,48 @@ local function test_init()
110145
})
111146
mock_device:expect_metadata_update({ profile = "fan-modular", optional_component_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}} })
112147
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
113-
114-
local updated_device_profile = t_utils.get_profile_definition("fan-modular.yml",
115-
{enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}}
116-
)
117-
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile }))
118-
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
119148
end
120149

121150
test.set_test_init_function(test_init)
122151

152+
test.register_coroutine_test(
153+
"Component-capability update without profile ID update should cause re-subscribe in infoChanged handler", function()
154+
local cluster_subscribe_list ={
155+
clusters.FanControl.attributes.FanModeSequence,
156+
clusters.FanControl.attributes.FanMode,
157+
clusters.FanControl.attributes.PercentCurrent,
158+
}
159+
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_capabilities_disabled)
160+
for i, clus in ipairs(cluster_subscribe_list) do
161+
if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_capabilities_disabled)) end
162+
end
163+
test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed(
164+
{profile = {id = "00000000-1111-2222-3333-000000000004", components = { main = {capabilities={["fanSpeedPercent"] = {id="fanSpeedPercent", version=1}, ["fanMode"] = {id="fanMode", version=1}, ["firmwareUpdate"] = {id="firmwareUpdate", version=1}, ["refresh"] = {id="refresh", version=1}}}}}})
165+
)
166+
test.socket.matter:__expect_send({mock_device_capabilities_disabled.id, subscribe_request})
167+
end,
168+
{ test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end }
169+
)
170+
171+
test.register_coroutine_test(
172+
"No component-capability update an no profile ID update should not cause a re-subscribe in infoChanged handler", function()
173+
local cluster_subscribe_list ={
174+
clusters.FanControl.attributes.FanModeSequence,
175+
clusters.FanControl.attributes.FanMode,
176+
clusters.FanControl.attributes.PercentCurrent,
177+
}
178+
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_capabilities_disabled)
179+
for i, clus in ipairs(cluster_subscribe_list) do
180+
if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_capabilities_disabled)) end
181+
end
182+
test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed(
183+
{profile = {id = "00000000-1111-2222-3333-000000000004", components = { main = {capabilities={["firmwareUpdate"] = {id="firmwareUpdate", version=1}, ["refresh"] = {id="refresh", version=1}}}}}})
184+
)
185+
end,
186+
{ test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end }
187+
)
188+
189+
123190
test.register_coroutine_test(
124191
"Switch capability should send the appropriate commands", function()
125192
test.socket.capability:__queue_receive(

0 commit comments

Comments
 (0)