mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 14:57:09 +00:00
Add device_entities template function/filter (#46406)
This commit is contained in:
parent
2f3c2f5f4d
commit
54cf954353
@ -33,7 +33,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import State, callback, split_entity_id, valid_entity_id
|
from homeassistant.core import State, callback, split_entity_id, valid_entity_id
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import location as loc_helper
|
from homeassistant.helpers import entity_registry, location as loc_helper
|
||||||
from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType
|
from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util import convert, dt as dt_util, location as loc_util
|
from homeassistant.util import convert, dt as dt_util, location as loc_util
|
||||||
@ -48,6 +48,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|||||||
|
|
||||||
_RENDER_INFO = "template.render_info"
|
_RENDER_INFO = "template.render_info"
|
||||||
_ENVIRONMENT = "template.environment"
|
_ENVIRONMENT = "template.environment"
|
||||||
|
_ENVIRONMENT_LIMITED = "template.environment_limited"
|
||||||
|
|
||||||
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#")
|
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#")
|
||||||
# Match "simple" ints and floats. -1.0, 1, +5, 5.0
|
# Match "simple" ints and floats. -1.0, 1, +5, 5.0
|
||||||
@ -300,11 +301,12 @@ class Template:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _env(self) -> TemplateEnvironment:
|
def _env(self) -> TemplateEnvironment:
|
||||||
if self.hass is None or self._limited:
|
if self.hass is None:
|
||||||
return _NO_HASS_ENV
|
return _NO_HASS_ENV
|
||||||
ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT)
|
wanted_env = _ENVIRONMENT_LIMITED if self._limited else _ENVIRONMENT
|
||||||
|
ret: Optional[TemplateEnvironment] = self.hass.data.get(wanted_env)
|
||||||
if ret is None:
|
if ret is None:
|
||||||
ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) # type: ignore[no-untyped-call]
|
ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited) # type: ignore[no-untyped-call]
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def ensure_valid(self) -> None:
|
def ensure_valid(self) -> None:
|
||||||
@ -867,6 +869,13 @@ def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
|
|||||||
return sorted(found.values(), key=lambda a: a.entity_id)
|
return sorted(found.values(), key=lambda a: a.entity_id)
|
||||||
|
|
||||||
|
|
||||||
|
def device_entities(hass: HomeAssistantType, device_id: str) -> Iterable[str]:
|
||||||
|
"""Get entity ids for entities tied to a device."""
|
||||||
|
entity_reg = entity_registry.async_get(hass)
|
||||||
|
entries = entity_registry.async_entries_for_device(entity_reg, device_id)
|
||||||
|
return [entry.entity_id for entry in entries]
|
||||||
|
|
||||||
|
|
||||||
def closest(hass, *args):
|
def closest(hass, *args):
|
||||||
"""Find closest entity.
|
"""Find closest entity.
|
||||||
|
|
||||||
@ -1311,7 +1320,7 @@ def urlencode(value):
|
|||||||
class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||||
"""The Home Assistant template environment."""
|
"""The Home Assistant template environment."""
|
||||||
|
|
||||||
def __init__(self, hass):
|
def __init__(self, hass, limited=False):
|
||||||
"""Initialise template environment."""
|
"""Initialise template environment."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -1368,7 +1377,27 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||||||
self.globals["strptime"] = strptime
|
self.globals["strptime"] = strptime
|
||||||
self.globals["urlencode"] = urlencode
|
self.globals["urlencode"] = urlencode
|
||||||
if hass is None:
|
if hass is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# We mark these as a context functions to ensure they get
|
||||||
|
# evaluated fresh with every execution, rather than executed
|
||||||
|
# at compile time and the value stored. The context itself
|
||||||
|
# can be discarded, we only need to get at the hass object.
|
||||||
|
def hassfunction(func):
|
||||||
|
"""Wrap function that depend on hass."""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(hass, *args[1:], **kwargs)
|
||||||
|
|
||||||
|
return contextfunction(wrapper)
|
||||||
|
|
||||||
|
self.globals["device_entities"] = hassfunction(device_entities)
|
||||||
|
self.filters["device_entities"] = contextfilter(self.globals["device_entities"])
|
||||||
|
|
||||||
|
if limited:
|
||||||
|
# Only device_entities is available to limited templates, mark other
|
||||||
|
# functions and filters as unsupported.
|
||||||
def unsupported(name):
|
def unsupported(name):
|
||||||
def warn_unsupported(*args, **kwargs):
|
def warn_unsupported(*args, **kwargs):
|
||||||
raise TemplateError(
|
raise TemplateError(
|
||||||
@ -1395,19 +1424,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||||||
self.filters[filt] = unsupported(filt)
|
self.filters[filt] = unsupported(filt)
|
||||||
return
|
return
|
||||||
|
|
||||||
# We mark these as a context functions to ensure they get
|
|
||||||
# evaluated fresh with every execution, rather than executed
|
|
||||||
# at compile time and the value stored. The context itself
|
|
||||||
# can be discarded, we only need to get at the hass object.
|
|
||||||
def hassfunction(func):
|
|
||||||
"""Wrap function that depend on hass."""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(hass, *args[1:], **kwargs)
|
|
||||||
|
|
||||||
return contextfunction(wrapper)
|
|
||||||
|
|
||||||
self.globals["expand"] = hassfunction(expand)
|
self.globals["expand"] = hassfunction(expand)
|
||||||
self.filters["expand"] = contextfilter(self.globals["expand"])
|
self.filters["expand"] = contextfilter(self.globals["expand"])
|
||||||
self.globals["closest"] = hassfunction(closest)
|
self.globals["closest"] = hassfunction(closest)
|
||||||
|
@ -24,6 +24,8 @@ from homeassistant.setup import async_setup_component
|
|||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.unit_system import UnitSystem
|
from homeassistant.util.unit_system import UnitSystem
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
|
||||||
|
|
||||||
|
|
||||||
def _set_up_units(hass):
|
def _set_up_units(hass):
|
||||||
"""Set up the tests."""
|
"""Set up the tests."""
|
||||||
@ -1470,6 +1472,79 @@ async def test_expand(hass):
|
|||||||
assert info.rate_limit is None
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_entities(hass):
|
||||||
|
"""Test expand function."""
|
||||||
|
config_entry = MockConfigEntry(domain="light")
|
||||||
|
device_registry = mock_device_registry(hass)
|
||||||
|
entity_registry = mock_registry(hass)
|
||||||
|
|
||||||
|
# Test non existing device ids
|
||||||
|
info = render_to_info(hass, "{{ device_entities('abc123') }}")
|
||||||
|
assert_result_info(info, [])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
info = render_to_info(hass, "{{ device_entities(56) }}")
|
||||||
|
assert_result_info(info, [])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
# Test device without entities
|
||||||
|
device_entry = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={("mac", "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
|
||||||
|
assert_result_info(info, [])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
# Test device with single entity, which has no state
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"5678",
|
||||||
|
config_entry=config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
|
||||||
|
assert_result_info(info, ["light.hue_5678"], [])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}",
|
||||||
|
)
|
||||||
|
assert_result_info(info, "", ["light.hue_5678"])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
# Test device with single entity, with state
|
||||||
|
hass.states.async_set("light.hue_5678", "happy")
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}",
|
||||||
|
)
|
||||||
|
assert_result_info(info, "light.hue_5678", ["light.hue_5678"])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
# Test device with multiple entities, which have a state
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"ABCD",
|
||||||
|
config_entry=config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
hass.states.async_set("light.hue_abcd", "camper")
|
||||||
|
info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}")
|
||||||
|
assert_result_info(info, ["light.hue_5678", "light.hue_abcd"], [])
|
||||||
|
assert info.rate_limit is None
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}",
|
||||||
|
)
|
||||||
|
assert_result_info(
|
||||||
|
info, "light.hue_5678, light.hue_abcd", ["light.hue_5678", "light.hue_abcd"]
|
||||||
|
)
|
||||||
|
assert info.rate_limit is None
|
||||||
|
|
||||||
|
|
||||||
def test_closest_function_to_coord(hass):
|
def test_closest_function_to_coord(hass):
|
||||||
"""Test closest function to coord."""
|
"""Test closest function to coord."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user