1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-25 05:26:47 +00:00

Extract device template functions into a devices Jinja2 extension (#156619)

This commit is contained in:
Franck Nijhof
2025-11-15 00:23:38 +01:00
committed by GitHub
parent 56ab6b2512
commit aefdf412b0
5 changed files with 471 additions and 411 deletions

View File

@@ -1166,13 +1166,6 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]:
return list(found.values())
def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]:
"""Get entity ids for entities tied to a device."""
entity_reg = er.async_get(hass)
entries = er.async_entries_for_device(entity_reg, _device_id)
return [entry.entity_id for entry in entries]
def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]:
"""Get entity ids for entities tied to an integration/domain.
@@ -1214,65 +1207,6 @@ def config_entry_id(hass: HomeAssistant, entity_id: str) -> str | None:
return None
def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None:
"""Get a device ID from an entity ID or device name."""
entity_reg = er.async_get(hass)
entity = entity_reg.async_get(entity_id_or_device_name)
if entity is not None:
return entity.device_id
dev_reg = dr.async_get(hass)
return next(
(
device_id
for device_id, device in dev_reg.devices.items()
if (name := device.name_by_user or device.name)
and (str(entity_id_or_device_name) == name)
),
None,
)
def device_name(hass: HomeAssistant, lookup_value: str) -> str | None:
"""Get the device name from an device id, or entity id."""
device_reg = dr.async_get(hass)
if device := device_reg.async_get(lookup_value):
return device.name_by_user or device.name
ent_reg = er.async_get(hass)
# Import here, not at top-level to avoid circular import
from homeassistant.helpers import config_validation as cv # noqa: PLC0415
try:
cv.entity_id(lookup_value)
except vol.Invalid:
pass
else:
if entity := ent_reg.async_get(lookup_value):
if entity.device_id and (device := device_reg.async_get(entity.device_id)):
return device.name_by_user or device.name
return None
def device_attr(hass: HomeAssistant, device_or_entity_id: str, attr_name: str) -> Any:
"""Get the device specific attribute."""
device_reg = dr.async_get(hass)
if not isinstance(device_or_entity_id, str):
raise TemplateError("Must provide a device or entity ID")
device = None
if (
"." in device_or_entity_id
and (_device_id := device_id(hass, device_or_entity_id)) is not None
):
device = device_reg.async_get(_device_id)
elif "." not in device_or_entity_id:
device = device_reg.async_get(device_or_entity_id)
if device is None or not hasattr(device, attr_name):
return None
return getattr(device, attr_name)
def config_entry_attr(
hass: HomeAssistant, config_entry_id_: str, attr_name: str
) -> Any:
@@ -1291,13 +1225,6 @@ def config_entry_attr(
return getattr(config_entry, attr_name)
def is_device_attr(
hass: HomeAssistant, device_or_entity_id: str, attr_name: str, attr_value: Any
) -> bool:
"""Test if a device's attribute is a specific value."""
return bool(device_attr(hass, device_or_entity_id, attr_name) == attr_value)
def issues(hass: HomeAssistant) -> dict[tuple[str, str], dict[str, Any]]:
"""Return all open issues."""
current_issues = ir.async_get(hass).issues
@@ -2260,6 +2187,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"homeassistant.helpers.template.extensions.CollectionExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
@@ -2377,23 +2305,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["config_entry_id"] = hassfunction(config_entry_id)
self.filters["config_entry_id"] = self.globals["config_entry_id"]
# Device extensions
self.globals["device_name"] = hassfunction(device_name)
self.filters["device_name"] = self.globals["device_name"]
self.globals["device_attr"] = hassfunction(device_attr)
self.filters["device_attr"] = self.globals["device_attr"]
self.globals["device_entities"] = hassfunction(device_entities)
self.filters["device_entities"] = self.globals["device_entities"]
self.globals["is_device_attr"] = hassfunction(is_device_attr)
self.tests["is_device_attr"] = hassfunction(is_device_attr, pass_eval_context)
self.globals["device_id"] = hassfunction(device_id)
self.filters["device_id"] = self.globals["device_id"]
# Issue extensions
self.globals["issues"] = hassfunction(issues)
@@ -2415,12 +2326,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"area_id",
"area_name",
"closest",
"device_attr",
"device_id",
"distance",
"expand",
"has_value",
"is_device_attr",
"is_hidden_entity",
"is_state_attr",
"is_state",
@@ -2438,7 +2346,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"area_id",
"area_name",
"closest",
"device_id",
"expand",
"has_value",
]

View File

@@ -3,6 +3,7 @@
from .base64 import Base64Extension
from .collection import CollectionExtension
from .crypto import CryptoExtension
from .devices import DeviceExtension
from .floors import FloorExtension
from .labels import LabelExtension
from .math import MathExtension
@@ -13,6 +14,7 @@ __all__ = [
"Base64Extension",
"CollectionExtension",
"CryptoExtension",
"DeviceExtension",
"FloorExtension",
"LabelExtension",
"MathExtension",

View File

@@ -0,0 +1,139 @@
"""Device functions for Home Assistant templates."""
from __future__ import annotations
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from .base import BaseTemplateExtension, TemplateFunction
if TYPE_CHECKING:
from homeassistant.helpers.template import TemplateEnvironment
class DeviceExtension(BaseTemplateExtension):
"""Extension for device-related template functions."""
def __init__(self, environment: TemplateEnvironment) -> None:
"""Initialize the device extension."""
super().__init__(
environment,
functions=[
TemplateFunction(
"device_entities",
self.device_entities,
as_global=True,
as_filter=True,
requires_hass=True,
),
TemplateFunction(
"device_id",
self.device_id,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"device_name",
self.device_name,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"device_attr",
self.device_attr,
as_global=True,
as_filter=True,
requires_hass=True,
limited_ok=False,
),
TemplateFunction(
"is_device_attr",
self.is_device_attr,
as_global=True,
as_test=True,
requires_hass=True,
limited_ok=False,
),
],
)
def device_entities(self, _device_id: str) -> Iterable[str]:
"""Get entity ids for entities tied to a device."""
entity_reg = er.async_get(self.hass)
entries = er.async_entries_for_device(entity_reg, _device_id)
return [entry.entity_id for entry in entries]
def device_id(self, entity_id_or_device_name: str) -> str | None:
"""Get a device ID from an entity ID or device name."""
entity_reg = er.async_get(self.hass)
entity = entity_reg.async_get(entity_id_or_device_name)
if entity is not None:
return entity.device_id
dev_reg = dr.async_get(self.hass)
return next(
(
device_id
for device_id, device in dev_reg.devices.items()
if (name := device.name_by_user or device.name)
and (str(entity_id_or_device_name) == name)
),
None,
)
def device_name(self, lookup_value: str) -> str | None:
"""Get the device name from an device id, or entity id."""
device_reg = dr.async_get(self.hass)
if device := device_reg.async_get(lookup_value):
return device.name_by_user or device.name
ent_reg = er.async_get(self.hass)
try:
cv.entity_id(lookup_value)
except vol.Invalid:
pass
else:
if entity := ent_reg.async_get(lookup_value):
if entity.device_id and (
device := device_reg.async_get(entity.device_id)
):
return device.name_by_user or device.name
return None
def device_attr(self, device_or_entity_id: str, attr_name: str) -> Any:
"""Get the device specific attribute."""
device_reg = dr.async_get(self.hass)
if not isinstance(device_or_entity_id, str):
raise TemplateError("Must provide a device or entity ID")
device = None
if (
"." in device_or_entity_id
and (_device_id := self.device_id(device_or_entity_id)) is not None
):
device = device_reg.async_get(_device_id)
elif "." not in device_or_entity_id:
device = device_reg.async_get(device_or_entity_id)
if device is None or not hasattr(device, attr_name):
return None
return getattr(device, attr_name)
def is_device_attr(
self, device_or_entity_id: str, attr_name: str, attr_value: Any
) -> bool:
"""Test if a device's attribute is a specific value."""
return bool(self.device_attr(device_or_entity_id, attr_name) == attr_value)

View File

@@ -0,0 +1,329 @@
"""Test device template functions."""
from __future__ import annotations
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.template import TemplateError
from tests.common import MockConfigEntry
from tests.helpers.template.helpers import assert_result_info, render_to_info
async def test_device_entities(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_entities function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(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={(dr.CONNECTION_NETWORK_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 "
"| sort(attribute='entity_id') | 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 "
"| sort(attribute='entity_id') | 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 "
"| sort(attribute='entity_id') | 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
async def test_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_id function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="test",
name="test",
)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id
)
entity_entry_no_device = entity_registry.async_get_or_create(
"sensor", "test", "test_no_device", suggested_object_id="test"
)
info = render_to_info(hass, "{{ 'sensor.fail' | device_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 56 | device_id }}")
assert_result_info(info, None)
info = render_to_info(hass, "{{ 'not_a_real_entity_id' | device_id }}")
assert_result_info(info, None)
info = render_to_info(
hass, f"{{{{ device_id('{entity_entry_no_device.entity_id}') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.id)
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_id('test') }}")
assert_result_info(info, device_entry.id)
assert info.rate_limit is None
async def test_device_name(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_name function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing entity id
info = render_to_info(hass, "{{ device_name('sensor.fake') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing device id
info = render_to_info(hass, "{{ device_name('1234567890') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ device_name(56) }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test device with single entity
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
name="A light",
)
entity_entry = 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_name('{device_entry.id}') }}}}")
assert_result_info(info, device_entry.name)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.name)
assert info.rate_limit is None
# Test device after renaming
device_entry = device_registry.async_update_device(
device_entry.id,
name_by_user="My light",
)
info = render_to_info(hass, f"{{{{ device_name('{device_entry.id}') }}}}")
assert_result_info(info, device_entry.name_by_user)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.name_by_user)
assert info.rate_limit is None
async def test_device_attr(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_attr and is_device_attr functions."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing device ids (device_attr)
info = render_to_info(hass, "{{ device_attr('abc123', 'id') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_attr(56, 'id') }}")
with pytest.raises(TemplateError):
assert_result_info(info, None)
# Test non existing device ids (is_device_attr)
info = render_to_info(hass, "{{ is_device_attr('abc123', 'id', 'test') }}")
assert_result_info(info, False)
assert info.rate_limit is None
info = render_to_info(hass, "{{ is_device_attr(56, 'id', 'test') }}")
with pytest.raises(TemplateError):
assert_result_info(info, False)
# Test non existing entity id (device_attr)
info = render_to_info(hass, "{{ device_attr('entity.test', 'id') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing entity id (is_device_attr)
info = render_to_info(hass, "{{ is_device_attr('entity.test', 'id', 'test') }}")
assert_result_info(info, False)
assert info.rate_limit is None
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="test",
)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id
)
# Test non existent device attribute (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{device_entry.id}', 'invalid_attr') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existent device attribute (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'invalid_attr', 'test') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test None device attribute (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{device_entry.id}', 'manufacturer') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
# Test None device attribute mismatch (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', 'test') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test None device attribute match (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', None) }}}}"
)
assert_result_info(info, True)
assert info.rate_limit is None
# Test valid device attribute match (device_attr)
info = render_to_info(hass, f"{{{{ device_attr('{device_entry.id}', 'model') }}}}")
assert_result_info(info, "test")
assert info.rate_limit is None
# Test valid device attribute match (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{entity_entry.entity_id}', 'model') }}}}"
)
assert_result_info(info, "test")
assert info.rate_limit is None
# Test valid device attribute mismatch (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'fail') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test valid device attribute match (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'test') }}}}"
)
assert_result_info(info, True)
assert info.rate_limit is None
# Test filter syntax (device_attr)
info = render_to_info(
hass, f"{{{{ '{entity_entry.entity_id}' | device_attr('model') }}}}"
)
assert_result_info(info, "test")
assert info.rate_limit is None
# Test test syntax (is_device_attr)
info = render_to_info(
hass,
(
f"{{{{ ['{device_entry.id}'] | select('is_device_attr', 'model', 'test') "
"| list }}"
),
)
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None

View File

@@ -2392,91 +2392,6 @@ async def test_expand(hass: HomeAssistant) -> None:
)
async def test_device_entities(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_entities function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(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={(dr.CONNECTION_NETWORK_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 "
"| sort(attribute='entity_id') | 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 "
"| sort(attribute='entity_id') | 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 "
"| sort(attribute='entity_id') | 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
async def test_integration_entities(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
@@ -2569,238 +2484,6 @@ async def test_config_entry_id(
assert info.rate_limit is None
async def test_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_id function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="test",
name="test",
)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id
)
entity_entry_no_device = entity_registry.async_get_or_create(
"sensor", "test", "test_no_device", suggested_object_id="test"
)
info = render_to_info(hass, "{{ 'sensor.fail' | device_id }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ 56 | device_id }}")
assert_result_info(info, None)
info = render_to_info(hass, "{{ 'not_a_real_entity_id' | device_id }}")
assert_result_info(info, None)
info = render_to_info(
hass, f"{{{{ device_id('{entity_entry_no_device.entity_id}') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_id('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.id)
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_id('test') }}")
assert_result_info(info, device_entry.id)
assert info.rate_limit is None
async def test_device_name(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_name function."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing entity id
info = render_to_info(hass, "{{ device_name('sensor.fake') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing device id
info = render_to_info(hass, "{{ device_name('1234567890') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test wrong value type
info = render_to_info(hass, "{{ device_name(56) }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test device with single entity
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
name="A light",
)
entity_entry = 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_name('{device_entry.id}') }}}}")
assert_result_info(info, device_entry.name)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.name)
assert info.rate_limit is None
# Test device after renaming
device_entry = device_registry.async_update_device(
device_entry.id,
name_by_user="My light",
)
info = render_to_info(hass, f"{{{{ device_name('{device_entry.id}') }}}}")
assert_result_info(info, device_entry.name_by_user)
assert info.rate_limit is None
info = render_to_info(hass, f"{{{{ device_name('{entity_entry.entity_id}') }}}}")
assert_result_info(info, device_entry.name_by_user)
assert info.rate_limit is None
async def test_device_attr(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test device_attr and is_device_attr functions."""
config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
# Test non existing device ids (device_attr)
info = render_to_info(hass, "{{ device_attr('abc123', 'id') }}")
assert_result_info(info, None)
assert info.rate_limit is None
info = render_to_info(hass, "{{ device_attr(56, 'id') }}")
with pytest.raises(TemplateError):
assert_result_info(info, None)
# Test non existing device ids (is_device_attr)
info = render_to_info(hass, "{{ is_device_attr('abc123', 'id', 'test') }}")
assert_result_info(info, False)
assert info.rate_limit is None
info = render_to_info(hass, "{{ is_device_attr(56, 'id', 'test') }}")
with pytest.raises(TemplateError):
assert_result_info(info, False)
# Test non existing entity id (device_attr)
info = render_to_info(hass, "{{ device_attr('entity.test', 'id') }}")
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existing entity id (is_device_attr)
info = render_to_info(hass, "{{ is_device_attr('entity.test', 'id', 'test') }}")
assert_result_info(info, False)
assert info.rate_limit is None
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="test",
)
entity_entry = entity_registry.async_get_or_create(
"sensor", "test", "test", suggested_object_id="test", device_id=device_entry.id
)
# Test non existent device attribute (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{device_entry.id}', 'invalid_attr') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
# Test non existent device attribute (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'invalid_attr', 'test') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test None device attribute (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{device_entry.id}', 'manufacturer') }}}}"
)
assert_result_info(info, None)
assert info.rate_limit is None
# Test None device attribute mismatch (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', 'test') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test None device attribute match (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'manufacturer', None) }}}}"
)
assert_result_info(info, True)
assert info.rate_limit is None
# Test valid device attribute match (device_attr)
info = render_to_info(hass, f"{{{{ device_attr('{device_entry.id}', 'model') }}}}")
assert_result_info(info, "test")
assert info.rate_limit is None
# Test valid device attribute match (device_attr)
info = render_to_info(
hass, f"{{{{ device_attr('{entity_entry.entity_id}', 'model') }}}}"
)
assert_result_info(info, "test")
assert info.rate_limit is None
# Test valid device attribute mismatch (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'fail') }}}}"
)
assert_result_info(info, False)
assert info.rate_limit is None
# Test valid device attribute match (is_device_attr)
info = render_to_info(
hass, f"{{{{ is_device_attr('{device_entry.id}', 'model', 'test') }}}}"
)
assert_result_info(info, True)
assert info.rate_limit is None
# Test filter syntax (device_attr)
info = render_to_info(
hass, f"{{{{ '{entity_entry.entity_id}' | device_attr('model') }}}}"
)
assert_result_info(info, "test")
assert info.rate_limit is None
# Test test syntax (is_device_attr)
info = render_to_info(
hass,
(
f"{{{{ ['{device_entry.id}'] | select('is_device_attr', 'model', 'test') "
"| list }}"
),
)
assert_result_info(info, [device_entry.id])
assert info.rate_limit is None
async def test_config_entry_attr(hass: HomeAssistant) -> None:
"""Test config entry attr."""
info = {