light.py 13 KB

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