4
0

cover.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. if state is not None:
  99. return self._state_to_percent(state)
  100. if self._position_dp:
  101. pos = self._position_dp.get_value(self._device)
  102. return pos
  103. @property
  104. def current_cover_tilt_position(self):
  105. """Return current tilt position of cover."""
  106. if self._tiltpos_dp:
  107. r = self._tiltpos_dp.range(self._device)
  108. val = self._tiltpos_dp.get_value(self._device)
  109. if r and val is not None:
  110. return ranged_value_to_percentage(r, val)
  111. return val
  112. @property
  113. def _current_state(self):
  114. """Return the current state of the cover if it can be determined,
  115. or None if it is inconclusive.
  116. """
  117. if self._action_dp:
  118. action = self._action_dp.get_value(self._device)
  119. if action in ["opening", "closing", "opened", "closed"]:
  120. return action
  121. pos = self.current_cover_position
  122. if pos is None:
  123. return None
  124. if pos < 5:
  125. return "closed"
  126. elif pos > 95:
  127. return "opened"
  128. if self._currentpos_dp and self._position_dp:
  129. setpos = self._position_dp.get_value(self._device)
  130. if setpos == pos:
  131. # if the current position is around the set position,
  132. # which is not closed, then we want is_closed to return
  133. # false, so HA gets the full state from position.
  134. return "opened"
  135. if self._control_dp:
  136. cmd = self._control_dp.get_value(self._device)
  137. if cmd == "open":
  138. return "opening"
  139. elif cmd == "close":
  140. return "closing"
  141. @property
  142. def is_opening(self):
  143. """Return if the cover is opening or not."""
  144. state = self._current_state
  145. if state is None:
  146. # If we return false, and is_closing and is_opening are also false,
  147. # HA assumes open. If we don't know, return None.
  148. return None
  149. else:
  150. return state == "opening"
  151. @property
  152. def is_closing(self):
  153. """Return if the cover is closing or not."""
  154. state = self._current_state
  155. if state is None:
  156. # If we return false, and is_closing and is_opening are also false,
  157. # HA assumes open. If we don't know, return None.
  158. return None
  159. else:
  160. return state == "closing"
  161. @property
  162. def is_closed(self):
  163. """Return if the cover is closed or not, if it can be determined."""
  164. state = self._current_state
  165. if state is None:
  166. # If we return false, and is_closing and is_opening are also false,
  167. # HA assumes open. If we don't know, return None.
  168. return None
  169. else:
  170. return state == "closed"
  171. async def async_open_cover(self, **kwargs):
  172. """Open the cover."""
  173. if self._control_dp and "open" in self._control_dp.values(self._device):
  174. await self._control_dp.async_set_value(self._device, "open")
  175. elif self._position_dp:
  176. pos = 100
  177. await self._position_dp.async_set_value(self._device, pos)
  178. else:
  179. raise NotImplementedError()
  180. async def async_close_cover(self, **kwargs):
  181. """Close the cover."""
  182. if self._control_dp and "close" in self._control_dp.values(self._device):
  183. await self._control_dp.async_set_value(self._device, "close")
  184. elif self._position_dp:
  185. pos = 0
  186. await self._position_dp.async_set_value(self._device, pos)
  187. else:
  188. raise NotImplementedError()
  189. async def async_set_cover_position(self, position, **kwargs):
  190. """Set the cover to a specific position."""
  191. if position is None:
  192. raise AttributeError()
  193. if self._position_dp:
  194. await self._position_dp.async_set_value(self._device, position)
  195. else:
  196. raise NotImplementedError()
  197. async def async_set_cover_tilt_position(self, tilt_position, **kwargs):
  198. """Set the cover tilt position."""
  199. if self._tiltpos_dp:
  200. # If there is a fixed list of values, snap to the closest one
  201. if self._tiltpos_dp.values(self._device):
  202. tilt_position = min(
  203. self._tiltpos_dp.values(self._device),
  204. key=lambda x: abs(x - tilt_position),
  205. )
  206. elif self._tiltpos_dp.range(self._device):
  207. r = self._tiltpos_dp.range(self._device)
  208. tilt_position = percentage_to_ranged_value(r, tilt_position)
  209. await self._tiltpos_dp.async_set_value(self._device, tilt_position)
  210. else:
  211. raise NotImplementedError
  212. async def async_stop_cover(self, **kwargs):
  213. """Stop the cover."""
  214. if self._control_dp and "stop" in self._control_dp.values(self._device):
  215. await self._control_dp.async_set_value(self._device, "stop")
  216. else:
  217. raise NotImplementedError()