diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 49569f66ac0..98959320d7a 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -32,6 +32,7 @@ from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" +_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] REGEX_TYPE = type(re.compile("")) @@ -450,8 +451,10 @@ class DefaultAgent(AbstractConversationAgent): @core.callback def _async_handle_entity_registry_changed(self, event: core.Event) -> None: - """Clear names list cache when an entity changes aliases.""" - if event.data["action"] == "update" and "aliases" not in event.data["changes"]: + """Clear names list cache when an entity registry entry has changed.""" + if event.data["action"] == "update" and not any( + field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS + ): return self._slot_lists = None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index eb38d875bfa..91bf444123a 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -148,7 +148,7 @@ async def test_http_processing_intent_target_ha_agent( } -async def test_http_processing_intent_entity_added( +async def test_http_processing_intent_entity_added_removed( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, @@ -198,7 +198,7 @@ async def test_http_processing_intent_entity_added( "conversation_id": None, } - # Add an alias + # Add an entity entity_registry.async_get_or_create( "light", "demo", "5678", suggested_object_id="late" ) @@ -294,6 +294,288 @@ async def test_http_processing_intent_entity_added( } +async def test_http_processing_intent_alias_added_removed( + hass: HomeAssistant, + init_components, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + entity_registry: er.EntityRegistry, +) -> None: + """Test processing intent via HTTP API with aliases added later. + + We want to ensure that adding an alias later busts the cache + so that the new alias is available. + """ + entity_registry.async_get_or_create( + "light", "demo", "1234", suggested_object_id="kitchen" + ) + hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"}) + + calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + assert len(calls) == 1 + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Add an alias + entity_registry.async_update_entity("light.kitchen", aliases={"late added alias"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added alias"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now remove the alieas + entity_registry.async_update_entity("light.kitchen", aliases={}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added alias"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand that", + } + }, + }, + } + + +async def test_http_processing_intent_entity_renamed( + hass: HomeAssistant, + init_components, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + entity_registry: er.EntityRegistry, + enable_custom_integrations: None, +) -> None: + """Test processing intent via HTTP API with entities renamed later. + + We want to ensure that renaming an entity later busts the cache + so that the new name is used. + """ + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + entity = platform.MockLight("kitchen light", "on") + entity._attr_unique_id = "1234" + entity.entity_id = "light.kitchen" + platform.ENTITIES.append(entity) + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: [{"platform": "test"}]}, + ) + + calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + assert len(calls) == 1 + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Rename the entity + entity_registry.async_update_entity("light.kitchen", name="renamed light") + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on renamed light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "renamed light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand that", + } + }, + }, + } + + # Now clear the custom name + entity_registry.async_update_entity("light.kitchen", name=None) + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on renamed light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand that", + } + }, + }, + } + + @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent(