Implement percentage step sizes for fans (#46512)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
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.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -85,6 +86,11 @@ class BondFan(BondEntity, FanEntity):
return 0 return 0
return ranged_value_to_percentage(self._speed_range, self._speed) 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 @property
def current_direction(self) -> Optional[str]: def current_direction(self) -> Optional[str]:
"""Return fan rotation direction.""" """Return fan rotation direction."""

View file

@ -13,6 +13,7 @@ from pycomfoconnect import (
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -101,6 +102,11 @@ class ComfoConnectFan(FanEntity):
return None return None
return ranged_value_to_percentage(SPEED_RANGE, speed) 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( def turn_on(
self, speed: str = None, percentage=None, preset_mode=None, **kwargs self, speed: str = None, percentage=None, preset_mode=None, **kwargs
) -> None: ) -> None:

View file

@ -215,6 +215,11 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
"""Return the current speed.""" """Return the current speed."""
return self._percentage 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: def set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage.""" """Set the speed of the fan, as a percentage."""
self._percentage = percentage self._percentage = percentage
@ -270,6 +275,11 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
"""Return the current speed.""" """Return the current speed."""
return self._percentage 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: async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage.""" """Set the speed of the fan, as a percentage."""
self._percentage = 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.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -154,6 +155,11 @@ class DysonFanEntity(DysonEntity, FanEntity):
return None return None
return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed)) 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 @property
def preset_modes(self): def preset_modes(self):
"""Return the available preset modes.""" """Return the available preset modes."""

View file

@ -119,6 +119,11 @@ class EsphomeFan(EsphomeEntity, FanEntity):
ORDERED_NAMED_FAN_SPEEDS, self._state.speed 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 @esphome_state_property
def oscillating(self) -> None: def oscillating(self) -> None:
"""Return the oscillation state.""" """Return the oscillation state."""

View file

@ -2,6 +2,7 @@
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
import logging import logging
import math
from typing import List, Optional from typing import List, Optional
import voluptuous as vol import voluptuous as vol
@ -23,6 +24,8 @@ from homeassistant.loader import bind_hass
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
ordered_list_item_to_percentage, ordered_list_item_to_percentage,
percentage_to_ordered_list_item, percentage_to_ordered_list_item,
percentage_to_ranged_value,
ranged_value_to_percentage,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,6 +42,8 @@ SUPPORT_DIRECTION = 4
SUPPORT_PRESET_MODE = 8 SUPPORT_PRESET_MODE = 8
SERVICE_SET_SPEED = "set_speed" SERVICE_SET_SPEED = "set_speed"
SERVICE_INCREASE_SPEED = "increase_speed"
SERVICE_DECREASE_SPEED = "decrease_speed"
SERVICE_OSCILLATE = "oscillate" SERVICE_OSCILLATE = "oscillate"
SERVICE_SET_DIRECTION = "set_direction" SERVICE_SET_DIRECTION = "set_direction"
SERVICE_SET_PERCENTAGE = "set_percentage" SERVICE_SET_PERCENTAGE = "set_percentage"
@ -54,6 +59,7 @@ DIRECTION_REVERSE = "reverse"
ATTR_SPEED = "speed" ATTR_SPEED = "speed"
ATTR_PERCENTAGE = "percentage" ATTR_PERCENTAGE = "percentage"
ATTR_PERCENTAGE_STEP = "percentage_step"
ATTR_SPEED_LIST = "speed_list" ATTR_SPEED_LIST = "speed_list"
ATTR_OSCILLATING = "oscillating" ATTR_OSCILLATING = "oscillating"
ATTR_DIRECTION = "direction" ATTR_DIRECTION = "direction"
@ -142,6 +148,26 @@ async def async_setup(hass, config: dict):
"async_set_speed_deprecated", "async_set_speed_deprecated",
[SUPPORT_SET_SPEED], [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( component.async_register_entity_service(
SERVICE_OSCILLATE, SERVICE_OSCILLATE,
{vol.Required(ATTR_OSCILLATING): cv.boolean}, {vol.Required(ATTR_OSCILLATING): cv.boolean},
@ -246,6 +272,33 @@ class FanEntity(ToggleEntity):
else: else:
await self.async_set_speed(self.percentage_to_speed(percentage)) 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 @_fan_native
def set_preset_mode(self, preset_mode: str) -> None: def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
@ -408,6 +461,19 @@ class FanEntity(ToggleEntity):
return self.speed_to_percentage(self.speed) return self.speed_to_percentage(self.speed)
return 0 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 @property
def speed_list(self) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
@ -531,6 +597,7 @@ 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 data[ATTR_PERCENTAGE] = self.percentage
data[ATTR_PERCENTAGE_STEP] = self.percentage_step
if ( if (
supported_features & SUPPORT_PRESET_MODE supported_features & SUPPORT_PRESET_MODE

View file

@ -100,3 +100,41 @@ set_direction:
options: options:
- "forward" - "forward"
- "reverse" - "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 import math
from typing import Callable 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.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -48,6 +49,13 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
return None return None
return ranged_value_to_percentage(SPEED_RANGE, self._node.status) 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get if the fan is on.""" """Get if the fan is on."""
@ -95,6 +103,13 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
return None return None
return ranged_value_to_percentage(SPEED_RANGE, self._node.status) 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get if the fan is on.""" """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.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -68,6 +69,13 @@ class KNXFan(KnxEntity, FanEntity):
) )
return self._device.current_speed 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( async def async_turn_on(
self, self,
speed: Optional[str] = None, speed: Optional[str] = None,

View file

@ -48,6 +48,11 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity):
ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"] 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 @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features. Speed Only.""" """Flag supported features. Speed Only."""

View file

@ -9,6 +9,7 @@ from homeassistant.components.fan import (
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -72,6 +73,11 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
""" """
return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value) 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 @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""

View file

@ -6,6 +6,7 @@ from pysmartthings import Capability
from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -79,6 +80,11 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
"""Return the current speed percentage.""" """Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed) 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 @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""

View file

@ -46,6 +46,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_FANS = "fans" CONF_FANS = "fans"
CONF_SPEED_LIST = "speeds" CONF_SPEED_LIST = "speeds"
CONF_SPEED_COUNT = "speed_count"
CONF_PRESET_MODES = "preset_modes" CONF_PRESET_MODES = "preset_modes"
CONF_SPEED_TEMPLATE = "speed_template" CONF_SPEED_TEMPLATE = "speed_template"
CONF_PERCENTAGE_TEMPLATE = "percentage_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_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_OSCILLATING_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_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
vol.Optional( vol.Optional(
CONF_SPEED_LIST, CONF_SPEED_LIST,
default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], 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) set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
speed_list = device_config[CONF_SPEED_LIST] speed_list = device_config[CONF_SPEED_LIST]
speed_count = device_config.get(CONF_SPEED_COUNT)
preset_modes = device_config.get(CONF_PRESET_MODES) preset_modes = device_config.get(CONF_PRESET_MODES)
unique_id = device_config.get(CONF_UNIQUE_ID) unique_id = device_config.get(CONF_UNIQUE_ID)
@ -148,6 +151,7 @@ async def _async_create_entities(hass, config):
set_preset_mode_action, set_preset_mode_action,
set_oscillating_action, set_oscillating_action,
set_direction_action, set_direction_action,
speed_count,
speed_list, speed_list,
preset_modes, preset_modes,
unique_id, unique_id,
@ -185,6 +189,7 @@ class TemplateFan(TemplateEntity, FanEntity):
set_preset_mode_action, set_preset_mode_action,
set_oscillating_action, set_oscillating_action,
set_direction_action, set_direction_action,
speed_count,
speed_list, speed_list,
preset_modes, preset_modes,
unique_id, unique_id,
@ -260,6 +265,9 @@ class TemplateFan(TemplateEntity, FanEntity):
self._unique_id = unique_id self._unique_id = unique_id
# Number of valid speeds
self._speed_count = speed_count
# List of valid speeds # List of valid speeds
self._speed_list = speed_list self._speed_list = speed_list
@ -281,6 +289,11 @@ class TemplateFan(TemplateEntity, FanEntity):
"""Flag supported features.""" """Flag supported features."""
return self._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 @property
def speed_list(self) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""

View file

@ -102,6 +102,13 @@ class TuyaFanDevice(TuyaDevice, FanEntity):
"""Oscillate the fan.""" """Oscillate the fan."""
self._tuya.oscillate(oscillating) 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 @property
def oscillating(self): def oscillating(self):
"""Return current oscillating status.""" """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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -77,6 +78,11 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
return ranged_value_to_percentage(SPEED_RANGE, current_level) return ranged_value_to_percentage(SPEED_RANGE, current_level)
return None return None
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return int_states_in_range(SPEED_RANGE)
@property @property
def preset_modes(self): def preset_modes(self):
"""Get the list of available preset modes.""" """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 import entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -130,6 +131,11 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity):
"""Return the current speed percentage.""" """Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode) 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 @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""

View file

@ -87,6 +87,11 @@ class WiLightFan(WiLightDevice, FanEntity):
return None return None
return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed) 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 @property
def current_direction(self) -> str: def current_direction(self) -> str:
"""Return the current direction of the fan.""" """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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -68,6 +69,11 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity):
"""Return the current speed percentage.""" """Return the current speed percentage."""
return ranged_value_to_percentage(SPEED_RANGE, self._state) 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 @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""

View file

@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
int_states_in_range,
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage, ranged_value_to_percentage,
) )
@ -96,6 +97,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
return None return None
return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) 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 @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""

View file

@ -67,7 +67,7 @@ def ranged_value_to_percentage(
(1,255), 127: 50 (1,255), 127: 50
(1,255), 10: 4 (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( def percentage_to_ranged_value(
@ -84,4 +84,14 @@ def percentage_to_ranged_value(
(1,255), 50: 127.5 (1,255), 50: 127.5
(1,255), 4: 10.2 (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 + [ FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [
"fan.percentage_limited_fan", "fan.percentage_limited_fan",
] ]
PERCENTAGE_MODEL_FANS = ["fan.percentage_full_fan", "fan.percentage_limited_fan"]
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -397,6 +398,128 @@ async def test_set_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_PERCENTAGE] == 33 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) @pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)
async def test_oscillate(hass, fan_entity_id): async def test_oscillate(hass, fan_entity_id):
"""Test oscillating the fan.""" """Test oscillating the fan."""

View file

@ -7,9 +7,12 @@ from homeassistant.components.fan import (
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_OSCILLATING, ATTR_OSCILLATING,
ATTR_PERCENTAGE, ATTR_PERCENTAGE,
ATTR_PERCENTAGE_STEP,
ATTR_PRESET_MODE, ATTR_PRESET_MODE,
ATTR_SPEED, ATTR_SPEED,
DOMAIN, DOMAIN,
SERVICE_DECREASE_SPEED,
SERVICE_INCREASE_SPEED,
SERVICE_OSCILLATE, SERVICE_OSCILLATE,
SERVICE_SET_DIRECTION, SERVICE_SET_DIRECTION,
SERVICE_SET_PERCENTAGE, SERVICE_SET_PERCENTAGE,
@ -106,6 +109,38 @@ async def async_set_percentage(
await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True) 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( 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

@ -19,6 +19,8 @@ def test_fanentity():
assert len(fan.speed_list) == 0 assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0 assert len(fan.preset_modes) == 0
assert fan.supported_features == 0 assert fan.supported_features == 0
assert fan.percentage_step == 1
assert fan.speed_count == 100
assert fan.capability_attributes == {} assert fan.capability_attributes == {}
# Test set_speed not required # Test set_speed not required
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
@ -43,6 +45,8 @@ async def test_async_fanentity(hass):
assert len(fan.speed_list) == 0 assert len(fan.speed_list) == 0
assert len(fan.preset_modes) == 0 assert len(fan.preset_modes) == 0
assert fan.supported_features == 0 assert fan.supported_features == 0
assert fan.percentage_step == 1
assert fan.speed_count == 100
assert fan.capability_attributes == {} assert fan.capability_attributes == {}
# Test set_speed not required # Test set_speed not required
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
@ -57,3 +61,7 @@ async def test_async_fanentity(hass):
await fan.async_turn_on() await fan.async_turn_on()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
await fan.async_turn_off() 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') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}",
"oscillating_template": "{{ states('input_select.osc') }}", "oscillating_template": "{{ states('input_select.osc') }}",
"direction_template": "{{ states('input_select.direction') }}", "direction_template": "{{ states('input_select.direction') }}",
"speed_count": "3",
"set_percentage": { "set_percentage": {
"service": "script.fans_set_speed", "service": "script.fans_set_speed",
"data_template": {"percentage": "{{ percentage }}"}, "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) _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): async def test_set_invalid_speed_from_initial_stage(hass, calls):
"""Test set invalid speed when fan is in initial state.""" """Test set invalid speed when fan is in initial state."""
await _register_components(hass) await _register_components(hass)