mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix target matching in Assist sentence parser debugger (#178202)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a107789a5b
commit
935764fb6c
@@ -409,10 +409,13 @@ class DefaultAgent(ConversationEntity):
|
||||
}
|
||||
|
||||
if successful_match:
|
||||
satellite_area, _ = self._get_satellite_area_and_device(
|
||||
user_input.satellite_id, user_input.device_id
|
||||
)
|
||||
result_dict["targets"] = {
|
||||
state.entity_id: {"matched": is_matched}
|
||||
for state, is_matched in _get_debug_targets(
|
||||
self.hass, intent_result
|
||||
self.hass, intent_result, satellite_area
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1676,12 +1679,14 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No
|
||||
def _get_debug_targets(
|
||||
hass: HomeAssistant,
|
||||
result: RecognizeResult,
|
||||
satellite_area: ar.AreaEntry | None = None,
|
||||
) -> Iterable[tuple[State, bool]]:
|
||||
"""Yield state/is_matched pairs for a hassil recognition."""
|
||||
entities = result.entities
|
||||
|
||||
name: str | None = None
|
||||
area_name: str | None = None
|
||||
floor_name: str | None = None
|
||||
domains: set[str] | None = None
|
||||
device_classes: set[str] | None = None
|
||||
state_names: set[str] | None = None
|
||||
@@ -1692,6 +1697,9 @@ def _get_debug_targets(
|
||||
if "area" in entities:
|
||||
area_name = str(entities["area"].value)
|
||||
|
||||
if "floor" in entities:
|
||||
floor_name = str(entities["floor"].value)
|
||||
|
||||
if "domain" in entities:
|
||||
domains = set(cv.ensure_list(entities["domain"].value))
|
||||
|
||||
@@ -1702,24 +1710,27 @@ def _get_debug_targets(
|
||||
# HassGetState only
|
||||
state_names = set(cv.ensure_list(entities["state"].value))
|
||||
|
||||
if (
|
||||
(name is None)
|
||||
and (area_name is None)
|
||||
and (not domains)
|
||||
and (not device_classes)
|
||||
and (not state_names)
|
||||
):
|
||||
constraints = intent.MatchTargetsConstraints(
|
||||
name=name,
|
||||
area_name=area_name,
|
||||
floor_name=floor_name,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
assistant=DOMAIN,
|
||||
)
|
||||
|
||||
if not (constraints.has_constraints or state_names):
|
||||
# Avoid "matching" all entities when there is no filter
|
||||
return
|
||||
|
||||
states = intent.async_match_states(
|
||||
hass,
|
||||
name=name,
|
||||
area_name=area_name,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
# Mirror the preferences used when the intent is actually handled so that
|
||||
# duplicate names are deduplicated the same way.
|
||||
preferences = intent.MatchTargetsPreferences(
|
||||
area_id=satellite_area.id if satellite_area is not None else None
|
||||
)
|
||||
|
||||
states = intent.async_match_targets(hass, constraints, preferences).states
|
||||
|
||||
for state in states:
|
||||
# For queries, a target is "matched" based on its state
|
||||
is_matched = (state_names is None) or (state.state in state_names)
|
||||
|
||||
@@ -9,6 +9,7 @@ from freezegun import freeze_time
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.conversation import (
|
||||
AssistantContent,
|
||||
ConversationInput,
|
||||
@@ -17,13 +18,16 @@ from homeassistant.components.conversation import (
|
||||
)
|
||||
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
|
||||
from homeassistant.components.conversation.models import ConversationResult
|
||||
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 HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
chat_session,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -31,7 +35,12 @@ from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import MockAgent
|
||||
|
||||
from tests.common import MockUser, async_fire_time_changed, async_mock_service
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockUser,
|
||||
async_fire_time_changed,
|
||||
async_mock_service,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
|
||||
AGENT_ID_OPTIONS = [
|
||||
@@ -454,6 +463,148 @@ async def test_ws_hass_agent_debug_null_result(
|
||||
assert msg["result"]["results"] == [None]
|
||||
|
||||
|
||||
async def test_ws_hass_agent_debug_floor(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
area_registry: ar.AreaRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that debug targets are restricted to the matched floor."""
|
||||
first_floor = floor_registry.async_create("first floor")
|
||||
floor_registry.async_create("ground floor")
|
||||
|
||||
bedroom_area = area_registry.async_create("bedroom", floor_id=first_floor.floor_id)
|
||||
bedroom_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "bedroom", original_name="bedroom light"
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
bedroom_light.entity_id, area_id=bedroom_area.id
|
||||
)
|
||||
hass.states.async_set(bedroom_light.entity_id, "on")
|
||||
|
||||
# Not assigned to a floor
|
||||
garage_area = area_registry.async_create("garage")
|
||||
garage_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "garage", original_name="garage light"
|
||||
)
|
||||
entity_registry.async_update_entity(garage_light.entity_id, area_id=garage_area.id)
|
||||
hass.states.async_set(garage_light.entity_id, "on")
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "conversation/agent/homeassistant/debug",
|
||||
"sentences": [
|
||||
"turn off the lights on the first floor",
|
||||
"turn off the lights on the ground floor",
|
||||
],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
results = msg["result"]["results"]
|
||||
assert results[0]["match"]
|
||||
assert results[0]["targets"] == {bedroom_light.entity_id: {"matched": True}}
|
||||
|
||||
# No areas are assigned to the ground floor
|
||||
assert results[1]["match"]
|
||||
assert results[1]["targets"] == {}
|
||||
|
||||
|
||||
async def test_ws_hass_agent_debug_unexposed_entity(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
area_registry: ar.AreaRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that debug targets only include entities exposed to Assist."""
|
||||
kitchen_area = area_registry.async_create("kitchen")
|
||||
|
||||
exposed_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "exposed", original_name="exposed light"
|
||||
)
|
||||
hidden_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "hidden", original_name="hidden light"
|
||||
)
|
||||
for entity_entry in (exposed_light, hidden_light):
|
||||
entity_registry.async_update_entity(
|
||||
entity_entry.entity_id, area_id=kitchen_area.id
|
||||
)
|
||||
hass.states.async_set(entity_entry.entity_id, "on")
|
||||
|
||||
async_expose_entity(hass, conversation.DOMAIN, hidden_light.entity_id, False)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "conversation/agent/homeassistant/debug",
|
||||
"sentences": ["turn off the lights in the kitchen"],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
results = msg["result"]["results"]
|
||||
assert results[0]["match"]
|
||||
assert results[0]["targets"] == {exposed_light.entity_id: {"matched": True}}
|
||||
|
||||
|
||||
async def test_ws_hass_agent_debug_preferred_area(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that debug targets use the requesting device's area to disambiguate."""
|
||||
config_entry = MockConfigEntry()
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
bedroom_area = area_registry.async_create("bedroom")
|
||||
office_area = area_registry.async_create("office")
|
||||
|
||||
# Duplicate names in two areas
|
||||
bedroom_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "bedroom", original_name="overhead light"
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
bedroom_light.entity_id, area_id=bedroom_area.id
|
||||
)
|
||||
hass.states.async_set(bedroom_light.entity_id, "on")
|
||||
|
||||
office_light = entity_registry.async_get_or_create(
|
||||
"light", "demo", "office", original_name="overhead light"
|
||||
)
|
||||
entity_registry.async_update_entity(office_light.entity_id, area_id=office_area.id)
|
||||
hass.states.async_set(office_light.entity_id, "on")
|
||||
|
||||
voice_device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={("demo", "voice-satellite")},
|
||||
)
|
||||
device_registry.async_update_device(voice_device.id, area_id=office_area.id)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "conversation/agent/homeassistant/debug",
|
||||
"sentences": ["turn off the overhead light"],
|
||||
"device_id": voice_device.id,
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
results = msg["result"]["results"]
|
||||
assert results[0]["match"]
|
||||
assert results[0]["targets"] == {office_light.entity_id: {"matched": True}}
|
||||
|
||||
|
||||
async def test_ws_hass_agent_debug_out_of_range(
|
||||
hass: HomeAssistant,
|
||||
init_components,
|
||||
|
||||
Reference in New Issue
Block a user