From 254aa8c9ead8476a214700edbe9b5d37554a51f1 Mon Sep 17 00:00:00 2001 From: Andrii Mitnovych <10116550+formatBCE@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:00:23 -0700 Subject: [PATCH] Add entity deduplication by assist device ID in conversation agent (#123957) * Add entities deduplication by assist device ID in default conversation agent * Updated test. --- .../components/conversation/default_agent.py | 25 +++++-- .../conversation/test_default_agent.py | 66 +++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 1661d2ad30d..05b4d194d33 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -349,6 +349,9 @@ class DefaultAgent(ConversationEntity): } for entity in result.entities_list } + device_area = self._get_device_area(user_input.device_id) + if device_area: + slots["preferred_area_id"] = {"value": device_area.id} async_conversation_trace_append( ConversationTraceEventType.TOOL_CALL, { @@ -917,18 +920,26 @@ class DefaultAgent(ConversationEntity): if not user_input.device_id: return None - devices = dr.async_get(self.hass) - device = devices.async_get(user_input.device_id) - if (device is None) or (device.area_id is None): - return None - - areas = ar.async_get(self.hass) - device_area = areas.async_get_area(device.area_id) + device_area = self._get_device_area(user_input.device_id) if device_area is None: return None return {"area": {"value": device_area.name, "text": device_area.name}} + def _get_device_area(self, device_id: str | None) -> ar.AreaEntry | None: + """Return area object for given device identifier.""" + if device_id is None: + return None + + devices = dr.async_get(self.hass) + device = devices.async_get(device_id) + if (device is None) or (device.area_id is None): + return None + + areas = ar.async_get(self.hass) + + return areas.async_get_area(device.area_id) + def _get_error_text( self, error_key: ErrorKey | str, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 315b73bacfd..935ef205d4f 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -308,6 +308,72 @@ async def test_unexposed_entities_skipped( assert result.response.matched_states[0].entity_id == exposed_light.entity_id +@pytest.mark.usefixtures("init_components") +async def test_duplicated_names_resolved_with_device_area( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test entities deduplication with device ID context.""" + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + + kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") + bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") + + # Same name and alias + for light in (kitchen_light, bedroom_light): + light = entity_registry.async_update_entity( + light.entity_id, + name="top light", + aliases={"overhead light"}, + ) + hass.states.async_set( + light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: light.name}, + ) + # Different areas + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, + area_id=area_kitchen.id, + ) + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, + area_id=area_bedroom.id, + ) + + # Pipeline device in bedroom area + entry = MockConfigEntry() + entry.add_to_hass(hass) + assist_device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, + ) + assist_device = device_registry.async_update_device( + assist_device.id, + area_id=area_bedroom.id, + ) + + # Check name and alias + for name in ("top light", "overhead light"): + # Only one light should be turned on + calls = async_mock_service(hass, "light", "turn_on") + result = await conversation.async_converse( + hass, f"turn on {name}", None, Context(), device_id=assist_device.id + ) + + assert len(calls) == 1 + assert calls[0].data["entity_id"][0] == bedroom_light.entity_id + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots.get("name", {}).get("value") == name + assert result.response.intent.slots.get("name", {}).get("text") == name + + @pytest.mark.usefixtures("init_components") async def test_trigger_sentences(hass: HomeAssistant) -> None: """Test registering/unregistering/matching a few trigger sentences."""