light.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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. from_dp = self.raw_color_mode
  68. if from_dp:
  69. return from_dp
  70. if self._rgbhsv_dps:
  71. return ColorMode.RGBW
  72. elif self._color_temp_dps:
  73. return ColorMode.COLOR_TEMP
  74. elif self._brightness_dps:
  75. return ColorMode.BRIGHTNESS
  76. elif self._switch_dps:
  77. return ColorMode.ONOFF
  78. else:
  79. return ColorMode.UNKNOWN
  80. @property
  81. def raw_color_mode(self):
  82. """Return the color_mode as set from the dps."""
  83. if self._color_mode_dps:
  84. mode = self._color_mode_dps.get_value(self._device)
  85. if mode and hasattr(ColorMode, mode.upper()):
  86. return ColorMode(mode)
  87. @property
  88. def color_temp(self):
  89. """Return the color temperature in mireds"""
  90. if self._color_temp_dps:
  91. unscaled = self._color_temp_dps.get_value(self._device)
  92. r = self._color_temp_dps.range(self._device)
  93. if r and isinstance(unscaled, (int, float)):
  94. return round(unscaled * 347 / (r["max"] - r["min"]) + 153 - r["min"])
  95. else:
  96. return unscaled
  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. fmt = self._rgbhsv_dps.format
  123. if fmt and color:
  124. vals = unpack(fmt.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 = fmt["names"][idx]
  131. r = fmt["ranges"][idx]
  132. if r["min"] != 0:
  133. raise AttributeError(
  134. f"Unhandled minimum range for {n} in RGBW value"
  135. )
  136. mx = r["max"]
  137. scale = 1
  138. if n == "h":
  139. scale = 360 / mx
  140. elif n == "s":
  141. scale = 100 / mx
  142. else:
  143. scale = 255 / mx
  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 and not hasattr(ColorMode, effect.upper())
  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 and not hasattr(ColorMode, mode.upper()):
  171. return mode
  172. async def async_turn_on(self, **params):
  173. settings = {}
  174. color_mode = None
  175. if self._color_temp_dps and ATTR_COLOR_TEMP in params:
  176. if self.color_mode != ColorMode.COLOR_TEMP:
  177. color_mode = ColorMode.COLOR_TEMP
  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 self.raw_color_mode == ColorMode.RGBW)
  192. ):
  193. if self.raw_color_mode != ColorMode.RGBW:
  194. color_mode = ColorMode.RGBW
  195. rgbw = params.get(ATTR_RGBW_COLOR, self.rgbw_color or (0, 0, 0, 0))
  196. brightness = params.get(ATTR_BRIGHTNESS, self.brightness or 255)
  197. fmt = self._rgbhsv_dps.format
  198. if rgbw and fmt:
  199. rgb = (rgbw[0], rgbw[1], rgbw[2])
  200. hs = color_util.color_RGB_to_hs(rgbw[0], rgbw[1], rgbw[2])
  201. rgbhsv = {
  202. "r": rgb[0],
  203. "g": rgb[1],
  204. "b": rgb[2],
  205. "h": hs[0],
  206. "s": hs[1],
  207. "v": brightness,
  208. }
  209. _LOGGER.debug(
  210. f"Setting RGBW as {rgb[0]},{rgb[1]},{rgb[2]},{hs[0]},{hs[1]},{brightness}"
  211. )
  212. ordered = []
  213. idx = 0
  214. for n in fmt["names"]:
  215. r = fmt["ranges"][idx]
  216. scale = 1
  217. if n == "s":
  218. scale = r["max"] / 100
  219. elif n == "h":
  220. scale = r["max"] / 360
  221. else:
  222. scale = r["max"] / 255
  223. ordered.append(round(rgbhsv[n] * scale))
  224. idx += 1
  225. binary = pack(fmt["format"], *ordered)
  226. settings = {
  227. **settings,
  228. **self._rgbhsv_dps.get_values_to_set(
  229. self._device,
  230. self._rgbhsv_dps.encode_value(binary),
  231. ),
  232. }
  233. if self._color_mode_dps:
  234. if color_mode:
  235. _LOGGER.debug(f"Auto setting color mode to {color_mode}")
  236. settings = {
  237. **settings,
  238. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  239. }
  240. elif not self._effect_dps:
  241. effect = params.get(ATTR_EFFECT)
  242. if effect:
  243. _LOGGER.debug(f"Emulating effect using color mode of {effect}")
  244. settings = {
  245. **settings,
  246. **self._color_mode_dps.get_values_to_set(
  247. self._device,
  248. effect,
  249. ),
  250. }
  251. if (
  252. ATTR_BRIGHTNESS in params
  253. and self.raw_color_mode != ColorMode.RGBW
  254. and self._brightness_dps
  255. ):
  256. bright = params.get(ATTR_BRIGHTNESS)
  257. _LOGGER.debug(f"Setting brightness to {bright}")
  258. settings = {
  259. **settings,
  260. **self._brightness_dps.get_values_to_set(
  261. self._device,
  262. bright,
  263. ),
  264. }
  265. if self._effect_dps:
  266. effect = params.get(ATTR_EFFECT, None)
  267. if effect:
  268. _LOGGER.debug(f"Setting effect to {effect}")
  269. settings = {
  270. **settings,
  271. **self._effect_dps.get_values_to_set(
  272. self._device,
  273. effect,
  274. ),
  275. }
  276. if self._switch_dps and not self.is_on:
  277. if (
  278. self._switch_dps.readonly
  279. and self._effect_dps
  280. and "on" in self._effect_dps.values(self._device)
  281. ):
  282. # Special case for motion sensor lights with readonly switch
  283. # that have tristate switch available as effect
  284. if self._effect_dps.id not in settings:
  285. settings = settings | self._effect_dps.get_values_to_set(
  286. self._device, "on"
  287. )
  288. else:
  289. settings = settings | self._switch_dps.get_values_to_set(
  290. self._device, True
  291. )
  292. if settings:
  293. await self._device.async_set_properties(settings)
  294. async def async_turn_off(self):
  295. if self._switch_dps:
  296. if (
  297. self._switch_dps.readonly
  298. and self._effect_dps
  299. and "off" in self._effect_dps.values(self._device)
  300. ):
  301. # Special case for motion sensor lights with readonly switch
  302. # that have tristate switch available as effect
  303. await self._effect_dps.async_set_value(self._device, "off")
  304. else:
  305. await self._switch_dps.async_set_value(self._device, False)
  306. elif self._brightness_dps:
  307. await self._brightness_dps.async_set_value(self._device, 0)
  308. else:
  309. raise NotImplementedError()
  310. async def async_toggle(self):
  311. disp_on = self.is_on
  312. await (self.async_turn_on() if not disp_on else self.async_turn_off())