mirror of
https://github.com/home-assistant/core.git
synced 2025-12-02 22:18:08 +00:00
384 lines
12 KiB
Python
384 lines
12 KiB
Python
"""Helpers for template integration."""
|
|
|
|
from collections.abc import Callable
|
|
from enum import Enum
|
|
import hashlib
|
|
import itertools
|
|
import logging
|
|
from typing import Any
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components import blueprint
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
CONF_ENTITY_PICTURE_TEMPLATE,
|
|
CONF_FRIENDLY_NAME,
|
|
CONF_ICON,
|
|
CONF_ICON_TEMPLATE,
|
|
CONF_NAME,
|
|
CONF_STATE,
|
|
CONF_UNIQUE_ID,
|
|
CONF_VALUE_TEMPLATE,
|
|
SERVICE_RELOAD,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
from homeassistant.helpers import issue_registry as ir, template
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.entity_platform import (
|
|
AddConfigEntryEntitiesCallback,
|
|
AddEntitiesCallback,
|
|
async_get_platforms,
|
|
)
|
|
from homeassistant.helpers.issue_registry import IssueSeverity
|
|
from homeassistant.helpers.singleton import singleton
|
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
from homeassistant.util import yaml as yaml_util
|
|
from homeassistant.util.hass_dict import HassKey
|
|
|
|
from .const import (
|
|
CONF_ADVANCED_OPTIONS,
|
|
CONF_ATTRIBUTE_TEMPLATES,
|
|
CONF_ATTRIBUTES,
|
|
CONF_AVAILABILITY,
|
|
CONF_AVAILABILITY_TEMPLATE,
|
|
CONF_DEFAULT_ENTITY_ID,
|
|
CONF_PICTURE,
|
|
DOMAIN,
|
|
PLATFORMS,
|
|
)
|
|
from .entity import AbstractTemplateEntity
|
|
from .template_entity import TemplateEntity
|
|
from .trigger_entity import TriggerEntity
|
|
|
|
LEGACY_TEMPLATE_DEPRECATION_KEY = "deprecate_legacy_templates"
|
|
|
|
DATA_BLUEPRINTS = "template_blueprints"
|
|
DATA_DEPRECATION: HassKey[list[str]] = HassKey(LEGACY_TEMPLATE_DEPRECATION_KEY)
|
|
|
|
LEGACY_FIELDS = {
|
|
CONF_ICON_TEMPLATE: CONF_ICON,
|
|
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
|
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
|
|
CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
|
|
CONF_FRIENDLY_NAME: CONF_NAME,
|
|
}
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
type CreateTemplateEntitiesCallback = Callable[
|
|
[type[TemplateEntity], AddEntitiesCallback, HomeAssistant, list[dict], str | None],
|
|
None,
|
|
]
|
|
|
|
|
|
@callback
|
|
def templates_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]:
|
|
"""Return all template entity ids that reference the blueprint."""
|
|
return [
|
|
entity_id
|
|
for platform in async_get_platforms(hass, DOMAIN)
|
|
for entity_id, template_entity in platform.entities.items()
|
|
if isinstance(template_entity, AbstractTemplateEntity)
|
|
and template_entity.referenced_blueprint == blueprint_path
|
|
]
|
|
|
|
|
|
@callback
|
|
def blueprint_in_template(hass: HomeAssistant, entity_id: str) -> str | None:
|
|
"""Return the blueprint the template entity is based on or None."""
|
|
for platform in async_get_platforms(hass, DOMAIN):
|
|
if isinstance(
|
|
(template_entity := platform.entities.get(entity_id)),
|
|
AbstractTemplateEntity,
|
|
):
|
|
return template_entity.referenced_blueprint
|
|
return None
|
|
|
|
|
|
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
|
"""Return True if any template references the blueprint."""
|
|
return len(templates_with_blueprint(hass, blueprint_path)) > 0
|
|
|
|
|
|
async def _reload_blueprint_templates(hass: HomeAssistant, blueprint_path: str) -> None:
|
|
"""Reload all templates that rely on a specific blueprint."""
|
|
await hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
|
|
|
|
|
@singleton(DATA_BLUEPRINTS)
|
|
@callback
|
|
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
|
"""Get template blueprints."""
|
|
from .config import TEMPLATE_BLUEPRINT_SCHEMA # noqa: PLC0415
|
|
|
|
return blueprint.DomainBlueprints(
|
|
hass,
|
|
DOMAIN,
|
|
_LOGGER,
|
|
_blueprint_in_use,
|
|
_reload_blueprint_templates,
|
|
TEMPLATE_BLUEPRINT_SCHEMA,
|
|
)
|
|
|
|
|
|
def rewrite_legacy_to_modern_config(
|
|
hass: HomeAssistant,
|
|
entity_cfg: dict[str, Any],
|
|
extra_legacy_fields: dict[str, str],
|
|
) -> dict[str, Any]:
|
|
"""Rewrite legacy config."""
|
|
entity_cfg = {**entity_cfg}
|
|
|
|
for from_key, to_key in itertools.chain(
|
|
LEGACY_FIELDS.items(), extra_legacy_fields.items()
|
|
):
|
|
if from_key not in entity_cfg or to_key in entity_cfg:
|
|
continue
|
|
|
|
val = entity_cfg.pop(from_key)
|
|
if isinstance(val, str):
|
|
val = template.Template(val, hass)
|
|
entity_cfg[to_key] = val
|
|
|
|
if CONF_NAME in entity_cfg and isinstance(entity_cfg[CONF_NAME], str):
|
|
entity_cfg[CONF_NAME] = template.Template(entity_cfg[CONF_NAME], hass)
|
|
|
|
return entity_cfg
|
|
|
|
|
|
def rewrite_legacy_to_modern_configs(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
entity_cfg: dict[str, dict],
|
|
extra_legacy_fields: dict[str, str],
|
|
) -> list[dict]:
|
|
"""Rewrite legacy configuration definitions to modern ones."""
|
|
entities = []
|
|
for object_id, entity_conf in entity_cfg.items():
|
|
entity_conf = {**entity_conf, CONF_DEFAULT_ENTITY_ID: f"{domain}.{object_id}"}
|
|
|
|
entity_conf = rewrite_legacy_to_modern_config(
|
|
hass, entity_conf, extra_legacy_fields
|
|
)
|
|
|
|
if CONF_NAME not in entity_conf:
|
|
entity_conf[CONF_NAME] = template.Template(object_id, hass)
|
|
|
|
entities.append(entity_conf)
|
|
|
|
return entities
|
|
|
|
|
|
@callback
|
|
def async_create_template_tracking_entities(
|
|
entity_cls: type[Entity],
|
|
async_add_entities: AddEntitiesCallback,
|
|
hass: HomeAssistant,
|
|
definitions: list[dict],
|
|
unique_id_prefix: str | None,
|
|
) -> None:
|
|
"""Create the template tracking entities."""
|
|
entities: list[Entity] = []
|
|
for definition in definitions:
|
|
unique_id = definition.get(CONF_UNIQUE_ID)
|
|
if unique_id and unique_id_prefix:
|
|
unique_id = f"{unique_id_prefix}-{unique_id}"
|
|
entities.append(entity_cls(hass, definition, unique_id)) # type: ignore[call-arg]
|
|
async_add_entities(entities)
|
|
|
|
|
|
def _format_template(value: Any) -> Any:
|
|
if isinstance(value, template.Template):
|
|
return value.template
|
|
|
|
if isinstance(value, Enum):
|
|
return value.name
|
|
|
|
if isinstance(value, (int, float, str, bool)):
|
|
return value
|
|
|
|
return str(value)
|
|
|
|
|
|
def format_migration_config(
|
|
config: ConfigType | list[ConfigType], depth: int = 0
|
|
) -> ConfigType | list[ConfigType]:
|
|
"""Recursive method to format templates as strings from ConfigType."""
|
|
types = (dict, list)
|
|
if depth > 9:
|
|
raise RecursionError
|
|
|
|
if isinstance(config, list):
|
|
items = []
|
|
for item in config:
|
|
if isinstance(item, types):
|
|
if len(item) > 0:
|
|
items.append(format_migration_config(item, depth + 1))
|
|
else:
|
|
items.append(_format_template(item))
|
|
return items # type: ignore[return-value]
|
|
|
|
formatted_config = {}
|
|
for field, value in config.items():
|
|
if isinstance(value, types):
|
|
if len(value) > 0:
|
|
formatted_config[field] = format_migration_config(value, depth + 1)
|
|
else:
|
|
formatted_config[field] = _format_template(value)
|
|
|
|
return formatted_config
|
|
|
|
|
|
def create_legacy_template_issue(
|
|
hass: HomeAssistant, config: ConfigType, domain: str
|
|
) -> None:
|
|
"""Create a repair for legacy template entities."""
|
|
if domain not in PLATFORMS:
|
|
return
|
|
|
|
breadcrumb = "Template Entity"
|
|
# Default entity id should be in most legacy configuration because
|
|
# it's created from the legacy slug. Vacuum and Lock do not have a
|
|
# slug, therefore we need to use the name or unique_id.
|
|
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
|
breadcrumb = default_entity_id.split(".")[-1]
|
|
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
|
breadcrumb = f"unique_id: {unique_id}"
|
|
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
|
breadcrumb = name.template
|
|
|
|
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
|
|
|
|
if (deprecation_list := hass.data.get(DATA_DEPRECATION)) is None:
|
|
hass.data[DATA_DEPRECATION] = deprecation_list = []
|
|
|
|
deprecation_list.append(issue_id)
|
|
|
|
try:
|
|
modified_yaml = format_migration_config(config)
|
|
yaml_config = yaml_util.dump({DOMAIN: [{domain: [modified_yaml]}]})
|
|
# Format to show up properly in a numbered bullet on the repair.
|
|
yaml_config = " ```\n " + yaml_config.replace("\n", "\n ") + "```"
|
|
except RecursionError:
|
|
yaml_config = f"{DOMAIN}:\n - {domain}: - ..."
|
|
|
|
ir.async_create_issue(
|
|
hass,
|
|
DOMAIN,
|
|
issue_id,
|
|
breaks_in_ha_version="2026.6",
|
|
is_fixable=False,
|
|
severity=IssueSeverity.WARNING,
|
|
translation_key="deprecated_legacy_templates",
|
|
translation_placeholders={
|
|
"domain": domain,
|
|
"breadcrumb": breadcrumb,
|
|
"config": yaml_config,
|
|
},
|
|
)
|
|
|
|
|
|
async def async_setup_template_platform(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
config: ConfigType,
|
|
state_entity_cls: type[TemplateEntity],
|
|
trigger_entity_cls: type[TriggerEntity] | None,
|
|
async_add_entities: AddEntitiesCallback,
|
|
discovery_info: DiscoveryInfoType | None,
|
|
legacy_fields: dict[str, str] | None = None,
|
|
legacy_key: str | None = None,
|
|
) -> None:
|
|
"""Set up the Template platform."""
|
|
if discovery_info is None:
|
|
# Legacy Configuration
|
|
if legacy_fields is not None:
|
|
if legacy_key:
|
|
configs = rewrite_legacy_to_modern_configs(
|
|
hass, domain, config[legacy_key], legacy_fields
|
|
)
|
|
else:
|
|
configs = [rewrite_legacy_to_modern_config(hass, config, legacy_fields)]
|
|
|
|
for definition in configs:
|
|
create_legacy_template_issue(hass, definition, domain)
|
|
|
|
async_create_template_tracking_entities(
|
|
state_entity_cls,
|
|
async_add_entities,
|
|
hass,
|
|
configs,
|
|
None,
|
|
)
|
|
else:
|
|
_LOGGER.warning(
|
|
"Template %s entities can only be configured under template:", domain
|
|
)
|
|
return
|
|
|
|
# Trigger Configuration
|
|
if "coordinator" in discovery_info:
|
|
if trigger_entity_cls:
|
|
entities = [
|
|
trigger_entity_cls(hass, discovery_info["coordinator"], config)
|
|
for config in discovery_info["entities"]
|
|
]
|
|
async_add_entities(entities)
|
|
else:
|
|
raise PlatformNotReady(
|
|
f"The template {domain} platform doesn't support trigger entities"
|
|
)
|
|
return
|
|
|
|
# Modern Configuration
|
|
async_create_template_tracking_entities(
|
|
state_entity_cls,
|
|
async_add_entities,
|
|
hass,
|
|
discovery_info["entities"],
|
|
discovery_info["unique_id"],
|
|
)
|
|
|
|
|
|
async def async_setup_template_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
state_entity_cls: type[TemplateEntity],
|
|
config_schema: vol.Schema | vol.All,
|
|
replace_value_template: bool = False,
|
|
) -> None:
|
|
"""Setup the Template from a config entry."""
|
|
options = dict(config_entry.options)
|
|
options.pop("template_type")
|
|
|
|
if advanced_options := options.pop(CONF_ADVANCED_OPTIONS, None):
|
|
options = {**options, **advanced_options}
|
|
|
|
if replace_value_template and CONF_VALUE_TEMPLATE in options:
|
|
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
|
|
|
validated_config = config_schema(options)
|
|
|
|
async_add_entities(
|
|
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
|
)
|
|
|
|
|
|
def async_setup_template_preview[T: TemplateEntity](
|
|
hass: HomeAssistant,
|
|
name: str,
|
|
config: ConfigType,
|
|
state_entity_cls: type[T],
|
|
schema: vol.Schema | vol.All,
|
|
replace_value_template: bool = False,
|
|
) -> T:
|
|
"""Setup the Template preview."""
|
|
if replace_value_template and CONF_VALUE_TEMPLATE in config:
|
|
config[CONF_STATE] = config.pop(CONF_VALUE_TEMPLATE)
|
|
|
|
validated_config = schema(config | {CONF_NAME: name})
|
|
return state_entity_cls(hass, validated_config, None)
|