Home Assistant Unofficial Reference 2024.12.1
fan.py
Go to the documentation of this file.
1 """Support for MQTT fans."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 import math
8 from typing import Any
9 
10 import voluptuous as vol
11 
12 from homeassistant.components import fan
13 from homeassistant.components.fan import (
14  ATTR_DIRECTION,
15  ATTR_OSCILLATING,
16  ATTR_PERCENTAGE,
17  ATTR_PRESET_MODE,
18  FanEntity,
19  FanEntityFeature,
20 )
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.const import (
23  CONF_NAME,
24  CONF_OPTIMISTIC,
25  CONF_PAYLOAD_OFF,
26  CONF_PAYLOAD_ON,
27  CONF_STATE,
28 )
29 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.helpers.entity_platform import AddEntitiesCallback
32 from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
33 from homeassistant.helpers.template import Template
34 from homeassistant.helpers.typing import ConfigType, VolSchemaType
36  percentage_to_ranged_value,
37  ranged_value_to_percentage,
38 )
39 from homeassistant.util.scaling import int_states_in_range
40 
41 from . import subscription
42 from .config import MQTT_RW_SCHEMA
43 from .const import (
44  CONF_COMMAND_TEMPLATE,
45  CONF_COMMAND_TOPIC,
46  CONF_STATE_TOPIC,
47  CONF_STATE_VALUE_TEMPLATE,
48  PAYLOAD_NONE,
49 )
50 from .entity import MqttEntity, async_setup_entity_entry_helper
51 from .models import (
52  MqttCommandTemplate,
53  MqttValueTemplate,
54  PublishPayloadType,
55  ReceiveMessage,
56 )
57 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
58 from .util import valid_publish_topic, valid_subscribe_topic
59 
60 PARALLEL_UPDATES = 0
61 
62 CONF_DIRECTION_STATE_TOPIC = "direction_state_topic"
63 CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic"
64 CONF_DIRECTION_VALUE_TEMPLATE = "direction_value_template"
65 CONF_DIRECTION_COMMAND_TEMPLATE = "direction_command_template"
66 CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
67 CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
68 CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"
69 CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"
70 CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"
71 CONF_SPEED_RANGE_MIN = "speed_range_min"
72 CONF_SPEED_RANGE_MAX = "speed_range_max"
73 CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"
74 CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"
75 CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"
76 CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"
77 CONF_PRESET_MODES_LIST = "preset_modes"
78 CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"
79 CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
80 CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
81 CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"
82 CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"
83 CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"
84 CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"
85 
86 DEFAULT_NAME = "MQTT Fan"
87 DEFAULT_PAYLOAD_ON = "ON"
88 DEFAULT_PAYLOAD_OFF = "OFF"
89 DEFAULT_PAYLOAD_RESET = "None"
90 DEFAULT_SPEED_RANGE_MIN = 1
91 DEFAULT_SPEED_RANGE_MAX = 100
92 
93 OSCILLATE_ON_PAYLOAD = "oscillate_on"
94 OSCILLATE_OFF_PAYLOAD = "oscillate_off"
95 
96 MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset(
97  {
98  fan.ATTR_DIRECTION,
99  fan.ATTR_OSCILLATING,
100  fan.ATTR_PERCENTAGE_STEP,
101  fan.ATTR_PERCENTAGE,
102  fan.ATTR_PRESET_MODE,
103  fan.ATTR_PRESET_MODES,
104  }
105 )
106 
107 _LOGGER = logging.getLogger(__name__)
108 
109 
110 def valid_speed_range_configuration(config: ConfigType) -> ConfigType:
111  """Validate that the fan speed_range configuration is valid, throws if it isn't."""
112  if config[CONF_SPEED_RANGE_MIN] == 0:
113  raise vol.Invalid("speed_range_min must be > 0")
114  if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]:
115  raise vol.Invalid("speed_range_max must be > speed_range_min")
116  return config
117 
118 
119 def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
120  """Validate that the preset mode reset payload is not one of the preset modes."""
121  if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]:
122  raise vol.Invalid("preset_modes must not contain payload_reset_preset_mode")
123  return config
124 
125 
126 _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
127  {
128  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
129  vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
130  vol.Optional(CONF_DIRECTION_COMMAND_TOPIC): valid_publish_topic,
131  vol.Optional(CONF_DIRECTION_COMMAND_TEMPLATE): cv.template,
132  vol.Optional(CONF_DIRECTION_STATE_TOPIC): valid_subscribe_topic,
133  vol.Optional(CONF_DIRECTION_VALUE_TEMPLATE): cv.template,
134  vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): valid_publish_topic,
135  vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template,
136  vol.Optional(CONF_OSCILLATION_STATE_TOPIC): valid_subscribe_topic,
137  vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template,
138  vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): valid_publish_topic,
139  vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template,
140  vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): valid_subscribe_topic,
141  vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template,
142  # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST
143  # must be used together
144  vol.Inclusive(
145  CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes"
146  ): valid_publish_topic,
147  vol.Inclusive(
148  CONF_PRESET_MODES_LIST, "preset_modes", default=[]
149  ): cv.ensure_list,
150  vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template,
151  vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic,
152  vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template,
153  vol.Optional(
154  CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN
155  ): cv.positive_int,
156  vol.Optional(
157  CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX
158  ): cv.positive_int,
159  vol.Optional(
160  CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET
161  ): cv.string,
162  vol.Optional(
163  CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET
164  ): cv.string,
165  vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
166  vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
167  vol.Optional(
168  CONF_PAYLOAD_OSCILLATION_OFF, default=OSCILLATE_OFF_PAYLOAD
169  ): cv.string,
170  vol.Optional(
171  CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD
172  ): cv.string,
173  vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
174  }
175 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
176 
177 PLATFORM_SCHEMA_MODERN = vol.All(
178  _PLATFORM_SCHEMA_BASE,
179  valid_speed_range_configuration,
180  valid_preset_mode_configuration,
181 )
182 
183 DISCOVERY_SCHEMA = vol.All(
184  _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
185  valid_speed_range_configuration,
186  valid_preset_mode_configuration,
187 )
188 
189 
191  hass: HomeAssistant,
192  config_entry: ConfigEntry,
193  async_add_entities: AddEntitiesCallback,
194 ) -> None:
195  """Set up MQTT fan through YAML and through MQTT discovery."""
197  hass,
198  config_entry,
199  MqttFan,
200  fan.DOMAIN,
201  async_add_entities,
202  DISCOVERY_SCHEMA,
203  PLATFORM_SCHEMA_MODERN,
204  )
205 
206 
208  """A MQTT fan component."""
209 
210  _attr_percentage: int | None = None
211  _attr_preset_mode: str | None = None
212 
213  _default_name = DEFAULT_NAME
214  _entity_id_format = fan.ENTITY_ID_FORMAT
215  _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED
216 
217  _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]]
218  _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]]
219  _feature_percentage: bool
220  _feature_preset_mode: bool
221  _topic: dict[str, Any]
222  _optimistic: bool
223  _optimistic_direction: bool
224  _optimistic_oscillation: bool
225  _optimistic_percentage: bool
226  _optimistic_preset_mode: bool
227  _payload: dict[str, Any]
228  _speed_range: tuple[int, int]
229  _enable_turn_on_off_backwards_compatibility = False
230 
231  @staticmethod
232  def config_schema() -> VolSchemaType:
233  """Return the config schema."""
234  return DISCOVERY_SCHEMA
235 
236  def _setup_from_config(self, config: ConfigType) -> None:
237  """(Re)Setup the entity."""
238  self._speed_range_speed_range = (
239  config[CONF_SPEED_RANGE_MIN],
240  config[CONF_SPEED_RANGE_MAX],
241  )
242  self._topic_topic = {
243  key: config.get(key)
244  for key in (
245  CONF_STATE_TOPIC,
246  CONF_COMMAND_TOPIC,
247  CONF_DIRECTION_STATE_TOPIC,
248  CONF_DIRECTION_COMMAND_TOPIC,
249  CONF_PERCENTAGE_STATE_TOPIC,
250  CONF_PERCENTAGE_COMMAND_TOPIC,
251  CONF_PRESET_MODE_STATE_TOPIC,
252  CONF_PRESET_MODE_COMMAND_TOPIC,
253  CONF_OSCILLATION_STATE_TOPIC,
254  CONF_OSCILLATION_COMMAND_TOPIC,
255  )
256  }
257  self._payload_payload = {
258  "STATE_ON": config[CONF_PAYLOAD_ON],
259  "STATE_OFF": config[CONF_PAYLOAD_OFF],
260  "OSCILLATE_ON_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_ON],
261  "OSCILLATE_OFF_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_OFF],
262  "PERCENTAGE_RESET": config[CONF_PAYLOAD_RESET_PERCENTAGE],
263  "PRESET_MODE_RESET": config[CONF_PAYLOAD_RESET_PRESET_MODE],
264  }
265 
266  self._feature_percentage_feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config
267  self._feature_preset_mode_feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config
268  if self._feature_preset_mode_feature_preset_mode:
269  self._attr_preset_modes_attr_preset_modes = config[CONF_PRESET_MODES_LIST]
270  else:
271  self._attr_preset_modes_attr_preset_modes = []
272 
273  self._attr_speed_count_attr_speed_count = (
274  min(int_states_in_range(self._speed_range_speed_range), 100)
275  if self._feature_percentage_feature_percentage
276  else 100
277  )
278 
279  optimistic = config[CONF_OPTIMISTIC]
280  self._optimistic_optimistic = optimistic or self._topic_topic[CONF_STATE_TOPIC] is None
281  self._attr_assumed_state_attr_assumed_state = bool(self._optimistic_optimistic)
282  self._optimistic_direction_optimistic_direction = (
283  optimistic or self._topic_topic[CONF_DIRECTION_STATE_TOPIC] is None
284  )
285  self._optimistic_oscillation_optimistic_oscillation = (
286  optimistic or self._topic_topic[CONF_OSCILLATION_STATE_TOPIC] is None
287  )
288  self._optimistic_percentage_optimistic_percentage = (
289  optimistic or self._topic_topic[CONF_PERCENTAGE_STATE_TOPIC] is None
290  )
291  self._optimistic_preset_mode_optimistic_preset_mode = (
292  optimistic or self._topic_topic[CONF_PRESET_MODE_STATE_TOPIC] is None
293  )
294 
295  self._attr_supported_features_attr_supported_features = (
296  FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
297  )
298  self._attr_supported_features_attr_supported_features |= (
299  self._topic_topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None
300  and FanEntityFeature.OSCILLATE
301  )
302  self._attr_supported_features_attr_supported_features |= (
303  self._topic_topic[CONF_DIRECTION_COMMAND_TOPIC] is not None
304  and FanEntityFeature.DIRECTION
305  )
306  if self._feature_percentage_feature_percentage:
307  self._attr_supported_features_attr_supported_features |= FanEntityFeature.SET_SPEED
308  if self._feature_preset_mode_feature_preset_mode:
309  self._attr_supported_features_attr_supported_features |= FanEntityFeature.PRESET_MODE
310 
311  command_templates: dict[str, Template | None] = {
312  CONF_STATE: config.get(CONF_COMMAND_TEMPLATE),
313  ATTR_DIRECTION: config.get(CONF_DIRECTION_COMMAND_TEMPLATE),
314  ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE),
315  ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE),
316  ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE),
317  }
318  self._command_templates_command_templates = {
319  key: MqttCommandTemplate(tpl, entity=self).async_render
320  for key, tpl in command_templates.items()
321  }
322 
323  value_templates: dict[str, Template | None] = {
324  CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
325  ATTR_DIRECTION: config.get(CONF_DIRECTION_VALUE_TEMPLATE),
326  ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE),
327  ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE),
328  ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE),
329  }
330  self._value_templates_value_templates = {
331  key: MqttValueTemplate(
332  tpl, entity=self
333  ).async_render_with_possible_json_value
334  for key, tpl in value_templates.items()
335  }
336 
337  @callback
338  def _state_received(self, msg: ReceiveMessage) -> None:
339  """Handle new received MQTT message."""
340  payload = self._value_templates_value_templates[CONF_STATE](msg.payload)
341  if not payload:
342  _LOGGER.debug("Ignoring empty state from '%s'", msg.topic)
343  return
344  if payload == self._payload_payload["STATE_ON"]:
345  self._attr_is_on_attr_is_on = True
346  elif payload == self._payload_payload["STATE_OFF"]:
347  self._attr_is_on_attr_is_on = False
348  elif payload == PAYLOAD_NONE:
349  self._attr_is_on_attr_is_on = None
350 
351  @callback
352  def _percentage_received(self, msg: ReceiveMessage) -> None:
353  """Handle new received MQTT message for the percentage."""
354  rendered_percentage_payload = self._value_templates_value_templates[ATTR_PERCENTAGE](
355  msg.payload
356  )
357  if not rendered_percentage_payload:
358  _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic)
359  return
360  if rendered_percentage_payload == self._payload_payload["PERCENTAGE_RESET"]:
361  self._attr_percentage_attr_percentage = None
362  return
363  try:
364  percentage = ranged_value_to_percentage(
365  self._speed_range_speed_range, int(rendered_percentage_payload)
366  )
367  except ValueError:
368  _LOGGER.warning(
369  (
370  "'%s' received on topic %s. '%s' is not a valid speed within"
371  " the speed range"
372  ),
373  msg.payload,
374  msg.topic,
375  rendered_percentage_payload,
376  )
377  return
378  if percentage < 0 or percentage > 100:
379  _LOGGER.warning(
380  (
381  "'%s' received on topic %s. '%s' is not a valid speed within"
382  " the speed range"
383  ),
384  msg.payload,
385  msg.topic,
386  rendered_percentage_payload,
387  )
388  return
389  self._attr_percentage_attr_percentage = percentage
390 
391  @callback
392  def _preset_mode_received(self, msg: ReceiveMessage) -> None:
393  """Handle new received MQTT message for preset mode."""
394  preset_mode = str(self._value_templates_value_templates[ATTR_PRESET_MODE](msg.payload))
395  if preset_mode == self._payload_payload["PRESET_MODE_RESET"]:
396  self._attr_preset_mode_attr_preset_mode = None
397  return
398  if not preset_mode:
399  _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic)
400  return
401  if not self.preset_modespreset_modes or preset_mode not in self.preset_modespreset_modes:
402  _LOGGER.warning(
403  "'%s' received on topic %s. '%s' is not a valid preset mode",
404  msg.payload,
405  msg.topic,
406  preset_mode,
407  )
408  return
409 
410  self._attr_preset_mode_attr_preset_mode = preset_mode
411 
412  @callback
413  def _oscillation_received(self, msg: ReceiveMessage) -> None:
414  """Handle new received MQTT message for the oscillation."""
415  payload = self._value_templates_value_templates[ATTR_OSCILLATING](msg.payload)
416  if not payload:
417  _LOGGER.debug("Ignoring empty oscillation from '%s'", msg.topic)
418  return
419  if payload == self._payload_payload["OSCILLATE_ON_PAYLOAD"]:
420  self._attr_oscillating_attr_oscillating = True
421  elif payload == self._payload_payload["OSCILLATE_OFF_PAYLOAD"]:
422  self._attr_oscillating_attr_oscillating = False
423 
424  @callback
425  def _direction_received(self, msg: ReceiveMessage) -> None:
426  """Handle new received MQTT message for the direction."""
427  direction = self._value_templates_value_templates[ATTR_DIRECTION](msg.payload)
428  if not direction:
429  _LOGGER.debug("Ignoring empty direction from '%s'", msg.topic)
430  return
431  self._attr_current_direction_attr_current_direction = str(direction)
432 
433  @callback
434  def _prepare_subscribe_topics(self) -> None:
435  """(Re)Subscribe to topics."""
436  self.add_subscriptionadd_subscription(CONF_STATE_TOPIC, self._state_received_state_received, {"_attr_is_on"})
437  self.add_subscriptionadd_subscription(
438  CONF_PERCENTAGE_STATE_TOPIC, self._percentage_received_percentage_received, {"_attr_percentage"}
439  )
440  self.add_subscriptionadd_subscription(
441  CONF_PRESET_MODE_STATE_TOPIC,
442  self._preset_mode_received_preset_mode_received,
443  {"_attr_preset_mode"},
444  )
445  if self.add_subscriptionadd_subscription(
446  CONF_OSCILLATION_STATE_TOPIC,
447  self._oscillation_received_oscillation_received,
448  {"_attr_oscillating"},
449  ):
450  self._attr_oscillating_attr_oscillating = False
451  self.add_subscriptionadd_subscription(
452  CONF_DIRECTION_STATE_TOPIC,
453  self._direction_received_direction_received,
454  {"_attr_current_direction"},
455  )
456 
457  async def _subscribe_topics(self) -> None:
458  """(Re)Subscribe to topics."""
459  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
460 
461  @property
462  def is_on(self) -> bool | None:
463  """Return true if device is on."""
464  # The default for FanEntity is to compute it based on percentage
465  return self._attr_is_on_attr_is_on
466 
467  async def async_turn_on(
468  self,
469  percentage: int | None = None,
470  preset_mode: str | None = None,
471  **kwargs: Any,
472  ) -> None:
473  """Turn on the entity.
474 
475  This method is a coroutine.
476  """
477  mqtt_payload = self._command_templates_command_templates[CONF_STATE](self._payload_payload["STATE_ON"])
478  await self.async_publish_with_configasync_publish_with_config(
479  self._config_config[CONF_COMMAND_TOPIC], mqtt_payload
480  )
481  if percentage:
482  await self.async_set_percentageasync_set_percentageasync_set_percentage(percentage)
483  if preset_mode:
484  await self.async_set_preset_modeasync_set_preset_modeasync_set_preset_mode(preset_mode)
485  if self._optimistic_optimistic:
486  self._attr_is_on_attr_is_on = True
487  self.async_write_ha_stateasync_write_ha_state()
488 
489  async def async_turn_off(self, **kwargs: Any) -> None:
490  """Turn off the entity.
491 
492  This method is a coroutine.
493  """
494  mqtt_payload = self._command_templates_command_templates[CONF_STATE](self._payload_payload["STATE_OFF"])
495  await self.async_publish_with_configasync_publish_with_config(
496  self._config_config[CONF_COMMAND_TOPIC], mqtt_payload
497  )
498  if self._optimistic_optimistic:
499  self._attr_is_on_attr_is_on = False
500  self.async_write_ha_stateasync_write_ha_state()
501 
502  async def async_set_percentage(self, percentage: int) -> None:
503  """Set the percentage of the fan.
504 
505  This method is a coroutine.
506  """
507  percentage_payload = math.ceil(
508  percentage_to_ranged_value(self._speed_range_speed_range, percentage)
509  )
510  mqtt_payload = self._command_templates_command_templates[ATTR_PERCENTAGE](percentage_payload)
511  await self.async_publish_with_configasync_publish_with_config(
512  self._config_config[CONF_PERCENTAGE_COMMAND_TOPIC], mqtt_payload
513  )
514  if self._optimistic_percentage_optimistic_percentage:
515  self._attr_percentage_attr_percentage = percentage
516  self.async_write_ha_stateasync_write_ha_state()
517 
518  async def async_set_preset_mode(self, preset_mode: str) -> None:
519  """Set the preset mode of the fan.
520 
521  This method is a coroutine.
522  """
523  mqtt_payload = self._command_templates_command_templates[ATTR_PRESET_MODE](preset_mode)
524  await self.async_publish_with_configasync_publish_with_config(
525  self._config_config[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload
526  )
527  if self._optimistic_preset_mode_optimistic_preset_mode:
528  self._attr_preset_mode_attr_preset_mode = preset_mode
529  self.async_write_ha_stateasync_write_ha_state()
530 
531  async def async_oscillate(self, oscillating: bool) -> None:
532  """Set oscillation.
533 
534  This method is a coroutine.
535  """
536  if oscillating:
537  mqtt_payload = self._command_templates_command_templates[ATTR_OSCILLATING](
538  self._payload_payload["OSCILLATE_ON_PAYLOAD"]
539  )
540  else:
541  mqtt_payload = self._command_templates_command_templates[ATTR_OSCILLATING](
542  self._payload_payload["OSCILLATE_OFF_PAYLOAD"]
543  )
544  await self.async_publish_with_configasync_publish_with_config(
545  self._config_config[CONF_OSCILLATION_COMMAND_TOPIC], mqtt_payload
546  )
547  if self._optimistic_oscillation_optimistic_oscillation:
548  self._attr_oscillating_attr_oscillating = oscillating
549  self.async_write_ha_stateasync_write_ha_state()
550 
551  async def async_set_direction(self, direction: str) -> None:
552  """Set direction.
553 
554  This method is a coroutine.
555  """
556  mqtt_payload = self._command_templates_command_templates[ATTR_DIRECTION](direction)
557  await self.async_publish_with_configasync_publish_with_config(
558  self._config_config[CONF_DIRECTION_COMMAND_TOPIC], mqtt_payload
559  )
560  if self._optimistic_direction_optimistic_direction:
561  self._attr_current_direction_attr_current_direction = direction
562  self.async_write_ha_stateasync_write_ha_state()
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
None async_publish_with_config(self, str topic, PublishPayloadType payload)
Definition: entity.py:1377
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
None async_turn_off(self, **Any kwargs)
Definition: fan.py:489
None async_oscillate(self, bool oscillating)
Definition: fan.py:531
None _setup_from_config(self, ConfigType config)
Definition: fan.py:236
None async_set_direction(self, str direction)
Definition: fan.py:551
None async_set_preset_mode(self, str preset_mode)
Definition: fan.py:518
None _oscillation_received(self, ReceiveMessage msg)
Definition: fan.py:413
None async_turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
Definition: fan.py:472
None _percentage_received(self, ReceiveMessage msg)
Definition: fan.py:352
None _direction_received(self, ReceiveMessage msg)
Definition: fan.py:425
None _preset_mode_received(self, ReceiveMessage msg)
Definition: fan.py:392
None _state_received(self, ReceiveMessage msg)
Definition: fan.py:338
None async_set_percentage(self, int percentage)
Definition: fan.py:502
None async_setup_entity_entry_helper(HomeAssistant hass, ConfigEntry entry, type[MqttEntity]|None entity_class, str domain, AddEntitiesCallback async_add_entities, VolSchemaType discovery_schema, VolSchemaType platform_schema_modern, dict[str, type[MqttEntity]]|None schema_class_mapping=None)
Definition: entity.py:245
ConfigType valid_speed_range_configuration(ConfigType config)
Definition: fan.py:110
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: fan.py:194
ConfigType valid_preset_mode_configuration(ConfigType config)
Definition: fan.py:119
float percentage_to_ranged_value(tuple[float, float] low_high_range, float percentage)
Definition: percentage.py:81
int ranged_value_to_percentage(tuple[float, float] low_high_range, float value)
Definition: percentage.py:64
int int_states_in_range(tuple[float, float] low_high_range)
Definition: scaling.py:61