Skip to content

Commit 405a994

Browse files
authored
Deprecate legacy and undocumented template entity configurations (home-assistant#155355)
1 parent 0e3bab3 commit 405a994

File tree

8 files changed

+456
-15
lines changed

8 files changed

+456
-15
lines changed

homeassistant/components/template/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919
from homeassistant.core import Event, HomeAssistant, ServiceCall
2020
from homeassistant.exceptions import ConfigEntryError, HomeAssistantError
21-
from homeassistant.helpers import discovery
21+
from homeassistant.helpers import discovery, issue_registry as ir
2222
from homeassistant.helpers.device import (
2323
async_remove_stale_devices_links_keep_current_device,
2424
)
@@ -30,12 +30,21 @@
3030

3131
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN, PLATFORMS
3232
from .coordinator import TriggerUpdateCoordinator
33-
from .helpers import async_get_blueprints
33+
from .helpers import DATA_DEPRECATION, async_get_blueprints
3434

3535
_LOGGER = logging.getLogger(__name__)
3636
DATA_COORDINATORS: HassKey[list[TriggerUpdateCoordinator]] = HassKey(DOMAIN)
3737

3838

39+
def _clean_up_legacy_template_deprecations(hass: HomeAssistant) -> None:
40+
if (found_issues := hass.data.pop(DATA_DEPRECATION, None)) is not None:
41+
issue_registry = ir.async_get(hass)
42+
for domain, issue_id in set(issue_registry.issues):
43+
if domain != DOMAIN or issue_id in found_issues:
44+
continue
45+
ir.async_delete_issue(hass, DOMAIN, issue_id)
46+
47+
3948
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
4049
"""Set up the template integration."""
4150

@@ -54,6 +63,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
5463

5564
async def _reload_config(call: Event | ServiceCall) -> None:
5665
"""Reload top-level + platforms."""
66+
hass.data.pop(DATA_DEPRECATION, None)
67+
5768
await async_get_blueprints(hass).async_reset_cache()
5869
try:
5970
unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
@@ -74,6 +85,7 @@ async def _reload_config(call: Event | ServiceCall) -> None:
7485
if DOMAIN in conf:
7586
await _process_config(hass, conf)
7687

88+
_clean_up_legacy_template_deprecations(hass)
7789
hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
7890

7991
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)

homeassistant/components/template/config.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@
7272
weather as weather_platform,
7373
)
7474
from .const import CONF_DEFAULT_ENTITY_ID, DOMAIN, PLATFORMS, TemplateConfig
75-
from .helpers import async_get_blueprints, rewrite_legacy_to_modern_configs
75+
from .helpers import (
76+
async_get_blueprints,
77+
create_legacy_template_issue,
78+
rewrite_legacy_to_modern_configs,
79+
)
7680

7781
_LOGGER = logging.getLogger(__name__)
7882

@@ -386,11 +390,11 @@ async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> Conf
386390
definitions = (
387391
list(template_config[new_key]) if new_key in template_config else []
388392
)
389-
definitions.extend(
390-
rewrite_legacy_to_modern_configs(
391-
hass, new_key, template_config[old_key], legacy_fields
392-
)
393-
)
393+
for definition in rewrite_legacy_to_modern_configs(
394+
hass, new_key, template_config[old_key], legacy_fields
395+
):
396+
create_legacy_template_issue(hass, definition, new_key)
397+
definitions.append(definition)
394398
template_config = TemplateConfig({**template_config, new_key: definitions})
395399

396400
config_sections.append(template_config)

homeassistant/components/template/helpers.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Helpers for template integration."""
22

33
from collections.abc import Callable
4+
from enum import Enum
5+
import hashlib
46
import itertools
57
import logging
68
from typing import Any
@@ -22,15 +24,18 @@
2224
)
2325
from homeassistant.core import HomeAssistant, callback
2426
from homeassistant.exceptions import PlatformNotReady
25-
from homeassistant.helpers import template
27+
from homeassistant.helpers import issue_registry as ir, template
2628
from homeassistant.helpers.entity import Entity
2729
from homeassistant.helpers.entity_platform import (
2830
AddConfigEntryEntitiesCallback,
2931
AddEntitiesCallback,
3032
async_get_platforms,
3133
)
34+
from homeassistant.helpers.issue_registry import IssueSeverity
3235
from homeassistant.helpers.singleton import singleton
3336
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
37+
from homeassistant.util import yaml as yaml_util
38+
from homeassistant.util.hass_dict import HassKey
3439

3540
from .const import (
3641
CONF_ADVANCED_OPTIONS,
@@ -46,7 +51,10 @@
4651
from .template_entity import TemplateEntity
4752
from .trigger_entity import TriggerEntity
4853

54+
LEGACY_TEMPLATE_DEPRECATION_KEY = "deprecate_legacy_templates"
55+
4956
DATA_BLUEPRINTS = "template_blueprints"
57+
DATA_DEPRECATION: HassKey[list[str]] = HassKey(LEGACY_TEMPLATE_DEPRECATION_KEY)
5058

5159
LEGACY_FIELDS = {
5260
CONF_ICON_TEMPLATE: CONF_ICON,
@@ -180,6 +188,95 @@ def async_create_template_tracking_entities(
180188
async_add_entities(entities)
181189

182190

191+
def _format_template(value: Any) -> Any:
192+
if isinstance(value, template.Template):
193+
return value.template
194+
195+
if isinstance(value, Enum):
196+
return value.name
197+
198+
if isinstance(value, (int, float, str, bool)):
199+
return value
200+
201+
return str(value)
202+
203+
204+
def format_migration_config(
205+
config: ConfigType | list[ConfigType], depth: int = 0
206+
) -> ConfigType | list[ConfigType]:
207+
"""Recursive method to format templates as strings from ConfigType."""
208+
types = (dict, list)
209+
if depth > 9:
210+
raise RecursionError
211+
212+
if isinstance(config, list):
213+
items = []
214+
for item in config:
215+
if isinstance(item, types):
216+
if len(item) > 0:
217+
items.append(format_migration_config(item, depth + 1))
218+
else:
219+
items.append(_format_template(item))
220+
return items # type: ignore[return-value]
221+
222+
formatted_config = {}
223+
for field, value in config.items():
224+
if isinstance(value, types):
225+
if len(value) > 0:
226+
formatted_config[field] = format_migration_config(value, depth + 1)
227+
else:
228+
formatted_config[field] = _format_template(value)
229+
230+
return formatted_config
231+
232+
233+
def create_legacy_template_issue(
234+
hass: HomeAssistant, config: ConfigType, domain: str
235+
) -> None:
236+
"""Create a repair for legacy template entities."""
237+
238+
breadcrumb = "Template Entity"
239+
# Default entity id should be in most legacy configuration because
240+
# it's created from the legacy slug. Vacuum and Lock do not have a
241+
# slug, therefore we need to use the name or unique_id.
242+
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
243+
breadcrumb = default_entity_id.split(".")[-1]
244+
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
245+
breadcrumb = f"unique_id: {unique_id}"
246+
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
247+
breadcrumb = name.template
248+
249+
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
250+
251+
if (deprecation_list := hass.data.get(DATA_DEPRECATION)) is None:
252+
hass.data[DATA_DEPRECATION] = deprecation_list = []
253+
254+
deprecation_list.append(issue_id)
255+
256+
try:
257+
modified_yaml = format_migration_config(config)
258+
yaml_config = yaml_util.dump({DOMAIN: [{domain: [modified_yaml]}]})
259+
# Format to show up properly in a numbered bullet on the repair.
260+
yaml_config = " ```\n " + yaml_config.replace("\n", "\n ") + "```"
261+
except RecursionError:
262+
yaml_config = f"{DOMAIN}:\n - {domain}: - ..."
263+
264+
ir.async_create_issue(
265+
hass,
266+
DOMAIN,
267+
issue_id,
268+
breaks_in_ha_version="2026.6",
269+
is_fixable=False,
270+
severity=IssueSeverity.WARNING,
271+
translation_key="deprecated_legacy_templates",
272+
translation_placeholders={
273+
"domain": domain,
274+
"breadcrumb": breadcrumb,
275+
"config": yaml_config,
276+
},
277+
)
278+
279+
183280
async def async_setup_template_platform(
184281
hass: HomeAssistant,
185282
domain: str,
@@ -201,6 +298,10 @@ async def async_setup_template_platform(
201298
)
202299
else:
203300
configs = [rewrite_legacy_to_modern_config(hass, config, legacy_fields)]
301+
302+
for definition in configs:
303+
create_legacy_template_issue(hass, definition, domain)
304+
204305
async_create_template_tracking_entities(
205306
state_entity_cls,
206307
async_add_entities,

homeassistant/components/template/strings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@
527527
"deprecated_battery_level": {
528528
"description": "The template vacuum options `battery_level` and `battery_level_template` are being removed in 2026.8.\n\nPlease remove the `battery_level` or `battery_level_template` option from the YAML configuration for {entity_id} ({entity_name}).",
529529
"title": "Deprecated battery level option in {entity_name}"
530+
},
531+
"deprecated_legacy_templates": {
532+
"description": "The legacy `platform: template` syntax for `{domain}` is being removed. Please migrate `{breadcrumb}` to the modern template syntax.\n\n1. Remove existing template definition.\n2. Add new template definition:\n{config}\n3. Restart Home Assistant or reload template entities.",
533+
"title": "Legacy {domain} template deprecation"
530534
}
531535
},
532536
"options": {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
sensor:
2+
- platform: template
3+
sensors:
4+
state:
5+
value_template: "{{ states.sensor.test_sensor.state }}"
6+
template:
7+
- sensor:
8+
- default_entity_id: sensor.state2
9+
state: "{{ states.sensor.test_sensor.state }}"
10+
- default_entity_id: sensor.state3
11+
state: "{{ states.sensor.test_sensor.state }}"

0 commit comments

Comments
 (0)