curtain.py 6.5 KB

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