1 """Provides functionality to interact with fans."""
3 from __future__
import annotations
6 from datetime
import timedelta
7 from enum
import IntFlag
11 from typing
import Any, final
13 from propcache
import cached_property
14 import voluptuous
as vol
27 DeprecatedConstantEnum,
28 all_with_deprecated_constants,
29 check_if_deprecated_constant,
30 dir_with_deprecated_constants,
39 percentage_to_ranged_value,
40 ranged_value_to_percentage,
43 _LOGGER = logging.getLogger(__name__)
46 DATA_COMPONENT: HassKey[EntityComponent[FanEntity]] =
HassKey(DOMAIN)
47 ENTITY_ID_FORMAT = DOMAIN +
".{}"
48 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
49 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
54 """Supported features of the fan entity."""
67 FanEntityFeature.SET_SPEED,
"2025.1"
70 FanEntityFeature.OSCILLATE,
"2025.1"
73 FanEntityFeature.DIRECTION,
"2025.1"
76 FanEntityFeature.PRESET_MODE,
"2025.1"
79 SERVICE_INCREASE_SPEED =
"increase_speed"
80 SERVICE_DECREASE_SPEED =
"decrease_speed"
81 SERVICE_OSCILLATE =
"oscillate"
82 SERVICE_SET_DIRECTION =
"set_direction"
83 SERVICE_SET_PERCENTAGE =
"set_percentage"
84 SERVICE_SET_PRESET_MODE =
"set_preset_mode"
86 DIRECTION_FORWARD =
"forward"
87 DIRECTION_REVERSE =
"reverse"
89 ATTR_PERCENTAGE =
"percentage"
90 ATTR_PERCENTAGE_STEP =
"percentage_step"
91 ATTR_OSCILLATING =
"oscillating"
92 ATTR_DIRECTION =
"direction"
93 ATTR_PRESET_MODE =
"preset_mode"
94 ATTR_PRESET_MODES =
"preset_modes"
100 """Raised when the preset_mode is not in the preset_modes list."""
103 self, *args: object, translation_placeholders: dict[str, str] |
None =
None
105 """Initialize the exception."""
108 translation_domain=DOMAIN,
109 translation_key=
"not_valid_preset_mode",
110 translation_placeholders=translation_placeholders,
115 def is_on(hass: HomeAssistant, entity_id: str) -> bool:
116 """Return if the fans are on based on the statemachine."""
117 entity = hass.states.get(entity_id)
119 return entity.state == STATE_ON
122 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
123 """Expose fan control via statemachine and services."""
124 component = hass.data[DATA_COMPONENT] = EntityComponent[FanEntity](
125 _LOGGER, DOMAIN, hass, SCAN_INTERVAL
128 await component.async_setup(config)
132 component.async_register_entity_service(
135 vol.Optional(ATTR_PERCENTAGE): vol.All(
136 vol.Coerce(int), vol.Range(min=0, max=100)
138 vol.Optional(ATTR_PRESET_MODE): cv.string,
140 "async_handle_turn_on_service",
141 [FanEntityFeature.TURN_ON],
143 component.async_register_entity_service(
144 SERVICE_TURN_OFF,
None,
"async_turn_off", [FanEntityFeature.TURN_OFF]
146 component.async_register_entity_service(
150 [FanEntityFeature.TURN_OFF, FanEntityFeature.TURN_ON],
152 component.async_register_entity_service(
153 SERVICE_INCREASE_SPEED,
155 vol.Optional(ATTR_PERCENTAGE_STEP): vol.All(
156 vol.Coerce(int), vol.Range(min=0, max=100)
159 "async_increase_speed",
160 [FanEntityFeature.SET_SPEED],
162 component.async_register_entity_service(
163 SERVICE_DECREASE_SPEED,
165 vol.Optional(ATTR_PERCENTAGE_STEP): vol.All(
166 vol.Coerce(int), vol.Range(min=0, max=100)
169 "async_decrease_speed",
170 [FanEntityFeature.SET_SPEED],
172 component.async_register_entity_service(
174 {vol.Required(ATTR_OSCILLATING): cv.boolean},
176 [FanEntityFeature.OSCILLATE],
178 component.async_register_entity_service(
179 SERVICE_SET_DIRECTION,
180 {vol.Optional(ATTR_DIRECTION): cv.string},
181 "async_set_direction",
182 [FanEntityFeature.DIRECTION],
184 component.async_register_entity_service(
185 SERVICE_SET_PERCENTAGE,
187 vol.Required(ATTR_PERCENTAGE): vol.All(
188 vol.Coerce(int), vol.Range(min=0, max=100)
191 "async_set_percentage",
192 [FanEntityFeature.SET_SPEED],
194 component.async_register_entity_service(
195 SERVICE_SET_PRESET_MODE,
196 {vol.Required(ATTR_PRESET_MODE): cv.string},
197 "async_handle_set_preset_mode_service",
198 [FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
205 """Set up a config entry."""
210 """Unload a config entry."""
215 """A class that describes fan entities."""
218 CACHED_PROPERTIES_WITH_ATTR_ = {
223 "supported_features",
230 """Base class for fan entities."""
232 _entity_component_unrecorded_attributes = frozenset({ATTR_PRESET_MODES})
234 entity_description: FanEntityDescription
235 _attr_current_direction: str |
None =
None
236 _attr_oscillating: bool |
None =
None
237 _attr_percentage: int |
None = 0
238 _attr_preset_mode: str |
None =
None
239 _attr_preset_modes: list[str] |
None =
None
240 _attr_speed_count: int = 100
246 _enable_turn_on_off_backwards_compatibility: bool =
True
251 Modify return of `supported_features` to
252 include `_mod_supported_features` if attribute is set.
254 if name !=
"supported_features":
263 "_FanEntity__mod_supported_features"
265 if type(_supported_features)
is int:
269 _features = _supported_features
271 if not _mod_supported_features:
276 return _features | _mod_supported_features
282 platform: EntityPlatform,
283 parallel_updates: asyncio.Semaphore |
None,
285 """Start adding an entity to a platform."""
288 def _report_turn_on_off(feature: str, method: str) ->
None:
289 """Log warning not implemented turn on/off feature."""
292 "Entity %s (%s) does not set FanEntityFeature.%s"
293 " but implements the %s method. Please %s"
306 if self._enable_turn_on_off_backwards_compatibility
is False:
311 if supported_features & (FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF):
316 if not supported_features & FanEntityFeature.TURN_OFF
and (
317 type(self).async_turn_off
is not ToggleEntity.async_turn_off
318 or type(self).turn_off
is not ToggleEntity.turn_off
321 _report_turn_on_off(
"TURN_OFF",
"turn_off")
322 self.__mod_supported_features |= (
323 FanEntityFeature.TURN_OFF
326 if not supported_features & FanEntityFeature.TURN_ON
and (
327 type(self).async_turn_on
is not FanEntity.async_turn_on
328 or type(self).turn_on
is not FanEntity.turn_on
331 _report_turn_on_off(
"TURN_ON",
"turn_on")
332 self.__mod_supported_features |= (
333 FanEntityFeature.TURN_ON
337 """Set the speed of the fan, as a percentage."""
338 raise NotImplementedError
341 """Set the speed of the fan, as a percentage."""
344 await self.
hasshass.async_add_executor_job(self.
set_percentageset_percentage, percentage)
347 """Increase the speed of the fan."""
351 """Decrease the speed of the fan."""
355 self, modifier: int, percentage_step: int |
None
357 """Increase or decrease the speed of the fan."""
358 current_percentage = self.
percentagepercentage
or 0
360 if percentage_step
is not None:
361 new_percentage = current_percentage + (percentage_step * modifier)
364 speed_index = math.ceil(
368 speed_range, speed_index + modifier
371 new_percentage =
max(0,
min(100, new_percentage))
376 """Set new preset mode."""
377 raise NotImplementedError
381 """Validate and set new preset mode."""
386 """Set new preset mode."""
387 await self.
hasshass.async_add_executor_job(self.
set_preset_modeset_preset_mode, preset_mode)
392 """Raise NotValidPresetModeError on invalid preset_mode."""
394 if not preset_modes
or preset_mode
not in preset_modes:
395 preset_modes_str: str =
", ".join(preset_modes
or [])
397 translation_placeholders={
398 "preset_mode": preset_mode,
399 "preset_modes": preset_modes_str,
404 """Set the direction of the fan."""
405 raise NotImplementedError
408 """Set the direction of the fan."""
409 await self.
hasshass.async_add_executor_job(self.
set_directionset_direction, direction)
413 percentage: int |
None =
None,
414 preset_mode: str |
None =
None,
417 """Turn on the fan."""
418 raise NotImplementedError
423 percentage: int |
None =
None,
424 preset_mode: str |
None =
None,
427 """Validate and turn on the fan."""
428 if preset_mode
is not None:
434 percentage: int |
None =
None,
435 preset_mode: str |
None =
None,
438 """Turn on the fan."""
439 await self.
hasshass.async_add_executor_job(
442 percentage=percentage,
443 preset_mode=preset_mode,
449 """Oscillate the fan."""
450 raise NotImplementedError
453 """Oscillate the fan."""
454 await self.
hasshass.async_add_executor_job(self.
oscillateoscillate, oscillating)
458 """Return true if the entity is on."""
465 """Return the current speed as a percentage."""
466 return self._attr_percentage
470 """Return the number of speeds the fan supports."""
471 return self._attr_speed_count
475 """Return the step size for percentage."""
480 """Return the current direction of the fan."""
481 return self._attr_current_direction
485 """Return whether or not the fan is currently oscillating."""
486 return self._attr_oscillating
490 """Return capability attributes."""
495 FanEntityFeature.SET_SPEED
in supported_features
496 or FanEntityFeature.PRESET_MODE
in supported_features
498 attrs[ATTR_PRESET_MODES] = self.
preset_modespreset_modes
505 """Return optional state attributes."""
506 data: dict[str, float | str |
None] = {}
509 if FanEntityFeature.DIRECTION
in supported_features:
512 if FanEntityFeature.OSCILLATE
in supported_features:
513 data[ATTR_OSCILLATING] = self.
oscillatingoscillating
515 has_set_speed = FanEntityFeature.SET_SPEED
in supported_features
518 data[ATTR_PERCENTAGE] = self.
percentagepercentage
521 if has_set_speed
or FanEntityFeature.PRESET_MODE
in supported_features:
522 data[ATTR_PRESET_MODE] = self.
preset_modepreset_mode
528 """Flag supported features."""
529 return self._attr_supported_features
533 """Return the current preset mode, e.g., auto, smart, interval, favorite.
535 Requires FanEntityFeature.SET_SPEED.
537 return self._attr_preset_mode
541 """Return a list of available preset modes.
543 Requires FanEntityFeature.SET_SPEED.
545 return self._attr_preset_modes
549 __getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
550 __dir__ = ft.partial(
551 dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
None _async_adjust_speed(self, int modifier, int|None percentage_step)
None oscillate(self, bool oscillating)
Any __getattribute__(self, str name)
None set_preset_mode(self, str preset_mode)
None async_decrease_speed(self, int|None percentage_step=None)
None set_percentage(self, int percentage)
int|None percentage(self)
None async_handle_set_preset_mode_service(self, str preset_mode)
list[str]|None preset_modes(self)
str|None preset_mode(self)
None async_set_direction(self, str direction)
bool|None oscillating(self)
str|None current_direction(self)
None set_direction(self, str direction)
None turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
None async_turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
float percentage_step(self)
None _valid_preset_mode_or_raise(self, str preset_mode)
None async_set_percentage(self, int percentage)
None add_to_platform_start(self, HomeAssistant hass, EntityPlatform platform, asyncio.Semaphore|None parallel_updates)
None async_set_preset_mode(self, str preset_mode)
None async_oscillate(self, bool oscillating)
dict[str, float|str|None] state_attributes(self)
None async_handle_turn_on_service(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
FanEntityFeature supported_features(self)
dict[str, list[str]|None] capability_attributes(self)
None async_increase_speed(self, int|None percentage_step=None)
None __init__(self, *object args, dict[str, str]|None translation_placeholders=None)
None _report_deprecated_supported_features_values(self, IntFlag replacement)
str _suggest_report_issue(self)
int|None supported_features(self)
None async_turn_off(self, **Any kwargs)
None async_turn_on(self, **Any kwargs)
None turn_on(self, **Any kwargs)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool is_on(HomeAssistant hass, str entity_id)
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
float percentage_to_ranged_value(tuple[float, float] low_high_range, float percentage)
int ranged_value_to_percentage(tuple[float, float] low_high_range, float value)