bulb.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. from __future__ import annotations
  2. import asyncio
  3. import logging
  4. from enum import Enum
  5. from typing import Any
  6. from switchbot.models import SwitchBotAdvertisement
  7. from .device import SwitchbotDevice
  8. REQ_HEADER = "570f"
  9. BULB_COMMMAND_HEADER = "4701"
  10. BULB_REQUEST = f"{REQ_HEADER}4801"
  11. BULB_COMMAND = f"{REQ_HEADER}{BULB_COMMMAND_HEADER}"
  12. # Bulb keys
  13. BULB_ON_KEY = f"{BULB_COMMAND}01"
  14. BULB_OFF_KEY = f"{BULB_COMMAND}02"
  15. RGB_BRIGHTNESS_KEY = f"{BULB_COMMAND}12"
  16. CW_BRIGHTNESS_KEY = f"{BULB_COMMAND}13"
  17. BRIGHTNESS_KEY = f"{BULB_COMMAND}14"
  18. RGB_KEY = f"{BULB_COMMAND}16"
  19. CW_KEY = f"{BULB_COMMAND}17"
  20. _LOGGER = logging.getLogger(__name__)
  21. class ColorMode(Enum):
  22. OFF = 0
  23. COLOR_TEMP = 1
  24. RGB = 2
  25. EFFECT = 3
  26. class SwitchbotBulb(SwitchbotDevice):
  27. """Representation of a Switchbot bulb."""
  28. def __init__(self, *args: Any, **kwargs: Any) -> None:
  29. """Switchbot bulb constructor."""
  30. super().__init__(*args, **kwargs)
  31. self._state: dict[str, Any] = {}
  32. @property
  33. def on(self) -> bool | None:
  34. """Return if bulb is on."""
  35. return self.is_on()
  36. @property
  37. def rgb(self) -> tuple[int, int, int] | None:
  38. """Return the current rgb value."""
  39. if "r" not in self._state or "g" not in self._state or "b" not in self._state:
  40. return None
  41. return self._state["r"], self._state["g"], self._state["b"]
  42. @property
  43. def color_temp(self) -> int | None:
  44. """Return the current color temp value."""
  45. return self._state.get("cw") or self.min_temp
  46. @property
  47. def brightness(self) -> int | None:
  48. """Return the current brightness value."""
  49. return self._get_adv_value("brightness") or 0
  50. @property
  51. def color_mode(self) -> ColorMode:
  52. """Return the current color mode."""
  53. return ColorMode(self._get_adv_value("color_mode") or 0)
  54. @property
  55. def min_temp(self) -> int:
  56. """Return minimum color temp."""
  57. return 2700
  58. @property
  59. def max_temp(self) -> int:
  60. """Return maximum color temp."""
  61. return 6500
  62. async def update(self) -> None:
  63. """Update state of device."""
  64. result = await self._sendcommand(BULB_REQUEST)
  65. self._update_state(result)
  66. async def turn_on(self) -> bool:
  67. """Turn device on."""
  68. result = await self._sendcommand(BULB_ON_KEY)
  69. self._update_state(result)
  70. return result[1] == 0x80
  71. async def turn_off(self) -> bool:
  72. """Turn device off."""
  73. result = await self._sendcommand(BULB_OFF_KEY)
  74. self._update_state(result)
  75. return result[1] == 0x00
  76. async def set_brightness(self, brightness: int) -> bool:
  77. """Set brightness."""
  78. assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
  79. result = await self._sendcommand(f"{BRIGHTNESS_KEY}{brightness:02X}")
  80. self._update_state(result)
  81. return result[1] == 0x80
  82. async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
  83. """Set color temp."""
  84. assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
  85. assert 2700 <= color_temp <= 6500, "Color Temp must be between 0 and 100"
  86. result = await self._sendcommand(
  87. f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
  88. )
  89. self._update_state(result)
  90. return result[1] == 0x80
  91. async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
  92. """Set rgb."""
  93. assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
  94. assert 0 <= r <= 255, "r must be between 0 and 255"
  95. assert 0 <= g <= 255, "g must be between 0 and 255"
  96. assert 0 <= b <= 255, "b must be between 0 and 255"
  97. result = await self._sendcommand(
  98. f"{RGB_BRIGHTNESS_KEY}{brightness:02X}{r:02X}{g:02X}{b:02X}"
  99. )
  100. self._update_state(result)
  101. return result[1] == 0x80
  102. def is_on(self) -> bool | None:
  103. """Return bulb state from cache."""
  104. return self._get_adv_value("isOn")
  105. def _update_state(self, result: bytes) -> None:
  106. """Update device state."""
  107. if len(result) < 10:
  108. return
  109. self._state["r"] = result[3]
  110. self._state["g"] = result[4]
  111. self._state["b"] = result[5]
  112. self._state["cw"] = int(result[6:8].hex(), 16)
  113. _LOGGER.debug(
  114. "%s: Bulb update state: %s = %s", self.name, result.hex(), self._state
  115. )
  116. self._fire_callbacks()
  117. def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
  118. """Update device data from advertisement."""
  119. current_state = self._get_adv_value("sequence_number")
  120. super().update_from_advertisement(advertisement)
  121. new_state = self._get_adv_value("sequence_number")
  122. _LOGGER.debug(
  123. "%s: Bulb update advertisement: %s (seq before: %s) (seq after: %s)",
  124. self.name,
  125. advertisement,
  126. current_state,
  127. new_state,
  128. )
  129. if current_state != new_state:
  130. asyncio.ensure_future(self.update())