diff --git a/homeassistant/components/prana/fan.py b/homeassistant/components/prana/fan.py index 718b7edfb09e..f7c494475189 100644 --- a/homeassistant/components/prana/fan.py +++ b/homeassistant/components/prana/fan.py @@ -3,7 +3,6 @@ from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum -import math from typing import Any, override from prana_local_api_client.models.prana_state import FanState @@ -137,11 +136,14 @@ class PranaFan(PranaBaseEntity, FanEntity): await self.async_turn_off() return await self.coordinator.api_client.set_speed( - math.ceil( - percentage_to_ranged_value( - self.entity_description.speed_range(self.coordinator), - percentage, - ) + max( + 1, + round( + percentage_to_ranged_value( + self.entity_description.speed_range(self.coordinator), + percentage, + ) + ), ) * PRANA_SPEED_MULTIPLIER, self._api_target_key, diff --git a/tests/components/prana/test_fan.py b/tests/components/prana/test_fan.py index bf51222420b3..17907eea0cbc 100644 --- a/tests/components/prana/test_fan.py +++ b/tests/components/prana/test_fan.py @@ -1,6 +1,5 @@ """Integration-style tests for Prana fans.""" -import math from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -153,7 +152,7 @@ async def test_fans_set_percentage( blocking=True, ) expected_speed = ( - math.ceil(percentage_to_ranged_value((1, fan_mock_state.max_speed), 50)) + round(percentage_to_ranged_value((1, fan_mock_state.max_speed), 50)) * PRANA_SPEED_MULTIPLIER ) mock_prana_api.set_speed.assert_called_once_with( @@ -171,6 +170,58 @@ async def test_fans_set_percentage( mock_prana_api.set_speed_is_on.assert_called_with(False, expected_api_key) +@pytest.mark.parametrize( + ("percentage", "expected_step"), + [ + (1, 1), # any non-zero percentage turns the fan on at least at step 1 + (16, 1), + (17, 1), # UI-displayed value for step 1 of 6 (16.67% rounded up) + (33, 2), + (50, 3), + (66, 4), + (67, 4), # UI-displayed value for step 4 of 6 (66.67% rounded up) + (83, 5), + (100, 6), + ], +) +async def test_fans_percentage_maps_to_nearest_step( + hass: HomeAssistant, + mock_prana_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + percentage: int, + expected_step: int, +) -> None: + """Test that displayed percentages map back to the same speed step. + + A 6-step fan reports step 4 as 66.67%, which the UI displays as 67%. + Sending that displayed value back must select step 4 again, not step 5. + """ + mock_prana_api.get_state.return_value.supply.max_speed = 6 + target, fan_mock_state = await _async_setup_fan_entity( + hass, + mock_prana_api, + mock_config_entry, + entity_registry, + "supply", + False, + ) + + fan_mock_state.is_on = True + await hass.async_block_till_done() + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: target, ATTR_PERCENTAGE: percentage}, + blocking=True, + ) + mock_prana_api.set_speed.assert_called_once_with( + expected_step * PRANA_SPEED_MULTIPLIER, + "supply", + ) + + @pytest.mark.parametrize( ("type_key", "is_bound_mode", "expected_api_key"), FAN_TEST_CASES,