light.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. """
  2. Setup for different kinds of Tuya light devices
  3. """
  4. import logging
  5. from struct import pack, unpack
  6. import homeassistant.util.color as color_util
  7. from homeassistant.components.light import (
  8. ATTR_BRIGHTNESS,
  9. ATTR_COLOR_TEMP_KELVIN,
  10. ATTR_EFFECT,
  11. ATTR_HS_COLOR,
  12. ATTR_WHITE,
  13. ColorMode,
  14. LightEntity,
  15. LightEntityFeature,
  16. )
  17. from .device import TuyaLocalDevice
  18. from .helpers.config import async_tuya_setup_platform
  19. from .helpers.device_config import TuyaEntityConfig
  20. from .helpers.mixin import TuyaLocalEntity
  21. _LOGGER = logging.getLogger(__name__)
  22. async def async_setup_entry(hass, config_entry, async_add_entities):
  23. config = {**config_entry.data, **config_entry.options}
  24. await async_tuya_setup_platform(
  25. hass,
  26. async_add_entities,
  27. config,
  28. "light",
  29. TuyaLocalLight,
  30. )
  31. class TuyaLocalLight(TuyaLocalEntity, LightEntity):
  32. """Representation of a Tuya WiFi-connected light."""
  33. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  34. """
  35. Initialize the light.
  36. Args:
  37. device (TuyaLocalDevice): The device API instance.
  38. config (TuyaEntityConfig): The configuration for this entity.
  39. """
  40. super().__init__()
  41. dps_map = self._init_begin(device, config)
  42. self._switch_dps = dps_map.pop("switch", None)
  43. self._brightness_dps = dps_map.pop("brightness", None)
  44. self._color_mode_dps = dps_map.pop("color_mode", None)
  45. self._color_temp_dps = dps_map.pop("color_temp", None)
  46. self._rgbhsv_dps = dps_map.pop("rgbhsv", None)
  47. self._effect_dps = dps_map.pop("effect", None)
  48. self._init_end(dps_map)
  49. # Set min and max color temp
  50. if self._color_temp_dps:
  51. m = self._color_temp_dps._find_map_for_dps(0)
  52. if m:
  53. tr = m.get("target_range")
  54. if tr:
  55. self._attr_min_color_temp_kelvin = tr.get("min")
  56. self._attr_max_color_temp_kelvin = tr.get("max")
  57. @property
  58. def supported_color_modes(self):
  59. """Return the supported color modes for this light."""
  60. if self._color_mode_dps:
  61. return {
  62. ColorMode(mode)
  63. for mode in self._color_mode_dps.values(self._device)
  64. if mode and hasattr(ColorMode, mode.upper())
  65. }
  66. else:
  67. try:
  68. mode = ColorMode(self.color_mode)
  69. if mode and mode != ColorMode.UNKNOWN:
  70. return {mode}
  71. except ValueError:
  72. _LOGGER.warning(
  73. "%s/%s: Unrecognised color mode %s ignored",
  74. self._config._device.config,
  75. self.name or "light",
  76. self.color_mode,
  77. )
  78. return set()
  79. @property
  80. def supported_features(self):
  81. """Return the supported features for this light."""
  82. if self.effect_list:
  83. return LightEntityFeature.EFFECT
  84. else:
  85. return LightEntityFeature(0)
  86. @property
  87. def color_mode(self):
  88. """Return the color mode of the light"""
  89. from_dp = self.raw_color_mode
  90. if from_dp:
  91. return from_dp
  92. if self._rgbhsv_dps:
  93. return ColorMode.HS
  94. elif self._color_temp_dps:
  95. return ColorMode.COLOR_TEMP
  96. elif self._brightness_dps:
  97. return ColorMode.BRIGHTNESS
  98. elif self._switch_dps:
  99. return ColorMode.ONOFF
  100. else:
  101. return ColorMode.UNKNOWN
  102. @property
  103. def raw_color_mode(self):
  104. """Return the color_mode as set from the dps."""
  105. if self._color_mode_dps:
  106. mode = self._color_mode_dps.get_value(self._device)
  107. if mode and hasattr(ColorMode, mode.upper()):
  108. return ColorMode(mode)
  109. @property
  110. def color_temp_kelvin(self):
  111. """Return the color temperature in kelvin."""
  112. if self._color_temp_dps:
  113. return self._color_temp_dps.get_value(self._device)
  114. @property
  115. def is_on(self):
  116. """Return the current state."""
  117. if self._switch_dps:
  118. return self._switch_dps.get_value(self._device)
  119. elif self._brightness_dps:
  120. b = self.brightness
  121. return isinstance(b, int) and b > 0
  122. else:
  123. # There shouldn't be lights without control, but if there are,
  124. # assume always on if they are responding
  125. return self.available
  126. @property
  127. def brightness(self):
  128. """Get the current brightness of the light"""
  129. if self.raw_color_mode == ColorMode.HS and self._rgbhsv_dps:
  130. return self._hsv_brightness
  131. return self._white_brightness
  132. @property
  133. def _white_brightness(self):
  134. if self._brightness_dps:
  135. r = self._brightness_dps.range(self._device)
  136. val = self._brightness_dps.get_value(self._device)
  137. if r:
  138. val = color_util.value_to_brightness(r, val)
  139. return val
  140. @property
  141. def _unpacked_rgbhsv(self):
  142. """Get the unpacked rgbhsv data"""
  143. if self._rgbhsv_dps:
  144. color = self._rgbhsv_dps.decoded_value(self._device)
  145. fmt = self._rgbhsv_dps.format
  146. if fmt and color:
  147. vals = unpack(fmt.get("format"), color)
  148. idx = 0
  149. rgbhsv = {}
  150. for v in vals:
  151. # HA range: s = 0-100, rgbv = 0-255, h = 0-360
  152. n = fmt["names"][idx]
  153. r = fmt["ranges"][idx]
  154. mx = r["max"]
  155. scale = 1
  156. if n == "h":
  157. scale = 360 / mx
  158. elif n == "s":
  159. scale = 100 / mx
  160. else:
  161. scale = 255 / mx
  162. rgbhsv[n] = round(scale * v)
  163. idx += 1
  164. return rgbhsv
  165. @property
  166. def _hsv_brightness(self):
  167. """Get the colour mode brightness from the light"""
  168. rgbhsv = self._unpacked_rgbhsv
  169. if rgbhsv:
  170. return rgbhsv.get("v", self._white_brightness)
  171. return self._white_brightness
  172. @property
  173. def hs_color(self):
  174. """Get the current hs color of the light"""
  175. rgbhsv = self._unpacked_rgbhsv
  176. if rgbhsv:
  177. if "h" in rgbhsv and "s" in rgbhsv:
  178. hs = (rgbhsv["h"], rgbhsv["s"])
  179. else:
  180. r = rgbhsv.get("r")
  181. g = rgbhsv.get("g")
  182. b = rgbhsv.get("b")
  183. hs = color_util.color_RGB_to_hs(r, g, b)
  184. return hs
  185. @property
  186. def effect_list(self):
  187. """Return the list of valid effects for the light"""
  188. if self._effect_dps:
  189. return self._effect_dps.values(self._device)
  190. elif self._color_mode_dps:
  191. return [
  192. effect
  193. for effect in self._color_mode_dps.values(self._device)
  194. if effect and not hasattr(ColorMode, effect.upper())
  195. ]
  196. @property
  197. def effect(self):
  198. """Return the current effect setting of this light"""
  199. if self._effect_dps:
  200. return self._effect_dps.get_value(self._device)
  201. elif self._color_mode_dps:
  202. mode = self._color_mode_dps.get_value(self._device)
  203. if mode and not hasattr(ColorMode, mode.upper()):
  204. return mode
  205. async def async_turn_on(self, **params):
  206. settings = {}
  207. color_mode = None
  208. if self._color_mode_dps and ATTR_WHITE in params:
  209. if self.color_mode != ColorMode.WHITE:
  210. color_mode = ColorMode.WHITE
  211. if ATTR_BRIGHTNESS not in params and self._brightness_dps:
  212. bright = params.get(ATTR_WHITE)
  213. _LOGGER.debug(
  214. "Setting brightness via WHITE parameter to %d",
  215. bright,
  216. )
  217. r = self._brightness_dps.range(self._device)
  218. if r:
  219. bright = color_util.brightness_to_value(r, bright)
  220. settings = {
  221. **settings,
  222. **self._brightness_dps.get_values_to_set(
  223. self._device,
  224. bright,
  225. ),
  226. }
  227. elif self._color_temp_dps and ATTR_COLOR_TEMP_KELVIN in params:
  228. if self.color_mode != ColorMode.COLOR_TEMP:
  229. color_mode = ColorMode.COLOR_TEMP
  230. color_temp = params.get(ATTR_COLOR_TEMP_KELVIN)
  231. # Light groups use the widest range from the lights in the
  232. # group, so we are expected to silently handle out of range values
  233. if color_temp < self.min_color_temp_kelvin:
  234. color_temp = self.min_color_temp_kelvin
  235. if color_temp > self.max_color_temp_kelvin:
  236. color_temp = self.max_color_temp_kelvin
  237. _LOGGER.debug("Setting color temp to %d", color_temp)
  238. settings = {
  239. **settings,
  240. **self._color_temp_dps.get_values_to_set(
  241. self._device,
  242. color_temp,
  243. ),
  244. }
  245. elif self._rgbhsv_dps and (
  246. ATTR_HS_COLOR in params
  247. or (ATTR_BRIGHTNESS in params and self.raw_color_mode == ColorMode.HS)
  248. ):
  249. if self.raw_color_mode != ColorMode.HS:
  250. color_mode = ColorMode.HS
  251. hs = params.get(ATTR_HS_COLOR, self.hs_color or (0, 0))
  252. brightness = params.get(ATTR_BRIGHTNESS, self.brightness or 255)
  253. fmt = self._rgbhsv_dps.format
  254. if hs and fmt:
  255. rgb = color_util.color_hsv_to_RGB(*hs, brightness / 2.55)
  256. rgbhsv = {
  257. "r": rgb[0],
  258. "g": rgb[1],
  259. "b": rgb[2],
  260. "h": hs[0],
  261. "s": hs[1],
  262. "v": brightness,
  263. }
  264. _LOGGER.debug(
  265. "Setting color as R:%d,G:%d,B:%d,H:%d,S:%d,V:%d",
  266. rgb[0],
  267. rgb[1],
  268. rgb[2],
  269. hs[0],
  270. hs[1],
  271. brightness,
  272. )
  273. ordered = []
  274. idx = 0
  275. for n in fmt["names"]:
  276. r = fmt["ranges"][idx]
  277. scale = 1
  278. if n == "s":
  279. scale = r["max"] / 100
  280. elif n == "h":
  281. scale = r["max"] / 360
  282. else:
  283. scale = r["max"] / 255
  284. val = round(rgbhsv[n] * scale)
  285. if val < r["min"]:
  286. _LOGGER.warning(
  287. "%s/%s: Color data %s=%d constrained to be above %d",
  288. self._config._device.config,
  289. self.name or "light",
  290. n,
  291. val,
  292. r["min"],
  293. )
  294. val = r["min"]
  295. ordered.append(val)
  296. idx += 1
  297. binary = pack(fmt["format"], *ordered)
  298. settings = {
  299. **settings,
  300. **self._rgbhsv_dps.get_values_to_set(
  301. self._device,
  302. self._rgbhsv_dps.encode_value(binary),
  303. ),
  304. }
  305. if self._color_mode_dps:
  306. if color_mode:
  307. _LOGGER.debug("Auto setting color mode to %s", color_mode)
  308. settings = {
  309. **settings,
  310. **self._color_mode_dps.get_values_to_set(
  311. self._device,
  312. color_mode,
  313. ),
  314. }
  315. elif not self._effect_dps:
  316. effect = params.get(ATTR_EFFECT)
  317. if effect:
  318. _LOGGER.debug(
  319. "Emulating effect using color mode of %s",
  320. effect,
  321. )
  322. settings = {
  323. **settings,
  324. **self._color_mode_dps.get_values_to_set(
  325. self._device,
  326. effect,
  327. ),
  328. }
  329. if (
  330. ATTR_BRIGHTNESS in params
  331. and (
  332. (self.raw_color_mode != ColorMode.HS and color_mode is None)
  333. or (color_mode != ColorMode.HS and color_mode is not None)
  334. )
  335. and self._brightness_dps
  336. ):
  337. bright = params.get(ATTR_BRIGHTNESS)
  338. _LOGGER.debug("Setting brightness to %s", bright)
  339. r = self._brightness_dps.range(self._device)
  340. if r:
  341. bright = color_util.brightness_to_value(r, bright)
  342. settings = {
  343. **settings,
  344. **self._brightness_dps.get_values_to_set(
  345. self._device,
  346. bright,
  347. ),
  348. }
  349. if self._effect_dps:
  350. effect = params.get(ATTR_EFFECT, None)
  351. if effect:
  352. _LOGGER.debug("Setting effect to %s", effect)
  353. settings = {
  354. **settings,
  355. **self._effect_dps.get_values_to_set(
  356. self._device,
  357. effect,
  358. ),
  359. }
  360. if self._switch_dps and not self.is_on:
  361. if (
  362. self._switch_dps.readonly
  363. and self._effect_dps
  364. and "on" in self._effect_dps.values(self._device)
  365. ):
  366. # Special case for motion sensor lights with readonly switch
  367. # that have tristate switch available as effect
  368. if self._effect_dps.id not in settings:
  369. settings = settings | self._effect_dps.get_values_to_set(
  370. self._device, "on"
  371. )
  372. else:
  373. settings = settings | self._switch_dps.get_values_to_set(
  374. self._device, True
  375. )
  376. elif self._brightness_dps and not self.is_on:
  377. bright = 255
  378. r = self._brightness_dps.range(self._device)
  379. if r:
  380. bright = color_util.brightness_to_value(r, bright)
  381. settings = settings | self._brightness_dps.get_values_to_set(
  382. self._device, bright
  383. )
  384. if settings:
  385. await self._device.async_set_properties(settings)
  386. async def async_turn_off(self):
  387. if self._switch_dps:
  388. if (
  389. self._switch_dps.readonly
  390. and self._effect_dps
  391. and "off" in self._effect_dps.values(self._device)
  392. ):
  393. # Special case for motion sensor lights with readonly switch
  394. # that have tristate switch available as effect
  395. await self._effect_dps.async_set_value(self._device, "off")
  396. else:
  397. await self._switch_dps.async_set_value(self._device, False)
  398. elif self._brightness_dps:
  399. await self._brightness_dps.async_set_value(self._device, 0)
  400. else:
  401. raise NotImplementedError()
  402. async def async_toggle(self):
  403. disp_on = self.is_on
  404. await (self.async_turn_on() if not disp_on else self.async_turn_off())