208 files

This commit is contained in:
Home Assistant Version Control
2026-10-07 02:49:21 +00:00
parent a7107ecbe1
commit cfd0d40c8e
208 changed files with 5434 additions and 901 deletions
@@ -8,7 +8,10 @@ from homeassistant.components import script
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
from ....reference_extraction import (
extract_targets_from_config,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
if TYPE_CHECKING:
@@ -42,6 +45,10 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
# 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)
# 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
)
return async_filter_known_area_ids(
self.hass,
@@ -8,7 +8,10 @@ from homeassistant.components import script
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 extract_targets_from_config
from ....reference_extraction import (
extract_targets_from_config,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
from ....template_extraction import extract_device_ids_from_config
@@ -46,6 +49,14 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
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))
# 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)
),
)
return async_filter_known_device_ids(
self.hass,
@@ -8,7 +8,9 @@ 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 ....entity_filtering import async_get_all_entity_ids, async_get_all_services
from ....reference_extraction import without_disabled_steps
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
from ....template_extraction import (
async_extract_entities_from_config,
@@ -19,37 +21,6 @@ if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
def extract_entities_from_trigger_config(config: dict[str, Any] | list) -> set[str]:
"""Extract entity IDs from a trigger config."""
entities = set()
if not config:
return entities
if isinstance(config, list):
for item in config:
entities.update(extract_entities_from_trigger_config(item))
return entities
if not isinstance(config, dict):
return entities
# Extract entity_id from trigger config
if "entity_id" in config:
entity_id = config["entity_id"]
if isinstance(entity_id, str):
entities.add(entity_id)
elif isinstance(entity_id, list):
entities.update([e for e in entity_id if isinstance(e, str)])
# Recursively process nested configs
for value in config.values():
if isinstance(value, (dict, list)):
entities.update(extract_entities_from_trigger_config(value))
return entities
def extract_referenced_entities_from_script(entity: script.ScriptEntity) -> set[str]:
"""Return entity references from a script entity."""
try:
@@ -74,18 +45,12 @@ async def extract_template_entities_from_script_entity(
``known_services`` is built once per inspection and handed down, because
building it flattens every service Home Assistant has and every script
with a template in it needs the same answer.
"""
# Get the script configuration
config = None
if hasattr(entity, "script"):
# Try to get configuration safely
if hasattr(entity.script, "config"):
config = entity.script.config
elif hasattr(entity.script, "_config"):
# Fallback to _config if needed
config = getattr(entity.script, "_config", None)
if not config:
Read from the configuration as written, like the automation repair does.
The script helper underneath keeps no configuration of its own, so this
used to find nothing at all.
"""
if not (config := getattr(entity, "raw_config", None)):
return set()
return await async_extract_entities_from_config(hass, config, known_services)
@@ -112,34 +77,6 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
_known_entity_ids: set[str]
_known_services: set[str]
def _get_blueprint_trigger_entities(self, entity: script.ScriptEntity) -> set[str]:
"""Extract entity references from blueprint trigger inputs."""
entities = set()
if (
not hasattr(entity, "referenced_blueprint")
or not entity.referenced_blueprint
):
return entities
config = getattr(entity, "_config", None)
if not config or not isinstance(config, dict) or "use_blueprint" not in config:
return entities
blueprint_config = config["use_blueprint"]
if "input" not in blueprint_config:
return entities
input_config = blueprint_config["input"]
# Look for inputs that might contain triggers (like discard_when)
for value in input_config.values():
if isinstance(value, (dict, list)) and "trigger" in str(value):
trigger_entities = extract_entities_from_trigger_config(value)
if trigger_entities:
entities.update(trigger_entities)
return entities
async def _async_setup_inspection(self) -> None:
"""Cache what every script in this cycle needs looked up.
@@ -152,13 +89,42 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
)
self._known_services = async_get_all_services(self.hass)
async def _async_named_in(self, config: dict[str, Any]) -> 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.
"""
steps = config.get("sequence") or []
named = await async_extract_entities_from_action_config(
self.hass,
# A single step can be written without a list.
[steps] if isinstance(steps, dict) else steps,
known_services=self._known_services,
)
named |= await async_extract_entities_from_config(
self.hass, config, self._known_services
)
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)
# Check for blueprint trigger inputs
all_entities.update(self._get_blueprint_trigger_entities(entity))
# Home Assistant's own list leaves out entities handed over as action
# data, like `entity: light.kitchen` in a call to another script. The
# automation repair reads those from the configuration as written, and
# so does this one.
if isinstance(raw_config := getattr(entity, "raw_config", None), dict):
all_entities.update(
await async_extract_entities_from_action_config(
self.hass,
raw_config.get("sequence") or [],
known_services=self._known_services,
)
)
# Extract entities from Template objects within the script entity
all_entities.update(
@@ -167,6 +133,15 @@ 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(
self.hass,
entity_ids=all_entities,
@@ -8,7 +8,10 @@ from homeassistant.components import script
from homeassistant.helpers import 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
from ....reference_extraction import (
extract_targets_from_config,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
if TYPE_CHECKING:
@@ -42,6 +45,10 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
# 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)
# 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
)
return async_filter_known_floor_ids(
self.hass,
@@ -8,7 +8,10 @@ from homeassistant.components import script
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
from ....reference_extraction import (
extract_targets_from_config,
only_in_disabled_steps,
)
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
if TYPE_CHECKING:
@@ -42,6 +45,10 @@ class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
# 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)
# 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
)
return async_filter_known_label_ids(
self.hass,