light.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. """
  2. Platform to control Tuya lights.
  3. Initially based on the secondary panel lighting control on some climate
  4. devices, so only providing simple on/off control.
  5. """
  6. from homeassistant.components.light import (
  7. ATTR_BRIGHTNESS,
  8. ATTR_COLOR_MODE,
  9. ATTR_COLOR_TEMP,
  10. ATTR_EFFECT,
  11. ATTR_RGBW_COLOR,
  12. ColorMode,
  13. LightEntity,
  14. LightEntityFeature,
  15. )
  16. import homeassistant.util.color as color_util
  17. import logging
  18. from struct import pack, unpack
  19. from ..device import TuyaLocalDevice
  20. from ..helpers.device_config import TuyaEntityConfig
  21. from ..helpers.mixin import TuyaLocalEntity
  22. _LOGGER = logging.getLogger(__name__)
  23. class TuyaLocalLight(TuyaLocalEntity, LightEntity):
  24. """Representation of a Tuya WiFi-connected light."""
  25. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  26. """
  27. Initialize the light.
  28. Args:
  29. device (TuyaLocalDevice): The device API instance.
  30. config (TuyaEntityConfig): The configuration for this entity.
  31. """
  32. dps_map = self._init_begin(device, config)
  33. self._switch_dps = dps_map.pop("switch", None)
  34. self._brightness_dps = dps_map.pop("brightness", None)
  35. self._color_mode_dps = dps_map.pop("color_mode", None)
  36. self._color_temp_dps = dps_map.pop("color_temp", None)
  37. self._rgbhsv_dps = dps_map.pop("rgbhsv", None)
  38. self._effect_dps = dps_map.pop("effect", None)
  39. self._init_end(dps_map)
  40. @property
  41. def supported_color_modes(self):
  42. """Return the supported color modes for this light."""
  43. if self._color_mode_dps:
  44. return [
  45. ColorMode(mode)
  46. for mode in self._color_mode_dps.values(self._device)
  47. if mode and hasattr(ColorMode, mode.upper())
  48. ]
  49. else:
  50. try:
  51. mode = ColorMode(self.color_mode)
  52. if mode and mode != ColorMode.UNKNOWN:
  53. return [mode]
  54. except ValueError:
  55. _LOGGER.warning(f"Unrecognised color mode {self.color_mode} ignored")
  56. return []
  57. @property
  58. def supported_features(self):
  59. """Return the supported features for this light."""
  60. if self.effect_list:
  61. return LightEntityFeature.EFFECT
  62. else:
  63. return 0
  64. @property
  65. def color_mode(self):
  66. """Return the color mode of the light"""
  67. if self._color_mode_dps:
  68. mode = self._color_mode_dps.get_value(self._device)
  69. if mode and hasattr(ColorMode, mode.upper()):
  70. return ColorMode(mode)
  71. if self._rgbhsv_dps:
  72. return ColorMode.RGBW
  73. elif self._color_temp_dps:
  74. return ColorMode.COLOR_TEMP
  75. elif self._brightness_dps:
  76. return ColorMode.BRIGHTNESS
  77. elif self._switch_dps:
  78. return ColorMode.ONOFF
  79. else:
  80. return ColorMode.UNKNOWN
  81. @property
  82. def color_temp(self):
  83. """Return the color temperature in mireds"""
  84. if self._color_temp_dps:
  85. unscaled = self._color_temp_dps.get_value(self._device)
  86. r = self._color_temp_dps.range(self._device)
  87. if r and isinstance(unscaled, (int, float)):
  88. return round(unscaled * 347 / (r["max"] - r["min"]) + 153 - r["min"])
  89. else:
  90. return unscaled
  91. @property
  92. def is_on(self):
  93. """Return the current state."""
  94. if self._switch_dps:
  95. return self._switch_dps.get_value(self._device)
  96. elif self._brightness_dps:
  97. b = self.brightness
  98. return isinstance(b, int) and b > 0
  99. else:
  100. # There shouldn't be lights without control, but if there are,
  101. # assume always on if they are responding
  102. return self.available
  103. @property
  104. def brightness(self):
  105. """Get the current brightness of the light"""
  106. if self._brightness_dps:
  107. return self._brightness_dps.get_value(self._device)
  108. @property
  109. def rgbw_color(self):
  110. """Get the current RGBW color of the light"""
  111. if self._rgbhsv_dps:
  112. # color data in hex format RRGGBBHHHHSSVV (14 digit hex)
  113. # can also be base64 encoded.
  114. # Either RGB or HSV can be used.
  115. color = self._rgbhsv_dps.decoded_value(self._device)
  116. fmt = self._rgbhsv_dps.format
  117. if fmt:
  118. vals = unpack(fmt.get("format"), color)
  119. rgbhsv = {}
  120. idx = 0
  121. for v in vals:
  122. # Range in HA is 0-100 for s, 0-255 for rgb and v, 0-360
  123. # for h
  124. n = fmt["names"][idx]
  125. r = fmt["ranges"][idx]
  126. if r["min"] != 0:
  127. raise AttributeError(
  128. f"Unhandled minimum range for {n} in RGBW value"
  129. )
  130. mx = r["max"]
  131. scale = 1
  132. if n == "h":
  133. scale = 360 / mx
  134. elif n == "s":
  135. scale = 100 / mx
  136. else:
  137. scale = 255 / mx
  138. rgbhsv[n] = round(scale * v)
  139. idx += 1
  140. h = rgbhsv["h"]
  141. s = rgbhsv["s"]
  142. # convert RGB from H and S to seperate out the V component
  143. r, g, b = color_util.color_hs_to_RGB(h, s)
  144. w = rgbhsv["v"]
  145. return (r, g, b, w)
  146. @property
  147. def effect_list(self):
  148. """Return the list of valid effects for the light"""
  149. if self._effect_dps:
  150. return self._effect_dps.values(self._device)
  151. elif self._color_mode_dps:
  152. return [
  153. effect
  154. for effect in self._color_mode_dps.values(self._device)
  155. if effect and not hasattr(ColorMode, effect.upper())
  156. ]
  157. @property
  158. def effect(self):
  159. """Return the current effect setting of this light"""
  160. if self._effect_dps:
  161. return self._effect_dps.get_value(self._device)
  162. elif self._color_mode_dps:
  163. mode = self._color_mode_dps.get_value(self._device)
  164. if mode and not hasattr(ColorMode, mode.upper()):
  165. return mode
  166. async def async_turn_on(self, **params):
  167. settings = {}
  168. color_mode = params.get(ATTR_COLOR_MODE, self.color_mode)
  169. if self._color_temp_dps and ATTR_COLOR_TEMP in params:
  170. if ATTR_COLOR_MODE not in params:
  171. color_mode = ColorMode.COLOR_TEMP
  172. if self._color_mode_dps:
  173. _LOGGER.debug("Auto setting color mode to COLOR_TEMP")
  174. settings = {
  175. **settings,
  176. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  177. }
  178. color_temp = params.get(ATTR_COLOR_TEMP)
  179. r = self._color_temp_dps.range(self._device)
  180. if r and color_temp:
  181. color_temp = round(
  182. (color_temp - 153 + r["min"]) * (r["max"] - r["min"]) / 347
  183. )
  184. _LOGGER.debug(f"Setting color temp to {color_temp}")
  185. settings = {
  186. **settings,
  187. **self._color_temp_dps.get_values_to_set(self._device, color_temp),
  188. }
  189. elif self._rgbhsv_dps and (
  190. ATTR_RGBW_COLOR in params
  191. or (ATTR_BRIGHTNESS in params and color_mode == ColorMode.RGBW)
  192. ):
  193. if ATTR_COLOR_MODE not in params:
  194. color_mode = ColorMode.RGBW
  195. if self._color_mode_dps:
  196. _LOGGER.debug("Auto setting color mode to RGBW")
  197. settings = {
  198. **settings,
  199. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  200. }
  201. rgbw = params.get(ATTR_RGBW_COLOR, self.rgbw_color or (0, 0, 0, 0))
  202. brightness = params.get(ATTR_BRIGHTNESS, self.brightness or 255)
  203. fmt = self._rgbhsv_dps.format
  204. if rgbw and fmt:
  205. rgb = (rgbw[0], rgbw[1], rgbw[2])
  206. hs = color_util.color_RGB_to_hs(rgbw[0], rgbw[1], rgbw[2])
  207. rgbhsv = {
  208. "r": rgb[0],
  209. "g": rgb[1],
  210. "b": rgb[2],
  211. "h": hs[0],
  212. "s": hs[1],
  213. "v": brightness,
  214. }
  215. _LOGGER.debug(
  216. f"Setting RGBW as {rgb[0]},{rgb[1]},{rgb[2]},{hs[0]},{hs[1]},{brightness}"
  217. )
  218. ordered = []
  219. idx = 0
  220. for n in fmt["names"]:
  221. r = fmt["ranges"][idx]
  222. scale = 1
  223. if n == "s":
  224. scale = r["max"] / 100
  225. elif n == "h":
  226. scale = r["max"] / 360
  227. else:
  228. scale = r["max"] / 255
  229. ordered.append(round(rgbhsv[n] * scale))
  230. idx += 1
  231. binary = pack(fmt["format"], *ordered)
  232. settings = {
  233. **settings,
  234. **self._rgbhsv_dps.get_values_to_set(
  235. self._device,
  236. self._rgbhsv_dps.encode_value(binary),
  237. ),
  238. }
  239. elif self._color_mode_dps and ATTR_COLOR_MODE in params:
  240. if color_mode:
  241. _LOGGER.debug(f"Explicitly setting color mode to {color_mode}")
  242. settings = {
  243. **settings,
  244. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  245. }
  246. elif not self._effect_dps:
  247. effect = params.get(ATTR_EFFECT)
  248. if effect:
  249. _LOGGER.debug(f"Emulating effect using color mode of {effect}")
  250. settings = {
  251. **settings,
  252. **self._color_mode_dps.get_values_to_set(
  253. self._device,
  254. effect,
  255. ),
  256. }
  257. if (
  258. ATTR_BRIGHTNESS in params
  259. and color_mode != ColorMode.RGBW
  260. and self._brightness_dps
  261. ):
  262. bright = params.get(ATTR_BRIGHTNESS)
  263. _LOGGER.debug(f"Setting brightness to {bright}")
  264. settings = {
  265. **settings,
  266. **self._brightness_dps.get_values_to_set(
  267. self._device,
  268. bright,
  269. ),
  270. }
  271. if self._effect_dps:
  272. effect = params.get(ATTR_EFFECT, None)
  273. if effect:
  274. _LOGGER.debug(f"Setting effect to {effect}")
  275. settings = {
  276. **settings,
  277. **self._effect_dps.get_values_to_set(
  278. self._device,
  279. effect,
  280. ),
  281. }
  282. if self._switch_dps and not self.is_on:
  283. settings = {
  284. **settings,
  285. **self._switch_dps.get_values_to_set(self._device, True),
  286. }
  287. if settings:
  288. await self._device.async_set_properties(settings)
  289. async def async_turn_off(self):
  290. if self._switch_dps:
  291. await self._switch_dps.async_set_value(self._device, False)
  292. elif self._brightness_dps:
  293. await self._brightness_dps.async_set_value(self._device, 0)
  294. else:
  295. raise NotImplementedError()
  296. async def async_toggle(self):
  297. disp_on = self.is_on
  298. await (self.async_turn_on() if not disp_on else self.async_turn_off())