mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add conversation mobile app webhook (#86239)
* Add conversation mobile app webhook * Re-instate removed unused import which was used as fixture
This commit is contained in:
parent
c0d9dcdb3f
commit
9631146745
@ -33,11 +33,18 @@ SERVICE_PROCESS = "process"
|
||||
SERVICE_RELOAD = "reload"
|
||||
|
||||
SERVICE_PROCESS_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string}
|
||||
{
|
||||
vol.Required(ATTR_TEXT): cv.string,
|
||||
vol.Optional(ATTR_LANGUAGE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
SERVICE_RELOAD_SCHEMA = vol.Schema({vol.Optional(ATTR_LANGUAGE): cv.string})
|
||||
SERVICE_RELOAD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_LANGUAGE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@ -101,8 +108,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
"type": "conversation/process",
|
||||
"text": str,
|
||||
vol.Required("type"): "conversation/process",
|
||||
vol.Required("text"): str,
|
||||
vol.Optional("conversation_id"): vol.Any(str, None),
|
||||
vol.Optional("language"): str,
|
||||
}
|
||||
@ -114,7 +121,7 @@ async def websocket_process(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Process text."""
|
||||
result = await _async_converse(
|
||||
result = await async_converse(
|
||||
hass,
|
||||
msg["text"],
|
||||
msg.get("conversation_id"),
|
||||
@ -142,7 +149,11 @@ async def websocket_prepare(
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.websocket_command({"type": "conversation/agent/info"})
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "conversation/agent/info",
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_get_agent_info(
|
||||
hass: HomeAssistant,
|
||||
@ -161,7 +172,12 @@ async def websocket_get_agent_info(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool})
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "conversation/onboarding/set",
|
||||
vol.Required("shown"): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_set_onboarding(
|
||||
hass: HomeAssistant,
|
||||
@ -197,7 +213,7 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
async def post(self, request, data):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app["hass"]
|
||||
result = await _async_converse(
|
||||
result = await async_converse(
|
||||
hass,
|
||||
text=data["text"],
|
||||
conversation_id=data.get("conversation_id"),
|
||||
@ -216,7 +232,7 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent:
|
||||
return agent
|
||||
|
||||
|
||||
async def _async_converse(
|
||||
async def async_converse(
|
||||
hass: core.HomeAssistant,
|
||||
text: str,
|
||||
conversation_id: str | None,
|
||||
|
@ -67,14 +67,13 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if "intent" not in self.hass.config.components:
|
||||
await setup.async_setup_component(self.hass, "intent", {})
|
||||
|
||||
config = config.get(DOMAIN, {})
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if config:
|
||||
if config and config.get(DOMAIN):
|
||||
_LOGGER.warning(
|
||||
"Custom intent sentences have been moved to config/custom_sentences"
|
||||
)
|
||||
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
async def async_process(
|
||||
self,
|
||||
text: str,
|
||||
|
@ -5,7 +5,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mobile_app",
|
||||
"requirements": ["PyNaCl==1.5.0"],
|
||||
"dependencies": ["http", "webhook", "person", "tag", "websocket_api"],
|
||||
"after_dependencies": ["cloud", "camera", "notify"],
|
||||
"after_dependencies": ["cloud", "camera", "conversation", "notify"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push",
|
||||
|
@ -15,7 +15,13 @@ from nacl.exceptions import CryptoError
|
||||
from nacl.secret import SecretBox
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import camera, cloud, notify as hass_notify, tag
|
||||
from homeassistant.components import (
|
||||
camera,
|
||||
cloud,
|
||||
conversation,
|
||||
notify as hass_notify,
|
||||
tag,
|
||||
)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES as BINARY_SENSOR_CLASSES,
|
||||
)
|
||||
@ -301,6 +307,28 @@ async def webhook_fire_event(
|
||||
return empty_okay_response()
|
||||
|
||||
|
||||
@WEBHOOK_COMMANDS.register("conversation_process")
|
||||
@validate_schema(
|
||||
{
|
||||
vol.Required("text"): cv.string,
|
||||
vol.Optional("language"): cv.string,
|
||||
vol.Optional("conversation_id"): cv.string,
|
||||
}
|
||||
)
|
||||
async def webhook_conversation_process(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
|
||||
) -> Response:
|
||||
"""Handle a conversation process webhook."""
|
||||
result = await conversation.async_converse(
|
||||
hass,
|
||||
text=data["text"],
|
||||
language=data.get("language"),
|
||||
conversation_id=data.get("conversation_id"),
|
||||
context=registration_context(config_entry.data),
|
||||
)
|
||||
return webhook_response(result.as_dict(), registration=config_entry.data)
|
||||
|
||||
|
||||
@WEBHOOK_COMMANDS.register("stream_camera")
|
||||
@validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string})
|
||||
async def webhook_stream_camera(
|
||||
|
@ -1 +1,30 @@
|
||||
"""Tests for the conversation component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.core import Context
|
||||
from homeassistant.helpers import intent
|
||||
|
||||
|
||||
class MockAgent(conversation.AbstractConversationAgent):
|
||||
"""Test Agent."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the agent."""
|
||||
self.calls = []
|
||||
self.response = "Test response"
|
||||
|
||||
async def async_process(
|
||||
self,
|
||||
text: str,
|
||||
context: Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> conversation.ConversationResult | None:
|
||||
"""Process some text."""
|
||||
self.calls.append((text, context, conversation_id, language))
|
||||
response = intent.IntentResponse(language=language)
|
||||
response.async_set_speech(self.response)
|
||||
return conversation.ConversationResult(
|
||||
response=response, conversation_id=conversation_id
|
||||
)
|
||||
|
15
tests/components/conversation/conftest.py
Normal file
15
tests/components/conversation/conftest.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Conversation test helpers."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import conversation
|
||||
|
||||
from . import MockAgent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent(hass):
|
||||
"""Mock agent."""
|
||||
agent = MockAgent()
|
||||
conversation.async_set_agent(hass, agent)
|
||||
return agent
|
@ -222,25 +222,8 @@ async def test_http_api_wrong_data(hass, init_components, hass_client):
|
||||
assert resp.status == HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
||||
async def test_custom_agent(hass, hass_client, hass_admin_user):
|
||||
async def test_custom_agent(hass, hass_client, hass_admin_user, mock_agent):
|
||||
"""Test a custom conversation agent."""
|
||||
|
||||
calls = []
|
||||
|
||||
class MyAgent(conversation.AbstractConversationAgent):
|
||||
"""Test Agent."""
|
||||
|
||||
async def async_process(self, text, context, conversation_id, language):
|
||||
"""Process some text."""
|
||||
calls.append((text, context, conversation_id, language))
|
||||
response = intent.IntentResponse(language=language)
|
||||
response.async_set_speech("Test response")
|
||||
return conversation.ConversationResult(
|
||||
response=response, conversation_id=conversation_id
|
||||
)
|
||||
|
||||
conversation.async_set_agent(hass, MyAgent())
|
||||
|
||||
assert await async_setup_component(hass, "conversation", {})
|
||||
|
||||
client = await hass_client()
|
||||
@ -270,11 +253,11 @@ async def test_custom_agent(hass, hass_client, hass_admin_user):
|
||||
"conversation_id": "test-conv-id",
|
||||
}
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0][0] == "Test Text"
|
||||
assert calls[0][1].user_id == hass_admin_user.id
|
||||
assert calls[0][2] == "test-conv-id"
|
||||
assert calls[0][3] == "test-language"
|
||||
assert len(mock_agent.calls) == 1
|
||||
assert mock_agent.calls[0][0] == "Test Text"
|
||||
assert mock_agent.calls[0][1].user_id == hass_admin_user.id
|
||||
assert mock_agent.calls[0][2] == "test-conv-id"
|
||||
assert mock_agent.calls[0][3] == "test-language"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -22,6 +22,10 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE
|
||||
|
||||
from tests.common import async_capture_events, async_mock_service
|
||||
from tests.components.conversation.conftest import mock_agent
|
||||
|
||||
# To avoid autoflake8 removing the import
|
||||
mock_agent = mock_agent
|
||||
|
||||
|
||||
def encrypt_payload(secret_key, payload, encode_json=True):
|
||||
@ -974,3 +978,42 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client):
|
||||
assert reg_resp.status == HTTPStatus.CREATED
|
||||
entry = ent_reg.async_get("sensor.test_1_battery_state")
|
||||
assert entry.disabled_by is None
|
||||
|
||||
|
||||
async def test_webhook_handle_conversation_process(
|
||||
hass, create_registrations, webhook_client, mock_agent
|
||||
):
|
||||
"""Test that we can converse."""
|
||||
webhook_client.server.app.router._frozen = False
|
||||
|
||||
resp = await webhook_client.post(
|
||||
"/api/webhook/{}".format(create_registrations[1]["webhook_id"]),
|
||||
json={
|
||||
"type": "conversation_process",
|
||||
"data": {
|
||||
"text": "Turn the kitchen light off",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
json = await resp.json()
|
||||
assert json == {
|
||||
"response": {
|
||||
"response_type": "action_done",
|
||||
"card": {},
|
||||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Test response",
|
||||
}
|
||||
},
|
||||
"language": hass.config.language,
|
||||
"data": {
|
||||
"targets": [],
|
||||
"success": [],
|
||||
"failed": [],
|
||||
},
|
||||
},
|
||||
"conversation_id": None,
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user