Allow UI configuration of entities exposed to voice_assistant (#91233)

* Allow UI configuration of entities exposed to voice_assistant

* Invalidate cache when settings change

* Add tests

* Expose entities to conversation by default

* Update tests
This commit is contained in:
Erik Montnemery 2023-04-12 04:39:40 +02:00 committed by GitHub
parent e40a373c4b
commit 2c9e9d0fde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 473 additions and 107 deletions

View file

@ -1,21 +1,3 @@
"""Const for conversation integration.""" """Const for conversation integration."""
DOMAIN = "conversation" 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"}

View file

@ -17,6 +17,11 @@ from home_assistant_intents import get_intents
import yaml import yaml
from homeassistant import core, setup 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 ( from homeassistant.helpers import (
area_registry as ar, area_registry as ar,
device_registry as dr, device_registry as dr,
@ -28,7 +33,7 @@ from homeassistant.helpers import (
from homeassistant.util.json import JsonObjectType, json_loads_object from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent import AbstractConversationAgent, ConversationInput, ConversationResult from .agent import AbstractConversationAgent, ConversationInput, ConversationResult
from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" _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("")) 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: def json_load(fp: IO[str]) -> JsonObjectType:
"""Wrap json_loads for get_intents.""" """Wrap json_loads for get_intents."""
return json_loads_object(fp.read()) return json_loads_object(fp.read())
@ -105,10 +105,8 @@ class DefaultAgent(AbstractConversationAgent):
self._async_handle_entity_registry_changed, self._async_handle_entity_registry_changed,
run_immediately=True, run_immediately=True,
) )
self.hass.bus.async_listen( async_listen_entity_updates(
core.EVENT_STATE_CHANGED, self.hass, DOMAIN, self._async_exposed_entities_updated
self._async_handle_state_changed,
run_immediately=True,
) )
async def async_process(self, user_input: ConversationInput) -> ConversationResult: async def async_process(self, user_input: ConversationInput) -> ConversationResult:
@ -459,10 +457,8 @@ class DefaultAgent(AbstractConversationAgent):
self._slot_lists = None self._slot_lists = None
@core.callback @core.callback
def _async_handle_state_changed(self, event: core.Event) -> None: def _async_exposed_entities_updated(self) -> None:
"""Clear names list cache when a state is added or removed from the state machine.""" """Handle updated preferences."""
if event.data.get("old_state") and event.data.get("new_state"):
return
self._slot_lists = None self._slot_lists = None
def _make_slot_lists(self) -> dict[str, SlotList]: def _make_slot_lists(self) -> dict[str, SlotList]:
@ -471,48 +467,40 @@ class DefaultAgent(AbstractConversationAgent):
return self._slot_lists return self._slot_lists
area_ids_with_entities: set[str] = set() area_ids_with_entities: set[str] = set()
states = [ all_entities = er.async_get(self.hass)
state for state in self.hass.states.async_all() if is_entity_exposed(state) 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) devices = dr.async_get(self.hass)
# Gather exposed entity names # Gather exposed entity names
entity_names = [] entity_names = []
for state in states: for entity in entities:
# Checked against "requires_context" and "excludes_context" in hassil # Checked against "requires_context" and "excludes_context" in hassil
context = {"domain": state.domain} context = {"domain": entity.domain}
if state.attributes: if entity.device_class:
# Include some attributes context[ATTR_DEVICE_CLASS] = entity.device_class
for attr_key, attr_value in state.attributes.items():
if attr_key not in DEFAULT_EXPOSED_ATTRIBUTES:
continue
context[attr_key] = attr_value
entity = entities.async_get(state.entity_id) if entity.aliases:
if entity is not None: for alias in entity.aliases:
if entity.entity_category or entity.hidden: entity_names.append((alias, alias, context))
# Skip configuration/diagnostic/hidden entities
continue
if entity.aliases: # Default name
for alias in entity.aliases: name = entity.async_friendly_name(self.hass) or entity.entity_id.replace(
entity_names.append((alias, alias, context)) "_", " "
)
entity_names.append((name, name, context))
# Default name if entity.area_id:
entity_names.append((state.name, state.name, context)) # Expose area too
area_ids_with_entities.add(entity.area_id)
if entity.area_id: elif entity.device_id:
# Expose area too # Check device for area as well
area_ids_with_entities.add(entity.area_id) device = devices.async_get(entity.device_id)
elif entity.device_id: if (device is not None) and device.area_id:
# Check device for area as well area_ids_with_entities.add(device.area_id)
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))
# Gather areas from exposed entities # Gather areas from exposed entities
areas = ar.async_get(self.hass) areas = ar.async_get(self.hass)

View file

@ -2,7 +2,7 @@
"domain": "conversation", "domain": "conversation",
"name": "Conversation", "name": "Conversation",
"codeowners": ["@home-assistant/core", "@synesthesiam"], "codeowners": ["@home-assistant/core", "@synesthesiam"],
"dependencies": ["http"], "dependencies": ["homeassistant", "http"],
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"iot_class": "local_push", "iot_class": "local_push",

View file

@ -19,7 +19,7 @@ from homeassistant.helpers.storage import Store
from .const import DATA_EXPOSED_ENTITIES, DOMAIN 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_KEY = f"{DOMAIN}.exposed_entities"
STORAGE_VERSION = 1 STORAGE_VERSION = 1
@ -61,6 +61,10 @@ DEFAULT_EXPOSED_SENSOR_DEVICE_CLASSES = {
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
} }
DEFAULT_EXPOSED_ASSISTANT = {
"conversation": True,
}
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class AssistantPreferences: class AssistantPreferences:
@ -130,7 +134,7 @@ class ExposedEntities:
"""Check if new entities are exposed to an assistant.""" """Check if new entities are exposed to an assistant."""
if prefs := self._assistants.get(assistant): if prefs := self._assistants.get(assistant):
return prefs.expose_new return prefs.expose_new
return False return DEFAULT_EXPOSED_ASSISTANT.get(assistant, False)
@callback @callback
def async_set_expose_new_entities(self, assistant: str, expose_new: bool) -> None: 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"] should_expose = registry_entry.options[assistant]["should_expose"]
return 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) should_expose = self._is_default_exposed(entity_id, registry_entry)
else: else:
should_expose = False should_expose = False

View file

@ -309,6 +309,26 @@ class RegistryEntry:
hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) 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]]]]): class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
"""Store entity registry data.""" """Store entity registry data."""

View file

@ -2,6 +2,10 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components import conversation from homeassistant.components import conversation
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
)
from homeassistant.helpers import intent from homeassistant.helpers import intent
@ -24,3 +28,15 @@ class MockAgent(conversation.AbstractConversationAgent):
return conversation.ConversationResult( return conversation.ConversationResult(
response=response, conversation_id=user_input.conversation_id 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)

View file

@ -1,5 +1,4 @@
"""Test for the default agent.""" """Test for the default agent."""
from unittest.mock import patch
import pytest import pytest
@ -15,6 +14,8 @@ from homeassistant.helpers import (
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import expose_entity
from tests.common import async_mock_service 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) 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( entity_registry.async_update_entity(
kitchen_light.entity_id, device_id=kitchen_device.id kitchen_light.entity_id, device_id=kitchen_device.id
) )
hass.states.async_set( hass.states.async_set(kitchen_light.entity_id, "on")
kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
)
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( entity_registry.async_update_entity(
bedroom_light.entity_id, area_id=area_bedroom.id bedroom_light.entity_id, area_id=area_bedroom.id
) )
hass.states.async_set( hass.states.async_set(bedroom_light.entity_id, "on")
bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"}
# 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): # All is well for the exposed kitchen light
return state.entity_id != bedroom_light.entity_id assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
with patch( # Bedroom is not exposed because it has no exposed entities
"homeassistant.components.conversation.default_agent.is_entity_exposed", result = await conversation.async_converse(
is_entity_exposed, hass, "turn on lights in the bedroom", None, Context(), None
): )
result = await conversation.async_converse(
hass, "turn on lights in the kitchen", None, Context(), None
)
# All is well for the exposed kitchen light # This should be an intent match failure because the area isn't in the slot list
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
# 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
)

View file

@ -20,6 +20,8 @@ from homeassistant.helpers import (
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import expose_entity, expose_new
from tests.common import MockConfigEntry, MockUser, async_mock_service from tests.common import MockConfigEntry, MockUser, async_mock_service
from tests.typing import ClientSessionGenerator, WebSocketGenerator from tests.typing import ClientSessionGenerator, WebSocketGenerator
@ -200,7 +202,11 @@ async def test_http_processing_intent_entity_added_removed(
# Add an entity # Add an entity
entity_registry.async_get_or_create( 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"}) 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. so that the new alias is available.
""" """
entity_registry.async_get_or_create( 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"}) 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,
{LIGHT_DOMAIN: [{"platform": "test"}]}, {LIGHT_DOMAIN: [{"platform": "test"}]},
) )
await hass.async_block_till_done()
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client() 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("agent_id", AGENT_ID_OPTIONS)
@pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on"))
async def test_turn_on_intent( 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: ) -> None:
"""Test calling the turn on intent.""" """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") hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") 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")) @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.""" """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") hass.states.async_set("light.kitchen", "on")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off") 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( 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: ) -> None:
"""Test the HTTP conversation API with an error during handling.""" """Test the HTTP conversation API with an error during handling."""
client = await hass_client() 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") hass.states.async_set("light.kitchen", "off")
# Raise an error during intent handling # Raise an error during intent handling
@ -685,11 +1018,21 @@ async def test_http_api_handle_failure(
async def test_http_api_unexpected_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: ) -> None:
"""Test the HTTP conversation API with an unexpected error during handling.""" """Test the HTTP conversation API with an unexpected error during handling."""
client = await hass_client() 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") hass.states.async_set("light.kitchen", "off")
# Raise an "unexpected" error during intent handling # 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") 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.""" """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") hass.states.async_set("light.kitchen", "off")
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") 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) 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.""" """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") hass.states.async_set("cover.front_door", "closed")
calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)