fan.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """
  2. Platform to control tuya fan devices.
  3. """
  4. import logging
  5. from homeassistant.components.fan import (
  6. FanEntity,
  7. SUPPORT_DIRECTION,
  8. SUPPORT_OSCILLATE,
  9. SUPPORT_PRESET_MODE,
  10. SUPPORT_SET_SPEED,
  11. )
  12. from homeassistant.const import (
  13. STATE_UNAVAILABLE,
  14. )
  15. from ..device import TuyaLocalDevice
  16. from ..helpers.device_config import TuyaEntityConfig
  17. _LOGGER = logging.getLogger(__name__)
  18. class TuyaLocalFan(FanEntity):
  19. """Representation of a Tuya Fan entity."""
  20. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  21. """
  22. Initialise the fan device.
  23. Args:
  24. device (TuyaLocalDevice): The device API instance.
  25. config (TuyaEntityConfig): The entity config.
  26. """
  27. self._device = device
  28. self._config = config
  29. self._support_flags = 0
  30. self._attr_dps = []
  31. dps_map = {c.name: c for c in config.dps()}
  32. self._switch_dps = dps_map.pop("switch", None)
  33. self._preset_dps = dps_map.pop("preset_mode", None)
  34. self._speed_dps = dps_map.pop("speed", None)
  35. self._oscillate_dps = dps_map.pop("oscillate", None)
  36. self._direction_dps = dps_map.pop("direction", None)
  37. for d in dps_map.values():
  38. if not d.hidden:
  39. self._attr_dps.append(d)
  40. if self._preset_dps:
  41. self._support_flags |= SUPPORT_PRESET_MODE
  42. if self._speed_dps:
  43. self._support_flags |= SUPPORT_SET_SPEED
  44. if self._oscillate_dps:
  45. self._support_flags |= SUPPORT_OSCILLATE
  46. if self._direction_dps:
  47. self._support_flags |= SUPPORT_DIRECTION
  48. @property
  49. def supported_features(self):
  50. """Return the features supported by this climate device."""
  51. return self._support_flags
  52. @property
  53. def should_poll(self):
  54. """Return the polling state."""
  55. return True
  56. @property
  57. def name(self):
  58. """Return the name of the climate device."""
  59. return self._device.name
  60. @property
  61. def friendly_name(self):
  62. """Return the friendly name of the climate entity for the UI."""
  63. return self._config.name
  64. @property
  65. def unique_id(self):
  66. """Return the unique id for this climate device."""
  67. return self._device.unique_id
  68. @property
  69. def device_info(self):
  70. """Return device information about this heater."""
  71. return self._device.device_info
  72. @property
  73. def icon(self):
  74. """Return the icon to use in the frontend for this device."""
  75. icon = self._config.icon(self._device)
  76. if icon:
  77. return icon
  78. else:
  79. return super().icon
  80. @property
  81. def is_on(self):
  82. """Return whether the switch is on or not."""
  83. # If there is no switch, it is always on
  84. if self._switch_dps is None:
  85. return True
  86. is_switched_on = self._switch_dps.get_value(self._device)
  87. if is_switched_on is None:
  88. return STATE_UNAVAILABLE
  89. else:
  90. return bool(is_switched_on)
  91. async def async_turn_on(self, **kwargs):
  92. """Turn the switch on"""
  93. if self._switch_dps is None:
  94. raise NotImplementedError()
  95. await self._switch_dps.async_set_value(self._device, True)
  96. async def async_turn_off(self, **kwargs):
  97. """Turn the switch off"""
  98. if self._switch_dps is None:
  99. raise NotImplementedError
  100. await self._switch_dps.async_set_value(self._device, False)
  101. @property
  102. def percentage(self):
  103. """Return the currently set percentage."""
  104. if self._speed_dps is None:
  105. return None
  106. return self._speed_dps.get_value(self._device)
  107. @property
  108. def percentage_step(self):
  109. """Return the step for percentage."""
  110. if self._speed_dps is None:
  111. return None
  112. if self._speed_dps.values(self._device) is None:
  113. return self._speed_dps.step(self._device)
  114. else:
  115. return 100 / len(self._speed_dps.values(self._device))
  116. @property
  117. def speed_count(self):
  118. """Return the number of speeds supported by the fan."""
  119. if self._speed_dps is None:
  120. return 0
  121. if self._speed_dps.values(self._device) is not None:
  122. return len(self._speed_dps.values(self._device))
  123. return int(round(100 / self.percentage_step))
  124. async def async_set_percentage(self, percentage):
  125. """Set the fan speed as a percentage."""
  126. if self._speed_dps is None:
  127. return None
  128. # If there is a fixed list of values, snap to the closest one
  129. if self._speed_dps.values(self._device) is not None:
  130. percentage = min(
  131. self._speed_dps.values(self._device), key=lambda x: abs(x - percentage)
  132. )
  133. await self._speed_dps.async_set_value(self._device, percentage)
  134. @property
  135. def preset_mode(self):
  136. """Return the current preset mode."""
  137. if self._preset_dps is None:
  138. return None
  139. return self._preset_dps.get_value(self._device)
  140. @property
  141. def preset_modes(self):
  142. """Return the list of presets that this device supports."""
  143. if self._preset_dps is None:
  144. return []
  145. return self._preset_dps.values(self._device)
  146. async def async_set_preset_mode(self, preset_mode):
  147. """Set the preset mode."""
  148. if self._preset_dps is None:
  149. raise NotImplementedError()
  150. await self._preset_dps.async_set_value(self._device, preset_mode)
  151. @property
  152. def current_direction(self):
  153. """Return the current direction [forward or reverse]."""
  154. if self._direction_dps is None:
  155. return None
  156. return self._direction_dps.get_value(self._device)
  157. async def async_set_direction(self, direction):
  158. """Set the direction of the fan."""
  159. if self._direction_dps is None:
  160. raise NotImplementedError()
  161. await self._direction_dps.async_set_value(self._device, direction)
  162. @property
  163. def oscillating(self):
  164. """Return whether or not the fan is oscillating."""
  165. if self._oscillate_dps is None:
  166. return None
  167. return self._oscillate_dps.get_value(self._device)
  168. async def async_oscillate(self, oscillating):
  169. """Oscillate the fan."""
  170. if self._oscillate_dps is None:
  171. raise NotImplementedError()
  172. await self._oscillate_dps.async_set_value(self._device, oscillating)
  173. @property
  174. def device_state_attributes(self):
  175. """Get additional attributes that the integration itself does not support."""
  176. attr = {}
  177. for a in self._attr_dps:
  178. attr[a.name] = a.get_value(self._device)
  179. return attr
  180. async def async_update(self):
  181. await self._device.async_refresh()