Add template functions to get area_id and area_name (#54248)
* Add template function to get area_id * fix int bug * Prefer area name lookup * remove unnecessary checks * fix import * Add area_name function * change behavior to fail in ambiguous scenarios * Revert lotto winning exception checking * review comments * try except elsepull/55227/head
parent
d60f5e1721
commit
2f7a7b0309
|
@ -44,6 +44,7 @@ from homeassistant.core import (
|
|||
)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import (
|
||||
area_registry,
|
||||
device_registry,
|
||||
entity_registry,
|
||||
location as loc_helper,
|
||||
|
@ -949,6 +950,71 @@ def is_device_attr(
|
|||
return bool(device_attr(hass, device_or_entity_id, attr_name) == attr_value)
|
||||
|
||||
|
||||
def area_id(hass: HomeAssistant, lookup_value: str) -> str | None:
|
||||
"""Get the area ID from an area name, device id, or entity id."""
|
||||
area_reg = area_registry.async_get(hass)
|
||||
if area := area_reg.async_get_area_by_name(str(lookup_value)):
|
||||
return area.id
|
||||
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
# Import here, not at top-level to avoid circular import
|
||||
from homeassistant.helpers import ( # pylint: disable=import-outside-toplevel
|
||||
config_validation as cv,
|
||||
)
|
||||
|
||||
try:
|
||||
cv.entity_id(lookup_value)
|
||||
except vol.Invalid:
|
||||
pass
|
||||
else:
|
||||
if entity := ent_reg.async_get(lookup_value):
|
||||
return entity.area_id
|
||||
|
||||
# Check if this could be a device ID (hex string)
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
if device := dev_reg.async_get(lookup_value):
|
||||
return device.area_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_area_name(area_reg: area_registry.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 = area_registry.async_get(hass)
|
||||
area = area_reg.async_get_area(lookup_value)
|
||||
if area:
|
||||
return area.name
|
||||
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
# Import here, not at top-level to avoid circular import
|
||||
from homeassistant.helpers import ( # pylint: disable=import-outside-toplevel
|
||||
config_validation as cv,
|
||||
)
|
||||
|
||||
try:
|
||||
cv.entity_id(lookup_value)
|
||||
except vol.Invalid:
|
||||
pass
|
||||
else:
|
||||
if entity := ent_reg.async_get(lookup_value):
|
||||
if entity.area_id:
|
||||
return _get_area_name(area_reg, entity.area_id)
|
||||
return None
|
||||
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
if (device := dev_reg.async_get(lookup_value)) and device.area_id:
|
||||
return _get_area_name(area_reg, device.area_id)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def closest(hass, *args):
|
||||
"""Find closest entity.
|
||||
|
||||
|
@ -1532,6 +1598,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||
self.globals["device_id"] = hassfunction(device_id)
|
||||
self.filters["device_id"] = pass_context(self.globals["device_id"])
|
||||
|
||||
self.globals["area_id"] = hassfunction(area_id)
|
||||
self.filters["area_id"] = pass_context(self.globals["area_id"])
|
||||
|
||||
self.globals["area_name"] = hassfunction(area_name)
|
||||
self.filters["area_name"] = pass_context(self.globals["area_name"])
|
||||
|
||||
if limited:
|
||||
# Only device_entities is available to limited templates, mark other
|
||||
# functions and filters as unsupported.
|
||||
|
@ -1556,8 +1628,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||
"device_attr",
|
||||
"is_device_attr",
|
||||
"device_id",
|
||||
"area_id",
|
||||
"area_name",
|
||||
]
|
||||
hass_filters = ["closest", "expand", "device_id"]
|
||||
hass_filters = ["closest", "expand", "device_id", "area_id", "area_name"]
|
||||
for glob in hass_globals:
|
||||
self.globals[glob] = unsupported(glob)
|
||||
for filt in hass_filters:
|
||||
|
|
|
@ -23,7 +23,12 @@ from homeassistant.setup import async_setup_component
|
|||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import UnitSystem
|
||||
|
||||
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
mock_area_registry,
|
||||
mock_device_registry,
|
||||
mock_registry,
|
||||
)
|
||||
|
||||
|
||||
def _set_up_units(hass):
|
||||
|
@ -1513,7 +1518,7 @@ async def test_expand(hass):
|
|||
|
||||
|
||||
async def test_device_entities(hass):
|
||||
"""Test expand function."""
|
||||
"""Test device_entities function."""
|
||||
config_entry = MockConfigEntry(domain="light")
|
||||
device_registry = mock_device_registry(hass)
|
||||
entity_registry = mock_registry(hass)
|
||||
|
@ -1730,6 +1735,169 @@ async def test_device_attr(hass):
|
|||
assert info.rate_limit is None
|
||||
|
||||
|
||||
async def test_area_id(hass):
|
||||
"""Test area_id function."""
|
||||
config_entry = MockConfigEntry(domain="light")
|
||||
device_registry = mock_device_registry(hass)
|
||||
entity_registry = mock_registry(hass)
|
||||
area_registry = mock_area_registry(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
|
||||
|
||||
|
||||
async def test_area_name(hass):
|
||||
"""Test area_name function."""
|
||||
config_entry = MockConfigEntry(domain="light")
|
||||
device_registry = mock_device_registry(hass)
|
||||
entity_registry = mock_registry(hass)
|
||||
area_registry = mock_area_registry(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
|
||||
|
||||
|
||||
def test_closest_function_to_coord(hass):
|
||||
"""Test closest function to coord."""
|
||||
hass.states.async_set(
|
||||
|
|
Loading…
Reference in New Issue