Skip to content

Commit aec6e9f

Browse files
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.
1 parent 737773e commit aec6e9f

11 files changed

+134
-115
lines changed

drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ components:
99
- id: fanMode
1010
version: 1
1111
optional: true
12+
- id: fanSpeedPercent
13+
version: 1
14+
optional: true
1215
- id: fanOscillationMode
1316
version: 1
1417
optional: true

drivers/SmartThings/matter-thermostat/src/init.lua

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@ local log = require "log"
1717
local clusters = require "st.matter.clusters"
1818
local embedded_cluster_utils = require "embedded-cluster-utils"
1919
local im = require "st.matter.interaction_model"
20-
2120
local MatterDriver = require "st.matter.driver"
2221
local utils = require "st.utils"
22+
local version = require "version"
2323

2424
local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities"
2525
-- declare match_profile function for use throughout file
2626
local match_profile
2727

2828
-- Include driver-side definitions when lua libs api version is < 10
29-
local version = require "version"
3029
if version.api < 10 then
3130
clusters.HepaFilterMonitoring = require "HepaFilterMonitoring"
3231
clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring"
@@ -102,6 +101,9 @@ local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510
102101

103102
local MIN_ALLOWED_PERCENT_VALUE = 0
104103
local MAX_ALLOWED_PERCENT_VALUE = 100
104+
local DEFAULT_REPORT_TIME_INTERVAL = 15 * 60 -- Report cumulative energy every 15 minutes
105+
local MAX_REPORT_TIMEOUT = 30 * 60
106+
local POLL_INTERVAL = 60 -- To read CumulativeEnergyImported every 60 seconds.
105107

106108
local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported"
107109
local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp"
@@ -197,7 +199,8 @@ local subscribed_attributes = {
197199
clusters.FanControl.attributes.FanMode
198200
},
199201
[capabilities.fanSpeedPercent.ID] = {
200-
clusters.FanControl.attributes.PercentCurrent
202+
clusters.FanControl.attributes.PercentCurrent,
203+
clusters.FanControl.attributes.PercentSetting
201204
},
202205
[capabilities.windMode.ID] = {
203206
clusters.FanControl.attributes.WindSupport,
@@ -966,6 +969,7 @@ local function match_modular_profile_thermostat(driver, device)
966969

967970
if #fan_eps > 0 then
968971
table.insert(main_component_capabilities, capabilities.fanMode.ID)
972+
table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID)
969973
end
970974
if #rock_eps > 0 then
971975
table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID)
@@ -1575,10 +1579,21 @@ local function fan_mode_sequence_handler(driver, device, ib, response)
15751579
end
15761580

15771581
local function fan_speed_percent_attr_handler(driver, device, ib, response)
1578-
local speed = 0
1579-
if ib.data.value ~= nil then
1580-
speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE)
1582+
if ib.data.value == nil then return end
1583+
local thermostat_mode = device:get_latest_state(
1584+
device:endpoint_to_component(ib.endpoint_id),
1585+
capabilities.thermostatMode.ID,
1586+
capabilities.thermostatMode.thermostatMode.NAME
1587+
)
1588+
if thermostat_mode == capabilities.thermostatMode.thermostatMode.auto() then
1589+
local speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE)
1590+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed))
15811591
end
1592+
end
1593+
1594+
local function fan_speed_setting_attr_handler(driver, device, ib, response)
1595+
if ib.data.value == nil then return end
1596+
local speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE)
15821597
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed))
15831598
end
15841599

@@ -2075,6 +2090,7 @@ local matter_driver_template = {
20752090
[clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler,
20762091
[clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler,
20772092
[clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler,
2093+
[clusters.FanControl.attributes.PercentSetting.ID] = fan_speed_setting_attr_handler,
20782094
[clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler,
20792095
[clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler,
20802096
[clusters.FanControl.attributes.RockSupport.ID] = rock_support_handler,

drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ local cluster_subscribe_list = {
277277
clusters.FanControl.attributes.FanModeSequence,
278278
clusters.FanControl.attributes.FanMode,
279279
clusters.FanControl.attributes.PercentCurrent,
280+
clusters.FanControl.attributes.PercentSetting,
280281
clusters.FanControl.attributes.WindSupport,
281282
clusters.FanControl.attributes.WindSetting,
282283
clusters.HepaFilterMonitoring.attributes.ChangeIndication,
@@ -289,6 +290,7 @@ local cluster_subscribe_list_rock = {
289290
clusters.FanControl.attributes.FanModeSequence,
290291
clusters.FanControl.attributes.FanMode,
291292
clusters.FanControl.attributes.PercentCurrent,
293+
clusters.FanControl.attributes.PercentSetting,
292294
clusters.FanControl.attributes.WindSupport,
293295
clusters.FanControl.attributes.WindSetting,
294296
clusters.FanControl.attributes.RockSupport,
@@ -327,7 +329,8 @@ local cluster_subscribe_list_configured = {
327329
clusters.FanControl.attributes.FanMode
328330
},
329331
[capabilities.fanSpeedPercent.ID] = {
330-
clusters.FanControl.attributes.PercentCurrent
332+
clusters.FanControl.attributes.PercentCurrent,
333+
clusters.FanControl.attributes.PercentSetting
331334
},
332335
[capabilities.windMode.ID] = {
333336
clusters.FanControl.attributes.WindSupport,
@@ -581,7 +584,7 @@ test.register_message_test(
581584
direction = "receive",
582585
message = {
583586
mock_device.id,
584-
clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10)
587+
clusters.FanControl.attributes.PercentSetting:build_test_report_data(mock_device, 1, 10)
585588
}
586589
},
587590
{
@@ -764,26 +767,6 @@ test.register_message_test(
764767
}
765768
)
766769

767-
test.register_message_test(
768-
"Set percent command should clamp invalid percentage values",
769-
{
770-
{
771-
channel = "matter",
772-
direction = "receive",
773-
message = {
774-
mock_device.id,
775-
clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 255)
776-
}
777-
},
778-
{
779-
channel = "capability",
780-
direction = "send",
781-
message = mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(100))
782-
},
783-
}
784-
)
785-
786-
787770
local supportedFanRock = {
788771
capabilities.fanOscillationMode.fanOscillationMode.off.NAME,
789772
capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME,

drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ local cluster_subscribe_list = {
277277
clusters.FanControl.attributes.FanModeSequence,
278278
clusters.FanControl.attributes.FanMode,
279279
clusters.FanControl.attributes.PercentCurrent,
280+
clusters.FanControl.attributes.PercentSetting,
280281
clusters.FanControl.attributes.WindSupport,
281282
clusters.FanControl.attributes.WindSetting,
282283
clusters.HepaFilterMonitoring.attributes.ChangeIndication,
@@ -289,6 +290,7 @@ local cluster_subscribe_list_rock = {
289290
clusters.FanControl.attributes.FanModeSequence,
290291
clusters.FanControl.attributes.FanMode,
291292
clusters.FanControl.attributes.PercentCurrent,
293+
clusters.FanControl.attributes.PercentSetting,
292294
clusters.FanControl.attributes.WindSupport,
293295
clusters.FanControl.attributes.WindSetting,
294296
clusters.FanControl.attributes.RockSupport,
@@ -327,7 +329,8 @@ local cluster_subscribe_list_configured = {
327329
clusters.FanControl.attributes.FanMode
328330
},
329331
[capabilities.fanSpeedPercent.ID] = {
330-
clusters.FanControl.attributes.PercentCurrent
332+
clusters.FanControl.attributes.PercentCurrent,
333+
clusters.FanControl.attributes.PercentSetting
331334
},
332335
[capabilities.windMode.ID] = {
333336
clusters.FanControl.attributes.WindSupport,
@@ -581,7 +584,7 @@ test.register_message_test(
581584
direction = "receive",
582585
message = {
583586
mock_device.id,
584-
clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10)
587+
clusters.FanControl.attributes.PercentSetting:build_test_report_data(mock_device, 1, 10)
585588
}
586589
},
587590
{
@@ -765,26 +768,6 @@ test.register_message_test(
765768
}
766769
)
767770

768-
test.register_message_test(
769-
"Set percent command should clamp invalid percentage values",
770-
{
771-
{
772-
channel = "matter",
773-
direction = "receive",
774-
message = {
775-
mock_device.id,
776-
clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 255)
777-
}
778-
},
779-
{
780-
channel = "capability",
781-
direction = "send",
782-
message = mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(100))
783-
}
784-
}
785-
)
786-
787-
788771
local supportedFanRock = {
789772
capabilities.fanOscillationMode.fanOscillationMode.off.NAME,
790773
capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME,

drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ local cluster_subscribe_list = {
143143
clusters.FanControl.attributes.FanModeSequence,
144144
clusters.FanControl.attributes.FanMode,
145145
clusters.FanControl.attributes.PercentCurrent,
146+
clusters.FanControl.attributes.PercentSetting,
146147
clusters.FanControl.attributes.WindSupport,
147148
clusters.FanControl.attributes.WindSetting,
148149
clusters.HepaFilterMonitoring.attributes.ChangeIndication,
@@ -179,7 +180,8 @@ local cluster_subscribe_list_configured = {
179180
clusters.FanControl.attributes.FanMode
180181
},
181182
[capabilities.fanSpeedPercent.ID] = {
182-
clusters.FanControl.attributes.PercentCurrent
183+
clusters.FanControl.attributes.PercentCurrent,
184+
clusters.FanControl.attributes.PercentSetting
183185
},
184186
[capabilities.windMode.ID] = {
185187
clusters.FanControl.attributes.WindSupport,

drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,79 +13,80 @@
1313
-- limitations under the License.
1414
local test = require "integration_test"
1515
local t_utils = require "integration_test.utils"
16-
1716
local clusters = require "st.matter.clusters"
1817

1918
local mock_device = test.mock_device.build_test_matter_device({
20-
profile = t_utils.get_profile_definition("fan-rock-wind.yml"),
21-
manufacturer_info = {
22-
vendor_id = 0x0000,
23-
product_id = 0x0000,
19+
profile = t_utils.get_profile_definition("fan-rock-wind.yml"),
20+
manufacturer_info = {
21+
vendor_id = 0x0000,
22+
product_id = 0x0000,
23+
},
24+
endpoints = {
25+
{
26+
endpoint_id = 0,
27+
clusters = {
28+
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
29+
},
30+
device_types = {
31+
{device_type_id = 0x0016, device_type_revision = 1,} -- RootNode
32+
}
2433
},
25-
endpoints = {
26-
{
27-
endpoint_id = 0,
28-
clusters = {
29-
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
30-
},
31-
device_types = {
32-
{device_type_id = 0x0016, device_type_revision = 1,} -- RootNode
33-
}
34+
{
35+
endpoint_id = 1,
36+
clusters = {
37+
{cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15},
3438
},
35-
{
36-
endpoint_id = 1,
37-
clusters = {
38-
{cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15},
39-
},
40-
device_types = {
41-
{device_type_id = 0x002B, device_type_revision = 1,} -- Fan
42-
}
39+
device_types = {
40+
{device_type_id = 0x002B, device_type_revision = 1,} -- Fan
4341
}
4442
}
43+
}
4544
})
4645

4746
local mock_device_generic = test.mock_device.build_test_matter_device({
48-
profile = t_utils.get_profile_definition("fan-generic.yml"),
49-
manufacturer_info = {
50-
vendor_id = 0x0000,
51-
product_id = 0x0000,
47+
profile = t_utils.get_profile_definition("fan-generic.yml"),
48+
manufacturer_info = {
49+
vendor_id = 0x0000,
50+
product_id = 0x0000,
51+
},
52+
endpoints = {
53+
{
54+
endpoint_id = 0,
55+
clusters = {
56+
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
57+
},
58+
device_types = {
59+
{device_type_id = 0x0016, device_type_revision = 1,} -- RootNode
60+
}
5261
},
53-
endpoints = {
54-
{
55-
endpoint_id = 0,
56-
clusters = {
57-
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
58-
},
59-
device_types = {
60-
{device_type_id = 0x0016, device_type_revision = 1,} -- RootNode
61-
}
62+
{
63+
endpoint_id = 1,
64+
clusters = {
65+
{cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 0},
6266
},
63-
{
64-
endpoint_id = 1,
65-
clusters = {
66-
{cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 0},
67-
},
68-
device_types = {
69-
{device_type_id = 0x002B, device_type_revision = 1,} -- Fan
70-
}
67+
device_types = {
68+
{device_type_id = 0x002B, device_type_revision = 1,} -- Fan
7169
}
7270
}
71+
}
7372
})
7473

7574
local cluster_subscribe_list = {
76-
clusters.FanControl.attributes.FanMode,
77-
clusters.FanControl.attributes.FanModeSequence,
78-
clusters.FanControl.attributes.PercentCurrent,
79-
clusters.FanControl.attributes.WindSupport,
80-
clusters.FanControl.attributes.WindSetting,
81-
clusters.FanControl.attributes.RockSupport,
82-
clusters.FanControl.attributes.RockSetting,
75+
clusters.FanControl.attributes.FanMode,
76+
clusters.FanControl.attributes.FanModeSequence,
77+
clusters.FanControl.attributes.PercentCurrent,
78+
clusters.FanControl.attributes.PercentSetting,
79+
clusters.FanControl.attributes.WindSupport,
80+
clusters.FanControl.attributes.WindSetting,
81+
clusters.FanControl.attributes.RockSupport,
82+
clusters.FanControl.attributes.RockSetting,
8383
}
8484

8585
local cluster_subscribe_list_generic = {
86-
clusters.FanControl.attributes.FanMode,
87-
clusters.FanControl.attributes.FanModeSequence,
88-
clusters.FanControl.attributes.PercentCurrent,
86+
clusters.FanControl.attributes.FanMode,
87+
clusters.FanControl.attributes.FanModeSequence,
88+
clusters.FanControl.attributes.PercentCurrent,
89+
clusters.FanControl.attributes.PercentSetting,
8990
}
9091

9192
local function test_init()

0 commit comments

Comments
 (0)