Skip to content

Commit 002eed2

Browse files
authored
Fix template migration errors (home-assistant#157949)
1 parent 9a9f827 commit 002eed2

File tree

2 files changed

+280
-7
lines changed

2 files changed

+280
-7
lines changed

homeassistant/components/template/helpers.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Helpers for template integration."""
22

33
from collections.abc import Callable
4-
from enum import Enum
4+
from enum import StrEnum
55
import hashlib
66
import itertools
77
import logging
@@ -33,6 +33,7 @@
3333
async_get_platforms,
3434
)
3535
from homeassistant.helpers.issue_registry import IssueSeverity
36+
from homeassistant.helpers.script_variables import ScriptVariables
3637
from homeassistant.helpers.singleton import singleton
3738
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
3839
from homeassistant.util import yaml as yaml_util
@@ -190,12 +191,12 @@ def async_create_template_tracking_entities(
190191
async_add_entities(entities)
191192

192193

193-
def _format_template(value: Any) -> Any:
194+
def _format_template(value: Any, field: str | None = None) -> Any:
194195
if isinstance(value, template.Template):
195196
return value.template
196197

197-
if isinstance(value, Enum):
198-
return value.name
198+
if isinstance(value, StrEnum):
199+
return value.value
199200

200201
if isinstance(value, (int, float, str, bool)):
201202
return value
@@ -207,14 +208,13 @@ def format_migration_config(
207208
config: ConfigType | list[ConfigType], depth: int = 0
208209
) -> ConfigType | list[ConfigType]:
209210
"""Recursive method to format templates as strings from ConfigType."""
210-
types = (dict, list)
211211
if depth > 9:
212212
raise RecursionError
213213

214214
if isinstance(config, list):
215215
items = []
216216
for item in config:
217-
if isinstance(item, types):
217+
if isinstance(item, (dict, list)):
218218
if len(item) > 0:
219219
items.append(format_migration_config(item, depth + 1))
220220
else:
@@ -223,9 +223,18 @@ def format_migration_config(
223223

224224
formatted_config = {}
225225
for field, value in config.items():
226-
if isinstance(value, types):
226+
if isinstance(value, dict):
227227
if len(value) > 0:
228228
formatted_config[field] = format_migration_config(value, depth + 1)
229+
elif isinstance(value, list):
230+
if len(value) > 0:
231+
formatted_config[field] = format_migration_config(value, depth + 1)
232+
else:
233+
formatted_config[field] = []
234+
elif isinstance(value, ScriptVariables):
235+
formatted_config[field] = format_migration_config(
236+
value.as_dict(), depth + 1
237+
)
229238
else:
230239
formatted_config[field] = _format_template(value)
231240

tests/components/template/test_helpers.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,270 @@ async def test_legacy_deprecation(
600600
assert "platform: template" not in issue.translation_placeholders["config"]
601601

602602

603+
@pytest.mark.parametrize(
604+
("domain", "config", "strings_to_check"),
605+
[
606+
(
607+
"light",
608+
{
609+
"light": {
610+
"platform": "template",
611+
"lights": {
612+
"garage_light_template": {
613+
"friendly_name": "Garage Light Template",
614+
"min_mireds_template": 153,
615+
"max_mireds_template": 500,
616+
"turn_on": [],
617+
"turn_off": [],
618+
"set_temperature": [],
619+
"set_hs": [],
620+
"set_level": [],
621+
}
622+
},
623+
},
624+
},
625+
[
626+
"turn_on: []",
627+
"turn_off: []",
628+
"set_temperature: []",
629+
"set_hs: []",
630+
"set_level: []",
631+
],
632+
),
633+
(
634+
"switch",
635+
{
636+
"switch": {
637+
"platform": "template",
638+
"switches": {
639+
"my_switch": {
640+
"friendly_name": "Switch Template",
641+
"turn_on": [],
642+
"turn_off": [],
643+
}
644+
},
645+
},
646+
},
647+
[
648+
"turn_on: []",
649+
"turn_off: []",
650+
],
651+
),
652+
(
653+
"light",
654+
{
655+
"light": [
656+
{
657+
"platform": "template",
658+
"lights": {
659+
"atrium_lichterkette": {
660+
"unique_id": "atrium_lichterkette",
661+
"friendly_name": "Atrium Lichterkette",
662+
"value_template": "{{ states('input_boolean.atrium_lichterkette_power') }}",
663+
"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 %}",
664+
"effect_list_template": "{{ state_attr('input_select.atrium_lichterkette_mode', 'options') }}",
665+
"effect_template": "'{{ states('input_select.atrium_lichterkette_mode')}}'",
666+
"turn_on": [
667+
{
668+
"service": "button.press",
669+
"target": {
670+
"entity_id": "button.esphome_web_28a814_lichterkette_on"
671+
},
672+
},
673+
{
674+
"service": "input_boolean.turn_on",
675+
"target": {
676+
"entity_id": "input_boolean.atrium_lichterkette_power"
677+
},
678+
},
679+
],
680+
"turn_off": [
681+
{
682+
"service": "button.press",
683+
"target": {
684+
"entity_id": "button.esphome_web_28a814_lichterkette_off"
685+
},
686+
},
687+
{
688+
"service": "input_boolean.turn_off",
689+
"target": {
690+
"entity_id": "input_boolean.atrium_lichterkette_power"
691+
},
692+
},
693+
],
694+
"set_level": [
695+
{
696+
"variables": {
697+
"scaled": "{{ (brightness / (255 / state_attr('input_number.atrium_lichterkette_brightness', 'max'))) | round | int }}",
698+
"diff": "{{ scaled | int - states('input_number.atrium_lichterkette_brightness') | int }}",
699+
"direction": "{{ 'dim' if diff | int < 0 else 'bright' }}",
700+
}
701+
},
702+
{
703+
"repeat": {
704+
"count": "{{ diff | int | abs }}",
705+
"sequence": [
706+
{
707+
"service": "button.press",
708+
"target": {
709+
"entity_id": "button.esphome_web_28a814_lichterkette_{{ direction }}"
710+
},
711+
},
712+
{"delay": {"milliseconds": 500}},
713+
],
714+
}
715+
},
716+
{
717+
"service": "input_number.set_value",
718+
"data": {
719+
"value": "{{ scaled }}",
720+
"entity_id": "input_number.atrium_lichterkette_brightness",
721+
},
722+
},
723+
],
724+
"set_effect": [
725+
{
726+
"service": "button.press",
727+
"target": {
728+
"entity_id": "button.esphome_web_28a814_lichterkette_{{ effect }}"
729+
},
730+
}
731+
],
732+
}
733+
},
734+
}
735+
]
736+
},
737+
[
738+
"scaled: ",
739+
"diff: ",
740+
"direction: ",
741+
],
742+
),
743+
(
744+
"cover",
745+
{
746+
"cover": [
747+
{
748+
"platform": "template",
749+
"covers": {
750+
"large_garage_door": {
751+
"device_class": "garage",
752+
"friendly_name": "Large Garage Door",
753+
"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",
754+
"open_cover": [
755+
{
756+
"condition": "state",
757+
"entity_id": "binary_sensor.large_garage_door",
758+
"state": "off",
759+
},
760+
{
761+
"action": "switch.turn_on",
762+
"target": {
763+
"entity_id": "switch.garage_door_relay_1"
764+
},
765+
},
766+
{
767+
"action": "timer.start",
768+
"entity_id": "timer.large_garage_opening_timer",
769+
},
770+
],
771+
"close_cover": [
772+
{
773+
"condition": "state",
774+
"entity_id": "binary_sensor.large_garage_door",
775+
"state": "on",
776+
},
777+
{
778+
"action": "switch.turn_on",
779+
"target": {
780+
"entity_id": "switch.garage_door_relay_1"
781+
},
782+
},
783+
{
784+
"action": "timer.start",
785+
"entity_id": "timer.large_garage_closing_timer",
786+
},
787+
],
788+
"stop_cover": [
789+
{
790+
"action": "switch.turn_on",
791+
"target": {
792+
"entity_id": "switch.garage_door_relay_1"
793+
},
794+
},
795+
{
796+
"action": "timer.cancel",
797+
"entity_id": "timer.large_garage_opening_timer",
798+
},
799+
{
800+
"action": "timer.cancel",
801+
"entity_id": "timer.large_garage_closing_timer",
802+
},
803+
],
804+
}
805+
},
806+
}
807+
]
808+
},
809+
["device_class: garage"],
810+
),
811+
(
812+
"binary_sensor",
813+
{
814+
"binary_sensor": {
815+
"platform": "template",
816+
"sensors": {
817+
"motion_sensor": {
818+
"friendly_name": "Motion Sensor",
819+
"device_class": "motion",
820+
"value_template": "{{ is_state('sensor.motion_detector', 'on') }}",
821+
}
822+
},
823+
},
824+
},
825+
["device_class: motion"],
826+
),
827+
(
828+
"sensor",
829+
{
830+
"sensor": {
831+
"platform": "template",
832+
"sensors": {
833+
"some_sensor": {
834+
"friendly_name": "Sensor",
835+
"device_class": "timestamp",
836+
"value_template": "{{ now().isoformat() }}",
837+
}
838+
},
839+
},
840+
},
841+
["device_class: timestamp"],
842+
),
843+
],
844+
)
845+
async def test_legacy_deprecation_with_unique_objects(
846+
hass: HomeAssistant,
847+
domain: str,
848+
config: dict,
849+
strings_to_check: list[str],
850+
issue_registry: ir.IssueRegistry,
851+
) -> None:
852+
"""Test legacy configuration raises issue and unique objects are properly converted to valid configurations."""
853+
854+
await async_setup_component(hass, domain, config)
855+
await hass.async_block_till_done()
856+
857+
assert len(issue_registry.issues) == 1
858+
issue = next(iter(issue_registry.issues.values()))
859+
860+
assert issue.domain == "template"
861+
assert issue.severity == ir.IssueSeverity.WARNING
862+
assert issue.translation_placeholders is not None
863+
for string in strings_to_check:
864+
assert string in issue.translation_placeholders["config"]
865+
866+
603867
@pytest.mark.parametrize(
604868
("domain", "config"),
605869
[

0 commit comments

Comments
 (0)