291 files
This commit is contained in:
+99
-68
@@ -11,13 +11,19 @@ from homeassistant.helpers import entity_registry as er
|
||||
from ....action_extraction import (
|
||||
async_extract_entities_from_action_config,
|
||||
async_extract_entities_from_value,
|
||||
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 (
|
||||
VALUE_KEYS,
|
||||
core_references,
|
||||
event_payload_keys_to_leave_alone,
|
||||
names_given_to_templates,
|
||||
threshold_entities,
|
||||
without_never_rendered,
|
||||
)
|
||||
from ....template_extraction import (
|
||||
KNOWN_DOMAINS,
|
||||
async_extract_entities_from_config,
|
||||
async_filter_known_entity_ids_with_templates,
|
||||
)
|
||||
from . import AbstractSpookAutomationReferencesRepair
|
||||
|
||||
@@ -43,13 +49,21 @@ async def extract_template_entities_from_automation_entity(
|
||||
else:
|
||||
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 automation uses.
|
||||
return await async_extract_entities_from_config(
|
||||
hass,
|
||||
without_never_rendered(config),
|
||||
known_services,
|
||||
names_given_to_templates(config),
|
||||
)
|
||||
|
||||
|
||||
async def extract_entities_from_automation_config(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
known_services: set[str] | None = None,
|
||||
shadowed: frozenset[str] | None = None,
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from automation configuration.
|
||||
|
||||
@@ -57,6 +71,10 @@ async def extract_entities_from_automation_config(
|
||||
building it flattens every service Home Assistant has and the walk below
|
||||
passes a lot of template strings. See
|
||||
`action_extraction.async_extract_entities_from_action_config`.
|
||||
|
||||
The names the automation gives its templates, like a variable called
|
||||
`states`, are taken from all of it here, before walking its parts,
|
||||
unless ``shadowed`` already holds them.
|
||||
"""
|
||||
entities = set()
|
||||
|
||||
@@ -65,13 +83,15 @@ async def extract_entities_from_automation_config(
|
||||
|
||||
if known_services is None:
|
||||
known_services = async_get_all_services(hass)
|
||||
if shadowed is None:
|
||||
shadowed = names_given_to_templates(config)
|
||||
|
||||
# Extract entities from trigger config
|
||||
for key in ("trigger", "triggers"):
|
||||
if key in config:
|
||||
entities.update(
|
||||
await extract_entities_from_trigger_config(
|
||||
hass, config[key], known_services
|
||||
hass, config[key], known_services, shadowed=shadowed
|
||||
)
|
||||
)
|
||||
|
||||
@@ -80,7 +100,7 @@ async def extract_entities_from_automation_config(
|
||||
if key in config:
|
||||
entities.update(
|
||||
await extract_entities_from_condition_config(
|
||||
hass, config[key], known_services
|
||||
hass, config[key], known_services, shadowed=shadowed
|
||||
)
|
||||
)
|
||||
|
||||
@@ -89,7 +109,10 @@ async def extract_entities_from_automation_config(
|
||||
if key in config:
|
||||
entities.update(
|
||||
await async_extract_entities_from_action_config(
|
||||
hass, config[key], known_services=known_services
|
||||
hass,
|
||||
config[key],
|
||||
known_services=known_services,
|
||||
shadowed=shadowed,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -100,31 +123,36 @@ async def _entities_from_reference_fields(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
known_services: set[str],
|
||||
*,
|
||||
in_values: bool = False,
|
||||
shadowed: frozenset[str] = frozenset(),
|
||||
) -> set[str]:
|
||||
"""Extract entities from the config keys that name a reference.
|
||||
|
||||
``zone`` is in here because a zone trigger and a zone condition both name
|
||||
one, and it is read exactly like the others.
|
||||
one, and it is read exactly like the others. So is the entity a trigger
|
||||
or condition takes as its threshold, unless the dictionary sits below a
|
||||
key that holds values: event data that only looks like one is still event
|
||||
data.
|
||||
"""
|
||||
entities = set()
|
||||
entities = set() if in_values else threshold_entities(config)
|
||||
for key in ("entity_id", "device_id", "zone"):
|
||||
if key in config:
|
||||
entities.update(
|
||||
await async_extract_entities_from_value(
|
||||
hass, config[key], known_services=known_services
|
||||
hass, config[key], known_services=known_services, shadowed=shadowed
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
# Where an event trigger keeps what it matches on, rather than what it needs.
|
||||
_EVENT_PAYLOAD_KEYS = frozenset({"event_data", "event_data_template"})
|
||||
|
||||
|
||||
async def extract_entities_from_trigger_config(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any] | list,
|
||||
known_services: set[str] | None = None,
|
||||
*,
|
||||
_in_values: bool = False,
|
||||
shadowed: frozenset[str] = frozenset(),
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from trigger configuration."""
|
||||
entities = set()
|
||||
@@ -138,16 +166,26 @@ async def extract_entities_from_trigger_config(
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
entities.update(
|
||||
await extract_entities_from_trigger_config(hass, item, known_services)
|
||||
await extract_entities_from_trigger_config(
|
||||
hass,
|
||||
item,
|
||||
known_services,
|
||||
_in_values=_in_values,
|
||||
shadowed=shadowed,
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return entities
|
||||
|
||||
entities.update(await _entities_from_reference_fields(hass, config, known_services))
|
||||
entities.update(
|
||||
await _entities_from_reference_fields(
|
||||
hass, config, known_services, in_values=_in_values, shadowed=shadowed
|
||||
)
|
||||
)
|
||||
|
||||
payload_keys = _payload_keys_to_leave_alone(config)
|
||||
payload_keys = event_payload_keys_to_leave_alone(config)
|
||||
|
||||
# Extract from nested configs
|
||||
for key, value in config.items():
|
||||
@@ -156,42 +194,18 @@ async def extract_entities_from_trigger_config(
|
||||
|
||||
if isinstance(value, (dict, list)):
|
||||
entities.update(
|
||||
await extract_entities_from_trigger_config(hass, value, known_services)
|
||||
await extract_entities_from_trigger_config(
|
||||
hass,
|
||||
value,
|
||||
known_services,
|
||||
_in_values=_in_values or key in VALUE_KEYS,
|
||||
shadowed=shadowed,
|
||||
)
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def _payload_keys_to_leave_alone(config: dict[str, Any]) -> frozenset[str]:
|
||||
"""Return the event payload keys that hold data rather than references.
|
||||
|
||||
An `entity_id` inside `event_data` is a reference when the event comes
|
||||
from an integration that means it that way, `timer.finished` being the
|
||||
usual one. On somebody's own event it is whatever the sender put there,
|
||||
and reporting that as a missing entity is a repair about an automation
|
||||
that works perfectly well.
|
||||
|
||||
Told apart by the event type: one named after a domain comes from that
|
||||
integration, anything else is somebody's own.
|
||||
"""
|
||||
event_types = config.get("event_type")
|
||||
if event_types is None:
|
||||
return frozenset()
|
||||
|
||||
if isinstance(event_types, str):
|
||||
event_types = [event_types]
|
||||
|
||||
for event_type in event_types:
|
||||
if not isinstance(event_type, str):
|
||||
continue
|
||||
|
||||
domain, dot, _ = event_type.partition(".")
|
||||
if dot and domain in KNOWN_DOMAINS:
|
||||
return frozenset()
|
||||
|
||||
return _EVENT_PAYLOAD_KEYS
|
||||
|
||||
|
||||
def extract_event_types_from_trigger_config(config: dict[str, Any] | list) -> set[str]:
|
||||
"""Extract event types from trigger configuration."""
|
||||
event_types = set()
|
||||
@@ -224,6 +238,9 @@ async def extract_entities_from_condition_config(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any] | list,
|
||||
known_services: set[str] | None = None,
|
||||
*,
|
||||
_in_values: bool = False,
|
||||
shadowed: frozenset[str] = frozenset(),
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from condition configuration."""
|
||||
entities = set()
|
||||
@@ -237,21 +254,35 @@ async def extract_entities_from_condition_config(
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
entities.update(
|
||||
await extract_entities_from_condition_config(hass, item, known_services)
|
||||
await extract_entities_from_condition_config(
|
||||
hass,
|
||||
item,
|
||||
known_services,
|
||||
_in_values=_in_values,
|
||||
shadowed=shadowed,
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return entities
|
||||
|
||||
entities.update(await _entities_from_reference_fields(hass, config, known_services))
|
||||
entities.update(
|
||||
await _entities_from_reference_fields(
|
||||
hass, config, known_services, in_values=_in_values, shadowed=shadowed
|
||||
)
|
||||
)
|
||||
|
||||
# Extract from nested configs
|
||||
for value in config.values():
|
||||
for key, value in config.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
entities.update(
|
||||
await extract_entities_from_condition_config(
|
||||
hass, value, known_services
|
||||
hass,
|
||||
value,
|
||||
known_services,
|
||||
_in_values=_in_values or key in VALUE_KEYS,
|
||||
shadowed=shadowed,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -292,24 +323,30 @@ class SpookRepair(AbstractSpookAutomationReferencesRepair):
|
||||
)
|
||||
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, structure 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.
|
||||
"""
|
||||
named = await extract_entities_from_automation_config(
|
||||
self.hass, config, self._known_services
|
||||
self.hass, config, self._known_services, 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)."""
|
||||
all_entities = set(entity.referenced_entities)
|
||||
all_entities = core_references(entity, "entities")
|
||||
|
||||
# Also extract entities directly from raw configuration if available
|
||||
if hasattr(entity, "raw_config") and entity.raw_config:
|
||||
@@ -330,18 +367,12 @@ class SpookRepair(AbstractSpookAutomationReferencesRepair):
|
||||
)
|
||||
)
|
||||
|
||||
# Home Assistant's own list includes disabled steps, triggers and
|
||||
# conditions too. Something parked that way does nothing, so what only
|
||||
# it names is left out of the report: whatever this repair finds in
|
||||
# the configuration, and no longer finds once those are pruned.
|
||||
if isinstance(raw_config := getattr(entity, "raw_config", None), 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(
|
||||
raw_config = getattr(entity, "raw_config", None)
|
||||
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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user