Extract floor template functions into a floors Jinja2 extension (#156589)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Franck Nijhof
2025-11-14 18:06:55 +01:00
committed by GitHub
parent a670286b45
commit 25fbcbc68c
7 changed files with 608 additions and 408 deletions

View File

@@ -59,7 +59,6 @@ from homeassistant.helpers import (
area_registry as ar, area_registry as ar,
device_registry as dr, device_registry as dr,
entity_registry as er, entity_registry as er,
floor_registry as fr,
issue_registry as ir, issue_registry as ir,
location as loc_helper, location as loc_helper,
) )
@@ -79,7 +78,7 @@ from .context import (
template_context_manager, template_context_manager,
template_cv, 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 from .render_info import RenderInfo, render_info_cv
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -1318,74 +1317,6 @@ def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | N
return None 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]: def areas(hass: HomeAssistant) -> Iterable[str | None]:
"""Return all areas.""" """Return all areas."""
return list(ar.async_get(hass).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: def area_id(hass: HomeAssistant, lookup_value: str) -> str | None:
"""Get the area ID from an area name, alias, device id, or entity id.""" """Get the area ID from an area name, alias, device id, or entity id."""
area_reg = ar.async_get(hass) return resolve_area_id(hass, lookup_value)
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
def _get_area_name(area_reg: ar.AreaRegistry, valid_area_id: str) -> str: 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" "homeassistant.helpers.template.extensions.CollectionExtension"
) )
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension") 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.LabelExtension")
self.add_extension("homeassistant.helpers.template.extensions.MathExtension") self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension") self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
@@ -2462,23 +2364,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["area_devices"] = hassfunction(area_devices) self.globals["area_devices"] = hassfunction(area_devices)
self.filters["area_devices"] = self.globals["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 # Integration extensions
self.globals["integration_entities"] = hassfunction(integration_entities) self.globals["integration_entities"] = hassfunction(integration_entities)
@@ -2534,8 +2419,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"device_id", "device_id",
"distance", "distance",
"expand", "expand",
"floor_id",
"floor_name",
"has_value", "has_value",
"is_device_attr", "is_device_attr",
"is_hidden_entity", "is_hidden_entity",
@@ -2557,8 +2440,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"closest", "closest",
"device_id", "device_id",
"expand", "expand",
"floor_id",
"floor_name",
"has_value", "has_value",
] ]
hass_tests = [ hass_tests = [

View File

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

View File

@@ -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

View File

@@ -2,10 +2,21 @@
from __future__ import annotations 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 from .context import template_cv
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
def raise_no_default(function: str, value: Any) -> NoReturn: def raise_no_default(function: str, value: Any) -> NoReturn:
"""Raise ValueError when no default is specified for template functions.""" """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 error: {function} got invalid input '{value}' when {action} template"
f" '{template}' but no default was specified" 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

View File

@@ -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

View File

@@ -2,7 +2,15 @@
import pytest 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: 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", match="Template error: test got invalid input 'invalid' when rendering or compiling template '' but no default was specified",
): ):
raise_no_default("test", "invalid") 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

View File

@@ -40,7 +40,6 @@ from homeassistant.helpers import (
device_registry as dr, device_registry as dr,
entity, entity,
entity_registry as er, entity_registry as er,
floor_registry as fr,
issue_registry as ir, issue_registry as ir,
template, template,
translation, 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: async def test_template_thread_safety_checks(hass: HomeAssistant) -> None:
"""Test template thread safety checks.""" """Test template thread safety checks."""
hass.states.async_set("sensor.test", "23") hass.states.async_set("sensor.test", "23")