diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py index 1cae975c957..04bfa373061 100644 --- a/homeassistant/components/conversation/const.py +++ b/homeassistant/components/conversation/const.py @@ -1,21 +1,3 @@ """Const for conversation integration.""" DOMAIN = "conversation" - -DEFAULT_EXPOSED_DOMAINS = { - "binary_sensor", - "climate", - "cover", - "fan", - "humidifier", - "light", - "lock", - "scene", - "script", - "sensor", - "switch", - "vacuum", - "water_heater", -} - -DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"} diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 98959320d7a..d89b938a025 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -17,6 +17,11 @@ from home_assistant_intents import get_intents import yaml from homeassistant import core, setup +from homeassistant.components.homeassistant.exposed_entities import ( + async_listen_entity_updates, + async_should_expose, +) +from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.helpers import ( area_registry as ar, device_registry as dr, @@ -28,7 +33,7 @@ from homeassistant.helpers import ( from homeassistant.util.json import JsonObjectType, json_loads_object from .agent import AbstractConversationAgent, ConversationInput, ConversationResult -from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" @@ -37,11 +42,6 @@ _ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] REGEX_TYPE = type(re.compile("")) -def is_entity_exposed(state: core.State) -> bool: - """Return true if entity belongs to exposed domain list.""" - return state.domain in DEFAULT_EXPOSED_DOMAINS - - def json_load(fp: IO[str]) -> JsonObjectType: """Wrap json_loads for get_intents.""" return json_loads_object(fp.read()) @@ -105,10 +105,8 @@ class DefaultAgent(AbstractConversationAgent): self._async_handle_entity_registry_changed, run_immediately=True, ) - self.hass.bus.async_listen( - core.EVENT_STATE_CHANGED, - self._async_handle_state_changed, - run_immediately=True, + async_listen_entity_updates( + self.hass, DOMAIN, self._async_exposed_entities_updated ) async def async_process(self, user_input: ConversationInput) -> ConversationResult: @@ -459,10 +457,8 @@ class DefaultAgent(AbstractConversationAgent): self._slot_lists = None @core.callback - def _async_handle_state_changed(self, event: core.Event) -> None: - """Clear names list cache when a state is added or removed from the state machine.""" - if event.data.get("old_state") and event.data.get("new_state"): - return + def _async_exposed_entities_updated(self) -> None: + """Handle updated preferences.""" self._slot_lists = None def _make_slot_lists(self) -> dict[str, SlotList]: @@ -471,48 +467,40 @@ class DefaultAgent(AbstractConversationAgent): return self._slot_lists area_ids_with_entities: set[str] = set() - states = [ - state for state in self.hass.states.async_all() if is_entity_exposed(state) + all_entities = er.async_get(self.hass) + entities = [ + entity + for entity in all_entities.entities.values() + if async_should_expose(self.hass, DOMAIN, entity.entity_id) ] - entities = er.async_get(self.hass) devices = dr.async_get(self.hass) # Gather exposed entity names entity_names = [] - for state in states: + for entity in entities: # Checked against "requires_context" and "excludes_context" in hassil - context = {"domain": state.domain} - if state.attributes: - # Include some attributes - for attr_key, attr_value in state.attributes.items(): - if attr_key not in DEFAULT_EXPOSED_ATTRIBUTES: - continue - context[attr_key] = attr_value + context = {"domain": entity.domain} + if entity.device_class: + context[ATTR_DEVICE_CLASS] = entity.device_class - entity = entities.async_get(state.entity_id) - if entity is not None: - if entity.entity_category or entity.hidden: - # Skip configuration/diagnostic/hidden entities - continue + if entity.aliases: + for alias in entity.aliases: + entity_names.append((alias, alias, context)) - if entity.aliases: - for alias in entity.aliases: - entity_names.append((alias, alias, context)) + # Default name + name = entity.async_friendly_name(self.hass) or entity.entity_id.replace( + "_", " " + ) + entity_names.append((name, name, context)) - # Default name - entity_names.append((state.name, state.name, context)) - - if entity.area_id: - # Expose area too - area_ids_with_entities.add(entity.area_id) - elif entity.device_id: - # Check device for area as well - device = devices.async_get(entity.device_id) - if (device is not None) and device.area_id: - area_ids_with_entities.add(device.area_id) - else: - # Default name - entity_names.append((state.name, state.name, context)) + if entity.area_id: + # Expose area too + area_ids_with_entities.add(entity.area_id) + elif entity.device_id: + # Check device for area as well + device = devices.async_get(entity.device_id) + if (device is not None) and device.area_id: + area_ids_with_entities.add(device.area_id) # Gather areas from exposed entities areas = ar.async_get(self.hass) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 0753fcd5af9..d8aa802bfaa 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "codeowners": ["@home-assistant/core", "@synesthesiam"], - "dependencies": ["http"], + "dependencies": ["homeassistant", "http"], "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "iot_class": "local_push", diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 9317f43ea75..852e33721c0 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -19,7 +19,7 @@ from homeassistant.helpers.storage import Store from .const import DATA_EXPOSED_ENTITIES, DOMAIN -KNOWN_ASSISTANTS = ("cloud.alexa", "cloud.google_assistant") +KNOWN_ASSISTANTS = ("cloud.alexa", "cloud.google_assistant", "conversation") STORAGE_KEY = f"{DOMAIN}.exposed_entities" STORAGE_VERSION = 1 @@ -61,6 +61,10 @@ DEFAULT_EXPOSED_SENSOR_DEVICE_CLASSES = { SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, } +DEFAULT_EXPOSED_ASSISTANT = { + "conversation": True, +} + @dataclasses.dataclass(frozen=True) class AssistantPreferences: @@ -130,7 +134,7 @@ class ExposedEntities: """Check if new entities are exposed to an assistant.""" if prefs := self._assistants.get(assistant): return prefs.expose_new - return False + return DEFAULT_EXPOSED_ASSISTANT.get(assistant, False) @callback def async_set_expose_new_entities(self, assistant: str, expose_new: bool) -> None: @@ -170,7 +174,7 @@ class ExposedEntities: should_expose = registry_entry.options[assistant]["should_expose"] return should_expose - if (prefs := self._assistants.get(assistant)) and prefs.expose_new: + if self.async_get_expose_new_entities(assistant): should_expose = self._is_default_exposed(entity_id, registry_entry) else: should_expose = False diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 9cb119b81b4..9a30e20a8e8 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -309,6 +309,26 @@ class RegistryEntry: hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) + def async_friendly_name(self, hass: HomeAssistant) -> str | None: + """Return the friendly name. + + If self.name is not None, this returns self.name + If has_entity_name is False, self.original_name + If has_entity_name is True, this returns device.name + self.original_name + """ + if not self.has_entity_name or self.name is not None: + return self.name or self.original_name + + device_registry = dr.async_get(hass) + if not (device_id := self.device_id) or not ( + device_entry := device_registry.async_get(device_id) + ): + return self.original_name + + if not (original_name := self.original_name): + return device_entry.name_by_user or device_entry.name + return f"{device_entry.name_by_user or device_entry.name} {original_name}" + class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): """Store entity registry data.""" diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index fac9ae95e61..4812411fe23 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -2,6 +2,10 @@ from __future__ import annotations from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import ( + DATA_EXPOSED_ENTITIES, + ExposedEntities, +) from homeassistant.helpers import intent @@ -24,3 +28,15 @@ class MockAgent(conversation.AbstractConversationAgent): return conversation.ConversationResult( response=response, conversation_id=user_input.conversation_id ) + + +def expose_new(hass, expose_new): + """Enable exposing new entities to the default agent.""" + exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) + + +def expose_entity(hass, entity_id, should_expose): + """Expose an entity to the default agent.""" + exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities.async_expose_entity(conversation.DOMAIN, entity_id, should_expose) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 338d840c4a7..f375d3ca1e3 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -1,5 +1,4 @@ """Test for the default agent.""" -from unittest.mock import patch import pytest @@ -15,6 +14,8 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component +from . import expose_entity + from tests.common import async_mock_service @@ -86,43 +87,37 @@ async def test_exposed_areas( ) device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) - kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") + kitchen_light = entity_registry.async_get_or_create( + "light", "demo", "1234", original_name="kitchen light" + ) entity_registry.async_update_entity( kitchen_light.entity_id, device_id=kitchen_device.id ) - hass.states.async_set( - kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} - ) + hass.states.async_set(kitchen_light.entity_id, "on") - bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") + bedroom_light = entity_registry.async_get_or_create( + "light", "demo", "5678", original_name="bedroom light" + ) entity_registry.async_update_entity( bedroom_light.entity_id, area_id=area_bedroom.id ) - hass.states.async_set( - bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"} + hass.states.async_set(bedroom_light.entity_id, "on") + + # Hide the bedroom light + expose_entity(hass, bedroom_light.entity_id, False) + + result = await conversation.async_converse( + hass, "turn on lights in the kitchen", None, Context(), None ) - def is_entity_exposed(state): - return state.entity_id != bedroom_light.entity_id + # All is well for the exposed kitchen light + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - with patch( - "homeassistant.components.conversation.default_agent.is_entity_exposed", - is_entity_exposed, - ): - result = await conversation.async_converse( - hass, "turn on lights in the kitchen", None, Context(), None - ) + # Bedroom is not exposed because it has no exposed entities + result = await conversation.async_converse( + hass, "turn on lights in the bedroom", None, Context(), None + ) - # All is well for the exposed kitchen light - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - - # Bedroom is not exposed because it has no exposed entities - result = await conversation.async_converse( - hass, "turn on lights in the bedroom", None, Context(), None - ) - - # This should be an intent match failure because the area isn't in the slot list - assert result.response.response_type == intent.IntentResponseType.ERROR - assert ( - result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH - ) + # This should be an intent match failure because the area isn't in the slot list + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 91bf444123a..03f581b9aff 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -20,6 +20,8 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component +from . import expose_entity, expose_new + from tests.common import MockConfigEntry, MockUser, async_mock_service from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -200,7 +202,11 @@ async def test_http_processing_intent_entity_added_removed( # Add an entity entity_registry.async_get_or_create( - "light", "demo", "5678", suggested_object_id="late" + "light", + "demo", + "5678", + suggested_object_id="late", + original_name="friendly light", ) hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"}) @@ -307,7 +313,11 @@ async def test_http_processing_intent_alias_added_removed( so that the new alias is available. """ entity_registry.async_get_or_create( - "light", "demo", "1234", suggested_object_id="kitchen" + "light", + "demo", + "1234", + suggested_object_id="kitchen", + original_name="kitchen light", ) hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"}) @@ -428,6 +438,7 @@ async def test_http_processing_intent_entity_renamed( LIGHT_DOMAIN, {LIGHT_DOMAIN: [{"platform": "test"}]}, ) + await hass.async_block_till_done() calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") client = await hass_client() @@ -576,12 +587,315 @@ async def test_http_processing_intent_entity_renamed( } +async def test_http_processing_intent_entity_exposed( + 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 manual expose. + + We want to ensure that manually exposing an entity later busts the cache + so that the new setting 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"}]}, + ) + await hass.async_block_till_done() + entity_registry.async_update_entity("light.kitchen", aliases={"my cool 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, + } + + calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on my cool 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, + } + + # Unexpose the entity + expose_entity(hass, "light.kitchen", False) + 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 == { + "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", + } + }, + }, + } + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on my cool 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 expose the entity + expose_entity(hass, "light.kitchen", True) + 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 my cool 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, + } + + +async def test_http_processing_intent_conversion_not_expose_new( + 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 when not exposing new entities.""" + # Disable exposing new entities to the default agent + expose_new(hass, False) + + 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"}]}, + ) + await hass.async_block_till_done() + + 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 + 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", + } + }, + }, + } + + # Expose the entity + expose_entity(hass, "light.kitchen", True) + await hass.async_block_till_done() + + 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, + } + + @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent( - hass: HomeAssistant, init_components, sentence, agent_id + hass: HomeAssistant, + init_components, + entity_registry: er.EntityRegistry, + sentence, + agent_id, ) -> None: """Test calling the turn on intent.""" + entity_registry.async_get_or_create( + "light", + "demo", + "1234", + suggested_object_id="kitchen", + original_name="kitchen", + ) hass.states.async_set("light.kitchen", "off") calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") @@ -599,8 +913,17 @@ async def test_turn_on_intent( @pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off")) -async def test_turn_off_intent(hass: HomeAssistant, init_components, sentence) -> None: +async def test_turn_off_intent( + hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry, sentence +) -> None: """Test calling the turn on intent.""" + entity_registry.async_get_or_create( + "light", + "demo", + "1234", + suggested_object_id="kitchen", + original_name="kitchen", + ) hass.states.async_set("light.kitchen", "on") calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off") @@ -646,11 +969,21 @@ async def test_http_api_no_match( async def test_http_api_handle_failure( - hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator + hass: HomeAssistant, + init_components, + entity_registry: er.EntityRegistry, + hass_client: ClientSessionGenerator, ) -> None: """Test the HTTP conversation API with an error during handling.""" client = await hass_client() + entity_registry.async_get_or_create( + "light", + "demo", + "1234", + suggested_object_id="kitchen", + original_name="kitchen", + ) hass.states.async_set("light.kitchen", "off") # Raise an error during intent handling @@ -685,11 +1018,21 @@ async def test_http_api_handle_failure( async def test_http_api_unexpected_failure( - hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator + hass: HomeAssistant, + init_components, + entity_registry: er.EntityRegistry, + hass_client: ClientSessionGenerator, ) -> None: """Test the HTTP conversation API with an unexpected error during handling.""" client = await hass_client() + entity_registry.async_get_or_create( + "light", + "demo", + "1234", + suggested_object_id="kitchen", + original_name="kitchen", + ) hass.states.async_set("light.kitchen", "off") # Raise an "unexpected" error during intent handling @@ -1008,8 +1351,17 @@ async def test_prepare_fail(hass: HomeAssistant) -> None: assert not agent._lang_intents.get("not-a-language") -async def test_language_region(hass: HomeAssistant, init_components) -> None: +async def test_language_region( + hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry +) -> None: """Test calling the turn on intent.""" + entity_registry.async_get_or_create( + "light", + "demo", + "1234", + suggested_object_id="kitchen", + original_name="kitchen", + ) hass.states.async_set("light.kitchen", "off") calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") @@ -1057,8 +1409,17 @@ async def test_reload_on_new_component(hass: HomeAssistant) -> None: assert {"light"} == (lang_intents.loaded_components - loaded_components) -async def test_non_default_response(hass: HomeAssistant, init_components) -> None: +async def test_non_default_response( + hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry +) -> None: """Test intent response that is not the default.""" + entity_registry.async_get_or_create( + "cover", + "demo", + "1234", + suggested_object_id="front_door", + original_name="front door", + ) hass.states.async_set("cover.front_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)