diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 59d4d6e9dd8e3b..bd7c23b87f73ca 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -253,8 +253,13 @@ def _calculate_features( return self._feature_map = feature_map self._attr_supported_features = FanEntityFeature(0) + # PercentSetting is always a mandatory attribute of the FanControl cluster, + # so percentage-based speed control is always available. + self._attr_supported_features |= FanEntityFeature.SET_SPEED + # Reset to default so a featuremap change from MultiSpeed -> non-MultiSpeed + # does not leave a stale speed_count / percentage_step. + self._attr_speed_count = 100 if feature_map & FanControlFeature.kMultiSpeed: - self._attr_supported_features |= FanEntityFeature.SET_SPEED self._attr_speed_count = int( self.get_matter_attribute_value(clusters.FanControl.Attributes.SpeedMax) ) diff --git a/tests/components/matter/snapshots/test_fan.ambr b/tests/components/matter/snapshots/test_fan.ambr index cf5c8c568ffe06..df0b19220c15b0 100644 --- a/tests/components/matter/snapshots/test_fan.ambr +++ b/tests/components/matter/snapshots/test_fan.ambr @@ -37,7 +37,7 @@ 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'fan', 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterFan-514-0', 'unit_of_measurement': None, @@ -47,6 +47,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Longan link HVAC', + 'percentage': 0, + 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': list([ 'low', @@ -54,7 +56,7 @@ 'high', 'auto', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.longan_link_hvac', @@ -174,7 +176,7 @@ 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'fan', 'unique_id': '00000000000004D2-0000000000000049-MatterNodeDevice-1-MatterFan-514-0', 'unit_of_measurement': None, @@ -184,13 +186,15 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Mock Extractor hood', + 'percentage': 0, + 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': list([ 'low', 'medium', 'high', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.mock_extractor_hood', @@ -450,7 +454,7 @@ 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'fan', 'unique_id': '00000000000004D2-0000000000000072-MatterNodeDevice-1-MatterFan-514-0', 'unit_of_measurement': None, @@ -460,13 +464,15 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'SL-RangeHood', + 'percentage': 0, + 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': list([ 'low', 'medium', 'high', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.sl_rangehood', diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 6da7d3e86f0fe8..b9bba9d218d5b5 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -16,6 +16,7 @@ DOMAIN as FAN_DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, FanEntityFeature, ) from homeassistant.const import ( @@ -441,3 +442,34 @@ async def test_fan_features( state = hass.states.get(entity_id) assert state assert state.attributes["preset_modes"] == preset_modes + + +@pytest.mark.parametrize("node_fixture", ["silabs_range_hood"]) +async def test_fan_set_percentage_without_multispeed( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test percentage control on a fan without the MultiSpeed feature. + + PercentSetting is mandatory in the FanControl cluster regardless of features, + so SET_SPEED must be available and write to PercentSetting (attribute 0x0002). + """ + entity_id = "fan.sl_rangehood" + state = hass.states.get(entity_id) + assert state + assert state.attributes["supported_features"] & FanEntityFeature.SET_SPEED + assert state.attributes["percentage_step"] == 1.0 + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 75}, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=matter_node.node_id, + attribute_path="1/514/2", + value=75, + )