Updated apps
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.helpers import area_registry as ar
|
||||
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
|
||||
|
||||
from ....const import LOGGER
|
||||
from ....entity_filtering import async_filter_known_area_ids, async_get_all_area_ids
|
||||
from ....repairs import AbstractSpookRepair
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookRepair):
|
||||
"""Spook repair tries to find unknown referenced areas in scripts."""
|
||||
|
||||
domain = script.DOMAIN
|
||||
repair = "script_unknown_area_references"
|
||||
inspect_events = {ar.EVENT_AREA_REGISTRY_UPDATED}
|
||||
inspect_on_reload = True
|
||||
|
||||
automatically_clean_up_issues = True
|
||||
|
||||
async def async_inspect(self) -> None:
|
||||
"""Trigger a inspection."""
|
||||
if self.domain not in self.hass.data[DATA_INSTANCES]:
|
||||
return
|
||||
|
||||
entity_component: EntityComponent[script.ScriptEntity] = self.hass.data[
|
||||
DATA_INSTANCES
|
||||
][self.domain]
|
||||
|
||||
LOGGER.debug("Spook is inspecting: %s", self.repair)
|
||||
|
||||
known_area_ids = async_get_all_area_ids(self.hass)
|
||||
|
||||
for entity in entity_component.entities:
|
||||
self.possible_issue_ids.add(entity.entity_id)
|
||||
if not isinstance(entity, script.UnavailableScriptEntity) and (
|
||||
unknown_areas := async_filter_known_area_ids(
|
||||
self.hass,
|
||||
area_ids=entity.script.referenced_areas,
|
||||
known_area_ids=known_area_ids,
|
||||
)
|
||||
):
|
||||
self.async_create_issue(
|
||||
issue_id=entity.entity_id,
|
||||
translation_placeholders={
|
||||
"areas": "\n".join(f"- `{area}`" for area in unknown_areas),
|
||||
"script": entity.name,
|
||||
"edit": f"/config/script/edit/{entity.unique_id}",
|
||||
"entity_id": entity.entity_id,
|
||||
},
|
||||
)
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Spook found unknown areas in %s "
|
||||
"and created an issue for it; Areas: %s"
|
||||
),
|
||||
entity.entity_id,
|
||||
", ".join(unknown_areas),
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
|
||||
|
||||
from ....const import LOGGER
|
||||
from ....entity_filtering import async_filter_known_device_ids, async_get_all_device_ids
|
||||
from ....repairs import AbstractSpookRepair
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookRepair):
|
||||
"""Spook repair tries to find unknown referenced devices in scripts."""
|
||||
|
||||
domain = script.DOMAIN
|
||||
repair = "script_unknown_device_references"
|
||||
inspect_events = {dr.EVENT_DEVICE_REGISTRY_UPDATED}
|
||||
inspect_config_entry_changed = True
|
||||
inspect_on_reload = True
|
||||
|
||||
automatically_clean_up_issues = True
|
||||
|
||||
async def async_inspect(self) -> None:
|
||||
"""Trigger a inspection."""
|
||||
if self.domain not in self.hass.data[DATA_INSTANCES]:
|
||||
return
|
||||
|
||||
entity_component: EntityComponent[script.ScriptEntity] = self.hass.data[
|
||||
DATA_INSTANCES
|
||||
][self.domain]
|
||||
|
||||
known_device_ids = async_get_all_device_ids(self.hass)
|
||||
|
||||
LOGGER.debug("Spook is inspecting: %s", self.repair)
|
||||
for entity in entity_component.entities:
|
||||
self.possible_issue_ids.add(entity.entity_id)
|
||||
if not isinstance(entity, script.UnavailableScriptEntity) and (
|
||||
unknown_devices := async_filter_known_device_ids(
|
||||
self.hass,
|
||||
device_ids=entity.script.referenced_devices,
|
||||
known_device_ids=known_device_ids,
|
||||
)
|
||||
):
|
||||
self.async_create_issue(
|
||||
issue_id=entity.entity_id,
|
||||
translation_placeholders={
|
||||
"devices": "\n".join(
|
||||
f"- `{device}`" for device in unknown_devices
|
||||
),
|
||||
"script": entity.name,
|
||||
"edit": f"/config/script/edit/{entity.unique_id}",
|
||||
"entity_id": entity.entity_id,
|
||||
},
|
||||
)
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Spook found unknown devices in %s "
|
||||
"and created an issue for it; Devices: %s",
|
||||
),
|
||||
entity.entity_id,
|
||||
", ".join(unknown_devices),
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
|
||||
|
||||
from ....entity_filtering import (
|
||||
async_extract_entities_from_config,
|
||||
async_filter_known_entity_ids_with_templates,
|
||||
async_get_all_entity_ids,
|
||||
)
|
||||
from ....repairs import AbstractSpookRepair
|
||||
|
||||
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:
|
||||
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
|
||||
) -> set[str]:
|
||||
"""Extract entities from script configuration using Template analysis.
|
||||
|
||||
This function finds template strings in script configuration and creates
|
||||
Template objects to extract entity references using Template.async_render_to_info().
|
||||
This provides more comprehensive entity detection than regex-based parsing alone.
|
||||
"""
|
||||
# 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:
|
||||
return set()
|
||||
|
||||
return await async_extract_entities_from_config(hass, config)
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookRepair):
|
||||
"""Spook repair tries to find unknown referenced entity in scripts."""
|
||||
|
||||
domain = script.DOMAIN
|
||||
repair = "script_unknown_entity_references"
|
||||
inspect_events = {
|
||||
EVENT_COMPONENT_LOADED,
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_config_entry_changed = True
|
||||
inspect_on_reload = True
|
||||
|
||||
automatically_clean_up_issues = True
|
||||
|
||||
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_inspect(self) -> None:
|
||||
"""Trigger a inspection."""
|
||||
if self.domain not in self.hass.data[DATA_INSTANCES]:
|
||||
return
|
||||
|
||||
entity_component: EntityComponent[script.ScriptEntity] = self.hass.data[
|
||||
DATA_INSTANCES
|
||||
][self.domain]
|
||||
|
||||
known_entity_ids = async_get_all_entity_ids(self.hass, include_all_none=True)
|
||||
|
||||
for entity in entity_component.entities:
|
||||
self.possible_issue_ids.add(entity.entity_id)
|
||||
if isinstance(entity, script.UnavailableScriptEntity):
|
||||
continue
|
||||
|
||||
# Get all referenced entities from the script
|
||||
all_entities = extract_referenced_entities_from_script(entity)
|
||||
|
||||
# Check for blueprint trigger inputs
|
||||
blueprint_entities = self._get_blueprint_trigger_entities(entity)
|
||||
all_entities.update(blueprint_entities)
|
||||
|
||||
# Extract entities from Template objects within the script entity
|
||||
template_entities = await extract_template_entities_from_script_entity(
|
||||
self.hass, entity
|
||||
)
|
||||
all_entities.update(template_entities)
|
||||
|
||||
# Check for unknown entities
|
||||
if unknown_entities := await async_filter_known_entity_ids_with_templates(
|
||||
self.hass,
|
||||
entity_ids=all_entities,
|
||||
known_entity_ids=known_entity_ids,
|
||||
):
|
||||
self.async_create_issue(
|
||||
issue_id=entity.entity_id,
|
||||
translation_placeholders={
|
||||
"entities": "\n".join(
|
||||
f"- `{entity_id}`" for entity_id in unknown_entities
|
||||
),
|
||||
"script": entity.name,
|
||||
"edit": f"/config/script/edit/{entity.unique_id}",
|
||||
"entity_id": entity.entity_id,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.helpers import floor_registry as fr
|
||||
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
|
||||
|
||||
from ....const import LOGGER
|
||||
from ....entity_filtering import async_filter_known_floor_ids, async_get_all_floor_ids
|
||||
from ....repairs import AbstractSpookRepair
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookRepair):
|
||||
"""Spook repair tries to find unknown referenced floors in scripts."""
|
||||
|
||||
domain = script.DOMAIN
|
||||
repair = "script_unknown_floor_references"
|
||||
inspect_events = {fr.EVENT_FLOOR_REGISTRY_UPDATED}
|
||||
inspect_on_reload = True
|
||||
|
||||
automatically_clean_up_issues = True
|
||||
|
||||
async def async_inspect(self) -> None:
|
||||
"""Trigger a inspection."""
|
||||
if self.domain not in self.hass.data[DATA_INSTANCES]:
|
||||
return
|
||||
|
||||
entity_component: EntityComponent[script.ScriptEntity] = self.hass.data[
|
||||
DATA_INSTANCES
|
||||
][self.domain]
|
||||
|
||||
known_floor_ids = async_get_all_floor_ids(self.hass)
|
||||
|
||||
LOGGER.debug("Spook is inspecting: %s", self.repair)
|
||||
for entity in entity_component.entities:
|
||||
self.possible_issue_ids.add(entity.entity_id)
|
||||
if not isinstance(entity, script.UnavailableScriptEntity) and (
|
||||
unknown_floors := async_filter_known_floor_ids(
|
||||
self.hass,
|
||||
floor_ids=entity.script.referenced_floors,
|
||||
known_floor_ids=known_floor_ids,
|
||||
)
|
||||
):
|
||||
self.async_create_issue(
|
||||
issue_id=entity.entity_id,
|
||||
translation_placeholders={
|
||||
"floors": "\n".join(f"- `{floor}`" for floor in unknown_floors),
|
||||
"script": entity.name,
|
||||
"edit": f"/config/script/edit/{entity.unique_id}",
|
||||
"entity_id": entity.entity_id,
|
||||
},
|
||||
)
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Spook found unknown floors in %s "
|
||||
"and created an issue for it; Floors: %s",
|
||||
),
|
||||
entity.entity_id,
|
||||
", ".join(unknown_floors),
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.helpers import label_registry as lr
|
||||
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
|
||||
|
||||
from ....const import LOGGER
|
||||
from ....entity_filtering import async_filter_known_label_ids, async_get_all_label_ids
|
||||
from ....repairs import AbstractSpookRepair
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookRepair):
|
||||
"""Spook repair tries to find unknown referenced labels in scripts."""
|
||||
|
||||
domain = script.DOMAIN
|
||||
repair = "script_unknown_label_references"
|
||||
inspect_events = {lr.EVENT_LABEL_REGISTRY_UPDATED}
|
||||
inspect_on_reload = True
|
||||
|
||||
automatically_clean_up_issues = True
|
||||
|
||||
async def async_inspect(self) -> None:
|
||||
"""Trigger a inspection."""
|
||||
if self.domain not in self.hass.data[DATA_INSTANCES]:
|
||||
return
|
||||
|
||||
entity_component: EntityComponent[script.ScriptEntity] = self.hass.data[
|
||||
DATA_INSTANCES
|
||||
][self.domain]
|
||||
|
||||
known_label_ids = async_get_all_label_ids(self.hass)
|
||||
|
||||
LOGGER.debug("Spook is inspecting: %s", self.repair)
|
||||
for entity in entity_component.entities:
|
||||
self.possible_issue_ids.add(entity.entity_id)
|
||||
if not isinstance(entity, script.UnavailableScriptEntity) and (
|
||||
unknown_labels := async_filter_known_label_ids(
|
||||
self.hass,
|
||||
label_ids=entity.script.referenced_labels,
|
||||
known_label_ids=known_label_ids,
|
||||
)
|
||||
):
|
||||
self.async_create_issue(
|
||||
issue_id=entity.entity_id,
|
||||
translation_placeholders={
|
||||
"labels": "\n".join(f"- `{label}`" for label in unknown_labels),
|
||||
"script": entity.name,
|
||||
"edit": f"/config/script/edit/{entity.unique_id}",
|
||||
"entity_id": entity.entity_id,
|
||||
},
|
||||
)
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Spook found unknown labels in %s "
|
||||
"and created an issue for it; Labels: %s",
|
||||
),
|
||||
entity.entity_id,
|
||||
", ".join(unknown_labels),
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import script
|
||||
from homeassistant.const import (
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
EVENT_SERVICE_REMOVED,
|
||||
)
|
||||
|
||||
from ....entity_filtering import (
|
||||
async_filter_known_services,
|
||||
async_find_services_in_sequence,
|
||||
async_get_all_services,
|
||||
)
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced services in scripts."""
|
||||
|
||||
domain = script.DOMAIN
|
||||
repair = "script_unknown_service_references"
|
||||
inspect_events = {
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
EVENT_SERVICE_REMOVED,
|
||||
}
|
||||
inspect_config_entry_changed = True
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = script.UnavailableScriptEntity
|
||||
entity_label = "script"
|
||||
reference_label = "services"
|
||||
edit_url_pattern = "/config/script/edit/{unique_id}"
|
||||
|
||||
_known_services: set[str]
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known services for this inspection cycle."""
|
||||
self._known_services = async_get_all_services(self.hass)
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown services called by ``entity``."""
|
||||
return async_filter_known_services(
|
||||
self.hass,
|
||||
services=async_find_services_in_sequence(entity.script.sequence),
|
||||
known_services=self._known_services,
|
||||
)
|
||||
Reference in New Issue
Block a user