diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index bd6661b6c2b..cb6036a8938 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -15,6 +15,8 @@ from homeassistant.components.fan import ( PRESET_MODE_AUTO = "auto" PRESET_MODE_SMART = "smart" +PRESET_MODE_SLEEP = "sleep" +PRESET_MODE_ON = "on" FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED @@ -38,6 +40,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= SPEED_HIGH, PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ], ), DemoFan( @@ -54,7 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "fan3", "Percentage Full Fan", FULL_SUPPORT, - [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, + ], None, ), DemoPercentageFan( @@ -62,7 +71,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "fan4", "Percentage Limited Fan", LIMITED_SUPPORT, - [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, + ], None, ), AsyncDemoPercentageFan( @@ -70,7 +84,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "fan5", "Preset Only Limited Fan", SUPPORT_PRESET_MODE, - [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, + ], [], ), ] @@ -99,7 +118,7 @@ class BaseDemoFan(FanEntity): self._unique_id = unique_id self._supported_features = supported_features self._speed = SPEED_OFF - self._percentage = 0 + self._percentage = None self._speed_list = speed_list self._preset_modes = preset_modes self._preset_mode = None diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 7b6b083c964..8d6fcbea2c9 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -64,18 +64,22 @@ ATTR_PRESET_MODES = "preset_modes" # into core integrations at some point so we are temporarily # accommodating them in the transition to percentages. _NOT_SPEED_OFF = "off" +_NOT_SPEED_ON = "on" _NOT_SPEED_AUTO = "auto" _NOT_SPEED_SMART = "smart" _NOT_SPEED_INTERVAL = "interval" _NOT_SPEED_IDLE = "idle" _NOT_SPEED_FAVORITE = "favorite" +_NOT_SPEED_SLEEP = "sleep" _NOT_SPEEDS_FILTER = { _NOT_SPEED_OFF, + _NOT_SPEED_ON, _NOT_SPEED_AUTO, _NOT_SPEED_SMART, _NOT_SPEED_INTERVAL, _NOT_SPEED_IDLE, + _NOT_SPEED_SLEEP, _NOT_SPEED_FAVORITE, } @@ -83,6 +87,8 @@ _FAN_NATIVE = "_fan_native" OFF_SPEED_VALUES = [SPEED_OFF, None] +LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + class NoValidSpeedsError(ValueError): """Exception class when there are no valid speeds.""" @@ -386,7 +392,10 @@ class FanEntity(ToggleEntity): if preset_mode: return preset_mode if self._implemented_percentage: - return self.percentage_to_speed(self.percentage) + percentage = self.percentage + if percentage is None: + return None + return self.percentage_to_speed(percentage) return None @property @@ -404,7 +413,7 @@ class FanEntity(ToggleEntity): """Get the list of available speeds.""" speeds = [] if self._implemented_percentage: - speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + speeds += [SPEED_OFF, *LEGACY_SPEED_LIST] if self._implemented_preset_mode: speeds += self.preset_modes return speeds @@ -434,6 +443,17 @@ class FanEntity(ToggleEntity): return attrs + @property + def _speed_list_without_preset_modes(self) -> list: + """Return the speed list without preset modes. + + This property provides forward and backwards + compatibility for conversion to percentage speeds. + """ + if not self._implemented_speed: + return LEGACY_SPEED_LIST + return speed_list_without_preset_modes(self.speed_list) + def speed_to_percentage(self, speed: str) -> int: """ Map a speed to a percentage. @@ -453,7 +473,7 @@ class FanEntity(ToggleEntity): if speed in OFF_SPEED_VALUES: return 0 - speed_list = speed_list_without_preset_modes(self.speed_list) + speed_list = self._speed_list_without_preset_modes if speed_list and speed not in speed_list: raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") @@ -487,7 +507,7 @@ class FanEntity(ToggleEntity): if percentage == 0: return SPEED_OFF - speed_list = speed_list_without_preset_modes(self.speed_list) + speed_list = self._speed_list_without_preset_modes try: return percentage_to_ordered_list_item(speed_list, percentage) diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 5297e64bda9..2439d49685c 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -2,7 +2,12 @@ import pytest from homeassistant.components import fan -from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART +from homeassistant.components.demo.fan import ( + PRESET_MODE_AUTO, + PRESET_MODE_ON, + PRESET_MODE_SLEEP, + PRESET_MODE_SMART, +) from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -60,6 +65,28 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_MEDIUM}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -71,6 +98,39 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 66}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 0}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 0 + @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY) async def test_turn_on_with_preset_mode_only(hass, fan_entity_id): @@ -89,6 +149,8 @@ async def test_turn_on_with_preset_mode_only(hass, fan_entity_id): assert state.attributes[fan.ATTR_PRESET_MODES] == [ PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ] await hass.services.async_call( @@ -145,10 +207,14 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): fan.SPEED_HIGH, PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ] assert state.attributes[fan.ATTR_PRESET_MODES] == [ PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ] await hass.services.async_call(