1 """Integration providing core pieces of infrastructure."""
4 from collections.abc
import Callable, Coroutine
9 import voluptuous
as vol
11 from homeassistant
import config
as conf_util, core_config
21 SERVICE_SAVE_PERSISTENT_STATES,
37 async_extract_config_entry_ids,
38 async_extract_referenced_entity_ids,
39 async_register_admin_service,
49 from .
import scene
as scene_pre_import
51 DATA_EXPOSED_ENTITIES,
54 SERVICE_HOMEASSISTANT_RESTART,
55 SERVICE_HOMEASSISTANT_STOP,
57 from .exposed_entities
import ExposedEntities, async_should_expose
59 ATTR_ENTRY_ID =
"entry_id"
60 ATTR_SAFE_MODE =
"safe_mode"
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(
74 vol.Optional(ATTR_ENTRY_ID): str,
75 **cv.ENTITY_SERVICE_FIELDS,
78 cv.has_at_least_one_key(ATTR_ENTRY_ID, *cv.ENTITY_SERVICE_FIELDS),
80 SCHEMA_RESTART = vol.Schema({vol.Optional(ATTR_SAFE_MODE, default=
False): bool})
82 SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART)
85 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
86 """Set up general services related to Home Assistant."""
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)
92 async
def async_handle_turn_service(service: ServiceCall) ->
None:
93 """Handle calls to homeassistant.turn_on/off."""
95 all_referenced = referenced.referenced | referenced.indirectly_referenced
98 if not all_referenced:
100 "The service homeassistant.%s cannot be called without a target",
106 by_domain = it.groupby(
110 tasks: list[Coroutine[Any, Any, ServiceResponse]] = []
111 unsupported_entities: set[str] = set()
113 for domain, ent_ids
in by_domain:
117 "Called service homeassistant.%s with invalid entities %s",
123 if not hass.services.has_service(domain, service.service):
124 unsupported_entities.update(set(ent_ids) & referenced.referenced)
128 data =
dict(service.data)
131 data[ATTR_ENTITY_ID] =
list(ent_ids)
134 hass.services.async_call(
139 context=service.context,
143 if unsupported_entities:
145 "The service homeassistant.%s does not support entities %s",
147 ", ".join(sorted(unsupported_entities)),
151 await asyncio.gather(*tasks)
153 hass.services.async_register(
154 DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, async_save_persistent_states
157 service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA)
159 hass.services.async_register(
160 DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema
162 hass.services.async_register(
163 DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema
165 hass.services.async_register(
166 DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema
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]]
173 if call.service
in SHUTDOWN_SERVICES
and recorder.async_migration_in_progress(
177 "The system cannot %s while a database upgrade is in progress",
181 f
"The system cannot {call.service} "
182 "while a database upgrade is in progress."
185 if call.service == SERVICE_HOMEASSISTANT_STOP:
186 stop_handler = hass.data[DATA_STOP_HANDLER]
187 await stop_handler(hass,
False)
190 errors = await conf_util.async_check_ha_config_file(hass)
194 "The system cannot %s because the configuration is not valid: %s",
198 persistent_notification.async_create(
200 "Config error. See [the logs](/config/logs) for details.",
202 f
"{DOMAIN}.check_config",
205 f
"The system cannot {call.service} "
206 f
"because the configuration is not valid: {errors}"
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)
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)
222 context=call.context,
223 permission=POLICY_CONTROL,
224 user_id=call.context.user_id,
227 for entity
in call.data[ATTR_ENTITY_ID]:
228 if not user.permissions.check_entity(entity, POLICY_CONTROL):
230 context=call.context,
231 permission=POLICY_CONTROL,
232 user_id=call.context.user_id,
233 perm_category=CAT_ENTITIES,
241 await asyncio.gather(*tasks)
244 hass, DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service
249 SERVICE_HOMEASSISTANT_RESTART,
250 async_handle_core_service,
254 hass, DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
256 hass.services.async_register(
258 SERVICE_UPDATE_ENTITY,
259 async_handle_update_service,
260 schema=SCHEMA_UPDATE_ENTITY,
263 async
def async_handle_reload_config(call: ServiceCall) ->
None:
264 """Service handler for reloading core config."""
266 conf = await conf_util.async_hass_config_yaml(hass)
267 except HomeAssistantError
as err:
272 await core_config.async_process_ha_core_config(hass, conf.get(DOMAIN)
or {})
275 hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
278 async
def async_set_location(call: ServiceCall) ->
None:
279 """Service handler to set location."""
281 "latitude": call.data[ATTR_LATITUDE],
282 "longitude": call.data[ATTR_LONGITUDE],
285 if (elevation := call.data.get(ATTR_ELEVATION))
is not None:
286 service_data[
"elevation"] = elevation
288 await hass.config.async_update(**service_data)
293 SERVICE_SET_LOCATION,
297 vol.Required(ATTR_LATITUDE): cv.latitude,
298 vol.Required(ATTR_LONGITUDE): cv.longitude,
299 vol.Optional(ATTR_ELEVATION): int,
304 async
def async_handle_reload_templates(call: ServiceCall) ->
None:
305 """Service handler to reload custom Jinja."""
309 hass, DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES, async_handle_reload_templates
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])
318 if not reload_entries:
319 raise ValueError(
"There were no matching config entries to reload")
320 await asyncio.gather(
322 hass.config_entries.async_reload(config_entry_id)
323 for config_entry_id
in reload_entries
330 SERVICE_RELOAD_CONFIG_ENTRY,
331 async_handle_reload_config_entry,
332 schema=SCHEMA_RELOAD_CONFIG_ENTRY,
335 async
def async_handle_reload_all(call: ServiceCall) ->
None:
336 """Service handler for calling all integration reload services.
338 Calls all reload services on all active domains, which triggers the
339 reload of YAML configurations for the domain that support it.
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
347 We only do so, if there are no configuration errors.
350 if errors := await conf_util.async_check_ha_config_file(hass):
352 "The system cannot reload because the configuration is not valid: %s",
356 "Cannot quick reload all YAML configurations because the "
357 f
"configuration is not valid: {errors}"
360 services = hass.services.async_services_internal()
362 hass.services.async_call(
363 domain, SERVICE_RELOAD, context=call.context, blocking=
True
365 for domain, domain_services
in services.items()
366 if domain !=
"notify" and SERVICE_RELOAD
in domain_services
368 hass.services.async_call(
369 domain, service, context=call.context, blocking=
True
371 for domain, service
in (
372 (DOMAIN, SERVICE_RELOAD_CORE_CONFIG),
373 (
"frontend",
"reload_themes"),
374 (DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES),
378 await asyncio.gather(*tasks)
381 hass, DOMAIN, SERVICE_RELOAD_ALL, async_handle_reload_all
385 await exposed_entities.async_initialize()
386 hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
392 async
def _async_stop(hass: HomeAssistant, restart: bool) ->
None:
393 """Stop home assistant."""
394 exit_code = RESTART_EXIT_CODE
if restart
else 0
396 hass.data[KEY_HA_STOP] = asyncio.create_task(hass.async_stop(exit_code))
402 stop_handler: Callable[[HomeAssistant, bool], Coroutine[Any, Any,
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)
None _async_stop(HomeAssistant hass, bool restart)
bool async_setup(HomeAssistant hass, ConfigType config)
tuple[str, str] split_entity_id(str entity_id)
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)
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))
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
None async_load_custom_templates(HomeAssistant hass)