light.py 11 KB

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