Skip to content

Commit 07cf40e

Browse files
committed
Add greater dynamic profiling to matter switch
1 parent f525c8f commit 07cf40e

File tree

8 files changed

+184
-166
lines changed

8 files changed

+184
-166
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function SwitchLifecycleHandlers.device_init(driver, device)
101101
id = math.max(id, dt.device_type_id)
102102
end
103103
for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do
104-
if id == fields.GENERIC_SWITCH_ID and
104+
if id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH and
105105
attr ~= clusters.PowerSource.attributes.BatPercentRemaining and
106106
attr ~= clusters.PowerSource.attributes.BatChargeLevel then
107107
device:add_subscribed_event(attr)

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ local mock_device = test.mock_device.build_test_matter_device({
5656
{cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}
5757
},
5858
device_types = {
59-
{ device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug
59+
{ device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit
6060
}
6161
}
6262
},
@@ -88,10 +88,20 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({
8888
{ device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor
8989
}
9090
},
91+
{
92+
endpoint_id = 2,
93+
clusters = {
94+
{ cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, },
95+
},
96+
device_types = {
97+
{ device_type_id = 0x010A, device_type_revision = 1 }, -- On Off Plug In Unit
98+
}
99+
}
91100
},
92101
})
93102

94103
local subscribed_attributes_periodic = {
104+
clusters.OnOff.attributes.OnOff,
95105
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported,
96106
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
97107
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ local function test_init_mounted_on_off_control()
505505
test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request})
506506

507507
test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" })
508+
mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" })
508509
mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" })
509510
end
510511

@@ -526,6 +527,7 @@ local function test_init_mounted_dimmable_load_control()
526527
test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request})
527528

528529
test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" })
530+
mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" })
529531
mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" })
530532
end
531533

@@ -566,6 +568,7 @@ local function test_init_parent_child_different_types()
566568
test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request})
567569

568570
test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" })
571+
mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" })
569572
mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" })
570573

571574
mock_device_parent_child_different_types:expect_device_create({
@@ -617,6 +620,7 @@ local function test_init_light_level_motion()
617620
test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request})
618621

619622
test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" })
623+
mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" })
620624
mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" })
621625
end
622626

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ local function test_init()
189189
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
190190

191191
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
192+
mock_device:expect_metadata_update({ profile = "light-binary" })
192193
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
193194

194195
for _, child in pairs(mock_children) do
@@ -260,6 +261,7 @@ local function test_init_parent_child_endpoints_non_sequential()
260261
test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request})
261262

262263
test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" })
264+
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" })
263265
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" })
264266

265267
for _, child in pairs(mock_children_non_sequential) do

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ local function test_init()
146146
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
147147

148148
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
149+
mock_device:expect_metadata_update({ profile = "plug-binary" })
149150
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
150151

151152
for _, child in pairs(mock_children) do
@@ -196,6 +197,7 @@ local function test_init_child_profile_override()
196197
test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request})
197198

198199
test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" })
200+
mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" })
199201
mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" })
200202

201203
for _, child in pairs(mock_children_child_profile_override) do

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

Lines changed: 92 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -31,83 +31,69 @@ local DeviceConfiguration = {}
3131
local SwitchDeviceConfiguration = {}
3232
local ButtonDeviceConfiguration = {}
3333

34-
function SwitchDeviceConfiguration.assign_child_profile(device, child_ep)
35-
local profile
36-
37-
for _, ep in ipairs(device.endpoints) do
38-
if ep.endpoint_id == child_ep then
39-
-- Some devices report multiple device types which are a subset of
40-
-- a superset device type (For example, Dimmable Light is a superset of
41-
-- On/Off light). This mostly applies to the four light types, so we will want
42-
-- to match the profile for the superset device type. This can be done by
43-
-- matching to the device type with the highest ID
44-
local id = 0
45-
for _, dt in ipairs(ep.device_types) do
46-
id = math.max(id, dt.device_type_id)
34+
function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, onoff_ep_id, is_child_device)
35+
local ep = switch_utils.get_endpoint_info(device, onoff_ep_id)
36+
local primary_dt_id = switch_utils.find_max_subset_device_type(ep, fields.DEVICE_TYPE_ID.LIGHT)
37+
or (switch_utils.detect_matter_thing(device) and switch_utils.find_max_subset_device_type(ep, fields.DEVICE_TYPE_ID.SWITCH))
38+
or ep.device_types[1] and ep.device_types[1].device_type_id
39+
local profile = fields.device_type_profile_map[primary_dt_id]
40+
41+
if is_child_device then
42+
-- Check if device has an overridden child profile that differs from the profile that would match
43+
-- the child's device type for the following two cases:
44+
-- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption)
45+
-- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is
46+
-- determined in the "for" loop above (e.g., light-binary)
47+
-- 2. The selected profile for the child device matches the initial profile defined in
48+
-- child_device_profile_overrides
49+
for _, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do
50+
for _, fingerprint in ipairs(vendor) do
51+
if device.manufacturer_info.product_id == fingerprint.product_id and
52+
((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and onoff_ep_id == 1) or profile == fingerprint.initial_profile) then
53+
return fingerprint.target_profile
54+
end
4755
end
48-
profile = fields.device_type_profile_map[id]
49-
break
5056
end
51-
end
5257

53-
-- Check if device has an overridden child profile that differs from the profile that would match
54-
-- the child's device type for the following two cases:
55-
-- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption)
56-
-- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is
57-
-- determined in the "for" loop above (e.g., light-binary)
58-
-- 2. The selected profile for the child device matches the initial profile defined in
59-
-- child_device_profile_overrides
60-
for id, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do
61-
for _, fingerprint in ipairs(vendor) do
62-
if device.manufacturer_info.product_id == fingerprint.product_id and
63-
((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then
64-
return fingerprint.target_profile
65-
end
66-
end
58+
-- default to "switch-binary" if no profile is found
59+
return profile or "switch-binary"
6760
end
6861

69-
-- default to "switch-binary" if no profile is found
70-
return profile or "switch-binary"
62+
return profile
7163
end
7264

73-
function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint)
74-
local num_switch_server_eps = 0
75-
local parent_child_device = false
76-
local switch_eps = device:get_endpoints(clusters.OnOff.ID)
77-
table.sort(switch_eps)
78-
for idx, ep in ipairs(switch_eps) do
79-
if device:supports_server_cluster(clusters.OnOff.ID, ep) then
80-
num_switch_server_eps = num_switch_server_eps + 1
81-
if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint
82-
local name = string.format("%s %d", device.label, num_switch_server_eps)
83-
local child_profile = SwitchDeviceConfiguration.assign_child_profile(device, ep)
84-
driver:try_create_device(
85-
{
86-
type = "EDGE_CHILD",
87-
label = name,
88-
profile = child_profile,
89-
parent_device_id = device.id,
90-
parent_assigned_child_key = string.format("%d", ep),
91-
vendor_provided_label = name
92-
}
93-
)
94-
parent_child_device = true
95-
if idx == 1 and string.find(child_profile, "energy") then
96-
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
97-
device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true})
98-
end
65+
function SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, main_endpoint_id)
66+
if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == main_endpoint_id then -- no children will be created
67+
return
68+
end
69+
70+
local device_num = 0
71+
table.sort(server_onoff_ep_ids)
72+
for idx, ep_id in ipairs(server_onoff_ep_ids) do
73+
device_num = device_num + 1
74+
if ep_id ~= main_endpoint_id then -- don't create a child device that maps to the main endpoint
75+
local name = string.format("%s %d", device.label, device_num)
76+
local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true)
77+
driver:try_create_device(
78+
{
79+
type = "EDGE_CHILD",
80+
label = name,
81+
profile = child_profile,
82+
parent_device_id = device.id,
83+
parent_assigned_child_key = string.format("%d", ep_id),
84+
vendor_provided_label = name
85+
}
86+
)
87+
if idx == 1 and string.find(child_profile, "energy") then
88+
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
89+
device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep_id, {persist = true})
9990
end
10091
end
10192
end
10293

103-
-- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches
104-
-- is only run once, but find_child function should be set on each driver init.
105-
if parent_child_device then
106-
device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true})
107-
end
108-
109-
-- this is needed in initialize_buttons_and_switches
110-
return num_switch_server_eps
94+
-- Persist so that the find_child function is always set on each driver init.
95+
device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true})
96+
device:set_find_child(switch_utils.find_child)
11197
end
11298

11399
function SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint)
@@ -117,7 +103,7 @@ function SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(dev
117103
if main_endpoint == ep.endpoint_id then
118104
for _, dt in ipairs(ep.device_types) do
119105
-- no device type that is not in the switch subset should be considered.
120-
if (fields.ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= fields.ON_OFF_COLOR_DIMMER_SWITCH_ID) then
106+
if (fields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT <= dt.device_type_id and dt.device_type_id <= fields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER) then
121107
cluster_id = math.max(cluster_id, dt.device_type_id)
122108
end
123109
end
@@ -199,75 +185,56 @@ end
199185

200186
-- [[ PROFILE MATCHING AND CONFIGURATIONS ]] --
201187

202-
function DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint)
203-
local profile_found = false
204-
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
205-
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then
206-
ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, #button_eps)
207-
-- All button endpoints found will be added as additional components in the profile containing the main_endpoint.
208-
-- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field
209-
ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps)
210-
ButtonDeviceConfiguration.configure_buttons(device)
211-
profile_found = true
212-
end
213-
214-
-- Without support for bindings, only clusters that are implemented as server are counted. This count is handled
215-
-- while building switch child profiles
216-
local num_switch_server_eps = SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint)
217-
218-
-- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings.
219-
-- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'.
220-
-- Note: since their device type isn't supported, these devices join as a matter-thing.
221-
if num_switch_server_eps > 0 and switch_utils.detect_matter_thing(device) then
222-
SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint)
223-
profile_found = true
224-
end
225-
return profile_found
226-
end
227-
228188
function DeviceConfiguration.match_profile(driver, device)
229-
local main_endpoint = switch_utils.find_default_endpoint(device)
230-
-- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices.
231-
local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint)
232-
if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then
233-
device:set_find_child(switch_utils.find_child)
234-
end
235-
if profile_found then
236-
return
237-
end
189+
local main_endpoint_id = switch_utils.find_default_endpoint(device)
190+
local updated_profile = nil
238191

239-
local fan_eps = device:get_endpoints(clusters.FanControl.ID)
240-
local level_eps = device:get_endpoints(clusters.LevelControl.ID)
241-
local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID)
242-
local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID)
243192
local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID)
244-
local profile_name = nil
245-
local level_support = ""
246-
if #level_eps > 0 then
247-
level_support = "-level"
248-
end
249-
if #energy_eps > 0 and #power_eps > 0 then
250-
profile_name = "plug" .. level_support .. "-power-energy-powerConsumption"
251-
elseif #energy_eps > 0 then
252-
profile_name = "plug" .. level_support .. "-energy-powerConsumption"
253-
elseif #power_eps > 0 then
254-
profile_name = "plug" .. level_support .. "-power"
255-
elseif #valve_eps > 0 then
256-
profile_name = "water-valve"
193+
if #valve_eps > 0 then
194+
updated_profile = "water-valve"
257195
if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID,
258196
{feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then
259-
profile_name = profile_name .. "-level"
197+
updated_profile = updated_profile .. "-level"
260198
end
261-
elseif #fan_eps > 0 then
262-
profile_name = "light-color-level-fan"
263199
end
264-
if profile_name then
265-
device:try_update_metadata({ profile = profile_name })
200+
201+
local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID, { cluster_type = "SERVER" })
202+
if #server_onoff_ep_ids > 0 then
203+
SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, main_endpoint_id)
204+
updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, main_endpoint_id)
205+
local find_substr = function(s, p) return string.find(s or "", p, 1, true) end
206+
207+
if find_substr(updated_profile, "plug-binary") or find_substr(updated_profile, "plug-level") then
208+
local electrical_tags = ""
209+
if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" end
210+
if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-energy-powerConsumption" end
211+
if electrical_tags ~= "" then updated_profile = string.gsub(updated_profile, "-binary", "") .. electrical_tags end
212+
elseif find_substr(updated_profile, "light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then
213+
updated_profile = "light-color-level-fan"
214+
elseif find_substr(updated_profile, "light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then
215+
updated_profile = "light-level-motion"
216+
elseif find_substr(updated_profile, "light-level-colorTemperature") or find_substr(updated_profile, "light-color-level") then
217+
-- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since
218+
-- these may lose fingerprinted Kelvin ranges when dynamically profiled.
219+
return
220+
end
266221
end
222+
223+
-- initialize the main device card with buttons if applicable
224+
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
225+
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then
226+
ButtonDeviceConfiguration.update_button_profile(device, main_endpoint_id, #button_eps)
227+
-- All button endpoints found will be added as additional components in the profile containing the main_endpoint.
228+
ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint_id, button_eps)
229+
ButtonDeviceConfiguration.configure_buttons(device)
230+
return
231+
end
232+
233+
device:try_update_metadata({ profile = updated_profile })
267234
end
268235

269236
return {
270237
DeviceCfg = DeviceConfiguration,
271238
SwitchCfg = SwitchDeviceConfiguration,
272239
ButtonCfg = ButtonDeviceConfiguration
273-
}
240+
}

0 commit comments

Comments
 (0)