fan.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """
  2. Setup for different kinds of Tuya fan devices
  3. """
  4. import logging
  5. from typing import Any
  6. from homeassistant.components.fan import FanEntity, FanEntityFeature
  7. from homeassistant.util.percentage import (
  8. percentage_to_ranged_value,
  9. ranged_value_to_percentage,
  10. )
  11. from .device import TuyaLocalDevice
  12. from .entity import TuyaLocalEntity
  13. from .helpers.config import async_tuya_setup_platform
  14. from .helpers.device_config import TuyaEntityConfig
  15. _LOGGER = logging.getLogger(__name__)
  16. async def async_setup_entry(hass, config_entry, async_add_entities):
  17. config = {**config_entry.data, **config_entry.options}
  18. await async_tuya_setup_platform(
  19. hass,
  20. async_add_entities,
  21. config,
  22. "fan",
  23. TuyaLocalFan,
  24. )
  25. class TuyaLocalFan(TuyaLocalEntity, FanEntity):
  26. """Representation of a Tuya Fan entity."""
  27. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  28. """
  29. Initialise the fan device.
  30. Args:
  31. device (TuyaLocalDevice): The device API instance.
  32. config (TuyaEntityConfig): The entity config.
  33. """
  34. super().__init__()
  35. dps_map = self._init_begin(device, config)
  36. self._switch_dps = dps_map.pop("switch", None)
  37. self._preset_dps = dps_map.pop("preset_mode", None)
  38. self._speed_dps = dps_map.pop("speed", None)
  39. self._oscillate_dps = dps_map.pop("oscillate", None)
  40. self._direction_dps = dps_map.pop("direction", None)
  41. self._init_end(dps_map)
  42. self._support_flags = FanEntityFeature(0)
  43. if self._preset_dps:
  44. self._support_flags |= FanEntityFeature.PRESET_MODE
  45. if self._speed_dps:
  46. self._support_flags |= FanEntityFeature.SET_SPEED
  47. if self._oscillate_dps:
  48. self._support_flags |= FanEntityFeature.OSCILLATE
  49. if self._direction_dps:
  50. self._support_flags |= FanEntityFeature.DIRECTION
  51. if self._switch_dps:
  52. self._support_flags |= FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
  53. elif self._speed_dps:
  54. r = self._speed_dps.range(self._device)
  55. if r and r[0] == 0:
  56. self._support_flags |= FanEntityFeature.TURN_OFF
  57. # Until the deprecation period ends (expected 2025.2)
  58. self._enable_turn_on_off_backwards_compatibility = False
  59. @property
  60. def supported_features(self):
  61. """Return the features supported by this climate device."""
  62. return self._support_flags
  63. @property
  64. def is_on(self):
  65. """Return whether the switch is on or not."""
  66. # If there is no switch, it is always on
  67. if self._switch_dps is None:
  68. return self.available
  69. return self._switch_dps.get_value(self._device)
  70. async def async_turn_on(
  71. self,
  72. percentage: int | None = None,
  73. preset_mode: str | None = None,
  74. **kwargs: Any,
  75. ):
  76. """Turn the fan on, setting any other parameters given"""
  77. settings = {}
  78. if self._switch_dps:
  79. _LOGGER.info("%s turning on", self._config.config_id)
  80. settings = {
  81. **settings,
  82. **self._switch_dps.get_values_to_set(self._device, True, settings),
  83. }
  84. if percentage is not None and self._speed_dps:
  85. r = self._speed_dps.range(self._device)
  86. if r:
  87. if r[0] == 0:
  88. r = (1, r[1])
  89. percentage = percentage_to_ranged_value(r, percentage)
  90. _LOGGER.info(
  91. "%s turning on to speed %s", self._config.config_id, percentage
  92. )
  93. settings = {
  94. **settings,
  95. **self._speed_dps.get_values_to_set(self._device, percentage, settings),
  96. }
  97. if preset_mode and self._preset_dps:
  98. _LOGGER.info(
  99. "%s turning on to preset %s", self._config.config_id, preset_mode
  100. )
  101. settings = {
  102. **settings,
  103. **self._preset_dps.get_values_to_set(
  104. self._device, preset_mode, settings
  105. ),
  106. }
  107. # TODO: potentially handle other kwargs.
  108. if settings:
  109. await self._device.async_set_properties(settings)
  110. async def async_turn_off(self, **kwargs):
  111. """Turn the switch off"""
  112. if self._switch_dps:
  113. _LOGGER.info("%s turning off", self._config.config_id)
  114. await self._switch_dps.async_set_value(self._device, False)
  115. elif (
  116. self._speed_dps
  117. and self._speed_dps.range(self._device)
  118. and self._speed_dps.range(self._device)[0] == 0
  119. ):
  120. _LOGGER.info("%s turning off by setting speed to 0", self._config.config_id)
  121. await self._speed_dps.async_set_value(self._device, 0)
  122. else:
  123. raise NotImplementedError
  124. @property
  125. def percentage(self):
  126. """Return the currently set percentage."""
  127. if self._speed_dps is None:
  128. return None
  129. r = self._speed_dps.range(self._device)
  130. val = self._speed_dps.get_value(self._device)
  131. if r and val is not None:
  132. if r[0] == 0:
  133. r = (1, r[1])
  134. val = ranged_value_to_percentage(r, val)
  135. return val
  136. @property
  137. def percentage_step(self):
  138. """Return the step for percentage."""
  139. if self._speed_dps is None:
  140. return None
  141. if self._speed_dps.values(self._device):
  142. return 100 / len(self._speed_dps.values(self._device))
  143. r = self._speed_dps.range(self._device)
  144. scale = 100 / r[1] if r else 1.0
  145. return self._speed_dps.step(self._device) * scale
  146. @property
  147. def speed_count(self):
  148. """Return the number of speeds supported by the fan."""
  149. if self._speed_dps is None:
  150. return 0
  151. if self._speed_dps.values(self._device):
  152. return len(self._speed_dps.values(self._device))
  153. return int(round(100 / self.percentage_step))
  154. async def async_set_percentage(self, percentage):
  155. """Set the fan speed as a percentage."""
  156. # If speed is 0, turn the fan off
  157. if percentage == 0 and self._switch_dps:
  158. return await self.async_turn_off()
  159. if self._speed_dps is None:
  160. return None
  161. # If there is a fixed list of values, snap to the closest one
  162. if self._speed_dps.values(self._device):
  163. percentage = min(
  164. self._speed_dps.values(self._device),
  165. key=lambda x: abs(x - percentage),
  166. )
  167. elif self._speed_dps.range(self._device):
  168. r = self._speed_dps.range(self._device)
  169. if r[0] == 0:
  170. r = (1, r[1])
  171. percentage = percentage_to_ranged_value(r, percentage)
  172. _LOGGER.info("%s setting speed to %s", self._config.config_id, percentage)
  173. values_to_set = self._speed_dps.get_values_to_set(self._device, percentage)
  174. if not self.is_on and self._switch_dps:
  175. values_to_set.update(
  176. self._switch_dps.get_values_to_set(self._device, True, values_to_set)
  177. )
  178. await self._device.async_set_properties(values_to_set)
  179. @property
  180. def preset_mode(self):
  181. """Return the current preset mode."""
  182. if self._preset_dps:
  183. return self._preset_dps.get_value(self._device)
  184. @property
  185. def preset_modes(self):
  186. """Return the list of presets that this device supports."""
  187. if self._preset_dps is None:
  188. return []
  189. return self._preset_dps.values(self._device)
  190. async def async_set_preset_mode(self, preset_mode):
  191. """Set the preset mode."""
  192. if self._preset_dps is None:
  193. raise NotImplementedError()
  194. _LOGGER.info(
  195. "%s setting preset mode to %s", self._config.config_id, preset_mode
  196. )
  197. await self._preset_dps.async_set_value(self._device, preset_mode)
  198. @property
  199. def current_direction(self):
  200. """Return the current direction [forward or reverse]."""
  201. if self._direction_dps:
  202. return self._direction_dps.get_value(self._device)
  203. async def async_set_direction(self, direction):
  204. """Set the direction of the fan."""
  205. if self._direction_dps is None:
  206. raise NotImplementedError()
  207. _LOGGER.info("%s setting direction to %s", self._config.config_id, direction)
  208. await self._direction_dps.async_set_value(self._device, direction)
  209. @property
  210. def oscillating(self):
  211. """Return whether or not the fan is oscillating."""
  212. if self._oscillate_dps:
  213. return self._oscillate_dps.get_value(self._device)
  214. async def async_oscillate(self, oscillating):
  215. """Oscillate the fan."""
  216. if self._oscillate_dps is None:
  217. raise NotImplementedError()
  218. _LOGGER.info("%s setting oscillate to %s", self._config.config_id, oscillating)
  219. await self._oscillate_dps.async_set_value(self._device, oscillating)