Skip to content

Commit 16cc922

Browse files
Update lifecycle events for Eve and Aqara subdrivers
With the changes from #2041, the matter-switch subdrivers should be updated to maintain consistency. This involves: * Moving the initialization code from device_init into do_configure * Implement the driverSwitched lifecycle event * Additionally, improve the lifecycle event testing for the Aqara subdriver, removing the `TEST_CONFIGURE` field and instead leveraging a technique used in other test files to set `device.profile.id` for a mock device.
1 parent 3d2d422 commit 16cc922

File tree

3 files changed

+132
-69
lines changed

3 files changed

+132
-69
lines changed

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

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
115
local capabilities = require "st.capabilities"
216
local clusters = require "st.matter.clusters"
317
local device_lib = require "st.device"
@@ -6,23 +20,23 @@ local log = require "log"
620
local cubeAction = capabilities["stse.cubeAction"]
721
local cubeFace = capabilities["stse.cubeFace"]
822

9-
local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button"
10-
local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE"
11-
12-
-- used in unit testing, since device.profile.id and args.old_st_store.profile.id are always the same
13-
-- and this is to avoid the crash of the test case that occurs when try_update_metadata is performed in the device_init stage.
14-
local TEST_CONFIGURE = "__test_configure"
23+
local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map"
1524
local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease)
1625

26+
local updated_fields = {
27+
{ current_field_name = "__component_to_endpoint_map_button", updated_field_name = COMPONENT_TO_ENDPOINT_MAP }
28+
}
29+
1730
-- after 3 seconds of cubeAction, to automatically change the action status of Plugin UI or Device Card to noAction
1831
local CUBEACTION_TIMER = "__cubeAction_timer"
1932
local CUBEACTION_TIME = 3
2033

2134
local function is_aqara_cube(opts, driver, device)
22-
local name = string.format("%s", device.manufacturer_info.product_name)
23-
if device.network_type == device_lib.NETWORK_TYPE_MATTER and
24-
string.find(name, "Aqara Cube T1 Pro") then
35+
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
36+
local name = string.format("%s", device.manufacturer_info.product_name)
37+
if string.find(name, "Aqara Cube T1 Pro") then
2538
return true
39+
end
2640
end
2741
return false
2842
end
@@ -110,6 +124,17 @@ local function endpoint_to_component(device, endpoint)
110124
return "main"
111125
end
112126

127+
local function check_field_name_updates(device)
128+
for _, field in ipairs(updated_fields) do
129+
if device:get_field(field.current_field_name) then
130+
if field.updated_field_name ~= nil then
131+
device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true})
132+
end
133+
device:set_field(field.current_field_name, nil)
134+
end
135+
end
136+
end
137+
113138
-- This is called either on add for parent/child devices, or after the device profile changes for components
114139
local function configure_buttons(device)
115140
if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then
@@ -145,50 +170,50 @@ local function set_configure(driver, device)
145170
current_component_number = current_component_number + 1
146171
end
147172

148-
device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true})
173+
device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true})
149174
device:try_update_metadata({profile = "cube-t1-pro"})
150175
configure_buttons(device)
151176
end
152177

153178
local function device_init(driver, device)
154179
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
180+
check_field_name_updates(device)
155181
device:subscribe()
156182
device:set_endpoint_to_component_fn(endpoint_to_component)
157-
158-
-- when unit testing, call set_configure elsewhere
159-
if not device:get_field(TEST_CONFIGURE) then
160-
set_configure(driver, device)
161-
end
162183
end
163184
end
164185

165186
local function device_added(driver, device)
166-
if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then
167-
device:set_field(DEFERRED_CONFIGURE, true)
187+
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
188+
device_init(driver, device)
168189
end
169190
end
170191

171-
local function info_changed(driver, device, event, args)
172-
-- for unit testing
173-
if device:get_field(TEST_CONFIGURE) then
192+
local function do_configure(driver, device)
193+
-- when unit testing, call set_configure elsewhere
194+
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
174195
set_configure(driver, device)
175196
end
197+
end
176198

177-
if (device.profile.id ~= args.old_st_store.profile.id or device:get_field(TEST_CONFIGURE))
178-
and device:get_field(DEFERRED_CONFIGURE)
179-
and device.network_type ~= device_lib.NETWORK_TYPE_CHILD then
199+
local function driver_switched(driver, device)
200+
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
201+
set_configure(driver, device)
202+
end
203+
end
180204

205+
local function info_changed(driver, device, event, args)
206+
if device.profile.id ~= args.old_st_store.profile.id and
207+
device.network_type == device_lib.NETWORK_TYPE_MATTER then
181208
reset_thread(device)
182209
device:emit_event(cubeAction.cubeAction("flipToSide1"))
183210
device:emit_event(cubeFace.cubeFace("face1Up"))
184-
185-
device:set_field(DEFERRED_CONFIGURE, nil)
186211
end
187212
end
188213

189214
local function initial_press_event_handler(driver, device, ib, response)
190215
if get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then
191-
local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON) or {}
216+
local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {}
192217
local face = 1
193218
for component, ep in pairs(map) do
194219
if map[component] == ib.endpoint_id then
@@ -214,7 +239,9 @@ local aqara_cube_handler = {
214239
lifecycle_handlers = {
215240
init = device_init,
216241
added = device_added,
217-
infoChanged = info_changed
242+
infoChanged = info_changed,
243+
doConfigure = do_configure,
244+
driverSwitched = driver_switched
218245
},
219246
matter_handlers = {
220247
attr = {
@@ -232,4 +259,3 @@ local aqara_cube_handler = {
232259
}
233260

234261
return aqara_cube_handler
235-

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- Copyright 2023 SmartThings
1+
-- Copyright 2025 SmartThings
22
--
33
-- Licensed under the Apache License, Version 2.0 (the "License");
44
-- you may not use this file except in compliance with the License.
@@ -23,10 +23,13 @@ local utils = require "st.utils"
2323
local data_types = require "st.matter.data_types"
2424
local device_lib = require "st.device"
2525

26-
local SWITCH_INITIALIZED = "__switch_intialized"
2726
local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map"
2827
local ON_OFF_STATES = "ON_OFF_STATES"
2928

29+
local updated_fields = {
30+
{ current_field_name = "__switch_intialized", updated_field_name = nil }
31+
}
32+
3033
local EVE_MANUFACTURER_ID = 0x130A
3134
local PRIVATE_CLUSTER_ID = 0x130AFC01
3235

@@ -203,8 +206,6 @@ local function initialize_switch(driver, device)
203206
end
204207
end
205208
end
206-
207-
device:set_field(SWITCH_INITIALIZED, true)
208209
end
209210

210211
local function component_to_endpoint(device, component)
@@ -245,18 +246,25 @@ local function set_on_off_state(device, endpoint, value)
245246
device:set_field(ON_OFF_STATES, map)
246247
end
247248

249+
local function check_field_name_updates(device)
250+
for _, field in ipairs(updated_fields) do
251+
if device:get_field(field.current_field_name) then
252+
if field.updated_field_name ~= nil then
253+
device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true})
254+
end
255+
device:set_field(field.current_field_name, nil)
256+
end
257+
end
258+
end
259+
248260

249261
-------------------------------------------------------------------------------------
250262
-- Device Management
251263
-------------------------------------------------------------------------------------
252264

253265
local function device_init(driver, device)
254266
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
255-
if not device:get_field(COMPONENT_TO_ENDPOINT_MAP) and
256-
not device:get_field(SWITCH_INITIALIZED) then
257-
-- create child devices as needed for multi-switch devices
258-
initialize_switch(driver, device)
259-
end
267+
check_field_name_updates(device)
260268
device:set_component_to_endpoint_fn(component_to_endpoint)
261269
device:set_endpoint_to_component_fn(endpoint_to_component)
262270
device:set_find_child(find_child)
@@ -277,6 +285,19 @@ local function device_removed(driver, device)
277285
delete_poll_schedule(device)
278286
end
279287

288+
local function do_configure(driver, device)
289+
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
290+
-- create child devices as needed for multi-switch devices
291+
initialize_switch(driver, device)
292+
end
293+
end
294+
295+
local function driver_switched(driver, device)
296+
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
297+
initialize_switch(driver, device)
298+
end
299+
end
300+
280301
local function handle_refresh(self, device)
281302
requestData(device)
282303
end
@@ -368,6 +389,8 @@ local eve_energy_handler = {
368389
init = device_init,
369390
added = device_added,
370391
removed = device_removed,
392+
doConfigure = do_configure,
393+
driverSwitched = driver_switched
371394
},
372395
matter_handlers = {
373396
attr = {

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

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
115
local test = require "integration_test"
216
test.add_package_capability("cubeAction.yml")
317
test.add_package_capability("cubeFace.yml")
418
local capabilities = require "st.capabilities"
519
local cubeAction = capabilities["stse.cubeAction"]
620
local cubeFace = capabilities["stse.cubeFace"]
721

8-
local t_utils = require "integration_test.utils"
922
local clusters = require "st.matter.clusters"
10-
11-
-- used in unit testing, since device.profile.id and args.old_st_store.profile.id are always the same
12-
-- and this is to avoid the crash of the test case that occurs when try_update_metadata is performed in the device_init stage.
13-
local TEST_CONFIGURE = "__test_configure"
23+
local dkjson = require "dkjson"
24+
local t_utils = require "integration_test.utils"
25+
local utils = require "st.utils"
1426

1527
--mock the actual device1
1628
local mock_device = test.mock_device.build_test_matter_device(
@@ -146,16 +158,22 @@ local CLUSTER_SUBSCRIBE_LIST ={
146158
}
147159

148160
local function test_init()
149-
local opts = { persist = true }
150-
mock_device:set_field(TEST_CONFIGURE, true, opts)
151-
152161
local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device)
153162
for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do
154163
if i > 1 then
155164
subscribe_request:merge(clus:subscribe(mock_device))
156165
end
157166
end
158167
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
168+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
169+
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
170+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
171+
mock_device:expect_metadata_update({ profile = "cube-t1-pro" })
172+
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
173+
local device_info_copy = utils.deep_copy(mock_device.raw_st_data)
174+
device_info_copy.profile.id = "matter-thing"
175+
local device_info_json = dkjson.encode(device_info_copy)
176+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json })
159177
test.mock_device.add_test_device(mock_device)
160178
test.socket.capability:__expect_send(
161179
mock_device:generate_test_message("main", cubeAction.cubeAction({value = "flipToSide1"}))
@@ -168,16 +186,22 @@ end
168186
test.set_test_init_function(test_init)
169187

170188
local function test_init_exhausted()
171-
local opts = { persist = true }
172-
mock_device_exhausted:set_field(TEST_CONFIGURE, true, opts)
173-
174189
local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_exhausted)
175190
for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do
176191
if i > 1 then
177192
subscribe_request:merge(clus:subscribe(mock_device_exhausted))
178193
end
179194
end
180195
test.socket.matter:__expect_send({mock_device_exhausted.id, subscribe_request})
196+
test.socket.device_lifecycle:__queue_receive({ mock_device_exhausted.id, "added" })
197+
test.socket.matter:__expect_send({mock_device_exhausted.id, subscribe_request})
198+
test.socket.device_lifecycle:__queue_receive({ mock_device_exhausted.id, "doConfigure" })
199+
mock_device_exhausted:expect_metadata_update({ profile = "cube-t1-pro" })
200+
mock_device_exhausted:expect_metadata_update({ provisioning_state = "PROVISIONED" })
201+
local device_info_copy = utils.deep_copy(mock_device_exhausted.raw_st_data)
202+
device_info_copy.profile.id = "matter-thing"
203+
local device_info_json = dkjson.encode(device_info_copy)
204+
test.socket.device_lifecycle:__queue_receive({ mock_device_exhausted.id, "infoChanged", device_info_json })
181205
test.mock_device.add_test_device(mock_device_exhausted)
182206
test.socket.capability:__expect_send(
183207
mock_device_exhausted:generate_test_message("main", cubeAction.cubeAction({value = "flipToSide1"}))
@@ -188,22 +212,13 @@ local function test_init_exhausted()
188212
end
189213

190214
test.register_coroutine_test(
191-
"Handle single press sequence when changing the device_lifecycle",
215+
"Handle single press sequence",
192216
function()
193-
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
194-
test.mock_devices_api._expected_device_updates[mock_device.device_id] = "00000000-1111-2222-3333-000000000001"
195-
test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"}
196-
test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="cube-t1-pro"}
197-
198-
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({value = "face1Up"}))
199-
-- let the driver run
200-
test.wait_for_events()
201-
202217
test.socket.matter:__queue_receive(
203218
{
204219
mock_device.id,
205220
clusters.Switch.events.InitialPress:build_test_event_report(
206-
mock_device, 2, {new_position = 1} --move to position 1?
221+
mock_device, 2, {new_position = 1}
207222
)
208223
}
209224
)
@@ -236,20 +251,11 @@ test.register_coroutine_test(
236251
test.register_coroutine_test(
237252
"Handle single press sequence in case of exhausted endpoint",
238253
function()
239-
test.socket.device_lifecycle:__queue_receive({ mock_device_exhausted.id, "added" })
240-
test.mock_devices_api._expected_device_updates[mock_device_exhausted.device_id] = "00000000-1111-2222-3333-000000000003"
241-
test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000003"}
242-
test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000003", profileReference="cube-t1-pro"}
243-
244-
test.socket.device_lifecycle:__queue_receive(mock_device_exhausted:generate_info_changed({value = "face1Up"}))
245-
-- let the driver run
246-
test.wait_for_events()
247-
248254
test.socket.matter:__queue_receive(
249255
{
250256
mock_device_exhausted.id,
251257
clusters.Switch.events.InitialPress:build_test_event_report(
252-
mock_device_exhausted, 250, {new_position = 1} --move to position 1?
258+
mock_device_exhausted, 250, {new_position = 1}
253259
)
254260
}
255261
)
@@ -265,6 +271,14 @@ test.register_coroutine_test(
265271
{ test_init = test_init_exhausted }
266272
)
267273

274+
test.register_coroutine_test(
275+
"Test driver switched event",
276+
function()
277+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" })
278+
mock_device:expect_metadata_update({ profile = "cube-t1-pro" })
279+
end
280+
)
281+
268282
-- run the tests
269283
test.run_registered_tests()
270284

0 commit comments

Comments
 (0)