Home Assistant Unofficial Reference 2024.12.1
fan.py
Go to the documentation of this file.
1 """Support for Template fans."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 import voluptuous as vol
9 
10 from homeassistant.components.fan import (
11  ATTR_DIRECTION,
12  ATTR_OSCILLATING,
13  ATTR_PERCENTAGE,
14  ATTR_PRESET_MODE,
15  DIRECTION_FORWARD,
16  DIRECTION_REVERSE,
17  ENTITY_ID_FORMAT,
18  FanEntity,
19  FanEntityFeature,
20 )
21 from homeassistant.const import (
22  CONF_ENTITY_ID,
23  CONF_FRIENDLY_NAME,
24  CONF_UNIQUE_ID,
25  CONF_VALUE_TEMPLATE,
26  STATE_ON,
27  STATE_UNAVAILABLE,
28  STATE_UNKNOWN,
29 )
30 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.exceptions import TemplateError
33 from homeassistant.helpers.entity import async_generate_entity_id
34 from homeassistant.helpers.entity_platform import AddEntitiesCallback
35 from homeassistant.helpers.script import Script
36 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
37 
38 from .const import DOMAIN
39 from .template_entity import (
40  TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
41  TemplateEntity,
42  rewrite_common_legacy_to_modern_conf,
43 )
44 
45 _LOGGER = logging.getLogger(__name__)
46 
47 CONF_FANS = "fans"
48 CONF_SPEED_COUNT = "speed_count"
49 CONF_PRESET_MODES = "preset_modes"
50 CONF_PERCENTAGE_TEMPLATE = "percentage_template"
51 CONF_PRESET_MODE_TEMPLATE = "preset_mode_template"
52 CONF_OSCILLATING_TEMPLATE = "oscillating_template"
53 CONF_DIRECTION_TEMPLATE = "direction_template"
54 CONF_ON_ACTION = "turn_on"
55 CONF_OFF_ACTION = "turn_off"
56 CONF_SET_PERCENTAGE_ACTION = "set_percentage"
57 CONF_SET_OSCILLATING_ACTION = "set_oscillating"
58 CONF_SET_DIRECTION_ACTION = "set_direction"
59 CONF_SET_PRESET_MODE_ACTION = "set_preset_mode"
60 
61 _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE]
62 
63 FAN_SCHEMA = vol.All(
64  cv.deprecated(CONF_ENTITY_ID),
65  vol.Schema(
66  {
67  vol.Optional(CONF_FRIENDLY_NAME): cv.string,
68  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
69  vol.Optional(CONF_PERCENTAGE_TEMPLATE): cv.template,
70  vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template,
71  vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template,
72  vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template,
73  vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
74  vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
75  vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA,
76  vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
77  vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
78  vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
79  vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
80  vol.Optional(CONF_PRESET_MODES): cv.ensure_list,
81  vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
82  vol.Optional(CONF_UNIQUE_ID): cv.string,
83  }
84  ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema),
85 )
86 
87 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
88  {vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_SCHEMA)}
89 )
90 
91 
92 async def _async_create_entities(hass, config):
93  """Create the Template Fans."""
94  fans = []
95 
96  for object_id, entity_config in config[CONF_FANS].items():
97  entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config)
98 
99  unique_id = entity_config.get(CONF_UNIQUE_ID)
100 
101  fans.append(
102  TemplateFan(
103  hass,
104  object_id,
105  entity_config,
106  unique_id,
107  )
108  )
109 
110  return fans
111 
112 
114  hass: HomeAssistant,
115  config: ConfigType,
116  async_add_entities: AddEntitiesCallback,
117  discovery_info: DiscoveryInfoType | None = None,
118 ) -> None:
119  """Set up the template fans."""
120  async_add_entities(await _async_create_entities(hass, config))
121 
122 
124  """A template fan component."""
125 
126  _attr_should_poll = False
127  _enable_turn_on_off_backwards_compatibility = False
128 
129  def __init__(
130  self,
131  hass,
132  object_id,
133  config,
134  unique_id,
135  ):
136  """Initialize the fan."""
137  super().__init__(
138  hass, config=config, fallback_name=object_id, unique_id=unique_id
139  )
140  self.hasshasshass = hass
142  ENTITY_ID_FORMAT, object_id, hass=hass
143  )
144  friendly_name = self._attr_name_attr_name
145 
146  self._template_template = config.get(CONF_VALUE_TEMPLATE)
147  self._percentage_template_percentage_template = config.get(CONF_PERCENTAGE_TEMPLATE)
148  self._preset_mode_template_preset_mode_template = config.get(CONF_PRESET_MODE_TEMPLATE)
149  self._oscillating_template_oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE)
150  self._direction_template_direction_template = config.get(CONF_DIRECTION_TEMPLATE)
151 
152  self._on_script_on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN)
153  self._off_script_off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN)
154 
155  self._set_percentage_script_set_percentage_script = None
156  if set_percentage_action := config.get(CONF_SET_PERCENTAGE_ACTION):
157  self._set_percentage_script_set_percentage_script = Script(
158  hass, set_percentage_action, friendly_name, DOMAIN
159  )
160 
161  self._set_preset_mode_script_set_preset_mode_script = None
162  if set_preset_mode_action := config.get(CONF_SET_PRESET_MODE_ACTION):
163  self._set_preset_mode_script_set_preset_mode_script = Script(
164  hass, set_preset_mode_action, friendly_name, DOMAIN
165  )
166 
167  self._set_oscillating_script_set_oscillating_script = None
168  if set_oscillating_action := config.get(CONF_SET_OSCILLATING_ACTION):
169  self._set_oscillating_script_set_oscillating_script = Script(
170  hass, set_oscillating_action, friendly_name, DOMAIN
171  )
172 
173  self._set_direction_script_set_direction_script = None
174  if set_direction_action := config.get(CONF_SET_DIRECTION_ACTION):
175  self._set_direction_script_set_direction_script = Script(
176  hass, set_direction_action, friendly_name, DOMAIN
177  )
178 
179  self._state_state: bool | None = False
180  self._percentage_percentage = None
181  self._preset_mode_preset_mode = None
182  self._oscillating_oscillating = None
183  self._direction_direction = None
184 
185  # Number of valid speeds
186  self._speed_count_speed_count = config.get(CONF_SPEED_COUNT)
187 
188  # List of valid preset modes
189  self._preset_modes_preset_modes = config.get(CONF_PRESET_MODES)
190 
191  if self._percentage_template_percentage_template:
192  self._attr_supported_features |= FanEntityFeature.SET_SPEED
193  if self._preset_mode_template_preset_mode_template and self._preset_modes_preset_modes:
194  self._attr_supported_features |= FanEntityFeature.PRESET_MODE
195  if self._oscillating_template_oscillating_template:
196  self._attr_supported_features |= FanEntityFeature.OSCILLATE
197  if self._direction_template_direction_template:
198  self._attr_supported_features |= FanEntityFeature.DIRECTION
199  self._attr_supported_features |= (
200  FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
201  )
202 
203  self._attr_assumed_state_attr_assumed_state = self._template_template is None
204 
205  @property
206  def speed_count(self) -> int:
207  """Return the number of speeds the fan supports."""
208  return self._speed_count_speed_count or 100
209 
210  @property
211  def preset_modes(self) -> list[str]:
212  """Get the list of available preset modes."""
213  return self._preset_modes_preset_modes
214 
215  @property
216  def is_on(self) -> bool | None:
217  """Return true if device is on."""
218  return self._state_state
219 
220  @property
221  def preset_mode(self) -> str | None:
222  """Return the current preset mode."""
223  return self._preset_mode_preset_mode
224 
225  @property
226  def percentage(self) -> int | None:
227  """Return the current speed percentage."""
228  return self._percentage_percentage
229 
230  @property
231  def oscillating(self) -> bool | None:
232  """Return the oscillation state."""
233  return self._oscillating_oscillating
234 
235  @property
236  def current_direction(self) -> str | None:
237  """Return the oscillation state."""
238  return self._direction_direction
239 
240  async def async_turn_on(
241  self,
242  percentage: int | None = None,
243  preset_mode: str | None = None,
244  **kwargs: Any,
245  ) -> None:
246  """Turn on the fan."""
247  await self.async_run_scriptasync_run_script(
248  self._on_script_on_script,
249  run_variables={
250  ATTR_PERCENTAGE: percentage,
251  ATTR_PRESET_MODE: preset_mode,
252  },
253  context=self._context_context,
254  )
255 
256  if preset_mode is not None:
257  await self.async_set_preset_modeasync_set_preset_modeasync_set_preset_mode(preset_mode)
258  elif percentage is not None:
259  await self.async_set_percentageasync_set_percentageasync_set_percentage(percentage)
260 
261  if self._template_template is None:
262  self._state_state = True
263  self.async_write_ha_stateasync_write_ha_state()
264 
265  async def async_turn_off(self, **kwargs: Any) -> None:
266  """Turn off the fan."""
267  await self.async_run_scriptasync_run_script(self._off_script_off_script, context=self._context_context)
268 
269  if self._template_template is None:
270  self._state_state = False
271  self.async_write_ha_stateasync_write_ha_state()
272 
273  async def async_set_percentage(self, percentage: int) -> None:
274  """Set the percentage speed of the fan."""
275  self._percentage_percentage = percentage
276 
277  if self._set_percentage_script_set_percentage_script:
278  await self.async_run_scriptasync_run_script(
279  self._set_percentage_script_set_percentage_script,
280  run_variables={ATTR_PERCENTAGE: self._percentage_percentage},
281  context=self._context_context,
282  )
283 
284  if self._template_template is None:
285  self._state_state = percentage != 0
286  self.async_write_ha_stateasync_write_ha_state()
287 
288  async def async_set_preset_mode(self, preset_mode: str) -> None:
289  """Set the preset_mode of the fan."""
290  self._preset_mode_preset_mode = preset_mode
291 
292  if self._set_preset_mode_script_set_preset_mode_script:
293  await self.async_run_scriptasync_run_script(
294  self._set_preset_mode_script_set_preset_mode_script,
295  run_variables={ATTR_PRESET_MODE: self._preset_mode_preset_mode},
296  context=self._context_context,
297  )
298 
299  if self._template_template is None:
300  self._state_state = True
301  self.async_write_ha_stateasync_write_ha_state()
302 
303  async def async_oscillate(self, oscillating: bool) -> None:
304  """Set oscillation of the fan."""
305  if self._set_oscillating_script_set_oscillating_script is None:
306  return
307 
308  self._oscillating_oscillating = oscillating
309  await self.async_run_scriptasync_run_script(
310  self._set_oscillating_script_set_oscillating_script,
311  run_variables={ATTR_OSCILLATING: self.oscillatingoscillatingoscillating},
312  context=self._context_context,
313  )
314 
315  async def async_set_direction(self, direction: str) -> None:
316  """Set the direction of the fan."""
317  if self._set_direction_script_set_direction_script is None:
318  return
319 
320  if direction in _VALID_DIRECTIONS:
321  self._direction_direction = direction
322  await self.async_run_scriptasync_run_script(
323  self._set_direction_script_set_direction_script,
324  run_variables={ATTR_DIRECTION: direction},
325  context=self._context_context,
326  )
327  else:
328  _LOGGER.error(
329  "Received invalid direction: %s for entity %s. Expected: %s",
330  direction,
331  self.entity_identity_identity_identity_id,
332  ", ".join(_VALID_DIRECTIONS),
333  )
334 
335  @callback
336  def _update_state(self, result):
337  super()._update_state(result)
338  if isinstance(result, TemplateError):
339  self._state_state = None
340  return
341 
342  if isinstance(result, bool):
343  self._state_state = result
344  return
345 
346  if isinstance(result, str):
347  self._state_state = result.lower() in ("true", STATE_ON)
348  return
349 
350  self._state_state = False
351 
352  @callback
353  def _async_setup_templates(self) -> None:
354  """Set up templates."""
355  if self._template_template:
356  self.add_template_attributeadd_template_attribute(
357  "_state", self._template_template, None, self._update_state_update_state_update_state
358  )
359 
360  if self._preset_mode_template_preset_mode_template is not None:
361  self.add_template_attributeadd_template_attribute(
362  "_preset_mode",
363  self._preset_mode_template_preset_mode_template,
364  None,
365  self._update_preset_mode_update_preset_mode,
366  none_on_template_error=True,
367  )
368  if self._percentage_template_percentage_template is not None:
369  self.add_template_attributeadd_template_attribute(
370  "_percentage",
371  self._percentage_template_percentage_template,
372  None,
373  self._update_percentage_update_percentage,
374  none_on_template_error=True,
375  )
376  if self._oscillating_template_oscillating_template is not None:
377  self.add_template_attributeadd_template_attribute(
378  "_oscillating",
379  self._oscillating_template_oscillating_template,
380  None,
381  self._update_oscillating_update_oscillating,
382  none_on_template_error=True,
383  )
384  if self._direction_template_direction_template is not None:
385  self.add_template_attributeadd_template_attribute(
386  "_direction",
387  self._direction_template_direction_template,
388  None,
389  self._update_direction_update_direction,
390  none_on_template_error=True,
391  )
392  super()._async_setup_templates()
393 
394  @callback
395  def _update_percentage(self, percentage):
396  # Validate percentage
397  try:
398  percentage = int(float(percentage))
399  except (ValueError, TypeError):
400  _LOGGER.error(
401  "Received invalid percentage: %s for entity %s",
402  percentage,
403  self.entity_identity_identity_identity_id,
404  )
405  self._percentage_percentage = 0
406  return
407 
408  if 0 <= percentage <= 100:
409  self._percentage_percentage = percentage
410  else:
411  _LOGGER.error(
412  "Received invalid percentage: %s for entity %s",
413  percentage,
414  self.entity_identity_identity_identity_id,
415  )
416  self._percentage_percentage = 0
417 
418  @callback
419  def _update_preset_mode(self, preset_mode):
420  # Validate preset mode
421  preset_mode = str(preset_mode)
422 
423  if self.preset_modespreset_modespreset_modes and preset_mode in self.preset_modespreset_modespreset_modes:
424  self._preset_mode_preset_mode = preset_mode
425  elif preset_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN):
426  self._preset_mode_preset_mode = None
427  else:
428  _LOGGER.error(
429  "Received invalid preset_mode: %s for entity %s. Expected: %s",
430  preset_mode,
431  self.entity_identity_identity_identity_id,
432  self.preset_modepreset_modepreset_mode,
433  )
434  self._preset_mode_preset_mode = None
435 
436  @callback
437  def _update_oscillating(self, oscillating):
438  # Validate osc
439  if oscillating == "True" or oscillating is True:
440  self._oscillating_oscillating = True
441  elif oscillating == "False" or oscillating is False:
442  self._oscillating_oscillating = False
443  elif oscillating in (STATE_UNAVAILABLE, STATE_UNKNOWN):
444  self._oscillating_oscillating = None
445  else:
446  _LOGGER.error(
447  "Received invalid oscillating: %s for entity %s. Expected: True/False",
448  oscillating,
449  self.entity_identity_identity_identity_id,
450  )
451  self._oscillating_oscillating = None
452 
453  @callback
454  def _update_direction(self, direction):
455  # Validate direction
456  if direction in _VALID_DIRECTIONS:
457  self._direction_direction = direction
458  elif direction in (STATE_UNAVAILABLE, STATE_UNKNOWN):
459  self._direction_direction = None
460  else:
461  _LOGGER.error(
462  "Received invalid direction: %s for entity %s. Expected: %s",
463  direction,
464  self.entity_identity_identity_identity_id,
465  ", ".join(_VALID_DIRECTIONS),
466  )
467  self._direction_direction = None
list[str]|None preset_modes(self)
Definition: __init__.py:540
None async_set_percentage(self, int percentage)
Definition: __init__.py:340
None async_set_preset_mode(self, str preset_mode)
Definition: __init__.py:385
def _update_preset_mode(self, preset_mode)
Definition: fan.py:419
None async_turn_off(self, **Any kwargs)
Definition: fan.py:265
None async_set_percentage(self, int percentage)
Definition: fan.py:273
def __init__(self, hass, object_id, config, unique_id)
Definition: fan.py:135
None async_oscillate(self, bool oscillating)
Definition: fan.py:303
def _update_oscillating(self, oscillating)
Definition: fan.py:437
None async_set_direction(self, str direction)
Definition: fan.py:315
def _update_percentage(self, percentage)
Definition: fan.py:395
None async_turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
Definition: fan.py:245
None async_set_preset_mode(self, str preset_mode)
Definition: fan.py:288
None async_run_script(self, Script script, *_VarsType|None run_variables=None, Context|None context=None)
None add_template_attribute(self, str attribute, Template template, Callable[[Any], Any]|None validator=None, Callable[[Any], None]|None on_update=None, bool none_on_template_error=False)
def _async_create_entities(hass, config)
Definition: fan.py:92
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: fan.py:118
dict[str, Any] rewrite_common_legacy_to_modern_conf(HomeAssistant hass, dict[str, Any] entity_cfg, dict[str, str]|None extra_legacy_fields=None)
str async_generate_entity_id(str entity_id_format, str|None name, Iterable[str]|None current_ids=None, HomeAssistant|None hass=None)
Definition: entity.py:119