1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00

Remove legacy fan compatibility shim (#59781)

This commit is contained in:
J. Nick Koston 2021-12-03 19:57:46 -10:00 committed by GitHub
parent 566716d697
commit 7fbe1dbc99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 124 additions and 550 deletions

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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"