Skip to content

Commit

Permalink
Add TURN_OFF/TURN_ON feature flags for fan (#121447)
Browse files Browse the repository at this point in the history
  • Loading branch information
gjohansson-ST authored Jul 19, 2024
1 parent 1727780 commit ca4c617
Show file tree
Hide file tree
Showing 58 changed files with 858 additions and 132 deletions.
3 changes: 3 additions & 0 deletions homeassistant/components/baf/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ class BAFFan(BAFEntity, FanEntity):
FanEntityFeature.SET_SPEED
| FanEntityFeature.DIRECTION
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
_enable_turn_on_off_backwards_compatibility = False
_attr_preset_modes = [PRESET_MODE_AUTO]
_attr_speed_count = SPEED_COUNT
_attr_name = None
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/balboa/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ async def async_setup_entry(
class BalboaPumpFanEntity(BalboaEntity, FanEntity):
"""Representation of a Balboa Spa pump fan entity."""

_attr_supported_features = FanEntityFeature.SET_SPEED
_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
_enable_turn_on_off_backwards_compatibility = False
_attr_translation_key = "pump"

def __init__(self, control: SpaControl) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/bond/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def __init__(self, data: BondData, device: BondDevice) -> None:
super().__init__(data, device)
if self._device.has_action(Action.BREEZE_ON):
self._attr_preset_modes = [PRESET_MODE_BREEZE]
features = FanEntityFeature(0)
features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
if self._device.supports_speed():
features |= FanEntityFeature.SET_SPEED
if self._device.supports_direction():
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/comfoconnect/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ class ComfoConnectFan(FanEntity):

_attr_icon = "mdi:air-conditioner"
_attr_should_poll = False
_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE
_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
_enable_turn_on_off_backwards_compatibility = False
_attr_preset_modes = PRESET_MODES
current_speed: float | None = None

Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/deconz/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ class DeconzFan(DeconzDevice[Light], FanEntity):
TYPE = DOMAIN
_default_on_speed = LightFanSpeed.PERCENT_50

_attr_supported_features = FanEntityFeature.SET_SPEED
_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
)
_enable_turn_on_off_backwards_compatibility = False

def __init__(self, device: Light, hub: DeconzHub) -> None:
"""Set up fan."""
Expand Down
15 changes: 12 additions & 3 deletions homeassistant/components/demo/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@
PRESET_MODE_ON = "on"

FULL_SUPPORT = (
FanEntityFeature.SET_SPEED | FanEntityFeature.OSCILLATE | FanEntityFeature.DIRECTION
FanEntityFeature.SET_SPEED
| FanEntityFeature.OSCILLATE
| FanEntityFeature.DIRECTION
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
LIMITED_SUPPORT = (
FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
)
LIMITED_SUPPORT = FanEntityFeature.SET_SPEED


async def async_setup_entry(
Expand Down Expand Up @@ -75,7 +81,9 @@ async def async_setup_entry(
hass,
"fan5",
"Preset Only Limited Fan",
FanEntityFeature.PRESET_MODE,
FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON,
[
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
Expand All @@ -92,6 +100,7 @@ class BaseDemoFan(FanEntity):

_attr_should_poll = False
_attr_translation_key = "demo"
_enable_turn_on_off_backwards_compatibility = False

def __init__(
self,
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/esphome/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
"""A fan implementation for ESPHome."""

_supports_speed_levels: bool = True
_enable_turn_on_off_backwards_compatibility = False

async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
Expand Down Expand Up @@ -148,7 +149,7 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None:
api_version = self._api_version
supports_speed_levels = api_version.major == 1 and api_version.minor > 3
self._supports_speed_levels = supports_speed_levels
flags = FanEntityFeature(0)
flags = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
if static_info.supports_oscillation:
flags |= FanEntityFeature.OSCILLATE
if static_info.supports_speed:
Expand Down
126 changes: 109 additions & 17 deletions homeassistant/components/fan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
from datetime import timedelta
from enum import IntFlag
import functools as ft
Expand Down Expand Up @@ -30,6 +31,7 @@
)
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.percentage import (
Expand All @@ -53,6 +55,8 @@ class FanEntityFeature(IntFlag):
OSCILLATE = 2
DIRECTION = 4
PRESET_MODE = 8
TURN_OFF = 16
TURN_ON = 32


# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
Expand Down Expand Up @@ -132,9 +136,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Optional(ATTR_PRESET_MODE): cv.string,
},
"async_handle_turn_on_service",
[FanEntityFeature.TURN_ON],
)
component.async_register_entity_service(
SERVICE_TURN_OFF, {}, "async_turn_off", [FanEntityFeature.TURN_OFF]
)
component.async_register_entity_service(
SERVICE_TOGGLE,
{},
"async_toggle",
[FanEntityFeature.TURN_OFF, FanEntityFeature.TURN_ON],
)
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_INCREASE_SPEED,
{
Expand Down Expand Up @@ -228,6 +240,99 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_speed_count: int
_attr_supported_features: FanEntityFeature = FanEntityFeature(0)

__mod_supported_features: FanEntityFeature = FanEntityFeature(0)
# Integrations should set `_enable_turn_on_off_backwards_compatibility` to False
# once migrated and set the feature flags TURN_ON/TURN_OFF as needed.
_enable_turn_on_off_backwards_compatibility: bool = True

def __getattribute__(self, __name: str) -> Any:
"""Get attribute.
Modify return of `supported_features` to
include `_mod_supported_features` if attribute is set.
"""
if __name != "supported_features":
return super().__getattribute__(__name)

# Convert the supported features to ClimateEntityFeature.
# Remove this compatibility shim in 2025.1 or later.
_supported_features: FanEntityFeature = super().__getattribute__(
"supported_features"
)
_mod_supported_features: FanEntityFeature = super().__getattribute__(
"_FanEntity__mod_supported_features"
)
if type(_supported_features) is int: # noqa: E721
_features = FanEntityFeature(_supported_features)
self._report_deprecated_supported_features_values(_features)
else:
_features = _supported_features

if not _mod_supported_features:
return _features

# Add automatically calculated FanEntityFeature.TURN_OFF/TURN_ON to
# supported features and return it
return _features | _mod_supported_features

@callback
def add_to_platform_start(
self,
hass: HomeAssistant,
platform: EntityPlatform,
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates)

def _report_turn_on_off(feature: str, method: str) -> None:
"""Log warning not implemented turn on/off feature."""
report_issue = self._suggest_report_issue()
message = (
"Entity %s (%s) does not set FanEntityFeature.%s"
" but implements the %s method. Please %s"
)
_LOGGER.warning(
message,
self.entity_id,
type(self),
feature,
method,
report_issue,
)

# Adds FanEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented
# This should be removed in 2025.2.
if self._enable_turn_on_off_backwards_compatibility is False:
# Return if integration has migrated already
return

supported_features = self.supported_features
if supported_features & (FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF):
# The entity supports both turn_on and turn_off, the backwards compatibility
# checks are not needed
return

if not supported_features & FanEntityFeature.TURN_OFF and (
type(self).async_turn_off is not ToggleEntity.async_turn_off
or type(self).turn_off is not ToggleEntity.turn_off
):
# turn_off implicitly supported by implementing turn_off method
_report_turn_on_off("TURN_OFF", "turn_off")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
FanEntityFeature.TURN_OFF
)

if not supported_features & FanEntityFeature.TURN_ON and (
type(self).async_turn_on is not FanEntity.async_turn_on
or type(self).turn_on is not FanEntity.turn_on
):
# turn_on implicitly supported by implementing turn_on method
_report_turn_on_off("TURN_ON", "turn_on")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
FanEntityFeature.TURN_ON
)

def set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
raise NotImplementedError
Expand Down Expand Up @@ -388,7 +493,7 @@ def oscillating(self) -> bool | None:
def capability_attributes(self) -> dict[str, list[str] | None]:
"""Return capability attributes."""
attrs = {}
supported_features = self.supported_features_compat
supported_features = self.supported_features

if (
FanEntityFeature.SET_SPEED in supported_features
Expand All @@ -403,7 +508,7 @@ def capability_attributes(self) -> dict[str, list[str] | None]:
def state_attributes(self) -> dict[str, float | str | None]:
"""Return optional state attributes."""
data: dict[str, float | str | None] = {}
supported_features = self.supported_features_compat
supported_features = self.supported_features

if FanEntityFeature.DIRECTION in supported_features:
data[ATTR_DIRECTION] = self.current_direction
Expand All @@ -427,19 +532,6 @@ def supported_features(self) -> FanEntityFeature:
"""Flag supported features."""
return self._attr_supported_features

@property
def supported_features_compat(self) -> FanEntityFeature:
"""Return the supported features as FanEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = FanEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features

@cached_property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., auto, smart, interval, favorite.
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/fan/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ turn_on:
target:
entity:
domain: fan
supported_features:
- fan.FanEntityFeature.TURN_ON
fields:
percentage:
filter:
Expand All @@ -53,6 +55,8 @@ turn_off:
target:
entity:
domain: fan
supported_features:
- fan.FanEntityFeature.TURN_OFF

oscillate:
target:
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/fjaraskupan/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ def _constructor(coordinator: FjaraskupanCoordinator):
class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity):
"""Fan entity."""

_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE
_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
_enable_turn_on_off_backwards_compatibility = False
_attr_has_entity_name = True
_attr_name = None

Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/freedompro/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class FreedomproFan(CoordinatorEntity[FreedomproDataUpdateCoordinator], FanEntit
_attr_name = None
_attr_is_on = False
_attr_percentage = 0
_enable_turn_on_off_backwards_compatibility = False

def __init__(
self,
Expand All @@ -62,8 +63,11 @@ def __init__(
model=device["type"],
name=device["name"],
)
self._attr_supported_features = (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
)
if "rotationSpeed" in self._characteristics:
self._attr_supported_features = FanEntityFeature.SET_SPEED
self._attr_supported_features |= FanEntityFeature.SET_SPEED

@property
def is_on(self) -> bool | None:
Expand Down
11 changes: 9 additions & 2 deletions homeassistant/components/group/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
FanEntityFeature.SET_SPEED,
FanEntityFeature.DIRECTION,
FanEntityFeature.OSCILLATE,
FanEntityFeature.TURN_OFF,
FanEntityFeature.TURN_ON,
}

DEFAULT_NAME = "Fan Group"
Expand Down Expand Up @@ -107,6 +109,7 @@ class FanGroup(GroupEntity, FanEntity):
"""Representation of a FanGroup."""

_attr_available: bool = False
_enable_turn_on_off_backwards_compatibility = False

def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
"""Initialize a FanGroup entity."""
Expand Down Expand Up @@ -200,11 +203,15 @@ async def async_turn_on(
if percentage is not None:
await self.async_set_percentage(percentage)
return
await self._async_call_all_entities(SERVICE_TURN_ON)
await self._async_call_supported_entities(
SERVICE_TURN_ON, FanEntityFeature.TURN_ON, {}
)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fans off."""
await self._async_call_all_entities(SERVICE_TURN_OFF)
await self._async_call_supported_entities(
SERVICE_TURN_OFF, FanEntityFeature.TURN_OFF, {}
)

async def _async_call_supported_entities(
self, service: str, support_flag: int, data: dict[str, Any]
Expand Down
Loading

0 comments on commit ca4c617

Please sign in to comment.