Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Provide the functionality to group entities."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Collection
7 import logging
8 from typing import Any
9 
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.const import (
14  ATTR_ENTITY_ID, # noqa: F401
15  ATTR_ICON,
16  ATTR_NAME,
17  CONF_ENTITIES,
18  CONF_ICON,
19  CONF_NAME,
20  SERVICE_RELOAD,
21  Platform,
22 )
23 from homeassistant.core import HomeAssistant, ServiceCall
24 from homeassistant.helpers import config_validation as cv, entity_registry as er
25 from homeassistant.helpers.group import (
26  expand_entity_ids as _expand_entity_ids,
27  get_entity_ids as _get_entity_ids,
28 )
29 from homeassistant.helpers.reload import async_reload_integration_platforms
30 from homeassistant.helpers.typing import ConfigType
31 from homeassistant.loader import bind_hass
32 
33 #
34 # Below we ensure the config_flow is imported so it does not need the import
35 # executor later.
36 #
37 # Since group is pre-imported, the loader will not get a chance to pre-import
38 # the config flow as there is no run time import of the group component in the
39 # executor.
40 #
41 from . import config_flow as config_flow_pre_import # noqa: F401
42 from .const import ( # noqa: F401
43  ATTR_ADD_ENTITIES,
44  ATTR_ALL,
45  ATTR_AUTO,
46  ATTR_ENTITIES,
47  ATTR_OBJECT_ID,
48  ATTR_ORDER,
49  ATTR_REMOVE_ENTITIES,
50  CONF_HIDE_MEMBERS,
51  DATA_COMPONENT,
52  DOMAIN,
53  GROUP_ORDER,
54  REG_KEY,
55 )
56 from .entity import Group, async_get_component
57 from .registry import async_setup as async_setup_registry
58 
59 CONF_ALL = "all"
60 
61 
62 SERVICE_SET = "set"
63 SERVICE_REMOVE = "remove"
64 
65 PLATFORMS = [
66  Platform.BINARY_SENSOR,
67  Platform.COVER,
68  Platform.FAN,
69  Platform.LIGHT,
70  Platform.LOCK,
71  Platform.MEDIA_PLAYER,
72  Platform.NOTIFY,
73  Platform.SENSOR,
74  Platform.SWITCH,
75 ]
76 
77 _LOGGER = logging.getLogger(__name__)
78 
79 
80 def _conf_preprocess(value: Any) -> dict[str, Any]:
81  """Preprocess alternative configuration formats."""
82  if not isinstance(value, dict):
83  return {CONF_ENTITIES: value}
84 
85  return value
86 
87 
88 GROUP_SCHEMA = vol.All(
89  vol.Schema(
90  {
91  vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
92  CONF_NAME: cv.string,
93  CONF_ICON: cv.icon,
94  CONF_ALL: cv.boolean,
95  }
96  )
97 )
98 
99 CONFIG_SCHEMA = vol.Schema(
100  {DOMAIN: vol.Schema({cv.match_all: vol.All(_conf_preprocess, GROUP_SCHEMA)})},
101  extra=vol.ALLOW_EXTRA,
102 )
103 
104 
105 @bind_hass
106 def is_on(hass: HomeAssistant, entity_id: str) -> bool:
107  """Test if the group state is in its ON-state."""
108  if REG_KEY not in hass.data:
109  # Integration not setup yet, it cannot be on
110  return False
111 
112  if (state := hass.states.get(entity_id)) is not None:
113  return state.state in hass.data[REG_KEY].on_off_mapping
114 
115  return False
116 
117 
118 # expand_entity_ids and get_entity_ids are for backwards compatibility only
119 expand_entity_ids = bind_hass(_expand_entity_ids)
120 get_entity_ids = bind_hass(_get_entity_ids)
121 
122 
123 @bind_hass
124 def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
125  """Get all groups that contain this entity.
126 
127  Async friendly.
128  """
129  if DOMAIN not in hass.data:
130  return []
131 
132  return [
133  group.entity_id
134  for group in hass.data[DATA_COMPONENT].entities
135  if entity_id in group.tracking
136  ]
137 
138 
139 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
140  """Set up a config entry."""
141  await hass.config_entries.async_forward_entry_setups(
142  entry, (entry.options["group_type"],)
143  )
144  entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
145  return True
146 
147 
148 async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
149  """Update listener, called when the config entry options are changed."""
150  await hass.config_entries.async_reload(entry.entry_id)
151 
152 
153 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
154  """Unload a config entry."""
155  return await hass.config_entries.async_unload_platforms(
156  entry, (entry.options["group_type"],)
157  )
158 
159 
160 async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
161  """Remove a config entry."""
162  # Unhide the group members
163  registry = er.async_get(hass)
164 
165  if not entry.options[CONF_HIDE_MEMBERS]:
166  return
167 
168  for member in entry.options[CONF_ENTITIES]:
169  if not (entity_id := er.async_resolve_entity_id(registry, member)):
170  continue
171  if (entity_entry := registry.async_get(entity_id)) is None:
172  continue
173  if entity_entry.hidden_by != er.RegistryEntryHider.INTEGRATION:
174  continue
175 
176  registry.async_update_entity(entity_id, hidden_by=None)
177 
178 
179 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
180  """Set up all groups found defined in the configuration."""
181  component = async_get_component(hass)
182 
183  await async_setup_registry(hass)
184 
185  await _async_process_config(hass, config)
186 
187  async def reload_service_handler(service: ServiceCall) -> None:
188  """Group reload handler.
189 
190  - Remove group.group entities not created by service calls and set them up again
191  - Reload xxx.group platforms
192  """
193  if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
194  return
195 
196  # Simplified + modified version of EntityPlatform.async_reset:
197  # - group.group never retries setup
198  # - group.group never polls
199  # - We don't need to reset EntityPlatform._setup_complete
200  # - Only remove entities which were not created by service calls
201  tasks = [
202  entity.async_remove()
203  for entity in component.entities
204  if entity.entity_id.startswith("group.") and not entity.created_by_service
205  ]
206 
207  if tasks:
208  await asyncio.gather(*tasks)
209 
210  component.config = None
211 
212  await _async_process_config(hass, conf)
213 
214  await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
215 
216  hass.services.async_register(
217  DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
218  )
219 
220  service_lock = asyncio.Lock()
221 
222  async def locked_service_handler(service: ServiceCall) -> None:
223  """Handle a service with an async lock."""
224  async with service_lock:
225  await groups_service_handler(service)
226 
227  async def groups_service_handler(service: ServiceCall) -> None:
228  """Handle dynamic group service functions."""
229  object_id = service.data[ATTR_OBJECT_ID]
230  entity_id = f"{DOMAIN}.{object_id}"
231  group = component.get_entity(entity_id)
232 
233  # new group
234  if service.service == SERVICE_SET and group is None:
235  entity_ids = (
236  service.data.get(ATTR_ENTITIES)
237  or service.data.get(ATTR_ADD_ENTITIES)
238  or None
239  )
240 
241  await Group.async_create_group(
242  hass,
243  service.data.get(ATTR_NAME, object_id),
244  created_by_service=True,
245  entity_ids=entity_ids,
246  icon=service.data.get(ATTR_ICON),
247  mode=service.data.get(ATTR_ALL),
248  object_id=object_id,
249  order=None,
250  )
251  return
252 
253  if group is None:
254  _LOGGER.warning("%s:Group '%s' doesn't exist!", service.service, object_id)
255  return
256 
257  # update group
258  if service.service == SERVICE_SET:
259  need_update = False
260 
261  if ATTR_ADD_ENTITIES in service.data:
262  delta = service.data[ATTR_ADD_ENTITIES]
263  entity_ids = set(group.tracking) | set(delta)
264  group.async_update_tracked_entity_ids(entity_ids)
265 
266  if ATTR_REMOVE_ENTITIES in service.data:
267  delta = service.data[ATTR_REMOVE_ENTITIES]
268  entity_ids = set(group.tracking) - set(delta)
269  group.async_update_tracked_entity_ids(entity_ids)
270 
271  if ATTR_ENTITIES in service.data:
272  entity_ids = service.data[ATTR_ENTITIES]
273  group.async_update_tracked_entity_ids(entity_ids)
274 
275  if ATTR_NAME in service.data:
276  group.set_name(service.data[ATTR_NAME])
277  need_update = True
278 
279  if ATTR_ICON in service.data:
280  group.set_icon(service.data[ATTR_ICON])
281  need_update = True
282 
283  if ATTR_ALL in service.data:
284  group.mode = all if service.data[ATTR_ALL] else any
285  need_update = True
286 
287  if need_update:
288  group.async_write_ha_state()
289 
290  return
291 
292  # remove group
293  if service.service == SERVICE_REMOVE:
294  await component.async_remove_entity(entity_id)
295 
296  hass.services.async_register(
297  DOMAIN,
298  SERVICE_SET,
299  locked_service_handler,
300  schema=vol.All(
301  vol.Schema(
302  {
303  vol.Required(ATTR_OBJECT_ID): cv.slug,
304  vol.Optional(ATTR_NAME): cv.string,
305  vol.Optional(ATTR_ICON): cv.string,
306  vol.Optional(ATTR_ALL): cv.boolean,
307  vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids,
308  vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids,
309  vol.Exclusive(ATTR_REMOVE_ENTITIES, "entities"): cv.entity_ids,
310  }
311  )
312  ),
313  )
314 
315  hass.services.async_register(
316  DOMAIN,
317  SERVICE_REMOVE,
318  groups_service_handler,
319  schema=vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}),
320  )
321 
322  return True
323 
324 
325 async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None:
326  """Process group configuration."""
327  hass.data.setdefault(GROUP_ORDER, 0)
328 
329  entities = []
330  domain_config: dict[str, dict[str, Any]] = config.get(DOMAIN, {})
331 
332  for object_id, conf in domain_config.items():
333  name: str = conf.get(CONF_NAME, object_id)
334  entity_ids: Collection[str] = conf.get(CONF_ENTITIES) or []
335  icon: str | None = conf.get(CONF_ICON)
336  mode = bool(conf.get(CONF_ALL))
337  order = hass.data[GROUP_ORDER]
338 
339  # We keep track of the order when we are creating the tasks
340  # in the same way that async_create_group does to make
341  # sure we use the same ordering system. This overcomes
342  # the problem with concurrently creating the groups
343  entities.append(
344  Group.async_create_group_entity(
345  hass,
346  name,
347  created_by_service=False,
348  entity_ids=entity_ids,
349  icon=icon,
350  object_id=object_id,
351  mode=mode,
352  order=order,
353  )
354  )
355 
356  # Keep track of the group order without iterating
357  # every state in the state machine every time
358  # we setup a new group
359  hass.data[GROUP_ORDER] += 1
360 
361  # If called before the platform async_setup is called (test cases)
362  await async_get_component(hass).async_add_entities(entities)
EntityComponent[Group] async_get_component(HomeAssistant hass)
Definition: entity.py:479
None _async_process_config(HomeAssistant hass, ConfigType config)
Definition: __init__.py:325
dict[str, Any] _conf_preprocess(Any value)
Definition: __init__.py:80
list[str] groups_with_entity(HomeAssistant hass, str entity_id)
Definition: __init__.py:124
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:139
None config_entry_update_listener(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:148
bool is_on(HomeAssistant hass, str entity_id)
Definition: __init__.py:106
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:179
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:153
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:160
None async_reload_integration_platforms(HomeAssistant hass, str integration_domain, Iterable[str] platform_domains)
Definition: reload.py:30