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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ components:
- id: fanMode
version: 1
optional: true
- id: fanSpeedPercent
version: 1
config:
values:
- key: "percent.value"
range: [ 1, 100 ]
optional: true
- id: fanOscillationMode
version: 1
optional: true
Expand Down
56 changes: 40 additions & 16 deletions drivers/SmartThings/matter-thermostat/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@ local log = require "log"
local clusters = require "st.matter.clusters"
local embedded_cluster_utils = require "embedded-cluster-utils"
local im = require "st.matter.interaction_model"

local MatterDriver = require "st.matter.driver"
local utils = require "st.utils"
local version = require "version"

local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities"
-- declare match_profile function for use throughout file
local match_profile

-- Include driver-side definitions when lua libs api version is < 10
local version = require "version"
if version.api < 10 then
clusters.HepaFilterMonitoring = require "HepaFilterMonitoring"
clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring"
Expand Down Expand Up @@ -966,6 +965,7 @@ local function match_modular_profile_thermostat(driver, device)

if #fan_eps > 0 then
table.insert(main_component_capabilities, capabilities.fanMode.ID)
table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID)
end
if #rock_eps > 0 then
table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID)
Expand Down Expand Up @@ -1392,7 +1392,11 @@ local function system_mode_handler(driver, device, ib, response)
return
end

local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {}
local supported_modes = device:get_latest_state(
device:endpoint_to_component(ib.endpoint_id),
capabilities.thermostatMode.ID,
capabilities.thermostatMode.supportedThermostatModes.NAME
) or {}
-- check that the given mode was in the supported modes list
if tbl_contains(supported_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then
device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]())
Expand Down Expand Up @@ -1538,39 +1542,59 @@ local function fan_mode_handler(driver, device, ib, response)
end

local function fan_mode_sequence_handler(driver, device, ib, response)
local supportedFanModes, supported_fan_modes_attribute
local supported_fan_modes, supported_fan_modes_capability, supported_fan_modes_attribute
if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then
supportedFanModes = { "off", "low", "medium", "high" }
supported_fan_modes = { "off", "low", "medium", "high" }
elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then
supportedFanModes = { "off", "low", "high" }
supported_fan_modes = { "off", "low", "high" }
elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then
supportedFanModes = { "off", "low", "medium", "high", "auto" }
supported_fan_modes = { "off", "low", "medium", "high", "auto" }
elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then
supportedFanModes = { "off", "low", "high", "auto" }
supported_fan_modes = { "off", "low", "high", "auto" }
elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then
supportedFanModes = { "off", "high", "auto" }
supported_fan_modes = { "off", "high", "auto" }
else
supportedFanModes = { "off", "high" }
supported_fan_modes = { "off", "high" }
end

if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then
supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes
supported_fan_modes_capability = capabilities.airPurifierFanMode
supported_fan_modes_attribute = supported_fan_modes_capability.supportedAirPurifierFanModes
elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then
supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes
supported_fan_modes_capability = capabilities.airConditionerFanMode
supported_fan_modes_attribute = supported_fan_modes_capability.supportedAcFanModes
elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then
supported_fan_modes_capability = capabilities.thermostatFanMode
supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes
-- Our thermostat fan mode control is not granular enough to handle all of the supported modes
if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and
ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then
supportedFanModes = { "auto", "on" }
supported_fan_modes = { "auto", "on" }
else
supportedFanModes = { "on" }
supported_fan_modes = { "on" }
end
else
supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes
supported_fan_modes_capability = capabilities.fanMode
supported_fan_modes_attribute = supported_fan_modes_capability.supportedFanModes
end

-- remove 'off' as a supported fan mode for thermostat device types, unless the
-- device previously had 'off' as a supported fan mode to avoid breaking routines
if get_device_type(device) == THERMOSTAT_DEVICE_TYPE_ID then
Copy link
Contributor

Choose a reason for hiding this comment

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

have you checked if any routines use off? want to make sure this isn't superfluous at this point.

local prev_supported_fan_modes = device:get_latest_state(
device:endpoint_to_component(ib.endpoint_id),
supported_fan_modes_capability.ID,
supported_fan_modes_attribute.NAME
) or {}
if not tbl_contains(prev_supported_fan_modes, "off") then
local _, off_idx = tbl_contains(supported_fan_modes, "off")
if off_idx then
table.remove(supported_fan_modes, off_idx)
end
end
Comment on lines +1589 to +1594
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm starting to wonder, we don't expect this value to change, do we? I almost wonder if it makes sense to just return early if some prev_supported_fan_modes is found, or to just use this table rather than dynamically remove something off the newly made supported_fan_modes table.

However, if we don't go that route, then:

Suggested change
if not tbl_contains(prev_supported_fan_modes, "off") then
local _, off_idx = tbl_contains(supported_fan_modes, "off")
if off_idx then
table.remove(supported_fan_modes, off_idx)
end
end
-- per the definitions set above, the first index always contains "off"
if prev_supported_fan_modes[1] ~= "off" then
table.remove(supported_fan_modes, 1)
end

Copy link
Contributor

@hcarter-775 hcarter-775 Nov 20, 2025

Choose a reason for hiding this comment

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

I see this rule wouldn't work for thermostatFanMode. To solve this, I'd say do the following:

if get_device_type(device) == THERMOSTAT_DEVICE_TYPE_ID and device:supports_capability_by_id(capabilities.fanMode.ID) then

end

local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}})
local event = supported_fan_modes_attribute(supported_fan_modes, {visibility = {displayed = false}})
device:emit_event_for_endpoint(ib.endpoint_id, event)
Comment on lines +1597 to 1598
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
local event = supported_fan_modes_attribute(supported_fan_modes, {visibility = {displayed = false}})
device:emit_event_for_endpoint(ib.endpoint_id, event)
device:emit_event_for_endpoint(ib.endpoint_id, supported_fan_modes_attribute(supported_fan_modes, {visibility = {displayed = false}}))

nit

end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ local expected_metadata = {
{
"relativeHumidityMeasurement",
"fanMode",
"fanSpeedPercent",
"fanOscillationMode",
"thermostatHeatingSetpoint",
"thermostatCoolingSetpoint"
Expand All @@ -231,6 +232,7 @@ local new_cluster_subscribe_list = {
clusters.RelativeHumidityMeasurement.attributes.MeasuredValue,
clusters.FanControl.attributes.FanMode,
clusters.FanControl.attributes.FanModeSequence,
clusters.FanControl.attributes.PercentCurrent,
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.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,6 @@ test.register_message_test(
}
)

-- test.socket.matter:__expect_send({mock_device_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_auto)})

test.register_message_test(
"Thermostat mode reports should generate correct messages",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ local mock_device_basic = test.mock_device.build_test_matter_device({
{cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0},
{cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 0},
},
device_types = {
{device_type_id = 0x0301, device_type_revision = 1} -- Thermostat
Expand All @@ -65,7 +66,6 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at
subscribe_request:merge(cluster:subscribe(generic_mock_device))
end
end
test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request})
return subscribe_request
end

Expand Down Expand Up @@ -107,6 +107,7 @@ local function test_init()

test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" })
subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes)
test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request_basic})
end

-- run the profile configuration tests
Expand Down Expand Up @@ -134,6 +135,7 @@ local expected_metadata = {
{
"relativeHumidityMeasurement",
"fanMode",
"fanSpeedPercent",
"thermostatHeatingSetpoint",
"thermostatCoolingSetpoint"
},
Expand All @@ -145,7 +147,28 @@ local expected_metadata = {
test.register_coroutine_test(
"Device with modular profile should enable correct optional capabilities",
function()
test_thermostat_device_type_update_modular_profile(mock_device_basic, expected_metadata, subscribe_request_basic)
local subscribed_attributes = {
clusters.Thermostat.attributes.LocalTemperature,
clusters.Thermostat.attributes.OccupiedCoolingSetpoint,
clusters.Thermostat.attributes.OccupiedHeatingSetpoint,
clusters.Thermostat.attributes.AbsMinCoolSetpointLimit,
clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit,
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.attributes.SystemMode,
clusters.Thermostat.attributes.ThermostatRunningState,
clusters.Thermostat.attributes.ControlSequenceOfOperation,
clusters.TemperatureMeasurement.attributes.MeasuredValue,
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
clusters.TemperatureMeasurement.attributes.MaxMeasuredValue,
clusters.RelativeHumidityMeasurement.attributes.MeasuredValue,
clusters.FanControl.attributes.FanMode,
clusters.FanControl.attributes.FanModeSequence,
clusters.FanControl.attributes.PercentCurrent,
clusters.PowerSource.attributes.BatPercentRemaining,
}
local subscribe_request = initialize_mock_device(mock_device_basic, subscribed_attributes)
test_thermostat_device_type_update_modular_profile(mock_device_basic, expected_metadata, subscribe_request)
end,
{ test_init = test_init }
)
Expand Down
Loading