mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 19:25:12 +02:00
Remove legacy template extract_entities (#42601)
This commit is contained in:
@@ -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,
|
||||
|
@@ -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(
|
||||
|
Reference in New Issue
Block a user