From 2d44b667a432b7e9d172bf87ea92c14e7a9ca555 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Sat, 3 Oct 2026 07:00:01 -0400 Subject: [PATCH] Ensure template results are converted to string before validating against a list of strings. (#184123) --- homeassistant/components/template/climate.py | 16 ++++- homeassistant/components/template/light.py | 6 +- .../components/template/validators.py | 4 ++ tests/components/template/test_validators.py | 63 +++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/climate.py b/homeassistant/components/template/climate.py index 082555df37d6..bbe982eb93af 100644 --- a/homeassistant/components/template/climate.py +++ b/homeassistant/components/template/climate.py @@ -508,7 +508,13 @@ class AbstractTemplateClimate(AbstractTemplateEntity, ClimateEntity, RestoreEnti self.setup_template( CONF_FAN_MODE, "_attr_fan_mode", - tcv.item_in_list(self, CONF_FAN_MODE, "_attr_fan_modes", CONF_FAN_MODES), + tcv.item_in_list( + self, + CONF_FAN_MODE, + "_attr_fan_modes", + CONF_FAN_MODES, + stringify_result=True, + ), ) self.add_assumed_attribute("_attr_fan_mode", CONF_FAN_MODE, SET_FAN_MODE_ACTION) @@ -521,7 +527,11 @@ class AbstractTemplateClimate(AbstractTemplateEntity, ClimateEntity, RestoreEnti CONF_SWING_MODE, "_attr_swing_mode", tcv.item_in_list( - self, CONF_SWING_MODE, "_attr_swing_modes", CONF_SWING_MODES + self, + CONF_SWING_MODE, + "_attr_swing_modes", + CONF_SWING_MODES, + stringify_result=True, ), ) self.add_assumed_attribute( @@ -541,6 +551,7 @@ class AbstractTemplateClimate(AbstractTemplateEntity, ClimateEntity, RestoreEnti CONF_SWING_HORIZONTAL_MODE, "_attr_swing_horizontal_modes", CONF_SWING_HORIZONTAL_MODES, + stringify_result=True, ), ) self.add_assumed_attribute( @@ -562,6 +573,7 @@ class AbstractTemplateClimate(AbstractTemplateEntity, ClimateEntity, RestoreEnti CONF_PRESET_MODE, "_attr_preset_modes", CONF_PRESET_MODES, + stringify_result=True, ), ) self.add_assumed_attribute( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index f20bf37d8233..a5fc0b26a413 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -450,7 +450,11 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity, RestoreEntity): CONF_EFFECT, "_attr_effect", tcv.item_in_list( - self, "_attr_effect", "_attr_effect_list", CONF_EFFECT_LIST + self, + "_attr_effect", + "_attr_effect_list", + CONF_EFFECT_LIST, + stringify_result=True, ), ) diff --git a/homeassistant/components/template/validators.py b/homeassistant/components/template/validators.py index 7f946e54abb3..7f94ef603f5b 100644 --- a/homeassistant/components/template/validators.py +++ b/homeassistant/components/template/validators.py @@ -350,6 +350,7 @@ def item_in_list[T]( attribute: str, items: list[Any] | str | None, items_attribute: str | None = None, + stringify_result: bool = False, **kwargs: Any, ) -> Callable[[Any], Any | None]: """Assert the result of the template is an item inside a list. @@ -381,6 +382,9 @@ def item_in_list[T]( return None + if stringify_result: + result = str(result) + if result not in _items: log_validation_result_error( entity, diff --git a/tests/components/template/test_validators.py b/tests/components/template/test_validators.py index 018ebdcdc3a9..03b1ba61820e 100644 --- a/tests/components/template/test_validators.py +++ b/tests/components/template/test_validators.py @@ -893,6 +893,69 @@ async def test_item_in_list( check_for_error(value, expected, caplog.text, error.format(value)) +@pytest.mark.parametrize( + ("config", "error"), + [ + ( + {"default_entity_id": "test.test"}, + "Received invalid test state: {} for entity test.test, expected 1, 2, 3, 4", + ), + ( + {}, + "Received invalid state: {} for entity Test, expected 1, 2, 3, 4", + ), + ], +) +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + (1, "1"), + (2, "2"), + (3, "3"), + (4, "4"), + *expect_none( + None, + "1.0", + "2.0", + "3.0", + "4.0", + "BEER", + "IS", + "good", + "al;dfj", + "unknown", + "unavailable", + "tru", # codespell:ignore tru + 83242.2342, + True, + False, + {}, + {"junk": "stuff"}, + {"junk"}, + ), + ], +) +async def test_string_item_in_list( + hass: HomeAssistant, + config: dict, + error: str, + value: Any, + expected: bool | None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test result is in a list.""" + entity = create_test_entity(hass, config) + value = cv.item_in_list( + entity, "state", ["1", "2", "3", "4"], stringify_result=True + )(value) + assert value == expected + check_for_error(value, expected, caplog.text, error.format(value)) + + @pytest.mark.parametrize( ("config", "error"), [