Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Platform allowing several lights to be grouped into one light."""
2 
3 from __future__ import annotations
4 
5 from collections import Counter
6 import itertools
7 import logging
8 from typing import Any, cast
9 
10 import voluptuous as vol
11 
12 from homeassistant.components import light
14  ATTR_BRIGHTNESS,
15  ATTR_COLOR_MODE,
16  ATTR_COLOR_TEMP_KELVIN,
17  ATTR_EFFECT,
18  ATTR_EFFECT_LIST,
19  ATTR_FLASH,
20  ATTR_HS_COLOR,
21  ATTR_MAX_COLOR_TEMP_KELVIN,
22  ATTR_MIN_COLOR_TEMP_KELVIN,
23  ATTR_RGB_COLOR,
24  ATTR_RGBW_COLOR,
25  ATTR_RGBWW_COLOR,
26  ATTR_SUPPORTED_COLOR_MODES,
27  ATTR_TRANSITION,
28  ATTR_WHITE,
29  ATTR_XY_COLOR,
30  PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
31  ColorMode,
32  LightEntity,
33  LightEntityFeature,
34  filter_supported_color_modes,
35 )
36 from homeassistant.config_entries import ConfigEntry
37 from homeassistant.const import (
38  ATTR_ENTITY_ID,
39  ATTR_SUPPORTED_FEATURES,
40  CONF_ENTITIES,
41  CONF_NAME,
42  CONF_UNIQUE_ID,
43  SERVICE_TURN_OFF,
44  SERVICE_TURN_ON,
45  STATE_ON,
46  STATE_UNAVAILABLE,
47  STATE_UNKNOWN,
48 )
49 from homeassistant.core import HomeAssistant, callback
50 from homeassistant.helpers import config_validation as cv, entity_registry as er
51 from homeassistant.helpers.entity_platform import AddEntitiesCallback
52 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
53 
54 from .entity import GroupEntity
55 from .util import find_state_attributes, mean_tuple, reduce_attribute
56 
57 DEFAULT_NAME = "Light Group"
58 CONF_ALL = "all"
59 
60 # No limit on parallel updates to enable a group calling another group
61 PARALLEL_UPDATES = 0
62 
63 PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
64  {
65  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
66  vol.Optional(CONF_UNIQUE_ID): cv.string,
67  vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN),
68  vol.Optional(CONF_ALL): cv.boolean,
69  }
70 )
71 
72 SUPPORT_GROUP_LIGHT = (
73  LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION
74 )
75 
76 _LOGGER = logging.getLogger(__name__)
77 
78 
80  hass: HomeAssistant,
81  config: ConfigType,
82  async_add_entities: AddEntitiesCallback,
83  discovery_info: DiscoveryInfoType | None = None,
84 ) -> None:
85  """Initialize light.group platform."""
87  [
88  LightGroup(
89  config.get(CONF_UNIQUE_ID),
90  config[CONF_NAME],
91  config[CONF_ENTITIES],
92  config.get(CONF_ALL),
93  )
94  ]
95  )
96 
97 
99  hass: HomeAssistant,
100  config_entry: ConfigEntry,
101  async_add_entities: AddEntitiesCallback,
102 ) -> None:
103  """Initialize Light Group config entry."""
104  registry = er.async_get(hass)
105  entities = er.async_validate_entity_ids(
106  registry, config_entry.options[CONF_ENTITIES]
107  )
108  mode = config_entry.options.get(CONF_ALL, False)
109 
111  [LightGroup(config_entry.entry_id, config_entry.title, entities, mode)]
112  )
113 
114 
115 @callback
117  hass: HomeAssistant, name: str, validated_config: dict[str, Any]
118 ) -> LightGroup:
119  """Create a preview sensor."""
120  return LightGroup(
121  None,
122  name,
123  validated_config[CONF_ENTITIES],
124  validated_config.get(CONF_ALL, False),
125  )
126 
127 
128 FORWARDED_ATTRIBUTES = frozenset(
129  {
130  ATTR_BRIGHTNESS,
131  ATTR_COLOR_TEMP_KELVIN,
132  ATTR_EFFECT,
133  ATTR_FLASH,
134  ATTR_HS_COLOR,
135  ATTR_RGB_COLOR,
136  ATTR_RGBW_COLOR,
137  ATTR_RGBWW_COLOR,
138  ATTR_TRANSITION,
139  ATTR_WHITE,
140  ATTR_XY_COLOR,
141  }
142 )
143 
144 
146  """Representation of a light group."""
147 
148  _attr_available = False
149  _attr_icon = "mdi:lightbulb-group"
150  _attr_max_color_temp_kelvin = 6500
151  _attr_min_color_temp_kelvin = 2000
152  _attr_should_poll = False
153 
154  def __init__(
155  self, unique_id: str | None, name: str, entity_ids: list[str], mode: bool | None
156  ) -> None:
157  """Initialize a light group."""
158  self._entity_ids_entity_ids = entity_ids
159 
160  self._attr_name_attr_name = name
161  self._attr_extra_state_attributes_attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
162  self._attr_unique_id_attr_unique_id = unique_id
163  self.modemode = any
164  if mode:
165  self.modemode = all
166 
167  self._attr_color_mode_attr_color_mode = ColorMode.UNKNOWN
168  self._attr_supported_color_modes_attr_supported_color_modes = {ColorMode.ONOFF}
169 
170  async def async_turn_on(self, **kwargs: Any) -> None:
171  """Forward the turn_on command to all lights in the light group."""
172  data = {
173  key: value for key, value in kwargs.items() if key in FORWARDED_ATTRIBUTES
174  }
175  data[ATTR_ENTITY_ID] = self._entity_ids_entity_ids
176 
177  _LOGGER.debug("Forwarded turn_on command: %s", data)
178 
179  await self.hasshass.services.async_call(
180  light.DOMAIN,
181  SERVICE_TURN_ON,
182  data,
183  blocking=True,
184  context=self._context_context,
185  )
186 
187  async def async_turn_off(self, **kwargs: Any) -> None:
188  """Forward the turn_off command to all lights in the light group."""
189  data = {ATTR_ENTITY_ID: self._entity_ids_entity_ids}
190 
191  if ATTR_TRANSITION in kwargs:
192  data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION]
193 
194  await self.hasshass.services.async_call(
195  light.DOMAIN,
196  SERVICE_TURN_OFF,
197  data,
198  blocking=True,
199  context=self._context_context,
200  )
201 
202  @callback
203  def async_update_group_state(self) -> None:
204  """Query all members and determine the light group state."""
205  states = [
206  state
207  for entity_id in self._entity_ids_entity_ids
208  if (state := self.hasshass.states.get(entity_id)) is not None
209  ]
210  on_states = [state for state in states if state.state == STATE_ON]
211 
212  valid_state = self.modemode(
213  state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
214  )
215 
216  if not valid_state:
217  # Set as unknown if any / all member is unknown or unavailable
218  self._attr_is_on_attr_is_on = None
219  else:
220  # Set as ON if any / all member is ON
221  self._attr_is_on_attr_is_on = self.modemode(state.state == STATE_ON for state in states)
222 
223  self._attr_available_attr_available_attr_available = any(state.state != STATE_UNAVAILABLE for state in states)
224  self._attr_brightness_attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS)
225 
226  self._attr_hs_color_attr_hs_color = reduce_attribute(
227  on_states, ATTR_HS_COLOR, reduce=mean_tuple
228  )
229  self._attr_rgb_color_attr_rgb_color = reduce_attribute(
230  on_states, ATTR_RGB_COLOR, reduce=mean_tuple
231  )
232  self._attr_rgbw_color_attr_rgbw_color = reduce_attribute(
233  on_states, ATTR_RGBW_COLOR, reduce=mean_tuple
234  )
235  self._attr_rgbww_color_attr_rgbww_color = reduce_attribute(
236  on_states, ATTR_RGBWW_COLOR, reduce=mean_tuple
237  )
238  self._attr_xy_color_attr_xy_color = reduce_attribute(
239  on_states, ATTR_XY_COLOR, reduce=mean_tuple
240  )
241 
242  self._attr_color_temp_kelvin_attr_color_temp_kelvin = reduce_attribute(
243  on_states, ATTR_COLOR_TEMP_KELVIN
244  )
246  states, ATTR_MIN_COLOR_TEMP_KELVIN, default=2000, reduce=min
247  )
249  states, ATTR_MAX_COLOR_TEMP_KELVIN, default=6500, reduce=max
250  )
251 
252  self._attr_effect_list_attr_effect_list = None
253  all_effect_lists = list(find_state_attributes(states, ATTR_EFFECT_LIST))
254  if all_effect_lists:
255  # Merge all effects from all effect_lists with a union merge.
256  self._attr_effect_list_attr_effect_list = list(set().union(*all_effect_lists))
257  self._attr_effect_list_attr_effect_list.sort()
258  if "None" in self._attr_effect_list_attr_effect_list:
259  self._attr_effect_list_attr_effect_list.remove("None")
260  self._attr_effect_list_attr_effect_list.insert(0, "None")
261 
262  self._attr_effect_attr_effect = None
263  all_effects = list(find_state_attributes(on_states, ATTR_EFFECT))
264  if all_effects:
265  # Report the most common effect.
266  effects_count = Counter(itertools.chain(all_effects))
267  self._attr_effect_attr_effect = effects_count.most_common(1)[0][0]
268 
269  supported_color_modes = {ColorMode.ONOFF}
270  all_supported_color_modes = list(
271  find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES)
272  )
273  if all_supported_color_modes:
274  # Merge all color modes.
275  supported_color_modes = filter_supported_color_modes(
276  cast(set[ColorMode], set().union(*all_supported_color_modes))
277  )
278  self._attr_supported_color_modes_attr_supported_color_modes = supported_color_modes
279 
280  self._attr_color_mode_attr_color_mode = ColorMode.UNKNOWN
281  all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE))
282  if all_color_modes:
283  # Report the most common color mode, select brightness and onoff last
284  color_mode_count = Counter(itertools.chain(all_color_modes))
285  if ColorMode.ONOFF in color_mode_count:
286  if ColorMode.ONOFF in supported_color_modes:
287  color_mode_count[ColorMode.ONOFF] = -1
288  else:
289  color_mode_count.pop(ColorMode.ONOFF)
290  if ColorMode.BRIGHTNESS in color_mode_count:
291  if ColorMode.BRIGHTNESS in supported_color_modes:
292  color_mode_count[ColorMode.BRIGHTNESS] = 0
293  else:
294  color_mode_count.pop(ColorMode.BRIGHTNESS)
295  if color_mode_count:
296  self._attr_color_mode_attr_color_mode = color_mode_count.most_common(1)[0][0]
297  else:
298  self._attr_color_mode_attr_color_mode = next(iter(supported_color_modes))
299 
300  self._attr_supported_features_attr_supported_features = LightEntityFeature(0)
301  for support in find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
302  # Merge supported features by emulating support for every feature
303  # we find.
304  self._attr_supported_features_attr_supported_features |= support
305  # Bitwise-and the supported features with the GroupedLight's features
306  # so that we don't break in the future when a new feature is added.
307  self._attr_supported_features_attr_supported_features &= SUPPORT_GROUP_LIGHT
None __init__(self, str|None unique_id, str name, list[str] entity_ids, bool|None mode)
Definition: light.py:156
None async_turn_off(self, **Any kwargs)
Definition: light.py:187
None async_turn_on(self, **Any kwargs)
Definition: light.py:170
bool remove(self, _T matcher)
Definition: match.py:214
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:102
LightGroup async_create_preview_light(HomeAssistant hass, str name, dict[str, Any] validated_config)
Definition: light.py:118
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: light.py:84
Iterator[Any] find_state_attributes(list[State] states, str key)
Definition: util.py:12
Any reduce_attribute(list[State] states, str key, Any|None default=None, Callable[..., Any] reduce=mean_int)
Definition: util.py:72
set[ColorMode] filter_supported_color_modes(Iterable[ColorMode] color_modes)
Definition: __init__.py:122