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
48 changes: 48 additions & 0 deletions drivers/SmartThings/matter-switch/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
device:subscribe()
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,59 @@ 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)
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
}
)
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 All @@ -43,34 +94,6 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on
return generic_profile or "switch-binary"
end

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

table.sort(server_onoff_ep_ids)
for device_num, ep_id in ipairs(server_onoff_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 = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true)
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
}
)
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

-- Per the spec, these attributes are "meant to be changed only during commissioning."
function SwitchDeviceConfiguration.set_device_control_options(device)
for _, ep in ipairs(device.endpoints) do
Expand Down Expand Up @@ -172,6 +195,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 @@ -184,15 +208,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_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 @@ -205,6 +227,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

-- initialize the main device card with buttons if applicable
local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})
if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then
Expand All @@ -215,7 +243,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
3 changes: 3 additions & 0 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
30 changes: 18 additions & 12 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
-- 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 main switch
-- endpoint. If it is not a supported device type, return the first button 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
Loading
Loading