diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d6c89f28e6e..cd286b15dc7 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -11,7 +11,7 @@ import math from operator import attrgetter import random import re -from typing import Any, Dict, Generator, Iterable, List, Optional, Type, Union +from typing import Any, Dict, Generator, Iterable, Optional, Type, Union from urllib.parse import urlencode as urllib_urlencode import weakref @@ -27,13 +27,11 @@ from homeassistant.const import ( ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, LENGTH_METERS, - MATCH_ALL, STATE_UNKNOWN, ) 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 @@ -184,59 +182,6 @@ RESULT_WRAPPERS: Dict[Type, Type] = { RESULT_WRAPPERS[tuple] = TupleWrapper -def extract_entities( - hass: HomeAssistantType, - template: Optional[str], - 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 [] - - if _RE_NONE_ENTITIES.search(template): - return MATCH_ALL - - extraction_final = [] - - for result in _RE_GET_ENTITIES.finditer(template): - if ( - result.group("entity_id") == "trigger.entity_id" - and variables - and "trigger" in variables - and "entity_id" in variables["trigger"] - ): - extraction_final.append(variables["trigger"]["entity_id"]) - elif result.group("entity_id"): - if result.group("func") == "expand": - for entity in expand(hass, result.group("entity_id")): - extraction_final.append(entity.entity_id) - - extraction_final.append(result.group("entity_id")) - elif result.group("domain_inner") or result.group("domain_outer"): - extraction_final.extend( - hass.states.async_entity_ids( - result.group("domain_inner") or result.group("domain_outer") - ) - ) - - if ( - variables - and result.group("variable") in variables - and isinstance(variables[result.group("variable")], str) - and valid_entity_id(variables[result.group("variable")]) - ): - extraction_final.append(variables[result.group("variable")]) - - if extraction_final: - return list(set(extraction_final)) - return MATCH_ALL - - def _true(arg: Any) -> bool: return True @@ -370,15 +315,6 @@ class Template: except jinja2.TemplateError as err: raise TemplateError(err) from err - def extract_entities( - self, variables: TemplateVarsType = None - ) -> Union[str, List[str]]: - """Extract all entities for state_changed listener.""" - if self.is_static: - return [] - - return extract_entities(self.hass, self.template, variables) - def render( self, variables: TemplateVarsType = None, diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index e3eafda52f1..fe2f23c0033 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -13,7 +13,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, LENGTH_METERS, MASS_GRAMS, - MATCH_ALL, PRESSURE_PA, TEMP_CELSIUS, VOLUME_LITERS, @@ -24,14 +23,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem -from tests.async_mock import Mock, patch - - -@pytest.fixture() -def allow_extract_entities(): - """Allow extract entities.""" - with patch("homeassistant.helpers.template.report"): - yield +from tests.async_mock import patch def _set_up_units(hass): @@ -1870,48 +1862,6 @@ def test_closest_function_no_location_states(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) == [] - - assert template.extract_entities(hass, "mdi:water") == [] - - assert ( - template.extract_entities( - hass, - "{{ closest(states.zone.far_away, states.test_domain.xxx).entity_id }}", - ) - == MATCH_ALL - ) - - assert ( - template.extract_entities( - hass, '{{ distance("123", states.test_object_2.user) }}' - ) - == MATCH_ALL - ) - - -def test_extract_entities_no_match_entities(hass, allow_extract_entities): - """Test extract entities function with none entities stuff.""" - assert ( - template.extract_entities( - hass, "{{ value_json.tst | timestamp_custom('%Y' True) }}" - ) - == MATCH_ALL - ) - - info = render_to_info( - hass, - """ -{% for state in states.sensor %} -{{ state.entity_id }}={{ state.state }},d -{% endfor %} - """, - ) - assert_result_info(info, "", domains=["sensor"]) - - def test_generate_filter_iterators(hass): """Test extract entities function with none entities stuff.""" info = render_to_info( @@ -2030,252 +1980,6 @@ 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, allow_extract_entities): - """Test extract entities function with entities stuff.""" - assert ( - template.extract_entities( - hass, - """ -{% if is_state('device_tracker.phone_1', 'home') %} -Ha, Hercules is home! -{% else %} -Hercules is at {{ states('device_tracker.phone_1') }}. -{% endif %} - """, - ) - == ["device_tracker.phone_1"] - ) - - assert ( - template.extract_entities( - hass, - """ -{{ as_timestamp(states.binary_sensor.garage_door.last_changed) }} - """, - ) - == ["binary_sensor.garage_door"] - ) - - assert ( - template.extract_entities( - hass, - """ -{{ states("binary_sensor.garage_door") }} - """, - ) - == ["binary_sensor.garage_door"] - ) - - hass.states.async_set("device_tracker.phone_2", "not_home", {"battery": 20}) - - assert ( - template.extract_entities( - hass, - """ -{{ is_state_attr('device_tracker.phone_2', 'battery', 40) }} - """, - ) - == ["device_tracker.phone_2"] - ) - - assert sorted(["device_tracker.phone_1", "device_tracker.phone_2"]) == sorted( - template.extract_entities( - hass, - """ -{% if is_state('device_tracker.phone_1', 'home') %} -Ha, Hercules is home! -{% elif states.device_tracker.phone_2.attributes.battery < 40 %} -Hercules you power goes done!. -{% endif %} - """, - ) - ) - - assert sorted(["sensor.pick_humidity", "sensor.pick_temperature"]) == sorted( - template.extract_entities( - hass, - """ -{{ -states.sensor.pick_temperature.state ~ "°C (" ~ -states.sensor.pick_humidity.state ~ " %" -}} - """, - ) - ) - - assert sorted( - ["sensor.luftfeuchtigkeit_mean", "input_number.luftfeuchtigkeit"] - ) == sorted( - template.extract_entities( - hass, - "{% if (states('sensor.luftfeuchtigkeit_mean') | int)" - " > (states('input_number.luftfeuchtigkeit') | int +1.5)" - " %}true{% endif %}", - ) - ) - - assert await async_setup_component(hass, "group", {}) - await hass.async_block_till_done() - await group.Group.async_create_group(hass, "empty group", []) - - assert ["group.empty_group"] == template.extract_entities( - hass, "{{ expand('group.empty_group') | list | length }}" - ) - - hass.states.async_set("test_domain.object", "exists") - await group.Group.async_create_group(hass, "expand group", ["test_domain.object"]) - - assert sorted(["group.expand_group", "test_domain.object"]) == sorted( - template.extract_entities( - hass, "{{ expand('group.expand_group') | list | length }}" - ) - ) - assert ["test_domain.entity"] == template.Template( - '{{ is_state("test_domain.entity", "on") }}', hass - ).extract_entities() - - # No expand, extract finds the group - assert template.extract_entities(hass, "{{ states('group.empty_group') }}") == [ - "group.empty_group" - ] - - -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( - hass, "{{ is_state('input_boolean.switch', 'off') }}", {} - ) - - assert ["input_boolean.switch"] == template.extract_entities( - hass, - "{{ is_state(trigger.entity_id, 'off') }}", - {"trigger": {"entity_id": "input_boolean.switch"}}, - ) - - assert MATCH_ALL == template.extract_entities( - hass, "{{ is_state(data, 'off') }}", {"data": "no_state"} - ) - - assert ["input_boolean.switch"] == template.extract_entities( - hass, "{{ is_state(data, 'off') }}", {"data": "input_boolean.switch"} - ) - - assert ["input_boolean.switch"] == template.extract_entities( - hass, - "{{ is_state(trigger.entity_id, 'off') }}", - {"trigger": {"entity_id": "input_boolean.switch"}}, - ) - - hass.states.async_set("media_player.livingroom", "off") - assert {"media_player.livingroom"} == extract_entities( - hass, - "{{ is_state('media_player.' ~ where , 'playing') }}", - {"where": "livingroom"}, - ) - - -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") - hass.states.async_set("light.switch3", "off") - - assert ( - set( - template.extract_entities( - hass, - "{{ states['light'] | selectattr('state','eq','on') | list | count > 0 }}", - {}, - ) - ) - == {"light.switch", "light.switch2", "light.switch3"} - ) - - -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") - hass.states.async_set("light.switch3", "off") - - assert ( - set( - template.extract_entities( - hass, - "{{ states.light | selectattr('state','eq','off') | list | count > 0 }}", - {}, - ) - ) - == {"light.switch", "light.switch2", "light.switch3"} - ) - - -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") - hass.states.async_set("light.switch3", "off") - hass.states.async_set("switch.pool_light", "off") - hass.states.async_set("group.lights", "off", {"entity_id": ["switch.pool_light"]}) - - assert ( - set( - template.extract_entities( - hass, - "{{ states.light | selectattr('entity_id', 'in', state_attr('group.lights', 'entity_id')) }}", - {}, - ) - ) - == {"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(