base_light.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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) 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. @staticmethod
  61. def _validate_brightness(brightness: int) -> None:
  62. if not 0 <= brightness <= 100:
  63. raise ValueError("Brightness must be between 0 and 100")
  64. @staticmethod
  65. def _validate_rgb(r: int, g: int, b: int) -> None:
  66. if not 0 <= r <= 255:
  67. raise ValueError("r must be between 0 and 255")
  68. if not 0 <= g <= 255:
  69. raise ValueError("g must be between 0 and 255")
  70. if not 0 <= b <= 255:
  71. raise ValueError("b must be between 0 and 255")
  72. @staticmethod
  73. def _validate_color_temp(color_temp: int) -> None:
  74. if not 2700 <= color_temp <= 6500:
  75. raise ValueError("Color Temp must be between 2700 and 6500")
  76. @update_after_operation
  77. async def set_brightness(self, brightness: int) -> bool:
  78. """Set brightness."""
  79. self._validate_brightness(brightness)
  80. hex_brightness = f"{brightness:02X}"
  81. self._check_function_support(self._set_brightness_command)
  82. result = await self._send_command(
  83. self._set_brightness_command.format(hex_brightness)
  84. )
  85. return self._check_command_result(result, 0, {1})
  86. @update_after_operation
  87. async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
  88. """Set color temp."""
  89. self._validate_brightness(brightness)
  90. self._validate_color_temp(color_temp)
  91. hex_data = f"{brightness:02X}{color_temp:04X}"
  92. self._check_function_support(self._set_color_temp_command)
  93. result = await self._send_command(self._set_color_temp_command.format(hex_data))
  94. return self._check_command_result(result, 0, {1})
  95. @update_after_operation
  96. async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
  97. """Set rgb."""
  98. self._validate_brightness(brightness)
  99. self._validate_rgb(r, g, b)
  100. self._check_function_support(self._set_rgb_command)
  101. hex_data = f"{brightness:02X}{r:02X}{g:02X}{b:02X}"
  102. result = await self._send_command(self._set_rgb_command.format(hex_data))
  103. return self._check_command_result(result, 0, {1})
  104. @update_after_operation
  105. async def set_effect(self, effect: str) -> bool:
  106. """Set effect."""
  107. effect_template = self._effect_dict.get(effect.lower())
  108. if not effect_template:
  109. raise SwitchbotOperationError(f"Effect {effect} not supported")
  110. result = await self._send_multiple_commands(effect_template)
  111. if result:
  112. self._override_state({"effect": effect})
  113. return result
  114. async def _get_multi_commands_results(
  115. self, commands: list[str]
  116. ) -> tuple[bytes, bytes] | None:
  117. """Check results after sending multiple commands."""
  118. if not (results := await self._get_basic_info_by_multi_commands(commands)):
  119. return None
  120. _version_info, _data = results[0], results[1]
  121. _LOGGER.debug(
  122. "version info: %s, data: %s, address: %s",
  123. _version_info,
  124. _data,
  125. self._device.address,
  126. )
  127. return _version_info, _data
  128. async def _get_basic_info_by_multi_commands(
  129. self, commands: list[str]
  130. ) -> list[bytes] | None:
  131. """Get device basic settings by sending multiple commands."""
  132. results = []
  133. for command in commands:
  134. result = await self._send_command(command)
  135. if not self._check_command_result(result, 0, {1}):
  136. return None
  137. results.append(result)
  138. return results
  139. class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
  140. """Representation of a Switchbot light."""
  141. def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
  142. """Update device data from advertisement."""
  143. current_state = self._get_adv_value("sequence_number")
  144. super().update_from_advertisement(advertisement)
  145. new_state = self._get_adv_value("sequence_number")
  146. _LOGGER.debug(
  147. "%s: update advertisement: %s (seq before: %s) (seq after: %s)",
  148. self.name,
  149. advertisement,
  150. current_state,
  151. new_state,
  152. )
  153. if current_state != new_state:
  154. create_background_task(self.update())