Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Hansen
cd4db314cd Implement suggestions 2026-03-03 11:58:18 -06:00
Michael Hansen
5b2e0e2a4f Add missing parameters from handle API 2026-03-03 11:27:30 -06:00
2 changed files with 138 additions and 7 deletions

View File

@@ -627,13 +627,17 @@ class IntentHandleView(http.HomeAssistantView):
{
vol.Required("name"): cv.string,
vol.Optional("data"): vol.Schema({cv.string: object}),
vol.Optional("language"): cv.string,
vol.Optional("assistant"): vol.Any(cv.string, None),
vol.Optional("device_id"): vol.Any(cv.string, None),
vol.Optional("satellite_id"): vol.Any(cv.string, None),
}
)
)
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle intent with name/data."""
hass = request.app[http.KEY_HASS]
language = hass.config.language
language = data.get("language", hass.config.language)
try:
intent_name = data["name"]
@@ -641,14 +645,21 @@ class IntentHandleView(http.HomeAssistantView):
key: {"value": value} for key, value in data.get("data", {}).items()
}
intent_result = await intent.async_handle(
hass, DOMAIN, intent_name, slots, "", self.context(request)
hass,
DOMAIN,
intent_name,
slots,
"",
self.context(request),
language=language,
assistant=data.get("assistant"),
device_id=data.get("device_id"),
satellite_id=data.get("satellite_id"),
)
except (intent.IntentHandleError, intent.MatchFailedError) as err:
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(str(err))
if intent_result is None:
intent_result = intent.IntentResponse(language=language) # type: ignore[unreachable]
intent_result.async_set_speech("Sorry, I couldn't handle that")
intent_result.async_set_error(
intent.IntentResponseErrorCode.FAILED_TO_HANDLE, str(err)
)
return self.json(intent_result)

View File

@@ -4,6 +4,7 @@ from typing import Any
import pytest
from homeassistant.components import conversation
from homeassistant.components.button import SERVICE_PRESS
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
@@ -12,6 +13,7 @@ from homeassistant.components.cover import (
SERVICE_STOP_COVER,
CoverState,
)
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
from homeassistant.components.valve import (
DOMAIN as VALVE_DOMAIN,
@@ -82,6 +84,70 @@ async def test_http_handle_intent(
}
},
"language": hass.config.language,
"response_type": intent.IntentResponseType.ACTION_DONE.value,
"data": {"targets": [], "success": [], "failed": []},
}
async def test_http_language_device_satellite_id(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
) -> None:
"""Test handle intent with language, device id, and satellite id."""
device_id = "test-device-id"
satellite_id = "test-satellite-id"
language = "en-GB"
class TestIntentHandler(intent.IntentHandler):
"""Test Intent Handler."""
intent_type = "TestIntent"
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
assert intent_obj.context.user_id == hass_admin_user.id
assert intent_obj.device_id == device_id
assert intent_obj.satellite_id == satellite_id
assert intent_obj.language == language
response = intent_obj.create_response()
response.async_set_speech("Test response")
response.async_set_speech_slots({"slot1": "value 1", "slot2": 2})
return response
intent.async_register(hass, TestIntentHandler())
result = await async_setup_component(hass, "intent", {})
assert result
client = await hass_client()
resp = await client.post(
"/api/intent/handle",
json={
"name": "TestIntent",
"language": language,
"device_id": device_id,
"satellite_id": satellite_id,
},
)
assert resp.status == 200
data = await resp.json()
# Verify language, device id, and satellite id were passed through.
# Also check speech slots.
assert data == {
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Test response",
}
},
"speech_slots": {
"slot1": "value 1",
"slot2": 2,
},
"language": language,
"response_type": "action_done",
"data": {"targets": [], "success": [], "failed": []},
}
@@ -113,6 +179,60 @@ async def test_http_handle_intent_match_failure(
assert "DUPLICATE_NAME" in data["speech"]["plain"]["speech"]
async def test_http_assistant(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
) -> None:
"""Test handle intent only targets exposed entities with 'assistant' set."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "intent", {})
hass.states.async_set(
"cover.garage_door_1", "closed", {ATTR_FRIENDLY_NAME: "Garage Door 1"}
)
async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
client = await hass_client()
# Exposed
async_expose_entity(hass, conversation.DOMAIN, "cover.garage_door_1", True)
resp = await client.post(
"/api/intent/handle",
json={
"name": "HassTurnOn",
"data": {"name": "Garage Door 1"},
"assistant": conversation.DOMAIN,
},
)
assert resp.status == 200
data = await resp.json()
assert data["response_type"] == intent.IntentResponseType.ACTION_DONE.value
# Not exposed
async_expose_entity(hass, conversation.DOMAIN, "cover.garage_door_1", False)
resp = await client.post(
"/api/intent/handle",
json={
"name": "HassTurnOn",
"data": {"name": "Garage Door 1"},
"assistant": conversation.DOMAIN,
},
)
assert resp.status == 200
data = await resp.json()
assert data["response_type"] == intent.IntentResponseType.ERROR.value
assert data["data"]["code"] == intent.IntentResponseErrorCode.FAILED_TO_HANDLE.value
# No assistant (exposure is irrelevant)
resp = await client.post(
"/api/intent/handle",
json={"name": "HassTurnOn", "data": {"name": "Garage Door 1"}},
)
assert resp.status == 200
data = await resp.json()
assert data["response_type"] == intent.IntentResponseType.ACTION_DONE.value
async def test_cover_intents_loading(hass: HomeAssistant) -> None:
"""Test Cover Intents Loading."""
assert await async_setup_component(hass, "intent", {})