From 7fbe1dbc99b7cdf503820d2d0356090766ae1f7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 19:57:46 -1000 Subject: [PATCH] Remove legacy fan compatibility shim (#59781) --- homeassistant/components/demo/fan.py | 80 ++----- homeassistant/components/fan/__init__.py | 268 +++-------------------- homeassistant/components/template/fan.py | 113 +--------- tests/components/demo/test_fan.py | 30 ++- tests/components/fan/test_init.py | 18 +- tests/components/template/test_fan.py | 165 +++----------- 6 files changed, 124 insertions(+), 550 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index c406ffdb214..cf5cb7708d2 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -2,10 +2,6 @@ from __future__ import annotations from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, @@ -26,33 +22,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the demo fan platform.""" async_add_entities( [ - # These fans implement the old model - DemoFan( + DemoPercentageFan( hass, "fan1", "Living Room Fan", FULL_SUPPORT, - None, [ - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, PRESET_MODE_AUTO, PRESET_MODE_SMART, PRESET_MODE_SLEEP, PRESET_MODE_ON, ], ), - DemoFan( + DemoPercentageFan( hass, "fan2", "Ceiling Fan", LIMITED_SUPPORT, None, - [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], ), - # These fans implement the newer model AsyncDemoPercentageFan( hass, "fan3", @@ -64,7 +52,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PRESET_MODE_SLEEP, PRESET_MODE_ON, ], - None, ), DemoPercentageFan( hass, @@ -77,7 +64,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PRESET_MODE_SLEEP, PRESET_MODE_ON, ], - None, ), AsyncDemoPercentageFan( hass, @@ -90,7 +76,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PRESET_MODE_SLEEP, PRESET_MODE_ON, ], - [], ), ] ) @@ -111,15 +96,12 @@ class BaseDemoFan(FanEntity): name: str, supported_features: int, preset_modes: list[str] | None, - speed_list: list[str] | None, ) -> None: """Initialize the entity.""" self.hass = hass self._unique_id = unique_id self._supported_features = supported_features - self._speed = SPEED_OFF self._percentage = None - self._speed_list = speed_list self._preset_modes = preset_modes self._preset_mode = None self._oscillating = None @@ -161,52 +143,6 @@ class BaseDemoFan(FanEntity): return self._supported_features -class DemoFan(BaseDemoFan, FanEntity): - """A demonstration fan component that uses legacy fan speeds.""" - - @property - def speed(self) -> str: - """Return the current speed.""" - return self._speed - - @property - def speed_list(self): - """Return the speed list.""" - return self._speed_list - - def turn_on( - self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, - ) -> None: - """Turn on the entity.""" - if speed is None: - speed = SPEED_MEDIUM - self.set_speed(speed) - - def turn_off(self, **kwargs) -> None: - """Turn off the entity.""" - self.oscillate(False) - self.set_speed(SPEED_OFF) - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - self._speed = speed - self.schedule_update_ha_state() - - def set_direction(self, direction: str) -> None: - """Set the direction of the fan.""" - self._direction = direction - self.schedule_update_ha_state() - - def oscillate(self, oscillating: bool) -> None: - """Set oscillation.""" - self._oscillating = oscillating - self.schedule_update_ha_state() - - class DemoPercentageFan(BaseDemoFan, FanEntity): """A demonstration fan component that uses percentages.""" @@ -238,7 +174,7 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode in self.preset_modes: + if self.preset_modes and preset_mode in self.preset_modes: self._preset_mode = preset_mode self._percentage = None self.schedule_update_ha_state() @@ -266,6 +202,16 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): """Turn off the entity.""" self.set_percentage(0) + def set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + self._direction = direction + self.schedule_update_ha_state() + + def oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + self._oscillating = oscillating + self.schedule_update_ha_state() + class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): """An async demonstration fan component that uses percentages.""" diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 9289c899b57..87f50c7d938 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -72,32 +72,7 @@ ATTR_DIRECTION = "direction" ATTR_PRESET_MODE = "preset_mode" ATTR_PRESET_MODES = "preset_modes" -# Invalid speeds do not conform to the entity model, but have crept -# 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_SPEED_SILENT = "silent" - -_NOT_SPEEDS_FILTER = { - _NOT_SPEED_OFF, - _NOT_SPEED_ON, - _NOT_SPEED_AUTO, - _NOT_SPEED_SMART, - _NOT_SPEED_INTERVAL, - _NOT_SPEED_IDLE, - _NOT_SPEED_SILENT, - _NOT_SPEED_SLEEP, - _NOT_SPEED_FAVORITE, -} - -_FAN_NATIVE = "_fan_native" OFF_SPEED_VALUES = [SPEED_OFF, None] @@ -220,12 +195,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) -def _fan_native(method): - """Native fan method not overridden.""" - setattr(method, _FAN_NATIVE, True) - return method - - @dataclass class FanEntityDescription(ToggleEntityDescription): """A class that describes fan entities.""" @@ -243,19 +212,17 @@ class FanEntity(ToggleEntity): _attr_speed_count: int _attr_supported_features: int = 0 - @_fan_native def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" raise NotImplementedError() async def async_set_speed_deprecated(self, speed: str): """Set the speed of the fan.""" - _LOGGER.warning( - "The fan.set_speed service is deprecated, use fan.set_percentage or fan.set_preset_mode instead" + _LOGGER.error( + "The fan.set_speed service is deprecated and will fail in 2022.3 and later, use fan.set_percentage or fan.set_preset_mode instead" ) await self.async_set_speed(speed) - @_fan_native async def async_set_speed(self, speed: str): """Set the speed of the fan.""" if speed == SPEED_OFF: @@ -263,38 +230,20 @@ class FanEntity(ToggleEntity): return if self.preset_modes and speed in self.preset_modes: - if not hasattr(self.async_set_preset_mode, _FAN_NATIVE): - await self.async_set_preset_mode(speed) - return - if not hasattr(self.set_preset_mode, _FAN_NATIVE): - await self.hass.async_add_executor_job(self.set_preset_mode, speed) - return - else: - if not hasattr(self.async_set_percentage, _FAN_NATIVE): - await self.async_set_percentage(self.speed_to_percentage(speed)) - return - if not hasattr(self.set_percentage, _FAN_NATIVE): - await self.hass.async_add_executor_job( - self.set_percentage, self.speed_to_percentage(speed) - ) - return + await self.async_set_preset_mode(speed) + return - await self.hass.async_add_executor_job(self.set_speed, speed) + await self.async_set_percentage(self.speed_to_percentage(speed)) - @_fan_native def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" raise NotImplementedError() - @_fan_native async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" if percentage == 0: await self.async_turn_off() - elif not hasattr(self.set_percentage, _FAN_NATIVE): - await self.hass.async_add_executor_job(self.set_percentage, percentage) - else: - await self.async_set_speed(self.percentage_to_speed(percentage)) + await self.hass.async_add_executor_job(self.set_percentage, percentage) async def async_increase_speed(self, percentage_step: int | None = None) -> None: """Increase the speed of the fan.""" @@ -325,26 +274,18 @@ class FanEntity(ToggleEntity): await self.async_set_percentage(new_percentage) - @_fan_native def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - self._valid_preset_mode_or_raise(preset_mode) - self.set_speed(preset_mode) + raise NotImplementedError() - @_fan_native async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if not hasattr(self.set_preset_mode, _FAN_NATIVE): - await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) - return - - self._valid_preset_mode_or_raise(preset_mode) - await self.async_set_speed(preset_mode) + await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) def _valid_preset_mode_or_raise(self, preset_mode): """Raise NotValidPresetModeError on invalid preset_mode.""" preset_modes = self.preset_modes - if preset_mode not in preset_modes: + if not preset_modes or preset_mode not in preset_modes: raise NotValidPresetModeError( f"The preset_mode {preset_mode} is not a valid preset_mode: {preset_modes}" ) @@ -380,16 +321,15 @@ class FanEntity(ToggleEntity): This _compat version wraps async_turn_on with backwards and forward compatibility. - After the transition to percentage and preset_modes concludes, it - should be removed. + This compatibility shim will be removed in 2022.3 """ if preset_mode is not None: self._valid_preset_mode_or_raise(preset_mode) speed = preset_mode percentage = None elif speed is not None: - _LOGGER.warning( - "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead" + _LOGGER.error( + "Calling fan.turn_on with the speed argument is deprecated and will fail in 2022.3 and later, use percentage or preset_mode instead" ) if self.preset_modes and speed in self.preset_modes: preset_mode = speed @@ -441,52 +381,20 @@ class FanEntity(ToggleEntity): """Return true if the entity is on.""" return self.speed not in [SPEED_OFF, None] - @property - def _implemented_percentage(self) -> bool: - """Return true if percentage has been implemented.""" - return not hasattr(self.set_percentage, _FAN_NATIVE) or not hasattr( - self.async_set_percentage, _FAN_NATIVE - ) - - @property - def _implemented_preset_mode(self) -> bool: - """Return true if preset_mode has been implemented.""" - return not hasattr(self.set_preset_mode, _FAN_NATIVE) or not hasattr( - self.async_set_preset_mode, _FAN_NATIVE - ) - - @property - def _implemented_speed(self) -> bool: - """Return true if speed has been implemented.""" - return not hasattr(self.set_speed, _FAN_NATIVE) or not hasattr( - self.async_set_speed, _FAN_NATIVE - ) - @property def speed(self) -> str | None: """Return the current speed.""" - if self._implemented_preset_mode and (preset_mode := self.preset_mode): + if preset_mode := self.preset_mode: return preset_mode - if self._implemented_percentage: - if (percentage := self.percentage) is None: - return None - return self.percentage_to_speed(percentage) - return None + if (percentage := self.percentage) is None: + return None + return self.percentage_to_speed(percentage) @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" if hasattr(self, "_attr_percentage"): return self._attr_percentage - - if ( - not self._implemented_preset_mode - and self.preset_modes - and self.speed in self.preset_modes - ): - return None - if self.speed is not None and not self._implemented_percentage: - return self.speed_to_percentage(self.speed) return 0 @property @@ -494,10 +402,6 @@ class FanEntity(ToggleEntity): """Return the number of speeds the fan supports.""" if hasattr(self, "_attr_speed_count"): return self._attr_speed_count - - speed_list = speed_list_without_preset_modes(self.speed_list) - if speed_list: - return len(speed_list) return 100 @property @@ -508,11 +412,9 @@ class FanEntity(ToggleEntity): @property def speed_list(self) -> list: """Get the list of available speeds.""" - speeds = [] - if self._implemented_percentage: - speeds += [SPEED_OFF, *LEGACY_SPEED_LIST] - if self._implemented_preset_mode and self.preset_modes: - speeds += self.preset_modes + speeds = [SPEED_OFF, *LEGACY_SPEED_LIST] + if preset_modes := self.preset_modes: + speeds.extend(preset_modes) return speeds @property @@ -540,78 +442,21 @@ 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. - - Officially this should only have to deal with the 4 pre-defined speeds: - - return { - SPEED_OFF: 0, - SPEED_LOW: 33, - SPEED_MEDIUM: 66, - SPEED_HIGH: 100, - }[speed] - - Unfortunately lots of fans make up their own speeds. So the default - mapping is more dynamic. - """ + def speed_to_percentage(self, speed: str) -> int: # pylint: disable=no-self-use + """Map a legacy speed to a percentage.""" if speed in OFF_SPEED_VALUES: return 0 - - speed_list = self._speed_list_without_preset_modes - - if speed_list and speed not in speed_list: + if speed not in LEGACY_SPEED_LIST: raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") + return ordered_list_item_to_percentage(LEGACY_SPEED_LIST, speed) - try: - return ordered_list_item_to_percentage(speed_list, speed) - except ValueError as ex: - raise NoValidSpeedsError( - f"The speed_list {speed_list} does not contain any valid speeds." - ) from ex - - def percentage_to_speed(self, percentage: int) -> str: - """ - Map a percentage onto self.speed_list. - - Officially, this should only have to deal with 4 pre-defined speeds. - - if value == 0: - return SPEED_OFF - elif value <= 33: - return SPEED_LOW - elif value <= 66: - return SPEED_MEDIUM - else: - return SPEED_HIGH - - Unfortunately there is currently a high degree of non-conformancy. - Until fans have been corrected a more complicated and dynamic - mapping is used. - """ + def percentage_to_speed( # pylint: disable=no-self-use + self, percentage: int + ) -> str: + """Map a percentage to a legacy speed.""" if percentage == 0: return SPEED_OFF - - speed_list = self._speed_list_without_preset_modes - - try: - return percentage_to_ordered_list_item(speed_list, percentage) - except ValueError as ex: - raise NoValidSpeedsError( - f"The speed_list {speed_list} does not contain any valid speeds." - ) from ex + return percentage_to_ordered_list_item(LEGACY_SPEED_LIST, percentage) @final @property @@ -652,10 +497,6 @@ class FanEntity(ToggleEntity): """ if hasattr(self, "_attr_preset_mode"): return self._attr_preset_mode - - speed = self.speed - if self.preset_modes and speed in self.preset_modes: - return speed return None @property @@ -666,55 +507,4 @@ class FanEntity(ToggleEntity): """ if hasattr(self, "_attr_preset_modes"): return self._attr_preset_modes - - return preset_modes_from_speed_list(self.speed_list) - - -def speed_list_without_preset_modes(speed_list: list): - """Filter out non-speeds from the speed list. - - The goal is to get the speeds in a list from lowest to - highest by removing speeds that are not valid or out of order - so we can map them to percentages. - - Examples: - input: ["off", "low", "low-medium", "medium", "medium-high", "high", "auto"] - output: ["low", "low-medium", "medium", "medium-high", "high"] - - input: ["off", "auto", "low", "medium", "high"] - output: ["low", "medium", "high"] - - input: ["off", "1", "2", "3", "4", "5", "6", "7", "smart"] - output: ["1", "2", "3", "4", "5", "6", "7"] - - input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"] - output: ["Medium", "High", "Strong"] - """ - - return [speed for speed in speed_list if speed.lower() not in _NOT_SPEEDS_FILTER] - - -def preset_modes_from_speed_list(speed_list: list): - """Filter out non-preset modes from the speed list. - - The goal is to return only preset modes. - - Examples: - input: ["off", "low", "low-medium", "medium", "medium-high", "high", "auto"] - output: ["auto"] - - input: ["off", "auto", "low", "medium", "high"] - output: ["auto"] - - input: ["off", "1", "2", "3", "4", "5", "6", "7", "smart"] - output: ["smart"] - - input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"] - output: ["Auto", "Silent", "Favorite", "Idle"] - """ - - return [ - speed - for speed in speed_list - if speed.lower() in _NOT_SPEEDS_FILTER and speed.lower() != SPEED_OFF - ] + return None diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 4a872d51dbc..d9481e8f7af 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -1,4 +1,6 @@ """Support for Template fans.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -12,16 +14,11 @@ from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, ENTITY_ID_FORMAT, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, - preset_modes_from_speed_list, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -45,10 +42,8 @@ from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) CONF_FANS = "fans" -CONF_SPEED_LIST = "speeds" CONF_SPEED_COUNT = "speed_count" CONF_PRESET_MODES = "preset_modes" -CONF_SPEED_TEMPLATE = "speed_template" CONF_PERCENTAGE_TEMPLATE = "percentage_template" CONF_PRESET_MODE_TEMPLATE = "preset_mode_template" CONF_OSCILLATING_TEMPLATE = "oscillating_template" @@ -67,14 +62,10 @@ _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), - cv.deprecated(CONF_SPEED_LIST), - cv.deprecated(CONF_SPEED_TEMPLATE), - cv.deprecated(CONF_SET_SPEED_ACTION), vol.Schema( { vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_PERCENTAGE_TEMPLATE): cv.template, vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, @@ -82,16 +73,11 @@ FAN_SCHEMA = vol.All( vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), - vol.Optional( - CONF_SPEED_LIST, - default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], - ): cv.ensure_list, vol.Optional(CONF_PRESET_MODES): cv.ensure_list, vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, @@ -112,7 +98,6 @@ async def _async_create_entities(hass, config): friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) state_template = device_config[CONF_VALUE_TEMPLATE] - speed_template = device_config.get(CONF_SPEED_TEMPLATE) percentage_template = device_config.get(CONF_PERCENTAGE_TEMPLATE) preset_mode_template = device_config.get(CONF_PRESET_MODE_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) @@ -127,7 +112,6 @@ async def _async_create_entities(hass, config): set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) - speed_list = device_config[CONF_SPEED_LIST] speed_count = device_config.get(CONF_SPEED_COUNT) preset_modes = device_config.get(CONF_PRESET_MODES) unique_id = device_config.get(CONF_UNIQUE_ID) @@ -138,7 +122,6 @@ async def _async_create_entities(hass, config): device, friendly_name, state_template, - speed_template, percentage_template, preset_mode_template, oscillating_template, @@ -152,7 +135,6 @@ async def _async_create_entities(hass, config): set_oscillating_action, set_direction_action, speed_count, - speed_list, preset_modes, unique_id, ) @@ -175,7 +157,6 @@ class TemplateFan(TemplateEntity, FanEntity): device_id, friendly_name, state_template, - speed_template, percentage_template, preset_mode_template, oscillating_template, @@ -189,7 +170,6 @@ class TemplateFan(TemplateEntity, FanEntity): set_oscillating_action, set_direction_action, speed_count, - speed_list, preset_modes, unique_id, ): @@ -202,7 +182,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._name = friendly_name self._template = state_template - self._speed_template = speed_template self._percentage_template = percentage_template self._preset_mode_template = preset_mode_template self._oscillating_template = oscillating_template @@ -243,13 +222,12 @@ class TemplateFan(TemplateEntity, FanEntity): ) self._state = STATE_OFF - self._speed = None self._percentage = None self._preset_mode = None self._oscillating = None self._direction = None - if self._speed_template or self._percentage_template: + if self._percentage_template: self._supported_features |= SUPPORT_SET_SPEED if self._preset_mode_template and preset_modes: self._supported_features |= SUPPORT_PRESET_MODE @@ -263,17 +241,9 @@ class TemplateFan(TemplateEntity, FanEntity): # Number of valid speeds self._speed_count = speed_count - # List of valid speeds - self._speed_list = speed_list - # List of valid preset modes self._preset_modes = preset_modes - @property - def _implemented_speed(self): - """Return true if speed has been implemented.""" - return bool(self._set_speed_script or self._speed_template) - @property def name(self): """Return the display name of this fan.""" @@ -295,27 +265,15 @@ class TemplateFan(TemplateEntity, FanEntity): return self._speed_count or 100 @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - - @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" - if self._preset_modes is not None: - return self._preset_modes - return preset_modes_from_speed_list(self._speed_list) + return self._preset_modes @property def is_on(self): """Return true if device is on.""" return self._state == STATE_ON - @property - def speed(self): - """Return the current speed.""" - return self._speed - @property def preset_mode(self): """Return the current preset mode.""" @@ -366,30 +324,12 @@ class TemplateFan(TemplateEntity, FanEntity): """Turn off the fan.""" await self._off_script.async_run(context=self._context) self._state = STATE_OFF - - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed not in self.speed_list: - _LOGGER.error( - "Received invalid speed: %s. Expected: %s", speed, self.speed_list - ) - return - - self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON - self._speed = speed + self._percentage = 0 self._preset_mode = None - self._percentage = self.speed_to_percentage(speed) - - if self._set_speed_script: - await self._set_speed_script.async_run( - {ATTR_SPEED: self._speed}, context=self._context - ) async def async_set_percentage(self, percentage: int) -> None: """Set the percentage speed of the fan.""" - speed_list = self.speed_list self._state = STATE_OFF if percentage == 0 else STATE_ON - self._speed = self.percentage_to_speed(percentage) if speed_list else None self._percentage = percentage self._preset_mode = None @@ -400,7 +340,7 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset_mode of the fan.""" - if preset_mode not in self.preset_modes: + if self.preset_modes and preset_mode not in self.preset_modes: _LOGGER.error( "Received invalid preset_mode: %s. Expected: %s", preset_mode, @@ -410,7 +350,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = STATE_ON self._preset_mode = preset_mode - self._speed = preset_mode self._percentage = None if self._set_preset_mode_script: @@ -491,14 +430,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._update_percentage, none_on_template_error=True, ) - if self._speed_template is not None: - self.add_template_attribute( - "_speed", - self._speed_template, - None, - self._update_speed, - none_on_template_error=True, - ) if self._oscillating_template is not None: self.add_template_attribute( "_oscillating", @@ -517,27 +448,6 @@ class TemplateFan(TemplateEntity, FanEntity): ) await super().async_added_to_hass() - @callback - def _update_speed(self, speed): - # Validate speed - speed = str(speed) - - if speed in self._speed_list: - self._speed = speed - self._percentage = self.speed_to_percentage(speed) - self._preset_mode = speed if speed in self.preset_modes else None - elif speed in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._speed = None - self._percentage = 0 - self._preset_mode = None - else: - _LOGGER.error( - "Received invalid speed: %s. Expected: %s", speed, self._speed_list - ) - self._speed = None - self._percentage = 0 - self._preset_mode = None - @callback def _update_percentage(self, percentage): # Validate percentage @@ -545,19 +455,15 @@ class TemplateFan(TemplateEntity, FanEntity): percentage = int(float(percentage)) except ValueError: _LOGGER.error("Received invalid percentage: %s", percentage) - self._speed = None self._percentage = 0 self._preset_mode = None return if 0 <= percentage <= 100: self._percentage = percentage - if self._speed_list: - self._speed = self.percentage_to_speed(percentage) self._preset_mode = None else: _LOGGER.error("Received invalid percentage: %s", percentage) - self._speed = None self._percentage = 0 self._preset_mode = None @@ -566,12 +472,10 @@ class TemplateFan(TemplateEntity, FanEntity): # Validate preset mode preset_mode = str(preset_mode) - if preset_mode in self.preset_modes: - self._speed = preset_mode + if self.preset_modes and preset_mode in self.preset_modes: self._percentage = None self._preset_mode = preset_mode elif preset_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._speed = None self._percentage = None self._preset_mode = None else: @@ -580,7 +484,6 @@ class TemplateFan(TemplateEntity, FanEntity): preset_mode, self.preset_mode, ) - self._speed = None self._percentage = None self._preset_mode = None diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index a788e69b0d3..4767a99d0b3 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -321,7 +321,7 @@ async def test_set_direction(hass, fan_entity_id): assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE -@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) async def test_set_speed(hass, fan_entity_id): """Test setting the speed of the device.""" state = hass.states.get(fan_entity_id) @@ -336,6 +336,34 @@ async def test_set_speed(hass, fan_entity_id): state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_OFF}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + + +@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) +async def test_set_preset_mode_with_legacy_speed_service(hass, fan_entity_id): + """Test setting the preset mode is possible with the legacy service for backwards compat.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: PRESET_MODE_AUTO}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO + assert state.attributes[fan.ATTR_PERCENTAGE] is None + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO + @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) async def test_set_preset_mode(hass, fan_entity_id): diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 3167cb16e67..d29d4378941 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.fan import FanEntity, NotValidPresetModeError +from homeassistant.components.fan import FanEntity class BaseFan(FanEntity): @@ -16,8 +16,8 @@ def test_fanentity(): """Test fan entity methods.""" fan = BaseFan() assert fan.state == "off" - assert len(fan.speed_list) == 0 - assert len(fan.preset_modes) == 0 + assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high + assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 assert fan.speed_count == 100 @@ -26,10 +26,10 @@ def test_fanentity(): with pytest.raises(NotImplementedError): fan.oscillate(True) with pytest.raises(NotImplementedError): - fan.set_speed("slow") + fan.set_speed("low") with pytest.raises(NotImplementedError): fan.set_percentage(0) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotImplementedError): fan.set_preset_mode("auto") with pytest.raises(NotImplementedError): fan.turn_on() @@ -42,8 +42,8 @@ async def test_async_fanentity(hass): fan = BaseFan() fan.hass = hass assert fan.state == "off" - assert len(fan.speed_list) == 0 - assert len(fan.preset_modes) == 0 + assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high + assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 assert fan.speed_count == 100 @@ -52,10 +52,10 @@ async def test_async_fanentity(hass): with pytest.raises(NotImplementedError): await fan.async_oscillate(True) with pytest.raises(NotImplementedError): - await fan.async_set_speed("slow") + await fan.async_set_speed("low") with pytest.raises(NotImplementedError): await fan.async_set_percentage(0) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotImplementedError): await fan.async_set_preset_mode("auto") with pytest.raises(NotImplementedError): await fan.async_turn_on() diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 8ec771a05f3..1f1947183de 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -18,6 +18,7 @@ from homeassistant.components.fan import ( SPEED_OFF, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, + NotValidSpeedError, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -29,8 +30,6 @@ _TEST_FAN = "fan.test_fan" _STATE_INPUT_BOOLEAN = "input_boolean.state" # Represent for fan's state _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" -# Represent for fan's speed -_SPEED_INPUT_SELECT = "input_select.speed" # Represent for fan's preset mode _PRESET_MODE_INPUT_SELECT = "input_select.preset_mode" # Represent for fan's speed percentage @@ -148,7 +147,6 @@ async def test_wrong_template_config(hass, start_ha): {% endif %} """, "percentage_template": "{{ states('input_number.percentage') }}", - "speed_template": "{{ states('input_select.speed') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", @@ -170,7 +168,7 @@ async def test_templates_with_entities(hass, start_ha): _verify(hass, STATE_OFF, None, 0, None, None, None) hass.states.async_set(_STATE_INPUT_BOOLEAN, True) - hass.states.async_set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) + hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 66) hass.states.async_set(_OSC_INPUT, "True") for set_state, set_value, speed, value in [ @@ -262,7 +260,6 @@ async def test_templates_with_entities2(hass, entity, tests, start_ha): "test_fan": { "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", "value_template": "{{ 'on' }}", - "speed_template": "{{ 'medium' }}", "oscillating_template": "{{ 1 == 1 }}", "direction_template": "{{ 'forward' }}", "turn_on": {"service": "script.fan_on"}, @@ -307,9 +304,9 @@ async def test_availability_template_with_entities(hass, start_ha): "fans": { "test_fan": { "value_template": "{{ 'on' }}", - "speed_template": "{{ 'unavailable' }}", "oscillating_template": "{{ 'unavailable' }}", "direction_template": "{{ 'unavailable' }}", + "percentage_template": "{{ 0 }}", "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -325,9 +322,9 @@ async def test_availability_template_with_entities(hass, start_ha): "fans": { "test_fan": { "value_template": "{{ 'on' }}", - "speed_template": "{{ 'medium' }}", "oscillating_template": "{{ 1 == 1 }}", "direction_template": "{{ 'forward' }}", + "percentage_template": "{{ 66 }}", "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -343,9 +340,9 @@ async def test_availability_template_with_entities(hass, start_ha): "fans": { "test_fan": { "value_template": "{{ 'abc' }}", - "speed_template": "{{ '0' }}", "oscillating_template": "{{ 'xyz' }}", "direction_template": "{{ 'right' }}", + "percentage_template": "{{ 0 }}", "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -372,7 +369,6 @@ async def test_template_with_unavailable_entities(hass, states, start_ha): "test_fan": { "value_template": "{{ 'on' }}", "availability_template": "{{ x - 12 }}", - "speed_template": "{{ states('input_select.speed') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", @@ -411,38 +407,34 @@ async def test_set_speed(hass): await _register_components(hass, preset_modes=["auto", "smart"]) await common.async_turn_on(hass, _TEST_FAN) - for cmd, t_state, type, state, value in [ - (SPEED_HIGH, SPEED_HIGH, SPEED_HIGH, STATE_ON, 100), - (SPEED_MEDIUM, SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), - (SPEED_OFF, SPEED_OFF, SPEED_OFF, STATE_OFF, 0), - (SPEED_MEDIUM, SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), - ("invalid", SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), + for cmd, type, state, value in [ + (SPEED_HIGH, SPEED_HIGH, STATE_ON, 100), + (SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), + (SPEED_OFF, SPEED_OFF, STATE_OFF, 0), + (SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), ]: await common.async_set_speed(hass, _TEST_FAN, cmd) - assert hass.states.get(_SPEED_INPUT_SELECT).state == t_state + assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == value _verify(hass, state, type, value, None, None, None) + with pytest.raises(NotValidSpeedError): + await common.async_set_speed(hass, _TEST_FAN, "invalid") + + assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == 66 + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None) + async def test_set_invalid_speed(hass): """Test set invalid speed when fan has valid speed.""" await _register_components(hass) await common.async_turn_on(hass, _TEST_FAN) - for extra in [SPEED_HIGH, "invalid"]: - await common.async_set_speed(hass, _TEST_FAN, extra) - assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) + await common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) + assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == 100 + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) - -async def test_custom_speed_list(hass): - """Test set custom speed list.""" - await _register_components(hass, ["1", "2", "3"]) - - await common.async_turn_on(hass, _TEST_FAN) - for extra in ["1", SPEED_MEDIUM]: - await common.async_set_speed(hass, _TEST_FAN, extra) - assert hass.states.get(_SPEED_INPUT_SELECT).state == "1" - _verify(hass, STATE_ON, "1", 33, None, None, None) + with pytest.raises(NotValidSpeedError): + await common.async_set_speed(hass, _TEST_FAN, "invalid") async def test_set_invalid_direction_from_initial_stage(hass, calls): @@ -610,7 +602,7 @@ def _verify( state = hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == str(expected_state) - assert attributes.get(ATTR_SPEED) == expected_speed + assert attributes.get(ATTR_SPEED) == expected_speed or SPEED_OFF assert attributes.get(ATTR_PERCENTAGE) == expected_percentage assert attributes.get(ATTR_OSCILLATING) == expected_oscillating assert attributes.get(ATTR_DIRECTION) == expected_direction @@ -643,27 +635,12 @@ async def _register_components( }, ) - with assert_setup_component(4, "input_select"): + with assert_setup_component(3, "input_select"): assert await setup.async_setup_component( hass, "input_select", { "input_select": { - "speed": { - "name": "Speed", - "options": [ - "", - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - "1", - "2", - "3", - "auto", - "smart", - ], - }, "preset_mode": { "name": "Preset Mode", "options": ["auto", "smart"], @@ -688,7 +665,6 @@ async def _register_components( test_fan_config = { "value_template": value_template, - "speed_template": "{{ states('input_select.speed') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}", "percentage_template": "{{ states('input_number.percentage') }}", "oscillating_template": "{{ states('input_select.osc') }}", @@ -697,17 +673,19 @@ async def _register_components( "service": "input_boolean.turn_on", "entity_id": _STATE_INPUT_BOOLEAN, }, - "turn_off": { - "service": "input_boolean.turn_off", - "entity_id": _STATE_INPUT_BOOLEAN, - }, - "set_speed": { - "service": "input_select.select_option", - "data_template": { - "entity_id": _SPEED_INPUT_SELECT, - "option": "{{ speed }}", + "turn_off": [ + { + "service": "input_boolean.turn_off", + "entity_id": _STATE_INPUT_BOOLEAN, }, - }, + { + "service": "input_number.set_value", + "data_template": { + "entity_id": _PERCENTAGE_INPUT_NUMBER, + "value": 0, + }, + }, + ], "set_preset_mode": { "service": "input_select.select_option", "data_template": { @@ -738,9 +716,6 @@ async def _register_components( }, } - if speed_list: - test_fan_config["speeds"] = speed_list - if preset_modes: test_fan_config["preset_modes"] = preset_modes @@ -940,71 +915,3 @@ async def test_implemented_preset_mode(hass, start_ha): attributes = state.attributes assert attributes.get("percentage") is None assert attributes.get("supported_features") & SUPPORT_PRESET_MODE - - -@pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - DOMAIN: { - "platform": "template", - "fans": { - "mechanical_ventilation": { - "friendly_name": "Mechanische ventilatie", - "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72", - "value_template": "{{ states('light.mv_snelheid') }}", - "speed_template": "{{ 'fast' }}", - "speeds": ["slow", "fast"], - "set_preset_mode": [ - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": "{{ percentage }}"}, - } - ], - "turn_on": [ - { - "service": "switch.turn_off", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": 40}, - }, - ], - "turn_off": [ - { - "service": "light.turn_off", - "target": { - "entity_id": "light.mv_snelheid", - }, - }, - { - "service": "switch.turn_on", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - ], - }, - }, - } - }, - ], -) -async def test_implemented_speed(hass, start_ha): - """Test a fan that implements speed.""" - assert len(hass.states.async_all()) == 1 - - state = hass.states.get("fan.mechanical_ventilation") - attributes = state.attributes - assert attributes["percentage"] == 100 - assert attributes["speed"] == "fast"