From e6561f4d9343b56ec22b9623ac82b52e76fb835d Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 24 Sep 2025 11:31:53 -0500 Subject: [PATCH] Use fanSpeedPercent for thermostats This change adds the fanSpeedPercent capability to the thermostat-modular profile. Additionally, PercentSetting is subscribed in addition to PercentCurrent to set this capability, since it provides an accurate representation of the speed of the fan while helping avoid the following situation: (1) The fan speed is changed in the app and PercentSetting is routed to the device (2) The fan reports back a value of PercentCurrent that doesn't match PercentSetting because the speed takes a little while to change (3) The fanSpeedPercent capability jumps to the value reported by PercentCurrent PercentCurrent is still subscribed to, but its attribute handler is gated on the current fan mode being AUTO, in which case PercentSetting is NULL on the device side and PercentCurrent is used as a fallback for setting the capability. --- .../matter-thermostat/src/init.lua | 21 +++- .../src/test/test_matter_air_purifier.lua | 27 +---- .../test/test_matter_air_purifier_api9.lua | 27 +---- .../test/test_matter_air_purifier_modular.lua | 4 +- .../src/test/test_matter_fan.lua | 107 +++++++++--------- .../src/test/test_matter_room_ac.lua | 11 +- .../src/test/test_matter_room_ac_modular.lua | 9 +- ...st_matter_thermo_multiple_device_types.lua | 1 + .../test/test_matter_thermostat_modular.lua | 1 + 9 files changed, 99 insertions(+), 109 deletions(-) diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 64ac9d8b39..a50ee8b289 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -196,7 +196,8 @@ local subscribed_attributes = { clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -1575,10 +1576,21 @@ local function fan_mode_sequence_handler(driver, device, ib, response) end local function fan_speed_percent_attr_handler(driver, device, ib, response) - local speed = 0 - if ib.data.value ~= nil then - speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE) + if ib.data.value == nil then return end + local thermostat_mode = device:get_latest_state( + device:endpoint_to_component(ib.endpoint_id), + capabilities.thermostatMode.ID, + capabilities.thermostatMode.thermostatMode.NAME + ) + if thermostat_mode == capabilities.thermostatMode.thermostatMode.auto() then + local speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) end +end + +local function fan_speed_setting_attr_handler(driver, device, ib, response) + if ib.data.value == nil then return end + local speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) end @@ -2075,6 +2087,7 @@ local matter_driver_template = { [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler, + [clusters.FanControl.attributes.PercentSetting.ID] = fan_speed_setting_attr_handler, [clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler, [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler, [clusters.FanControl.attributes.RockSupport.ID] = rock_support_handler, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua index d9cb8c6309..9c320e0a04 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua @@ -277,6 +277,7 @@ local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.FanControl.attributes.WindSupport, clusters.FanControl.attributes.WindSetting, clusters.HepaFilterMonitoring.attributes.ChangeIndication, @@ -289,6 +290,7 @@ local cluster_subscribe_list_rock = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.FanControl.attributes.WindSupport, clusters.FanControl.attributes.WindSetting, clusters.FanControl.attributes.RockSupport, @@ -327,7 +329,8 @@ local cluster_subscribe_list_configured = { clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -581,7 +584,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10) + clusters.FanControl.attributes.PercentSetting:build_test_report_data(mock_device, 1, 10) } }, { @@ -764,26 +767,6 @@ test.register_message_test( } ) -test.register_message_test( - "Set percent command should clamp invalid percentage values", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 255) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(100)) - }, - } -) - - local supportedFanRock = { capabilities.fanOscillationMode.fanOscillationMode.off.NAME, capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua index 871693d5a6..c60f2cff64 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua @@ -277,6 +277,7 @@ local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.FanControl.attributes.WindSupport, clusters.FanControl.attributes.WindSetting, clusters.HepaFilterMonitoring.attributes.ChangeIndication, @@ -289,6 +290,7 @@ local cluster_subscribe_list_rock = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.FanControl.attributes.WindSupport, clusters.FanControl.attributes.WindSetting, clusters.FanControl.attributes.RockSupport, @@ -327,7 +329,8 @@ local cluster_subscribe_list_configured = { clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -581,7 +584,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10) + clusters.FanControl.attributes.PercentSetting:build_test_report_data(mock_device, 1, 10) } }, { @@ -765,26 +768,6 @@ test.register_message_test( } ) -test.register_message_test( - "Set percent command should clamp invalid percentage values", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 255) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(100)) - } - } -) - - local supportedFanRock = { capabilities.fanOscillationMode.fanOscillationMode.off.NAME, capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua index 4f2b5bd0f4..e5dc43db9d 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua @@ -143,6 +143,7 @@ local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.FanControl.attributes.WindSupport, clusters.FanControl.attributes.WindSetting, clusters.HepaFilterMonitoring.attributes.ChangeIndication, @@ -179,7 +180,8 @@ local cluster_subscribe_list_configured = { clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua index 4a94a9a7e9..607bdec26c 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -13,79 +13,80 @@ -- limitations under the License. local test = require "integration_test" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("fan-rock-wind.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, + profile = t_utils.get_profile_definition("fan-rock-wind.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1,} -- RootNode + } }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1,} -- RootNode - } + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15}, }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15}, - }, - device_types = { - {device_type_id = 0x002B, device_type_revision = 1,} -- Fan - } + device_types = { + {device_type_id = 0x002B, device_type_revision = 1,} -- Fan } } + } }) local mock_device_generic = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("fan-generic.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, + profile = t_utils.get_profile_definition("fan-generic.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1,} -- RootNode + } }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1,} -- RootNode - } + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 0}, }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x002B, device_type_revision = 1,} -- Fan - } + device_types = { + {device_type_id = 0x002B, device_type_revision = 1,} -- Fan } } + } }) local cluster_subscribe_list = { - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.PercentCurrent, - clusters.FanControl.attributes.WindSupport, - clusters.FanControl.attributes.WindSetting, - clusters.FanControl.attributes.RockSupport, - clusters.FanControl.attributes.RockSetting, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting, + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting, } local cluster_subscribe_list_generic = { - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, } local function test_init() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua index cec0cb9ffc..39bb8bab60 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua @@ -146,7 +146,8 @@ local function test_init() clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -204,7 +205,8 @@ local function test_init_configure() clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -259,7 +261,8 @@ local function test_init_nostate() clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, } local subscribe_request = nil @@ -324,7 +327,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10) + clusters.FanControl.attributes.PercentSetting:build_test_report_data(mock_device, 1, 10) } }, { diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua index 2da9e5c023..5210c15aa0 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua @@ -154,7 +154,8 @@ local function test_init_basic() clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -203,7 +204,8 @@ local subscribed_attributes_no_state = { clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, @@ -259,7 +261,8 @@ local function test_init_no_state() clusters.FanControl.attributes.FanMode }, [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting }, [capabilities.windMode.ID] = { clusters.FanControl.attributes.WindSupport, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua index eacde8b651..4b7dc8fb72 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua @@ -233,6 +233,7 @@ local new_cluster_subscribe_list = { clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.FanControl.attributes.RockSupport, -- These two attributes will be subscribed to following the profile clusters.FanControl.attributes.RockSetting, -- change since the fanOscillationMode capability will be enabled. } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua index 8c4c1b0aad..120cff3829 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua @@ -165,6 +165,7 @@ test.register_coroutine_test( clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.PercentSetting, clusters.PowerSource.attributes.BatPercentRemaining, } local subscribe_request = initialize_mock_device(mock_device_basic, subscribed_attributes)