light.py 12 KB

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