blind_tilt.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Library to handle connection with Switchbot."""
  2. from __future__ import annotations
  3. import logging
  4. from typing import Any
  5. from switchbot.devices.device import (
  6. REQ_HEADER,
  7. SwitchbotSequenceDevice,
  8. update_after_operation,
  9. )
  10. from ..models import SwitchBotAdvertisement
  11. from .base_cover import COVER_COMMAND, COVER_EXT_SUM_KEY, SwitchbotBaseCover
  12. _LOGGER = logging.getLogger(__name__)
  13. OPEN_KEYS = [
  14. f"{REQ_HEADER}{COVER_COMMAND}010132",
  15. f"{REQ_HEADER}{COVER_COMMAND}05ff32",
  16. ]
  17. CLOSE_DOWN_KEYS = [
  18. f"{REQ_HEADER}{COVER_COMMAND}010100",
  19. f"{REQ_HEADER}{COVER_COMMAND}05ff00",
  20. ]
  21. CLOSE_UP_KEYS = [
  22. f"{REQ_HEADER}{COVER_COMMAND}010164",
  23. f"{REQ_HEADER}{COVER_COMMAND}05ff64",
  24. ]
  25. class SwitchbotBlindTilt(SwitchbotBaseCover, SwitchbotSequenceDevice):
  26. """Representation of a Switchbot Blind Tilt."""
  27. # The position of the blind is saved returned with 0 = closed down, 50 = open and 100 = closed up.
  28. # This is independent of the calibration of the blind.
  29. # The parameter 'reverse_mode' reverse these values,
  30. # if 'reverse_mode' = True, position = 0 equals closed up
  31. # and position = 100 equals closed down. The parameter is default set to False so that
  32. # the definition of position is the same as in Home Assistant.
  33. # This is opposite to the base class so needs to be overwritten.
  34. def __init__(self, *args: Any, **kwargs: Any) -> None:
  35. """Switchbot Blind Tilt/woBlindTilt constructor."""
  36. self._reverse: bool = kwargs.pop("reverse_mode", False)
  37. super().__init__(self._reverse, *args, **kwargs)
  38. def _set_parsed_data(
  39. self, advertisement: SwitchBotAdvertisement, data: dict[str, Any]
  40. ) -> None:
  41. """Set data."""
  42. in_motion = data["inMotion"]
  43. previous_tilt = self._get_adv_value("tilt")
  44. new_tilt = data["tilt"]
  45. self._update_motion_direction(in_motion, previous_tilt, new_tilt)
  46. super()._set_parsed_data(advertisement, data)
  47. def _update_motion_direction(
  48. self, in_motion: bool, previous_tilt: int | None, new_tilt: int
  49. ) -> None:
  50. """Update opening/closing status based on movement."""
  51. if previous_tilt is None:
  52. return
  53. if in_motion is False:
  54. self._is_closing = self._is_opening = False
  55. return
  56. if new_tilt != previous_tilt:
  57. self._is_opening = new_tilt > previous_tilt
  58. self._is_closing = new_tilt < previous_tilt
  59. @update_after_operation
  60. async def open(self) -> bool:
  61. """Send open command."""
  62. self._is_opening = True
  63. self._is_closing = False
  64. return await self._send_multiple_commands(OPEN_KEYS)
  65. @update_after_operation
  66. async def close_up(self) -> bool:
  67. """Send close up command."""
  68. self._is_opening = False
  69. self._is_closing = True
  70. return await self._send_multiple_commands(CLOSE_UP_KEYS)
  71. @update_after_operation
  72. async def close_down(self) -> bool:
  73. """Send close down command."""
  74. self._is_opening = False
  75. self._is_closing = True
  76. return await self._send_multiple_commands(CLOSE_DOWN_KEYS)
  77. # The aim of this is to close to the nearest endpoint.
  78. # If we're open upwards we close up, if we're open downwards we close down.
  79. # If we're in the middle we default to close down as that seems to be the app's preference.
  80. @update_after_operation
  81. async def close(self) -> bool:
  82. """Send close command."""
  83. if self.get_position() > 50:
  84. return await self.close_up()
  85. else:
  86. return await self.close_down()
  87. def get_position(self) -> Any:
  88. """Return cached tilt (0-100) of Blind Tilt."""
  89. # To get actual tilt call update() first.
  90. return self._get_adv_value("tilt")
  91. async def get_basic_info(self) -> dict[str, Any] | None:
  92. """Get device basic settings."""
  93. if not (_data := await self._get_basic_info()):
  94. return None
  95. _tilt = max(min(_data[6], 100), 0)
  96. _moving = bool(_data[5] & 0b00000011)
  97. if _moving:
  98. _opening = bool(_data[5] & 0b00000010)
  99. _closing = not _opening and bool(_data[5] & 0b00000001)
  100. if _opening:
  101. _flag = bool(_data[5] & 0b00000001)
  102. _up = _flag if self._reverse else not _flag
  103. else:
  104. _up = _tilt < 50 if self._reverse else _tilt > 50
  105. return {
  106. "battery": _data[1],
  107. "firmware": _data[2] / 10.0,
  108. "light": bool(_data[4] & 0b00100000),
  109. "fault": bool(_data[4] & 0b00001000),
  110. "solarPanel": bool(_data[5] & 0b00001000),
  111. "calibration": bool(_data[5] & 0b00000100),
  112. "calibrated": bool(_data[5] & 0b00000100),
  113. "inMotion": _moving,
  114. "motionDirection": {
  115. "opening": _moving and _opening,
  116. "closing": _moving and _closing,
  117. "up": _moving and _up,
  118. "down": _moving and not _up,
  119. },
  120. "tilt": (100 - _tilt) if self._reverse else _tilt,
  121. "timers": _data[7],
  122. }
  123. async def get_extended_info_summary(self) -> dict[str, Any] | None:
  124. """Get extended info for all devices in chain."""
  125. _data = await self._send_command(key=COVER_EXT_SUM_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. self.ext_info_sum["device0"] = {
  133. "light": bool(_data[1] & 0b00100000),
  134. }
  135. return self.ext_info_sum