curtain.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. """Library to handle connection with Switchbot."""
  2. from __future__ import annotations
  3. import logging
  4. from typing import Any
  5. from .device import REQ_HEADER, SwitchbotDevice
  6. # Curtain keys
  7. CURTAIN_COMMAND = "4501"
  8. OPEN_KEYS = [
  9. f"{REQ_HEADER}{CURTAIN_COMMAND}010100",
  10. f"{REQ_HEADER}{CURTAIN_COMMAND}05ff00",
  11. ]
  12. CLOSE_KEYS = [
  13. f"{REQ_HEADER}{CURTAIN_COMMAND}010164",
  14. f"{REQ_HEADER}{CURTAIN_COMMAND}05ff64",
  15. ]
  16. POSITION_KEYS = [
  17. f"{REQ_HEADER}{CURTAIN_COMMAND}0101",
  18. f"{REQ_HEADER}{CURTAIN_COMMAND}05ff",
  19. ] # +actual_position
  20. STOP_KEYS = [f"{REQ_HEADER}{CURTAIN_COMMAND}0001", f"{REQ_HEADER}{CURTAIN_COMMAND}00ff"]
  21. CURTAIN_EXT_SUM_KEY = f"{REQ_HEADER}460401"
  22. CURTAIN_EXT_ADV_KEY = f"{REQ_HEADER}460402"
  23. CURTAIN_EXT_CHAIN_INFO_KEY = f"{REQ_HEADER}468101"
  24. _LOGGER = logging.getLogger(__name__)
  25. class SwitchbotCurtain(SwitchbotDevice):
  26. """Representation of a Switchbot Curtain."""
  27. def __init__(self, *args: Any, **kwargs: Any) -> None:
  28. """Switchbot Curtain/WoCurtain constructor."""
  29. # The position of the curtain is saved returned with 0 = open and 100 = closed.
  30. # This is independent of the calibration of the curtain bot (Open left to right/
  31. # Open right to left/Open from the middle).
  32. # The parameter 'reverse_mode' reverse these values,
  33. # if 'reverse_mode' = True, position = 0 equals close
  34. # and position = 100 equals open. The parameter is default set to True so that
  35. # the definition of position is the same as in Home Assistant.
  36. super().__init__(*args, **kwargs)
  37. self._reverse: bool = kwargs.pop("reverse_mode", True)
  38. self._settings: dict[str, Any] = {}
  39. self.ext_info_sum: dict[str, Any] = {}
  40. self.ext_info_adv: dict[str, Any] = {}
  41. async def _send_multiple_commands(self, keys: list[str]) -> bool:
  42. """Send multiple commands to device.
  43. Since we current have no way to tell which command the device
  44. needs we send both.
  45. """
  46. final_result = False
  47. for key in keys:
  48. result = await self._send_command(key)
  49. final_result |= self._check_command_result(result, 0, {1})
  50. return final_result
  51. async def open(self) -> bool:
  52. """Send open command."""
  53. return await self._send_multiple_commands(OPEN_KEYS)
  54. async def close(self) -> bool:
  55. """Send close command."""
  56. return await self._send_multiple_commands(CLOSE_KEYS)
  57. async def stop(self) -> bool:
  58. """Send stop command to device."""
  59. return await self._send_multiple_commands(STOP_KEYS)
  60. async def set_position(self, position: int) -> bool:
  61. """Send position command (0-100) to device."""
  62. position = (100 - position) if self._reverse else position
  63. hex_position = "%0.2X" % position
  64. return await self._send_multiple_commands(
  65. [key + hex_position for key in POSITION_KEYS]
  66. )
  67. async def update(self, interface: int | None = None) -> None:
  68. """Update position, battery percent and light level of device."""
  69. await self.get_device_data(retry=self._retry_count, interface=interface)
  70. def get_position(self) -> Any:
  71. """Return cached position (0-100) of Curtain."""
  72. # To get actual position call update() first.
  73. return self._get_adv_value("position")
  74. async def get_basic_info(self) -> dict[str, Any] | None:
  75. """Get device basic settings."""
  76. if not (_data := await self._get_basic_info()):
  77. return None
  78. _position = max(min(_data[6], 100), 0)
  79. return {
  80. "battery": _data[1],
  81. "firmware": _data[2] / 10.0,
  82. "chainLength": _data[3],
  83. "openDirection": (
  84. "right_to_left" if _data[4] & 0b10000000 == 128 else "left_to_right"
  85. ),
  86. "touchToOpen": bool(_data[4] & 0b01000000),
  87. "light": bool(_data[4] & 0b00100000),
  88. "fault": bool(_data[4] & 0b00001000),
  89. "solarPanel": bool(_data[5] & 0b00001000),
  90. "calibrated": bool(_data[5] & 0b00000100),
  91. "inMotion": bool(_data[5] & 0b01000011),
  92. "position": (100 - _position) if self._reverse else _position,
  93. "timers": _data[7],
  94. }
  95. async def get_extended_info_summary(self) -> dict[str, Any] | None:
  96. """Get basic info for all devices in chain."""
  97. _data = await self._send_command(key=CURTAIN_EXT_SUM_KEY)
  98. if not _data:
  99. _LOGGER.error("%s: Unsuccessful, no result from device", self.name)
  100. return None
  101. if _data in (b"\x07", b"\x00"):
  102. _LOGGER.error("%s: Unsuccessful, please try again", self.name)
  103. return None
  104. self.ext_info_sum["device0"] = {
  105. "openDirectionDefault": not bool(_data[1] & 0b10000000),
  106. "touchToOpen": bool(_data[1] & 0b01000000),
  107. "light": bool(_data[1] & 0b00100000),
  108. "openDirection": (
  109. "left_to_right" if _data[1] & 0b00010000 == 1 else "right_to_left"
  110. ),
  111. }
  112. # if grouped curtain device present.
  113. if _data[2] != 0:
  114. self.ext_info_sum["device1"] = {
  115. "openDirectionDefault": not bool(_data[1] & 0b10000000),
  116. "touchToOpen": bool(_data[1] & 0b01000000),
  117. "light": bool(_data[1] & 0b00100000),
  118. "openDirection": (
  119. "left_to_right" if _data[1] & 0b00010000 else "right_to_left"
  120. ),
  121. }
  122. return self.ext_info_sum
  123. async def get_extended_info_adv(self) -> dict[str, Any] | None:
  124. """Get advance page info for device chain."""
  125. _data = await self._send_command(key=CURTAIN_EXT_ADV_KEY)
  126. if not _data:
  127. _LOGGER.error("%s: Unsuccessful, no result from device", self.name)
  128. return None
  129. if _data in (b"\x07", b"\x00"):
  130. _LOGGER.error("%s: Unsuccessful, please try again", self.name)
  131. return None
  132. _state_of_charge = [
  133. "not_charging",
  134. "charging_by_adapter",
  135. "charging_by_solar",
  136. "fully_charged",
  137. "solar_not_charging",
  138. "charging_error",
  139. ]
  140. self.ext_info_adv["device0"] = {
  141. "battery": _data[1],
  142. "firmware": _data[2] / 10.0,
  143. "stateOfCharge": _state_of_charge[_data[3]],
  144. }
  145. # If grouped curtain device present.
  146. if _data[4]:
  147. self.ext_info_adv["device1"] = {
  148. "battery": _data[4],
  149. "firmware": _data[5] / 10.0,
  150. "stateOfCharge": _state_of_charge[_data[6]],
  151. }
  152. return self.ext_info_adv
  153. def get_light_level(self) -> Any:
  154. """Return cached light level."""
  155. # To get actual light level call update() first.
  156. return self._get_adv_value("lightLevel")
  157. def is_reversed(self) -> bool:
  158. """Return True if curtain position is opposite from SB data."""
  159. return self._reverse
  160. def is_calibrated(self) -> Any:
  161. """Return True curtain is calibrated."""
  162. # To get actual light level call update() first.
  163. return self._get_adv_value("calibration")