light.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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_HS_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._init_end(dps_map)
  49. @property
  50. def supported_color_modes(self):
  51. """Return the supported color modes for this light."""
  52. if self._color_mode_dps:
  53. return [
  54. ColorMode(mode)
  55. for mode in self._color_mode_dps.values(self._device)
  56. if mode and hasattr(ColorMode, mode.upper())
  57. ]
  58. else:
  59. try:
  60. mode = ColorMode(self.color_mode)
  61. if mode and mode != ColorMode.UNKNOWN:
  62. return [mode]
  63. except ValueError:
  64. _LOGGER.warning(f"Unrecognised color mode {self.color_mode} ignored")
  65. return []
  66. @property
  67. def supported_features(self):
  68. """Return the supported features for this light."""
  69. if self.effect_list:
  70. return LightEntityFeature.EFFECT
  71. else:
  72. return 0
  73. @property
  74. def color_mode(self):
  75. """Return the color mode of the light"""
  76. from_dp = self.raw_color_mode
  77. if from_dp:
  78. return from_dp
  79. if self._rgbhsv_dps:
  80. return ColorMode.HS
  81. elif self._color_temp_dps:
  82. return ColorMode.COLOR_TEMP
  83. elif self._brightness_dps:
  84. return ColorMode.BRIGHTNESS
  85. elif self._switch_dps:
  86. return ColorMode.ONOFF
  87. else:
  88. return ColorMode.UNKNOWN
  89. @property
  90. def raw_color_mode(self):
  91. """Return the color_mode as set from the dps."""
  92. if self._color_mode_dps:
  93. mode = self._color_mode_dps.get_value(self._device)
  94. if mode and hasattr(ColorMode, mode.upper()):
  95. return ColorMode(mode)
  96. @property
  97. def color_temp(self):
  98. """Return the color temperature in mireds"""
  99. if self._color_temp_dps:
  100. unscaled = self._color_temp_dps.get_value(self._device)
  101. r = self._color_temp_dps.range(self._device)
  102. if r and isinstance(unscaled, (int, float)):
  103. return round(unscaled * 347 / (r["max"] - r["min"]) + 153 - r["min"])
  104. else:
  105. return unscaled
  106. @property
  107. def is_on(self):
  108. """Return the current state."""
  109. if self._switch_dps:
  110. return self._switch_dps.get_value(self._device)
  111. elif self._brightness_dps:
  112. b = self.brightness
  113. return isinstance(b, int) and b > 0
  114. else:
  115. # There shouldn't be lights without control, but if there are,
  116. # assume always on if they are responding
  117. return self.available
  118. @property
  119. def brightness(self):
  120. """Get the current brightness of the light"""
  121. if self.raw_color_mode == ColorMode.HS and self._rgbhsv_dps:
  122. return self._hsv_brightness
  123. return self._white_brightness
  124. @property
  125. def _white_brightness(self):
  126. if self._brightness_dps:
  127. return self._brightness_dps.get_value(self._device)
  128. @property
  129. def _unpacked_rgbhsv(self):
  130. """Get the unpacked rgbhsv data"""
  131. if self._rgbhsv_dps:
  132. color = self._rgbhsv_dps.decoded_value(self._device)
  133. fmt = self._rgbhsv_dps.format
  134. if fmt and color:
  135. vals = unpack(fmt.get("format"), color)
  136. idx = 0
  137. rgbhsv = {}
  138. for v in vals:
  139. # Range in HA is 0-100 for s, 0-255 for rgb and v, 0-360 for h
  140. n = fmt["names"][idx]
  141. r = fmt["ranges"][idx]
  142. mx = r["max"]
  143. scale = 1
  144. if n == "h":
  145. scale = 360 / mx
  146. elif n == "s":
  147. scale = 100 / mx
  148. else:
  149. scale = 255 / mx
  150. rgbhsv[n] = round(scale * v)
  151. idx += 1
  152. return rgbhsv
  153. @property
  154. def _hsv_brightness(self):
  155. """Get the colour mode brightness from the light"""
  156. rgbhsv = self._unpacked_rgbhsv
  157. if rgbhsv:
  158. return rgbhsv.get("v", self._white_brightness)
  159. return self._white_brightness
  160. @property
  161. def hs_color(self):
  162. """Get the current hs color of the light"""
  163. rgbhsv = self._unpacked_rgbhsv
  164. if rgbhsv:
  165. if "h" in rgbhsv and "s" in rgbhsv:
  166. hs = (rgbhsv["h"], rgbhsv["s"])
  167. else:
  168. r = rgbhsv.get("r")
  169. g = rgbhsv.get("g")
  170. b = rgbhsv.get("b")
  171. hs = color_util.color_rgb_to_hs(r, g, b)
  172. return hs
  173. @property
  174. def effect_list(self):
  175. """Return the list of valid effects for the light"""
  176. if self._effect_dps:
  177. return self._effect_dps.values(self._device)
  178. elif self._color_mode_dps:
  179. return [
  180. effect
  181. for effect in self._color_mode_dps.values(self._device)
  182. if effect and not hasattr(ColorMode, effect.upper())
  183. ]
  184. @property
  185. def effect(self):
  186. """Return the current effect setting of this light"""
  187. if self._effect_dps:
  188. return self._effect_dps.get_value(self._device)
  189. elif self._color_mode_dps:
  190. mode = self._color_mode_dps.get_value(self._device)
  191. if mode and not hasattr(ColorMode, mode.upper()):
  192. return mode
  193. async def async_turn_on(self, **params):
  194. settings = {}
  195. color_mode = None
  196. if self._color_mode_dps and ATTR_WHITE in params:
  197. if self.color_mode != ColorMode.WHITE:
  198. color_mode = ColorMode.WHITE
  199. if ATTR_BRIGHTNESS not in params and self._brightness_dps:
  200. bright = params.get(ATTR_WHITE)
  201. _LOGGER.debug(f"Setting brightness via WHITE parameter to {bright}")
  202. settings = {
  203. **settings,
  204. **self._brightness_dps.get_values_to_set(
  205. self._device,
  206. bright,
  207. ),
  208. }
  209. elif self._color_temp_dps and ATTR_COLOR_TEMP in params:
  210. if self.color_mode != ColorMode.COLOR_TEMP:
  211. color_mode = ColorMode.COLOR_TEMP
  212. color_temp = params.get(ATTR_COLOR_TEMP)
  213. r = self._color_temp_dps.range(self._device)
  214. if r and color_temp:
  215. color_temp = round(
  216. (color_temp - 153 + r["min"]) * (r["max"] - r["min"]) / 347
  217. )
  218. _LOGGER.debug(f"Setting color temp to {color_temp}")
  219. settings = {
  220. **settings,
  221. **self._color_temp_dps.get_values_to_set(self._device, color_temp),
  222. }
  223. elif self._rgbhsv_dps and (
  224. ATTR_HS_COLOR in params
  225. or (ATTR_BRIGHTNESS in params and self.raw_color_mode == ColorMode.HS)
  226. ):
  227. if self.raw_color_mode != ColorMode.HS:
  228. color_mode = ColorMode.HS
  229. hs = params.get(ATTR_HS_COLOR, self.hs_color or (0, 0))
  230. brightness = params.get(ATTR_BRIGHTNESS, self.brightness or 255)
  231. fmt = self._rgbhsv_dps.format
  232. if hs and fmt:
  233. rgb = color_util.color_hsv_to_RGB(*hs, brightness / 2.55)
  234. rgbhsv = {
  235. "r": rgb[0],
  236. "g": rgb[1],
  237. "b": rgb[2],
  238. "h": hs[0],
  239. "s": hs[1],
  240. "v": brightness,
  241. }
  242. _LOGGER.debug(
  243. f"Setting color as {rgb[0]},{rgb[1]},{rgb[2]},{hs[0]},{hs[1]},{brightness}"
  244. )
  245. ordered = []
  246. idx = 0
  247. for n in fmt["names"]:
  248. r = fmt["ranges"][idx]
  249. scale = 1
  250. if n == "s":
  251. scale = r["max"] / 100
  252. elif n == "h":
  253. scale = r["max"] / 360
  254. else:
  255. scale = r["max"] / 255
  256. val = round(rgbhsv[n] * scale)
  257. if val < r["min"]:
  258. _LOGGER.warning(
  259. "Color data %s=%d constrained to be above %d",
  260. n,
  261. val,
  262. r["min"],
  263. )
  264. val = r["min"]
  265. ordered.append(val)
  266. idx += 1
  267. binary = pack(fmt["format"], *ordered)
  268. settings = {
  269. **settings,
  270. **self._rgbhsv_dps.get_values_to_set(
  271. self._device,
  272. self._rgbhsv_dps.encode_value(binary),
  273. ),
  274. }
  275. if self._color_mode_dps:
  276. if color_mode:
  277. _LOGGER.debug(f"Auto setting color mode to {color_mode}")
  278. settings = {
  279. **settings,
  280. **self._color_mode_dps.get_values_to_set(self._device, color_mode),
  281. }
  282. elif not self._effect_dps:
  283. effect = params.get(ATTR_EFFECT)
  284. if effect:
  285. _LOGGER.debug(f"Emulating effect using color mode of {effect}")
  286. settings = {
  287. **settings,
  288. **self._color_mode_dps.get_values_to_set(
  289. self._device,
  290. effect,
  291. ),
  292. }
  293. if (
  294. ATTR_BRIGHTNESS in params
  295. and self.raw_color_mode != ColorMode.HS
  296. and self._brightness_dps
  297. ):
  298. bright = params.get(ATTR_BRIGHTNESS)
  299. _LOGGER.debug(f"Setting brightness to {bright}")
  300. settings = {
  301. **settings,
  302. **self._brightness_dps.get_values_to_set(
  303. self._device,
  304. bright,
  305. ),
  306. }
  307. if self._effect_dps:
  308. effect = params.get(ATTR_EFFECT, None)
  309. if effect:
  310. _LOGGER.debug(f"Setting effect to {effect}")
  311. settings = {
  312. **settings,
  313. **self._effect_dps.get_values_to_set(
  314. self._device,
  315. effect,
  316. ),
  317. }
  318. if self._switch_dps and not self.is_on:
  319. if (
  320. self._switch_dps.readonly
  321. and self._effect_dps
  322. and "on" in self._effect_dps.values(self._device)
  323. ):
  324. # Special case for motion sensor lights with readonly switch
  325. # that have tristate switch available as effect
  326. if self._effect_dps.id not in settings:
  327. settings = settings | self._effect_dps.get_values_to_set(
  328. self._device, "on"
  329. )
  330. else:
  331. settings = settings | self._switch_dps.get_values_to_set(
  332. self._device, True
  333. )
  334. elif self._brightness_dps and not self.is_on:
  335. settings = settings | self._brightness_dps.get_values_to_set(
  336. self._device, 255
  337. )
  338. if settings:
  339. await self._device.async_set_properties(settings)
  340. async def async_turn_off(self):
  341. if self._switch_dps:
  342. if (
  343. self._switch_dps.readonly
  344. and self._effect_dps
  345. and "off" in self._effect_dps.values(self._device)
  346. ):
  347. # Special case for motion sensor lights with readonly switch
  348. # that have tristate switch available as effect
  349. await self._effect_dps.async_set_value(self._device, "off")
  350. else:
  351. await self._switch_dps.async_set_value(self._device, False)
  352. elif self._brightness_dps:
  353. await self._brightness_dps.async_set_value(self._device, 0)
  354. else:
  355. raise NotImplementedError()
  356. async def async_toggle(self):
  357. disp_on = self.is_on
  358. await (self.async_turn_on() if not disp_on else self.async_turn_off())