diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml index 1e7bd97dc7..9fdecf0af3 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml @@ -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 diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 58f0d75344..6f8ecee76e 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -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" @@ -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) @@ -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]()) @@ -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 + 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 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) end 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 7f9577fa8f..eacde8b651 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 @@ -208,6 +208,7 @@ local expected_metadata = { { "relativeHumidityMeasurement", "fanMode", + "fanSpeedPercent", "fanOscillationMode", "thermostatHeatingSetpoint", "thermostatCoolingSetpoint" @@ -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. } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua index 44ed395797..5e5fe81f75 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -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", { 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 6645d39692..8c4c1b0aad 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 @@ -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 @@ -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 @@ -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 @@ -134,6 +135,7 @@ local expected_metadata = { { "relativeHumidityMeasurement", "fanMode", + "fanSpeedPercent", "thermostatHeatingSetpoint", "thermostatCoolingSetpoint" }, @@ -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 } )