291 files

This commit is contained in:
Home Assistant Version Control
2026-10-14 18:10:18 +00:00
parent c4e0178d8a
commit 280501d2e8
291 changed files with 32540 additions and 3280 deletions
@@ -2,6 +2,7 @@
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING
from homeassistant.components import script
@@ -9,7 +10,8 @@ from homeassistant.helpers import area_registry as ar
from ....entity_filtering import async_filter_known_area_ids, async_get_all_area_ids
from ....reference_extraction import (
extract_targets_from_config,
async_raw_config_references,
core_references,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
@@ -39,16 +41,17 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
"""Return unknown area IDs referenced by ``entity``."""
area_ids = set(entity.script.referenced_areas)
area_ids = core_references(entity, "areas")
# Also walk the raw configuration; the built-in extraction misses
# references nested in some step types, like repeat sequences.
if raw_config := getattr(entity, "raw_config", None):
area_ids.update(extract_targets_from_config(raw_config).area_ids)
# Templates that look up an area that is not there are read too.
references = partial(async_raw_config_references, self.hass, kind="areas")
area_ids.update(references(raw_config))
# A disabled step does nothing, so what only it names is left out.
area_ids -= only_in_disabled_steps(
raw_config, lambda found: extract_targets_from_config(found).area_ids
)
# Read the same way, or a template there still counts.
area_ids -= only_in_disabled_steps(raw_config, references)
return async_filter_known_area_ids(
self.hass,
@@ -9,7 +9,9 @@ from homeassistant.helpers import device_registry as dr
from ....entity_filtering import async_filter_known_device_ids, async_get_all_device_ids
from ....reference_extraction import (
core_references,
extract_targets_from_config,
names_given_to_templates,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
@@ -41,20 +43,22 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
"""Return unknown device IDs referenced by ``entity``."""
device_ids = set(entity.script.referenced_devices)
device_ids = core_references(entity, "devices")
# Also walk the raw configuration; the built-in extraction misses
# references nested in some step types, like repeat sequences.
if raw_config := getattr(entity, "raw_config", None):
device_ids.update(extract_targets_from_config(raw_config).device_ids)
# Devices referenced via device_entities() in templates.
device_ids.update(extract_device_ids_from_config(raw_config))
# Devices referenced through the device functions in templates.
# The variables and fields it gives its templates hide a call.
shadowed = names_given_to_templates(raw_config)
device_ids.update(extract_device_ids_from_config(raw_config, shadowed))
# A disabled step does nothing, so what only it names is left out.
device_ids -= only_in_disabled_steps(
raw_config,
lambda found: (
extract_targets_from_config(found).device_ids
| extract_device_ids_from_config(found)
| extract_device_ids_from_config(found, shadowed)
),
)
@@ -8,29 +8,25 @@ from homeassistant.components import script
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.helpers import entity_registry as er
from ....action_extraction import async_extract_entities_from_action_config
from ....action_extraction import (
async_extract_entities_from_action_config,
async_unknown_entities_named,
)
from ....entity_filtering import async_get_all_entity_ids, async_get_all_services
from ....reference_extraction import without_disabled_steps
from ....reference_extraction import (
core_references,
names_given_to_templates,
without_never_rendered,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
from ....template_extraction import (
async_extract_entities_from_config,
async_filter_known_entity_ids_with_templates,
)
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
def extract_referenced_entities_from_script(entity: script.ScriptEntity) -> set[str]:
"""Return entity references from a script entity."""
try:
return set(entity.script.referenced_entities)
except TypeError as err:
if str(err) != "unhashable type: 'dict'":
raise
return set()
async def extract_template_entities_from_script_entity(
hass: HomeAssistant,
entity: Any,
@@ -53,7 +49,14 @@ async def extract_template_entities_from_script_entity(
if not (config := getattr(entity, "raw_config", None)):
return set()
return await async_extract_entities_from_config(hass, config, known_services)
# A description, a name or a field example is shown, never rendered: an
# entity in an example template there is not one the script uses.
return await async_extract_entities_from_config(
hass,
without_never_rendered(config),
known_services,
names_given_to_templates(config),
)
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
@@ -89,12 +92,15 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
)
self._known_services = async_get_all_services(self.hass)
async def _async_named_in(self, config: dict[str, Any]) -> set[str]:
async def _async_named_in(
self, config: dict[str, Any], shadowed: frozenset[str]
) -> set[str]:
"""Return the entities a configuration names, the way this repair reads it.
The same reading the report is built from, the steps and templates
alike, so comparing it with and without the disabled steps leaves out
exactly what only those name.
exactly what only those name. ``shadowed`` comes from the whole
configuration, as `config` can be one without its disabled steps.
"""
steps = config.get("sequence") or []
named = await async_extract_entities_from_action_config(
@@ -102,16 +108,20 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
# A single step can be written without a list.
[steps] if isinstance(steps, dict) else steps,
known_services=self._known_services,
shadowed=shadowed,
)
named |= await async_extract_entities_from_config(
self.hass, config, self._known_services
self.hass,
without_never_rendered(config),
self._known_services,
shadowed,
)
return named
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
"""Return unknown entity IDs referenced by ``entity`` (incl. templates)."""
# Get all referenced entities from the script
all_entities = extract_referenced_entities_from_script(entity)
all_entities = core_references(entity, "entities")
# Home Assistant's own list leaves out entities handed over as action
# data, like `entity: light.kitchen` in a call to another script. The
@@ -123,6 +133,7 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
self.hass,
raw_config.get("sequence") or [],
known_services=self._known_services,
shadowed=names_given_to_templates(raw_config),
)
)
@@ -133,18 +144,11 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
)
)
# Home Assistant's own list includes disabled steps too. A step parked
# that way does nothing, so what only it names is left out: whatever
# this repair finds in the configuration, and no longer finds once
# those are pruned.
if isinstance(raw_config, dict):
named = await self._async_named_in(raw_config)
still_named = await self._async_named_in(without_disabled_steps(raw_config))
all_entities -= named - still_named
return await async_filter_known_entity_ids_with_templates(
return await async_unknown_entities_named(
self.hass,
entity_ids=all_entities,
all_entities,
raw_config,
named_in=self._async_named_in,
known_entity_ids=self._known_entity_ids,
known_services=self._known_services,
)
@@ -2,14 +2,16 @@
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING
from homeassistant.components import script
from homeassistant.helpers import floor_registry as fr
from homeassistant.helpers import area_registry as ar, floor_registry as fr
from ....entity_filtering import async_filter_known_floor_ids, async_get_all_floor_ids
from ....reference_extraction import (
extract_targets_from_config,
async_raw_config_references,
core_references,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
@@ -23,7 +25,12 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
domain = script.DOMAIN
repair = "script_unknown_floor_references"
inspect_events = {fr.EVENT_FLOOR_REGISTRY_UPDATED}
# Templates look floors up through areas as well, so a renamed area
# can change what they find.
inspect_events = {
ar.EVENT_AREA_REGISTRY_UPDATED,
fr.EVENT_FLOOR_REGISTRY_UPDATED,
}
inspect_on_reload = True
unavailable_entity_class = script.UnavailableScriptEntity
@@ -39,16 +46,17 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
"""Return unknown floor IDs referenced by ``entity``."""
floor_ids = set(entity.script.referenced_floors)
floor_ids = core_references(entity, "floors")
# Also walk the raw configuration; the built-in extraction misses
# references nested in some step types, like repeat sequences.
if raw_config := getattr(entity, "raw_config", None):
floor_ids.update(extract_targets_from_config(raw_config).floor_ids)
# Templates that look up a floor that is not there are read too.
references = partial(async_raw_config_references, self.hass, kind="floors")
floor_ids.update(references(raw_config))
# A disabled step does nothing, so what only it names is left out.
floor_ids -= only_in_disabled_steps(
raw_config, lambda found: extract_targets_from_config(found).floor_ids
)
# Read the same way, or a template there still counts.
floor_ids -= only_in_disabled_steps(raw_config, references)
return async_filter_known_floor_ids(
self.hass,
@@ -2,6 +2,7 @@
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING
from homeassistant.components import script
@@ -9,7 +10,8 @@ from homeassistant.helpers import label_registry as lr
from ....entity_filtering import async_filter_known_label_ids, async_get_all_label_ids
from ....reference_extraction import (
extract_targets_from_config,
async_raw_config_references,
core_references,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
@@ -39,16 +41,17 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
"""Return unknown label IDs referenced by ``entity``."""
label_ids = set(entity.script.referenced_labels)
label_ids = core_references(entity, "labels")
# Also walk the raw configuration; the built-in extraction misses
# references nested in some step types, like repeat sequences.
if raw_config := getattr(entity, "raw_config", None):
label_ids.update(extract_targets_from_config(raw_config).label_ids)
# Templates that look up a label that is not there are read too.
references = partial(async_raw_config_references, self.hass, kind="labels")
label_ids.update(references(raw_config))
# A disabled step does nothing, so what only it names is left out.
label_ids -= only_in_disabled_steps(
raw_config, lambda found: extract_targets_from_config(found).label_ids
)
# Read the same way, or a template there still counts.
label_ids -= only_in_disabled_steps(raw_config, references)
return async_filter_known_label_ids(
self.hass,