Separate fan speeds into percentages and presets modes (#45407)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: John Carr <john.carr@unrouted.co.uk>
pull/45628/head
J. Nick Koston 2021-01-27 17:44:36 -06:00 committed by GitHub
parent 3f948e027a
commit 068d1b5eb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1607 additions and 112 deletions

View File

@ -118,7 +118,20 @@ class BondFan(BondEntity, FanEntity):
self._device.device_id, Action.set_speed(bond_speed)
)
async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan."""
_LOGGER.debug("Fan async_turn_on called with speed %s", speed)

View File

@ -102,7 +102,16 @@ class ComfoConnectFan(FanEntity):
"""List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self, speed: str = None, percentage=None, preset_mode=None, **kwargs
) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_LOW

View File

@ -107,7 +107,20 @@ class DeconzFan(DeconzDevice, FanEntity):
await self._device.set_speed(SPEEDS[speed])
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on fan."""
if not speed:
speed = convert_speed(self._default_on_speed)

View File

@ -1,14 +1,20 @@
"""Demo fan platform that has a fake fan."""
from typing import List, Optional
from homeassistant.components.fan import (
SPEED_HIGH,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_OFF,
SUPPORT_DIRECTION,
SUPPORT_OSCILLATE,
SUPPORT_PRESET_MODE,
SUPPORT_SET_SPEED,
FanEntity,
)
from homeassistant.const import STATE_OFF
PRESET_MODE_AUTO = "auto"
PRESET_MODE_SMART = "smart"
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
LIMITED_SUPPORT = SUPPORT_SET_SPEED
@ -18,8 +24,55 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"""Set up the demo fan platform."""
async_add_entities(
[
DemoFan(hass, "fan1", "Living Room Fan", FULL_SUPPORT),
DemoFan(hass, "fan2", "Ceiling Fan", LIMITED_SUPPORT),
# These fans implement the old model
DemoFan(
hass,
"fan1",
"Living Room Fan",
FULL_SUPPORT,
None,
[
SPEED_OFF,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_HIGH,
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
],
),
DemoFan(
hass,
"fan2",
"Ceiling Fan",
LIMITED_SUPPORT,
None,
[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH],
),
# These fans implement the newer model
AsyncDemoPercentageFan(
hass,
"fan3",
"Percentage Full Fan",
FULL_SUPPORT,
[PRESET_MODE_AUTO, PRESET_MODE_SMART],
None,
),
DemoPercentageFan(
hass,
"fan4",
"Percentage Limited Fan",
LIMITED_SUPPORT,
[PRESET_MODE_AUTO, PRESET_MODE_SMART],
None,
),
AsyncDemoPercentageFan(
hass,
"fan5",
"Preset Only Limited Fan",
SUPPORT_PRESET_MODE,
[PRESET_MODE_AUTO, PRESET_MODE_SMART],
[],
),
]
)
@ -29,21 +82,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities)
class DemoFan(FanEntity):
"""A demonstration fan component."""
class BaseDemoFan(FanEntity):
"""A demonstration fan component that uses legacy fan speeds."""
def __init__(
self, hass, unique_id: str, name: str, supported_features: int
self,
hass,
unique_id: str,
name: str,
supported_features: int,
preset_modes: Optional[List[str]],
speed_list: Optional[List[str]],
) -> None:
"""Initialize the entity."""
self.hass = hass
self._unique_id = unique_id
self._supported_features = supported_features
self._speed = STATE_OFF
self._speed = SPEED_OFF
self._percentage = 0
self._speed_list = speed_list
self._preset_modes = preset_modes
self._preset_mode = None
self._oscillating = None
self._direction = None
self._name = name
if supported_features & SUPPORT_OSCILLATE:
self._oscillating = False
if supported_features & SUPPORT_DIRECTION:
@ -64,17 +126,42 @@ class DemoFan(FanEntity):
"""No polling needed for a demo fan."""
return False
@property
def current_direction(self) -> str:
"""Fan direction."""
return self._direction
@property
def oscillating(self) -> bool:
"""Oscillating."""
return self._oscillating
@property
def supported_features(self) -> int:
"""Flag supported features."""
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) -> list:
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def speed_list(self):
"""Return the speed list."""
return self._speed_list
def turn_on(self, speed: str = None, **kwargs) -> None:
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
@ -83,7 +170,7 @@ class DemoFan(FanEntity):
def turn_off(self, **kwargs) -> None:
"""Turn off the entity."""
self.oscillate(False)
self.set_speed(STATE_OFF)
self.set_speed(SPEED_OFF)
def set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
@ -100,17 +187,124 @@ class DemoFan(FanEntity):
self._oscillating = oscillating
self.schedule_update_ha_state()
@property
def current_direction(self) -> str:
"""Fan direction."""
return self._direction
class DemoPercentageFan(BaseDemoFan, FanEntity):
"""A demonstration fan component that uses percentages."""
@property
def oscillating(self) -> bool:
"""Oscillating."""
return self._oscillating
def percentage(self) -> str:
"""Return the current speed."""
return self._percentage
def set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._percentage = percentage
self._preset_mode = None
self.schedule_update_ha_state()
@property
def supported_features(self) -> int:
"""Flag supported features."""
return self._supported_features
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., auto, smart, interval, favorite."""
return self._preset_mode
@property
def preset_modes(self) -> Optional[List[str]]:
"""Return a list of available preset modes."""
return self._preset_modes
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if preset_mode in self.preset_modes:
self._preset_mode = preset_mode
self._percentage = None
self.schedule_update_ha_state()
else:
raise ValueError(f"Invalid preset mode: {preset_mode}")
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the entity."""
if preset_mode:
self.set_preset_mode(preset_mode)
return
if percentage is None:
percentage = 67
self.set_percentage(percentage)
def turn_off(self, **kwargs) -> None:
"""Turn off the entity."""
self.set_percentage(0)
class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
"""An async demonstration fan component that uses percentages."""
@property
def percentage(self) -> str:
"""Return the current speed."""
return self._percentage
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._percentage = percentage
self._preset_mode = None
self.async_write_ha_state()
@property
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., auto, smart, interval, favorite."""
return self._preset_mode
@property
def preset_modes(self) -> Optional[List[str]]:
"""Return a list of available preset modes."""
return self._preset_modes
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if preset_mode not in self.preset_modes:
raise ValueError(
"{preset_mode} is not a valid preset_mode: {self.preset_modes}"
)
self._preset_mode = preset_mode
self._percentage = None
self.async_write_ha_state()
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the entity."""
if preset_mode:
await self.async_set_preset_mode(preset_mode)
return
if percentage is None:
percentage = 67
await self.async_set_percentage(percentage)
async def async_turn_off(self, **kwargs) -> None:
"""Turn off the entity."""
await self.async_oscillate(False)
await self.async_set_percentage(0)
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
self._direction = direction
self.async_write_ha_state()
async def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation."""
self._oscillating = oscillating
self.async_write_ha_state()

View File

@ -233,7 +233,20 @@ class DysonPureCoolLinkEntity(DysonFanEntity):
"""Initialize the fan."""
super().__init__(device, DysonPureCoolState)
def turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan."""
_LOGGER.debug("Turn on fan %s with speed %s", self.name, speed)
if speed is not None:
@ -299,7 +312,20 @@ class DysonPureCoolEntity(DysonFanEntity):
"""Initialize the fan."""
super().__init__(device, DysonPureCoolV2State)
def turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan."""
_LOGGER.debug("Turn on fan %s", self.name)

View File

@ -79,7 +79,20 @@ class EsphomeFan(EsphomeEntity, FanEntity):
self._static_info.key, speed=_fan_speeds.from_hass(speed)
)
async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan."""
if speed == SPEED_OFF:
await self.async_turn_off()

View File

@ -2,7 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
from typing import Optional
from typing import List, Optional
import voluptuous as vol
@ -20,6 +20,10 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import bind_hass
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
_LOGGER = logging.getLogger(__name__)
@ -32,10 +36,13 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
SUPPORT_SET_SPEED = 1
SUPPORT_OSCILLATE = 2
SUPPORT_DIRECTION = 4
SUPPORT_PRESET_MODE = 8
SERVICE_SET_SPEED = "set_speed"
SERVICE_OSCILLATE = "oscillate"
SERVICE_SET_DIRECTION = "set_direction"
SERVICE_SET_PERCENTAGE = "set_percentage"
SERVICE_SET_PRESET_MODE = "set_preset_mode"
SPEED_OFF = "off"
SPEED_LOW = "low"
@ -46,9 +53,47 @@ DIRECTION_FORWARD = "forward"
DIRECTION_REVERSE = "reverse"
ATTR_SPEED = "speed"
ATTR_PERCENTAGE = "percentage"
ATTR_SPEED_LIST = "speed_list"
ATTR_OSCILLATING = "oscillating"
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_AUTO = "auto"
_NOT_SPEED_SMART = "smart"
_NOT_SPEED_INTERVAL = "interval"
_NOT_SPEED_IDLE = "idle"
_NOT_SPEED_FAVORITE = "favorite"
_NOT_SPEEDS_FILTER = {
_NOT_SPEED_OFF,
_NOT_SPEED_AUTO,
_NOT_SPEED_SMART,
_NOT_SPEED_INTERVAL,
_NOT_SPEED_IDLE,
_NOT_SPEED_FAVORITE,
}
_FAN_NATIVE = "_fan_native"
OFF_SPEED_VALUES = [SPEED_OFF, None]
class NoValidSpeedsError(ValueError):
"""Exception class when there are no valid speeds."""
class NotValidSpeedError(ValueError):
"""Exception class when the speed in not in the speed list."""
class NotValidPresetModeError(ValueError):
"""Exception class when the preset_mode in not in the preset_modes list."""
@bind_hass
@ -56,7 +101,7 @@ def is_on(hass, entity_id: str) -> bool:
"""Return if the fans are on based on the statemachine."""
state = hass.states.get(entity_id)
if ATTR_SPEED in state.attributes:
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, None]
return state.attributes[ATTR_SPEED] not in OFF_SPEED_VALUES
return state.state == STATE_ON
@ -68,15 +113,27 @@ async def async_setup(hass, config: dict):
await component.async_setup(config)
# After the transition to percentage and preset_modes concludes,
# switch this back to async_turn_on and remove async_turn_on_compat
component.async_register_entity_service(
SERVICE_TURN_ON, {vol.Optional(ATTR_SPEED): cv.string}, "async_turn_on"
SERVICE_TURN_ON,
{
vol.Optional(ATTR_SPEED): cv.string,
vol.Optional(ATTR_PERCENTAGE): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
),
vol.Optional(ATTR_PRESET_MODE): cv.string,
},
"async_turn_on_compat",
)
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
# After the transition to percentage and preset_modes concludes,
# remove this service
component.async_register_entity_service(
SERVICE_SET_SPEED,
{vol.Required(ATTR_SPEED): cv.string},
"async_set_speed",
"async_set_speed_deprecated",
[SUPPORT_SET_SPEED],
)
component.async_register_entity_service(
@ -91,6 +148,22 @@ async def async_setup(hass, config: dict):
"async_set_direction",
[SUPPORT_DIRECTION],
)
component.async_register_entity_service(
SERVICE_SET_PERCENTAGE,
{
vol.Required(ATTR_PERCENTAGE): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
},
"async_set_percentage",
[SUPPORT_SET_SPEED],
)
component.async_register_entity_service(
SERVICE_SET_PRESET_MODE,
{vol.Required(ATTR_PRESET_MODE): cv.string},
"async_set_preset_mode",
[SUPPORT_SET_SPEED, SUPPORT_PRESET_MODE],
)
return True
@ -105,19 +178,91 @@ async def async_unload_entry(hass, entry):
return await hass.data[DOMAIN].async_unload_entry(entry)
def _fan_native(method):
"""Native fan method not overridden."""
setattr(method, _FAN_NATIVE, True)
return method
class FanEntity(ToggleEntity):
"""Representation of a fan."""
@_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(
"fan.set_speed is deprecated, 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:
await self.async_turn_off()
return
if 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:
await self.hass.async_add_executor_job(self.set_speed, speed)
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.hass.async_add_executor_job(self.set_speed, 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))
@_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)
@_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)
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:
raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode: {preset_modes}"
)
def set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
@ -128,18 +273,75 @@ class FanEntity(ToggleEntity):
await self.hass.async_add_executor_job(self.set_direction, direction)
# pylint: disable=arguments-differ
def turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
def turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan."""
raise NotImplementedError()
# pylint: disable=arguments-differ
async def async_turn_on(self, speed: Optional[str] = None, **kwargs):
async def async_turn_on_compat(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan.
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.
"""
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."
)
if speed in self.preset_modes:
preset_mode = speed
percentage = None
else:
percentage = self.speed_to_percentage(speed)
elif percentage is not None:
speed = self.percentage_to_speed(percentage)
await self.async_turn_on(
speed=speed,
percentage=percentage,
preset_mode=preset_mode,
**kwargs,
)
# pylint: disable=arguments-differ
async def async_turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs,
) -> None:
"""Turn on the fan."""
if speed == SPEED_OFF:
await self.async_turn_off()
else:
await self.hass.async_add_executor_job(
ft.partial(self.turn_on, speed, **kwargs)
ft.partial(
self.turn_on,
speed=speed,
percentage=percentage,
preset_mode=preset_mode,
**kwargs,
)
)
def oscillate(self, oscillating: bool) -> None:
@ -155,15 +357,57 @@ class FanEntity(ToggleEntity):
"""Return true if the entity is on."""
return self.speed not in [SPEED_OFF, None]
@property
def _implemented_percentage(self):
"""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):
"""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):
"""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) -> Optional[str]:
"""Return the current speed."""
if self._implemented_preset_mode:
preset_mode = self.preset_mode
if preset_mode:
return preset_mode
if self._implemented_percentage:
return self.percentage_to_speed(self.percentage)
return None
@property
def percentage(self) -> Optional[int]:
"""Return the current speed as a percentage."""
if not self._implemented_preset_mode:
if self.speed in self.preset_modes:
return None
if not self._implemented_percentage:
return self.speed_to_percentage(self.speed)
return 0
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return []
speeds = []
if self._implemented_percentage:
speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
if self._implemented_preset_mode:
speeds += self.preset_modes
return speeds
@property
def current_direction(self) -> Optional[str]:
@ -178,9 +422,79 @@ class FanEntity(ToggleEntity):
@property
def capability_attributes(self):
"""Return capability attributes."""
attrs = {}
if self.supported_features & SUPPORT_SET_SPEED:
return {ATTR_SPEED_LIST: self.speed_list}
return {}
attrs[ATTR_SPEED_LIST] = self.speed_list
if (
self.supported_features & SUPPORT_SET_SPEED
or self.supported_features & SUPPORT_PRESET_MODE
):
attrs[ATTR_PRESET_MODES] = self.preset_modes
return attrs
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.
"""
if speed in OFF_SPEED_VALUES:
return 0
speed_list = speed_list_without_preset_modes(self.speed_list)
if speed_list and speed not in speed_list:
raise NotValidSpeedError(f"The speed {speed} is not a valid 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.
"""
if percentage == 0:
return SPEED_OFF
speed_list = speed_list_without_preset_modes(self.speed_list)
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
@property
def state_attributes(self) -> dict:
@ -196,6 +510,13 @@ class FanEntity(ToggleEntity):
if supported_features & SUPPORT_SET_SPEED:
data[ATTR_SPEED] = self.speed
data[ATTR_PERCENTAGE] = self.percentage
if (
supported_features & SUPPORT_PRESET_MODE
or supported_features & SUPPORT_SET_SPEED
):
data[ATTR_PRESET_MODE] = self.preset_mode
return data
@ -203,3 +524,72 @@ class FanEntity(ToggleEntity):
def supported_features(self) -> int:
"""Flag supported features."""
return 0
@property
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., auto, smart, interval, favorite.
Requires SUPPORT_SET_SPEED.
"""
speed = self.speed
if speed in self.preset_modes:
return speed
return None
@property
def preset_modes(self) -> Optional[List[str]]:
"""Return a list of available preset modes.
Requires SUPPORT_SET_SPEED.
"""
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: ["Silent", "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", "Favorite", "Idle"]
"""
return [
speed
for speed in speed_list
if speed.lower() in _NOT_SPEEDS_FILTER and speed.lower() != SPEED_OFF
]

View File

@ -17,10 +17,14 @@ from homeassistant.helpers.typing import HomeAssistantType
from . import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
ATTR_SPEED,
DOMAIN,
SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SPEED,
)
@ -31,6 +35,8 @@ ATTRIBUTES = { # attribute: service
ATTR_DIRECTION: SERVICE_SET_DIRECTION,
ATTR_OSCILLATING: SERVICE_OSCILLATE,
ATTR_SPEED: SERVICE_SET_SPEED,
ATTR_PERCENTAGE: SERVICE_SET_PERCENTAGE,
ATTR_PRESET_MODE: SERVICE_SET_PRESET_MODE,
}

View File

@ -9,6 +9,26 @@ set_speed:
description: Speed setting
example: "low"
set_preset_mode:
description: Set preset mode for a fan device.
fields:
entity_id:
description: Name(s) of entities to change.
example: "fan.kitchen"
preset_mode:
description: New value of preset mode
example: "auto"
set_percentage:
description: Sets fan speed percentage.
fields:
entity_id:
description: Name(s) of the entities to set
example: "fan.living_room"
percentage:
description: Percentage speed setting
example: 25
turn_on:
description: Turns fan on.
fields:
@ -18,6 +38,12 @@ turn_on:
speed:
description: Speed setting
example: "high"
percentage:
description: Percentage speed setting
example: 75
preset_mode:
description: Preset mode setting
example: "auto"
turn_off:
description: Turns fan off.

View File

@ -130,9 +130,17 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
{CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0}
)
async def async_turn_on(self, speed=None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
):
"""Turn the specified fan on."""
characteristics = {}
if not self.is_on:

View File

@ -63,7 +63,20 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
"""Flag supported features."""
return SUPPORT_SET_SPEED
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_MEDIUM

View File

@ -71,7 +71,20 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
"""Send the set speed command to the ISY994 fan device."""
self._node.turn_on(val=STATE_TO_VALUE.get(speed, 255))
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Send the turn on command to the ISY994 fan device."""
self.set_speed(speed)
@ -108,7 +121,20 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
if not self._actions.run_then():
_LOGGER.error("Unable to turn off the fan")
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Send the turn off command to ISY994 fan program."""
if not self._actions.run_else():
_LOGGER.error("Unable to turn on the fan")

View File

@ -75,7 +75,20 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity):
"""Flag supported features. Speed Only."""
return SUPPORT_SET_SPEED
async def async_turn_on(self, speed: str = None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
):
"""Turn the fan on."""
if speed is None:
speed = SPEED_MEDIUM

View File

@ -317,7 +317,20 @@ class MqttFan(MqttEntity, FanEntity):
"""Return the oscillation state."""
return self._oscillation
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the entity.
This method is a coroutine.

View File

@ -57,7 +57,16 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
self._previous_speed = speed
self.values.primary.send_value(SPEED_TO_VALUE[speed])
async def async_turn_on(self, speed=None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
):
"""Turn the device on."""
if speed is None:
# Value 255 tells device to return to previous value

View File

@ -50,7 +50,20 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
# the entity state ahead of receiving the confirming push updates
self.async_write_ha_state()
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn the fan on."""
if speed is not None:
value = SPEED_TO_VALUE[speed]

View File

@ -86,7 +86,14 @@ class SmartyFan(FanEntity):
self._speed = speed
self._state = True
def turn_on(self, speed=None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs):
"""Turn on the fan."""
_LOGGER.debug("Turning on fan. Speed is %s", speed)
if speed is None:

View File

@ -79,7 +79,16 @@ class TasmotaFan(
else:
self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed])
async def async_turn_on(self, speed=None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
):
"""Turn the fan on."""
# Tasmota does not support turning a fan on with implicit speed
await self.async_set_speed(speed or fan.SPEED_MEDIUM)

View File

@ -251,8 +251,20 @@ class TemplateFan(TemplateEntity, FanEntity):
"""Return the oscillation state."""
return self._direction
# pylint: disable=arguments-differ
async def async_turn_on(self, speed: str = None) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the fan."""
await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context)
self._state = STATE_ON

View File

@ -75,7 +75,20 @@ class TuyaFanDevice(TuyaDevice, FanEntity):
else:
self._tuya.set_speed(speed)
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the fan."""
if speed is not None:
self.set_speed(speed)

View File

@ -137,7 +137,20 @@ class ValloxFan(FanEntity):
self._available = False
_LOGGER.error("Error updating fan: %s", err)
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn the device on."""
_LOGGER.debug("Turn on: %s", speed)

View File

@ -107,7 +107,20 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
self.smartfan.manual_mode()
self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed))
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn the device on."""
self.smartfan.turn_on()
self.set_speed(speed)

View File

@ -185,7 +185,20 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
self._available = False
self.wemo.reconnect_with_device()
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn the switch on."""
if speed is None:
try:

View File

@ -94,7 +94,20 @@ class WiLightFan(WiLightDevice, FanEntity):
self._direction = self._status["direction"]
return self._direction
async def async_turn_on(self, speed: str = None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the fan."""
if speed is None:
await self._client.set_fan_direction(self._index, self._direction)

View File

@ -40,7 +40,20 @@ class WinkFanDevice(WinkDevice, FanEntity):
"""Set the speed of the fan."""
self.wink.set_state(True, speed)
def turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the fan."""
self.wink.set_state(True, speed)

View File

@ -718,7 +718,20 @@ class XiaomiGenericDevice(FanEntity):
return False
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn the device on."""
if speed:
# If operation mode was set the device must not be turned on.

View File

@ -95,7 +95,16 @@ class BaseFan(FanEntity):
"""Flag supported features."""
return SUPPORT_SET_SPEED
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self, speed=None, percentage=None, preset_mode=None, **kwargs
) -> None:
"""Turn the entity on."""
if speed is None:
speed = SPEED_MEDIUM

View File

@ -58,7 +58,14 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
"""Set the speed of the fan."""
self.node.set_dimmer(self.values.primary.value_id, SPEED_TO_VALUE[speed])
def turn_on(self, speed=None, **kwargs):
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs):
"""Turn the device on."""
if speed is None:
# Value 255 tells device to return to previous value

View File

@ -72,7 +72,20 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, SPEED_TO_VALUE[speed])
async def async_turn_on(self, speed: Optional[str] = None, **kwargs: Any) -> None:
#
# The fan entity model has changed to use percentages and preset_modes
# instead of speeds.
#
# Please review
# https://developers.home-assistant.io/docs/core/entity/fan/
#
async def async_turn_on(
self,
speed: Optional[str] = None,
percentage: Optional[int] = None,
preset_mode: Optional[str] = None,
**kwargs: Any,
) -> None:
"""Turn the device on."""
if speed is None:
# Value 255 tells device to return to previous value

View File

@ -0,0 +1,87 @@
"""Percentage util functions."""
from typing import List, Tuple
def ordered_list_item_to_percentage(ordered_list: List[str], item: str) -> int:
"""Determine the percentage of an item in an ordered list.
When using this utility for fan speeds, do not include "off"
Given the list: ["low", "medium", "high", "very_high"], this
function will return the following when when the item is passed
in:
low: 25
medium: 50
high: 75
very_high: 100
"""
if item not in ordered_list:
raise ValueError
list_len = len(ordered_list)
list_position = ordered_list.index(item) + 1
return (list_position * 100) // list_len
def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) -> str:
"""Find the item that most closely matches the percentage in an ordered list.
When using this utility for fan speeds, do not include "off"
Given the list: ["low", "medium", "high", "very_high"], this
function will return the following when when the item is passed
in:
1-25: low
26-50: medium
51-75: high
76-100: very_high
"""
list_len = len(ordered_list)
if not list_len:
raise ValueError
for offset, speed in enumerate(ordered_list):
list_position = offset + 1
upper_bound = (list_position * 100) // list_len
if percentage <= upper_bound:
return speed
return ordered_list[-1]
def ranged_value_to_percentage(
low_high_range: Tuple[float, float], value: float
) -> int:
"""Given a range of low and high values convert a single value to a percentage.
When using this utility for fan speeds, do not include 0 if it is off
Given a low value of 1 and a high value of 255 this function
will return:
(1,255), 255: 100
(1,255), 127: 50
(1,255), 10: 4
"""
return int((value * 100) // (low_high_range[1] - low_high_range[0] + 1))
def percentage_to_ranged_value(
low_high_range: Tuple[float, float], percentage: int
) -> float:
"""Given a range of low and high values convert a percentage to a single value.
When using this utility for fan speeds, do not include 0 if it is off
Given a low value of 1 and a high value of 255 this function
will return:
(1,255), 100: 255
(1,255), 50: 127.5
(1,255), 4: 10.2
"""
return (low_high_range[1] - low_high_range[0] + 1) * percentage / 100

View File

@ -2,6 +2,7 @@
import pytest
from homeassistant.components import fan
from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART
from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
@ -12,7 +13,15 @@ from homeassistant.const import (
)
from homeassistant.setup import async_setup_component
FAN_ENTITY_ID = "fan.living_room_fan"
FULL_FAN_ENTITY_IDS = ["fan.living_room_fan", "fan.percentage_full_fan"]
FANS_WITH_PRESET_MODE_ONLY = ["fan.preset_only_limited_fan"]
LIMITED_AND_FULL_FAN_ENTITY_IDS = FULL_FAN_ENTITY_IDS + [
"fan.ceiling_fan",
"fan.percentage_limited_fan",
]
FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [
"fan.percentage_limited_fan",
]
@pytest.fixture(autouse=True)
@ -22,124 +31,338 @@ async def setup_comp(hass):
await hass.async_block_till_done()
async def test_turn_on(hass):
@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS)
async def test_turn_on(hass, fan_entity_id):
"""Test turning on the device."""
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id):
"""Test turning on the device."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_HIGH},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
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_HIGH},
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 100},
blocking=True,
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100
async def test_turn_off(hass):
@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY)
async def test_turn_on_with_preset_mode_only(hass, fan_entity_id):
"""Test turning on the device with a preset_mode and no speed setting."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_AUTO},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO
assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
]
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_SMART},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_SMART
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
assert state.attributes[fan.ATTR_PRESET_MODE] is None
with pytest.raises(ValueError):
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
assert state.attributes[fan.ATTR_PRESET_MODE] is None
@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES)
async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id):
"""Test turning on the device with a preset_mode and speed."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: 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
assert state.attributes[fan.ATTR_SPEED_LIST] == [
fan.SPEED_OFF,
fan.SPEED_LOW,
fan.SPEED_MEDIUM,
fan.SPEED_HIGH,
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
]
assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
]
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 100},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100
assert state.attributes[fan.ATTR_PRESET_MODE] is None
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_SMART},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_SMART
assert state.attributes[fan.ATTR_PERCENTAGE] is None
assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_SMART
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: fan_entity_id}, 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
assert state.attributes[fan.ATTR_PRESET_MODE] is None
with pytest.raises(ValueError):
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"},
blocking=True,
)
await hass.async_block_till_done()
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
assert state.attributes[fan.ATTR_PRESET_MODE] is None
@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS)
async def test_turn_off(hass, fan_entity_id):
"""Test turning off the device."""
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
async def test_turn_off_without_entity_id(hass):
@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS)
async def test_turn_off_without_entity_id(hass, fan_entity_id):
"""Test turning off all fans."""
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
async def test_set_direction(hass):
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_set_direction(hass, fan_entity_id):
"""Test setting the direction of the device."""
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_DIRECTION,
{ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_DIRECTION: fan.DIRECTION_REVERSE},
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_DIRECTION: fan.DIRECTION_REVERSE},
blocking=True,
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE
async def test_set_speed(hass):
@pytest.mark.parametrize("fan_entity_id", 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)
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: fan.SPEED_LOW},
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW},
blocking=True,
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
async def test_oscillate(hass):
@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES)
async def test_set_preset_mode(hass, fan_entity_id):
"""Test setting the preset mode of the device."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: 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", LIMITED_AND_FULL_FAN_ENTITY_IDS)
async def test_set_preset_mode_invalid(hass, fan_entity_id):
"""Test setting a invalid preset mode for the device."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
with pytest.raises(ValueError):
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"},
blocking=True,
)
await hass.async_block_till_done()
with pytest.raises(ValueError):
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"},
blocking=True,
)
await hass.async_block_till_done()
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_set_percentage(hass, fan_entity_id):
"""Test setting the percentage speed of the device."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
assert state.attributes[fan.ATTR_PERCENTAGE] == 33
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_oscillate(hass, fan_entity_id):
"""Test oscillating the fan."""
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
assert not state.attributes.get(fan.ATTR_OSCILLATING)
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_OSCILLATE,
{ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_OSCILLATING: True},
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_OSCILLATING: True},
blocking=True,
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_OSCILLATING] is True
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_OSCILLATE,
{ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_OSCILLATING: False},
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_OSCILLATING: False},
blocking=True,
)
state = hass.states.get(FAN_ENTITY_ID)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_OSCILLATING] is False
async def test_is_on(hass):
@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS)
async def test_is_on(hass, fan_entity_id):
"""Test is on service call."""
assert not fan.is_on(hass, FAN_ENTITY_ID)
assert not fan.is_on(hass, fan_entity_id)
await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True
fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True
)
assert fan.is_on(hass, FAN_ENTITY_ID)
assert fan.is_on(hass, fan_entity_id)

View File

@ -70,16 +70,19 @@ ENTITY_IDS_BY_NUMBER = {
"8": "media_player.lounge_room",
"9": "fan.living_room_fan",
"10": "fan.ceiling_fan",
"11": "cover.living_room_window",
"12": "climate.hvac",
"13": "climate.heatpump",
"14": "climate.ecobee",
"15": "light.no_brightness",
"16": "humidifier.humidifier",
"17": "humidifier.dehumidifier",
"18": "humidifier.hygrostat",
"19": "scene.light_on",
"20": "scene.light_off",
"11": "fan.percentage_full_fan",
"12": "fan.percentage_limited_fan",
"13": "fan.preset_only_limited_fan",
"14": "cover.living_room_window",
"15": "climate.hvac",
"16": "climate.heatpump",
"17": "climate.ecobee",
"18": "light.no_brightness",
"19": "humidifier.humidifier",
"20": "humidifier.dehumidifier",
"21": "humidifier.hygrostat",
"22": "scene.light_on",
"23": "scene.light_off",
}
ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()}

View File

@ -6,10 +6,14 @@ components. Instead call the service directly.
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
ATTR_SPEED,
DOMAIN,
SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SPEED,
)
from homeassistant.const import (
@ -20,11 +24,22 @@ from homeassistant.const import (
)
async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None:
async def async_turn_on(
hass,
entity_id=ENTITY_MATCH_ALL,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
) -> None:
"""Turn all or specified fan on."""
data = {
key: value
for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_SPEED, speed)]
for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_SPEED, speed),
(ATTR_PERCENTAGE, percentage),
(ATTR_PRESET_MODE, preset_mode),
]
if value is not None
}
@ -65,6 +80,32 @@ async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -
await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True)
async def async_set_preset_mode(
hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str = None
) -> None:
"""Set preset mode for all or specified fan."""
data = {
key: value
for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)]
if value is not None
}
await hass.services.async_call(DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True)
async def async_set_percentage(
hass, entity_id=ENTITY_MATCH_ALL, percentage: int = None
) -> None:
"""Set percentage for all or specified fan."""
data = {
key: value
for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)]
if value is not None
}
await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True)
async def async_set_direction(
hass, entity_id=ENTITY_MATCH_ALL, direction: str = None
) -> None:

View File

@ -2,7 +2,7 @@
import pytest
from homeassistant.components.fan import FanEntity
from homeassistant.components.fan import FanEntity, NotValidPresetModeError
class BaseFan(FanEntity):
@ -17,6 +17,7 @@ def test_fanentity():
fan = BaseFan()
assert fan.state == "off"
assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0
assert fan.capability_attributes == {}
# Test set_speed not required
@ -24,7 +25,35 @@ def test_fanentity():
fan.oscillate(True)
with pytest.raises(NotImplementedError):
fan.set_speed("slow")
with pytest.raises(NotImplementedError):
fan.set_percentage(0)
with pytest.raises(NotValidPresetModeError):
fan.set_preset_mode("auto")
with pytest.raises(NotImplementedError):
fan.turn_on()
with pytest.raises(NotImplementedError):
fan.turn_off()
async def test_async_fanentity(hass):
"""Test async fan entity methods."""
fan = BaseFan()
fan.hass = hass
assert fan.state == "off"
assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0
assert fan.capability_attributes == {}
# Test set_speed not required
with pytest.raises(NotImplementedError):
await fan.async_oscillate(True)
with pytest.raises(NotImplementedError):
await fan.async_set_speed("slow")
with pytest.raises(NotImplementedError):
await fan.async_set_percentage(0)
with pytest.raises(NotValidPresetModeError):
await fan.async_set_preset_mode("auto")
with pytest.raises(NotImplementedError):
await fan.async_turn_on()
with pytest.raises(NotImplementedError):
await fan.async_turn_off()

View File

@ -245,6 +245,27 @@ DEMO_DEVICES = [
"type": "action.devices.types.FAN",
"willReportState": False,
},
{
"id": "fan.percentage_full_fan",
"name": {"name": "Percentage Full Fan"},
"traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"],
"type": "action.devices.types.FAN",
"willReportState": False,
},
{
"id": "fan.percentage_limited_fan",
"name": {"name": "Percentage Limited Fan"},
"traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"],
"type": "action.devices.types.FAN",
"willReportState": False,
},
{
"id": "fan.preset_only_limited_fan",
"name": {"name": "Preset Only Limited Fan"},
"traits": ["action.devices.traits.OnOff"],
"type": "action.devices.types.FAN",
"willReportState": False,
},
{
"id": "climate.hvac",
"name": {"name": "Hvac"},

View File

@ -0,0 +1,158 @@
"""Test Home Assistant percentage conversions."""
import math
import pytest
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
SPEED_LOW = "low"
SPEED_MEDIUM = "medium"
SPEED_HIGH = "high"
SPEED_1 = SPEED_LOW
SPEED_2 = SPEED_MEDIUM
SPEED_3 = SPEED_HIGH
SPEED_4 = "very_high"
SPEED_5 = "storm"
SPEED_6 = "hurricane"
SPEED_7 = "solar_wind"
LEGACY_ORDERED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
SMALL_ORDERED_LIST = [SPEED_1, SPEED_2, SPEED_3, SPEED_4]
LARGE_ORDERED_LIST = [SPEED_1, SPEED_2, SPEED_3, SPEED_4, SPEED_5, SPEED_6, SPEED_7]
async def test_ordered_list_percentage_round_trip():
"""Test we can round trip."""
for ordered_list in (SMALL_ORDERED_LIST, LARGE_ORDERED_LIST):
for i in range(1, 100):
ordered_list_item_to_percentage(
ordered_list, percentage_to_ordered_list_item(ordered_list, i)
) == i
async def test_ordered_list_item_to_percentage():
"""Test percentage of an item in an ordered list."""
assert ordered_list_item_to_percentage(LEGACY_ORDERED_LIST, SPEED_LOW) == 33
assert ordered_list_item_to_percentage(LEGACY_ORDERED_LIST, SPEED_MEDIUM) == 66
assert ordered_list_item_to_percentage(LEGACY_ORDERED_LIST, SPEED_HIGH) == 100
assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_1) == 25
assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_2) == 50
assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_3) == 75
assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_4) == 100
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_1) == 14
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_2) == 28
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_3) == 42
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_4) == 57
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_5) == 71
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_6) == 85
assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_7) == 100
with pytest.raises(ValueError):
assert ordered_list_item_to_percentage([], SPEED_1)
async def test_percentage_to_ordered_list_item():
"""Test item that most closely matches the percentage in an ordered list."""
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 1) == SPEED_1
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 25) == SPEED_1
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 26) == SPEED_2
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 50) == SPEED_2
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 51) == SPEED_3
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 75) == SPEED_3
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 76) == SPEED_4
assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 100) == SPEED_4
assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 17) == SPEED_LOW
assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 33) == SPEED_LOW
assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 50) == SPEED_MEDIUM
assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 66) == SPEED_MEDIUM
assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 84) == SPEED_HIGH
assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 100) == SPEED_HIGH
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 1) == SPEED_1
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 14) == SPEED_1
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 25) == SPEED_2
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 26) == SPEED_2
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 28) == SPEED_2
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 29) == SPEED_3
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 41) == SPEED_3
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 42) == SPEED_3
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 43) == SPEED_4
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 56) == SPEED_4
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 50) == SPEED_4
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 51) == SPEED_4
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 75) == SPEED_6
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 76) == SPEED_6
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 100) == SPEED_7
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 1) == SPEED_1
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 25) == SPEED_2
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 26) == SPEED_2
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 50) == SPEED_4
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 51) == SPEED_4
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 75) == SPEED_6
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 76) == SPEED_6
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 100) == SPEED_7
assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 100.1) == SPEED_7
with pytest.raises(ValueError):
assert percentage_to_ordered_list_item([], 100)
async def test_ranged_value_to_percentage_large():
"""Test a large range of low and high values convert a single value to a percentage."""
range = (1, 255)
assert ranged_value_to_percentage(range, 255) == 100
assert ranged_value_to_percentage(range, 127) == 49
assert ranged_value_to_percentage(range, 10) == 3
assert ranged_value_to_percentage(range, 1) == 0
async def test_percentage_to_ranged_value_large():
"""Test a large range of low and high values convert a percentage to a single value."""
range = (1, 255)
assert percentage_to_ranged_value(range, 100) == 255
assert percentage_to_ranged_value(range, 50) == 127.5
assert percentage_to_ranged_value(range, 4) == 10.2
assert math.ceil(percentage_to_ranged_value(range, 100)) == 255
assert math.ceil(percentage_to_ranged_value(range, 50)) == 128
assert math.ceil(percentage_to_ranged_value(range, 4)) == 11
async def test_ranged_value_to_percentage_small():
"""Test a small range of low and high values convert a single value to a percentage."""
range = (1, 6)
assert ranged_value_to_percentage(range, 1) == 16
assert ranged_value_to_percentage(range, 2) == 33
assert ranged_value_to_percentage(range, 3) == 50
assert ranged_value_to_percentage(range, 4) == 66
assert ranged_value_to_percentage(range, 5) == 83
assert ranged_value_to_percentage(range, 6) == 100
async def test_percentage_to_ranged_value_small():
"""Test a small range of low and high values convert a percentage to a single value."""
range = (1, 6)
assert math.ceil(percentage_to_ranged_value(range, 16)) == 1
assert math.ceil(percentage_to_ranged_value(range, 33)) == 2
assert math.ceil(percentage_to_ranged_value(range, 50)) == 3
assert math.ceil(percentage_to_ranged_value(range, 66)) == 4
assert math.ceil(percentage_to_ranged_value(range, 83)) == 5
assert math.ceil(percentage_to_ranged_value(range, 100)) == 6