diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f6c88f16688..6cfae98a2ac 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1038,6 +1038,52 @@ def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: 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 = entity_registry.async_get(hass) + entity_ids = [ + entry.entity_id + for entry in entity_registry.async_entries_for_area(ent_reg, _area_id) + ] + dev_reg = device_registry.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 device_registry.async_entries_for_area(dev_reg, _area_id) + for entity in entity_registry.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 = device_registry.async_get(hass) + entries = device_registry.async_entries_for_area(dev_reg, _area_id) + return [entry.id for entry in entries] + + def closest(hass, *args): """Find closest entity. @@ -1783,6 +1829,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["area_name"] = hassfunction(area_name) self.filters["area_name"] = pass_context(self.globals["area_name"]) + self.globals["area_entities"] = hassfunction(area_entities) + self.filters["area_entities"] = pass_context(self.globals["area_entities"]) + + self.globals["area_devices"] = hassfunction(area_devices) + self.filters["area_devices"] = pass_context(self.globals["area_devices"]) + if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 66199e2a94c..305f8a6717b 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2143,6 +2143,91 @@ async def test_area_name(hass): assert info.rate_limit is None +async def test_area_entities(hass): + """Test area_entities function.""" + config_entry = MockConfigEntry(domain="light") + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + area_registry = mock_area_registry(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_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + 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): + """Test area_devices function.""" + config_entry = MockConfigEntry(domain="light") + device_registry = mock_device_registry(hass) + area_registry = mock_area_registry(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): """Test closest function to coord.""" hass.states.async_set(