light.py 14 KB

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