light.py 12 KB

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