Skip to content

Commit b17cb98

Browse files
Matter Switch: Improve battery profiling
This implements a few changes to the way that devices supporting batteries are profiled: * subscribe to PowerSource.AttributeList rather than reading this attribute, to help prevent failed reads from causing issues with device profiling * update the profile within match_profile rather than power_source_attribute_list_handler * use existing structure for handling waiting for profiling data before attempting a profile update
1 parent 2039f55 commit b17cb98

13 files changed

Lines changed: 956 additions & 645 deletions

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,13 @@ function SwitchLifecycleHandlers.device_init(driver, device)
103103
if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then
104104
device:set_find_child(switch_utils.find_child)
105105
end
106+
if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then
107+
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true})
108+
end
106109
device:extend_device("subscribe", switch_utils.subscribe)
107110
device:subscribe()
108111

109-
-- device energy reporting must be handled cumulatively, periodically, or by both simulatanously.
112+
-- device energy reporting must be handled cumulatively, periodically, or by both simultaneously.
110113
-- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported.
111114
if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID,
112115
{feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then

drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,10 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo
319319
local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
320320
for i, set_ep_info in pairs(set_topology_eps or {}) do
321321
if ib.endpoint_id == set_ep_info.endpoint_id then
322-
-- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
322+
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
323323
switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i)
324324
local available_endpoints_ids = {}
325-
for _, element in pairs(ib.data.elements) do
325+
for _, element in pairs(ib.data.elements or {}) do
326326
table.insert(available_endpoints_ids, element.value)
327327
end
328328
-- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling.
@@ -344,10 +344,10 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response)
344344
local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
345345
for i, tree_ep_info in pairs(tree_topology_eps or {}) do
346346
if ib.endpoint_id == tree_ep_info.endpoint_id then
347-
-- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
347+
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
348348
switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i)
349349
local associated_endpoints_ids = {}
350-
for _, element in pairs(ib.data.elements) do
350+
for _, element in pairs(ib.data.elements or {}) do
351351
table.insert(associated_endpoints_ids, element.value)
352352
end
353353
-- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling.
@@ -382,29 +382,19 @@ function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response
382382
end
383383

384384
function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response)
385-
local profile_name = ""
386-
387-
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
388-
for _, attr in ipairs(ib.data.elements) do
389-
-- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or
390-
-- BatChargeLevel (Attribute ID 0x0E) is present.
391-
if attr.value == 0x0C then
392-
profile_name = "button-battery"
393-
break
394-
elseif attr.value == 0x0E then
395-
profile_name = "button-batteryLevel"
385+
local previous_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT)
386+
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true})
387+
for _, attr in ipairs(ib.data.elements or {}) do
388+
if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then
389+
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist=true})
396390
break
391+
elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID and
392+
device:get_field(fields.profiling_data.BATTERY_SUPPORT) ~= fields.battery_support.BATTERY_PERCENTAGE then -- don't overwrite if percentage support is already detected
393+
device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist=true})
397394
end
398395
end
399-
if profile_name ~= "" then
400-
if #button_eps > 1 then
401-
profile_name = string.format("%d-", #button_eps) .. profile_name
402-
end
403-
404-
if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then
405-
profile_name = profile_name .. "-temperature-humidity"
406-
end
407-
device:try_update_metadata({ profile = profile_name })
396+
if not previous_battery_support or previous_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then
397+
device_cfg.match_profile(driver, device)
408398
end
409399
end
410400

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,16 @@ function ButtonDeviceConfiguration.update_button_profile(device, default_endpoin
123123
if #motion_eps > 0 and (num_button_eps == 3 or num_button_eps == 6) then -- only these two devices are handled
124124
profile_name = profile_name .. "-motion"
125125
end
126-
local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0
127-
if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler
128-
device:send(clusters.PowerSource.attributes.AttributeList:read(device))
129-
else
130-
device:try_update_metadata({profile = profile_name})
126+
local battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT)
127+
if battery_support == fields.battery_support.BATTERY_PERCENTAGE then
128+
profile_name = profile_name .. "-battery"
129+
elseif battery_support == fields.battery_support.BATTERY_LEVEL then
130+
profile_name = profile_name .. "-batteryLevel"
131131
end
132+
if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then
133+
profile_name = "3-button-battery-temperature-humidity"
134+
end
135+
return profile_name
132136
end
133137

134138
function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps)
@@ -238,13 +242,12 @@ function DeviceConfiguration.match_profile(driver, device)
238242
end
239243

240244
-- initialize the main device card with buttons if applicable
241-
local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
242-
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then
243-
ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momemtary_switch_ep_ids)
245+
local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
246+
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momentary_switch_ep_ids) then
247+
updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids)
244248
-- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id.
245-
ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momemtary_switch_ep_ids)
246-
ButtonDeviceConfiguration.configure_buttons(device, momemtary_switch_ep_ids)
247-
return
249+
ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids)
250+
ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids)
248251
end
249252

250253
device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities })
@@ -254,4 +257,4 @@ return {
254257
DeviceCfg = DeviceConfiguration,
255258
SwitchCfg = SwitchDeviceConfiguration,
256259
ButtonCfg = ButtonDeviceConfiguration
257-
}
260+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,20 @@ SwitchFields.switch_category_vendor_overrides = {
139139
SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps"
140140

141141
--- used in tandem with an EP ID. Stores the required electrical tags "-power", "-energy-powerConsumption", etc.
142-
--- for an Electrical Sensor EP with a "primary" endpoint, used during device profling.
142+
--- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling.
143143
SwitchFields.ELECTRICAL_TAGS = "__electrical_tags"
144144

145145
SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated"
146146

147147
SwitchFields.profiling_data = {
148148
POWER_TOPOLOGY = "__power_topology",
149+
BATTERY_SUPPORT = "__battery_support",
150+
}
151+
152+
SwitchFields.battery_support = {
153+
NO_BATTERY = "NO_BATTERY",
154+
BATTERY_LEVEL = "BATTERY_LEVEL",
155+
BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE",
149156
}
150157

151158
SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset"

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,13 @@ function utils.subscribe(device)
493493
-- attributes and not events.
494494
device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, attributes_seen)
495495

496+
-- If the type of battery support has not yet been determined, add the PowerSource AttributeList to the list of
497+
-- subscribed attributes in order to determine which if any battery capability should be used.
498+
if device:get_field(fields.profiling_data.BATTERY_SUPPORT) == nil then
499+
local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID)
500+
subscribe_request:with_info_block(ib)
501+
end
502+
496503
if #subscribe_request.info_blocks > 0 then
497504
device:send(subscribe_request)
498505
end

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

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
-- Copyright © 2024 SmartThings, Inc.
22
-- Licensed under the Apache License, Version 2.0
33

4-
local test = require "integration_test"
5-
local t_utils = require "integration_test.utils"
64
local capabilities = require "st.capabilities"
7-
local utils = require "st.utils"
8-
local dkjson = require "dkjson"
9-
local uint32 = require "st.matter.data_types.Uint32"
105
local clusters = require "st.matter.generated.zap_clusters"
11-
local button_attr = capabilities.button.button
6+
local t_utils = require "integration_test.utils"
7+
local test = require "integration_test"
8+
local uint32 = require "st.matter.data_types.Uint32"
129

1310
-- Mock a 3-button device with temperature and humidity sensor
1411
local aqara_mock_device = test.mock_device.build_test_matter_device({
@@ -100,19 +97,20 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({
10097

10198
local function configure_buttons()
10299
test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)})
103-
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.pushed({state_change = false})))
100+
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false})))
104101

105102
test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)})
106-
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false})))
103+
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false})))
107104

108105
test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)})
109-
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false})))
106+
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false})))
110107
end
111108

112109
local function test_init()
113110
test.disable_startup_messages()
114111
test.mock_device.add_test_device(aqara_mock_device)
115112
local cluster_subscribe_list = {
113+
clusters.PowerSource.server.attributes.AttributeList,
116114
clusters.PowerSource.server.attributes.BatPercentRemaining,
117115
clusters.TemperatureMeasurement.attributes.MeasuredValue,
118116
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
@@ -138,23 +136,16 @@ local function test_init()
138136
test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request})
139137

140138
test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" })
141-
local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read()
142-
test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list})
143-
configure_buttons()
144139
aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
145-
146-
local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data)
147-
device_info_copy.profile.id = "3-button-battery-temperature-humidity"
148-
local device_info_json = dkjson.encode(device_info_copy)
149-
test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "infoChanged", device_info_json })
150-
test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request})
151-
configure_buttons()
152140
end
153141

154142
test.set_test_init_function(test_init)
155143

156144
local function update_profile()
157-
test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(aqara_mock_device, 6, {uint32(0x0C)})})
145+
test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(
146+
aqara_mock_device, 6, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)}
147+
)})
148+
configure_buttons()
158149
aqara_mock_device:expect_metadata_update({ profile = "3-button-battery-temperature-humidity" })
159150
end
160151

@@ -315,7 +306,7 @@ test.register_coroutine_test(
315306
clusters.Switch.events.MultiPressComplete:build_test_event_report(aqara_mock_device, 4, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1})
316307
}
317308
)
318-
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.double({state_change = true})))
309+
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true})))
319310
end
320311
)
321312

@@ -335,7 +326,7 @@ test.register_coroutine_test(
335326
clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 3, {new_position = 1})
336327
}
337328
)
338-
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.held({state_change = true})))
329+
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true})))
339330
test.socket.matter:__queue_receive(
340331
{
341332
aqara_mock_device.id,
@@ -361,7 +352,7 @@ test.register_coroutine_test(
361352
clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 5, {new_position = 1})
362353
}
363354
)
364-
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.held({state_change = true})))
355+
test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.held({state_change = true})))
365356
test.socket.matter:__queue_receive(
366357
{
367358
aqara_mock_device.id,
@@ -382,7 +373,7 @@ test.register_coroutine_test(
382373
}
383374
)
384375
test.socket.capability:__expect_send(
385-
aqara_mock_device:generate_test_message("button1", button_attr.double({state_change = true}))
376+
aqara_mock_device:generate_test_message("button1", capabilities.button.button.double({state_change = true}))
386377
)
387378
end
388379
)
@@ -466,4 +457,3 @@ test.register_coroutine_test(
466457
)
467458

468459
test.run_registered_tests()
469-

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

Lines changed: 31 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -81,58 +81,45 @@ local function set_color_mode(device, endpoint, color_mode)
8181
test.socket.matter:__expect_send({device.id, read_req})
8282
end
8383

84-
local function test_init()
85-
local cluster_subscribe_list = {
86-
clusters.OnOff.attributes.OnOff,
87-
clusters.LevelControl.attributes.CurrentLevel,
88-
clusters.LevelControl.attributes.MaxLevel,
89-
clusters.LevelControl.attributes.MinLevel,
90-
clusters.ColorControl.attributes.CurrentHue,
91-
clusters.ColorControl.attributes.CurrentSaturation,
92-
clusters.ColorControl.attributes.CurrentX,
93-
clusters.ColorControl.attributes.CurrentY,
94-
clusters.ColorControl.attributes.ColorMode,
95-
clusters.ColorControl.attributes.ColorTemperatureMireds,
96-
clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds,
97-
clusters.ColorControl.attributes.ColorTempPhysicalMinMireds,
98-
clusters.IlluminanceMeasurement.attributes.MeasuredValue,
99-
clusters.OccupancySensing.attributes.Occupancy
100-
}
101-
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
102-
for i, cluster in ipairs(cluster_subscribe_list) do
103-
if i > 1 then
104-
subscribe_request:merge(cluster:subscribe(mock_device))
105-
end
84+
local cluster_subscribe_list = {
85+
clusters.OnOff.attributes.OnOff,
86+
clusters.LevelControl.attributes.CurrentLevel,
87+
clusters.LevelControl.attributes.MaxLevel,
88+
clusters.LevelControl.attributes.MinLevel,
89+
clusters.ColorControl.attributes.CurrentHue,
90+
clusters.ColorControl.attributes.CurrentSaturation,
91+
clusters.ColorControl.attributes.CurrentX,
92+
clusters.ColorControl.attributes.CurrentY,
93+
clusters.ColorControl.attributes.ColorMode,
94+
clusters.ColorControl.attributes.ColorTemperatureMireds,
95+
clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds,
96+
clusters.ColorControl.attributes.ColorTempPhysicalMinMireds,
97+
clusters.IlluminanceMeasurement.attributes.MeasuredValue,
98+
clusters.OccupancySensing.attributes.Occupancy
99+
}
100+
101+
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
102+
for i, cluster in ipairs(cluster_subscribe_list) do
103+
if i > 1 then
104+
subscribe_request:merge(cluster:subscribe(mock_device))
106105
end
106+
end
107+
108+
local function test_init()
109+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
110+
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
111+
112+
-- the following subscribe is due to the init event sent by the test framework.
107113
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
108114
test.mock_device.add_test_device(mock_device)
109115
set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION)
110116
end
111117
test.set_test_init_function(test_init)
112118

113119
local function test_init_x_y_color_mode()
114-
local cluster_subscribe_list = {
115-
clusters.OnOff.attributes.OnOff,
116-
clusters.LevelControl.attributes.CurrentLevel,
117-
clusters.LevelControl.attributes.MaxLevel,
118-
clusters.LevelControl.attributes.MinLevel,
119-
clusters.ColorControl.attributes.CurrentHue,
120-
clusters.ColorControl.attributes.CurrentSaturation,
121-
clusters.ColorControl.attributes.CurrentX,
122-
clusters.ColorControl.attributes.CurrentY,
123-
clusters.ColorControl.attributes.ColorMode,
124-
clusters.ColorControl.attributes.ColorTemperatureMireds,
125-
clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds,
126-
clusters.ColorControl.attributes.ColorTempPhysicalMinMireds,
127-
clusters.IlluminanceMeasurement.attributes.MeasuredValue,
128-
clusters.OccupancySensing.attributes.Occupancy
129-
}
130-
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
131-
for i, cluster in ipairs(cluster_subscribe_list) do
132-
if i > 1 then
133-
subscribe_request:merge(cluster:subscribe(mock_device))
134-
end
135-
end
120+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
121+
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
122+
136123
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
137124
test.mock_device.add_test_device(mock_device)
138125
set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY)

0 commit comments

Comments
 (0)