Add name/area/floor lists

This commit is contained in:
Michael Hansen
2025-11-11 14:17:26 -06:00
parent 2f2aee93c7
commit d1bd35b20b
2 changed files with 119 additions and 13 deletions
@@ -14,6 +14,7 @@ from home_assistant_intents import get_intents, get_languages
import voluptuous as vol
import yaml
from homeassistant.components.homeassistant.exposed_entities import async_should_expose
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MATCH_ALL
from homeassistant.core import (
@@ -24,7 +25,13 @@ from homeassistant.core import (
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
entity_registry as er,
floor_registry as fr,
intent,
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.typing import ConfigType
@@ -277,7 +284,7 @@ async def async_get_intents(
hass: HomeAssistant, language: str, source: IntentSource = IntentSource.ALL
) -> Intents:
"""Load intents for a language."""
intents_dict: dict[str, Any] = {"intents": {}}
intents_dict: dict[str, Any] = {"intents": {}, "lists": {}}
agent = get_agent_manager(hass).default_agent
assert agent is not None
@@ -310,15 +317,7 @@ async def async_get_intents(
if (source & IntentSource.CONVERSATION_CONFIG) and agent.config_intents:
# From conversation YAML config
merge_dict(
intents_dict,
{
"intents": {
intent_name: {"data": [{"sentences": sentences}]}
for intent_name, sentences in agent.config_intents.items()
}
},
)
merge_dict(intents_dict, agent.config_intents)
if (source & IntentSource.SENTENCE_TRIGGERS) and agent.trigger_details:
# From automations
@@ -336,6 +335,46 @@ async def async_get_intents(
},
)
# Add exposed entities + areas/floors
entity_tuples = []
entity_registry = er.async_get(hass)
for state in hass.states.async_all():
if not async_should_expose(hass, DOMAIN, state.entity_id):
continue
context: dict[str, Any] = {"domain": state.domain}
entity_tuples.append((state.name, context))
if not (entity := entity_registry.async_get(state.entity_id)):
continue
entity_tuples.extend(
(alias, context)
for alias in filter(None, (a.strip() for a in entity.aliases))
)
intents_dict["lists"]["name"] = {
"values": [
{"in": name, "out": name, "context": context}
for name, context in entity_tuples
]
}
# Areas/floors
area_registry = ar.async_get(hass)
floor_registry = fr.async_get(hass)
for list_name, entries in (
("area", area_registry.async_list_areas()),
("floor", floor_registry.async_list_floors()),
):
entry_names = set()
for entry in entries:
entry_names.add(entry.name)
entry_names.update(filter(None, (a.strip() for a in entry.aliases)))
if entry_names:
intents_dict["lists"][list_name] = {"values": list(entry_names)}
# Parse and post-process
intents_dict["language"] = language
intents = Intents.from_dict(intents_dict)
+69 -2
View File
@@ -3,6 +3,8 @@
from http import HTTPStatus
from unittest.mock import patch
from hassil.expression import TextChunk
from hassil.intents import TextSlotList
import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol
@@ -17,10 +19,17 @@ from homeassistant.components.conversation import (
default_agent,
)
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent
from homeassistant.helpers import (
area_registry as ar,
entity_registry as er,
floor_registry as fr,
intent,
)
from homeassistant.setup import async_setup_component
from . import MockAgent
@@ -437,7 +446,7 @@ async def test_async_get_intents_bad_language(hass: HomeAssistant) -> None:
async def test_async_get_intents_language_region(hass: HomeAssistant) -> None:
"""Test getting available intents with a regtional language code."""
"""Test getting available intents with a regional language code."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
@@ -446,3 +455,61 @@ async def test_async_get_intents_language_region(hass: HomeAssistant) -> None:
# built-in and custom sentences for "en" are included
for intent_name in ("HassTurnOn", "OrderBeer"):
assert intent_name in intents.intents
async def test_async_get_intents_lists(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
area_registry: ar.AreaRegistry,
floor_registry: fr.FloorRegistry,
) -> None:
"""Test that intents contain name/area/floor lists."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
# Create a light in an area on a floor
floor_ground = floor_registry.async_create("ground")
area_kitchen = area_registry.async_get_or_create("kitchen_id")
area_kitchen = area_registry.async_update(
area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id
)
kitchen_light = entity_registry.async_get_or_create("light", "demo", "light1234")
kitchen_light = entity_registry.async_update_entity(
kitchen_light.entity_id, area_id=area_kitchen.id
)
hass.states.async_set(
kitchen_light.entity_id, "off", {ATTR_FRIENDLY_NAME: "demo light"}
)
# Add a second unexposed light
unexposed_light = entity_registry.async_get_or_create("light", "demo", "light5678")
unexposed_light = entity_registry.async_update_entity(
unexposed_light.entity_id, area_id=area_kitchen.id
)
hass.states.async_set(
unexposed_light.entity_id, "off", {ATTR_FRIENDLY_NAME: "unexposed light"}
)
async_expose_entity(hass, conversation.DOMAIN, unexposed_light.entity_id, False)
# name/area/floor lists should reflect state of registries
intents = await conversation.async_get_intents(hass, "en")
name_list = intents.slot_lists.get("name")
assert isinstance(name_list, TextSlotList)
# 1 exposed light
assert len(name_list.values) == 1
assert name_list.values[0].text_in == TextChunk("demo light")
assert name_list.values[0].context == {"domain": "light"}
# 1 area
area_list = intents.slot_lists.get("area")
assert isinstance(area_list, TextSlotList)
assert len(area_list.values) == 1
assert area_list.values[0].text_in == TextChunk("kitchen")
# 1 floor
floor_list = intents.slot_lists.get("floor")
assert isinstance(floor_list, TextSlotList)
assert len(floor_list.values) == 1
assert floor_list.values[0].text_in == TextChunk("ground")