Skip to content

Commit fb3f845

Browse files
committed
Add greater dynamic profiling to matter switch
1 parent 9331e5d commit fb3f845

File tree

8 files changed

+209
-176
lines changed

8 files changed

+209
-176
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 = "switch-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: 84 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -31,92 +31,57 @@ local DeviceConfiguration = {}
3131
local SwitchDeviceConfiguration = {}
3232
local ButtonDeviceConfiguration = {}
3333

34-
function SwitchDeviceConfiguration.assign_child_profile(device, child_ep)
35-
local profile
34+
function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device)
35+
local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id)
3636

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)
47-
end
48-
profile = fields.device_type_profile_map[id]
49-
break
50-
end
51-
end
52-
53-
-- vendor override checks
54-
if child_ep == switch_utils.get_product_override_field(device, "ep_id") or profile == switch_utils.get_product_override_field(device, "initial_profile") then
55-
profile = switch_utils.get_product_override_field(device, "target_profile") or profile
56-
end
37+
-- per spec, the Switch device types support OnOff as CLIENT, though some vendors break spec and support it as SERVER.
38+
local primary_dt_id = switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.LIGHT)
39+
or switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.SWITCH)
40+
or ep_info.device_types[1] and ep_info.device_types[1].device_type_id
5741

58-
-- default to "switch-binary" if no profile is found
59-
return profile or "switch-binary"
60-
end
61-
62-
function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint)
63-
local num_switch_server_eps = 0
64-
local parent_child_device = false
65-
local switch_eps = device:get_endpoints(clusters.OnOff.ID)
66-
table.sort(switch_eps)
67-
for idx, ep in ipairs(switch_eps) do
68-
if device:supports_server_cluster(clusters.OnOff.ID, ep) then
69-
num_switch_server_eps = num_switch_server_eps + 1
70-
if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint
71-
local name = string.format("%s %d", device.label, num_switch_server_eps)
72-
local child_profile = SwitchDeviceConfiguration.assign_child_profile(device, ep)
73-
driver:try_create_device(
74-
{
75-
type = "EDGE_CHILD",
76-
label = name,
77-
profile = child_profile,
78-
parent_device_id = device.id,
79-
parent_assigned_child_key = string.format("%d", ep),
80-
vendor_provided_label = name
81-
}
82-
)
83-
parent_child_device = true
84-
if idx == 1 and string.find(child_profile, "energy") then
85-
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
86-
device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true})
87-
end
88-
end
89-
end
90-
end
42+
local generic_profile = fields.device_type_profile_map[primary_dt_id]
9143

92-
-- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches
93-
-- is only run once, but find_child function should be set on each driver init.
94-
if parent_child_device then
95-
device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true})
44+
if is_child_device and (
45+
server_onoff_ep_id == switch_utils.get_product_override_field(device, "ep_id") or
46+
generic_profile == switch_utils.get_product_override_field(device, "initial_profile")
47+
) then
48+
generic_profile = switch_utils.get_product_override_field(device, "target_profile") or generic_profile
9649
end
9750

98-
-- this is needed in initialize_buttons_and_switches
99-
return num_switch_server_eps
51+
-- if no supported device type is found, return switch-binary as a generic "OnOff EP" profile
52+
return generic_profile or "switch-binary"
10053
end
10154

102-
function SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint)
103-
local cluster_id = 0
104-
for _, ep in ipairs(device.endpoints) do
105-
-- main_endpoint only supports server cluster by definition of get_endpoints()
106-
if main_endpoint == ep.endpoint_id then
107-
for _, dt in ipairs(ep.device_types) do
108-
-- no device type that is not in the switch subset should be considered.
109-
if (fields.ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= fields.ON_OFF_COLOR_DIMMER_SWITCH_ID) then
110-
cluster_id = math.max(cluster_id, dt.device_type_id)
111-
end
55+
function SwitchDeviceConfiguration.create_child_devices(driver, device, onoff_ep_ids, main_endpoint_id)
56+
if #onoff_ep_ids == 1 and onoff_ep_ids[1] == main_endpoint_id then -- no children will be created
57+
return
58+
end
59+
60+
local device_num = 0
61+
table.sort(onoff_ep_ids)
62+
for idx, ep_id in ipairs(onoff_ep_ids) do
63+
device_num = device_num + 1
64+
if ep_id ~= main_endpoint_id then -- don't create a child device that maps to the main endpoint
65+
local name = string.format("%s %d", device.label, device_num)
66+
local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true)
67+
driver:try_create_device({
68+
type = "EDGE_CHILD",
69+
label = name,
70+
profile = child_profile,
71+
parent_device_id = device.id,
72+
parent_assigned_child_key = string.format("%d", ep_id),
73+
vendor_provided_label = name
74+
})
75+
if idx == 1 and string.find(child_profile, "energy") then
76+
-- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it.
77+
device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep_id, {persist = true})
11278
end
113-
break
11479
end
11580
end
11681

117-
if fields.device_type_profile_map[cluster_id] then
118-
device:try_update_metadata({profile = fields.device_type_profile_map[cluster_id]})
119-
end
82+
-- Persist so that the find_child function is always set on each driver init.
83+
device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true})
84+
device:set_find_child(switch_utils.find_child)
12085
end
12186

12287
function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, num_button_eps)
@@ -192,75 +157,61 @@ end
192157

193158
-- [[ PROFILE MATCHING AND CONFIGURATIONS ]] --
194159

195-
function DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint)
196-
local profile_found = false
197-
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
198-
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then
199-
ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, #button_eps)
200-
-- All button endpoints found will be added as additional components in the profile containing the main_endpoint.
201-
-- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field
202-
ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps)
203-
ButtonDeviceConfiguration.configure_buttons(device)
204-
profile_found = true
205-
end
206-
207-
-- Without support for bindings, only clusters that are implemented as server are counted. This count is handled
208-
-- while building switch child profiles
209-
local num_switch_server_eps = SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint)
160+
function DeviceConfiguration.match_profile(driver, device)
161+
local main_endpoint_id = switch_utils.find_default_endpoint(device)
162+
local updated_profile = nil
210163

211-
-- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings.
212-
-- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'.
213-
-- Note: since their device type isn't supported, these devices join as a matter-thing.
214-
if num_switch_server_eps > 0 and switch_utils.detect_matter_thing(device) then
215-
SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint)
216-
profile_found = true
164+
if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then
165+
updated_profile = "water-valve"
166+
if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID,
167+
{feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then
168+
updated_profile = updated_profile .. "-level"
169+
end
217170
end
218-
return profile_found
219-
end
220171

221-
function DeviceConfiguration.match_profile(driver, device)
222-
local main_endpoint = switch_utils.find_default_endpoint(device)
223-
-- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices.
224-
local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint)
225-
if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then
226-
device:set_find_child(switch_utils.find_child)
227-
end
228-
if profile_found then
229-
return
172+
local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH
173+
if #server_onoff_ep_ids > 0 then
174+
SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, main_endpoint_id)
230175
end
231176

232-
local fan_eps = device:get_endpoints(clusters.FanControl.ID)
233-
local level_eps = device:get_endpoints(clusters.LevelControl.ID)
234-
local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID)
235-
local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID)
236-
local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID)
237-
local profile_name = nil
238-
local level_support = ""
239-
if #level_eps > 0 then
240-
level_support = "-level"
241-
end
242-
if #energy_eps > 0 and #power_eps > 0 then
243-
profile_name = "plug" .. level_support .. "-power-energy-powerConsumption"
244-
elseif #energy_eps > 0 then
245-
profile_name = "plug" .. level_support .. "-energy-powerConsumption"
246-
elseif #power_eps > 0 then
247-
profile_name = "plug" .. level_support .. "-power"
248-
elseif #valve_eps > 0 then
249-
profile_name = "water-valve"
250-
if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID,
251-
{feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then
252-
profile_name = profile_name .. "-level"
177+
if switch_utils.tbl_contains(server_onoff_ep_ids, main_endpoint_id) then
178+
updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, main_endpoint_id)
179+
local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end
180+
if generic_profile("plug-binary") or generic_profile("plug-level") then
181+
if switch_utils.check_switch_category_vendor_overrides(device) then
182+
updated_profile = string.gsub(updated_profile, "plug", "switch")
183+
else
184+
local electrical_tags = ""
185+
if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" end
186+
if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-energy-powerConsumption" end
187+
if electrical_tags ~= "" then updated_profile = string.gsub(updated_profile, "-binary", "") .. electrical_tags end
188+
end
189+
elseif generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then
190+
updated_profile = "light-color-level-fan"
191+
elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then
192+
updated_profile = "light-level-motion"
193+
elseif generic_profile("light-level-colorTemperature") or generic_profile("light-color-level") then
194+
-- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since
195+
-- these may lose fingerprinted Kelvin ranges when dynamically profiled.
196+
return
253197
end
254-
elseif #fan_eps > 0 then
255-
profile_name = "light-color-level-fan"
256198
end
257-
if profile_name then
258-
device:try_update_metadata({ profile = profile_name })
199+
200+
-- initialize the main device card with buttons if applicable
201+
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
202+
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then
203+
ButtonDeviceConfiguration.update_button_profile(device, main_endpoint_id, #button_eps)
204+
-- All button endpoints found will be added as additional components in the profile containing the main_endpoint.
205+
ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint_id, button_eps)
206+
ButtonDeviceConfiguration.configure_buttons(device)
207+
return
259208
end
209+
210+
device:try_update_metadata({ profile = updated_profile })
260211
end
261212

262213
return {
263214
DeviceCfg = DeviceConfiguration,
264215
SwitchCfg = SwitchDeviceConfiguration,
265216
ButtonCfg = ButtonDeviceConfiguration
266-
}
217+
}

0 commit comments

Comments
 (0)