Fix template migration errors (#157949)

This commit is contained in:
Petro31
2025-12-04 10:16:58 -05:00
committed by GitHub
parent 9a9f8271b3
commit 002eed24f1
2 changed files with 280 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
"""Helpers for template integration."""
from collections.abc import Callable
from enum import Enum
from enum import StrEnum
import hashlib
import itertools
import logging
@@ -33,6 +33,7 @@ from homeassistant.helpers.entity_platform import (
async_get_platforms,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import yaml as yaml_util
@@ -190,12 +191,12 @@ def async_create_template_tracking_entities(
async_add_entities(entities)
def _format_template(value: Any) -> Any:
def _format_template(value: Any, field: str | None = None) -> Any:
if isinstance(value, template.Template):
return value.template
if isinstance(value, Enum):
return value.name
if isinstance(value, StrEnum):
return value.value
if isinstance(value, (int, float, str, bool)):
return value
@@ -207,14 +208,13 @@ 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 isinstance(item, (dict, list)):
if len(item) > 0:
items.append(format_migration_config(item, depth + 1))
else:
@@ -223,9 +223,18 @@ def format_migration_config(
formatted_config = {}
for field, value in config.items():
if isinstance(value, types):
if isinstance(value, dict):
if len(value) > 0:
formatted_config[field] = format_migration_config(value, depth + 1)
elif isinstance(value, list):
if len(value) > 0:
formatted_config[field] = format_migration_config(value, depth + 1)
else:
formatted_config[field] = []
elif isinstance(value, ScriptVariables):
formatted_config[field] = format_migration_config(
value.as_dict(), depth + 1
)
else:
formatted_config[field] = _format_template(value)

View File

@@ -600,6 +600,270 @@ async def test_legacy_deprecation(
assert "platform: template" not in issue.translation_placeholders["config"]
@pytest.mark.parametrize(
("domain", "config", "strings_to_check"),
[
(
"light",
{
"light": {
"platform": "template",
"lights": {
"garage_light_template": {
"friendly_name": "Garage Light Template",
"min_mireds_template": 153,
"max_mireds_template": 500,
"turn_on": [],
"turn_off": [],
"set_temperature": [],
"set_hs": [],
"set_level": [],
}
},
},
},
[
"turn_on: []",
"turn_off: []",
"set_temperature: []",
"set_hs: []",
"set_level: []",
],
),
(
"switch",
{
"switch": {
"platform": "template",
"switches": {
"my_switch": {
"friendly_name": "Switch Template",
"turn_on": [],
"turn_off": [],
}
},
},
},
[
"turn_on: []",
"turn_off: []",
],
),
(
"light",
{
"light": [
{
"platform": "template",
"lights": {
"atrium_lichterkette": {
"unique_id": "atrium_lichterkette",
"friendly_name": "Atrium Lichterkette",
"value_template": "{{ states('input_boolean.atrium_lichterkette_power') }}",
"level_template": "{% if is_state('input_boolean.atrium_lichterkette_power', 'off') %}\n 0\n{% else %}\n {{ states('input_number.atrium_lichterkette_brightness') | int * (255 / state_attr('input_number.atrium_lichterkette_brightness', 'max') | int) }}\n{% endif %}",
"effect_list_template": "{{ state_attr('input_select.atrium_lichterkette_mode', 'options') }}",
"effect_template": "'{{ states('input_select.atrium_lichterkette_mode')}}'",
"turn_on": [
{
"service": "button.press",
"target": {
"entity_id": "button.esphome_web_28a814_lichterkette_on"
},
},
{
"service": "input_boolean.turn_on",
"target": {
"entity_id": "input_boolean.atrium_lichterkette_power"
},
},
],
"turn_off": [
{
"service": "button.press",
"target": {
"entity_id": "button.esphome_web_28a814_lichterkette_off"
},
},
{
"service": "input_boolean.turn_off",
"target": {
"entity_id": "input_boolean.atrium_lichterkette_power"
},
},
],
"set_level": [
{
"variables": {
"scaled": "{{ (brightness / (255 / state_attr('input_number.atrium_lichterkette_brightness', 'max'))) | round | int }}",
"diff": "{{ scaled | int - states('input_number.atrium_lichterkette_brightness') | int }}",
"direction": "{{ 'dim' if diff | int < 0 else 'bright' }}",
}
},
{
"repeat": {
"count": "{{ diff | int | abs }}",
"sequence": [
{
"service": "button.press",
"target": {
"entity_id": "button.esphome_web_28a814_lichterkette_{{ direction }}"
},
},
{"delay": {"milliseconds": 500}},
],
}
},
{
"service": "input_number.set_value",
"data": {
"value": "{{ scaled }}",
"entity_id": "input_number.atrium_lichterkette_brightness",
},
},
],
"set_effect": [
{
"service": "button.press",
"target": {
"entity_id": "button.esphome_web_28a814_lichterkette_{{ effect }}"
},
}
],
}
},
}
]
},
[
"scaled: ",
"diff: ",
"direction: ",
],
),
(
"cover",
{
"cover": [
{
"platform": "template",
"covers": {
"large_garage_door": {
"device_class": "garage",
"friendly_name": "Large Garage Door",
"value_template": "{% if is_state('binary_sensor.large_garage_door', 'off') %}\n closed\n{% elif is_state('timer.large_garage_opening_timer', 'active') %}\n opening\n{% elif is_state('timer.large_garage_closing_timer', 'active') %} \n closing\n{% elif is_state('binary_sensor.large_garage_door', 'on') %}\n open\n{% endif %}\n",
"open_cover": [
{
"condition": "state",
"entity_id": "binary_sensor.large_garage_door",
"state": "off",
},
{
"action": "switch.turn_on",
"target": {
"entity_id": "switch.garage_door_relay_1"
},
},
{
"action": "timer.start",
"entity_id": "timer.large_garage_opening_timer",
},
],
"close_cover": [
{
"condition": "state",
"entity_id": "binary_sensor.large_garage_door",
"state": "on",
},
{
"action": "switch.turn_on",
"target": {
"entity_id": "switch.garage_door_relay_1"
},
},
{
"action": "timer.start",
"entity_id": "timer.large_garage_closing_timer",
},
],
"stop_cover": [
{
"action": "switch.turn_on",
"target": {
"entity_id": "switch.garage_door_relay_1"
},
},
{
"action": "timer.cancel",
"entity_id": "timer.large_garage_opening_timer",
},
{
"action": "timer.cancel",
"entity_id": "timer.large_garage_closing_timer",
},
],
}
},
}
]
},
["device_class: garage"],
),
(
"binary_sensor",
{
"binary_sensor": {
"platform": "template",
"sensors": {
"motion_sensor": {
"friendly_name": "Motion Sensor",
"device_class": "motion",
"value_template": "{{ is_state('sensor.motion_detector', 'on') }}",
}
},
},
},
["device_class: motion"],
),
(
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"some_sensor": {
"friendly_name": "Sensor",
"device_class": "timestamp",
"value_template": "{{ now().isoformat() }}",
}
},
},
},
["device_class: timestamp"],
),
],
)
async def test_legacy_deprecation_with_unique_objects(
hass: HomeAssistant,
domain: str,
config: dict,
strings_to_check: list[str],
issue_registry: ir.IssueRegistry,
) -> None:
"""Test legacy configuration raises issue and unique objects are properly converted to valid configurations."""
await async_setup_component(hass, domain, config)
await hass.async_block_till_done()
assert len(issue_registry.issues) == 1
issue = next(iter(issue_registry.issues.values()))
assert issue.domain == "template"
assert issue.severity == ir.IssueSeverity.WARNING
assert issue.translation_placeholders is not None
for string in strings_to_check:
assert string in issue.translation_placeholders["config"]
@pytest.mark.parametrize(
("domain", "config"),
[