From e5b2d44e8ebe90390d29b4171598673e6344d196 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 21 Nov 2025 17:56:16 +0100 Subject: [PATCH] Extract area template functions into an areas Jinja2 extension (#156629) --- homeassistant/helpers/template/__init__.py | 118 +------ .../helpers/template/extensions/__init__.py | 2 + .../helpers/template/extensions/areas.py | 159 +++++++++ .../helpers/template/extensions/test_areas.py | 313 ++++++++++++++++++ tests/helpers/template/test_init.py | 302 ----------------- 5 files changed, 476 insertions(+), 418 deletions(-) create mode 100644 homeassistant/helpers/template/extensions/areas.py create mode 100644 tests/helpers/template/extensions/test_areas.py diff --git a/homeassistant/helpers/template/__init__.py b/homeassistant/helpers/template/__init__.py index 7b61f56c85e..a0fca2ed5cc 100644 --- a/homeassistant/helpers/template/__init__.py +++ b/homeassistant/helpers/template/__init__.py @@ -56,8 +56,6 @@ from homeassistant.core import ( ) from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( - area_registry as ar, - device_registry as dr, entity_registry as er, issue_registry as ir, location as loc_helper, @@ -78,7 +76,7 @@ from .context import ( template_context_manager, template_cv, ) -from .helpers import raise_no_default, resolve_area_id +from .helpers import raise_no_default from .render_info import RenderInfo, render_info_cv if TYPE_CHECKING: @@ -1244,103 +1242,6 @@ def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | N return None -def areas(hass: HomeAssistant) -> Iterable[str | None]: - """Return all areas.""" - return list(ar.async_get(hass).areas) - - -def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: - """Get the area ID from an area name, alias, device id, or entity id.""" - return resolve_area_id(hass, lookup_value) - - -def _get_area_name(area_reg: ar.AreaRegistry, valid_area_id: str) -> str: - """Get area name from valid area ID.""" - area = area_reg.async_get_area(valid_area_id) - assert area - return area.name - - -def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: - """Get the area name from an area id, device id, or entity id.""" - area_reg = ar.async_get(hass) - if area := area_reg.async_get_area(lookup_value): - return area.name - - dev_reg = dr.async_get(hass) - 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 has an area ID, get the area name for that - if entity.area_id: - return _get_area_name(area_reg, entity.area_id) - # If entity has a device ID and the device exists with an area ID, get the - # area name for that - if ( - entity.device_id - and (device := dev_reg.async_get(entity.device_id)) - and device.area_id - ): - return _get_area_name(area_reg, device.area_id) - - if (device := dev_reg.async_get(lookup_value)) and device.area_id: - return _get_area_name(area_reg, device.area_id) - - return None - - -def area_entities(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: - """Return entities for a given area ID or name.""" - _area_id: str | None - # if area_name returns a value, we know the input was an ID, otherwise we - # assume it's a name, and if it's neither, we return early - if area_name(hass, area_id_or_name) is None: - _area_id = area_id(hass, area_id_or_name) - else: - _area_id = area_id_or_name - if _area_id is None: - return [] - ent_reg = er.async_get(hass) - entity_ids = [ - entry.entity_id for entry in er.async_entries_for_area(ent_reg, _area_id) - ] - dev_reg = dr.async_get(hass) - # We also need to add entities tied to a device in the area that don't themselves - # have an area specified since they inherit the area from the device. - entity_ids.extend( - [ - entity.entity_id - for device in dr.async_entries_for_area(dev_reg, _area_id) - for entity in er.async_entries_for_device(ent_reg, device.id) - if entity.area_id is None - ] - ) - return entity_ids - - -def area_devices(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: - """Return device IDs for a given area ID or name.""" - _area_id: str | None - # if area_name returns a value, we know the input was an ID, otherwise we - # assume it's a name, and if it's neither, we return early - if area_name(hass, area_id_or_name) is not None: - _area_id = area_id_or_name - else: - _area_id = area_id(hass, area_id_or_name) - if _area_id is None: - return [] - dev_reg = dr.async_get(hass) - entries = dr.async_entries_for_area(dev_reg, _area_id) - return [entry.id for entry in entries] - - def closest(hass: HomeAssistant, *args: Any) -> State | None: """Find closest entity. @@ -2182,6 +2083,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): ] = weakref.WeakValueDictionary() self.add_extension("jinja2.ext.loopcontrols") self.add_extension("jinja2.ext.do") + self.add_extension("homeassistant.helpers.template.extensions.AreaExtension") self.add_extension("homeassistant.helpers.template.extensions.Base64Extension") self.add_extension( "homeassistant.helpers.template.extensions.CollectionExtension" @@ -2276,22 +2178,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return jinja_context(wrapper) - # Area extensions - - self.globals["areas"] = hassfunction(areas) - - self.globals["area_id"] = hassfunction(area_id) - self.filters["area_id"] = self.globals["area_id"] - - self.globals["area_name"] = hassfunction(area_name) - self.filters["area_name"] = self.globals["area_name"] - - self.globals["area_entities"] = hassfunction(area_entities) - self.filters["area_entities"] = self.globals["area_entities"] - - self.globals["area_devices"] = hassfunction(area_devices) - self.filters["area_devices"] = self.globals["area_devices"] - # Integration extensions self.globals["integration_entities"] = hassfunction(integration_entities) diff --git a/homeassistant/helpers/template/extensions/__init__.py b/homeassistant/helpers/template/extensions/__init__.py index 02be2c1f359..9fdd8232c2a 100644 --- a/homeassistant/helpers/template/extensions/__init__.py +++ b/homeassistant/helpers/template/extensions/__init__.py @@ -1,5 +1,6 @@ """Home Assistant template extensions.""" +from .areas import AreaExtension from .base64 import Base64Extension from .collection import CollectionExtension from .crypto import CryptoExtension @@ -11,6 +12,7 @@ from .regex import RegexExtension from .string import StringExtension __all__ = [ + "AreaExtension", "Base64Extension", "CollectionExtension", "CryptoExtension", diff --git a/homeassistant/helpers/template/extensions/areas.py b/homeassistant/helpers/template/extensions/areas.py new file mode 100644 index 00000000000..1640243bb10 --- /dev/null +++ b/homeassistant/helpers/template/extensions/areas.py @@ -0,0 +1,159 @@ +"""Area functions for Home Assistant templates.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING + +import voluptuous as vol + +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) +from homeassistant.helpers.template.helpers import resolve_area_id + +from .base import BaseTemplateExtension, TemplateFunction + +if TYPE_CHECKING: + from homeassistant.helpers.template import TemplateEnvironment + + +class AreaExtension(BaseTemplateExtension): + """Extension for area-related template functions.""" + + def __init__(self, environment: TemplateEnvironment) -> None: + """Initialize the area extension.""" + super().__init__( + environment, + functions=[ + TemplateFunction( + "areas", + self.areas, + as_global=True, + requires_hass=True, + ), + TemplateFunction( + "area_id", + self.area_id, + as_global=True, + as_filter=True, + requires_hass=True, + limited_ok=False, + ), + TemplateFunction( + "area_name", + self.area_name, + as_global=True, + as_filter=True, + requires_hass=True, + limited_ok=False, + ), + TemplateFunction( + "area_entities", + self.area_entities, + as_global=True, + as_filter=True, + requires_hass=True, + ), + TemplateFunction( + "area_devices", + self.area_devices, + as_global=True, + as_filter=True, + requires_hass=True, + ), + ], + ) + + def areas(self) -> Iterable[str | None]: + """Return all areas.""" + return list(ar.async_get(self.hass).areas) + + def area_id(self, lookup_value: str) -> str | None: + """Get the area ID from an area name, alias, device id, or entity id.""" + return resolve_area_id(self.hass, lookup_value) + + def _get_area_name(self, area_reg: ar.AreaRegistry, valid_area_id: str) -> str: + """Get area name from valid area ID.""" + area = area_reg.async_get_area(valid_area_id) + assert area + return area.name + + def area_name(self, lookup_value: str) -> str | None: + """Get the area name from an area id, device id, or entity id.""" + area_reg = ar.async_get(self.hass) + if area := area_reg.async_get_area(lookup_value): + return area.name + + dev_reg = dr.async_get(self.hass) + ent_reg = er.async_get(self.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 has an area ID, get the area name for that + if entity.area_id: + return self._get_area_name(area_reg, entity.area_id) + # If entity has a device ID and the device exists with an area ID, get the + # area name for that + if ( + entity.device_id + and (device := dev_reg.async_get(entity.device_id)) + and device.area_id + ): + return self._get_area_name(area_reg, device.area_id) + + if (device := dev_reg.async_get(lookup_value)) and device.area_id: + return self._get_area_name(area_reg, device.area_id) + + return None + + def area_entities(self, area_id_or_name: str) -> Iterable[str]: + """Return entities for a given area ID or name.""" + _area_id: str | None + # if area_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if self.area_name(area_id_or_name) is None: + _area_id = self.area_id(area_id_or_name) + else: + _area_id = area_id_or_name + if _area_id is None: + return [] + ent_reg = er.async_get(self.hass) + entity_ids = [ + entry.entity_id for entry in er.async_entries_for_area(ent_reg, _area_id) + ] + dev_reg = dr.async_get(self.hass) + # We also need to add entities tied to a device in the area that don't themselves + # have an area specified since they inherit the area from the device. + entity_ids.extend( + [ + entity.entity_id + for device in dr.async_entries_for_area(dev_reg, _area_id) + for entity in er.async_entries_for_device(ent_reg, device.id) + if entity.area_id is None + ] + ) + return entity_ids + + def area_devices(self, area_id_or_name: str) -> Iterable[str]: + """Return device IDs for a given area ID or name.""" + _area_id: str | None + # if area_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if self.area_name(area_id_or_name) is not None: + _area_id = area_id_or_name + else: + _area_id = self.area_id(area_id_or_name) + if _area_id is None: + return [] + dev_reg = dr.async_get(self.hass) + entries = dr.async_entries_for_area(dev_reg, _area_id) + return [entry.id for entry in entries] diff --git a/tests/helpers/template/extensions/test_areas.py b/tests/helpers/template/extensions/test_areas.py new file mode 100644 index 00000000000..d0ee66c1ce9 --- /dev/null +++ b/tests/helpers/template/extensions/test_areas.py @@ -0,0 +1,313 @@ +"""Test area template functions.""" + +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) + +from tests.common import MockConfigEntry +from tests.helpers.template.helpers import assert_result_info, render_to_info + + +async def test_areas(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None: + """Test areas function.""" + # Test no areas + info = render_to_info(hass, "{{ areas() }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test one area + area1 = area_registry.async_get_or_create("area1") + info = render_to_info(hass, "{{ areas() }}") + assert_result_info(info, [area1.id]) + assert info.rate_limit is None + + # Test multiple areas + area2 = area_registry.async_get_or_create("area2") + info = render_to_info(hass, "{{ areas() }}") + assert_result_info(info, [area1.id, area2.id]) + assert info.rate_limit is None + + +async def test_area_id( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test area_id function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing entity id + info = render_to_info(hass, "{{ area_id('sensor.fake') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing device id (hex value) + info = render_to_info(hass, "{{ area_id('123abc') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing area name + info = render_to_info(hass, "{{ area_id('fake area name') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_id(56) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + area_registry.async_get_or_create("sensor.fake") + + # Test device with single entity, which has no area + 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")}, + ) + 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"{{{{ area_id('{device_entry.id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test device ID, entity ID and area name as input with area name that looks like + # a device ID. Try a filter too + area_entry_hex = area_registry.async_get_or_create("123abc") + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_id }}}}") + assert_result_info(info, area_entry_hex.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry_hex.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{area_entry_hex.name}') }}}}") + assert_result_info(info, area_entry_hex.id) + assert info.rate_limit is None + + # Test device ID, entity ID and area name as input with area name that looks like an + # entity ID + area_entry_entity_id = area_registry.async_get_or_create("sensor.fake") + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_entity_id.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_entity_id.id + ) + + info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{area_entry_entity_id.name}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + # Make sure that when entity doesn't have an area but its device does, that's what + # gets returned + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_entity_id.id + ) + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + +async def test_area_name( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test area_name function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing entity id + info = render_to_info(hass, "{{ area_name('sensor.fake') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing device id (hex value) + info = render_to_info(hass, "{{ area_name('123abc') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing area id + info = render_to_info(hass, "{{ area_name('1234567890') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_name(56) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test device with single entity, which has no area + 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")}, + ) + 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"{{{{ area_name('{device_entry.id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test device ID, entity ID and area id as input. Try a filter too + area_entry = area_registry.async_get_or_create("123abc") + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry.id + ) + + info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_name }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_name('{area_entry.id}') }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + # Make sure that when entity doesn't have an area but its device does, that's what + # gets returned + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=None + ) + + info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + +async def test_area_entities( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test area_entities function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing device id + info = render_to_info(hass, "{{ area_entities('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_entities(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + area_entry = area_registry.async_get_or_create("sensor.fake") + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + ) + entity_registry.async_update_entity(entity_entry.entity_id, area_id=area_entry.id) + + info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + # Test for entities that inherit area from device + device_entry = device_registry.async_get_or_create( + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + config_entry_id=config_entry.entry_id, + suggested_area="sensor.fake", + ) + entity_registry.async_get_or_create( + "light", + "hue_light", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") + assert_result_info(info, ["light.hue_5678", "light.hue_light_5678"]) + assert info.rate_limit is None + + +async def test_area_devices( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test area_devices function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing device id + info = render_to_info(hass, "{{ area_devices('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_devices(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + area_entry = area_registry.async_get_or_create("sensor.fake") + 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")}, + suggested_area=area_entry.name, + ) + + info = render_to_info(hass, f"{{{{ area_devices('{area_entry.id}') }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_devices }}}}") + 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 faf1481faff..48317703d3f 100644 --- a/tests/helpers/template/test_init.py +++ b/tests/helpers/template/test_init.py @@ -36,8 +36,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( - area_registry as ar, - device_registry as dr, entity, entity_registry as er, issue_registry as ir, @@ -2636,306 +2634,6 @@ async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> N assert info.rate_limit is None -async def test_areas(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None: - """Test areas function.""" - # Test no areas - info = render_to_info(hass, "{{ areas() }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test one area - area1 = area_registry.async_get_or_create("area1") - info = render_to_info(hass, "{{ areas() }}") - assert_result_info(info, [area1.id]) - assert info.rate_limit is None - - # Test multiple areas - area2 = area_registry.async_get_or_create("area2") - info = render_to_info(hass, "{{ areas() }}") - assert_result_info(info, [area1.id, area2.id]) - assert info.rate_limit is None - - -async def test_area_id( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area_id function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing entity id - info = render_to_info(hass, "{{ area_id('sensor.fake') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing device id (hex value) - info = render_to_info(hass, "{{ area_id('123abc') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing area name - info = render_to_info(hass, "{{ area_id('fake area name') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_id(56) }}") - assert_result_info(info, None) - assert info.rate_limit is None - - area_entry_entity_id = area_registry.async_get_or_create("sensor.fake") - - # Test device with single entity, which has no area - 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")}, - ) - 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"{{{{ area_id('{device_entry.id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test device ID, entity ID and area name as input with area name that looks like - # a device ID. Try a filter too - area_entry_hex = area_registry.async_get_or_create("123abc") - device_entry = device_registry.async_update_device( - device_entry.id, area_id=area_entry_hex.id - ) - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry_hex.id - ) - - info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_id }}}}") - assert_result_info(info, area_entry_hex.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry_hex.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{area_entry_hex.name}') }}}}") - assert_result_info(info, area_entry_hex.id) - assert info.rate_limit is None - - # Test device ID, entity ID and area name as input with area name that looks like an - # entity ID - area_entry_entity_id = area_registry.async_get_or_create("sensor.fake") - device_entry = device_registry.async_update_device( - device_entry.id, area_id=area_entry_entity_id.id - ) - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry_entity_id.id - ) - - info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{area_entry_entity_id.name}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - # Make sure that when entity doesn't have an area but its device does, that's what - # gets returned - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry_entity_id.id - ) - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - -async def test_area_name( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area_name function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing entity id - info = render_to_info(hass, "{{ area_name('sensor.fake') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing device id (hex value) - info = render_to_info(hass, "{{ area_name('123abc') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing area id - info = render_to_info(hass, "{{ area_name('1234567890') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_name(56) }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test device with single entity, which has no area - 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")}, - ) - 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"{{{{ area_name('{device_entry.id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test device ID, entity ID and area id as input. Try a filter too - area_entry = area_registry.async_get_or_create("123abc") - device_entry = device_registry.async_update_device( - device_entry.id, area_id=area_entry.id - ) - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry.id - ) - - info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_name }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_name('{area_entry.id}') }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - # Make sure that when entity doesn't have an area but its device does, that's what - # gets returned - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=None - ) - - info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - -async def test_area_entities( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area_entities function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing device id - info = render_to_info(hass, "{{ area_entities('deadbeef') }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_entities(56) }}") - assert_result_info(info, []) - assert info.rate_limit is None - - area_entry = area_registry.async_get_or_create("sensor.fake") - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "5678", - config_entry=config_entry, - ) - entity_registry.async_update_entity(entity_entry.entity_id, area_id=area_entry.id) - - info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}") - assert_result_info(info, ["light.hue_5678"]) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") - assert_result_info(info, ["light.hue_5678"]) - assert info.rate_limit is None - - # Test for entities that inherit area from device - device_entry = device_registry.async_get_or_create( - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - config_entry_id=config_entry.entry_id, - suggested_area="sensor.fake", - ) - entity_registry.async_get_or_create( - "light", - "hue_light", - "5678", - config_entry=config_entry, - device_id=device_entry.id, - ) - - info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") - assert_result_info(info, ["light.hue_5678", "light.hue_light_5678"]) - assert info.rate_limit is None - - -async def test_area_devices( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, -) -> None: - """Test area_devices function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing device id - info = render_to_info(hass, "{{ area_devices('deadbeef') }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_devices(56) }}") - assert_result_info(info, []) - assert info.rate_limit is None - - area_entry = area_registry.async_get_or_create("sensor.fake") - 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")}, - suggested_area=area_entry.name, - ) - - info = render_to_info(hass, f"{{{{ area_devices('{area_entry.id}') }}}}") - assert_result_info(info, [device_entry.id]) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_devices }}}}") - assert_result_info(info, [device_entry.id]) - assert info.rate_limit is None - - def test_closest_function_to_coord(hass: HomeAssistant) -> None: """Test closest function to coord.""" hass.states.async_set(