base_light.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. from __future__ import annotations
  2. import logging
  3. from abc import abstractmethod
  4. from typing import Any
  5. from ..helpers import create_background_task
  6. from ..models import SwitchBotAdvertisement
  7. from .device import SwitchbotDevice, SwitchbotOperationError, update_after_operation
  8. _LOGGER = logging.getLogger(__name__)
  9. class SwitchbotBaseLight(SwitchbotDevice):
  10. """Representation of a Switchbot light."""
  11. _effect_dict: dict[str, list[str]] = {}
  12. _set_brightness_command: str = ""
  13. _set_color_temp_command: str = ""
  14. _set_rgb_command: str = ""
  15. def __init__(self, *args: Any, **kwargs: Any) -> None:
  16. """Switchbot base light constructor."""
  17. super().__init__(*args, **kwargs)
  18. self._state: dict[str, Any] = {}
  19. @property
  20. def on(self) -> bool | None:
  21. """Return if light is on."""
  22. return self.is_on()
  23. @property
  24. def rgb(self) -> tuple[int, int, int] | None:
  25. """Return the current rgb value."""
  26. if "r" not in self._state or "g" not in self._state or "b" not in self._state:
  27. return None
  28. return self._state["r"], self._state["g"], self._state["b"]
  29. @property
  30. def color_temp(self) -> int | None:
  31. """Return the current color temp value."""
  32. return self._state.get("cw") or self.min_temp
  33. @property
  34. def brightness(self) -> int | None:
  35. """Return the current brightness value."""
  36. return self._get_adv_value("brightness") or 0
  37. @property
  38. @abstractmethod
  39. def color_mode(self) -> Any:
  40. """Return the current color mode."""
  41. raise NotImplementedError("Subclasses must implement color mode")
  42. @property
  43. def min_temp(self) -> int:
  44. """Return minimum color temp."""
  45. return 2700
  46. @property
  47. def max_temp(self) -> int:
  48. """Return maximum color temp."""
  49. return 6500
  50. @property
  51. def get_effect_list(self) -> list[str] | None:
  52. """Return the list of supported effects."""
  53. return list(self._effect_dict.keys()) if self._effect_dict else None
  54. def is_on(self) -> bool | None:
  55. """Return bulb state from cache."""
  56. return self._get_adv_value("isOn")
  57. def get_effect(self):
  58. """Return the current effect."""
  59. return self._get_adv_value("effect")
  60. @update_after_operation
  61. async def set_brightness(self, brightness: int) -> bool:
  62. """Set brightness."""
  63. assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
  64. hex_brightness = f"{brightness:02X}"
  65. self._check_function_support(self._set_brightness_command)
  66. result = await self._send_command(
  67. self._set_brightness_command.format(hex_brightness)
  68. )
  69. return self._check_command_result(result, 0, {1})
  70. @update_after_operation
  71. async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
  72. """Set color temp."""
  73. assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
  74. assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
  75. hex_data = f"{brightness:02X}{color_temp:04X}"
  76. self._check_function_support(self._set_color_temp_command)
  77. result = await self._send_command(self._set_color_temp_command.format(hex_data))
  78. return self._check_command_result(result, 0, {1})
  79. @update_after_operation
  80. async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
  81. """Set rgb."""
  82. assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
  83. assert 0 <= r <= 255, "r must be between 0 and 255"
  84. assert 0 <= g <= 255, "g must be between 0 and 255"
  85. assert 0 <= b <= 255, "b must be between 0 and 255"
  86. self._check_function_support(self._set_rgb_command)
  87. hex_data = f"{brightness:02X}{r:02X}{g:02X}{b:02X}"
  88. result = await self._send_command(self._set_rgb_command.format(hex_data))
  89. return self._check_command_result(result, 0, {1})
  90. @update_after_operation
  91. async def set_effect(self, effect: str) -> bool:
  92. """Set effect."""
  93. effect_template = self._effect_dict.get(effect)
  94. if not effect_template:
  95. raise SwitchbotOperationError(f"Effect {effect} not supported")
  96. result = await self._send_multiple_commands(effect_template)
  97. if result:
  98. self._override_state({"effect": effect})
  99. return result
  100. async def _send_multiple_commands(self, keys: list[str]) -> bool:
  101. """
  102. Send multiple commands to device.
  103. Since we current have no way to tell which command the device
  104. needs we send both.
  105. """
  106. final_result = False
  107. for key in keys:
  108. result = await self._send_command(key)
  109. final_result |= self._check_command_result(result, 0, {1})
  110. return final_result
  111. async def _get_multi_commands_results(
  112. self, commands: list[str]
  113. ) -> tuple[bytes, bytes] | None:
  114. """Check results after sending multiple commands."""
  115. if not (results := await self._get_basic_info_by_multi_commands(commands)):
  116. return None
  117. _version_info, _data = results[0], results[1]
  118. _LOGGER.debug(
  119. "version info: %s, data: %s, address: %s",
  120. _version_info,
  121. _data,
  122. self._device.address,
  123. )
  124. return _version_info, _data
  125. async def _get_basic_info_by_multi_commands(
  126. self, commands: list[str]
  127. ) -> list[bytes] | None:
  128. """Get device basic settings by sending multiple commands."""
  129. results = []
  130. for command in commands:
  131. if not (result := await self._get_basic_info(command)):
  132. return None
  133. results.append(result)
  134. return results
  135. class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
  136. """Representation of a Switchbot light."""
  137. def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
  138. """Update device data from advertisement."""
  139. current_state = self._get_adv_value("sequence_number")
  140. super().update_from_advertisement(advertisement)
  141. new_state = self._get_adv_value("sequence_number")
  142. _LOGGER.debug(
  143. "%s: update advertisement: %s (seq before: %s) (seq after: %s)",
  144. self.name,
  145. advertisement,
  146. current_state,
  147. new_state,
  148. )
  149. if current_state != new_state:
  150. create_background_task(self.update())