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>
This commit is contained in:
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) 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.""" """Turn on the fan."""
_LOGGER.debug("Fan async_turn_on called with speed %s", speed) _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.""" """List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] 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.""" """Turn on the fan."""
if speed is None: if speed is None:
speed = SPEED_LOW speed = SPEED_LOW

View File

@ -107,7 +107,20 @@ class DeconzFan(DeconzDevice, FanEntity):
await self._device.set_speed(SPEEDS[speed]) 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.""" """Turn on fan."""
if not speed: if not speed:
speed = convert_speed(self._default_on_speed) speed = convert_speed(self._default_on_speed)

View File

@ -1,14 +1,20 @@
"""Demo fan platform that has a fake fan.""" """Demo fan platform that has a fake fan."""
from typing import List, Optional
from homeassistant.components.fan import ( from homeassistant.components.fan import (
SPEED_HIGH, SPEED_HIGH,
SPEED_LOW, SPEED_LOW,
SPEED_MEDIUM, SPEED_MEDIUM,
SPEED_OFF,
SUPPORT_DIRECTION, SUPPORT_DIRECTION,
SUPPORT_OSCILLATE, SUPPORT_OSCILLATE,
SUPPORT_PRESET_MODE,
SUPPORT_SET_SPEED, SUPPORT_SET_SPEED,
FanEntity, FanEntity,
) )
from homeassistant.const import STATE_OFF
PRESET_MODE_AUTO = "auto"
PRESET_MODE_SMART = "smart"
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
LIMITED_SUPPORT = SUPPORT_SET_SPEED 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.""" """Set up the demo fan platform."""
async_add_entities( async_add_entities(
[ [
DemoFan(hass, "fan1", "Living Room Fan", FULL_SUPPORT), # These fans implement the old model
DemoFan(hass, "fan2", "Ceiling Fan", LIMITED_SUPPORT), 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) await async_setup_platform(hass, {}, async_add_entities)
class DemoFan(FanEntity): class BaseDemoFan(FanEntity):
"""A demonstration fan component.""" """A demonstration fan component that uses legacy fan speeds."""
def __init__( 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: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.hass = hass self.hass = hass
self._unique_id = unique_id self._unique_id = unique_id
self._supported_features = supported_features 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._oscillating = None
self._direction = None self._direction = None
self._name = name self._name = name
if supported_features & SUPPORT_OSCILLATE: if supported_features & SUPPORT_OSCILLATE:
self._oscillating = False self._oscillating = False
if supported_features & SUPPORT_DIRECTION: if supported_features & SUPPORT_DIRECTION:
@ -64,17 +126,42 @@ class DemoFan(FanEntity):
"""No polling needed for a demo fan.""" """No polling needed for a demo fan."""
return False 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 @property
def speed(self) -> str: def speed(self) -> str:
"""Return the current speed.""" """Return the current speed."""
return self._speed return self._speed
@property @property
def speed_list(self) -> list: def speed_list(self):
"""Get the list of available speeds.""" """Return the speed list."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] 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.""" """Turn on the entity."""
if speed is None: if speed is None:
speed = SPEED_MEDIUM speed = SPEED_MEDIUM
@ -83,7 +170,7 @@ class DemoFan(FanEntity):
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Turn off the entity.""" """Turn off the entity."""
self.oscillate(False) self.oscillate(False)
self.set_speed(STATE_OFF) self.set_speed(SPEED_OFF)
def set_speed(self, speed: str) -> None: def set_speed(self, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
@ -100,17 +187,124 @@ class DemoFan(FanEntity):
self._oscillating = oscillating self._oscillating = oscillating
self.schedule_update_ha_state() self.schedule_update_ha_state()
@property
def current_direction(self) -> str: class DemoPercentageFan(BaseDemoFan, FanEntity):
"""Fan direction.""" """A demonstration fan component that uses percentages."""
return self._direction
@property @property
def oscillating(self) -> bool: def percentage(self) -> str:
"""Oscillating.""" """Return the current speed."""
return self._oscillating 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 @property
def supported_features(self) -> int: def preset_mode(self) -> Optional[str]:
"""Flag supported features.""" """Return the current preset mode, e.g., auto, smart, interval, favorite."""
return self._supported_features 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.""" """Initialize the fan."""
super().__init__(device, DysonPureCoolState) 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.""" """Turn on the fan."""
_LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed)
if speed is not None: if speed is not None:
@ -299,7 +312,20 @@ class DysonPureCoolEntity(DysonFanEntity):
"""Initialize the fan.""" """Initialize the fan."""
super().__init__(device, DysonPureCoolV2State) 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.""" """Turn on the fan."""
_LOGGER.debug("Turn on fan %s", self.name) _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) 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.""" """Turn on the fan."""
if speed == SPEED_OFF: if speed == SPEED_OFF:
await self.async_turn_off() await self.async_turn_off()

View File

@ -2,7 +2,7 @@
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
import logging import logging
from typing import Optional from typing import List, Optional
import voluptuous as vol 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 import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import bind_hass 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__) _LOGGER = logging.getLogger(__name__)
@ -32,10 +36,13 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
SUPPORT_SET_SPEED = 1 SUPPORT_SET_SPEED = 1
SUPPORT_OSCILLATE = 2 SUPPORT_OSCILLATE = 2
SUPPORT_DIRECTION = 4 SUPPORT_DIRECTION = 4
SUPPORT_PRESET_MODE = 8
SERVICE_SET_SPEED = "set_speed" SERVICE_SET_SPEED = "set_speed"
SERVICE_OSCILLATE = "oscillate" SERVICE_OSCILLATE = "oscillate"
SERVICE_SET_DIRECTION = "set_direction" SERVICE_SET_DIRECTION = "set_direction"
SERVICE_SET_PERCENTAGE = "set_percentage"
SERVICE_SET_PRESET_MODE = "set_preset_mode"
SPEED_OFF = "off" SPEED_OFF = "off"
SPEED_LOW = "low" SPEED_LOW = "low"
@ -46,9 +53,47 @@ DIRECTION_FORWARD = "forward"
DIRECTION_REVERSE = "reverse" DIRECTION_REVERSE = "reverse"
ATTR_SPEED = "speed" ATTR_SPEED = "speed"
ATTR_PERCENTAGE = "percentage"
ATTR_SPEED_LIST = "speed_list" ATTR_SPEED_LIST = "speed_list"
ATTR_OSCILLATING = "oscillating" ATTR_OSCILLATING = "oscillating"
ATTR_DIRECTION = "direction" 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 @bind_hass
@ -56,7 +101,7 @@ def is_on(hass, entity_id: str) -> bool:
"""Return if the fans are on based on the statemachine.""" """Return if the fans are on based on the statemachine."""
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
if ATTR_SPEED in state.attributes: 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 return state.state == STATE_ON
@ -68,15 +113,27 @@ async def async_setup(hass, config: dict):
await component.async_setup(config) 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( 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_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") 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( component.async_register_entity_service(
SERVICE_SET_SPEED, SERVICE_SET_SPEED,
{vol.Required(ATTR_SPEED): cv.string}, {vol.Required(ATTR_SPEED): cv.string},
"async_set_speed", "async_set_speed_deprecated",
[SUPPORT_SET_SPEED], [SUPPORT_SET_SPEED],
) )
component.async_register_entity_service( component.async_register_entity_service(
@ -91,6 +148,22 @@ async def async_setup(hass, config: dict):
"async_set_direction", "async_set_direction",
[SUPPORT_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 return True
@ -105,19 +178,91 @@ async def async_unload_entry(hass, entry):
return await hass.data[DOMAIN].async_unload_entry(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): class FanEntity(ToggleEntity):
"""Representation of a fan.""" """Representation of a fan."""
@_fan_native
def set_speed(self, speed: str) -> None: def set_speed(self, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
raise NotImplementedError() 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): async def async_set_speed(self, speed: str):
"""Set the speed of the fan.""" """Set the speed of the fan."""
if speed == SPEED_OFF: if speed == SPEED_OFF:
await self.async_turn_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: 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: def set_direction(self, direction: str) -> None:
"""Set the direction of the fan.""" """Set the direction of the fan."""
@ -128,18 +273,75 @@ class FanEntity(ToggleEntity):
await self.hass.async_add_executor_job(self.set_direction, direction) await self.hass.async_add_executor_job(self.set_direction, direction)
# pylint: disable=arguments-differ # 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.""" """Turn on the fan."""
raise NotImplementedError() raise NotImplementedError()
# pylint: disable=arguments-differ # 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.""" """Turn on the fan."""
if speed == SPEED_OFF: if speed == SPEED_OFF:
await self.async_turn_off() await self.async_turn_off()
else: else:
await self.hass.async_add_executor_job( 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: def oscillate(self, oscillating: bool) -> None:
@ -155,15 +357,57 @@ class FanEntity(ToggleEntity):
"""Return true if the entity is on.""" """Return true if the entity is on."""
return self.speed not in [SPEED_OFF, None] 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 @property
def speed(self) -> Optional[str]: def speed(self) -> Optional[str]:
"""Return the current speed.""" """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 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 @property
def speed_list(self) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """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 @property
def current_direction(self) -> Optional[str]: def current_direction(self) -> Optional[str]:
@ -178,9 +422,79 @@ class FanEntity(ToggleEntity):
@property @property
def capability_attributes(self): def capability_attributes(self):
"""Return capability attributes.""" """Return capability attributes."""
attrs = {}
if self.supported_features & SUPPORT_SET_SPEED: if self.supported_features & SUPPORT_SET_SPEED:
return {ATTR_SPEED_LIST: self.speed_list} attrs[ATTR_SPEED_LIST] = self.speed_list
return {}
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 @property
def state_attributes(self) -> dict: def state_attributes(self) -> dict:
@ -196,6 +510,13 @@ class FanEntity(ToggleEntity):
if supported_features & SUPPORT_SET_SPEED: if supported_features & SUPPORT_SET_SPEED:
data[ATTR_SPEED] = self.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 return data
@ -203,3 +524,72 @@ class FanEntity(ToggleEntity):
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return 0 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 ( from . import (
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_OSCILLATING, ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
ATTR_SPEED, ATTR_SPEED,
DOMAIN, DOMAIN,
SERVICE_OSCILLATE, SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION, SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SPEED, SERVICE_SET_SPEED,
) )
@ -31,6 +35,8 @@ ATTRIBUTES = { # attribute: service
ATTR_DIRECTION: SERVICE_SET_DIRECTION, ATTR_DIRECTION: SERVICE_SET_DIRECTION,
ATTR_OSCILLATING: SERVICE_OSCILLATE, ATTR_OSCILLATING: SERVICE_OSCILLATE,
ATTR_SPEED: SERVICE_SET_SPEED, 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 description: Speed setting
example: "low" 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: turn_on:
description: Turns fan on. description: Turns fan on.
fields: fields:
@ -18,6 +38,12 @@ turn_on:
speed: speed:
description: Speed setting description: Speed setting
example: "high" example: "high"
percentage:
description: Percentage speed setting
example: 75
preset_mode:
description: Preset mode setting
example: "auto"
turn_off: turn_off:
description: Turns fan off. description: Turns fan off.

View File

@ -130,9 +130,17 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
{CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0} {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.""" """Turn the specified fan on."""
characteristics = {} characteristics = {}
if not self.is_on: if not self.is_on:

View File

@ -63,7 +63,20 @@ class InsteonFanEntity(InsteonEntity, FanEntity):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_SET_SPEED 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.""" """Turn on the fan."""
if speed is None: if speed is None:
speed = SPEED_MEDIUM speed = SPEED_MEDIUM

View File

@ -71,7 +71,20 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
"""Send the set speed command to the ISY994 fan device.""" """Send the set speed command to the ISY994 fan device."""
self._node.turn_on(val=STATE_TO_VALUE.get(speed, 255)) 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.""" """Send the turn on command to the ISY994 fan device."""
self.set_speed(speed) self.set_speed(speed)
@ -108,7 +121,20 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
if not self._actions.run_then(): if not self._actions.run_then():
_LOGGER.error("Unable to turn off the fan") _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.""" """Send the turn off command to ISY994 fan program."""
if not self._actions.run_else(): if not self._actions.run_else():
_LOGGER.error("Unable to turn on the fan") _LOGGER.error("Unable to turn on the fan")

View File

@ -75,7 +75,20 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity):
"""Flag supported features. Speed Only.""" """Flag supported features. Speed Only."""
return SUPPORT_SET_SPEED 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.""" """Turn the fan on."""
if speed is None: if speed is None:
speed = SPEED_MEDIUM speed = SPEED_MEDIUM

View File

@ -317,7 +317,20 @@ class MqttFan(MqttEntity, FanEntity):
"""Return the oscillation state.""" """Return the oscillation state."""
return self._oscillation 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. """Turn on the entity.
This method is a coroutine. This method is a coroutine.

View File

@ -57,7 +57,16 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
self._previous_speed = speed self._previous_speed = speed
self.values.primary.send_value(SPEED_TO_VALUE[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.""" """Turn the device on."""
if speed is None: if speed is None:
# Value 255 tells device to return to previous value # 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 # the entity state ahead of receiving the confirming push updates
self.async_write_ha_state() 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.""" """Turn the fan on."""
if speed is not None: if speed is not None:
value = SPEED_TO_VALUE[speed] value = SPEED_TO_VALUE[speed]

View File

@ -86,7 +86,14 @@ class SmartyFan(FanEntity):
self._speed = speed self._speed = speed
self._state = True 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.""" """Turn on the fan."""
_LOGGER.debug("Turning on fan. Speed is %s", speed) _LOGGER.debug("Turning on fan. Speed is %s", speed)
if speed is None: if speed is None:

View File

@ -79,7 +79,16 @@ class TasmotaFan(
else: else:
self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed]) 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.""" """Turn the fan on."""
# Tasmota does not support turning a fan on with implicit speed # Tasmota does not support turning a fan on with implicit speed
await self.async_set_speed(speed or fan.SPEED_MEDIUM) 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 the oscillation state."""
return self._direction 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.""" """Turn on the fan."""
await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context) await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context)
self._state = STATE_ON self._state = STATE_ON

View File

@ -75,7 +75,20 @@ class TuyaFanDevice(TuyaDevice, FanEntity):
else: else:
self._tuya.set_speed(speed) 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.""" """Turn on the fan."""
if speed is not None: if speed is not None:
self.set_speed(speed) self.set_speed(speed)

View File

@ -137,7 +137,20 @@ class ValloxFan(FanEntity):
self._available = False self._available = False
_LOGGER.error("Error updating fan: %s", err) _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.""" """Turn the device on."""
_LOGGER.debug("Turn on: %s", speed) _LOGGER.debug("Turn on: %s", speed)

View File

@ -107,7 +107,20 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
self.smartfan.manual_mode() self.smartfan.manual_mode()
self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed)) 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.""" """Turn the device on."""
self.smartfan.turn_on() self.smartfan.turn_on()
self.set_speed(speed) self.set_speed(speed)

View File

@ -185,7 +185,20 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
self._available = False self._available = False
self.wemo.reconnect_with_device() 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.""" """Turn the switch on."""
if speed is None: if speed is None:
try: try:

View File

@ -94,7 +94,20 @@ class WiLightFan(WiLightDevice, FanEntity):
self._direction = self._status["direction"] self._direction = self._status["direction"]
return self._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.""" """Turn on the fan."""
if speed is None: if speed is None:
await self._client.set_fan_direction(self._index, self._direction) 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.""" """Set the speed of the fan."""
self.wink.set_state(True, speed) 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.""" """Turn on the fan."""
self.wink.set_state(True, speed) self.wink.set_state(True, speed)

View File

@ -718,7 +718,20 @@ class XiaomiGenericDevice(FanEntity):
return False 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.""" """Turn the device on."""
if speed: if speed:
# If operation mode was set the device must not be turned on. # If operation mode was set the device must not be turned on.

View File

@ -95,7 +95,16 @@ class BaseFan(FanEntity):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_SET_SPEED 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.""" """Turn the entity on."""
if speed is None: if speed is None:
speed = SPEED_MEDIUM speed = SPEED_MEDIUM

View File

@ -58,7 +58,14 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
"""Set the speed of the fan.""" """Set the speed of the fan."""
self.node.set_dimmer(self.values.primary.value_id, SPEED_TO_VALUE[speed]) 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.""" """Turn the device on."""
if speed is None: if speed is None:
# Value 255 tells device to return to previous value # 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") target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, SPEED_TO_VALUE[speed]) 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.""" """Turn the device on."""
if speed is None: if speed is None:
# Value 255 tells device to return to previous value # 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 import pytest
from homeassistant.components import fan from homeassistant.components import fan
from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ENTITY_MATCH_ALL, ENTITY_MATCH_ALL,
@ -12,7 +13,15 @@ from homeassistant.const import (
) )
from homeassistant.setup import async_setup_component 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) @pytest.fixture(autouse=True)
@ -22,124 +31,338 @@ async def setup_comp(hass):
await hass.async_block_till_done() 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.""" """Test turning on the device."""
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF assert state.state == STATE_OFF
await hass.services.async_call( 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 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( await hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
SERVICE_TURN_ON, 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, blocking=True,
) )
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH 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.""" """Test turning off the device."""
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF assert state.state == STATE_OFF
await hass.services.async_call( 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 assert state.state == STATE_ON
await hass.services.async_call( 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 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.""" """Test turning off all fans."""
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF assert state.state == STATE_OFF
await hass.services.async_call( 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 assert state.state == STATE_ON
await hass.services.async_call( await hass.services.async_call(
fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True 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 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.""" """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 assert state.state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
fan.SERVICE_SET_DIRECTION, 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, 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 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.""" """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 assert state.state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
fan.SERVICE_SET_SPEED, 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, 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 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.""" """Test oscillating the fan."""
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert not state.attributes.get(fan.ATTR_OSCILLATING) assert not state.attributes.get(fan.ATTR_OSCILLATING)
await hass.services.async_call( await hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
fan.SERVICE_OSCILLATE, 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, blocking=True,
) )
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_OSCILLATING] is True assert state.attributes[fan.ATTR_OSCILLATING] is True
await hass.services.async_call( await hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
fan.SERVICE_OSCILLATE, 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, blocking=True,
) )
state = hass.states.get(FAN_ENTITY_ID) state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_OSCILLATING] is False 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.""" """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( 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", "8": "media_player.lounge_room",
"9": "fan.living_room_fan", "9": "fan.living_room_fan",
"10": "fan.ceiling_fan", "10": "fan.ceiling_fan",
"11": "cover.living_room_window", "11": "fan.percentage_full_fan",
"12": "climate.hvac", "12": "fan.percentage_limited_fan",
"13": "climate.heatpump", "13": "fan.preset_only_limited_fan",
"14": "climate.ecobee", "14": "cover.living_room_window",
"15": "light.no_brightness", "15": "climate.hvac",
"16": "humidifier.humidifier", "16": "climate.heatpump",
"17": "humidifier.dehumidifier", "17": "climate.ecobee",
"18": "humidifier.hygrostat", "18": "light.no_brightness",
"19": "scene.light_on", "19": "humidifier.humidifier",
"20": "scene.light_off", "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()} 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 ( from homeassistant.components.fan import (
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_OSCILLATING, ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
ATTR_SPEED, ATTR_SPEED,
DOMAIN, DOMAIN,
SERVICE_OSCILLATE, SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION, SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_SPEED, SERVICE_SET_SPEED,
) )
from homeassistant.const import ( 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.""" """Turn all or specified fan on."""
data = { data = {
key: value 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 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) 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( async def async_set_direction(
hass, entity_id=ENTITY_MATCH_ALL, direction: str = None hass, entity_id=ENTITY_MATCH_ALL, direction: str = None
) -> None: ) -> None:

View File

@ -2,7 +2,7 @@
import pytest import pytest
from homeassistant.components.fan import FanEntity from homeassistant.components.fan import FanEntity, NotValidPresetModeError
class BaseFan(FanEntity): class BaseFan(FanEntity):
@ -17,6 +17,7 @@ def test_fanentity():
fan = BaseFan() fan = BaseFan()
assert fan.state == "off" assert fan.state == "off"
assert len(fan.speed_list) == 0 assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0 assert fan.supported_features == 0
assert fan.capability_attributes == {} assert fan.capability_attributes == {}
# Test set_speed not required # Test set_speed not required
@ -24,7 +25,35 @@ def test_fanentity():
fan.oscillate(True) fan.oscillate(True)
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
fan.set_speed("slow") 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): with pytest.raises(NotImplementedError):
fan.turn_on() fan.turn_on()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
fan.turn_off() 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", "type": "action.devices.types.FAN",
"willReportState": False, "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", "id": "climate.hvac",
"name": {"name": "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