cover.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """
  2. Setup for different kinds of Tuya cover devices
  3. """
  4. import logging
  5. from homeassistant.components.cover import (
  6. CoverDeviceClass,
  7. CoverEntity,
  8. CoverEntityFeature,
  9. )
  10. from homeassistant.util.percentage import (
  11. percentage_to_ranged_value,
  12. ranged_value_to_percentage,
  13. )
  14. from .device import TuyaLocalDevice
  15. from .entity import TuyaLocalEntity
  16. from .helpers.config import async_tuya_setup_platform
  17. from .helpers.device_config import TuyaEntityConfig
  18. _LOGGER = logging.getLogger(__name__)
  19. async def async_setup_entry(hass, config_entry, async_add_entities):
  20. config = {**config_entry.data, **config_entry.options}
  21. await async_tuya_setup_platform(
  22. hass,
  23. async_add_entities,
  24. config,
  25. "cover",
  26. TuyaLocalCover,
  27. )
  28. class TuyaLocalCover(TuyaLocalEntity, CoverEntity):
  29. """Representation of a Tuya Cover Entity."""
  30. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  31. """
  32. Initialise the cover device.
  33. Args:
  34. device (TuyaLocalDevice): The device API instance
  35. config (TuyaEntityConfig): The entity config
  36. """
  37. super().__init__()
  38. dps_map = self._init_begin(device, config)
  39. self._position_dp = dps_map.pop("position", None)
  40. self._currentpos_dp = dps_map.pop("current_position", None)
  41. self._tiltpos_dp = dps_map.pop("tilt_position", None)
  42. self._control_dp = dps_map.pop("control", None)
  43. self._action_dp = dps_map.pop("action", None)
  44. self._open_dp = dps_map.pop("open", None)
  45. self._init_end(dps_map)
  46. self._support_flags = CoverEntityFeature(0)
  47. if self._position_dp:
  48. self._support_flags |= CoverEntityFeature.SET_POSITION
  49. if self._control_dp:
  50. if "stop" in self._control_dp.values(self._device):
  51. self._support_flags |= CoverEntityFeature.STOP
  52. if "open" in self._control_dp.values(self._device):
  53. self._support_flags |= CoverEntityFeature.OPEN
  54. if "close" in self._control_dp.values(self._device):
  55. self._support_flags |= CoverEntityFeature.CLOSE
  56. if self._tiltpos_dp:
  57. self._support_flags |= CoverEntityFeature.SET_TILT_POSITION
  58. # Open/close/stop tilt not yet supported, as no test devices known
  59. @property
  60. def device_class(self):
  61. """Return the class of ths device"""
  62. dclass = self._config.device_class
  63. try:
  64. return CoverDeviceClass(dclass)
  65. except ValueError:
  66. if dclass:
  67. _LOGGER.warning(
  68. "%s/%s: Unrecognised cover device class of %s ignored",
  69. self._config._device.config,
  70. self.name or "cover",
  71. dclass,
  72. )
  73. @property
  74. def supported_features(self):
  75. """Inform HA of the supported features."""
  76. return self._support_flags
  77. def _state_to_percent(self, state):
  78. """Convert a state to percent open"""
  79. if state == "opened":
  80. return 100
  81. elif state == "closed":
  82. return 0
  83. else:
  84. return 50
  85. @property
  86. def current_cover_position(self):
  87. """Return current position of cover."""
  88. if self._currentpos_dp:
  89. pos = self._currentpos_dp.get_value(self._device)
  90. if pos is not None:
  91. return pos
  92. if self._open_dp:
  93. state = self._open_dp.get_value(self._device)
  94. if state is not None:
  95. return 100 if state else 0
  96. if self._action_dp:
  97. state = self._action_dp.get_value(self._device)
  98. return self._state_to_percent(state)
  99. if self._position_dp:
  100. pos = self._position_dp.get_value(self._device)
  101. return pos
  102. @property
  103. def current_cover_tilt_position(self):
  104. """Return current tilt position of cover."""
  105. if self._tiltpos_dp:
  106. r = self._tiltpos_dp.range(self._device)
  107. val = self._tiltpos_dp.get_value(self._device)
  108. if r and val is not None:
  109. return ranged_value_to_percentage(r, val)
  110. return val
  111. @property
  112. def _current_state(self):
  113. """Return the current state of the cover if it can be determined,
  114. or None if it is inconclusive.
  115. """
  116. if self._action_dp:
  117. action = self._action_dp.get_value(self._device)
  118. if action in ["opening", "closing", "opened", "closed"]:
  119. return action
  120. pos = self.current_cover_position
  121. if pos is None:
  122. return None
  123. if pos < 5:
  124. return "closed"
  125. elif pos > 95:
  126. return "opened"
  127. if self._currentpos_dp and self._position_dp:
  128. setpos = self._position_dp.get_value(self._device)
  129. if setpos == pos:
  130. # if the current position is around the set position,
  131. # which is not closed, then we want is_closed to return
  132. # false, so HA gets the full state from position.
  133. return "opened"
  134. if self._control_dp:
  135. cmd = self._control_dp.get_value(self._device)
  136. if cmd == "open":
  137. return "opening"
  138. elif cmd == "close":
  139. return "closing"
  140. @property
  141. def is_opening(self):
  142. """Return if the cover is opening or not."""
  143. state = self._current_state
  144. if state is None:
  145. # If we return false, and is_closing and is_opening are also false,
  146. # HA assumes open. If we don't know, return None.
  147. return None
  148. else:
  149. return state == "opening"
  150. @property
  151. def is_closing(self):
  152. """Return if the cover is closing or not."""
  153. state = self._current_state
  154. if state is None:
  155. # If we return false, and is_closing and is_opening are also false,
  156. # HA assumes open. If we don't know, return None.
  157. return None
  158. else:
  159. return state == "closing"
  160. @property
  161. def is_closed(self):
  162. """Return if the cover is closed or not, if it can be determined."""
  163. state = self._current_state
  164. if state is None:
  165. # If we return false, and is_closing and is_opening are also false,
  166. # HA assumes open. If we don't know, return None.
  167. return None
  168. else:
  169. return state == "closed"
  170. async def async_open_cover(self, **kwargs):
  171. """Open the cover."""
  172. if self._control_dp and "open" in self._control_dp.values(self._device):
  173. await self._control_dp.async_set_value(self._device, "open")
  174. elif self._position_dp:
  175. pos = 100
  176. await self._position_dp.async_set_value(self._device, pos)
  177. else:
  178. raise NotImplementedError()
  179. async def async_close_cover(self, **kwargs):
  180. """Close the cover."""
  181. if self._control_dp and "close" in self._control_dp.values(self._device):
  182. await self._control_dp.async_set_value(self._device, "close")
  183. elif self._position_dp:
  184. pos = 0
  185. await self._position_dp.async_set_value(self._device, pos)
  186. else:
  187. raise NotImplementedError()
  188. async def async_set_cover_position(self, position, **kwargs):
  189. """Set the cover to a specific position."""
  190. if position is None:
  191. raise AttributeError()
  192. if self._position_dp:
  193. await self._position_dp.async_set_value(self._device, position)
  194. else:
  195. raise NotImplementedError()
  196. async def async_set_cover_tilt_position(self, tilt_position, **kwargs):
  197. """Set the cover tilt position."""
  198. if self._tiltpos_dp:
  199. # If there is a fixed list of values, snap to the closest one
  200. if self._tiltpos_dp.values(self._device):
  201. tilt_position = min(
  202. self._tiltpos_dp.values(self._device),
  203. key=lambda x: abs(x - tilt_position),
  204. )
  205. elif self._tiltpos_dp.range(self._device):
  206. r = self._tiltpos_dp.range(self._device)
  207. tilt_position = percentage_to_ranged_value(r, tilt_position)
  208. await self._tiltpos_dp.async_set_value(self._device, tilt_position)
  209. else:
  210. raise NotImplementedError
  211. async def async_stop_cover(self, **kwargs):
  212. """Stop the cover."""
  213. if self._control_dp and "stop" in self._control_dp.values(self._device):
  214. await self._control_dp.async_set_value(self._device, "stop")
  215. else:
  216. raise NotImplementedError()