light.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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_KELVIN,
  10. ATTR_EFFECT,
  11. ATTR_HS_COLOR,
  12. ATTR_WHITE,
  13. EFFECT_OFF,
  14. ColorMode,
  15. LightEntity,
  16. LightEntityFeature,
  17. )
  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. super().__init__()
  42. dps_map = self._init_begin(device, config)
  43. self._switch_dps = dps_map.pop("switch", None)
  44. self._brightness_dps = dps_map.pop("brightness", None)
  45. self._color_mode_dps = dps_map.pop("color_mode", None)
  46. self._color_temp_dps = dps_map.pop("color_temp", None)
  47. self._rgbhsv_dps = dps_map.pop("rgbhsv", None)
  48. self._effect_dps = dps_map.pop("effect", None)
  49. self._init_end(dps_map)
  50. # Set min and max color temp
  51. if self._color_temp_dps:
  52. m = self._color_temp_dps._find_map_for_dps(0)
  53. if m:
  54. tr = m.get("target_range")
  55. if tr:
  56. self._attr_min_color_temp_kelvin = tr.get("min")
  57. self._attr_max_color_temp_kelvin = tr.get("max")
  58. @property
  59. def supported_color_modes(self):
  60. """Return the supported color modes for this light."""
  61. if self._color_mode_dps:
  62. return {
  63. ColorMode(mode)
  64. for mode in self._color_mode_dps.values(self._device)
  65. if mode and hasattr(ColorMode, mode.upper())
  66. }
  67. else:
  68. try:
  69. mode = ColorMode(self.color_mode)
  70. if mode and mode != ColorMode.UNKNOWN:
  71. return {mode}
  72. except ValueError:
  73. _LOGGER.warning(
  74. "%s/%s: Unrecognised color mode %s ignored",
  75. self._config._device.config,
  76. self.name or "light",
  77. self.color_mode,
  78. )
  79. return set()
  80. @property
  81. def supported_features(self):
  82. """Return the supported features for this light."""
  83. if self.effect_list:
  84. return LightEntityFeature.EFFECT
  85. else:
  86. return LightEntityFeature(0)
  87. @property
  88. def color_mode(self):
  89. """Return the color mode of the light"""
  90. from_dp = self.raw_color_mode
  91. if from_dp:
  92. return from_dp
  93. if self._rgbhsv_dps:
  94. return ColorMode.HS
  95. elif self._color_temp_dps:
  96. return ColorMode.COLOR_TEMP
  97. elif self._brightness_dps:
  98. return ColorMode.BRIGHTNESS
  99. elif self._switch_dps:
  100. return ColorMode.ONOFF
  101. else:
  102. return ColorMode.UNKNOWN
  103. @property
  104. def raw_color_mode(self):
  105. """Return the color_mode as set from the dps."""
  106. if self._color_mode_dps:
  107. mode = self._color_mode_dps.get_value(self._device)
  108. if mode and hasattr(ColorMode, mode.upper()):
  109. return ColorMode(mode)
  110. @property
  111. def color_temp_kelvin(self):
  112. """Return the color temperature in kelvin."""
  113. if self._color_temp_dps:
  114. return self._color_temp_dps.get_value(self._device)
  115. @property
  116. def is_on(self):
  117. """Return the current state."""
  118. if self._switch_dps:
  119. return self._switch_dps.get_value(self._device)
  120. elif self._brightness_dps:
  121. b = self.brightness
  122. return isinstance(b, int) and b > 0
  123. else:
  124. # There shouldn't be lights without control, but if there are,
  125. # assume always on if they are responding
  126. return self.available
  127. @property
  128. def brightness(self):
  129. """Get the current brightness of the light"""
  130. if self.raw_color_mode == ColorMode.HS and self._rgbhsv_dps:
  131. return self._hsv_brightness
  132. return self._white_brightness
  133. @property
  134. def _white_brightness(self):
  135. if self._brightness_dps:
  136. r = self._brightness_dps.range(self._device)
  137. val = self._brightness_dps.get_value(self._device)
  138. if r and val is not None:
  139. val = color_util.value_to_brightness(r, val)
  140. return val
  141. @property
  142. def _unpacked_rgbhsv(self):
  143. """Get the unpacked rgbhsv data"""
  144. if self._rgbhsv_dps:
  145. color = self._rgbhsv_dps.decoded_value(self._device)
  146. fmt = self._rgbhsv_dps.format
  147. if fmt and color:
  148. vals = unpack(fmt.get("format"), color)
  149. idx = 0
  150. rgbhsv = {}
  151. for v in vals:
  152. # HA range: s = 0-100, rgbv = 0-255, h = 0-360
  153. n = fmt["names"][idx]
  154. r = fmt["ranges"][idx]
  155. mx = r["max"]
  156. scale = 1
  157. if n == "h":
  158. scale = 360 / mx
  159. elif n == "s":
  160. scale = 100 / mx
  161. else:
  162. scale = 255 / mx
  163. rgbhsv[n] = round(scale * v)
  164. idx += 1
  165. return rgbhsv
  166. @property
  167. def _hsv_brightness(self):
  168. """Get the colour mode brightness from the light"""
  169. rgbhsv = self._unpacked_rgbhsv
  170. if rgbhsv:
  171. return rgbhsv.get("v", self._white_brightness)
  172. return self._white_brightness
  173. @property
  174. def hs_color(self):
  175. """Get the current hs color of the light"""
  176. rgbhsv = self._unpacked_rgbhsv
  177. if rgbhsv:
  178. if "h" in rgbhsv and "s" in rgbhsv:
  179. hs = (rgbhsv["h"], rgbhsv["s"])
  180. else:
  181. r = rgbhsv.get("r")
  182. g = rgbhsv.get("g")
  183. b = rgbhsv.get("b")
  184. hs = color_util.color_RGB_to_hs(r, g, b)
  185. return hs
  186. @property
  187. def effect_list(self):
  188. """Return the list of valid effects for the light"""
  189. if self._effect_dps:
  190. return self._effect_dps.values(self._device)
  191. elif self._color_mode_dps:
  192. effects = [
  193. effect
  194. for effect in self._color_mode_dps.values(self._device)
  195. if effect and not hasattr(ColorMode, effect.upper())
  196. ]
  197. effects.append(EFFECT_OFF)
  198. return effects
  199. @property
  200. def effect(self):
  201. """Return the current effect setting of this light"""
  202. if self._effect_dps:
  203. return self._effect_dps.get_value(self._device)
  204. elif self._color_mode_dps:
  205. mode = self._color_mode_dps.get_value(self._device)
  206. if mode and not hasattr(ColorMode, mode.upper()):
  207. return mode
  208. return EFFECT_OFF
  209. async def async_turn_on(self, **params):
  210. settings = {}
  211. color_mode = None
  212. if self._color_mode_dps and ATTR_WHITE in params:
  213. if self.color_mode != ColorMode.WHITE:
  214. color_mode = ColorMode.WHITE
  215. if ATTR_BRIGHTNESS not in params and self._brightness_dps:
  216. bright = params.get(ATTR_WHITE)
  217. _LOGGER.debug(
  218. "Setting brightness via WHITE parameter to %d",
  219. bright,
  220. )
  221. r = self._brightness_dps.range(self._device)
  222. if r:
  223. bright = color_util.brightness_to_value(r, bright)
  224. settings = {
  225. **settings,
  226. **self._brightness_dps.get_values_to_set(
  227. self._device,
  228. bright,
  229. ),
  230. }
  231. elif self._color_temp_dps and ATTR_COLOR_TEMP_KELVIN in params:
  232. if self.color_mode != ColorMode.COLOR_TEMP:
  233. color_mode = ColorMode.COLOR_TEMP
  234. color_temp = params.get(ATTR_COLOR_TEMP_KELVIN)
  235. # Light groups use the widest range from the lights in the
  236. # group, so we are expected to silently handle out of range values
  237. if color_temp < self.min_color_temp_kelvin:
  238. color_temp = self.min_color_temp_kelvin
  239. if color_temp > self.max_color_temp_kelvin:
  240. color_temp = self.max_color_temp_kelvin
  241. _LOGGER.debug("Setting color temp to %d", color_temp)
  242. settings = {
  243. **settings,
  244. **self._color_temp_dps.get_values_to_set(
  245. self._device,
  246. color_temp,
  247. ),
  248. }
  249. elif self._rgbhsv_dps and (
  250. ATTR_HS_COLOR in params
  251. or (ATTR_BRIGHTNESS in params and self.raw_color_mode == ColorMode.HS)
  252. ):
  253. if self.raw_color_mode != ColorMode.HS:
  254. color_mode = ColorMode.HS
  255. hs = params.get(ATTR_HS_COLOR, self.hs_color or (0, 0))
  256. brightness = params.get(ATTR_BRIGHTNESS, self.brightness or 255)
  257. fmt = self._rgbhsv_dps.format
  258. if hs and fmt:
  259. rgb = color_util.color_hsv_to_RGB(*hs, brightness / 2.55)
  260. rgbhsv = {
  261. "r": rgb[0],
  262. "g": rgb[1],
  263. "b": rgb[2],
  264. "h": hs[0],
  265. "s": hs[1],
  266. "v": brightness,
  267. }
  268. _LOGGER.debug(
  269. "Setting color as R:%d,G:%d,B:%d,H:%d,S:%d,V:%d",
  270. rgb[0],
  271. rgb[1],
  272. rgb[2],
  273. hs[0],
  274. hs[1],
  275. brightness,
  276. )
  277. ordered = []
  278. idx = 0
  279. for n in fmt["names"]:
  280. r = fmt["ranges"][idx]
  281. scale = 1
  282. if n == "s":
  283. scale = r["max"] / 100
  284. elif n == "h":
  285. scale = r["max"] / 360
  286. else:
  287. scale = r["max"] / 255
  288. val = round(rgbhsv[n] * scale)
  289. if val < r["min"]:
  290. _LOGGER.warning(
  291. "%s/%s: Color data %s=%d constrained to be above %d",
  292. self._config._device.config,
  293. self.name or "light",
  294. n,
  295. val,
  296. r["min"],
  297. )
  298. val = r["min"]
  299. ordered.append(val)
  300. idx += 1
  301. binary = pack(fmt["format"], *ordered)
  302. settings = {
  303. **settings,
  304. **self._rgbhsv_dps.get_values_to_set(
  305. self._device,
  306. self._rgbhsv_dps.encode_value(binary),
  307. ),
  308. }
  309. if self._color_mode_dps:
  310. if color_mode:
  311. _LOGGER.debug("Auto setting color mode to %s", color_mode)
  312. settings = {
  313. **settings,
  314. **self._color_mode_dps.get_values_to_set(
  315. self._device,
  316. color_mode,
  317. ),
  318. }
  319. elif not self._effect_dps:
  320. effect = params.get(ATTR_EFFECT)
  321. if effect:
  322. if effect == EFFECT_OFF:
  323. # Turn off the effect. Ideally this should keep the
  324. # previous mode, but since the mode is shared with
  325. # effect, use the default, or first in the list
  326. effect = (
  327. self._color_mode_dps.default
  328. or self._color_mode_dps.values(self.device)[0]
  329. )
  330. _LOGGER.debug(
  331. "Emulating effect using color mode of %s",
  332. effect,
  333. )
  334. settings = {
  335. **settings,
  336. **self._color_mode_dps.get_values_to_set(
  337. self._device,
  338. effect,
  339. ),
  340. }
  341. if (
  342. ATTR_BRIGHTNESS in params
  343. and (
  344. (self.raw_color_mode != ColorMode.HS and color_mode is None)
  345. or (color_mode != ColorMode.HS and color_mode is not None)
  346. )
  347. and self._brightness_dps
  348. ):
  349. bright = params.get(ATTR_BRIGHTNESS)
  350. _LOGGER.debug("Setting brightness to %s", bright)
  351. r = self._brightness_dps.range(self._device)
  352. if r:
  353. bright = color_util.brightness_to_value(r, bright)
  354. settings = {
  355. **settings,
  356. **self._brightness_dps.get_values_to_set(
  357. self._device,
  358. bright,
  359. ),
  360. }
  361. if self._effect_dps:
  362. effect = params.get(ATTR_EFFECT, None)
  363. if effect:
  364. _LOGGER.debug("Setting effect to %s", effect)
  365. settings = {
  366. **settings,
  367. **self._effect_dps.get_values_to_set(
  368. self._device,
  369. effect,
  370. ),
  371. }
  372. if self._switch_dps and not self.is_on:
  373. if (
  374. self._switch_dps.readonly
  375. and self._effect_dps
  376. and "on" in self._effect_dps.values(self._device)
  377. ):
  378. # Special case for motion sensor lights with readonly switch
  379. # that have tristate switch available as effect
  380. if self._effect_dps.id not in settings:
  381. settings = settings | self._effect_dps.get_values_to_set(
  382. self._device, "on"
  383. )
  384. else:
  385. settings = settings | self._switch_dps.get_values_to_set(
  386. self._device, True
  387. )
  388. elif self._brightness_dps and not self.is_on:
  389. bright = 255
  390. r = self._brightness_dps.range(self._device)
  391. if r:
  392. bright = color_util.brightness_to_value(r, bright)
  393. settings = settings | self._brightness_dps.get_values_to_set(
  394. self._device, bright
  395. )
  396. if settings:
  397. await self._device.async_set_properties(settings)
  398. async def async_turn_off(self):
  399. if self._switch_dps:
  400. if (
  401. self._switch_dps.readonly
  402. and self._effect_dps
  403. and "off" in self._effect_dps.values(self._device)
  404. ):
  405. # Special case for motion sensor lights with readonly switch
  406. # that have tristate switch available as effect
  407. await self._effect_dps.async_set_value(self._device, "off")
  408. else:
  409. await self._switch_dps.async_set_value(self._device, False)
  410. elif self._brightness_dps:
  411. await self._brightness_dps.async_set_value(self._device, 0)
  412. else:
  413. raise NotImplementedError()
  414. async def async_toggle(self):
  415. disp_on = self.is_on
  416. await (self.async_turn_on() if not disp_on else self.async_turn_off())