Implement percentage step sizes for fans (#46512)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/46777/head
J. Nick Koston 2021-02-18 21:05:09 -10:00 committed by GitHub
parent 5df46b60e8
commit f2b303d509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 447 additions and 3 deletions

View File

@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -85,6 +86,11 @@ class BondFan(BondEntity, FanEntity):
return 0
return ranged_value_to_percentage(self._speed_range, self._speed)
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
return int_states_in_range(self._speed_range)
@property
def current_direction(self) -> Optional[str]:
"""Return fan rotation direction."""

View File

@ -13,6 +13,7 @@ from pycomfoconnect import (
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -101,6 +102,11 @@ class ComfoConnectFan(FanEntity):
return None
return ranged_value_to_percentage(SPEED_RANGE, speed)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
def turn_on(
self, speed: str = None, percentage=None, preset_mode=None, **kwargs
) -> None:

View File

@ -215,6 +215,11 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
"""Return the current speed."""
return self._percentage
@property
def speed_count(self) -> Optional[float]:
"""Return the number of speeds the fan supports."""
return 3
def set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._percentage = percentage
@ -270,6 +275,11 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
"""Return the current speed."""
return self._percentage
@property
def speed_count(self) -> Optional[float]:
"""Return the number of speeds the fan supports."""
return 3
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._percentage = percentage

View File

@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -154,6 +155,11 @@ class DysonFanEntity(DysonEntity, FanEntity):
return None
return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed))
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def preset_modes(self):
"""Return the available preset modes."""

View File

@ -119,6 +119,11 @@ class EsphomeFan(EsphomeEntity, FanEntity):
ORDERED_NAMED_FAN_SPEEDS, self._state.speed
)
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
return len(ORDERED_NAMED_FAN_SPEEDS)
@esphome_state_property
def oscillating(self) -> None:
"""Return the oscillation state."""

View File

@ -2,6 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
import math
from typing import List, Optional
import voluptuous as vol
@ -23,6 +24,8 @@ from homeassistant.loader import bind_hass
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
_LOGGER = logging.getLogger(__name__)
@ -39,6 +42,8 @@ SUPPORT_DIRECTION = 4
SUPPORT_PRESET_MODE = 8
SERVICE_SET_SPEED = "set_speed"
SERVICE_INCREASE_SPEED = "increase_speed"
SERVICE_DECREASE_SPEED = "decrease_speed"
SERVICE_OSCILLATE = "oscillate"
SERVICE_SET_DIRECTION = "set_direction"
SERVICE_SET_PERCENTAGE = "set_percentage"
@ -54,6 +59,7 @@ DIRECTION_REVERSE = "reverse"
ATTR_SPEED = "speed"
ATTR_PERCENTAGE = "percentage"
ATTR_PERCENTAGE_STEP = "percentage_step"
ATTR_SPEED_LIST = "speed_list"
ATTR_OSCILLATING = "oscillating"
ATTR_DIRECTION = "direction"
@ -142,6 +148,26 @@ async def async_setup(hass, config: dict):
"async_set_speed_deprecated",
[SUPPORT_SET_SPEED],
)
component.async_register_entity_service(
SERVICE_INCREASE_SPEED,
{
vol.Optional(ATTR_PERCENTAGE_STEP): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
},
"async_increase_speed",
[SUPPORT_SET_SPEED],
)
component.async_register_entity_service(
SERVICE_DECREASE_SPEED,
{
vol.Optional(ATTR_PERCENTAGE_STEP): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
},
"async_decrease_speed",
[SUPPORT_SET_SPEED],
)
component.async_register_entity_service(
SERVICE_OSCILLATE,
{vol.Required(ATTR_OSCILLATING): cv.boolean},
@ -246,6 +272,33 @@ class FanEntity(ToggleEntity):
else:
await self.async_set_speed(self.percentage_to_speed(percentage))
async def async_increase_speed(self, percentage_step=None) -> None:
"""Increase the speed of the fan."""
await self._async_adjust_speed(1, percentage_step)
async def async_decrease_speed(self, percentage_step=None) -> None:
"""Decrease the speed of the fan."""
await self._async_adjust_speed(-1, percentage_step)
async def _async_adjust_speed(self, modifier, percentage_step) -> None:
"""Increase or decrease the speed of the fan."""
current_percentage = self.percentage or 0
if percentage_step is not None:
new_percentage = current_percentage + (percentage_step * modifier)
else:
speed_range = (1, self.speed_count)
speed_index = math.ceil(
percentage_to_ranged_value(speed_range, current_percentage)
)
new_percentage = ranged_value_to_percentage(
speed_range, speed_index + modifier
)
new_percentage = max(0, min(100, new_percentage))
await self.async_set_percentage(new_percentage)
@_fan_native
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
@ -408,6 +461,19 @@ class FanEntity(ToggleEntity):
return self.speed_to_percentage(self.speed)
return 0
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
speed_list = speed_list_without_preset_modes(self.speed_list)
if speed_list:
return len(speed_list)
return 100
@property
def percentage_step(self) -> Optional[float]:
"""Return the step size for percentage."""
return 100 / self.speed_count
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
@ -531,6 +597,7 @@ class FanEntity(ToggleEntity):
if supported_features & SUPPORT_SET_SPEED:
data[ATTR_SPEED] = self.speed
data[ATTR_PERCENTAGE] = self.percentage
data[ATTR_PERCENTAGE_STEP] = self.percentage_step
if (
supported_features & SUPPORT_PRESET_MODE

View File

@ -100,3 +100,41 @@ set_direction:
options:
- "forward"
- "reverse"
increase_speed:
description: Increase the speed of the fan by one speed or a percentage_step.
fields:
entity_id:
description: Name(s) of the entities to increase speed
example: "fan.living_room"
percentage_step:
advanced: true
required: false
description: Increase speed by a percentage. Should be between 0..100. [optional]
example: 50
selector:
number:
min: 0
max: 100
step: 1
unit_of_measurement: "%"
mode: slider
decrease_speed:
description: Decrease the speed of the fan by one speed or a percentage_step.
fields:
entity_id:
description: Name(s) of the entities to decrease speed
example: "fan.living_room"
percentage_step:
advanced: true
required: false
description: Decrease speed by a percentage. Should be between 0..100. [optional]
example: 50
selector:
number:
min: 0
max: 100
step: 1
unit_of_measurement: "%"
mode: slider

View File

@ -2,12 +2,13 @@
import math
from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON
from homeassistant.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -48,6 +49,13 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
return None
return ranged_value_to_percentage(SPEED_RANGE, self._node.status)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
if self._node.protocol == PROTO_INSTEON:
return 3
return int_states_in_range(SPEED_RANGE)
@property
def is_on(self) -> bool:
"""Get if the fan is on."""
@ -95,6 +103,13 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
return None
return ranged_value_to_percentage(SPEED_RANGE, self._node.status)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
if self._node.protocol == PROTO_INSTEON:
return 3
return int_states_in_range(SPEED_RANGE)
@property
def is_on(self) -> bool:
"""Get if the fan is on."""

View File

@ -7,6 +7,7 @@ from xknx.devices.fan import FanSpeedMode
from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -68,6 +69,13 @@ class KNXFan(KnxEntity, FanEntity):
)
return self._device.current_speed
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
if self._step_range is None:
return super().speed_count
return int_states_in_range(self._step_range)
async def async_turn_on(
self,
speed: Optional[str] = None,

View File

@ -48,6 +48,11 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity):
ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"]
)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return len(ORDERED_NAMED_FAN_SPEEDS)
@property
def supported_features(self) -> int:
"""Flag supported features. Speed Only."""

View File

@ -9,6 +9,7 @@ from homeassistant.components.fan import (
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -72,6 +73,11 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
"""
return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def supported_features(self):
"""Flag supported features."""

View File

@ -6,6 +6,7 @@ from pysmartthings import Capability
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -79,6 +80,11 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed)
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def supported_features(self) -> int:
"""Flag supported features."""

View File

@ -46,6 +46,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_FANS = "fans"
CONF_SPEED_LIST = "speeds"
CONF_SPEED_COUNT = "speed_count"
CONF_PRESET_MODES = "preset_modes"
CONF_SPEED_TEMPLATE = "speed_template"
CONF_PERCENTAGE_TEMPLATE = "percentage_template"
@ -86,6 +87,7 @@ FAN_SCHEMA = vol.All(
vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
vol.Optional(
CONF_SPEED_LIST,
default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH],
@ -126,6 +128,7 @@ async def _async_create_entities(hass, config):
set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
speed_list = device_config[CONF_SPEED_LIST]
speed_count = device_config.get(CONF_SPEED_COUNT)
preset_modes = device_config.get(CONF_PRESET_MODES)
unique_id = device_config.get(CONF_UNIQUE_ID)
@ -148,6 +151,7 @@ async def _async_create_entities(hass, config):
set_preset_mode_action,
set_oscillating_action,
set_direction_action,
speed_count,
speed_list,
preset_modes,
unique_id,
@ -185,6 +189,7 @@ class TemplateFan(TemplateEntity, FanEntity):
set_preset_mode_action,
set_oscillating_action,
set_direction_action,
speed_count,
speed_list,
preset_modes,
unique_id,
@ -260,6 +265,9 @@ class TemplateFan(TemplateEntity, FanEntity):
self._unique_id = unique_id
# Number of valid speeds
self._speed_count = speed_count
# List of valid speeds
self._speed_list = speed_list
@ -281,6 +289,11 @@ class TemplateFan(TemplateEntity, FanEntity):
"""Flag supported features."""
return self._supported_features
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return self._speed_count or super().speed_count
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""

View File

@ -102,6 +102,13 @@ class TuyaFanDevice(TuyaDevice, FanEntity):
"""Oscillate the fan."""
self._tuya.oscillate(oscillating)
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
if self.speeds is None:
return super().speed_count
return len(self.speeds)
@property
def oscillating(self):
"""Return current oscillating status."""

View File

@ -6,6 +6,7 @@ from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -77,6 +78,11 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
return ranged_value_to_percentage(SPEED_RANGE, current_level)
return None
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def preset_modes(self):
"""Get the list of available preset modes."""

View File

@ -10,6 +10,7 @@ from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -130,6 +131,11 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def supported_features(self) -> int:
"""Flag supported features."""

View File

@ -87,6 +87,11 @@ class WiLightFan(WiLightDevice, FanEntity):
return None
return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return len(ORDERED_NAMED_FAN_SPEEDS)
@property
def current_direction(self) -> str:
"""Return the current direction of the fan."""

View File

@ -5,6 +5,7 @@ from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -68,6 +69,11 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
"""Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._state)
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def supported_features(self):
"""Flag supported features."""

View File

@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@ -96,6 +97,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
return None
return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value)
@property
def speed_count(self) -> Optional[int]:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property
def supported_features(self) -> int:
"""Flag supported features."""

View File

@ -67,7 +67,7 @@ def ranged_value_to_percentage(
(1,255), 127: 50
(1,255), 10: 4
"""
return int((value * 100) // (low_high_range[1] - low_high_range[0] + 1))
return int((value * 100) // states_in_range(low_high_range))
def percentage_to_ranged_value(
@ -84,4 +84,14 @@ def percentage_to_ranged_value(
(1,255), 50: 127.5
(1,255), 4: 10.2
"""
return (low_high_range[1] - low_high_range[0] + 1) * percentage / 100
return states_in_range(low_high_range) * percentage / 100
def states_in_range(low_high_range: Tuple[float, float]) -> float:
"""Given a range of low and high values return how many states exist."""
return low_high_range[1] - low_high_range[0] + 1
def int_states_in_range(low_high_range: Tuple[float, float]) -> int:
"""Given a range of low and high values return how many integer states exist."""
return int(states_in_range(low_high_range))

View File

@ -27,6 +27,7 @@ LIMITED_AND_FULL_FAN_ENTITY_IDS = FULL_FAN_ENTITY_IDS + [
FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [
"fan.percentage_limited_fan",
]
PERCENTAGE_MODEL_FANS = ["fan.percentage_full_fan", "fan.percentage_limited_fan"]
@pytest.fixture(autouse=True)
@ -397,6 +398,128 @@ async def test_set_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_PERCENTAGE] == 33
@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS)
async def test_increase_decrease_speed(hass, fan_entity_id):
"""Test increasing and decreasing the percentage speed of the device."""
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
assert state.attributes[fan.ATTR_PERCENTAGE_STEP] == 100 / 3
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
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
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
assert state.attributes[fan.ATTR_PERCENTAGE] == 66
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_DECREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_PERCENTAGE] == 66
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_DECREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
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
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_DECREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF
assert state.attributes[fan.ATTR_PERCENTAGE] == 0
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_DECREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF
assert state.attributes[fan.ATTR_PERCENTAGE] == 0
@pytest.mark.parametrize("fan_entity_id", PERCENTAGE_MODEL_FANS)
async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id):
"""Test increasing speed with a percentage step."""
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
assert state.attributes[fan.ATTR_PERCENTAGE] == 25
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
assert state.attributes[fan.ATTR_PERCENTAGE] == 50
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_INCREASE_SPEED,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 75
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_oscillate(hass, fan_entity_id):
"""Test oscillating the fan."""

View File

@ -7,9 +7,12 @@ from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PERCENTAGE_STEP,
ATTR_PRESET_MODE,
ATTR_SPEED,
DOMAIN,
SERVICE_DECREASE_SPEED,
SERVICE_INCREASE_SPEED,
SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE,
@ -106,6 +109,38 @@ async def async_set_percentage(
await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True)
async def async_increase_speed(
hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None
) -> None:
"""Increase speed for all or specified fan."""
data = {
key: value
for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_PERCENTAGE_STEP, percentage_step),
]
if value is not None
}
await hass.services.async_call(DOMAIN, SERVICE_INCREASE_SPEED, data, blocking=True)
async def async_decrease_speed(
hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None
) -> None:
"""Decrease speed for all or specified fan."""
data = {
key: value
for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_PERCENTAGE_STEP, percentage_step),
]
if value is not None
}
await hass.services.async_call(DOMAIN, SERVICE_DECREASE_SPEED, data, blocking=True)
async def async_set_direction(
hass, entity_id=ENTITY_MATCH_ALL, direction: str = None
) -> None:

View File

@ -19,6 +19,8 @@ def test_fanentity():
assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0
assert fan.percentage_step == 1
assert fan.speed_count == 100
assert fan.capability_attributes == {}
# Test set_speed not required
with pytest.raises(NotImplementedError):
@ -43,6 +45,8 @@ async def test_async_fanentity(hass):
assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0
assert fan.supported_features == 0
assert fan.percentage_step == 1
assert fan.speed_count == 100
assert fan.capability_attributes == {}
# Test set_speed not required
with pytest.raises(NotImplementedError):
@ -57,3 +61,7 @@ async def test_async_fanentity(hass):
await fan.async_turn_on()
with pytest.raises(NotImplementedError):
await fan.async_turn_off()
with pytest.raises(NotImplementedError):
await fan.async_increase_speed()
with pytest.raises(NotImplementedError):
await fan.async_decrease_speed()

View File

@ -203,6 +203,7 @@ async def test_templates_with_entities(hass, calls):
"preset_mode_template": "{{ states('input_select.preset_mode') }}",
"oscillating_template": "{{ states('input_select.osc') }}",
"direction_template": "{{ states('input_select.direction') }}",
"speed_count": "3",
"set_percentage": {
"service": "script.fans_set_speed",
"data_template": {"percentage": "{{ percentage }}"},
@ -648,6 +649,46 @@ async def test_set_percentage(hass, calls):
_verify(hass, STATE_ON, SPEED_MEDIUM, 50, None, None, None)
async def test_increase_decrease_speed(hass, calls):
"""Test set valid increase and derease speed."""
await _register_components(hass)
# Turn on fan
await common.async_turn_on(hass, _TEST_FAN)
# Set fan's percentage speed to 100
await common.async_set_percentage(hass, _TEST_FAN, 100)
# verify
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100
_verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None)
# Set fan's percentage speed to 66
await common.async_decrease_speed(hass, _TEST_FAN)
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 66
_verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None)
# Set fan's percentage speed to 33
await common.async_decrease_speed(hass, _TEST_FAN)
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 33
_verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None)
# Set fan's percentage speed to 0
await common.async_decrease_speed(hass, _TEST_FAN)
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 0
_verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None)
# Set fan's percentage speed to 33
await common.async_increase_speed(hass, _TEST_FAN)
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 33
_verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None)
async def test_set_invalid_speed_from_initial_stage(hass, calls):
"""Test set invalid speed when fan is in initial state."""
await _register_components(hass)