light.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. range = self._color_temp_dps.range(self._device)
  90. if range:
  91. min = range["min"]
  92. max = range["max"]
  93. return round(unscaled * 347 / (max - min) + 153 - min)
  94. else:
  95. return unscaled
  96. return None
  97. @property
  98. def is_on(self):
  99. """Return the current state."""
  100. if self._switch_dps:
  101. return self._switch_dps.get_value(self._device)
  102. elif self._brightness_dps:
  103. b = self.brightness
  104. return isinstance(b, int) and b > 0
  105. else:
  106. # There shouldn't be lights without control, but if there are,
  107. # assume always on if they are responding
  108. return self.available
  109. @property
  110. def brightness(self):
  111. """Get the current brightness of the light"""
  112. if self._brightness_dps:
  113. return self._brightness_dps.get_value(self._device)
  114. @property
  115. def rgbw_color(self):
  116. """Get the current RGBW color of the light"""
  117. if self._rgbhsv_dps:
  118. # color data in hex format RRGGBBHHHHSSVV (14 digit hex)
  119. # can also be base64 encoded.
  120. # Either RGB or HSV can be used.
  121. color = self._rgbhsv_dps.decoded_value(self._device)
  122. format = self._rgbhsv_dps.format
  123. if format:
  124. vals = unpack(format.get("format"), color)
  125. rgbhsv = {}
  126. idx = 0
  127. for v in vals:
  128. # Range in HA is 0-100 for s, 0-255 for rgb and v, 0-360
  129. # for h
  130. n = format["names"][idx]
  131. r = format["ranges"][idx]
  132. if r["min"] != 0:
  133. raise AttributeError(
  134. f"Unhandled minimum range for {n} in RGBW value"
  135. )
  136. max = r["max"]
  137. scale = 1
  138. if n == "h":
  139. scale = 360 / max
  140. elif n == "s":
  141. scale = 100 / max
  142. else:
  143. scale = 255 / max
  144. rgbhsv[n] = round(scale * v)
  145. idx += 1
  146. h = rgbhsv["h"]
  147. s = rgbhsv["s"]
  148. # convert RGB from H and S to seperate out the V component
  149. r, g, b = color_util.color_hs_to_RGB(h, s)
  150. w = rgbhsv["v"]
  151. return (r, g, b, w)
  152. @property
  153. def effect_list(self):
  154. """Return the list of valid effects for the light"""
  155. if self._effect_dps:
  156. return self._effect_dps.values(self._device)
  157. elif self._color_mode_dps:
  158. return [
  159. effect
  160. for effect in self._color_mode_dps.values(self._device)
  161. if effect not in VALID_COLOR_MODES
  162. ]
  163. @property
  164. def effect(self):
  165. """Return the current effect setting of this light"""
  166. if self._effect_dps:
  167. return self._effect_dps.get_value(self._device)
  168. elif self._color_mode_dps:
  169. mode = self._color_mode_dps.get_value(self._device)
  170. if mode in VALID_COLOR_MODES:
  171. return None
  172. return mode
  173. async def async_turn_on(self, **params):
  174. settings = {}
  175. color_mode = params.get(ATTR_COLOR_MODE, self.color_mode)
  176. if self._color_temp_dps and ATTR_COLOR_TEMP in params:
  177. if ATTR_COLOR_MODE not in params:
  178. color_mode = COLOR_MODE_WHITE
  179. if self._color_mode_dps:
  180. _LOGGER.debug("Auto setting color mode to WHITE for color temp")
  181. settings = {
  182. **settings,
  183. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  184. }
  185. color_temp = params.get(ATTR_COLOR_TEMP)
  186. range = self._color_temp_dps.range(self._device)
  187. if range and color_temp:
  188. min = range["min"]
  189. max = range["max"]
  190. color_temp = round((color_temp - 153 + min) * (max - min) / 347)
  191. _LOGGER.debug(f"Setting color temp to {color_temp}")
  192. settings = {
  193. **settings,
  194. **self._color_temp_dps.get_values_to_set(self._device, color_temp),
  195. }
  196. elif self._rgbhsv_dps and (
  197. ATTR_RGBW_COLOR in params
  198. or (ATTR_BRIGHTNESS in params and color_mode == COLOR_MODE_RGBW)
  199. ):
  200. if ATTR_COLOR_MODE not in params:
  201. color_mode = COLOR_MODE_RGBW
  202. if self._color_mode_dps:
  203. _LOGGER.debug("Auto setting color mode to RGBW")
  204. settings = {
  205. **settings,
  206. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  207. }
  208. rgbw = params.get(ATTR_RGBW_COLOR, self.rgbw_color or (0, 0, 0, 0))
  209. brightness = params.get(ATTR_BRIGHTNESS, rgbw[3])
  210. format = self._rgbhsv_dps.format
  211. if rgbw and format:
  212. rgb = (rgbw[0], rgbw[1], rgbw[2])
  213. hs = color_util.color_RGB_to_hs(rgbw[0], rgbw[1], rgbw[2])
  214. rgbhsv = {
  215. "r": rgb[0],
  216. "g": rgb[1],
  217. "b": rgb[2],
  218. "h": hs[0],
  219. "s": hs[1],
  220. "v": brightness,
  221. }
  222. _LOGGER.debug(
  223. f"Setting RGBW as {rgb[0]},{rgb[1]},{rgb[2]},{hs[0]},{hs[1]},{brightness}"
  224. )
  225. ordered = []
  226. idx = 0
  227. for n in format["names"]:
  228. r = format["ranges"][idx]
  229. scale = 1
  230. if n == "s":
  231. scale = r["max"] / 100
  232. elif n == "h":
  233. scale = r["max"] / 360
  234. else:
  235. scale = r["max"] / 255
  236. ordered.append(round(rgbhsv[n] * scale))
  237. idx += 1
  238. binary = pack(format["format"], *ordered)
  239. settings = {
  240. **settings,
  241. **self._rgbhsv_dps.get_values_to_set(
  242. self._device,
  243. self._rgbhsv_dps.encode_value(binary),
  244. ),
  245. }
  246. elif self._color_mode_dps and ATTR_COLOR_MODE in params:
  247. if color_mode:
  248. _LOGGER.debug(f"Explicitly setting color mode to {color_mode}")
  249. settings = {
  250. **settings,
  251. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  252. }
  253. elif not self._effect_dps:
  254. effect = params.get(ATTR_EFFECT)
  255. if effect:
  256. _LOGGER.debug(f"Emulating effect using color mode of {effect}")
  257. settings = {
  258. **settings,
  259. **self._color_mode_dps.get_values_to_set(
  260. self._device,
  261. effect,
  262. ),
  263. }
  264. if (
  265. ATTR_BRIGHTNESS in params
  266. and color_mode != COLOR_MODE_RGBW
  267. and self._brightness_dps
  268. ):
  269. bright = params.get(ATTR_BRIGHTNESS)
  270. _LOGGER.debug(f"Setting brightness to {bright}")
  271. settings = {
  272. **settings,
  273. **self._brightness_dps.get_values_to_set(
  274. self._device,
  275. bright,
  276. ),
  277. }
  278. if self._switch_dps:
  279. settings = {
  280. **settings,
  281. **self._switch_dps.get_values_to_set(self._device, True),
  282. }
  283. if self._effect_dps:
  284. effect = params.get(ATTR_EFFECT, None)
  285. if effect:
  286. _LOGGER.debug(f"Setting effect to {effect}")
  287. settings = {
  288. **settings,
  289. **self._effect_dps.get_values_to_set(
  290. self._device,
  291. effect,
  292. ),
  293. }
  294. await self._device.async_set_properties(settings)
  295. async def async_turn_off(self):
  296. if self._switch_dps:
  297. await self._switch_dps.async_set_value(self._device, False)
  298. elif self._brightness_dps:
  299. await self._brightness_dps.async_set_value(self._device, 0)
  300. else:
  301. raise NotImplementedError()
  302. async def async_toggle(self):
  303. disp_on = self.is_on
  304. await (self.async_turn_on() if not disp_on else self.async_turn_off())