From ca4c617d4bec671c7e0c04c3201a860c93e577d4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 19 Jul 2024 11:35:24 +0200 Subject: [PATCH] Add TURN_OFF/TURN_ON feature flags for fan (#121447) --- homeassistant/components/baf/fan.py | 3 + homeassistant/components/balboa/fan.py | 7 +- homeassistant/components/bond/fan.py | 2 +- homeassistant/components/comfoconnect/fan.py | 8 +- homeassistant/components/deconz/fan.py | 7 +- homeassistant/components/demo/fan.py | 15 +- homeassistant/components/esphome/fan.py | 3 +- homeassistant/components/fan/__init__.py | 126 +++++++- homeassistant/components/fan/services.yaml | 4 + homeassistant/components/fjaraskupan/fan.py | 8 +- homeassistant/components/freedompro/fan.py | 6 +- homeassistant/components/group/fan.py | 11 +- .../components/homekit_controller/fan.py | 3 +- homeassistant/components/insteon/fan.py | 7 +- homeassistant/components/intellifire/fan.py | 7 +- homeassistant/components/isy994/fan.py | 7 +- homeassistant/components/knx/fan.py | 7 +- homeassistant/components/lutron/fan.py | 7 +- homeassistant/components/lutron_caseta/fan.py | 7 +- homeassistant/components/matter/fan.py | 5 + homeassistant/components/modbus/fan.py | 14 +- homeassistant/components/modern_forms/fan.py | 8 +- homeassistant/components/mqtt/fan.py | 5 +- homeassistant/components/netatmo/fan.py | 1 + homeassistant/components/rabbitair/fan.py | 8 +- homeassistant/components/renson/fan.py | 7 +- homeassistant/components/smartthings/fan.py | 3 +- homeassistant/components/smarty/fan.py | 7 +- homeassistant/components/snooz/fan.py | 7 +- homeassistant/components/switch_as_x/fan.py | 9 +- homeassistant/components/tasmota/fan.py | 7 +- homeassistant/components/template/fan.py | 4 + homeassistant/components/tolo/fan.py | 4 +- homeassistant/components/tplink/fan.py | 7 +- homeassistant/components/tradfri/fan.py | 8 +- homeassistant/components/tuya/fan.py | 5 + homeassistant/components/vallox/fan.py | 8 +- homeassistant/components/vesync/fan.py | 8 +- homeassistant/components/wemo/fan.py | 7 +- homeassistant/components/wilight/fan.py | 8 +- homeassistant/components/xiaomi_miio/fan.py | 22 +- homeassistant/components/zha/fan.py | 7 +- homeassistant/components/zwave_js/fan.py | 21 +- homeassistant/components/zwave_me/fan.py | 7 +- .../components/balboa/snapshots/test_fan.ambr | 4 +- tests/components/fan/conftest.py | 23 ++ tests/components/fan/test_init.py | 296 +++++++++++++++++- .../snapshots/test_init.ambr | 44 +-- .../test_fan_that_changes_features.py | 53 +++- tests/components/mqtt/test_fan.py | 52 ++- tests/components/smartthings/test_fan.py | 26 +- tests/components/switch_as_x/test_fan.py | 2 +- tests/components/tasmota/test_fan.py | 7 +- .../components/tplink/snapshots/test_fan.ambr | 12 +- tests/components/tradfri/test_fan.py | 4 +- .../vesync/snapshots/test_diagnostics.ambr | 2 +- .../components/vesync/snapshots/test_fan.ambr | 16 +- tests/components/zwave_js/test_fan.py | 7 +- 58 files changed, 858 insertions(+), 132 deletions(-) create mode 100644 tests/components/fan/conftest.py diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index d8c800ea512..d0ba668373a 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -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 diff --git a/homeassistant/components/balboa/fan.py b/homeassistant/components/balboa/fan.py index 24fe3bdd71a..bf7425f0e64 100644 --- a/homeassistant/components/balboa/fan.py +++ b/homeassistant/components/balboa/fan.py @@ -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: diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 4bf091d7356..76a0daa46f9 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -69,7 +69,7 @@ class BondFan(BondEntity, FanEntity): 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(): diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index f0d261ab968..4e30b3ee3dc 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -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 diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index ee5456aab4e..6192421ecdc 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -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.""" diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 82b256cd75f..064ee3bb4f7 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -15,9 +15,15 @@ PRESET_MODE_SLEEP = "sleep" 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( @@ -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, @@ -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, diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 43ffd96c475..454c5edf030 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -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.""" @@ -148,7 +149,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): 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: diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index ef6c075a356..6ecc675a45e 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta from enum import IntFlag import functools as ft @@ -30,6 +31,7 @@ from homeassistant.helpers.deprecation import ( ) 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 ( @@ -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. @@ -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, { @@ -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 @@ -388,7 +493,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): 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 @@ -403,7 +508,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): 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 @@ -427,19 +532,6 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """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. diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 8bd329ac8fe..d2b467cd167 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -31,6 +31,8 @@ turn_on: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.TURN_ON fields: percentage: filter: @@ -53,6 +55,8 @@ turn_off: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.TURN_OFF oscillate: target: diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 67514eaa411..864160cb464 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -65,7 +65,13 @@ async def async_setup_entry( 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 diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index fe77398ece4..698d57d1001 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -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, @@ -62,8 +63,11 @@ class FreedomproFan(CoordinatorEntity[FreedomproDataUpdateCoordinator], FanEntit 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: diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index e09477430ef..93004e8a1b5 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -47,6 +47,8 @@ SUPPORTED_FLAGS = { FanEntityFeature.SET_SPEED, FanEntityFeature.DIRECTION, FanEntityFeature.OSCILLATE, + FanEntityFeature.TURN_OFF, + FanEntityFeature.TURN_ON, } DEFAULT_NAME = "Fan Group" @@ -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.""" @@ -200,11 +203,15 @@ class FanGroup(GroupEntity, FanEntity): 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] diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 79e302ace74..db01147494f 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -42,6 +42,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): # This must be set in subclasses to the name of a boolean characteristic # that controls whether the fan is on or off. on_characteristic: str + _enable_turn_on_off_backwards_compatibility = False @callback def _async_reconfigure(self) -> None: @@ -113,7 +114,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): @cached_property def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - features = FanEntityFeature(0) + features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON if self.service.has(CharacteristicsTypes.ROTATION_DIRECTION): features |= FanEntityFeature.DIRECTION diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index cbdae434df6..0a31e5915f6 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -50,8 +50,13 @@ async def async_setup_entry( class InsteonFanEntity(InsteonEntity, FanEntity): """An INSTEON fan entity.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _attr_speed_count = 3 + _enable_turn_on_off_backwards_compatibility = False @property def percentage(self) -> int | None: diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py index 387b6d059c6..f68827b0a56 100644 --- a/homeassistant/components/intellifire/fan.py +++ b/homeassistant/components/intellifire/fan.py @@ -75,7 +75,12 @@ class IntellifireFan(IntellifireEntity, FanEntity): """Fan entity for the fireplace.""" entity_description: IntellifireFanEntityDescription - _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 @property def is_on(self) -> bool: diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index da920540476..1d8af78f83c 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -48,7 +48,12 @@ async def async_setup_entry( class ISYFanEntity(ISYNodeEntity, FanEntity): """Representation of an ISY fan device.""" - _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 @property def percentage(self) -> int | None: diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index e94609bdc86..426a750f766 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -43,6 +43,7 @@ class KNXFan(KnxEntity, FanEntity): """Representation of a KNX fan.""" _device: XknxFan + _enable_turn_on_off_backwards_compatibility = False def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX fan.""" @@ -79,7 +80,11 @@ class KNXFan(KnxEntity, FanEntity): @property def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - flags = FanEntityFeature.SET_SPEED + flags = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) if self._device.supports_oscillation: flags |= FanEntityFeature.OSCILLATE diff --git a/homeassistant/components/lutron/fan.py b/homeassistant/components/lutron/fan.py index c350e70b222..dc881b393de 100644 --- a/homeassistant/components/lutron/fan.py +++ b/homeassistant/components/lutron/fan.py @@ -44,9 +44,14 @@ class LutronFan(LutronDevice, FanEntity): _attr_name = None _attr_should_poll = False _attr_speed_count = 3 - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _lutron_device: Output _prev_percentage: int | None = None + _enable_turn_on_off_backwards_compatibility = False def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index cd333ba22c4..1e7c0b2265c 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -40,8 +40,13 @@ async def async_setup_entry( class LutronCasetaFan(LutronCasetaDeviceUpdatableEntity, FanEntity): """Representation of a Lutron Caseta fan. Including Fan Speed.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) + _enable_turn_on_off_backwards_compatibility = False @property def percentage(self) -> int | None: diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 8cbd24977e3..a88c297d31a 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -57,6 +57,7 @@ class MatterFan(MatterEntity, FanEntity): """Representation of a Matter fan.""" _last_known_preset_mode: str | None = None + _enable_turn_on_off_backwards_compatibility = False async def async_turn_on( self, @@ -294,6 +295,10 @@ class MatterFan(MatterEntity, FanEntity): if feature_map & FanControlFeature.kAirflowDirection: self._attr_supported_features |= FanEntityFeature.DIRECTION + self._attr_supported_features |= ( + FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + ) + # Discovery schema(s) to map Matter Attributes to HA entities DISCOVERY_SCHEMAS = [ diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 93e3fdded1a..e8b9d3bdaa7 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.fan import FanEntity +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,6 +38,18 @@ async def async_setup_platform( class ModbusFan(BaseSwitch, FanEntity): """Class representing a Modbus fan.""" + _enable_turn_on_off_backwards_compatibility = False + + def __init__( + self, hass: HomeAssistant, hub: ModbusHub, config: dict[str, Any] + ) -> None: + """Initialize the fan.""" + super().__init__(hass, hub, config) + if self.command_on is not None and self._command_off is not None: + self._attr_supported_features |= ( + FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + ) + async def async_turn_on( self, percentage: int | None = None, diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 5f6b699fb47..c00549c327a 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -70,8 +70,14 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): SPEED_RANGE = (1, 6) # off is not included - _attr_supported_features = FanEntityFeature.DIRECTION | FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.DIRECTION + | FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _attr_translation_key = "fan" + _enable_turn_on_off_backwards_compatibility = False def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index dd777bd178e..1838ce20e4d 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -224,6 +224,7 @@ class MqttFan(MqttEntity, FanEntity): _optimistic_preset_mode: bool _payload: dict[str, Any] _speed_range: tuple[int, int] + _enable_turn_on_off_backwards_compatibility = False @staticmethod def config_schema() -> VolSchemaType: @@ -289,7 +290,9 @@ class MqttFan(MqttEntity, FanEntity): optimistic or self._topic[CONF_PRESET_MODE_STATE_TOPIC] is None ) - self._attr_supported_features = FanEntityFeature(0) + self._attr_supported_features = ( + FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + ) self._attr_supported_features |= ( self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None and FanEntityFeature.OSCILLATE diff --git a/homeassistant/components/netatmo/fan.py b/homeassistant/components/netatmo/fan.py index 71a8c548622..8610882a453 100644 --- a/homeassistant/components/netatmo/fan.py +++ b/homeassistant/components/netatmo/fan.py @@ -51,6 +51,7 @@ class NetatmoFan(NetatmoModuleEntity, FanEntity): _attr_configuration_url = CONF_URL_CONTROL _attr_name = None device: NaModules.Fan + _enable_turn_on_off_backwards_compatibility = False def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize of Netatmo fan.""" diff --git a/homeassistant/components/rabbitair/fan.py b/homeassistant/components/rabbitair/fan.py index ee4f136930d..ba1896cba2f 100644 --- a/homeassistant/components/rabbitair/fan.py +++ b/homeassistant/components/rabbitair/fan.py @@ -49,7 +49,13 @@ async def async_setup_entry( class RabbitAirFanEntity(RabbitAirBaseEntity, FanEntity): """Fan control functions of the Rabbit Air air purifier.""" - _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) + _enable_turn_on_off_backwards_compatibility = False def __init__( self, diff --git a/homeassistant/components/renson/fan.py b/homeassistant/components/renson/fan.py index 4680223c14e..44bea28ce3c 100644 --- a/homeassistant/components/renson/fan.py +++ b/homeassistant/components/renson/fan.py @@ -122,7 +122,12 @@ class RensonFan(RensonEntity, FanEntity): _attr_has_entity_name = True _attr_name = None _attr_translation_key = "fan" - _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 def __init__(self, api: RensonVentilation, coordinator: RensonCoordinator) -> None: """Initialize the Renson fan.""" diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 215986dfb0d..840c04c2a10 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -70,6 +70,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): """Define a SmartThings Fan.""" _attr_speed_count = int_states_in_range(SPEED_RANGE) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device): """Init the class.""" @@ -77,7 +78,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): self._attr_supported_features = self._determine_features() def _determine_features(self): - flags = FanEntityFeature(0) + flags = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON if self._device.get_capability(Capability.fan_speed): flags |= FanEntityFeature.SET_SPEED diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 6d46e040033..37f7c2e493f 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -46,7 +46,12 @@ class SmartyFan(FanEntity): _attr_icon = "mdi:air-conditioner" _attr_should_poll = False - _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 def __init__(self, name, smarty): """Initialize the entity.""" diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py index fd6e5e69556..8c721432709 100644 --- a/homeassistant/components/snooz/fan.py +++ b/homeassistant/components/snooz/fan.py @@ -75,10 +75,15 @@ class SnoozFan(FanEntity, RestoreEntity): _attr_has_entity_name = True _attr_name = None - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _attr_should_poll = False _is_on: bool | None = None _percentage: int | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, data: SnoozConfigurationData) -> None: """Initialize a Snooz fan entity.""" diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index fb795b4f54a..91d3a4d119a 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -4,7 +4,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN, FanEntity +from homeassistant.components.fan import ( + DOMAIN as FAN_DOMAIN, + FanEntity, + FanEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import HomeAssistant @@ -41,6 +45,9 @@ async def async_setup_entry( class FanSwitch(BaseToggleEntity, FanEntity): """Represents a Switch as a Fan.""" + _attr_supported_features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + _enable_turn_on_off_backwards_compatibility = False + @property def is_on(self) -> bool | None: """Return true if the entity is on. diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index cdb0fb8d2f6..340edff3b35 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -65,9 +65,14 @@ class TasmotaFan( ): """Representation of a Tasmota fan.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _fan_speed = tasmota_const.FAN_SPEED_MEDIUM _tasmota_entity: tasmota_fan.TasmotaFan + _enable_turn_on_off_backwards_compatibility = False def __init__(self, **kwds: Any) -> None: """Initialize the Tasmota fan.""" diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 106d3e4fd70..20a2159e378 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -124,6 +124,7 @@ class TemplateFan(TemplateEntity, FanEntity): """A template fan component.""" _attr_should_poll = False + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -195,6 +196,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._attr_supported_features |= FanEntityFeature.OSCILLATE if self._direction_template: self._attr_supported_features |= FanEntityFeature.DIRECTION + self._attr_supported_features |= ( + FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + ) self._attr_assumed_state = self._template is None diff --git a/homeassistant/components/tolo/fan.py b/homeassistant/components/tolo/fan.py index 7065290f2a8..034bdb0b6a6 100644 --- a/homeassistant/components/tolo/fan.py +++ b/homeassistant/components/tolo/fan.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.fan import FanEntity +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,6 +27,8 @@ class ToloFan(ToloSaunaCoordinatorEntity, FanEntity): """Sauna fan control.""" _attr_translation_key = "fan" + _attr_supported_features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py index 292240bca94..f90eadbc531 100644 --- a/homeassistant/components/tplink/fan.py +++ b/homeassistant/components/tplink/fan.py @@ -59,7 +59,12 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): """Representation of a fan for a TPLink Fan device.""" _attr_speed_count = int_states_in_range(SPEED_RANGE) - _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 def __init__( self, diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index 5c0f05004ba..6561fc166dc 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -55,7 +55,12 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): """The platform class required by Home Assistant.""" _attr_name = None - _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) _attr_preset_modes = [ATTR_AUTO] # These are the steps: # 0 = Off @@ -64,6 +69,7 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): # ... with step size 1 # 50 = Max _attr_speed_count = ATTR_MAX_FAN_STEPS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index d4c19f6b55a..01a7ccf5083 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -66,6 +66,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): _speeds: EnumTypeData | None = None _switch: DPCode | None = None _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -116,6 +117,10 @@ class TuyaFanEntity(TuyaEntity, FanEntity): ): self._direction = enum_type self._attr_supported_features |= FanEntityFeature.DIRECTION + if self._switch is not None: + self._attr_supported_features |= ( + FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF + ) def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index a5bdf0983ae..4fe2cfd45d4 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -77,7 +77,13 @@ class ValloxFanEntity(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_name = None - _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + _enable_turn_on_off_backwards_compatibility = False def __init__( self, diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 6272c033b4f..4dce2762eef 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -84,8 +84,14 @@ def _setup_entities(devices, async_add_entities): class VeSyncFanHA(VeSyncDevice, FanEntity): """Representation of a VeSync fan.""" - _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, fan) -> None: """Initialize the VeSync fan device.""" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index e1b9aaf2388..b7c9840bcdc 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -74,9 +74,14 @@ async def async_setup_entry( class WemoHumidifier(WemoBinaryStateEntity, FanEntity): """Representation of a WeMo humidifier.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) wemo: Humidifier _last_fan_on_mode: FanMode + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: DeviceCoordinator) -> None: """Initialize the WeMo switch.""" diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index 5c05575c4f8..71559658c35 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -57,7 +57,13 @@ class WiLightFan(WiLightDevice, FanEntity): _attr_name = None _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) - _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.DIRECTION + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> None: """Initialize the device.""" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 4e0e271b071..8e58cb07ec8 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -294,6 +294,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" @@ -479,6 +480,9 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): self._preset_modes = PRESET_MODES_AIRPURIFIER self._attr_supported_features = FanEntityFeature.PRESET_MODE self._speed_count = 1 + self._attr_supported_features |= ( + FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + ) self._state = self.coordinator.data.is_on self._state_attrs.update( @@ -609,7 +613,11 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C self._preset_modes = PRESET_MODES_AIRPURIFIER_3C - self._attr_supported_features = FanEntityFeature.PRESET_MODE + self._attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) self._state = self.coordinator.data.is_on self._mode = self.coordinator.data.mode.value @@ -663,7 +671,10 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): self._speed_count = 4 self._preset_modes = PRESET_MODES_AIRFRESH self._attr_supported_features = ( - FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) self._state = self.coordinator.data.is_on @@ -756,7 +767,10 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): self._device_features = FEATURE_FLAGS_AIRFRESH_A1 self._preset_modes = PRESET_MODES_AIRFRESH_A1 self._attr_supported_features = ( - FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) self._state = self.coordinator.data.is_on @@ -851,6 +865,8 @@ class XiaomiGenericFan(XiaomiGenericDevice): FanEntityFeature.SET_SPEED | FanEntityFeature.OSCILLATE | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) if self._model != MODEL_FAN_1C: self._attr_supported_features |= FanEntityFeature.DIRECTION diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index f9bd9535a07..e5c100f1dc6 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -43,8 +43,13 @@ async def async_setup_entry( class ZhaFan(FanEntity, ZHAEntity): """Representation of a ZHA fan.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) _attr_translation_key: str = "fan" + _enable_turn_on_off_backwards_compatibility = False @property def preset_mode(self) -> str | None: diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 925a48512d8..37d3fc57886 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -78,7 +78,12 @@ async def async_setup_entry( class ZwaveFan(ZWaveBaseEntity, FanEntity): """Representation of a Z-Wave fan.""" - _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 def __init__( self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo @@ -249,7 +254,11 @@ class ValueMappingZwaveFan(ZwaveFan): @property def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - flags = FanEntityFeature.SET_SPEED + flags = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) if self.has_fan_value_mapping and self.fan_value_mapping.presets: flags |= FanEntityFeature.PRESET_MODE @@ -382,7 +391,13 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): @property def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - return FanEntityFeature.PRESET_MODE + if not self._fan_off: + return FanEntityFeature.PRESET_MODE + return ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) @property def fan_state(self) -> str | None: diff --git a/homeassistant/components/zwave_me/fan.py b/homeassistant/components/zwave_me/fan.py index 25ccec9a0fb..b8a4b5e4ad2 100644 --- a/homeassistant/components/zwave_me/fan.py +++ b/homeassistant/components/zwave_me/fan.py @@ -44,7 +44,12 @@ async def async_setup_entry( class ZWaveMeFan(ZWaveMeEntity, FanEntity): """Representation of a ZWaveMe Fan.""" - _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 @property def percentage(self) -> int: diff --git a/tests/components/balboa/snapshots/test_fan.ambr b/tests/components/balboa/snapshots/test_fan.ambr index 2b87a961906..8d35ab6de7c 100644 --- a/tests/components/balboa/snapshots/test_fan.ambr +++ b/tests/components/balboa/snapshots/test_fan.ambr @@ -28,7 +28,7 @@ 'original_name': 'Pump 1', 'platform': 'balboa', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'pump', 'unique_id': 'FakeSpa-Pump 1-c0ffee', 'unit_of_measurement': None, @@ -42,7 +42,7 @@ 'percentage_step': 50.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.fakespa_pump_1', diff --git a/tests/components/fan/conftest.py b/tests/components/fan/conftest.py new file mode 100644 index 00000000000..2e3644793df --- /dev/null +++ b/tests/components/fan/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures for Fan platform tests.""" + +from collections.abc import Generator + +import pytest + +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant + +from tests.common import mock_config_flow, mock_platform + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 04f594b959c..a72ad5e48f6 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -1,5 +1,7 @@ """Tests for fan platforms.""" +from unittest.mock import patch + import pytest from homeassistant.components import fan @@ -12,15 +14,23 @@ from homeassistant.components.fan import ( FanEntityFeature, NotValidPresetModeError, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.entity_registry as er from homeassistant.setup import async_setup_component from .common import MockFan from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, help_test_all, import_and_test_deprecated_constant_enum, + mock_integration, + mock_platform, setup_test_component_platform, ) @@ -167,7 +177,10 @@ def test_deprecated_constants( enum: fan.FanEntityFeature, ) -> None: """Test deprecated constants.""" - import_and_test_deprecated_constant_enum(caplog, fan, enum, "SUPPORT_", "2025.1") + if not FanEntityFeature.TURN_OFF and not FanEntityFeature.TURN_ON: + import_and_test_deprecated_constant_enum( + caplog, fan, enum, "SUPPORT_", "2025.1" + ) def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: @@ -180,11 +193,288 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> return 1 entity = MockFan() - assert entity.supported_features_compat is FanEntityFeature(1) + assert entity.supported_features is FanEntityFeature(1) assert "MockFan" in caplog.text assert "is using deprecated supported features values" in caplog.text assert "Instead it should use" in caplog.text assert "FanEntityFeature.SET_SPEED" in caplog.text caplog.clear() - assert entity.supported_features_compat is FanEntityFeature(1) + assert entity.supported_features is FanEntityFeature(1) assert "is using deprecated supported features values" not in caplog.text + + +async def test_warning_not_implemented_turn_on_off_feature( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None +) -> None: + """Test adding feature flag and warn if missing when methods are set.""" + + called = [] + + class MockFanEntityTest(MockFan): + """Mock Fan device.""" + + def turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + ) -> None: + """Turn on.""" + called.append("turn_on") + + def turn_off(self) -> None: + """Turn off.""" + called.append("turn_off") + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_fan_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test fan platform via config entry.""" + async_add_entities([MockFanEntityTest(name="test", entity_id="fan.test")]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.fan", + MockPlatform(async_setup_entry=async_setup_entry_fan_platform), + ) + + with patch.object( + MockFanEntityTest, "__module__", "tests.custom_components.fan.test_init" + ): + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("fan.test") + assert state is not None + + assert ( + "Entity fan.test (.MockFanEntityTest'>) " + "does not set FanEntityFeature.TURN_OFF but implements the turn_off method. Please report it to the author of the 'test' custom integration" + in caplog.text + ) + assert ( + "Entity fan.test (.MockFanEntityTest'>) " + "does not set FanEntityFeature.TURN_ON but implements the turn_on method. Please report it to the author of the 'test' custom integration" + in caplog.text + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + { + "entity_id": "fan.test", + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + { + "entity_id": "fan.test", + }, + blocking=True, + ) + + assert len(called) == 2 + assert "turn_on" in called + assert "turn_off" in called + + +async def test_no_warning_implemented_turn_on_off_feature( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None +) -> None: + """Test no warning when feature flags are set.""" + + class MockFanEntityTest(MockFan): + """Mock Fan device.""" + + _attr_supported_features = ( + FanEntityFeature.DIRECTION + | FanEntityFeature.OSCILLATE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_fan_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test fan platform via config entry.""" + async_add_entities([MockFanEntityTest(name="test", entity_id="fan.test")]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.fan", + MockPlatform(async_setup_entry=async_setup_entry_fan_platform), + ) + + with patch.object( + MockFanEntityTest, "__module__", "tests.custom_components.fan.test_init" + ): + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("fan.test") + assert state is not None + + assert "does not set FanEntityFeature.TURN_OFF" not in caplog.text + assert "does not set FanEntityFeature.TURN_ON" not in caplog.text + + +async def test_no_warning_integration_has_migrated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None +) -> None: + """Test no warning when integration migrated using `_enable_turn_on_off_backwards_compatibility`.""" + + class MockFanEntityTest(MockFan): + """Mock Fan device.""" + + _enable_turn_on_off_backwards_compatibility = False + _attr_supported_features = ( + FanEntityFeature.DIRECTION + | FanEntityFeature.OSCILLATE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_fan_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test fan platform via config entry.""" + async_add_entities([MockFanEntityTest(name="test", entity_id="fan.test")]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.fan", + MockPlatform(async_setup_entry=async_setup_entry_fan_platform), + ) + + with patch.object( + MockFanEntityTest, "__module__", "tests.custom_components.fan.test_init" + ): + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("fan.test") + assert state is not None + + assert "does not set FanEntityFeature.TURN_OFF" not in caplog.text + assert "does not set FanEntityFeature.TURN_ON" not in caplog.text + + +async def test_no_warning_integration_implement_feature_flags( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None +) -> None: + """Test no warning when integration uses the correct feature flags.""" + + class MockFanEntityTest(MockFan): + """Mock Fan device.""" + + _attr_supported_features = ( + FanEntityFeature.DIRECTION + | FanEntityFeature.OSCILLATE + | FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_fan_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test fan platform via config entry.""" + async_add_entities([MockFanEntityTest(name="test", entity_id="fan.test")]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.fan", + MockPlatform(async_setup_entry=async_setup_entry_fan_platform), + ) + + with patch.object( + MockFanEntityTest, "__module__", "tests.custom_components.fan.test_init" + ): + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("fan.test") + assert state is not None + + assert "does not set FanEntityFeature.TURN_OFF" not in caplog.text + assert "does not set FanEntityFeature.TURN_ON" not in caplog.text diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 4f4f4fa364d..2e96295a0ab 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -103,7 +103,7 @@ 'original_name': 'Airversa AP2 1808 AirPurifier', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_32832', 'unit_of_measurement': None, @@ -115,7 +115,7 @@ 'percentage_step': 20.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.airversa_ap2_1808_airpurifier', 'state': 'off', @@ -6869,7 +6869,7 @@ 'original_name': 'HAA-C718B3', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', 'unit_of_measurement': None, @@ -6881,7 +6881,7 @@ 'percentage_step': 33.333333333333336, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.haa_c718b3', 'state': 'on', @@ -7837,7 +7837,7 @@ 'original_name': 'Ceiling Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_766313939_8', 'unit_of_measurement': None, @@ -7849,7 +7849,7 @@ 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.ceiling_fan', 'state': 'off', @@ -8034,7 +8034,7 @@ 'original_name': 'Living Room Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_8', 'unit_of_measurement': None, @@ -8047,7 +8047,7 @@ 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.living_room_fan', 'state': 'off', @@ -8223,7 +8223,7 @@ 'original_name': '89 Living Room', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_175', 'unit_of_measurement': None, @@ -8236,7 +8236,7 @@ 'percentage_step': 33.333333333333336, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.89_living_room', 'state': 'on', @@ -9219,7 +9219,7 @@ 'original_name': 'Ceiling Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_766313939_8', 'unit_of_measurement': None, @@ -9231,7 +9231,7 @@ 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.ceiling_fan', 'state': 'off', @@ -9416,7 +9416,7 @@ 'original_name': 'Living Room Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_8', 'unit_of_measurement': None, @@ -9430,7 +9430,7 @@ 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.living_room_fan', 'state': 'off', @@ -9619,7 +9619,7 @@ 'original_name': 'Living Room Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_8', 'unit_of_measurement': None, @@ -9633,7 +9633,7 @@ 'percentage_step': 1.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.living_room_fan', 'state': 'off', @@ -9818,7 +9818,7 @@ 'original_name': '89 Living Room', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_175', 'unit_of_measurement': None, @@ -9831,7 +9831,7 @@ 'percentage_step': 33.333333333333336, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.89_living_room', 'state': 'on', @@ -14233,7 +14233,7 @@ 'original_name': 'Caséta® Wireless Fan Speed Control', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_21474836482_2', 'unit_of_measurement': None, @@ -14245,7 +14245,7 @@ 'percentage_step': 25.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.caseta_r_wireless_fan_speed_control', 'state': 'off', @@ -17881,7 +17881,7 @@ 'original_name': 'SIMPLEconnect Fan-06F674 Hunter Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', 'unit_of_measurement': None, @@ -17894,7 +17894,7 @@ 'percentage_step': 25.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'entity_id': 'fan.simpleconnect_fan_06f674_hunter_fan', 'state': 'off', diff --git a/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py b/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py index aea53e74d46..d6dc0f70015 100644 --- a/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py +++ b/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py @@ -29,14 +29,22 @@ async def test_fan_add_feature_at_runtime( fan_state = hass.states.get("fan.living_room_fan") assert ( fan_state.attributes[ATTR_SUPPORTED_FEATURES] - is FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION + is FanEntityFeature.SET_SPEED + | FanEntityFeature.DIRECTION + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) fan = entity_registry.async_get("fan.ceiling_fan") assert fan.unique_id == "00:00:00:00:00:00_766313939_8" fan_state = hass.states.get("fan.ceiling_fan") - assert fan_state.attributes[ATTR_SUPPORTED_FEATURES] is FanEntityFeature.SET_SPEED + assert ( + fan_state.attributes[ATTR_SUPPORTED_FEATURES] + is FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) # Now change the config to add oscillation accessories = await setup_accessories_from_file( @@ -50,9 +58,16 @@ async def test_fan_add_feature_at_runtime( is FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION | FanEntityFeature.OSCILLATE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) fan_state = hass.states.get("fan.ceiling_fan") - assert fan_state.attributes[ATTR_SUPPORTED_FEATURES] is FanEntityFeature.SET_SPEED + assert ( + fan_state.attributes[ATTR_SUPPORTED_FEATURES] + is FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) async def test_fan_remove_feature_at_runtime( @@ -75,13 +90,20 @@ async def test_fan_remove_feature_at_runtime( is FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION | FanEntityFeature.OSCILLATE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) fan = entity_registry.async_get("fan.ceiling_fan") assert fan.unique_id == "00:00:00:00:00:00_766313939_8" fan_state = hass.states.get("fan.ceiling_fan") - assert fan_state.attributes[ATTR_SUPPORTED_FEATURES] is FanEntityFeature.SET_SPEED + assert ( + fan_state.attributes[ATTR_SUPPORTED_FEATURES] + is FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) # Now change the config to add oscillation accessories = await setup_accessories_from_file( @@ -92,10 +114,18 @@ async def test_fan_remove_feature_at_runtime( fan_state = hass.states.get("fan.living_room_fan") assert ( fan_state.attributes[ATTR_SUPPORTED_FEATURES] - is FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION + is FanEntityFeature.SET_SPEED + | FanEntityFeature.DIRECTION + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) fan_state = hass.states.get("fan.ceiling_fan") - assert fan_state.attributes[ATTR_SUPPORTED_FEATURES] is FanEntityFeature.SET_SPEED + assert ( + fan_state.attributes[ATTR_SUPPORTED_FEATURES] + is FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) async def test_bridge_with_two_fans_one_removed( @@ -119,13 +149,20 @@ async def test_bridge_with_two_fans_one_removed( is FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION | FanEntityFeature.OSCILLATE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) fan = entity_registry.async_get("fan.ceiling_fan") assert fan.unique_id == "00:00:00:00:00:00_766313939_8" fan_state = hass.states.get("fan.ceiling_fan") - assert fan_state.attributes[ATTR_SUPPORTED_FEATURES] is FanEntityFeature.SET_SPEED + assert ( + fan_state.attributes[ATTR_SUPPORTED_FEATURES] + is FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) # Now change the config to remove one of the fans accessories = await setup_accessories_from_file( @@ -141,6 +178,8 @@ async def test_bridge_with_two_fans_one_removed( is FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION | FanEntityFeature.OSCILLATE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) # The second fan should have been removed assert not hass.states.get("fan.ceiling_fan") diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 21c9f1f3e6a..1d0cc809fd6 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1590,7 +1590,7 @@ async def test_attributes( } }, True, - fan.FanEntityFeature(0), + fan.FanEntityFeature.TURN_OFF | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1605,7 +1605,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.OSCILLATE, + fan.FanEntityFeature.OSCILLATE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1620,7 +1622,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.SET_SPEED, + fan.FanEntityFeature.SET_SPEED + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1651,7 +1655,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.PRESET_MODE, + fan.FanEntityFeature.PRESET_MODE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1667,7 +1673,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.PRESET_MODE, + fan.FanEntityFeature.PRESET_MODE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1682,7 +1690,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.SET_SPEED, + fan.FanEntityFeature.SET_SPEED + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1698,7 +1708,10 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.OSCILLATE | fan.FanEntityFeature.SET_SPEED, + fan.FanEntityFeature.OSCILLATE + | fan.FanEntityFeature.SET_SPEED + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1714,7 +1727,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.PRESET_MODE, + fan.FanEntityFeature.PRESET_MODE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1730,7 +1745,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.PRESET_MODE, + fan.FanEntityFeature.PRESET_MODE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1747,7 +1764,10 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.PRESET_MODE | fan.FanEntityFeature.OSCILLATE, + fan.FanEntityFeature.PRESET_MODE + | fan.FanEntityFeature.OSCILLATE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1764,7 +1784,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.SET_SPEED, + fan.FanEntityFeature.SET_SPEED + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, None, ), ( @@ -1831,7 +1853,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.PRESET_MODE, + fan.FanEntityFeature.PRESET_MODE + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, "some error", ), ( @@ -1846,7 +1870,9 @@ async def test_attributes( } }, True, - fan.FanEntityFeature.DIRECTION, + fan.FanEntityFeature.DIRECTION + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON, "some error", ), ], diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 043c022b225..b78c453b402 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -39,7 +39,12 @@ async def test_entity_state(hass: HomeAssistant, device_factory) -> None: # Dimmer 1 state = hass.states.get("fan.fan_1") assert state.state == "on" - assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.SET_SPEED + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) assert state.attributes[ATTR_PERCENTAGE] == 66 @@ -100,7 +105,12 @@ async def test_setup_mode_capability(hass: HomeAssistant, device_factory) -> Non # Assert state = hass.states.get("fan.fan_1") assert state is not None - assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.PRESET_MODE + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) assert state.attributes[ATTR_PRESET_MODE] == "high" assert state.attributes[ATTR_PRESET_MODES] == ["high", "low", "medium"] @@ -122,7 +132,12 @@ async def test_setup_speed_capability(hass: HomeAssistant, device_factory) -> No # Assert state = hass.states.get("fan.fan_1") assert state is not None - assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.SET_SPEED + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) assert state.attributes[ATTR_PERCENTAGE] == 66 @@ -151,7 +166,10 @@ async def test_setup_both_capabilities(hass: HomeAssistant, device_factory) -> N assert state is not None assert ( state.attributes[ATTR_SUPPORTED_FEATURES] - == FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + == FanEntityFeature.SET_SPEED + | FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON ) assert state.attributes[ATTR_PERCENTAGE] == 66 assert state.attributes[ATTR_PRESET_MODE] == "high" diff --git a/tests/components/switch_as_x/test_fan.py b/tests/components/switch_as_x/test_fan.py index fd4296bd616..a33490dab45 100644 --- a/tests/components/switch_as_x/test_fan.py +++ b/tests/components/switch_as_x/test_fan.py @@ -44,7 +44,7 @@ async def test_default_state(hass: HomeAssistant) -> None: state = hass.states.get("fan.wind_machine") assert state is not None assert state.state == "unavailable" - assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_features"] == 48 async def test_service_calls(hass: HomeAssistant) -> None: diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 654b8c955d2..49d1d36ce20 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -61,7 +61,12 @@ async def test_controlling_state_via_mqtt( state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["percentage"] is None - assert state.attributes["supported_features"] == fan.FanEntityFeature.SET_SPEED + assert ( + state.attributes["supported_features"] + == fan.FanEntityFeature.SET_SPEED + | fan.FanEntityFeature.TURN_OFF + | fan.FanEntityFeature.TURN_ON + ) assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":1}') diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index bac691d1c08..1a7392dc63a 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -28,7 +28,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH', 'unit_of_measurement': None, @@ -42,7 +42,7 @@ 'percentage_step': 25.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.my_device', @@ -81,7 +81,7 @@ 'original_name': 'my_fan_0', 'platform': 'tplink', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH00', 'unit_of_measurement': None, @@ -95,7 +95,7 @@ 'percentage_step': 25.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.my_device_my_fan_0', @@ -134,7 +134,7 @@ 'original_name': 'my_fan_1', 'platform': 'tplink', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH01', 'unit_of_measurement': None, @@ -148,7 +148,7 @@ 'percentage_step': 25.0, 'preset_mode': None, 'preset_modes': None, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.my_device_my_fan_1', diff --git a/tests/components/tradfri/test_fan.py b/tests/components/tradfri/test_fan.py index 2abe03d629a..4f72e4709e9 100644 --- a/tests/components/tradfri/test_fan.py +++ b/tests/components/tradfri/test_fan.py @@ -52,7 +52,7 @@ async def test_fan_available( assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(2.040816) assert state.attributes[ATTR_PRESET_MODES] == ["Auto"] assert state.attributes[ATTR_PRESET_MODE] is None - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 9 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 57 await command_store.trigger_observe_callback( hass, device, {ATTR_REACHABLE_STATE: 0} @@ -172,7 +172,7 @@ async def test_services( assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(2.040816) assert state.attributes[ATTR_PRESET_MODES] == ["Auto"] assert state.attributes[ATTR_PRESET_MODE] is None - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 9 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 57 await hass.services.async_call( FAN_DOMAIN, diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index fcb2cc7b286..0db0a629e68 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -203,7 +203,7 @@ 'auto', 'sleep', ]), - 'supported_features': 9, + 'supported_features': 57, }), 'entity_id': 'fan.fan', 'last_changed': str, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 201ac7c041b..21985afd7bf 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -66,7 +66,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'air-purifier', 'unit_of_measurement': None, @@ -81,7 +81,7 @@ 'auto', 'sleep', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_131s', @@ -157,7 +157,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', 'unit_of_measurement': None, @@ -178,7 +178,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_200s', @@ -255,7 +255,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '400s-purifier', 'unit_of_measurement': None, @@ -277,7 +277,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_400s', @@ -354,7 +354,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '600s-purifier', 'unit_of_measurement': None, @@ -376,7 +376,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_600s', diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 03cd6bfb704..2551fc7b34a 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -653,7 +653,12 @@ async def test_thermostat_fan( assert state.state == STATE_ON assert state.attributes.get(ATTR_FAN_STATE) == "Idle / off" assert state.attributes.get(ATTR_PRESET_MODE) == "Auto low" - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == FanEntityFeature.PRESET_MODE + assert ( + state.attributes.get(ATTR_SUPPORTED_FEATURES) + == FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) # Test setting preset mode await hass.services.async_call(