Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Integration providing core pieces of infrastructure."""
2 
3 import asyncio
4 from collections.abc import Callable, Coroutine
5 import itertools as it
6 import logging
7 from typing import Any
8 
9 import voluptuous as vol
10 
11 from homeassistant import config as conf_util, core_config
12 from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
13 from homeassistant.components import persistent_notification
14 from homeassistant.const import (
15  ATTR_ELEVATION,
16  ATTR_ENTITY_ID,
17  ATTR_LATITUDE,
18  ATTR_LONGITUDE,
19  RESTART_EXIT_CODE,
20  SERVICE_RELOAD,
21  SERVICE_SAVE_PERSISTENT_STATES,
22  SERVICE_TOGGLE,
23  SERVICE_TURN_OFF,
24  SERVICE_TURN_ON,
25 )
26 from homeassistant.core import (
27  HomeAssistant,
28  ServiceCall,
29  ServiceResponse,
30  callback,
31  split_entity_id,
32 )
33 from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser
34 from homeassistant.helpers import config_validation as cv, recorder, restore_state
35 from homeassistant.helpers.entity_component import async_update_entity
37  async_extract_config_entry_ids,
38  async_extract_referenced_entity_ids,
39  async_register_admin_service,
40 )
41 from homeassistant.helpers.signal import KEY_HA_STOP
42 from homeassistant.helpers.template import async_load_custom_templates
43 from homeassistant.helpers.typing import ConfigType
44 
45 # The scene integration will do a late import of scene
46 # so we want to make sure its loaded with the component
47 # so its already in memory when its imported so the import
48 # does not do blocking I/O in the event loop.
49 from . import scene as scene_pre_import # noqa: F401
50 from .const import (
51  DATA_EXPOSED_ENTITIES,
52  DATA_STOP_HANDLER,
53  DOMAIN,
54  SERVICE_HOMEASSISTANT_RESTART,
55  SERVICE_HOMEASSISTANT_STOP,
56 )
57 from .exposed_entities import ExposedEntities, async_should_expose # noqa: F401
58 
59 ATTR_ENTRY_ID = "entry_id"
60 ATTR_SAFE_MODE = "safe_mode"
61 
62 _LOGGER = logging.getLogger(__name__)
63 SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
64 SERVICE_RELOAD_CONFIG_ENTRY = "reload_config_entry"
65 SERVICE_RELOAD_CUSTOM_TEMPLATES = "reload_custom_templates"
66 SERVICE_CHECK_CONFIG = "check_config"
67 SERVICE_UPDATE_ENTITY = "update_entity"
68 SERVICE_SET_LOCATION = "set_location"
69 SERVICE_RELOAD_ALL = "reload_all"
70 SCHEMA_UPDATE_ENTITY = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
71 SCHEMA_RELOAD_CONFIG_ENTRY = vol.All(
72  vol.Schema(
73  {
74  vol.Optional(ATTR_ENTRY_ID): str,
75  **cv.ENTITY_SERVICE_FIELDS,
76  },
77  ),
78  cv.has_at_least_one_key(ATTR_ENTRY_ID, *cv.ENTITY_SERVICE_FIELDS),
79 )
80 SCHEMA_RESTART = vol.Schema({vol.Optional(ATTR_SAFE_MODE, default=False): bool})
81 
82 SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART)
83 
84 
85 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
86  """Set up general services related to Home Assistant."""
87 
88  async def async_save_persistent_states(service: ServiceCall) -> None:
89  """Handle calls to homeassistant.save_persistent_states."""
90  await restore_state.RestoreStateData.async_save_persistent_states(hass)
91 
92  async def async_handle_turn_service(service: ServiceCall) -> None:
93  """Handle calls to homeassistant.turn_on/off."""
94  referenced = async_extract_referenced_entity_ids(hass, service)
95  all_referenced = referenced.referenced | referenced.indirectly_referenced
96 
97  # Generic turn on/off method requires entity id
98  if not all_referenced:
99  _LOGGER.error(
100  "The service homeassistant.%s cannot be called without a target",
101  service.service,
102  )
103  return
104 
105  # Group entity_ids by domain. groupby requires sorted data.
106  by_domain = it.groupby(
107  sorted(all_referenced), lambda item: split_entity_id(item)[0]
108  )
109 
110  tasks: list[Coroutine[Any, Any, ServiceResponse]] = []
111  unsupported_entities: set[str] = set()
112 
113  for domain, ent_ids in by_domain:
114  # This leads to endless loop.
115  if domain == DOMAIN:
116  _LOGGER.warning(
117  "Called service homeassistant.%s with invalid entities %s",
118  service.service,
119  ", ".join(ent_ids),
120  )
121  continue
122 
123  if not hass.services.has_service(domain, service.service):
124  unsupported_entities.update(set(ent_ids) & referenced.referenced)
125  continue
126 
127  # Create a new dict for this call
128  data = dict(service.data)
129 
130  # ent_ids is a generator, convert it to a list.
131  data[ATTR_ENTITY_ID] = list(ent_ids)
132 
133  tasks.append(
134  hass.services.async_call(
135  domain,
136  service.service,
137  data,
138  blocking=True,
139  context=service.context,
140  )
141  )
142 
143  if unsupported_entities:
144  _LOGGER.warning(
145  "The service homeassistant.%s does not support entities %s",
146  service.service,
147  ", ".join(sorted(unsupported_entities)),
148  )
149 
150  if tasks:
151  await asyncio.gather(*tasks)
152 
153  hass.services.async_register(
154  DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, async_save_persistent_states
155  )
156 
157  service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA)
158 
159  hass.services.async_register(
160  DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema
161  )
162  hass.services.async_register(
163  DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema
164  )
165  hass.services.async_register(
166  DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema
167  )
168 
169  async def async_handle_core_service(call: ServiceCall) -> None:
170  """Service handler for handling core services."""
171  stop_handler: Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]]
172 
173  if call.service in SHUTDOWN_SERVICES and recorder.async_migration_in_progress(
174  hass
175  ):
176  _LOGGER.error(
177  "The system cannot %s while a database upgrade is in progress",
178  call.service,
179  )
180  raise HomeAssistantError(
181  f"The system cannot {call.service} "
182  "while a database upgrade is in progress."
183  )
184 
185  if call.service == SERVICE_HOMEASSISTANT_STOP:
186  stop_handler = hass.data[DATA_STOP_HANDLER]
187  await stop_handler(hass, False)
188  return
189 
190  errors = await conf_util.async_check_ha_config_file(hass)
191 
192  if errors:
193  _LOGGER.error(
194  "The system cannot %s because the configuration is not valid: %s",
195  call.service,
196  errors,
197  )
198  persistent_notification.async_create(
199  hass,
200  "Config error. See [the logs](/config/logs) for details.",
201  "Config validating",
202  f"{DOMAIN}.check_config",
203  )
204  raise HomeAssistantError(
205  f"The system cannot {call.service} "
206  f"because the configuration is not valid: {errors}"
207  )
208 
209  if call.service == SERVICE_HOMEASSISTANT_RESTART:
210  if call.data[ATTR_SAFE_MODE]:
211  await conf_util.async_enable_safe_mode(hass)
212  stop_handler = hass.data[DATA_STOP_HANDLER]
213  await stop_handler(hass, True)
214 
215  async def async_handle_update_service(call: ServiceCall) -> None:
216  """Service handler for updating an entity."""
217  if call.context.user_id:
218  user = await hass.auth.async_get_user(call.context.user_id)
219 
220  if user is None:
221  raise UnknownUser(
222  context=call.context,
223  permission=POLICY_CONTROL,
224  user_id=call.context.user_id,
225  )
226 
227  for entity in call.data[ATTR_ENTITY_ID]:
228  if not user.permissions.check_entity(entity, POLICY_CONTROL):
229  raise Unauthorized(
230  context=call.context,
231  permission=POLICY_CONTROL,
232  user_id=call.context.user_id,
233  perm_category=CAT_ENTITIES,
234  )
235 
236  tasks = [
237  async_update_entity(hass, entity) for entity in call.data[ATTR_ENTITY_ID]
238  ]
239 
240  if tasks:
241  await asyncio.gather(*tasks)
242 
244  hass, DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service
245  )
247  hass,
248  DOMAIN,
249  SERVICE_HOMEASSISTANT_RESTART,
250  async_handle_core_service,
251  SCHEMA_RESTART,
252  )
254  hass, DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
255  )
256  hass.services.async_register(
257  DOMAIN,
258  SERVICE_UPDATE_ENTITY,
259  async_handle_update_service,
260  schema=SCHEMA_UPDATE_ENTITY,
261  )
262 
263  async def async_handle_reload_config(call: ServiceCall) -> None:
264  """Service handler for reloading core config."""
265  try:
266  conf = await conf_util.async_hass_config_yaml(hass)
267  except HomeAssistantError as err:
268  _LOGGER.error(err)
269  return
270 
271  # auth only processed during startup
272  await core_config.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
273 
275  hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
276  )
277 
278  async def async_set_location(call: ServiceCall) -> None:
279  """Service handler to set location."""
280  service_data = {
281  "latitude": call.data[ATTR_LATITUDE],
282  "longitude": call.data[ATTR_LONGITUDE],
283  }
284 
285  if (elevation := call.data.get(ATTR_ELEVATION)) is not None:
286  service_data["elevation"] = elevation
287 
288  await hass.config.async_update(**service_data)
289 
291  hass,
292  DOMAIN,
293  SERVICE_SET_LOCATION,
294  async_set_location,
295  vol.Schema(
296  {
297  vol.Required(ATTR_LATITUDE): cv.latitude,
298  vol.Required(ATTR_LONGITUDE): cv.longitude,
299  vol.Optional(ATTR_ELEVATION): int,
300  }
301  ),
302  )
303 
304  async def async_handle_reload_templates(call: ServiceCall) -> None:
305  """Service handler to reload custom Jinja."""
306  await async_load_custom_templates(hass)
307 
309  hass, DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES, async_handle_reload_templates
310  )
311 
312  async def async_handle_reload_config_entry(call: ServiceCall) -> None:
313  """Service handler for reloading a config entry."""
314  reload_entries: set[str] = set()
315  if ATTR_ENTRY_ID in call.data:
316  reload_entries.add(call.data[ATTR_ENTRY_ID])
317  reload_entries.update(await async_extract_config_entry_ids(hass, call))
318  if not reload_entries:
319  raise ValueError("There were no matching config entries to reload")
320  await asyncio.gather(
321  *(
322  hass.config_entries.async_reload(config_entry_id)
323  for config_entry_id in reload_entries
324  )
325  )
326 
328  hass,
329  DOMAIN,
330  SERVICE_RELOAD_CONFIG_ENTRY,
331  async_handle_reload_config_entry,
332  schema=SCHEMA_RELOAD_CONFIG_ENTRY,
333  )
334 
335  async def async_handle_reload_all(call: ServiceCall) -> None:
336  """Service handler for calling all integration reload services.
337 
338  Calls all reload services on all active domains, which triggers the
339  reload of YAML configurations for the domain that support it.
340 
341  Additionally, it also calls the `homeasssitant.reload_core_config`
342  service, as that reloads the core YAML configuration, the
343  `frontend.reload_themes` service that reloads the themes, and the
344  `homeassistant.reload_custom_templates` service that reloads any custom
345  jinja into memory.
346 
347  We only do so, if there are no configuration errors.
348  """
349 
350  if errors := await conf_util.async_check_ha_config_file(hass):
351  _LOGGER.error(
352  "The system cannot reload because the configuration is not valid: %s",
353  errors,
354  )
355  raise HomeAssistantError(
356  "Cannot quick reload all YAML configurations because the "
357  f"configuration is not valid: {errors}"
358  )
359 
360  services = hass.services.async_services_internal()
361  tasks = [
362  hass.services.async_call(
363  domain, SERVICE_RELOAD, context=call.context, blocking=True
364  )
365  for domain, domain_services in services.items()
366  if domain != "notify" and SERVICE_RELOAD in domain_services
367  ] + [
368  hass.services.async_call(
369  domain, service, context=call.context, blocking=True
370  )
371  for domain, service in (
372  (DOMAIN, SERVICE_RELOAD_CORE_CONFIG),
373  ("frontend", "reload_themes"),
374  (DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES),
375  )
376  ]
377 
378  await asyncio.gather(*tasks)
379 
381  hass, DOMAIN, SERVICE_RELOAD_ALL, async_handle_reload_all
382  )
383 
384  exposed_entities = ExposedEntities(hass)
385  await exposed_entities.async_initialize()
386  hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
387  async_set_stop_handler(hass, _async_stop)
388 
389  return True
390 
391 
392 async def _async_stop(hass: HomeAssistant, restart: bool) -> None:
393  """Stop home assistant."""
394  exit_code = RESTART_EXIT_CODE if restart else 0
395  # Track trask in hass.data. No need to cleanup, we're stopping.
396  hass.data[KEY_HA_STOP] = asyncio.create_task(hass.async_stop(exit_code))
397 
398 
399 @callback
401  hass: HomeAssistant,
402  stop_handler: Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]],
403 ) -> None:
404  """Set function which is called by the stop and restart services."""
405  hass.data[DATA_STOP_HANDLER] = stop_handler
None async_set_stop_handler(HomeAssistant hass, Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]] stop_handler)
Definition: __init__.py:403
None _async_stop(HomeAssistant hass, bool restart)
Definition: __init__.py:392
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:85
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
None async_update_entity(HomeAssistant hass, str entity_id)
set[str] async_extract_config_entry_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
Definition: service.py:635
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
Definition: service.py:507
None async_load_custom_templates(HomeAssistant hass)
Definition: template.py:2788