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/test/test_matter_thermo_multiple_device_types.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua index a8acdc253e..4ed44fad6b 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 @@ -197,6 +197,7 @@ local expected_metadata = { { "relativeHumidityMeasurement", "fanMode", + "fanSpeedPercent", "fanOscillationMode", "thermostatHeatingSetpoint", "thermostatCoolingSetpoint" @@ -220,6 +221,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 e40572ff5d..c087eb2e1f 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -320,8 +320,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 73d180da7e..fa95f692ce 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 @@ -38,6 +38,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 @@ -54,7 +55,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 @@ -96,6 +96,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 @@ -123,6 +124,7 @@ local expected_metadata = { { "relativeHumidityMeasurement", "fanMode", + "fanSpeedPercent", "thermostatHeatingSetpoint", "thermostatCoolingSetpoint" }, @@ -134,7 +136,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 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua index 78f5ee3bf2..9b9ceae5da 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -48,7 +48,11 @@ function AttributeHandlers.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 thermostat_utils.tbl_contains(supported_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) @@ -325,40 +329,58 @@ function AttributeHandlers.fan_mode_handler(driver, device, ib, response) end function AttributeHandlers.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 thermostat_utils.get_device_type(device) == fields.THERMOSTAT_DEVICE_TYPE_ID and + device:supports_capability_by_id(capabilities.fanMode.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 {} + -- 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 end - local event = supported_fan_modes_attribute(supportedFanModes, {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}})) end function AttributeHandlers.percent_current_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua index 102b6e8d3b..e6e6994e4b 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua @@ -201,6 +201,7 @@ function DeviceConfiguration.match_modular_profile_thermostat(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)