Home Assistant Unofficial Reference 2024.12.1
fan.py
Go to the documentation of this file.
1 """Support for Tuya Fan."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from tuya_sharing import CustomerDevice, Manager
8 
10  DIRECTION_FORWARD,
11  DIRECTION_REVERSE,
12  FanEntity,
13  FanEntityFeature,
14 )
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.helpers.dispatcher import async_dispatcher_connect
17 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19  ordered_list_item_to_percentage,
20  percentage_to_ordered_list_item,
21 )
22 
23 from . import TuyaConfigEntry
24 from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
25 from .entity import EnumTypeData, IntegerTypeData, TuyaEntity
26 
27 TUYA_SUPPORT_TYPE = {
28  "fs", # Fan
29  "fsd", # Fan with Light
30  "fskg", # Fan wall switch
31  "kj", # Air Purifier
32  "cs", # Dehumidifier
33 }
34 
35 
37  hass: HomeAssistant, entry: TuyaConfigEntry, async_add_entities: AddEntitiesCallback
38 ) -> None:
39  """Set up tuya fan dynamically through tuya discovery."""
40  hass_data = entry.runtime_data
41 
42  @callback
43  def async_discover_device(device_ids: list[str]) -> None:
44  """Discover and add a discovered tuya fan."""
45  entities: list[TuyaFanEntity] = []
46  for device_id in device_ids:
47  device = hass_data.manager.device_map[device_id]
48  if device and device.category in TUYA_SUPPORT_TYPE:
49  entities.append(TuyaFanEntity(device, hass_data.manager))
50  async_add_entities(entities)
51 
52  async_discover_device([*hass_data.manager.device_map])
53 
54  entry.async_on_unload(
55  async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
56  )
57 
58 
60  """Tuya Fan Device."""
61 
62  _direction: EnumTypeData | None = None
63  _oscillate: DPCode | None = None
64  _presets: EnumTypeData | None = None
65  _speed: IntegerTypeData | None = None
66  _speeds: EnumTypeData | None = None
67  _switch: DPCode | None = None
68  _attr_name = None
69  _enable_turn_on_off_backwards_compatibility = False
70 
71  def __init__(
72  self,
73  device: CustomerDevice,
74  device_manager: Manager,
75  ) -> None:
76  """Init Tuya Fan Device."""
77  super().__init__(device, device_manager)
78 
80  (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH), prefer_function=True
81  )
82 
83  self._attr_preset_modes_attr_preset_modes = []
84  if enum_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
85  (DPCode.FAN_MODE, DPCode.MODE), dptype=DPType.ENUM, prefer_function=True
86  ):
87  self._presets_presets = enum_type
88  self._attr_supported_features |= FanEntityFeature.PRESET_MODE
89  self._attr_preset_modes_attr_preset_modes = enum_type.range
90 
91  # Find speed controls, can be either percentage or a set of speeds
92  dpcodes = (
93  DPCode.FAN_SPEED_PERCENT,
94  DPCode.FAN_SPEED,
95  DPCode.SPEED,
96  DPCode.FAN_SPEED_ENUM,
97  )
98  if int_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
99  dpcodes, dptype=DPType.INTEGER, prefer_function=True
100  ):
101  self._attr_supported_features |= FanEntityFeature.SET_SPEED
102  self._speed_speed = int_type
103  elif enum_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
104  dpcodes, dptype=DPType.ENUM, prefer_function=True
105  ):
106  self._attr_supported_features |= FanEntityFeature.SET_SPEED
107  self._speeds_speeds = enum_type
108 
109  if dpcode := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
110  (DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL), prefer_function=True
111  ):
112  self._oscillate_oscillate = dpcode
113  self._attr_supported_features |= FanEntityFeature.OSCILLATE
114 
115  if enum_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
116  DPCode.FAN_DIRECTION, dptype=DPType.ENUM, prefer_function=True
117  ):
118  self._direction_direction = enum_type
119  self._attr_supported_features |= FanEntityFeature.DIRECTION
120  if self._switch_switch is not None:
121  self._attr_supported_features |= (
122  FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
123  )
124 
125  def set_preset_mode(self, preset_mode: str) -> None:
126  """Set the preset mode of the fan."""
127  if self._presets_presets is None:
128  return
129  self._send_command_send_command([{"code": self._presets_presets.dpcode, "value": preset_mode}])
130 
131  def set_direction(self, direction: str) -> None:
132  """Set the direction of the fan."""
133  if self._direction_direction is None:
134  return
135  self._send_command_send_command([{"code": self._direction_direction.dpcode, "value": direction}])
136 
137  def set_percentage(self, percentage: int) -> None:
138  """Set the speed of the fan, as a percentage."""
139  if self._speed_speed is not None:
140  self._send_command_send_command(
141  [
142  {
143  "code": self._speed_speed.dpcode,
144  "value": int(self._speed_speed.remap_value_from(percentage, 1, 100)),
145  }
146  ]
147  )
148  return
149 
150  if self._speeds_speeds is not None:
151  self._send_command_send_command(
152  [
153  {
154  "code": self._speeds_speeds.dpcode,
155  "value": percentage_to_ordered_list_item(
156  self._speeds_speeds.range, percentage
157  ),
158  }
159  ]
160  )
161 
162  def turn_off(self, **kwargs: Any) -> None:
163  """Turn the fan off."""
164  self._send_command_send_command([{"code": self._switch_switch, "value": False}])
165 
166  def turn_on(
167  self,
168  percentage: int | None = None,
169  preset_mode: str | None = None,
170  **kwargs: Any,
171  ) -> None:
172  """Turn on the fan."""
173  if self._switch_switch is None:
174  return
175 
176  commands: list[dict[str, str | bool | int]] = [
177  {"code": self._switch_switch, "value": True}
178  ]
179 
180  if percentage is not None and self._speed_speed is not None:
181  commands.append(
182  {
183  "code": self._speed_speed.dpcode,
184  "value": int(self._speed_speed.remap_value_from(percentage, 1, 100)),
185  }
186  )
187 
188  if percentage is not None and self._speeds_speeds is not None:
189  commands.append(
190  {
191  "code": self._speeds_speeds.dpcode,
192  "value": percentage_to_ordered_list_item(
193  self._speeds_speeds.range, percentage
194  ),
195  }
196  )
197 
198  if preset_mode is not None and self._presets_presets is not None:
199  commands.append({"code": self._presets_presets.dpcode, "value": preset_mode})
200 
201  self._send_command_send_command(commands)
202 
203  def oscillate(self, oscillating: bool) -> None:
204  """Oscillate the fan."""
205  if self._oscillate_oscillate is None:
206  return
207  self._send_command_send_command([{"code": self._oscillate_oscillate, "value": oscillating}])
208 
209  @property
210  def is_on(self) -> bool | None:
211  """Return true if fan is on."""
212  if self._switch_switch is None:
213  return None
214  return self.devicedevice.status.get(self._switch_switch)
215 
216  @property
217  def current_direction(self) -> str | None:
218  """Return the current direction of the fan."""
219  if (
220  self._direction_direction is None
221  or (value := self.devicedevice.status.get(self._direction_direction.dpcode)) is None
222  ):
223  return None
224 
225  if value.lower() == DIRECTION_FORWARD:
226  return DIRECTION_FORWARD
227 
228  if value.lower() == DIRECTION_REVERSE:
229  return DIRECTION_REVERSE
230 
231  return None
232 
233  @property
234  def oscillating(self) -> bool | None:
235  """Return true if the fan is oscillating."""
236  if self._oscillate_oscillate is None:
237  return None
238  return self.devicedevice.status.get(self._oscillate_oscillate)
239 
240  @property
241  def preset_mode(self) -> str | None:
242  """Return the current preset_mode."""
243  if self._presets_presets is None:
244  return None
245  return self.devicedevice.status.get(self._presets_presets.dpcode)
246 
247  @property
248  def percentage(self) -> int | None:
249  """Return the current speed."""
250  if self._speed_speed is not None:
251  if (value := self.devicedevice.status.get(self._speed_speed.dpcode)) is None:
252  return None
253  return int(self._speed_speed.remap_value_to(value, 1, 100))
254 
255  if self._speeds_speeds is not None:
256  if (value := self.devicedevice.status.get(self._speeds_speeds.dpcode)) is None:
257  return None
258  return ordered_list_item_to_percentage(self._speeds_speeds.range, value)
259 
260  return None
261 
262  @property
263  def speed_count(self) -> int:
264  """Return the number of speeds the fan supports."""
265  if self._speeds_speeds is not None:
266  return len(self._speeds_speeds.range)
267  return 100
None _send_command(self, list[dict[str, Any]] commands)
Definition: entity.py:295
DPCode|EnumTypeData|IntegerTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, DPType|None dptype=None)
Definition: entity.py:206
IntegerTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, Literal[DPType.INTEGER] dptype)
Definition: entity.py:190
DPCode|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False)
Definition: entity.py:198
EnumTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, Literal[DPType.ENUM] dptype)
Definition: entity.py:181
None set_preset_mode(self, str preset_mode)
Definition: fan.py:125
None oscillate(self, bool oscillating)
Definition: fan.py:203
None turn_off(self, **Any kwargs)
Definition: fan.py:162
None __init__(self, CustomerDevice device, Manager device_manager)
Definition: fan.py:75
None set_percentage(self, int percentage)
Definition: fan.py:137
None set_direction(self, str direction)
Definition: fan.py:131
None turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
Definition: fan.py:171
ElkSystem|None async_discover_device(HomeAssistant hass, str host)
Definition: discovery.py:78
None async_setup_entry(HomeAssistant hass, TuyaConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: fan.py:38
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103