1
0

light_strip.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. from __future__ import annotations
  2. from typing import Any
  3. from ..const import SwitchbotModel
  4. from ..const.light import (
  5. ColorMode,
  6. RGBICStripLightColorMode,
  7. RGBICWWCeilingLightColorMode,
  8. StripLightColorMode,
  9. )
  10. from .base_light import SwitchbotSequenceBaseLight
  11. from .device import SwitchbotEncryptedDevice, update_after_operation
  12. # Private mapping from device-specific color modes to original ColorMode enum
  13. _STRIP_LIGHT_COLOR_MODE_MAP = {
  14. StripLightColorMode.RGB: ColorMode.RGB,
  15. StripLightColorMode.SCENE: ColorMode.EFFECT,
  16. StripLightColorMode.MUSIC: ColorMode.EFFECT,
  17. StripLightColorMode.CONTROLLER: ColorMode.EFFECT,
  18. StripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
  19. StripLightColorMode.UNKNOWN: ColorMode.OFF,
  20. }
  21. _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP = {
  22. RGBICStripLightColorMode.SEGMENTED: ColorMode.EFFECT,
  23. RGBICStripLightColorMode.RGB: ColorMode.RGB,
  24. RGBICStripLightColorMode.SCENE: ColorMode.EFFECT,
  25. RGBICStripLightColorMode.MUSIC: ColorMode.EFFECT,
  26. RGBICStripLightColorMode.CONTROLLER: ColorMode.EFFECT,
  27. RGBICStripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
  28. RGBICStripLightColorMode.EFFECT: ColorMode.EFFECT,
  29. RGBICStripLightColorMode.UNKNOWN: ColorMode.OFF,
  30. }
  31. _RGBICWW_CEILING_LIGHT_COLOR_MODE_MAP = {
  32. RGBICWWCeilingLightColorMode.SEGMENTED: ColorMode.EFFECT,
  33. RGBICWWCeilingLightColorMode.COLOR: ColorMode.RGB,
  34. RGBICWWCeilingLightColorMode.SCENE: ColorMode.EFFECT,
  35. RGBICWWCeilingLightColorMode.MUSIC: ColorMode.EFFECT,
  36. RGBICWWCeilingLightColorMode.CONTROLLER: ColorMode.EFFECT,
  37. RGBICWWCeilingLightColorMode.WARMWHITE: ColorMode.COLOR_TEMP,
  38. RGBICWWCeilingLightColorMode.EFFECT: ColorMode.EFFECT,
  39. RGBICWWCeilingLightColorMode.UNKNOWN: ColorMode.OFF,
  40. }
  41. LIGHT_STRIP_CONTROL_HEADER = "570F4901"
  42. COMMON_EFFECTS = {
  43. "christmas": [
  44. "570F49070200033C01",
  45. "570F490701000600009902006D0EFF0021",
  46. "570F490701000603009902006D0EFF0021",
  47. ],
  48. "halloween": ["570F49070200053C04", "570F490701000300FF6A009E00ED00EA0F"],
  49. "sunset": [
  50. "570F49070200033C3C",
  51. "570F490701000900FF9000ED8C04DD5800",
  52. "570F490701000903FF2E008E0B004F0500",
  53. "570F4907010009063F0010270056140033",
  54. ],
  55. "vitality": [
  56. "570F49070200053C02",
  57. "570F490701000600C5003FD9530AEC9800",
  58. "570F490701000603FFDF0000895500468B",
  59. ],
  60. "flashing": [
  61. "570F49070200053C02",
  62. "570F4907010006000000FF00FF00FF0000",
  63. "570F490701000603FFFF0000FFFFA020F0",
  64. ],
  65. "strobe": ["570F49070200043C02", "570F490701000300FF00E19D70FFFF0515"],
  66. "fade": [
  67. "570F49070200043C04",
  68. "570F490701000500FF5481FF00E19D70FF",
  69. "570F490701000503FF0515FF7FEB",
  70. ],
  71. "smooth": [
  72. "570F49070200033C02",
  73. "570F4907010007000036FC00F6FF00ED13",
  74. "570F490701000703F6FF00FF8300FF0800",
  75. "570F490701000706FF00E1",
  76. ],
  77. "forest": [
  78. "570F49070200033C06",
  79. "570F490701000400006400228B223CB371",
  80. "570F49070100040390EE90",
  81. ],
  82. "ocean": [
  83. "570F49070200033C06",
  84. "570F4907010007004400FF0061FF007BFF",
  85. "570F490701000703009DFF00B2FF00CBFF",
  86. "570F49070100070600E9FF",
  87. ],
  88. "autumn": [
  89. "570F49070200043C05",
  90. "570F490701000700D10035922D13A16501",
  91. "570F490701000703AB9100DD8C00F4AA29",
  92. "570F490701000706E8D000",
  93. ],
  94. "cool": [
  95. "570F49070200043C04",
  96. "570F490701000600001A63006C9A00468B",
  97. "570F490701000603009DA50089BE4378B6",
  98. ],
  99. "flow": [
  100. "570F49070200033C02",
  101. "570F490701000600FF00D8E100FFAA00FF",
  102. "570F4907010006037F00FF5000FF1900FF",
  103. ],
  104. "relax": [
  105. "570F49070200033C03",
  106. "570F490701000400FF8C00FF7200FF1D00",
  107. "570F490701000403FF5500",
  108. ],
  109. "modern": [
  110. "570F49070200043C03",
  111. "570F49070100060089231A5F8969829E5A",
  112. "570F490701000603BCB05EEDBE5AFF9D60",
  113. ],
  114. "rose": [
  115. "570F49070200043C04",
  116. "570F490701000500FF1969BC215F7C0225",
  117. "570F490701000503600C2B35040C",
  118. ],
  119. }
  120. RGBIC_EFFECTS = {
  121. "romance": [
  122. "570F490D01350100FF10EE",
  123. "570F490D0363",
  124. ],
  125. "energy": [
  126. "570F490D01000300ED070F34FF14FFE114",
  127. "570F490D03FA",
  128. ],
  129. "heartbeat": [
  130. "570F490D01020400FFDEADFE90FDFF9E3D",
  131. "570F490D01020403FCBAFD",
  132. "570F490D03FA",
  133. ],
  134. "party": [
  135. "570F490D01030400FF8A47FF524DFF4DEE",
  136. "570F490D010304034DFF8C",
  137. "570F490D03FA",
  138. ],
  139. "dynamic": [
  140. "570F490D010403004DFFFB4DFF4FFFBF4D",
  141. "570F490D03FA",
  142. ],
  143. "mystery": [
  144. "570F490D01050300F660F6F6D460C6F660",
  145. "570F490D03FA",
  146. ],
  147. "lightning": [
  148. "570F490D01340100FFD700",
  149. "570F490D03FA",
  150. ],
  151. "rock": [
  152. "570F490D01090300B0F6606864FCFFBC3D",
  153. "570F490D03FA",
  154. ],
  155. "starlight": [
  156. "570F490D010A0100FF8C00",
  157. "570F490D0363",
  158. ],
  159. "valentine_day": [
  160. "570F490D010C0300FDE0FFFFCC8AD7FF8A",
  161. "570F490D03FA",
  162. ],
  163. "dream": [
  164. "570F490D010E0300A3E5FF73F019FFA8E5",
  165. "570F490D03FA",
  166. ],
  167. "alarm": [
  168. "570F490D013E0100FF0000",
  169. "570F490D03FA",
  170. ],
  171. "fireworks": [
  172. "570F490D01110300FFAA33FFE233FF5CDF",
  173. "570F490D03FA",
  174. ],
  175. "waves": [
  176. "570F490D013D01001E90FF",
  177. "570F490D03FA",
  178. ],
  179. "christmas": [
  180. "570F490D01380400DC143C228B22DAA520",
  181. "570F490D0363",
  182. "570F490D0138040332CD32",
  183. "570F490D0363",
  184. ],
  185. "rainbow": [
  186. "570F490D01160600FF0000FF7F00FFFF00",
  187. "570F490D03FA",
  188. "570F490D0116060300FF000000FF9400D3",
  189. "570F490D03FA",
  190. ],
  191. "game": [
  192. "570F490D011A0400D05CFF668FFFFFEFD5",
  193. "570F490D0363",
  194. "570F490D011A0403FFC55C",
  195. "570F490D0363",
  196. ],
  197. "halloween": [
  198. "570F490D01320300FF8C009370DB32CD32",
  199. "570F490D0364",
  200. ],
  201. "meditation": [
  202. "570F490D013502001E90FF9370DB",
  203. "570F490D0364",
  204. ],
  205. "starlit_sky": [
  206. "570F490D010D010099C8FF",
  207. "570F490D0364",
  208. ],
  209. "sleep": [
  210. "570F490D01370300FF8C002E4E3E3E3E5E",
  211. "570F490D0364",
  212. ],
  213. "movie": [
  214. "570F490D013602001919704B0082",
  215. "570F490D0364",
  216. ],
  217. "sunrise": [
  218. "570F490D013F0200FFD700FF4500",
  219. "570F490D03FA",
  220. "570F490D03FA",
  221. ],
  222. "sunset": [
  223. "570F490D01390300FF4500FFA500483D8B",
  224. "570F490D0363",
  225. "570F490D0363",
  226. ],
  227. "new_year": [
  228. "570F490D013F0300FF0000FFD700228B22",
  229. "570F490D0364",
  230. ],
  231. "cherry_blossom": [
  232. "570F490D01400200FFB3C1FF69B4",
  233. "570F490D0364",
  234. ],
  235. }
  236. class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
  237. """Representation of a Switchbot light strip."""
  238. _effect_dict = COMMON_EFFECTS
  239. _turn_on_command = f"{LIGHT_STRIP_CONTROL_HEADER}01"
  240. _turn_off_command = f"{LIGHT_STRIP_CONTROL_HEADER}02"
  241. _set_rgb_command = f"{LIGHT_STRIP_CONTROL_HEADER}12{{}}"
  242. _set_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}11{{}}"
  243. _set_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}14{{}}"
  244. _get_basic_info_command = ["570003", "570f4A01"]
  245. @property
  246. def color_modes(self) -> set[ColorMode]:
  247. """Return the supported color modes."""
  248. return {ColorMode.RGB}
  249. @property
  250. def color_mode(self) -> ColorMode:
  251. """Return the current color mode."""
  252. device_mode = StripLightColorMode(self._get_adv_value("color_mode") or 10)
  253. return _STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
  254. async def get_basic_info(self) -> dict[str, Any] | None:
  255. """Get device basic settings."""
  256. if not (
  257. res := await self._get_multi_commands_results(self._get_basic_info_command)
  258. ):
  259. return None
  260. _version_info, _data = res
  261. self._state["r"] = _data[3]
  262. self._state["g"] = _data[4]
  263. self._state["b"] = _data[5]
  264. self._state["cw"] = int.from_bytes(_data[7:9], "big")
  265. return {
  266. "isOn": bool(_data[1] & 0b10000000),
  267. "brightness": _data[2] & 0b01111111,
  268. "r": self._state["r"],
  269. "g": self._state["g"],
  270. "b": self._state["b"],
  271. "cw": self._state["cw"],
  272. "color_mode": _data[10] & 0b00001111,
  273. "firmware": _version_info[2] / 10.0,
  274. }
  275. class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
  276. """Support for switchbot strip light3 and floor lamp."""
  277. _model = SwitchbotModel.STRIP_LIGHT_3
  278. @property
  279. def color_modes(self) -> set[ColorMode]:
  280. """Return the supported color modes."""
  281. return {ColorMode.RGB, ColorMode.COLOR_TEMP}
  282. class SwitchbotCandleWarmerLamp(SwitchbotEncryptedDevice, SwitchbotLightStrip):
  283. """Support for Switchbot Candle Warmer Lamp."""
  284. _model = SwitchbotModel.CANDLE_WARMER_LAMP
  285. _effect_dict = {}
  286. _set_rgb_command = ""
  287. _set_color_temp_command = ""
  288. @property
  289. def color_modes(self) -> set[ColorMode]:
  290. """Return the supported color modes."""
  291. return {ColorMode.BRIGHTNESS}
  292. @property
  293. def color_mode(self) -> ColorMode:
  294. """Return the current color mode."""
  295. return ColorMode.BRIGHTNESS
  296. async def get_basic_info(self) -> dict[str, Any] | None:
  297. """Get device basic settings."""
  298. if not (
  299. res := await self._get_multi_commands_results(self._get_basic_info_command)
  300. ):
  301. return None
  302. _version_info, _data = res
  303. return {
  304. "isOn": bool(_data[1] & 0b10000000),
  305. "brightness": _data[2] & 0b01111111,
  306. "firmware": _version_info[2] / 10.0,
  307. }
  308. class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
  309. """Support for Switchbot RGBIC lights."""
  310. _model = SwitchbotModel.RGBICWW_STRIP_LIGHT
  311. _effect_dict = RGBIC_EFFECTS
  312. @property
  313. def color_modes(self) -> set[ColorMode]:
  314. """Return the supported color modes."""
  315. return {ColorMode.RGB, ColorMode.COLOR_TEMP}
  316. @property
  317. def color_mode(self) -> ColorMode:
  318. """Return the current color mode."""
  319. device_mode = RGBICStripLightColorMode(self._get_adv_value("color_mode") or 10)
  320. return _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
  321. class SwitchbotPermanentOutdoorLight(SwitchbotRgbicLight):
  322. """Support for Switchbot Permanent Outdoor Light."""
  323. _model = SwitchbotModel.PERMANENT_OUTDOOR_LIGHT
  324. class SwitchbotRgbicNeonLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
  325. """Support for Switchbot RGBIC Neon lights."""
  326. _model = SwitchbotModel.RGBIC_NEON_ROPE_LIGHT
  327. _effect_dict = RGBIC_EFFECTS
  328. @property
  329. def color_modes(self) -> set[ColorMode]:
  330. """Return the supported color modes."""
  331. return {ColorMode.RGB}
  332. @property
  333. def color_mode(self) -> ColorMode:
  334. """Return the current color mode."""
  335. return ColorMode.RGB
  336. class SwitchbotRgbicwwCeilingLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
  337. """Support for Switchbot RGBICWW Ceiling Light (warm-white + color sub-lights)."""
  338. _model = SwitchbotModel.RGBICWW_CEILING_LIGHT
  339. _effect_dict = RGBIC_EFFECTS
  340. # Color sub-light commands (sub_cmd 0x12 brightness+RGB, 0x14 brightness)
  341. _set_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}14{{}}"
  342. _set_rgb_command = f"{LIGHT_STRIP_CONTROL_HEADER}12{{}}"
  343. # Main (warm-white) sub-light commands (sub_cmd 0x09, 0x10, 0x11)
  344. _set_main_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}09{{}}"
  345. _set_main_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}10{{}}"
  346. _set_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}11{{}}"
  347. # Sub-light power control: 0x49 0x01 <onoff> <selector>.
  348. # onoff = 0x01 (on)/0x02 (off)/0x03 (toggle); here we always send 0x01 and
  349. # let the selector drive the individual sub-lights.
  350. # selector: bit7=1 (specify), bits[3:2]=color state, bits[1:0]=white state;
  351. # state encoding 00=keep, 01=on, 02=off, 03=toggle.
  352. _turn_on_main_command = f"{LIGHT_STRIP_CONTROL_HEADER}0181"
  353. _turn_off_main_command = f"{LIGHT_STRIP_CONTROL_HEADER}0182"
  354. _turn_on_color_command = f"{LIGHT_STRIP_CONTROL_HEADER}0184"
  355. _turn_off_color_command = f"{LIGHT_STRIP_CONTROL_HEADER}0188"
  356. @property
  357. def color_modes(self) -> set[ColorMode]:
  358. """Return the supported color modes (color sub-light)."""
  359. return {ColorMode.RGB, ColorMode.COLOR_TEMP}
  360. @property
  361. def color_mode(self) -> ColorMode:
  362. """Return the current color mode."""
  363. device_mode = RGBICWWCeilingLightColorMode(
  364. self._get_adv_value("color_mode") or 10
  365. )
  366. return _RGBICWW_CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
  367. @property
  368. def is_main_on(self) -> bool | None:
  369. """Return whether the main (warm-white) sub-light is on."""
  370. return self._get_adv_value("main_isOn")
  371. @property
  372. def main_brightness(self) -> int:
  373. """Return the main (warm-white) sub-light brightness 0-100."""
  374. return self._get_adv_value("main_brightness") or 0
  375. @update_after_operation
  376. async def turn_on_main(self) -> bool:
  377. """Turn the main (warm-white) sub-light on."""
  378. result = await self._send_command(self._turn_on_main_command)
  379. return self._check_command_result(result, 0, {1})
  380. @update_after_operation
  381. async def turn_off_main(self) -> bool:
  382. """Turn the main (warm-white) sub-light off."""
  383. result = await self._send_command(self._turn_off_main_command)
  384. return self._check_command_result(result, 0, {1})
  385. @update_after_operation
  386. async def turn_on_color(self) -> bool:
  387. """Turn the color sub-light on."""
  388. result = await self._send_command(self._turn_on_color_command)
  389. return self._check_command_result(result, 0, {1})
  390. @update_after_operation
  391. async def turn_off_color(self) -> bool:
  392. """Turn the color sub-light off."""
  393. result = await self._send_command(self._turn_off_color_command)
  394. return self._check_command_result(result, 0, {1})
  395. @update_after_operation
  396. async def set_main_brightness(self, brightness: int) -> bool:
  397. """Set the main (warm-white) sub-light brightness (sub_cmd 0x09)."""
  398. self._validate_brightness(brightness)
  399. hex_brightness = f"{brightness:02X}"
  400. result = await self._send_command(
  401. self._set_main_brightness_command.format(hex_brightness)
  402. )
  403. return self._check_command_result(result, 0, {1})
  404. @update_after_operation
  405. async def set_main_color_temp(self, color_temp: int) -> bool:
  406. """Set the main (warm-white) sub-light color temperature (sub_cmd 0x10)."""
  407. self._validate_color_temp(color_temp)
  408. hex_data = f"{color_temp:04X}"
  409. result = await self._send_command(
  410. self._set_main_color_temp_command.format(hex_data)
  411. )
  412. return self._check_command_result(result, 0, {1})
  413. async def get_basic_info(self) -> dict[str, Any] | None:
  414. """
  415. Read the RGB color (and color temp) over GATT.
  416. Power, brightness and color mode are taken from the advertisement
  417. (which tracks them reliably). The device's 0x4A01 status response does
  418. NOT carry a usable color power state - byte 1 stays 0 even when the
  419. color sub-light is on - so those fields are deliberately not returned
  420. here, otherwise update() would clobber the correct advertised values.
  421. """
  422. if not (
  423. res := await self._get_multi_commands_results(self._get_basic_info_command)
  424. ):
  425. return None
  426. _version_info, _data = res
  427. self._state["r"] = _data[3]
  428. self._state["g"] = _data[4]
  429. self._state["b"] = _data[5]
  430. self._state["cw"] = int.from_bytes(_data[7:9], "big")
  431. return {
  432. "r": self._state["r"],
  433. "g": self._state["g"],
  434. "b": self._state["b"],
  435. "firmware": _version_info[2] / 10.0,
  436. }