Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Light for Shelly."""
2 
3 from __future__ import annotations
4 
5 from typing import Any, cast
6 
7 from aioshelly.block_device import Block
8 from aioshelly.const import MODEL_BULB, RPC_GENERATIONS
9 
11  ATTR_BRIGHTNESS,
12  ATTR_COLOR_TEMP_KELVIN,
13  ATTR_EFFECT,
14  ATTR_RGB_COLOR,
15  ATTR_RGBW_COLOR,
16  ATTR_TRANSITION,
17  DOMAIN as LIGHT_DOMAIN,
18  ColorMode,
19  LightEntity,
20  LightEntityFeature,
21  brightness_supported,
22 )
23 from homeassistant.core import HomeAssistant, callback
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 
26 from .const import (
27  BLOCK_MAX_TRANSITION_TIME_MS,
28  DUAL_MODE_LIGHT_MODELS,
29  KELVIN_MAX_VALUE,
30  KELVIN_MIN_VALUE_COLOR,
31  KELVIN_MIN_VALUE_WHITE,
32  LOGGER,
33  MODELS_SUPPORTING_LIGHT_TRANSITION,
34  RGBW_MODELS,
35  RPC_MIN_TRANSITION_TIME_SEC,
36  SHBLB_1_RGB_EFFECTS,
37  STANDARD_RGB_EFFECTS,
38 )
39 from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
40 from .entity import ShellyBlockEntity, ShellyRpcEntity
41 from .utils import (
42  async_remove_orphaned_entities,
43  async_remove_shelly_entity,
44  brightness_to_percentage,
45  get_device_entry_gen,
46  get_rpc_key_ids,
47  is_block_channel_type_light,
48  is_rpc_channel_type_light,
49  percentage_to_brightness,
50 )
51 
52 
54  hass: HomeAssistant,
55  config_entry: ShellyConfigEntry,
56  async_add_entities: AddEntitiesCallback,
57 ) -> None:
58  """Set up lights for device."""
59  if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
60  return async_setup_rpc_entry(hass, config_entry, async_add_entities)
61 
62  return async_setup_block_entry(hass, config_entry, async_add_entities)
63 
64 
65 @callback
67  hass: HomeAssistant,
68  config_entry: ShellyConfigEntry,
69  async_add_entities: AddEntitiesCallback,
70 ) -> None:
71  """Set up entities for block device."""
72  coordinator = config_entry.runtime_data.block
73  assert coordinator
74  blocks = []
75  assert coordinator.device.blocks
76  for block in coordinator.device.blocks:
77  if block.type == "light":
78  blocks.append(block)
79  elif block.type == "relay" and block.channel is not None:
81  coordinator.device.settings, int(block.channel)
82  ):
83  continue
84 
85  blocks.append(block)
86  unique_id = f"{coordinator.mac}-{block.type}_{block.channel}"
87  async_remove_shelly_entity(hass, "switch", unique_id)
88 
89  if not blocks:
90  return
91 
92  async_add_entities(BlockShellyLight(coordinator, block) for block in blocks)
93 
94 
95 @callback
97  hass: HomeAssistant,
98  config_entry: ShellyConfigEntry,
99  async_add_entities: AddEntitiesCallback,
100 ) -> None:
101  """Set up entities for RPC device."""
102  coordinator = config_entry.runtime_data.rpc
103  assert coordinator
104  switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch")
105 
106  switch_ids = []
107  for id_ in switch_key_ids:
108  if not is_rpc_channel_type_light(coordinator.device.config, id_):
109  continue
110 
111  switch_ids.append(id_)
112  unique_id = f"{coordinator.mac}-switch:{id_}"
113  async_remove_shelly_entity(hass, "switch", unique_id)
114 
115  if switch_ids:
117  RpcShellySwitchAsLight(coordinator, id_) for id_ in switch_ids
118  )
119  return
120 
121  entities: list[RpcShellyLightBase] = []
122  if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
123  entities.extend(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
124  if cct_key_ids := get_rpc_key_ids(coordinator.device.status, "cct"):
125  entities.extend(RpcShellyCctLight(coordinator, id_) for id_ in cct_key_ids)
126  if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
127  entities.extend(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
128  if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
129  entities.extend(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
130 
131  async_add_entities(entities)
132 
134  hass,
135  config_entry.entry_id,
136  coordinator.mac,
137  LIGHT_DOMAIN,
138  coordinator.device.status,
139  )
140 
141 
143  """Entity that controls a light on block based Shelly devices."""
144 
145  _attr_supported_color_modes: set[str]
146 
147  def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
148  """Initialize light."""
149  super().__init__(coordinator, block)
150  self.control_resultcontrol_result: dict[str, Any] | None = None
151  self._attr_supported_color_modes_attr_supported_color_modes = set()
152  self._attr_min_color_temp_kelvin_attr_min_color_temp_kelvin = KELVIN_MIN_VALUE_WHITE
153  self._attr_max_color_temp_kelvin_attr_max_color_temp_kelvin = KELVIN_MAX_VALUE
154 
155  if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
156  self._attr_min_color_temp_kelvin_attr_min_color_temp_kelvin = KELVIN_MIN_VALUE_COLOR
157  if coordinator.model in RGBW_MODELS:
158  self._attr_supported_color_modes_attr_supported_color_modes.add(ColorMode.RGBW)
159  else:
160  self._attr_supported_color_modes_attr_supported_color_modes.add(ColorMode.RGB)
161 
162  if hasattr(block, "colorTemp"):
163  self._attr_supported_color_modes_attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
164 
165  if not self._attr_supported_color_modes_attr_supported_color_modes:
166  if hasattr(block, "brightness") or hasattr(block, "gain"):
167  self._attr_supported_color_modes_attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
168  else:
169  self._attr_supported_color_modes_attr_supported_color_modes.add(ColorMode.ONOFF)
170 
171  if hasattr(block, "effect"):
172  self._attr_supported_features |= LightEntityFeature.EFFECT
173 
174  if coordinator.model in MODELS_SUPPORTING_LIGHT_TRANSITION:
175  self._attr_supported_features |= LightEntityFeature.TRANSITION
176 
177  @property
178  def is_on(self) -> bool:
179  """If light is on."""
180  if self.control_resultcontrol_result:
181  return cast(bool, self.control_resultcontrol_result["ison"])
182 
183  return bool(self.blockblock.output)
184 
185  @property
186  def mode(self) -> str:
187  """Return the color mode of the light."""
188  if self.control_resultcontrol_result and self.control_resultcontrol_result.get("mode"):
189  return cast(str, self.control_resultcontrol_result["mode"])
190 
191  if hasattr(self.blockblock, "mode"):
192  return cast(str, self.blockblock.mode)
193 
194  if (
195  hasattr(self.blockblock, "red")
196  and hasattr(self.blockblock, "green")
197  and hasattr(self.blockblock, "blue")
198  ):
199  return "color"
200 
201  return "white"
202 
203  @property
204  def brightness(self) -> int:
205  """Return the brightness of this light between 0..255."""
206  if self.modemodemode == "color":
207  if self.control_resultcontrol_result:
208  return percentage_to_brightness(self.control_resultcontrol_result["gain"])
209  return percentage_to_brightness(cast(int, self.blockblock.gain))
210 
211  # white mode
212  if self.control_resultcontrol_result:
213  return percentage_to_brightness(self.control_resultcontrol_result["brightness"])
214  return percentage_to_brightness(cast(int, self.blockblock.brightness))
215 
216  @property
217  def color_mode(self) -> ColorMode:
218  """Return the color mode of the light."""
219  if self.modemodemode == "color":
220  if self.coordinator.model in RGBW_MODELS:
221  return ColorMode.RGBW
222  return ColorMode.RGB
223 
224  if hasattr(self.blockblock, "colorTemp"):
225  return ColorMode.COLOR_TEMP
226 
227  if hasattr(self.blockblock, "brightness") or hasattr(self.blockblock, "gain"):
228  return ColorMode.BRIGHTNESS
229 
230  return ColorMode.ONOFF
231 
232  @property
233  def rgb_color(self) -> tuple[int, int, int]:
234  """Return the rgb color value [int, int, int]."""
235  if self.control_resultcontrol_result:
236  red = self.control_resultcontrol_result["red"]
237  green = self.control_resultcontrol_result["green"]
238  blue = self.control_resultcontrol_result["blue"]
239  else:
240  red = self.blockblock.red
241  green = self.blockblock.green
242  blue = self.blockblock.blue
243  return (cast(int, red), cast(int, green), cast(int, blue))
244 
245  @property
246  def rgbw_color(self) -> tuple[int, int, int, int]:
247  """Return the rgbw color value [int, int, int, int]."""
248  if self.control_resultcontrol_result:
249  white = self.control_resultcontrol_result["white"]
250  else:
251  white = self.blockblock.white
252 
253  return (*self.rgb_colorrgb_colorrgb_color, cast(int, white))
254 
255  @property
256  def color_temp_kelvin(self) -> int:
257  """Return the CT color value in kelvin."""
258  color_temp = cast(int, self.blockblock.colorTemp)
259  if self.control_resultcontrol_result:
260  color_temp = self.control_resultcontrol_result["temp"]
261 
262  return min(
263  self.max_color_temp_kelvinmax_color_temp_kelvin,
264  max(self.min_color_temp_kelvinmin_color_temp_kelvin, color_temp),
265  )
266 
267  @property
268  def effect_list(self) -> list[str] | None:
269  """Return the list of supported effects."""
270  if self.coordinator.model == MODEL_BULB:
271  return list(SHBLB_1_RGB_EFFECTS.values())
272 
273  return list(STANDARD_RGB_EFFECTS.values())
274 
275  @property
276  def effect(self) -> str | None:
277  """Return the current effect."""
278  if self.control_resultcontrol_result:
279  effect_index = self.control_resultcontrol_result["effect"]
280  else:
281  effect_index = self.blockblock.effect
282 
283  if self.coordinator.model == MODEL_BULB:
284  return SHBLB_1_RGB_EFFECTS[cast(int, effect_index)]
285 
286  return STANDARD_RGB_EFFECTS[cast(int, effect_index)]
287 
288  async def async_turn_on(self, **kwargs: Any) -> None:
289  """Turn on light."""
290  if self.blockblock.type == "relay":
291  self.control_resultcontrol_result = await self.set_stateset_state(turn="on")
292  self.async_write_ha_stateasync_write_ha_state()
293  return
294 
295  set_mode = None
296  supported_color_modes = self._attr_supported_color_modes_attr_supported_color_modes
297  params: dict[str, Any] = {"turn": "on"}
298 
299  if ATTR_TRANSITION in kwargs:
300  params["transition"] = min(
301  int(kwargs[ATTR_TRANSITION] * 1000), BLOCK_MAX_TRANSITION_TIME_MS
302  )
303 
304  if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes):
305  if hasattr(self.blockblock, "gain"):
306  params["gain"] = brightness_to_percentage(kwargs[ATTR_BRIGHTNESS])
307  if hasattr(self.blockblock, "brightness"):
308  params["brightness"] = brightness_to_percentage(kwargs[ATTR_BRIGHTNESS])
309 
310  if (
311  ATTR_COLOR_TEMP_KELVIN in kwargs
312  and ColorMode.COLOR_TEMP in supported_color_modes
313  ):
314  # Color temperature change - used only in white mode,
315  # switch device mode to white
316  color_temp = kwargs[ATTR_COLOR_TEMP_KELVIN]
317  set_mode = "white"
318  params["temp"] = int(
319  min(
320  self.max_color_temp_kelvinmax_color_temp_kelvin,
321  max(self.min_color_temp_kelvinmin_color_temp_kelvin, color_temp),
322  )
323  )
324 
325  if ATTR_RGB_COLOR in kwargs and ColorMode.RGB in supported_color_modes:
326  # Color channels change - used only in color mode,
327  # switch device mode to color
328  set_mode = "color"
329  (params["red"], params["green"], params["blue"]) = kwargs[ATTR_RGB_COLOR]
330 
331  if ATTR_RGBW_COLOR in kwargs and ColorMode.RGBW in supported_color_modes:
332  # Color channels change - used only in color mode,
333  # switch device mode to color
334  set_mode = "color"
335  (params["red"], params["green"], params["blue"], params["white"]) = kwargs[
336  ATTR_RGBW_COLOR
337  ]
338 
339  if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP_KELVIN not in kwargs:
340  # Color effect change - used only in color mode, switch device mode to color
341  set_mode = "color"
342  if self.coordinator.model == MODEL_BULB:
343  effect_dict = SHBLB_1_RGB_EFFECTS
344  else:
345  effect_dict = STANDARD_RGB_EFFECTS
346  if kwargs[ATTR_EFFECT] in effect_dict.values():
347  params["effect"] = [
348  k for k, v in effect_dict.items() if v == kwargs[ATTR_EFFECT]
349  ][0]
350  else:
351  LOGGER.error(
352  "Effect '%s' not supported by device %s",
353  kwargs[ATTR_EFFECT],
354  self.coordinator.model,
355  )
356 
357  if (
358  set_mode
359  and set_mode != self.modemodemode
360  and self.coordinator.model in DUAL_MODE_LIGHT_MODELS
361  ):
362  params["mode"] = set_mode
363 
364  self.control_resultcontrol_result = await self.set_stateset_state(**params)
365  self.async_write_ha_stateasync_write_ha_state()
366 
367  async def async_turn_off(self, **kwargs: Any) -> None:
368  """Turn off light."""
369  params: dict[str, Any] = {"turn": "off"}
370 
371  if ATTR_TRANSITION in kwargs:
372  params["transition"] = min(
373  int(kwargs[ATTR_TRANSITION] * 1000), BLOCK_MAX_TRANSITION_TIME_MS
374  )
375 
376  self.control_resultcontrol_result = await self.set_stateset_state(**params)
377 
378  self.async_write_ha_stateasync_write_ha_state()
379 
380  @callback
381  def _update_callback(self) -> None:
382  """When device updates, clear control & mode result that overrides state."""
383  self.control_resultcontrol_result = None
384  super()._update_callback()
385 
386 
388  """Base Entity for RPC based Shelly devices."""
389 
390  _component: str = "Light"
391 
392  def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
393  """Initialize light."""
394  super().__init__(coordinator, f"{self._component.lower()}:{id_}")
395  self._id_id = id_
396 
397  @property
398  def is_on(self) -> bool:
399  """If light is on."""
400  return bool(self.statusstatus["output"])
401 
402  @property
403  def brightness(self) -> int:
404  """Return the brightness of this light between 0..255."""
405  return percentage_to_brightness(self.statusstatus["brightness"])
406 
407  @property
408  def rgb_color(self) -> tuple[int, int, int]:
409  """Return the rgb color value [int, int, int]."""
410  return cast(tuple, self.statusstatus["rgb"])
411 
412  @property
413  def rgbw_color(self) -> tuple[int, int, int, int]:
414  """Return the rgbw color value [int, int, int, int]."""
415  return (*self.statusstatus["rgb"], self.statusstatus["white"])
416 
417  async def async_turn_on(self, **kwargs: Any) -> None:
418  """Turn on light."""
419  params: dict[str, Any] = {"id": self._id_id, "on": True}
420 
421  if ATTR_BRIGHTNESS in kwargs:
422  params["brightness"] = brightness_to_percentage(kwargs[ATTR_BRIGHTNESS])
423 
424  if ATTR_COLOR_TEMP_KELVIN in kwargs:
425  params["ct"] = kwargs[ATTR_COLOR_TEMP_KELVIN]
426 
427  if ATTR_TRANSITION in kwargs:
428  params["transition_duration"] = max(
429  kwargs[ATTR_TRANSITION], RPC_MIN_TRANSITION_TIME_SEC
430  )
431 
432  if ATTR_RGB_COLOR in kwargs:
433  params["rgb"] = list(kwargs[ATTR_RGB_COLOR])
434 
435  if ATTR_RGBW_COLOR in kwargs:
436  params["rgb"] = list(kwargs[ATTR_RGBW_COLOR][:-1])
437  params["white"] = kwargs[ATTR_RGBW_COLOR][-1]
438 
439  await self.call_rpccall_rpc(f"{self._component}.Set", params)
440 
441  async def async_turn_off(self, **kwargs: Any) -> None:
442  """Turn off light."""
443  params: dict[str, Any] = {"id": self._id_id, "on": False}
444 
445  if ATTR_TRANSITION in kwargs:
446  params["transition_duration"] = max(
447  kwargs[ATTR_TRANSITION], RPC_MIN_TRANSITION_TIME_SEC
448  )
449 
450  await self.call_rpccall_rpc(f"{self._component}.Set", params)
451 
452 
454  """Entity that controls a relay as light on RPC based Shelly devices."""
455 
456  _component = "Switch"
457 
458  _attr_color_mode = ColorMode.ONOFF
459  _attr_supported_color_modes = {ColorMode.ONOFF}
460 
461 
463  """Entity that controls a light on RPC based Shelly devices."""
464 
465  _component = "Light"
466 
467  _attr_color_mode = ColorMode.BRIGHTNESS
468  _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
469  _attr_supported_features = LightEntityFeature.TRANSITION
470 
471 
473  """Entity that controls a CCT light on RPC based Shelly devices."""
474 
475  _component = "CCT"
476 
477  _attr_color_mode = ColorMode.COLOR_TEMP
478  _attr_supported_color_modes = {ColorMode.COLOR_TEMP}
479  _attr_supported_features = LightEntityFeature.TRANSITION
480 
481  def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None:
482  """Initialize light."""
483  color_temp_range = coordinator.device.config[f"cct:{id_}"]["ct_range"]
484  self._attr_min_color_temp_kelvin_attr_min_color_temp_kelvin = color_temp_range[0]
485  self._attr_max_color_temp_kelvin_attr_max_color_temp_kelvin = color_temp_range[1]
486 
487  super().__init__(coordinator, id_)
488 
489  @property
490  def color_temp_kelvin(self) -> int:
491  """Return the CT color value in Kelvin."""
492  return cast(int, self.statusstatus["ct"])
493 
494 
496  """Entity that controls a RGB light on RPC based Shelly devices."""
497 
498  _component = "RGB"
499 
500  _attr_color_mode = ColorMode.RGB
501  _attr_supported_color_modes = {ColorMode.RGB}
502  _attr_supported_features = LightEntityFeature.TRANSITION
503 
504 
506  """Entity that controls a RGBW light on RPC based Shelly devices."""
507 
508  _component = "RGBW"
509 
510  _attr_color_mode = ColorMode.RGBW
511  _attr_supported_color_modes = {ColorMode.RGBW}
512  _attr_supported_features = LightEntityFeature.TRANSITION
tuple[int, int, int]|None rgb_color(self)
Definition: __init__.py:957
Any call_rpc(self, str method, Any params)
Definition: entity.py:384
tuple[int, int, int, int] rgbw_color(self)
Definition: light.py:246
None __init__(self, ShellyBlockCoordinator coordinator, Block block)
Definition: light.py:147
None __init__(self, ShellyRpcCoordinator coordinator, int id_)
Definition: light.py:481
None __init__(self, ShellyRpcCoordinator coordinator, int id_)
Definition: light.py:392
tuple[int, int, int, int] rgbw_color(self)
Definition: light.py:413
bool add(self, _T matcher)
Definition: match.py:185
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool brightness_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:155
None async_setup_block_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:70
None async_setup_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:57
None async_setup_rpc_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:100
bool is_rpc_channel_type_light(dict[str, Any] config, int channel)
Definition: utils.py:387
int percentage_to_brightness(int percentage)
Definition: utils.py:443
None async_remove_orphaned_entities(HomeAssistant hass, str config_entry_id, str mac, str platform, Iterable[str] keys, str|None key_suffix=None)
Definition: utils.py:555
int brightness_to_percentage(int brightness)
Definition: utils.py:438
list[int] get_rpc_key_ids(dict[str, Any] keys_dict, str key)
Definition: utils.py:369
int get_device_entry_gen(ConfigEntry entry)
Definition: utils.py:353
bool is_block_channel_type_light(dict[str, Any] settings, int channel)
Definition: utils.py:381
None async_remove_shelly_entity(HomeAssistant hass, str domain, str unique_id)
Definition: utils.py:67