Skip to content
Merged
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
50 changes: 49 additions & 1 deletion drivers/SmartThings/matter-switch/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3587,7 +3587,7 @@ matterGeneric:
deviceTypes:
- id: 0x010D # Extended Color Light
- id: 0x002B # Fan
deviceProfileName: light-color-level-fan
deviceProfileName: fan-modular
- id: "matter/dimmable/light/motion"
deviceLabel: Matter Dimmable Light Occupancy Sensor
deviceTypes:
Expand All @@ -3599,6 +3599,54 @@ matterGeneric:
deviceTypes:
- id: 0x0142 # Camera
deviceProfileName: camera
- id: "matter/on-off/fan/light"
deviceLabel: Matter OnOff Fan Light
deviceTypes:
- id: 0x002B # Fan
- id: 0x0100 # OnOff Light
deviceProfileName: fan-modular
- id: "matter/dimmable/fan/light"
deviceLabel: Matter Dimmable Fan Light
deviceTypes:
- id: 0x002B # Fan
- id: 0x0101 # Dimmable Light
deviceProfileName: fan-modular
- id: "matter/colorTemperature/fan/light"
deviceLabel: Matter Color Temperature Fan Light
deviceTypes:
- id: 0x002B # Fan
- id: 0x010C # Color Temperature Light
deviceProfileName: fan-modular
- id: "matter/color/fan/light"
deviceLabel: Matter Color Fan Light
deviceTypes:
- id: 0x002B # Fan
- id: 0x010D # Extended Color Light
deviceProfileName: fan-modular
- id: "matter/on-off/fan/plug"
deviceLabel: Matter OnOff Fan Plug
deviceTypes:
- id: 0x002B # Fan
- id: 0x010A # On Off Plug-in Unit
deviceProfileName: fan-modular
- id: "matter/dimmable/fan/plug"
deviceLabel: Matter Dimmable Fan Plug
deviceTypes:
- id: 0x002B # Fan
- id: 0x010B # Dimmable Plug-in Unit
deviceProfileName: fan-modular
- id: "matter/mounted/on-off/control/fan"
deviceLabel: Matter Mounted OnOff Control Fan
deviceTypes:
- id: 0x002B # Fan
- id: 0x010F # Mounted On/Off Control
deviceProfileName: fan-modular
- id: "matter/mounted/dim/load/control/fan"
deviceLabel: Matter Mounted Dimmable Load Control Fan
deviceTypes:
- id: 0x002B # Fan
- id: 0x0110 # Mounted Dimmable Load Control
deviceProfileName: fan-modular

matterThing:
- id: SmartThings/MatterThing
Expand Down
17 changes: 17 additions & 0 deletions drivers/SmartThings/matter-switch/profiles/fan-modular.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: fan-modular
components:
- id: main
capabilities:
- id: fanMode
version: 1
optional: true
- id: fanSpeedPercent
version: 1
optional: true
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories:
- name: Fan

3 changes: 2 additions & 1 deletion drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ function SwitchLifecycleHandlers.driver_switched(driver, device)
end

function SwitchLifecycleHandlers.info_changed(driver, device, event, args)
if device.profile.id ~= args.old_st_store.profile.id then
if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then
device:set_field(fields.MODULAR_PROFILE_UPDATED, nil)
if device.network_type == device_lib.NETWORK_TYPE_MATTER then
device:subscribe()
button_cfg.configure_buttons(device,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,64 @@ if version.api < 11 then
end

local DeviceConfiguration = {}
local ChildConfiguration = {}
local SwitchDeviceConfiguration = {}
local ButtonDeviceConfiguration = {}
local FanDeviceConfiguration = {}

function ChildConfiguration.create_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn)
if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created
return
end

table.sort(server_cluster_ep_ids)
for device_num, ep_id in ipairs(server_cluster_ep_ids) do
if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint
local label_and_name = string.format("%s %d", device.label, device_num)
local child_profile, _ = assign_profile_fn(device, ep_id, true)
local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id)
if not existing_child_device then
driver:try_create_device({
type = "EDGE_CHILD",
label = label_and_name,
profile = child_profile,
parent_device_id = device.id,
parent_assigned_child_key = string.format("%d", ep_id),
vendor_provided_label = label_and_name
})
else
existing_child_device:try_update_metadata({
profile = child_profile
})
end
end
end

-- Persist so that the find_child function is always set on each driver init.
device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true})
device:set_find_child(switch_utils.find_child)
end

function FanDeviceConfiguration.assign_profile_for_fan_ep(device, server_fan_ep_id)
local ep_info = switch_utils.get_endpoint_info(device, server_fan_ep_id)
local fan_cluster_info = switch_utils.find_cluster_on_ep(ep_info, clusters.FanControl.ID)
local optional_supported_component_capabilities = {}
local main_component_capabilities = {}

if clusters.FanControl.are_features_supported(clusters.FanControl.types.Feature.MULTI_SPEED, fan_cluster_info.feature_map) then
table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID)
-- only fanMode can trigger AUTO, so a multi-speed fan still requires this capability if it supports AUTO
if clusters.FanControl.are_features_supported(clusters.FanControl.types.Feature.AUTO, fan_cluster_info.feature_map) then
table.insert(main_component_capabilities, capabilities.fanMode.ID)
end
else -- MULTI_SPEED is not supported
table.insert(main_component_capabilities, capabilities.fanMode.ID)
end

table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities})
return "fan-modular", optional_supported_component_capabilities
end


function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device)
local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id)
Expand Down Expand Up @@ -176,6 +232,7 @@ function DeviceConfiguration.match_profile(driver, device)
if profiling_data_still_required(device) then return end

local default_endpoint_id = switch_utils.find_default_endpoint(device)
local optional_component_capabilities
local updated_profile

if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then
Expand All @@ -188,15 +245,13 @@ function DeviceConfiguration.match_profile(driver, device)

local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH
if #server_onoff_ep_ids > 0 then
SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id)
ChildConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep)
end

if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then
updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id)
local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end
if generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then
updated_profile = "light-color-level-fan"
elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then
if 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
Expand All @@ -209,6 +264,12 @@ function DeviceConfiguration.match_profile(driver, device)
end
end

local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN)
if #fan_device_type_ep_ids > 0 then
updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id)
device:set_field(fields.MODULAR_PROFILE_UPDATED, true)
end
Comment on lines +267 to +271
Copy link
Contributor

@nickolas-deboom nickolas-deboom Jan 8, 2026

Choose a reason for hiding this comment

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

For now this is perfectly fine, but it might be worth discussing alternate approaches for the default endpoint logic at some point. Because at this point we already know the default endpoint contains a fan device type so it seems like we shouldn't need the get_endpoints_by_device_type call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed


-- initialize the main device card with buttons if applicable
local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then
Expand All @@ -219,7 +280,7 @@ function DeviceConfiguration.match_profile(driver, device)
return
end

device:try_update_metadata({ profile = updated_profile })
device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities })
end

return {
Expand Down
9 changes: 7 additions & 2 deletions drivers/SmartThings/matter-switch/src/switch_utils/fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ SwitchFields.DEVICE_TYPE_ID = {
DIMMABLE_PLUG_IN_UNIT = 0x010B,
DOORBELL = 0x0143,
ELECTRICAL_SENSOR = 0x0510,
FAN = 0x002B,
GENERIC_SWITCH = 0x000F,
MOUNTED_ON_OFF_CONTROL = 0x010F,
MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110,
Expand Down Expand Up @@ -151,6 +152,8 @@ SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps"
--- for an Electrical Sensor EP with a "primary" endpoint, used during device profling.
SwitchFields.ELECTRICAL_TAGS = "__electrical_tags"

SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated"

SwitchFields.profiling_data = {
POWER_TOPOLOGY = "__power_topology",
}
Expand Down Expand Up @@ -255,7 +258,8 @@ SwitchFields.device_type_attribute_map = {
clusters.ColorControl.attributes.CurrentHue,
clusters.ColorControl.attributes.CurrentSaturation,
clusters.ColorControl.attributes.CurrentX,
clusters.ColorControl.attributes.CurrentY
clusters.ColorControl.attributes.CurrentY,
clusters.ColorControl.attributes.ColorMode
},
[SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = {
clusters.OnOff.attributes.OnOff
Expand Down Expand Up @@ -286,7 +290,8 @@ SwitchFields.device_type_attribute_map = {
clusters.ColorControl.attributes.CurrentHue,
clusters.ColorControl.attributes.CurrentSaturation,
clusters.ColorControl.attributes.CurrentX,
clusters.ColorControl.attributes.CurrentY
clusters.ColorControl.attributes.CurrentY,
clusters.ColorControl.attributes.ColorMode
},
[SwitchFields.DEVICE_TYPE_ID.GENERIC_SWITCH] = {
clusters.PowerSource.attributes.BatPercentRemaining,
Expand Down
32 changes: 19 additions & 13 deletions drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ function utils.find_default_endpoint(device)
return device.MATTER_DEFAULT_ENDPOINT
end

local switch_eps = device:get_endpoints(clusters.OnOff.ID)
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
local onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID)
local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN)

local get_first_non_zero_endpoint = function(endpoints)
table.sort(endpoints)
Expand All @@ -164,26 +165,31 @@ function utils.find_default_endpoint(device)
return nil
end

-- Return the first switch endpoint as the default endpoint if no button endpoints are present
if #button_eps == 0 and #switch_eps > 0 then
return get_first_non_zero_endpoint(switch_eps)
-- Return the first fan endpoint as the default endpoint if any is found
if #fan_endpoint_ids > 0 then
return get_first_non_zero_endpoint(fan_endpoint_ids)
end

-- Return the first button endpoint as the default endpoint if no switch endpoints are present
if #switch_eps == 0 and #button_eps > 0 then
return get_first_non_zero_endpoint(button_eps)
-- Return the first onoff endpoint as the default endpoint if no momentary switch endpoints are present
if #momentary_switch_ep_ids == 0 and #onoff_ep_ids > 0 then
return get_first_non_zero_endpoint(onoff_ep_ids)
end

-- If both switch and button endpoints are present, check the device type on the main switch
-- endpoint. If it is not a supported device type, return the first button endpoint as the
-- Return the first momentary switch endpoint as the default endpoint if no onoff endpoints are present
if #onoff_ep_ids == 0 and #momentary_switch_ep_ids > 0 then
return get_first_non_zero_endpoint(momentary_switch_ep_ids)
end

-- If both onoff and momentary switch endpoints are present, check the device type on the first onoff
-- endpoint. If it is not a supported device type, return the first momentary switch endpoint as the
-- default endpoint.
if #switch_eps > 0 and #button_eps > 0 then
local default_endpoint_id = get_first_non_zero_endpoint(switch_eps)
if #onoff_ep_ids > 0 and #momentary_switch_ep_ids > 0 then
local default_endpoint_id = get_first_non_zero_endpoint(onoff_ep_ids)
if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then
return default_endpoint_id
else
device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons")
return get_first_non_zero_endpoint(button_eps)
return get_first_non_zero_endpoint(momentary_switch_ep_ids)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ local function update_device_profile()
clusters.ColorControl.attributes.CurrentSaturation,
clusters.ColorControl.attributes.CurrentX,
clusters.ColorControl.attributes.CurrentY,
clusters.ColorControl.attributes.ColorMode,
clusters.OccupancySensing.attributes.Occupancy,
clusters.Switch.server.events.InitialPress,
clusters.Switch.server.events.LongPress,
Expand Down
Loading
Loading