Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
642 changes: 321 additions & 321 deletions drivers/SmartThings/matter-switch/fingerprints.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: light-color-level-noTemp
components:
- id: main
capabilities:
- id: switch
version: 1
- id: switchLevel
version: 1
config:
values:
- key: "level.value"
range: [1, 100]
- id: colorControl
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories:
- name: Light
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on

local generic_profile = fields.device_type_profile_map[primary_dt_id]

-- remove colorTemperature capability from profile if device doesn't support it
if generic_profile == "light-color-level" and
not switch_utils.find_cluster_on_ep(ep_info, clusters.ColorControl.ID, {feature_bitmap=clusters.ColorControl.types.Feature.COLOR_TEMPERATURE}) then
generic_profile = "light-color-level-noTemp"
end

local static_electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, server_onoff_ep_id)
if static_electrical_tags ~= nil then
-- profiles like 'light-binary' and 'plug-binary' should drop the '-binary' and become 'light-power', 'plug-energy-powerConsumption', etc.
Expand Down Expand Up @@ -194,13 +200,13 @@ function DeviceConfiguration.match_profile(driver, device)
updated_profile = "light-color-level-fan"
elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then
updated_profile = "light-level-motion"
elseif generic_profile("plug-binary") or generic_profile("plug-level") then
if switch_utils.check_switch_category_vendor_overrides(device) then
updated_profile = string.gsub(updated_profile, "plug", "switch")
end
elseif generic_profile("light-level-colorTemperature") or generic_profile("light-color-level") then
-- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since
-- these may lose fingerprinted Kelvin ranges when dynamically profiled.
elseif switch_utils.check_switch_category_vendor_overrides(device) then
-- check whether the overwrite should be over "plug" or "light" based on the current profile
local overwrite_category = string.find(updated_profile, "plug") and "plug" or "light"
updated_profile = string.gsub(updated_profile, overwrite_category, "switch")
Comment on lines +203 to +206
Copy link
Contributor Author

@hcarter-775 hcarter-775 Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is technically unrelated. When looking this over, I didn't think it was generic to assume all devices using a "Switch" category override have a plug device type. That does make the most since I think, but should not be a prerequisite in this logic.

elseif switch_utils.check_deprecated_color_temperature_profile_vendor_overrides(device) then
-- ignore attempts to dynamically profile certain devices containing the colorTemperature capability
-- for now, since these may lose fingerprinted Kelvin ranges when dynamically profiled.
return
end
end
Expand Down
31 changes: 31 additions & 0 deletions drivers/SmartThings/matter-switch/src/switch_utils/fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ SwitchFields.vendor_overrides = {
},
}

--- A table of vendorIDs and corresponding productIDs indicating devices that require
--- a non-default Category, specifically "Switch". These overrides are applied during device
--- onboarding to ensure an accurate display in the SmartThings app.
SwitchFields.switch_category_vendor_overrides = {
[0x1432] = -- Elko
{0x1000},
Expand All @@ -143,6 +146,34 @@ SwitchFields.switch_category_vendor_overrides = {
{0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05}
}

--- Devices that were certified with profiles defining non-default min/max ColorTemperature values for the driver.
--- These profiles have since been deprecated in favor of having devices report their own min/max values, but to ensure
--- backwards compatibility, we override these devices with their current fingerprint at present.
SwitchFields.deprecated_color_temperature_profile_vendor_overrides = {
[0x115a] = -- Nanoleaf
{0x0035, 0x0036, 0x0041, 0x0043, 0x0044, 0x0711, 0x0047, 0x0048, 0x0049, 0x004B},
[0x1160] = -- Sengled
{0x9002},
[0x147F] = -- U-Tec
{0x0002},
[0x1339] = -- GE
{0x00B1, 0x0083, 0x00B6, 0x00B5, 0x00AF, 0x00B4, 0x00AE, 0x002C, 0x0029, 0x002A,
0x002B, 0x0089, 0x002E, 0x0062, 0x00AB, 0x0061, 0x0068, 0x006E, 0x007B, 0x006D,
0x006B, 0x00AD, 0x0069, 0x0065, 0x0015, 0x006C, 0x0016},
[0x1168] = -- AiDot
{0x03F4, 0x03F3, 0x03F2, 0x0405, 0x03ec, 0x03eb, 0x03ea, 0x03e8, 0x03f8, 0x03F5,
0x1000, 0x03EE, 0x03ED, 0x03EF, 0x03f9, 0x03FB, 0x03FC},
[0x115F] = -- Aqara
{0x1802, 0x1806},
[0x1423] = -- Lifx
{0x00A1, 0x00A2, 0x00A8, 0x00A9, 0x00AB, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B2,
0x00B3, 0x00B4, 0x00B9, 0x00C9, 0x00DB, 0x00DC, 0x00D5, 0x00D7, 0x00D9, 0x0077},
[0x1312] = -- Yeelight
{0x0001},
[0x1407] = -- ThirdReality
{0x1088}
}

--- stores a table of endpoints that support the Electrical Sensor device type, used during profiling
--- in AvailableEndpoints and PartsList handlers for SET and TREE PowerTopology features, respectively
SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps"
Expand Down
8 changes: 8 additions & 0 deletions drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ function utils.check_switch_category_vendor_overrides(device)
end
end

function utils.check_deprecated_color_temperature_profile_vendor_overrides(device)
for _, product_id in ipairs(fields.deprecated_color_temperature_profile_vendor_overrides[device.manufacturer_info.vendor_id] or {}) do
if device.manufacturer_info.product_id == product_id then
return true
end
end
end

--- device_type_supports_button_switch_combination helper function used to check
--- whether the device type for an endpoint is currently supported by a profile for
--- combination button/switch devices.
Expand Down
64 changes: 62 additions & 2 deletions drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,29 @@ local mock_device_extended_color = test.mock_device.build_test_matter_device({
}
})

local mock_device_extended_color_no_temp = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("light-color-level.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
},
endpoints = {
{
endpoint_id = 1,
clusters = {
{cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"},
{cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 14},
{cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}
},
device_types = {
{device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light
{device_type_id = 0x0101, device_type_revision = 1}, -- Dimmable Light
{device_type_id = 0x010D, device_type_revision = 1}, -- Extended Color Light
}
}
}
})

local cluster_subscribe_list = {
clusters.OnOff.attributes.OnOff,
clusters.LevelControl.attributes.CurrentLevel,
Expand Down Expand Up @@ -216,6 +239,7 @@ local function test_init_color_temp()
mock_device_color_temp.id,
clusters.ColorControl.attributes.Options:write(mock_device_color_temp, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)
})
mock_device_color_temp:expect_metadata_update({ profile = "light-level-colorTemperature" })
mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request})
end
Expand Down Expand Up @@ -243,22 +267,58 @@ local function test_init_extended_color()
mock_device_extended_color.id,
clusters.ColorControl.attributes.Options:write(mock_device_extended_color, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)
})
mock_device_extended_color:expect_metadata_update({ profile = "light-color-level" })
mock_device_extended_color:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request})
end

local function test_init_extended_color_no_temp()
test.mock_device.add_test_device(mock_device_extended_color_no_temp)
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_extended_color_no_temp)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device_extended_color_no_temp))
end
end
test.socket.matter:__expect_send({mock_device_extended_color_no_temp.id, subscribe_request})
test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color_no_temp.id, "added" })

test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color_no_temp.id, "init" })
test.socket.matter:__expect_send({mock_device_extended_color_no_temp.id, subscribe_request})

test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color_no_temp.id, "doConfigure" })
test.socket.matter:__expect_send({
mock_device_extended_color_no_temp.id,
clusters.LevelControl.attributes.Options:write(mock_device_extended_color_no_temp, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)
})
test.socket.matter:__expect_send({
mock_device_extended_color_no_temp.id,
clusters.ColorControl.attributes.Options:write(mock_device_extended_color_no_temp, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)
})
mock_device_extended_color_no_temp:expect_metadata_update({ profile = "light-color-level-noTemp" })
mock_device_extended_color_no_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.socket.matter:__expect_send({mock_device_extended_color_no_temp.id, subscribe_request})
end


test.register_message_test(
"Test that Color Temperature Light device does not switch profiles",
"Test that Color Temperature Light device switches profiles",
{},
{ test_init = test_init_color_temp }
)

test.register_message_test(
"Test that Extended Color Light device does not switch profiles",
"Test that Extended Color Light device switches profiles",
{},
{ test_init = test_init_extended_color }
)

test.register_message_test(
"Test that Extended Color Light device without color temperature switches profiles",
{},
{ test_init = test_init_extended_color_no_temp }
)

test.register_message_test(
"On command should send the appropriate commands",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ local function test_init_parent_child_endpoints_non_sequential()
test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({unsup_mock_device.id, clusters.ColorControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)})

unsup_mock_device:expect_metadata_update({ profile = "light-binary" })
unsup_mock_device:expect_metadata_update({ profile = "switch-binary" })
unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })

for _, child in pairs(mock_children_non_sequential) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ local function test_init_parent_child_endpoints_non_sequential()
test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.ColorControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)})
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" })
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "switch-binary" })
mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" })

for _, child in pairs(mock_children_non_sequential) do
Expand Down
Loading