diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 39317d873f5..bc18a26368d 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -30,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import State, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, location as loc_helper +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util @@ -98,6 +99,11 @@ def extract_entities( variables: TemplateVarsType = None, ) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" + + report( + "called template.extract_entities. Please use event.async_track_template_result instead as it can accurately handle watching entities" + ) + if template is None or not is_template_string(template): return [] diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 74cd19a165d..9d8b763c275 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -20,7 +20,14 @@ from homeassistant.helpers import template import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem -from tests.async_mock import patch +from tests.async_mock import Mock, patch + + +@pytest.fixture() +def allow_extract_entities(): + """Allow extract entities.""" + with patch("homeassistant.helpers.template.report"): + yield def _set_up_units(hass): @@ -1758,7 +1765,7 @@ def test_closest_function_no_location_states(hass): ) -def test_extract_entities_none_exclude_stuff(hass): +def test_extract_entities_none_exclude_stuff(hass, allow_extract_entities): """Test extract entities function with none or exclude stuff.""" assert template.extract_entities(hass, None) == [] @@ -1780,7 +1787,7 @@ def test_extract_entities_none_exclude_stuff(hass): ) -def test_extract_entities_no_match_entities(hass): +def test_extract_entities_no_match_entities(hass, allow_extract_entities): """Test extract entities function with none entities stuff.""" assert ( template.extract_entities( @@ -1920,7 +1927,7 @@ async def test_async_render_to_info_in_conditional(hass): assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], []) -async def test_extract_entities_match_entities(hass): +async def test_extract_entities_match_entities(hass, allow_extract_entities): """Test extract entities function with entities stuff.""" assert ( template.extract_entities( @@ -2028,7 +2035,7 @@ states.sensor.pick_humidity.state ~ " %" ] -def test_extract_entities_with_variables(hass): +def test_extract_entities_with_variables(hass, allow_extract_entities): """Test extract entities function with variables and entities stuff.""" hass.states.async_set("input_boolean.switch", "on") assert ["input_boolean.switch"] == template.extract_entities( @@ -2063,7 +2070,7 @@ def test_extract_entities_with_variables(hass): ) -def test_extract_entities_domain_states_inner(hass): +def test_extract_entities_domain_states_inner(hass, allow_extract_entities): """Test extract entities function by domain.""" hass.states.async_set("light.switch", "on") hass.states.async_set("light.switch2", "on") @@ -2078,7 +2085,7 @@ def test_extract_entities_domain_states_inner(hass): ) == {"light.switch", "light.switch2", "light.switch3"} -def test_extract_entities_domain_states_outer(hass): +def test_extract_entities_domain_states_outer(hass, allow_extract_entities): """Test extract entities function by domain.""" hass.states.async_set("light.switch", "on") hass.states.async_set("light.switch2", "on") @@ -2093,7 +2100,7 @@ def test_extract_entities_domain_states_outer(hass): ) == {"light.switch", "light.switch2", "light.switch3"} -def test_extract_entities_domain_states_outer_with_group(hass): +def test_extract_entities_domain_states_outer_with_group(hass, allow_extract_entities): """Test extract entities function by domain.""" hass.states.async_set("light.switch", "on") hass.states.async_set("light.switch2", "on") @@ -2110,6 +2117,43 @@ def test_extract_entities_domain_states_outer_with_group(hass): ) == {"light.switch", "light.switch2", "light.switch3", "group.lights"} +def test_extract_entities_blocked_from_core_code(hass): + """Test extract entities is blocked from core code.""" + with pytest.raises(RuntimeError): + template.extract_entities( + hass, "{{ states.light }}", {}, + ) + + +def test_extract_entities_warns_and_logs_from_an_integration(hass, caplog): + """Test extract entities works from a custom_components with a log message.""" + + correct_frame = Mock( + filename="/config/custom_components/burncpu/light.py", + lineno="23", + line="self.light.is_on", + ) + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/dev/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + ], + ): + template.extract_entities( + hass, "{{ states.light }}", {}, + ) + + assert "custom_components/burncpu/light.py" in caplog.text + assert "23" in caplog.text + assert "self.light.is_on" in caplog.text + + def test_jinja_namespace(hass): """Test Jinja's namespace command can be used.""" test_template = template.Template(