diff --git a/homeassistant/helpers/template/__init__.py b/homeassistant/helpers/template/__init__.py index f45a4b55326..7b61f56c85e 100644 --- a/homeassistant/helpers/template/__init__.py +++ b/homeassistant/helpers/template/__init__.py @@ -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", ] diff --git a/homeassistant/helpers/template/extensions/__init__.py b/homeassistant/helpers/template/extensions/__init__.py index 96cc11ccab1..02be2c1f359 100644 --- a/homeassistant/helpers/template/extensions/__init__.py +++ b/homeassistant/helpers/template/extensions/__init__.py @@ -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", diff --git a/homeassistant/helpers/template/extensions/devices.py b/homeassistant/helpers/template/extensions/devices.py new file mode 100644 index 00000000000..aeef013f18a --- /dev/null +++ b/homeassistant/helpers/template/extensions/devices.py @@ -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) diff --git a/tests/helpers/template/extensions/test_devices.py b/tests/helpers/template/extensions/test_devices.py new file mode 100644 index 00000000000..e67e5255b37 --- /dev/null +++ b/tests/helpers/template/extensions/test_devices.py @@ -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 diff --git a/tests/helpers/template/test_init.py b/tests/helpers/template/test_init.py index 7da8f9f0abb..faf1481faff 100644 --- a/tests/helpers/template/test_init.py +++ b/tests/helpers/template/test_init.py @@ -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 = {