blind_tilt.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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 REQ_HEADER, update_after_operation
  6. from .curtain import CURTAIN_EXT_SUM_KEY, SwitchbotCurtain
  7. _LOGGER = logging.getLogger(__name__)
  8. BLIND_COMMAND = "4501"
  9. OPEN_KEYS = [
  10. f"{REQ_HEADER}{BLIND_COMMAND}010132",
  11. f"{REQ_HEADER}{BLIND_COMMAND}05ff32",
  12. ]
  13. CLOSE_DOWN_KEYS = [
  14. f"{REQ_HEADER}{BLIND_COMMAND}010100",
  15. f"{REQ_HEADER}{BLIND_COMMAND}05ff00",
  16. ]
  17. CLOSE_UP_KEYS = [
  18. f"{REQ_HEADER}{BLIND_COMMAND}010164",
  19. f"{REQ_HEADER}{BLIND_COMMAND}05ff64",
  20. ]
  21. class SwitchbotBlindTilt(SwitchbotCurtain):
  22. """Representation of a Switchbot Blind Tilt."""
  23. # The position of the blind is saved returned with 0 = closed down, 50 = open and 100 = closed up.
  24. # This is independent of the calibration of the blind.
  25. # The parameter 'reverse_mode' reverse these values,
  26. # if 'reverse_mode' = True, position = 0 equals closed up
  27. # and position = 100 equals closed down. The parameter is default set to False so that
  28. # the definition of position is the same as in Home Assistant.
  29. # This is opposite to the base class so needs to be overwritten.
  30. def __init__(self, *args: Any, **kwargs: Any) -> None:
  31. """Switchbot Blind Tilt/woBlindTilt constructor."""
  32. super().__init__(*args, **kwargs)
  33. self._reverse: bool = kwargs.pop("reverse_mode", False)
  34. @update_after_operation
  35. async def open(self) -> bool:
  36. """Send open command."""
  37. return await self._send_multiple_commands(OPEN_KEYS)
  38. @update_after_operation
  39. async def close_up(self) -> bool:
  40. """Send close up command."""
  41. return await self._send_multiple_commands(CLOSE_UP_KEYS)
  42. @update_after_operation
  43. async def close_down(self) -> bool:
  44. """Send close down command."""
  45. return await self._send_multiple_commands(CLOSE_DOWN_KEYS)
  46. # The aim of this is to close to the nearest endpoint.
  47. # If we're open upwards we close up, if we're open downwards we close down.
  48. # If we're in the middle we default to close down as that seems to be the app's preference.
  49. @update_after_operation
  50. async def close(self) -> bool:
  51. """Send close command."""
  52. if self.get_position() > 50:
  53. return await self.close_up()
  54. else:
  55. return await self.close_down()
  56. def get_position(self) -> Any:
  57. """Return cached tilt (0-100) of Blind Tilt."""
  58. # To get actual tilt call update() first.
  59. return self._get_adv_value("tilt")
  60. async def get_basic_info(self) -> dict[str, Any] | None:
  61. """Get device basic settings."""
  62. if not (_data := await self._get_basic_info()):
  63. return None
  64. _tilt = max(min(_data[6], 100), 0)
  65. _moving = bool(_data[5] & 0b00000011)
  66. if _moving:
  67. _opening = bool(_data[5] & 0b00000010)
  68. _closing = not _opening and bool(_data[5] & 0b00000001)
  69. if _opening:
  70. _flag = bool(_data[5] & 0b00000001)
  71. _up = _flag if self._reverse else not _flag
  72. else:
  73. _up = _tilt < 50 if self._reverse else _tilt > 50
  74. return {
  75. "battery": _data[1],
  76. "firmware": _data[2] / 10.0,
  77. "light": bool(_data[4] & 0b00100000),
  78. "fault": bool(_data[4] & 0b00001000),
  79. "solarPanel": bool(_data[5] & 0b00001000),
  80. "calibration": bool(_data[5] & 0b00000100),
  81. "calibrated": bool(_data[5] & 0b00000100),
  82. "inMotion": _moving,
  83. "motionDirection": {
  84. "opening": _moving and _opening,
  85. "closing": _moving and _closing,
  86. "up": _moving and _up,
  87. "down": _moving and not _up,
  88. },
  89. "tilt": (100 - _tilt) if self._reverse else _tilt,
  90. "timers": _data[7],
  91. }
  92. async def get_extended_info_summary(self) -> dict[str, Any] | None:
  93. """Get basic info for all devices in chain."""
  94. _data = await self._send_command(key=CURTAIN_EXT_SUM_KEY)
  95. if not _data:
  96. _LOGGER.error("%s: Unsuccessful, no result from device", self.name)
  97. return None
  98. if _data in (b"\x07", b"\x00"):
  99. _LOGGER.error("%s: Unsuccessful, please try again", self.name)
  100. return None
  101. self.ext_info_sum["device0"] = {
  102. "light": bool(_data[1] & 0b00100000),
  103. }
  104. return self.ext_info_sum