diff --git a/homeassistant/helpers/template/__init__.py b/homeassistant/helpers/template/__init__.py index 818c39cc1e6..f45a4b55326 100644 --- a/homeassistant/helpers/template/__init__.py +++ b/homeassistant/helpers/template/__init__.py @@ -59,7 +59,6 @@ from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, - floor_registry as fr, issue_registry as ir, location as loc_helper, ) @@ -79,7 +78,7 @@ from .context import ( template_context_manager, template_cv, ) -from .helpers import raise_no_default +from .helpers import raise_no_default, resolve_area_id from .render_info import RenderInfo, render_info_cv if TYPE_CHECKING: @@ -1318,74 +1317,6 @@ def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | N return None -def floors(hass: HomeAssistant) -> Iterable[str | None]: - """Return all floors.""" - floor_registry = fr.async_get(hass) - return [floor.floor_id for floor in floor_registry.async_list_floors()] - - -def floor_id(hass: HomeAssistant, lookup_value: Any) -> str | None: - """Get the floor ID from a floor or area name, alias, device id, or entity id.""" - floor_registry = fr.async_get(hass) - lookup_str = str(lookup_value) - if floor := floor_registry.async_get_floor_by_name(lookup_str): - return floor.floor_id - floors_list = floor_registry.async_get_floors_by_alias(lookup_str) - if floors_list: - return floors_list[0].floor_id - - if aid := area_id(hass, lookup_value): - area_reg = ar.async_get(hass) - if area := area_reg.async_get_area(aid): - return area.floor_id - - return None - - -def floor_name(hass: HomeAssistant, lookup_value: str) -> str | None: - """Get the floor name from a floor id.""" - floor_registry = fr.async_get(hass) - if floor := floor_registry.async_get_floor(lookup_value): - return floor.name - - if aid := area_id(hass, lookup_value): - area_reg = ar.async_get(hass) - if ( - (area := area_reg.async_get_area(aid)) - and area.floor_id - and (floor := floor_registry.async_get_floor(area.floor_id)) - ): - return floor.name - - return None - - -def floor_areas(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: - """Return area IDs for a given floor ID or name.""" - _floor_id: str | None - # If floor_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 floor_name(hass, floor_id_or_name) is not None: - _floor_id = floor_id_or_name - else: - _floor_id = floor_id(hass, floor_id_or_name) - if _floor_id is None: - return [] - - area_reg = ar.async_get(hass) - entries = ar.async_entries_for_floor(area_reg, _floor_id) - return [entry.id for entry in entries if entry.id] - - -def floor_entities(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: - """Return entity_ids for a given floor ID or name.""" - return [ - entity_id - for area_id in floor_areas(hass, floor_id_or_name) - for entity_id in area_entities(hass, area_id) - ] - - def areas(hass: HomeAssistant) -> Iterable[str | None]: """Return all areas.""" return list(ar.async_get(hass).areas) @@ -1393,37 +1324,7 @@ def areas(hass: HomeAssistant) -> Iterable[str | None]: def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the area ID from an area name, alias, device id, or entity id.""" - area_reg = ar.async_get(hass) - lookup_str = str(lookup_value) - if area := area_reg.async_get_area_by_name(lookup_str): - return area.id - areas_list = area_reg.async_get_areas_by_alias(lookup_str) - if areas_list: - return areas_list[0].id - - ent_reg = er.async_get(hass) - dev_reg = dr.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, return that - if entity.area_id: - return entity.area_id - # If entity has a device ID, return the area ID for the device - if entity.device_id and (device := dev_reg.async_get(entity.device_id)): - return device.area_id - - # Check if this could be a device ID - if device := dev_reg.async_get(lookup_value): - return device.area_id - - return None + return resolve_area_id(hass, lookup_value) def _get_area_name(area_reg: ar.AreaRegistry, valid_area_id: str) -> str: @@ -2359,6 +2260,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "homeassistant.helpers.template.extensions.CollectionExtension" ) self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension") + self.add_extension("homeassistant.helpers.template.extensions.FloorExtension") self.add_extension("homeassistant.helpers.template.extensions.LabelExtension") self.add_extension("homeassistant.helpers.template.extensions.MathExtension") self.add_extension("homeassistant.helpers.template.extensions.RegexExtension") @@ -2462,23 +2364,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["area_devices"] = hassfunction(area_devices) self.filters["area_devices"] = self.globals["area_devices"] - # Floor extensions - - self.globals["floors"] = hassfunction(floors) - self.filters["floors"] = self.globals["floors"] - - self.globals["floor_id"] = hassfunction(floor_id) - self.filters["floor_id"] = self.globals["floor_id"] - - self.globals["floor_name"] = hassfunction(floor_name) - self.filters["floor_name"] = self.globals["floor_name"] - - self.globals["floor_areas"] = hassfunction(floor_areas) - self.filters["floor_areas"] = self.globals["floor_areas"] - - self.globals["floor_entities"] = hassfunction(floor_entities) - self.filters["floor_entities"] = self.globals["floor_entities"] - # Integration extensions self.globals["integration_entities"] = hassfunction(integration_entities) @@ -2534,8 +2419,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "device_id", "distance", "expand", - "floor_id", - "floor_name", "has_value", "is_device_attr", "is_hidden_entity", @@ -2557,8 +2440,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "closest", "device_id", "expand", - "floor_id", - "floor_name", "has_value", ] hass_tests = [ diff --git a/homeassistant/helpers/template/extensions/__init__.py b/homeassistant/helpers/template/extensions/__init__.py index b136bcd18b1..96cc11ccab1 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 .floors import FloorExtension from .labels import LabelExtension from .math import MathExtension from .regex import RegexExtension @@ -12,6 +13,7 @@ __all__ = [ "Base64Extension", "CollectionExtension", "CryptoExtension", + "FloorExtension", "LabelExtension", "MathExtension", "RegexExtension", diff --git a/homeassistant/helpers/template/extensions/floors.py b/homeassistant/helpers/template/extensions/floors.py new file mode 100644 index 00000000000..ef163e39b4d --- /dev/null +++ b/homeassistant/helpers/template/extensions/floors.py @@ -0,0 +1,157 @@ +"""Floor functions for Home Assistant templates.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any + +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, + floor_registry as fr, +) +from homeassistant.helpers.template.helpers import resolve_area_id + +from .base import BaseTemplateExtension, TemplateFunction + +if TYPE_CHECKING: + from homeassistant.helpers.template import TemplateEnvironment + + +class FloorExtension(BaseTemplateExtension): + """Extension for floor-related template functions.""" + + def __init__(self, environment: TemplateEnvironment) -> None: + """Initialize the floor extension.""" + super().__init__( + environment, + functions=[ + TemplateFunction( + "floors", + self.floors, + as_global=True, + requires_hass=True, + ), + TemplateFunction( + "floor_id", + self.floor_id, + as_global=True, + as_filter=True, + requires_hass=True, + limited_ok=False, + ), + TemplateFunction( + "floor_name", + self.floor_name, + as_global=True, + as_filter=True, + requires_hass=True, + limited_ok=False, + ), + TemplateFunction( + "floor_areas", + self.floor_areas, + as_global=True, + as_filter=True, + requires_hass=True, + ), + TemplateFunction( + "floor_entities", + self.floor_entities, + as_global=True, + as_filter=True, + requires_hass=True, + ), + ], + ) + + def floors(self) -> Iterable[str | None]: + """Return all floors.""" + floor_registry = fr.async_get(self.hass) + return [floor.floor_id for floor in floor_registry.async_list_floors()] + + def floor_id(self, lookup_value: Any) -> str | None: + """Get the floor ID from a floor or area name, alias, device id, or entity id.""" + floor_registry = fr.async_get(self.hass) + lookup_str = str(lookup_value) + + # Check if it's a floor name or alias + if floor := floor_registry.async_get_floor_by_name(lookup_str): + return floor.floor_id + floors_list = floor_registry.async_get_floors_by_alias(lookup_str) + if floors_list: + return floors_list[0].floor_id + + # Resolve to area ID and get floor from area + if aid := resolve_area_id(self.hass, lookup_value): + area_reg = ar.async_get(self.hass) + if area := area_reg.async_get_area(aid): + return area.floor_id + + return None + + def floor_name(self, lookup_value: str) -> str | None: + """Get the floor name from a floor id.""" + floor_registry = fr.async_get(self.hass) + + # Check if it's a floor ID + if floor := floor_registry.async_get_floor(lookup_value): + return floor.name + + # Resolve to area ID and get floor name from area's floor + if aid := resolve_area_id(self.hass, lookup_value): + area_reg = ar.async_get(self.hass) + if ( + (area := area_reg.async_get_area(aid)) + and area.floor_id + and (floor := floor_registry.async_get_floor(area.floor_id)) + ): + return floor.name + + return None + + def _floor_id_or_name(self, floor_id_or_name: str) -> str | None: + """Get the floor ID from a floor name or ID.""" + # If floor_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.floor_name(floor_id_or_name) is not None: + return floor_id_or_name + return self.floor_id(floor_id_or_name) + + def floor_areas(self, floor_id_or_name: str) -> Iterable[str]: + """Return area IDs for a given floor ID or name.""" + if (_floor_id := self._floor_id_or_name(floor_id_or_name)) is None: + return [] + + area_reg = ar.async_get(self.hass) + entries = ar.async_entries_for_floor(area_reg, _floor_id) + return [entry.id for entry in entries if entry.id] + + def floor_entities(self, floor_id_or_name: str) -> Iterable[str]: + """Return entity_ids for a given floor ID or name.""" + ent_reg = er.async_get(self.hass) + dev_reg = dr.async_get(self.hass) + entity_ids = [] + + for area_id in self.floor_areas(floor_id_or_name): + # Get entities directly assigned to the area + entity_ids.extend( + [ + entry.entity_id + for entry in er.async_entries_for_area(ent_reg, area_id) + ] + ) + + # Also 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 diff --git a/homeassistant/helpers/template/helpers.py b/homeassistant/helpers/template/helpers.py index 2e5942f3b74..71c95f77c47 100644 --- a/homeassistant/helpers/template/helpers.py +++ b/homeassistant/helpers/template/helpers.py @@ -2,10 +2,21 @@ from __future__ import annotations -from typing import Any, NoReturn +from typing import TYPE_CHECKING, Any, NoReturn + +import voluptuous as vol + +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from .context import template_cv +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + def raise_no_default(function: str, value: Any) -> NoReturn: """Raise ValueError when no default is specified for template functions.""" @@ -14,3 +25,47 @@ def raise_no_default(function: str, value: Any) -> NoReturn: f"Template error: {function} got invalid input '{value}' when {action} template" f" '{template}' but no default was specified" ) + + +def resolve_area_id(hass: HomeAssistant, lookup_value: Any) -> str | None: + """Resolve lookup value to an area ID. + + Accepts area name, area alias, device ID, or entity ID. + Returns the area ID or None if not found. + """ + area_reg = ar.async_get(hass) + dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + lookup_str = str(lookup_value) + + # Check if it's an area name + if area := area_reg.async_get_area_by_name(lookup_str): + return area.id + + # Check if it's an area alias + areas_list = area_reg.async_get_areas_by_alias(lookup_str) + if areas_list: + return areas_list[0].id + + # Import here, not at top-level to avoid circular import + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 + + # Check if it's an entity ID + 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, return that + if entity.area_id: + return entity.area_id + # If entity has a device ID, return the area ID for the device + if entity.device_id and (device := dev_reg.async_get(entity.device_id)): + return device.area_id + + # Check if it's a device ID + if device := dev_reg.async_get(lookup_value): + return device.area_id + + return None diff --git a/tests/helpers/template/extensions/test_floors.py b/tests/helpers/template/extensions/test_floors.py new file mode 100644 index 00000000000..97981de129d --- /dev/null +++ b/tests/helpers/template/extensions/test_floors.py @@ -0,0 +1,297 @@ +"""Test floor 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, + floor_registry as fr, +) + +from tests.common import MockConfigEntry +from tests.helpers.template.helpers import assert_result_info, render_to_info + + +async def test_floors( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, +) -> None: + """Test floors function.""" + + # Test no floors + info = render_to_info(hass, "{{ floors() }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test one floor + floor1 = floor_registry.async_create("First floor") + info = render_to_info(hass, "{{ floors() }}") + assert_result_info(info, [floor1.floor_id]) + assert info.rate_limit is None + + # Test multiple floors + floor2 = floor_registry.async_create("Second floor") + info = render_to_info(hass, "{{ floors() }}") + assert_result_info(info, [floor1.floor_id, floor2.floor_id]) + assert info.rate_limit is None + + +async def test_floor_id( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test floor_id function.""" + + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_id('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{value}' | floor_id }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) + + # Test wrong value type + info = render_to_info(hass, "{{ floor_id(42) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_id }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test with an actual floor + floor = floor_registry.async_create("First floor") + test("First floor", floor.floor_id) + + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and 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")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + 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 + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.floor_id) + test(device_entry.id, floor.floor_id) + test(entity_entry.entity_id, floor.floor_id) + + +async def test_floor_name( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test floor_name function.""" + + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_name('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{value}' | floor_name }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) + + # Test wrong value type + info = render_to_info(hass, "{{ floor_name(42) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_name }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test existing floor ID + floor = floor_registry.async_create("First floor") + test(floor.floor_id, floor.name) + + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and 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")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + 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 + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.name) + test(device_entry.id, floor.name) + test(entity_entry.entity_id, floor.name) + + +async def test_floor_areas( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, +) -> None: + """Test floor_areas function.""" + + # Test non existing floor ID + info = render_to_info(hass, "{{ floor_areas('skyring') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'skyring' | floor_areas }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ floor_areas(42) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_areas }}") + assert_result_info(info, []) + assert info.rate_limit is None + + floor = floor_registry.async_create("First floor") + area = area_registry.async_create("Living room") + area_registry.async_update(area.id, floor_id=floor.floor_id) + + # Get areas by floor ID + info = render_to_info(hass, f"{{{{ floor_areas('{floor.floor_id}') }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_areas }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + # Get areas by floor name + info = render_to_info(hass, f"{{{{ floor_areas('{floor.name}') }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{floor.name}' | floor_areas }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + +async def test_floor_entities( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test floor_entities function.""" + + # Test non existing floor ID + info = render_to_info(hass, "{{ floor_entities('skyring') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'skyring' | floor_entities }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ floor_entities(42) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_entities }}") + assert_result_info(info, []) + assert info.rate_limit is None + + floor = floor_registry.async_create("First floor") + area1 = area_registry.async_create("Living room") + area2 = area_registry.async_create("Dining room") + area_registry.async_update(area1.id, floor_id=floor.floor_id) + area_registry.async_update(area2.id, floor_id=floor.floor_id) + + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "living_room", + config_entry=config_entry, + ) + entity_registry.async_update_entity(entity_entry.entity_id, area_id=area1.id) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "dining_room", + config_entry=config_entry, + ) + entity_registry.async_update_entity(entity_entry.entity_id, area_id=area2.id) + + # Get entities by floor ID + expected = ["light.hue_living_room", "light.hue_dining_room"] + info = render_to_info(hass, f"{{{{ floor_entities('{floor.floor_id}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_entities }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Get entities by floor name + info = render_to_info(hass, f"{{{{ floor_entities('{floor.name}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{floor.name}' | floor_entities }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None diff --git a/tests/helpers/template/test_helpers.py b/tests/helpers/template/test_helpers.py index 64d1c5a9364..d166080aca1 100644 --- a/tests/helpers/template/test_helpers.py +++ b/tests/helpers/template/test_helpers.py @@ -2,7 +2,15 @@ import pytest -from homeassistant.helpers.template.helpers import raise_no_default +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) +from homeassistant.helpers.template.helpers import raise_no_default, resolve_area_id + +from tests.common import MockConfigEntry def test_raise_no_default() -> None: @@ -12,3 +20,87 @@ def test_raise_no_default() -> None: match="Template error: test got invalid input 'invalid' when rendering or compiling template '' but no default was specified", ): raise_no_default("test", "invalid") + + +async def test_resolve_area_id( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test resolve_area_id function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing entity id + assert resolve_area_id(hass, "sensor.fake") is None + + # Test non existing device id (hex value) + assert resolve_area_id(hass, "123abc") is None + + # Test non existing area name + assert resolve_area_id(hass, "fake area name") is None + + # Test wrong value type + assert resolve_area_id(hass, 56) 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, + ) + assert resolve_area_id(hass, device_entry.id) is None + assert resolve_area_id(hass, entity_entry.entity_id) is None + + # Test device ID, entity ID and area name as input with area name that looks like + # a device ID + 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 + ) + + assert resolve_area_id(hass, device_entry.id) == area_entry_hex.id + assert resolve_area_id(hass, entity_entry.entity_id) == area_entry_hex.id + assert resolve_area_id(hass, area_entry_hex.name) == area_entry_hex.id + + # 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 + ) + + assert resolve_area_id(hass, device_entry.id) == area_entry_entity_id.id + assert resolve_area_id(hass, entity_entry.entity_id) == area_entry_entity_id.id + assert resolve_area_id(hass, area_entry_entity_id.name) == area_entry_entity_id.id + + # 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 + ) + + assert resolve_area_id(hass, entity_entry.entity_id) == area_entry_entity_id.id + + # Test area alias + area_with_alias = area_registry.async_get_or_create("Living Room") + area_registry.async_update(area_with_alias.id, aliases={"lounge", "family room"}) + + assert resolve_area_id(hass, "Living Room") == area_with_alias.id + assert resolve_area_id(hass, "lounge") == area_with_alias.id + assert resolve_area_id(hass, "family room") == area_with_alias.id diff --git a/tests/helpers/template/test_init.py b/tests/helpers/template/test_init.py index 7548dd283e1..7da8f9f0abb 100644 --- a/tests/helpers/template/test_init.py +++ b/tests/helpers/template/test_init.py @@ -40,7 +40,6 @@ from homeassistant.helpers import ( device_registry as dr, entity, entity_registry as er, - floor_registry as fr, issue_registry as ir, template, translation, @@ -4419,289 +4418,6 @@ async def test_lru_increases_with_many_entities(hass: HomeAssistant) -> None: ) -async def test_floors( - hass: HomeAssistant, - floor_registry: fr.FloorRegistry, -) -> None: - """Test floors function.""" - - # Test no floors - info = render_to_info(hass, "{{ floors() }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test one floor - floor1 = floor_registry.async_create("First floor") - info = render_to_info(hass, "{{ floors() }}") - assert_result_info(info, [floor1.floor_id]) - assert info.rate_limit is None - - # Test multiple floors - floor2 = floor_registry.async_create("Second floor") - info = render_to_info(hass, "{{ floors() }}") - assert_result_info(info, [floor1.floor_id, floor2.floor_id]) - assert info.rate_limit is None - - -async def test_floor_id( - hass: HomeAssistant, - floor_registry: fr.FloorRegistry, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test floor_id function.""" - - def test(value: str, expected: str | None) -> None: - info = render_to_info(hass, f"{{{{ floor_id('{value}') }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{value}' | floor_id }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - # Test non existing floor name - test("Third floor", None) - - # Test wrong value type - info = render_to_info(hass, "{{ floor_id(42) }}") - assert_result_info(info, None) - assert info.rate_limit is None - - info = render_to_info(hass, "{{ 42 | floor_id }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test with an actual floor - floor = floor_registry.async_create("First floor") - test("First floor", floor.floor_id) - - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - area_entry_hex = area_registry.async_get_or_create("123abc") - - # Create area, device, entity and assign area to device and 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")}, - ) - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "5678", - config_entry=config_entry, - device_id=device_entry.id, - ) - 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 - ) - - test(area_entry_hex.id, None) - test(device_entry.id, None) - test(entity_entry.entity_id, None) - - # Add floor to area - area_entry_hex = area_registry.async_update( - area_entry_hex.id, floor_id=floor.floor_id - ) - - test(area_entry_hex.id, floor.floor_id) - test(device_entry.id, floor.floor_id) - test(entity_entry.entity_id, floor.floor_id) - - -async def test_floor_name( - hass: HomeAssistant, - floor_registry: fr.FloorRegistry, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test floor_name function.""" - - def test(value: str, expected: str | None) -> None: - info = render_to_info(hass, f"{{{{ floor_name('{value}') }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{value}' | floor_name }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - # Test non existing floor name - test("Third floor", None) - - # Test wrong value type - info = render_to_info(hass, "{{ floor_name(42) }}") - assert_result_info(info, None) - assert info.rate_limit is None - - info = render_to_info(hass, "{{ 42 | floor_name }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test existing floor ID - floor = floor_registry.async_create("First floor") - test(floor.floor_id, floor.name) - - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - area_entry_hex = area_registry.async_get_or_create("123abc") - - # Create area, device, entity and assign area to device and 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")}, - ) - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "5678", - config_entry=config_entry, - device_id=device_entry.id, - ) - 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 - ) - - test(area_entry_hex.id, None) - test(device_entry.id, None) - test(entity_entry.entity_id, None) - - # Add floor to area - area_entry_hex = area_registry.async_update( - area_entry_hex.id, floor_id=floor.floor_id - ) - - test(area_entry_hex.id, floor.name) - test(device_entry.id, floor.name) - test(entity_entry.entity_id, floor.name) - - -async def test_floor_areas( - hass: HomeAssistant, - floor_registry: fr.FloorRegistry, - area_registry: ar.AreaRegistry, -) -> None: - """Test floor_areas function.""" - - # Test non existing floor ID - info = render_to_info(hass, "{{ floor_areas('skyring') }}") - assert_result_info(info, []) - assert info.rate_limit is None - - info = render_to_info(hass, "{{ 'skyring' | floor_areas }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ floor_areas(42) }}") - assert_result_info(info, []) - assert info.rate_limit is None - - info = render_to_info(hass, "{{ 42 | floor_areas }}") - assert_result_info(info, []) - assert info.rate_limit is None - - floor = floor_registry.async_create("First floor") - area = area_registry.async_create("Living room") - area_registry.async_update(area.id, floor_id=floor.floor_id) - - # Get areas by floor ID - info = render_to_info(hass, f"{{{{ floor_areas('{floor.floor_id}') }}}}") - assert_result_info(info, [area.id]) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_areas }}}}") - assert_result_info(info, [area.id]) - assert info.rate_limit is None - - # Get entities by floor name - info = render_to_info(hass, f"{{{{ floor_areas('{floor.name}') }}}}") - assert_result_info(info, [area.id]) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{floor.name}' | floor_areas }}}}") - assert_result_info(info, [area.id]) - assert info.rate_limit is None - - -async def test_floor_entities( - hass: HomeAssistant, - floor_registry: fr.FloorRegistry, - area_registry: ar.AreaRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test floor_entities function.""" - - # Test non existing floor ID - info = render_to_info(hass, "{{ floor_entities('skyring') }}") - assert_result_info(info, []) - assert info.rate_limit is None - - info = render_to_info(hass, "{{ 'skyring' | floor_entities }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ floor_entities(42) }}") - assert_result_info(info, []) - assert info.rate_limit is None - - info = render_to_info(hass, "{{ 42 | floor_entities }}") - assert_result_info(info, []) - assert info.rate_limit is None - - floor = floor_registry.async_create("First floor") - area1 = area_registry.async_create("Living room") - area2 = area_registry.async_create("Dining room") - area_registry.async_update(area1.id, floor_id=floor.floor_id) - area_registry.async_update(area2.id, floor_id=floor.floor_id) - - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "living_room", - config_entry=config_entry, - ) - entity_registry.async_update_entity(entity_entry.entity_id, area_id=area1.id) - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "dining_room", - config_entry=config_entry, - ) - entity_registry.async_update_entity(entity_entry.entity_id, area_id=area2.id) - - # Get entities by floor ID - expected = ["light.hue_living_room", "light.hue_dining_room"] - info = render_to_info(hass, f"{{{{ floor_entities('{floor.floor_id}') }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_entities }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - # Get entities by floor name - info = render_to_info(hass, f"{{{{ floor_entities('{floor.name}') }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{floor.name}' | floor_entities }}}}") - assert_result_info(info, expected) - assert info.rate_limit is None - - async def test_template_thread_safety_checks(hass: HomeAssistant) -> None: """Test template thread safety checks.""" hass.states.async_set("sensor.test", "23")