cover.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """
  2. Platform to control tuya cover devices.
  3. """
  4. import logging
  5. from homeassistant.components.cover import (
  6. CoverDeviceClass,
  7. CoverEntity,
  8. CoverEntityFeature,
  9. )
  10. from ..device import TuyaLocalDevice
  11. from ..helpers.device_config import TuyaEntityConfig
  12. from ..helpers.mixin import TuyaLocalEntity
  13. _LOGGER = logging.getLogger(__name__)
  14. class TuyaLocalCover(TuyaLocalEntity, CoverEntity):
  15. """Representation of a Tuya Cover Entity."""
  16. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  17. """
  18. Initialise the cover device.
  19. Args:
  20. device (TuyaLocalDevice): The device API instance
  21. config (TuyaEntityConfig): The entity config
  22. """
  23. dps_map = self._init_begin(device, config)
  24. self._position_dp = dps_map.pop("position", None)
  25. self._currentpos_dp = dps_map.pop("current_position", None)
  26. self._control_dp = dps_map.pop("control", None)
  27. self._action_dp = dps_map.pop("action", None)
  28. self._open_dp = dps_map.pop("open", None)
  29. self._reversed_dp = dps_map.pop("reversed", None)
  30. self._init_end(dps_map)
  31. self._support_flags = 0
  32. if self._position_dp:
  33. self._support_flags |= CoverEntityFeature.SET_POSITION
  34. if self._control_dp:
  35. if "stop" in self._control_dp.values(self._device):
  36. self._support_flags |= CoverEntityFeature.STOP
  37. if "open" in self._control_dp.values(self._device):
  38. self._support_flags |= CoverEntityFeature.OPEN
  39. if "close" in self._control_dp.values(self._device):
  40. self._support_flags |= CoverEntityFeature.CLOSE
  41. # Tilt not yet supported, as no test devices known
  42. @property
  43. def _is_reversed(self):
  44. return self._reversed_dp and self._reversed_dp.get_value(self._device)
  45. def _maybe_reverse(self, percent):
  46. """Reverse the percentage if it should be, otherwise leave it alone"""
  47. return 100 - percent if self._is_reversed else percent
  48. @property
  49. def device_class(self):
  50. """Return the class of ths device"""
  51. dclass = self._config.device_class
  52. try:
  53. return CoverDeviceClass(dclass)
  54. except ValueError:
  55. if dclass:
  56. _LOGGER.warning(f"Unrecognised cover device class of {dclass} ignored")
  57. return None
  58. @property
  59. def supported_features(self):
  60. """Inform HA of the supported features."""
  61. return self._support_flags
  62. def _state_to_percent(self, state):
  63. """Convert a state to percent open"""
  64. if state == "opened":
  65. return 100
  66. elif state == "closed":
  67. return 0
  68. else:
  69. return 50
  70. @property
  71. def current_cover_position(self):
  72. """Return current position of cover."""
  73. if self._currentpos_dp:
  74. pos = self._currentpos_dp.get_value(self._device)
  75. if pos is not None:
  76. return self._maybe_reverse(pos)
  77. if self._position_dp:
  78. pos = self._position_dp.get_value(self._device)
  79. return self._maybe_reverse(pos)
  80. if self._open_dp:
  81. state = self._open_dp.get_value(self._device)
  82. if state is not None:
  83. return 100 if state else 0
  84. if self._action_dp:
  85. state = self._action_dp.get_value(self._device)
  86. return self._state_to_percent(state)
  87. @property
  88. def is_opening(self):
  89. """Return if the cover is opening or not."""
  90. # If dps is available to inform current action, use that
  91. if self._action_dp:
  92. return self._action_dp.get_value(self._device) == "opening"
  93. # Otherwise use last command and check it hasn't completed
  94. if self._control_dp:
  95. pos = self.current_cover_position
  96. if pos is not None:
  97. return (
  98. self._control_dp.get_value(self._device) == "open"
  99. and self.current_cover_position != 100
  100. )
  101. @property
  102. def is_closing(self):
  103. """Return if the cover is closing or not."""
  104. # If dps is available to inform current action, use that
  105. if self._action_dp:
  106. return self._action_dp.get_value(self._device) == "closing"
  107. # Otherwise use last command and check it hasn't completed
  108. if self._control_dp:
  109. closed = self.is_closed
  110. if closed is not None:
  111. return (
  112. self._control_dp.get_value(self._device) == "close" and not closed
  113. )
  114. @property
  115. def is_closed(self):
  116. """Return if the cover is closed or not, if it can be determined."""
  117. # Only use position if it is reliable, otherwise curtain can become
  118. # stuck in "open" state when we don't actually know what state it is.
  119. pos = self.current_cover_position
  120. if isinstance(pos, int):
  121. return pos == 0
  122. async def async_open_cover(self, **kwargs):
  123. """Open the cover."""
  124. if self._control_dp and "open" in self._control_dp.values(self._device):
  125. await self._control_dp.async_set_value(self._device, "open")
  126. elif self._position_dp:
  127. pos = 0 if self._is_reversed else 100
  128. await self._position_dp.async_set_value(self._device, pos)
  129. else:
  130. raise NotImplementedError()
  131. async def async_close_cover(self, **kwargs):
  132. """Close the cover."""
  133. if self._control_dp and "close" in self._control_dp.values(self._device):
  134. await self._control_dp.async_set_value(self._device, "close")
  135. elif self._position_dp:
  136. pos = 100 if self._is_reversed else 0
  137. await self._position_dp.async_set_value(self._device, pos)
  138. else:
  139. raise NotImplementedError()
  140. async def async_set_cover_position(self, position, **kwargs):
  141. """Set the cover to a specific position."""
  142. if position is None:
  143. raise AttributeError()
  144. if self._position_dp:
  145. position = self._maybe_reverse(position)
  146. await self._position_dp.async_set_value(self._device, position)
  147. else:
  148. raise NotImplementedError()
  149. async def async_stop_cover(self, **kwargs):
  150. """Stop the cover."""
  151. if self._control_dp and "stop" in self._control_dp.values(self._device):
  152. await self._control_dp.async_set_value(self._device, "stop")
  153. else:
  154. raise NotImplementedError()