1 """Service calling related helpers."""
3 from __future__
import annotations
6 from collections.abc
import Awaitable, Callable, Coroutine, Iterable
9 from functools
import cache, partial
11 from types
import ModuleType
12 from typing
import TYPE_CHECKING, Any, TypedDict, TypeGuard, cast
14 import voluptuous
as vol
26 CONF_SERVICE_DATA_TEMPLATE,
27 CONF_SERVICE_TEMPLATE,
34 EntityServiceResponse,
57 config_validation
as cv,
65 from .group
import expand_entity_ids
66 from .selector
import TargetSelector
67 from .typing
import ConfigType, TemplateVarsType, VolDictType, VolSchemaType
70 from .entity
import Entity
72 CONF_SERVICE_ENTITY_ID =
"entity_id"
74 _LOGGER = logging.getLogger(__name__)
76 SERVICE_DESCRIPTION_CACHE: HassKey[dict[tuple[str, str], dict[str, Any] |
None]] = (
77 HassKey(
"service_description_cache")
79 ALL_SERVICE_DESCRIPTIONS_CACHE: HassKey[
80 tuple[set[tuple[str, str]], dict[str, dict[str, Any]]]
81 ] =
HassKey(
"all_service_descriptions_cache")
86 """Return a cached lookup of base components."""
109 "alarm_control_panel": alarm_control_panel,
110 "calendar": calendar,
115 "humidifier": humidifier,
118 "media_player": media_player,
125 "water_heater": water_heater,
130 """Validate attribute option or supported feature."""
132 domain, enum, option = option_or_feature.split(
".", 2)
133 except ValueError
as exc:
135 f
"Invalid {label} '{option_or_feature}', expected "
136 "<domain>.<enum>.<member>"
140 if not (base_component := base_components.get(domain)):
141 raise vol.Invalid(f
"Unknown base component '{domain}'")
144 attribute_enum = getattr(base_component, enum)
145 except AttributeError
as exc:
146 raise vol.Invalid(f
"Unknown {label} enum '{domain}.{enum}'")
from exc
148 if not issubclass(attribute_enum, Enum):
149 raise vol.Invalid(f
"Expected {label} '{domain}.{enum}' to be an enum")
152 return getattr(attribute_enum, option).value
153 except AttributeError
as exc:
154 raise vol.Invalid(f
"Unknown {label} '{enum}.{option}'")
from exc
158 """Validate attribute option."""
163 """Validate supported feature."""
169 _FIELD_SCHEMA = vol.Schema(
171 vol.Optional(
"filter"): {
172 vol.Optional(
"attribute"): {
173 vol.Required(str): [vol.All(str, validate_attribute_option)],
175 vol.Optional(
"supported_features"): [
176 vol.All(str, validate_supported_feature)
180 extra=vol.ALLOW_EXTRA,
183 _SECTION_SCHEMA = vol.Schema(
185 vol.Required(
"fields"): vol.Schema({str: _FIELD_SCHEMA}),
187 extra=vol.ALLOW_EXTRA,
190 _SERVICE_SCHEMA = vol.Schema(
192 vol.Optional(
"target"): vol.Any(TargetSelector.CONFIG_SCHEMA,
None),
193 vol.Optional(
"fields"): vol.Schema(
194 {str: vol.Any(_SECTION_SCHEMA, _FIELD_SCHEMA)}
197 extra=vol.ALLOW_EXTRA,
202 """Check if key starts with dot."""
203 if not key.startswith(
"."):
204 raise vol.Invalid(
"Key does not start with .")
208 _SERVICES_SCHEMA = vol.Schema(
210 vol.Remove(vol.All(str, starts_with_dot)): object,
211 cv.slug: vol.Any(
None, _SERVICE_SCHEMA),
217 """Type for service call parameters."""
221 service_data: dict[str, Any]
226 """Class to hold a target selector for a service."""
228 __slots__ = (
"entity_ids",
"device_ids",
"area_ids",
"floor_ids",
"label_ids")
230 def __init__(self, service_call: ServiceCall) ->
None:
231 """Extract ids from service call data."""
232 service_call_data = service_call.data
233 entity_ids: str | list |
None = service_call_data.get(ATTR_ENTITY_ID)
234 device_ids: str | list |
None = service_call_data.get(ATTR_DEVICE_ID)
235 area_ids: str | list |
None = service_call_data.get(ATTR_AREA_ID)
236 floor_ids: str | list |
None = service_call_data.get(ATTR_FLOOR_ID)
237 label_ids: str | list |
None = service_call_data.get(ATTR_LABEL_ID)
240 set(cv.ensure_list(entity_ids))
if _has_match(entity_ids)
else set()
243 set(cv.ensure_list(device_ids))
if _has_match(device_ids)
else set()
247 set(cv.ensure_list(floor_ids))
if _has_match(floor_ids)
else set()
250 set(cv.ensure_list(label_ids))
if _has_match(label_ids)
else set()
255 """Determine if any selectors are present."""
265 @dataclasses.dataclass(slots=True)
267 """Class to hold the selected entities."""
270 referenced: set[str] = dataclasses.field(default_factory=set)
274 indirectly_referenced: set[str] = dataclasses.field(default_factory=set)
277 missing_devices: set[str] = dataclasses.field(default_factory=set)
278 missing_areas: set[str] = dataclasses.field(default_factory=set)
279 missing_floors: set[str] = dataclasses.field(default_factory=set)
280 missing_labels: set[str] = dataclasses.field(default_factory=set)
283 referenced_devices: set[str] = dataclasses.field(default_factory=set)
284 referenced_areas: set[str] = dataclasses.field(default_factory=set)
287 """Log about missing items."""
289 for label, items
in (
290 (
"floors", self.missing_floors),
291 (
"areas", self.missing_areas),
292 (
"devices", self.missing_devices),
293 (
"entities", missing_entities),
294 (
"labels", self.missing_labels),
297 parts.append(f
"{label} {', '.join(sorted(items))}")
303 "Referenced %s are missing or not currently available",
312 blocking: bool =
False,
313 variables: TemplateVarsType =
None,
314 validate_config: bool =
True,
316 """Call a service based on a config hash."""
317 asyncio.run_coroutine_threadsafe(
327 blocking: bool =
False,
328 variables: TemplateVarsType =
None,
329 validate_config: bool =
True,
330 context: Context |
None =
None,
332 """Call a service based on a config hash."""
335 hass, config, variables, validate_config
337 except HomeAssistantError
as ex:
342 await hass.services.async_call(**params, blocking=blocking, context=context)
350 variables: TemplateVarsType =
None,
351 validate_config: bool =
False,
353 """Prepare to call a service based on a config hash."""
356 config = cv.SERVICE_SCHEMA(config)
357 except vol.Invalid
as ex:
359 f
"Invalid config for calling service: {ex}"
362 if CONF_ACTION
in config:
363 domain_service = config[CONF_ACTION]
365 domain_service = config[CONF_SERVICE_TEMPLATE]
369 domain_service = domain_service.async_render(variables)
370 domain_service = cv.service(domain_service)
371 except TemplateError
as ex:
373 f
"Error rendering service name template: {ex}"
375 except vol.Invalid
as ex:
377 f
"Template rendered invalid service: {domain_service}"
380 domain, _, service = domain_service.partition(
".")
383 if CONF_TARGET
in config:
384 conf = config[CONF_TARGET]
387 target.update(conf.async_render(variables))
389 target.update(template.render_complex(conf, variables))
391 if CONF_ENTITY_ID
in target:
392 registry = entity_registry.async_get(hass)
393 entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID])
394 if entity_ids
not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE):
395 entity_ids = entity_registry.async_validate_entity_ids(
398 target[CONF_ENTITY_ID] = entity_ids
399 except TemplateError
as ex:
401 f
"Error rendering service target template: {ex}"
403 except vol.Invalid
as ex:
405 f
"Template rendered invalid entity IDs: {target[CONF_ENTITY_ID]}"
410 for conf
in (CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE):
411 if conf
not in config:
414 render = template.render_complex(config[conf], variables)
415 if not isinstance(render, dict):
417 "Error rendering data template: Result is not a Dictionary"
419 service_data.update(render)
420 except TemplateError
as ex:
423 if CONF_SERVICE_ENTITY_ID
in config:
425 target[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID]
427 target = {ATTR_ENTITY_ID: config[CONF_SERVICE_ENTITY_ID]}
432 "service_data": service_data,
439 hass: HomeAssistant, service_call: ServiceCall, expand_group: bool =
True
441 """Extract a list of entity ids from a service call.
443 Will convert group entity ids to the entity ids it represents.
445 return asyncio.run_coroutine_threadsafe(
453 entities: Iterable[_EntityT],
454 service_call: ServiceCall,
455 expand_group: bool =
True,
457 """Extract a list of entity objects from a service call.
459 Will convert group entity ids to the entity ids it represents.
461 data_ent_id = service_call.data.get(ATTR_ENTITY_ID)
463 if data_ent_id == ENTITY_MATCH_ALL:
464 return [entity
for entity
in entities
if entity.available]
467 combined = referenced.referenced | referenced.indirectly_referenced
471 for entity
in entities:
472 if entity.entity_id
not in combined:
475 combined.remove(entity.entity_id)
477 if not entity.available:
482 referenced.log_missing(referenced.referenced & combined)
489 hass: HomeAssistant, service_call: ServiceCall, expand_group: bool =
True
491 """Extract a set of entity ids from a service call.
493 Will convert group entity ids to the entity ids it represents.
496 return referenced.referenced | referenced.indirectly_referenced
499 def _has_match(ids: str | list[str] |
None) -> TypeGuard[str | list[str]]:
500 """Check if ids can match anything."""
501 return ids
not in (
None, ENTITY_MATCH_NONE)
506 hass: HomeAssistant, service_call: ServiceCall, expand_group: bool =
True
507 ) -> SelectedEntities:
508 """Extract referenced entity IDs from a service call."""
512 if not selector.has_any_selector:
515 entity_ids: set[str] | list[str] = selector.entity_ids
519 selected.referenced.update(entity_ids)
522 not selector.device_ids
523 and not selector.area_ids
524 and not selector.floor_ids
525 and not selector.label_ids
529 entities = entity_registry.async_get(hass).entities
530 dev_reg = device_registry.async_get(hass)
531 area_reg = area_registry.async_get(hass)
533 if selector.floor_ids:
534 floor_reg = floor_registry.async_get(hass)
535 for floor_id
in selector.floor_ids:
536 if floor_id
not in floor_reg.floors:
537 selected.missing_floors.add(floor_id)
539 for area_id
in selector.area_ids:
540 if area_id
not in area_reg.areas:
541 selected.missing_areas.add(area_id)
543 for device_id
in selector.device_ids:
544 if device_id
not in dev_reg.devices:
545 selected.missing_devices.add(device_id)
547 if selector.label_ids:
548 label_reg = label_registry.async_get(hass)
549 for label_id
in selector.label_ids:
550 if label_id
not in label_reg.labels:
551 selected.missing_labels.add(label_id)
553 for entity_entry
in entities.get_entries_for_label(label_id):
555 entity_entry.entity_category
is None
556 and entity_entry.hidden_by
is None
558 selected.indirectly_referenced.add(entity_entry.entity_id)
560 for device_entry
in dev_reg.devices.get_devices_for_label(label_id):
561 selected.referenced_devices.add(device_entry.id)
563 for area_entry
in area_reg.areas.get_areas_for_label(label_id):
564 selected.referenced_areas.add(area_entry.id)
567 if selector.floor_ids:
568 selected.referenced_areas.update(
570 for floor_id
in selector.floor_ids
571 for area_entry
in area_reg.areas.get_areas_for_floor(floor_id)
574 selected.referenced_areas.update(selector.area_ids)
575 selected.referenced_devices.update(selector.device_ids)
577 if not selected.referenced_areas
and not selected.referenced_devices:
581 selected.indirectly_referenced.update(
583 for device_id
in selected.referenced_devices
584 for entry
in entities.get_entries_for_device_id(device_id)
587 if (entry.entity_category
is None and entry.hidden_by
is None)
591 referenced_devices_by_area: set[str] = set()
592 if selected.referenced_areas:
593 for area_id
in selected.referenced_areas:
594 referenced_devices_by_area.update(
596 for device_entry
in dev_reg.devices.get_devices_for_area_id(area_id)
598 selected.referenced_devices.update(referenced_devices_by_area)
601 selected.indirectly_referenced.update(
603 for area_id
in selected.referenced_areas
605 for entry
in entities.get_entries_for_area_id(area_id)
608 if entry.entity_category
is None and entry.hidden_by
is None
611 selected.indirectly_referenced.update(
613 for device_id
in referenced_devices_by_area
614 for entry
in entities.get_entries_for_device_id(device_id)
618 entry.entity_category
is None
619 and entry.hidden_by
is None
634 hass: HomeAssistant, service_call: ServiceCall, expand_group: bool =
True
636 """Extract referenced config entry ids from a service call."""
638 ent_reg = entity_registry.async_get(hass)
639 dev_reg = device_registry.async_get(hass)
640 config_entry_ids: set[str] = set()
643 for device_id
in referenced.referenced_devices:
645 device_id
in dev_reg.devices
646 and (device := dev_reg.async_get(device_id))
is not None
648 config_entry_ids.update(device.config_entries)
650 for entity_id
in referenced.referenced | referenced.indirectly_referenced:
651 entry = ent_reg.async_get(entity_id)
652 if entry
is not None and entry.config_entry_id
is not None:
653 config_entry_ids.add(entry.config_entry_id)
655 return config_entry_ids
659 """Load services file for an integration."""
667 except FileNotFoundError:
669 "Unable to find services.yaml for the %s integration", integration.domain
672 except (HomeAssistantError, vol.Invalid)
as ex:
674 "Unable to parse services.yaml for the %s integration: %s",
682 hass: HomeAssistant, integrations: Iterable[Integration]
683 ) -> list[JSON_TYPE]:
684 """Load service files for multiple integrations."""
690 hass: HomeAssistant, domain: str, service: str
691 ) -> dict[str, Any] |
None:
692 """Return the cached description for a service."""
693 return hass.data.get(SERVICE_DESCRIPTION_CACHE, {}).
get((domain, service))
699 ) -> dict[str, dict[str, Any]]:
700 """Return descriptions (i.e. user documentation) for all service calls."""
701 descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
706 services = hass.services.async_services_internal()
711 (domain, service_name)
712 for domain, services_by_domain
in services.items()
713 for service_name
in services_by_domain
716 all_cache: tuple[set[tuple[str, str]], dict[str, dict[str, Any]]] |
None
717 if all_cache := hass.data.get(ALL_SERVICE_DESCRIPTIONS_CACHE):
718 previous_all_services, previous_descriptions_cache = all_cache
720 if previous_all_services == all_services:
721 return previous_descriptions_cache
724 loaded: dict[str, JSON_TYPE] = {}
729 services = {domain: service.copy()
for domain, service
in services.items()}
731 if domains_with_missing_services := {
732 domain
for domain, _
in all_services.difference(descriptions_cache)
735 integrations: list[Integration] = []
736 for domain, int_or_exc
in ints_or_excs.items():
737 if type(int_or_exc)
is Integration
and int_or_exc.has_services:
738 integrations.append(int_or_exc)
741 assert isinstance(int_or_exc, Exception)
742 _LOGGER.error(
"Failed to load integration: %s", domain, exc_info=int_or_exc)
745 contents = await hass.async_add_executor_job(
746 _load_services_files, hass, integrations
748 loaded =
dict(zip(domains_with_missing_services, contents, strict=
False))
751 translations = await translation.async_get_translations(
752 hass,
"en",
"services", services
756 descriptions: dict[str, dict[str, Any]] = {}
757 for domain, services_map
in services.items():
758 descriptions[domain] = {}
759 domain_descriptions = descriptions[domain]
761 for service_name, service
in services_map.items():
762 cache_key = (domain, service_name)
763 description = descriptions_cache.get(cache_key)
764 if description
is not None:
765 domain_descriptions[service_name] = description
769 domain_yaml = loaded.get(domain)
or {}
776 domain_yaml.get(service_name)
or {}
787 "name": translations.get(
788 f
"component.{domain}.services.{service_name}.name",
789 yaml_description.get(
"name",
""),
791 "description": translations.get(
792 f
"component.{domain}.services.{service_name}.description",
793 yaml_description.get(
"description",
""),
795 "fields":
dict(yaml_description.get(
"fields", {})),
799 for field_name, field_schema
in description[
"fields"].items():
800 if name := translations.get(
801 f
"component.{domain}.services.{service_name}.fields.{field_name}.name"
803 field_schema[
"name"] = name
804 if desc := translations.get(
805 f
"component.{domain}.services.{service_name}.fields.{field_name}.description"
807 field_schema[
"description"] = desc
808 if example := translations.get(
809 f
"component.{domain}.services.{service_name}.fields.{field_name}.example"
811 field_schema[
"example"] = example
813 if "target" in yaml_description:
814 description[
"target"] = yaml_description[
"target"]
816 response = service.supports_response
817 if response
is not SupportsResponse.NONE:
818 description[
"response"] = {
819 "optional": response
is SupportsResponse.OPTIONAL,
822 descriptions_cache[cache_key] = description
824 domain_descriptions[service_name] = description
826 hass.data[ALL_SERVICE_DESCRIPTIONS_CACHE] = (all_services, descriptions)
832 """Remove entity service fields."""
835 for key, val
in call.data.items()
836 if key
not in cv.ENTITY_SERVICE_FIELDS
843 hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any]
845 """Register a description for a service."""
846 domain = domain.lower()
847 service = service.lower()
849 descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
852 "name": schema.get(
"name",
""),
853 "description": schema.get(
"description",
""),
854 "fields": schema.get(
"fields", {}),
857 if "target" in schema:
858 description[
"target"] = schema[
"target"]
861 response := hass.services.supports_response(domain, service)
862 ) != SupportsResponse.NONE:
863 description[
"response"] = {
864 "optional": response == SupportsResponse.OPTIONAL,
867 hass.data.pop(ALL_SERVICE_DESCRIPTIONS_CACHE,
None)
868 descriptions_cache[(domain, service)] = description
873 entities: dict[str, Entity],
874 entity_perms: Callable[[str, str], bool] |
None,
875 target_all_entities: bool,
876 all_referenced: set[str] |
None,
878 """Get entity candidates that the user is allowed to access."""
879 if entity_perms
is not None:
881 if target_all_entities:
886 for entity_id, entity
in entities.items()
887 if entity_perms(entity_id, POLICY_CONTROL)
890 assert all_referenced
is not None
893 for entity_id
in all_referenced:
894 if not entity_perms(entity_id, POLICY_CONTROL):
896 context=call.context,
898 permission=POLICY_CONTROL,
901 elif target_all_entities:
902 return list(entities.values())
907 assert all_referenced
is not None
909 len(all_referenced) == 1
910 and (single_entity :=
list(all_referenced)[0])
911 and (entity := entities.get(single_entity))
is not None
915 return [entities[entity_id]
for entity_id
in all_referenced.intersection(entities)]
921 registered_entities: dict[str, Entity],
924 required_features: Iterable[int] |
None =
None,
925 ) -> EntityServiceResponse |
None:
926 """Handle an entity service call.
928 Calls all platforms simultaneously.
930 entity_perms: Callable[[str, str], bool] |
None =
None
931 return_response = call.return_response
933 if call.context.user_id:
934 user = await hass.auth.async_get_user(call.context.user_id)
937 if not user.is_admin:
938 entity_perms = user.permissions.check_entity
940 target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL
942 if target_all_entities:
943 referenced: SelectedEntities |
None =
None
944 all_referenced: set[str] |
None =
None
948 all_referenced = referenced.referenced | referenced.indirectly_referenced
951 if isinstance(func, str):
966 if not target_all_entities:
967 assert referenced
is not None
969 missing = referenced.referenced.copy()
970 for entity
in entity_candidates:
971 missing.discard(entity.entity_id)
972 referenced.log_missing(missing)
974 entities: list[Entity] = []
975 for entity
in entity_candidates:
976 if not entity.available:
980 if required_features
is not None and (
981 entity.supported_features
is None
983 entity.supported_features & feature_set == feature_set
984 for feature_set
in required_features
988 if referenced
is not None and entity.entity_id
in referenced.referenced:
990 f
"Entity {entity.entity_id} does not support this service."
995 entities.append(entity)
1000 "Service call requested response data but did not match any entities"
1004 if len(entities) == 1:
1006 entity = entities[0]
1008 hass, entity, func, data, call.context
1010 if entity.should_poll:
1013 entity.async_set_context(call.context)
1014 await entity.async_update_ha_state(
True)
1015 return {entity.entity_id: single_response}
if return_response
else None
1019 results: list[ServiceResponse | BaseException] = await asyncio.gather(
1021 entity.async_request_call(
1024 for entity
in entities
1026 return_exceptions=
True,
1029 response_data: EntityServiceResponse = {}
1030 for entity, result
in zip(entities, results, strict=
False):
1031 if isinstance(result, BaseException):
1032 raise result
from None
1033 response_data[entity.entity_id] = result
1035 tasks: list[asyncio.Task[
None]] = []
1037 for entity
in entities:
1038 if not entity.should_poll:
1043 entity.async_set_context(call.context)
1044 tasks.append(create_eager_task(entity.async_update_ha_state(
True)))
1047 done, pending = await asyncio.wait(tasks)
1052 return response_data
if return_response
and response_data
else None
1056 hass: HomeAssistant,
1058 func: str | HassJob,
1059 data: dict | ServiceCall,
1061 ) -> ServiceResponse:
1062 """Handle calling service method."""
1063 entity.async_set_context(context)
1065 task: asyncio.Future[ServiceResponse] |
None
1066 if isinstance(func, str):
1068 partial(getattr(entity, func), **data),
1069 job_type=entity.get_hassjob_type(func),
1071 task = hass.async_run_hass_job(job)
1073 task = hass.async_run_hass_job(func, entity, data)
1077 result: ServiceResponse =
None
1078 if task
is not None:
1081 if asyncio.iscoroutine(result):
1084 "Service %s for %s incorrectly returns a coroutine object. Await result"
1085 " instead in service handler. Report bug to integration author"
1090 result = await result
1096 hass: HomeAssistant,
1097 service_job: HassJob[[ServiceCall], Awaitable[
None] |
None],
1100 """Run an admin service."""
1101 if call.context.user_id:
1102 user = await hass.auth.async_get_user(call.context.user_id)
1105 if not user.is_admin:
1108 result = hass.async_run_hass_job(service_job, call)
1109 if result
is not None:
1116 hass: HomeAssistant,
1119 service_func: Callable[[ServiceCall], Awaitable[
None] |
None],
1120 schema: VolSchemaType = vol.Schema({}, extra=vol.PREVENT_EXTRA),
1122 """Register a service that requires admin access."""
1123 hass.services.async_register(
1127 _async_admin_handler,
1129 HassJob(service_func, f
"admin service {domain}.{service}"),
1138 hass: HomeAssistant, domain: str
1139 ) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]:
1140 """Ensure permission to access any entity under domain in service call."""
1143 service_handler: Callable[[ServiceCall], Any],
1144 ) -> Callable[[ServiceCall], Any]:
1146 if not asyncio.iscoroutinefunction(service_handler):
1149 async
def check_permissions(call: ServiceCall) -> Any:
1150 """Check user permission and raise before call if unauthorized."""
1151 if not call.context.user_id:
1152 return await service_handler(call)
1154 user = await hass.auth.async_get_user(call.context.user_id)
1158 context=call.context,
1159 permission=POLICY_CONTROL,
1160 user_id=call.context.user_id,
1163 reg = entity_registry.async_get(hass)
1167 for entity
in reg.entities.values():
1168 if entity.platform != domain:
1171 if user.permissions.check_entity(entity.entity_id, POLICY_CONTROL):
1177 context=call.context,
1178 permission=POLICY_CONTROL,
1179 user_id=call.context.user_id,
1180 perm_category=CAT_ENTITIES,
1183 return await service_handler(call)
1185 return check_permissions
1191 """Helper for reload services.
1193 The helper has the following purposes:
1194 - Make sure reloads do not happen in parallel
1195 - Avoid redundant reloads of the same target
1200 service_func: Callable[[ServiceCall], Coroutine[Any, Any, Any]],
1201 reload_targets_func: Callable[[ServiceCall], set[_T]],
1203 """Initialize ReloadServiceHelper."""
1207 self._pending_reload_targets: set[_T] = set()
1211 """Execute the service.
1213 If a previous reload task is currently in progress, wait for it to finish first.
1214 Once the previous reload task has finished, one of the waiting tasks will be
1215 assigned to execute the reload of the targets it is assigned to reload. The
1216 other tasks will wait if they should reload the same target, otherwise they
1217 will wait for the next round.
1221 reload_targets =
None
1234 if reload_targets
is None:
1236 self._pending_reload_targets |= reload_targets
1245 if reload_targets.isdisjoint(self._pending_reload_targets):
1253 self._pending_reload_targets -= reload_targets
1259 hass: HomeAssistant,
1263 entities: dict[str, Entity],
1264 func: str | Callable[..., Any],
1265 job_type: HassJobType |
None,
1266 required_features: Iterable[int] |
None =
None,
1267 schema: VolDictType | VolSchemaType |
None,
1268 supports_response: SupportsResponse = SupportsResponse.NONE,
1270 """Help registering an entity service.
1272 This is called by EntityComponent.async_register_entity_service and
1273 EntityPlatform.async_register_entity_service and should not be called
1274 directly by integrations.
1276 if schema
is None or isinstance(schema, dict):
1277 schema = cv.make_entity_service_schema(schema)
1278 elif not cv.is_entity_service_schema(schema):
1280 from .frame
import ReportBehavior, report_usage
1283 "registers an entity service with a non entity service schema",
1284 core_behavior=ReportBehavior.LOG,
1285 breaks_in_ha_version=
"2025.9",
1288 service_func: str | HassJob[..., Any]
1289 service_func = func
if isinstance(func, str)
else HassJob(func)
1291 hass.services.async_register(
1295 entity_service_call,
1299 required_features=required_features,
None execute_service(self, ServiceCall service_call)
None __init__(self, Callable[[ServiceCall], Coroutine[Any, Any, Any]] service_func, Callable[[ServiceCall], set[_T]] reload_targets_func)
None log_missing(self, set[str] missing_entities)
None __init__(self, ServiceCall service_call)
bool has_any_selector(self)
web.Response get(self, web.Request request, str config_key)
set[str] async_extract_entities(ConfigType|Template config)
None report_usage(str what, *str|None breaks_in_ha_version=None, ReportBehavior core_behavior=ReportBehavior.ERROR, ReportBehavior core_integration_behavior=ReportBehavior.LOG, ReportBehavior custom_integration_behavior=ReportBehavior.LOG, set[str]|None exclude_integrations=None, str|None integration_domain=None, int level=logging.WARNING)
None async_register_entity_service(HomeAssistant hass, str domain, str name, *dict[str, Entity] entities, str|Callable[..., Any] func, HassJobType|None job_type, Iterable[int]|None required_features=None, VolDictType|VolSchemaType|None schema, SupportsResponse supports_response=SupportsResponse.NONE)
set[str] async_extract_config_entry_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]] verify_domain_control(HomeAssistant hass, str domain)
Any validate_supported_feature(str supported_feature)
ServiceParams async_prepare_call_from_config(HomeAssistant hass, ConfigType config, TemplateVarsType variables=None, bool validate_config=False)
set[str] extract_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
dict[str, ModuleType] _base_components()
dict[Any, Any] remove_entity_service_fields(ServiceCall call)
None async_set_service_schema(HomeAssistant hass, str domain, str service, dict[str, Any] schema)
str starts_with_dot(str key)
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))
ServiceResponse _handle_entity_call(HomeAssistant hass, Entity entity, str|HassJob func, dict|ServiceCall data, Context context)
None async_call_from_config(HomeAssistant hass, ConfigType config, bool blocking=False, TemplateVarsType variables=None, bool validate_config=True, Context|None context=None)
dict[str, Any]|None async_get_cached_service_description(HomeAssistant hass, str domain, str service)
list[Entity] _get_permissible_entity_candidates(ServiceCall call, dict[str, Entity] entities, Callable[[str, str], bool]|None entity_perms, bool target_all_entities, set[str]|None all_referenced)
None call_from_config(HomeAssistant hass, ConfigType config, bool blocking=False, TemplateVarsType variables=None, bool validate_config=True)
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
TypeGuard[str|list[str]] _has_match(str|list[str]|None ids)
set[str] async_extract_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
EntityServiceResponse|None entity_service_call(HomeAssistant hass, dict[str, Entity] registered_entities, str|HassJob func, ServiceCall call, Iterable[int]|None required_features=None)
Any _validate_option_or_feature(str option_or_feature, str label)
list[JSON_TYPE] _load_services_files(HomeAssistant hass, Iterable[Integration] integrations)
JSON_TYPE _load_services_file(HomeAssistant hass, Integration integration)
Any validate_attribute_option(str attribute_option)
dict[str, dict[str, Any]] async_get_all_descriptions(HomeAssistant hass)
None _async_admin_handler(HomeAssistant hass, HassJob[[ServiceCall], Awaitable[None]|None] service_job, ServiceCall call)
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
dict load_yaml_dict(str|os.PathLike[str] fname, Secrets|None secrets=None)