2018-02-28 21:39:01 +00:00
|
|
|
"""Tests for the intent helpers."""
|
2024-02-20 03:28:42 +00:00
|
|
|
|
2023-06-16 14:01:40 +00:00
|
|
|
import asyncio
|
2023-06-03 18:02:23 +00:00
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
2019-04-30 16:20:38 +00:00
|
|
|
import pytest
|
2019-12-09 15:52:24 +00:00
|
|
|
import voluptuous as vol
|
2019-04-30 16:20:38 +00:00
|
|
|
|
2023-02-16 19:01:41 +00:00
|
|
|
from homeassistant.components import conversation
|
2023-01-19 23:15:01 +00:00
|
|
|
from homeassistant.components.switch import SwitchDeviceClass
|
2023-01-07 21:20:21 +00:00
|
|
|
from homeassistant.const import ATTR_FRIENDLY_NAME
|
2023-02-16 19:01:41 +00:00
|
|
|
from homeassistant.core import Context, HomeAssistant, State
|
2023-01-19 23:15:01 +00:00
|
|
|
from homeassistant.helpers import (
|
2023-03-01 15:24:31 +00:00
|
|
|
area_registry as ar,
|
2023-01-19 23:15:01 +00:00
|
|
|
config_validation as cv,
|
2023-03-01 15:24:31 +00:00
|
|
|
device_registry as dr,
|
|
|
|
entity_registry as er,
|
2024-03-30 20:59:20 +00:00
|
|
|
floor_registry as fr,
|
2023-01-19 23:15:01 +00:00
|
|
|
intent,
|
|
|
|
)
|
2023-02-16 19:01:41 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
2018-07-01 15:51:40 +00:00
|
|
|
|
2023-08-10 17:28:16 +00:00
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
|
2018-07-01 15:51:40 +00:00
|
|
|
|
|
|
|
class MockIntentHandler(intent.IntentHandler):
|
|
|
|
"""Provide a mock intent handler."""
|
|
|
|
|
|
|
|
def __init__(self, slot_schema):
|
|
|
|
"""Initialize the mock handler."""
|
|
|
|
self.slot_schema = slot_schema
|
2018-02-28 21:39:01 +00:00
|
|
|
|
|
|
|
|
2023-03-01 15:24:31 +00:00
|
|
|
async def test_async_match_states(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
area_registry: ar.AreaRegistry,
|
|
|
|
entity_registry: er.EntityRegistry,
|
2024-03-30 20:59:20 +00:00
|
|
|
floor_registry: fr.FloorRegistry,
|
2023-03-01 15:24:31 +00:00
|
|
|
) -> None:
|
2018-02-28 21:39:01 +00:00
|
|
|
"""Test async_match_state helper."""
|
2023-03-01 15:24:31 +00:00
|
|
|
area_kitchen = area_registry.async_get_or_create("kitchen")
|
2024-03-30 20:59:20 +00:00
|
|
|
area_kitchen = area_registry.async_update(area_kitchen.id, aliases={"food room"})
|
2023-03-01 15:24:31 +00:00
|
|
|
area_bedroom = area_registry.async_get_or_create("bedroom")
|
2023-01-19 23:15:01 +00:00
|
|
|
|
2024-03-30 20:59:20 +00:00
|
|
|
# Kitchen is on the first floor
|
|
|
|
floor_1 = floor_registry.async_create("first floor", aliases={"ground floor"})
|
|
|
|
area_kitchen = area_registry.async_update(
|
|
|
|
area_kitchen.id, floor_id=floor_1.floor_id
|
|
|
|
)
|
|
|
|
|
|
|
|
# Bedroom is on the second floor
|
|
|
|
floor_2 = floor_registry.async_create("second floor")
|
|
|
|
area_bedroom = area_registry.async_update(
|
|
|
|
area_bedroom.id, floor_id=floor_2.floor_id
|
|
|
|
)
|
|
|
|
|
2023-01-07 21:20:21 +00:00
|
|
|
state1 = State(
|
|
|
|
"light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
|
|
|
|
)
|
|
|
|
state2 = State(
|
2023-01-19 23:15:01 +00:00
|
|
|
"switch.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom switch"}
|
|
|
|
)
|
|
|
|
|
|
|
|
# Put entities into different areas
|
2023-03-01 15:24:31 +00:00
|
|
|
entity_registry.async_get_or_create(
|
|
|
|
"light", "demo", "1234", suggested_object_id="kitchen"
|
|
|
|
)
|
|
|
|
entity_registry.async_update_entity(state1.entity_id, area_id=area_kitchen.id)
|
2023-01-19 23:15:01 +00:00
|
|
|
|
2023-03-01 15:24:31 +00:00
|
|
|
entity_registry.async_get_or_create(
|
2023-01-26 15:48:49 +00:00
|
|
|
"switch", "demo", "5678", suggested_object_id="bedroom"
|
2023-01-19 23:15:01 +00:00
|
|
|
)
|
2023-03-01 15:24:31 +00:00
|
|
|
entity_registry.async_update_entity(
|
2023-01-19 23:15:01 +00:00
|
|
|
state2.entity_id,
|
|
|
|
area_id=area_bedroom.id,
|
|
|
|
device_class=SwitchDeviceClass.OUTLET,
|
|
|
|
aliases={"kill switch"},
|
|
|
|
)
|
|
|
|
|
|
|
|
# Match on name
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-19 23:15:01 +00:00
|
|
|
intent.async_match_states(hass, name="kitchen light", states=[state1, state2])
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state1]
|
2023-01-19 23:15:01 +00:00
|
|
|
|
|
|
|
# Test alias
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-19 23:15:01 +00:00
|
|
|
intent.async_match_states(hass, name="kill switch", states=[state1, state2])
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state2]
|
2023-01-19 23:15:01 +00:00
|
|
|
|
|
|
|
# Name + area
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-19 23:15:01 +00:00
|
|
|
intent.async_match_states(
|
|
|
|
hass, name="kitchen light", area_name="kitchen", states=[state1, state2]
|
|
|
|
)
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state1]
|
2018-02-28 21:39:01 +00:00
|
|
|
|
2023-01-31 04:46:25 +00:00
|
|
|
# Test area alias
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-31 04:46:25 +00:00
|
|
|
intent.async_match_states(
|
|
|
|
hass, name="kitchen light", area_name="food room", states=[state1, state2]
|
|
|
|
)
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state1]
|
2023-01-31 04:46:25 +00:00
|
|
|
|
2023-01-19 23:15:01 +00:00
|
|
|
# Wrong area
|
|
|
|
assert not list(
|
|
|
|
intent.async_match_states(
|
|
|
|
hass, name="kitchen light", area_name="bedroom", states=[state1, state2]
|
|
|
|
)
|
|
|
|
)
|
2018-07-01 15:51:40 +00:00
|
|
|
|
2024-03-30 20:59:20 +00:00
|
|
|
# Invalid area
|
|
|
|
assert not list(
|
|
|
|
intent.async_match_states(
|
|
|
|
hass, area_name="invalid area", states=[state1, state2]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-01-19 23:15:01 +00:00
|
|
|
# Domain + area
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-19 23:15:01 +00:00
|
|
|
intent.async_match_states(
|
|
|
|
hass, domains={"switch"}, area_name="bedroom", states=[state1, state2]
|
|
|
|
)
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state2]
|
2023-01-19 23:15:01 +00:00
|
|
|
|
|
|
|
# Device class + area
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-19 23:15:01 +00:00
|
|
|
intent.async_match_states(
|
|
|
|
hass,
|
|
|
|
device_classes={SwitchDeviceClass.OUTLET},
|
|
|
|
area_name="bedroom",
|
|
|
|
states=[state1, state2],
|
|
|
|
)
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state2]
|
2023-01-07 21:20:21 +00:00
|
|
|
|
2024-03-30 20:59:20 +00:00
|
|
|
# Floor
|
|
|
|
assert list(
|
|
|
|
intent.async_match_states(
|
|
|
|
hass, floor_name="first floor", states=[state1, state2]
|
|
|
|
)
|
|
|
|
) == [state1]
|
|
|
|
|
|
|
|
assert list(
|
|
|
|
intent.async_match_states(
|
|
|
|
# Check alias
|
|
|
|
hass,
|
|
|
|
floor_name="ground floor",
|
|
|
|
states=[state1, state2],
|
|
|
|
)
|
|
|
|
) == [state1]
|
|
|
|
|
|
|
|
assert list(
|
|
|
|
intent.async_match_states(
|
|
|
|
hass, floor_name="second floor", states=[state1, state2]
|
|
|
|
)
|
|
|
|
) == [state2]
|
|
|
|
|
|
|
|
# Invalid floor
|
|
|
|
assert not list(
|
|
|
|
intent.async_match_states(
|
|
|
|
hass, floor_name="invalid floor", states=[state1, state2]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2018-07-01 15:51:40 +00:00
|
|
|
|
2023-03-01 15:24:31 +00:00
|
|
|
async def test_match_device_area(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
area_registry: ar.AreaRegistry,
|
|
|
|
device_registry: dr.DeviceRegistry,
|
|
|
|
entity_registry: er.EntityRegistry,
|
|
|
|
) -> None:
|
2023-01-26 15:48:49 +00:00
|
|
|
"""Test async_match_state with a device in an area."""
|
2023-08-10 17:28:16 +00:00
|
|
|
config_entry = MockConfigEntry()
|
|
|
|
config_entry.add_to_hass(hass)
|
2023-03-01 15:24:31 +00:00
|
|
|
area_kitchen = area_registry.async_get_or_create("kitchen")
|
|
|
|
area_bedroom = area_registry.async_get_or_create("bedroom")
|
2023-01-26 15:48:49 +00:00
|
|
|
|
2023-03-01 15:24:31 +00:00
|
|
|
kitchen_device = device_registry.async_get_or_create(
|
2023-08-10 17:28:16 +00:00
|
|
|
config_entry_id=config_entry.entry_id,
|
|
|
|
connections=set(),
|
|
|
|
identifiers={("demo", "id-1234")},
|
2023-01-26 15:48:49 +00:00
|
|
|
)
|
2023-03-01 15:24:31 +00:00
|
|
|
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
|
2023-01-26 15:48:49 +00:00
|
|
|
|
|
|
|
state1 = State(
|
|
|
|
"light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"}
|
|
|
|
)
|
|
|
|
state2 = State(
|
|
|
|
"light.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"}
|
|
|
|
)
|
|
|
|
state3 = State(
|
|
|
|
"light.living_room", "on", attributes={ATTR_FRIENDLY_NAME: "living room light"}
|
|
|
|
)
|
2023-03-01 15:24:31 +00:00
|
|
|
entity_registry.async_get_or_create(
|
|
|
|
"light", "demo", "1234", suggested_object_id="kitchen"
|
|
|
|
)
|
|
|
|
entity_registry.async_update_entity(state1.entity_id, device_id=kitchen_device.id)
|
2023-01-26 15:48:49 +00:00
|
|
|
|
2023-03-01 15:24:31 +00:00
|
|
|
entity_registry.async_get_or_create(
|
|
|
|
"light", "demo", "5678", suggested_object_id="bedroom"
|
|
|
|
)
|
|
|
|
entity_registry.async_update_entity(state2.entity_id, area_id=area_bedroom.id)
|
2023-01-26 15:48:49 +00:00
|
|
|
|
|
|
|
# Match on area/domain
|
2023-01-31 16:59:00 +00:00
|
|
|
assert list(
|
2023-01-26 15:48:49 +00:00
|
|
|
intent.async_match_states(
|
|
|
|
hass,
|
|
|
|
domains={"light"},
|
|
|
|
area_name="kitchen",
|
|
|
|
states=[state1, state2, state3],
|
|
|
|
)
|
2023-01-31 16:59:00 +00:00
|
|
|
) == [state1]
|
2023-01-26 15:48:49 +00:00
|
|
|
|
|
|
|
|
2023-02-07 13:20:06 +00:00
|
|
|
def test_async_validate_slots() -> None:
|
2019-04-30 16:20:38 +00:00
|
|
|
"""Test async_validate_slots of IntentHandler."""
|
2019-07-31 19:25:30 +00:00
|
|
|
handler1 = MockIntentHandler({vol.Required("name"): cv.string})
|
2019-04-30 16:20:38 +00:00
|
|
|
|
|
|
|
with pytest.raises(vol.error.MultipleInvalid):
|
|
|
|
handler1.async_validate_slots({})
|
|
|
|
with pytest.raises(vol.error.MultipleInvalid):
|
2019-07-31 19:25:30 +00:00
|
|
|
handler1.async_validate_slots({"name": 1})
|
2019-04-30 16:20:38 +00:00
|
|
|
with pytest.raises(vol.error.MultipleInvalid):
|
2019-07-31 19:25:30 +00:00
|
|
|
handler1.async_validate_slots({"name": "kitchen"})
|
|
|
|
handler1.async_validate_slots({"name": {"value": "kitchen"}})
|
|
|
|
handler1.async_validate_slots(
|
|
|
|
{"name": {"value": "kitchen"}, "probability": {"value": "0.5"}}
|
|
|
|
)
|
2023-02-16 19:01:41 +00:00
|
|
|
|
|
|
|
|
2024-02-20 03:28:42 +00:00
|
|
|
def test_async_validate_slots_no_schema() -> None:
|
|
|
|
"""Test async_validate_slots of IntentHandler with no schema."""
|
|
|
|
handler1 = MockIntentHandler(None)
|
|
|
|
assert handler1.async_validate_slots({"name": {"value": "kitchen"}}) == {
|
|
|
|
"name": {"value": "kitchen"}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-02-22 15:36:42 +00:00
|
|
|
async def test_cant_turn_on_lock(hass: HomeAssistant) -> None:
|
2023-02-17 21:19:22 +00:00
|
|
|
"""Test that we can't turn on entities that don't support it."""
|
2023-02-16 19:01:41 +00:00
|
|
|
assert await async_setup_component(hass, "homeassistant", {})
|
|
|
|
assert await async_setup_component(hass, "conversation", {})
|
|
|
|
assert await async_setup_component(hass, "intent", {})
|
2023-02-22 15:36:42 +00:00
|
|
|
assert await async_setup_component(hass, "lock", {})
|
2023-02-17 21:19:22 +00:00
|
|
|
|
|
|
|
hass.states.async_set(
|
2023-02-22 15:36:42 +00:00
|
|
|
"lock.test", "123", attributes={ATTR_FRIENDLY_NAME: "Test Lock"}
|
2023-02-17 21:19:22 +00:00
|
|
|
)
|
|
|
|
|
2023-02-16 19:01:41 +00:00
|
|
|
result = await conversation.async_converse(
|
2023-02-22 15:36:42 +00:00
|
|
|
hass, "turn on test lock", None, Context(), None
|
2023-02-16 19:01:41 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
2024-01-04 23:09:20 +00:00
|
|
|
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
2023-06-03 18:02:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_async_register(hass: HomeAssistant) -> None:
|
|
|
|
"""Test registering an intent and verifying it is stored correctly."""
|
|
|
|
handler = MagicMock()
|
|
|
|
handler.intent_type = "test_intent"
|
|
|
|
|
|
|
|
intent.async_register(hass, handler)
|
|
|
|
|
|
|
|
assert hass.data[intent.DATA_KEY]["test_intent"] == handler
|
|
|
|
|
|
|
|
|
|
|
|
def test_async_register_overwrite(hass: HomeAssistant) -> None:
|
|
|
|
"""Test registering multiple intents with the same type, ensuring the last one overwrites the previous one and a warning is emitted."""
|
|
|
|
handler1 = MagicMock()
|
|
|
|
handler1.intent_type = "test_intent"
|
|
|
|
|
|
|
|
handler2 = MagicMock()
|
|
|
|
handler2.intent_type = "test_intent"
|
|
|
|
|
|
|
|
with patch.object(intent._LOGGER, "warning") as mock_warning:
|
|
|
|
intent.async_register(hass, handler1)
|
|
|
|
intent.async_register(hass, handler2)
|
|
|
|
|
|
|
|
mock_warning.assert_called_once_with(
|
|
|
|
"Intent %s is being overwritten by %s", "test_intent", handler2
|
|
|
|
)
|
|
|
|
|
|
|
|
assert hass.data[intent.DATA_KEY]["test_intent"] == handler2
|
|
|
|
|
|
|
|
|
|
|
|
def test_async_remove(hass: HomeAssistant) -> None:
|
|
|
|
"""Test removing an intent and verifying it is no longer present in the Home Assistant data."""
|
|
|
|
handler = MagicMock()
|
|
|
|
handler.intent_type = "test_intent"
|
|
|
|
|
|
|
|
intent.async_register(hass, handler)
|
|
|
|
intent.async_remove(hass, "test_intent")
|
|
|
|
|
|
|
|
assert "test_intent" not in hass.data[intent.DATA_KEY]
|
|
|
|
|
|
|
|
|
|
|
|
def test_async_remove_no_existing_entry(hass: HomeAssistant) -> None:
|
|
|
|
"""Test the removal of a non-existing intent from Home Assistant's data."""
|
|
|
|
handler = MagicMock()
|
|
|
|
handler.intent_type = "test_intent"
|
|
|
|
intent.async_register(hass, handler)
|
|
|
|
|
|
|
|
intent.async_remove(hass, "test_intent2")
|
|
|
|
|
|
|
|
assert "test_intent2" not in hass.data[intent.DATA_KEY]
|
|
|
|
|
|
|
|
|
|
|
|
def test_async_remove_no_existing(hass: HomeAssistant) -> None:
|
|
|
|
"""Test the removal of an intent where no config exists."""
|
|
|
|
|
|
|
|
intent.async_remove(hass, "test_intent2")
|
|
|
|
# simply shouldn't cause an exception
|
|
|
|
|
|
|
|
assert intent.DATA_KEY not in hass.data
|
2023-06-16 14:01:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_validate_then_run_in_background(hass: HomeAssistant) -> None:
|
|
|
|
"""Test we don't execute a service in foreground forever."""
|
|
|
|
hass.states.async_set("light.kitchen", "off")
|
|
|
|
call_done = asyncio.Event()
|
|
|
|
calls = []
|
|
|
|
|
|
|
|
# Register a service that takes 0.1 seconds to execute
|
|
|
|
async def mock_service(call):
|
|
|
|
"""Mock service."""
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
call_done.set()
|
|
|
|
calls.append(call)
|
|
|
|
|
|
|
|
hass.services.async_register("light", "turn_on", mock_service)
|
|
|
|
|
|
|
|
# Create intent handler with a service timeout of 0.05 seconds
|
|
|
|
handler = intent.ServiceIntentHandler(
|
|
|
|
"TestType", "light", "turn_on", "Turned {} on"
|
|
|
|
)
|
|
|
|
handler.service_timeout = 0.05
|
|
|
|
intent.async_register(hass, handler)
|
|
|
|
|
|
|
|
result = await intent.async_handle(
|
|
|
|
hass,
|
|
|
|
"test",
|
|
|
|
"TestType",
|
|
|
|
slots={"name": {"value": "kitchen"}},
|
|
|
|
)
|
|
|
|
|
|
|
|
assert result.response_type == intent.IntentResponseType.ACTION_DONE
|
|
|
|
|
|
|
|
assert not call_done.is_set()
|
|
|
|
await call_done.wait()
|
|
|
|
|
|
|
|
assert len(calls) == 1
|
|
|
|
assert calls[0].data == {"entity_id": "light.kitchen"}
|
2024-03-30 20:59:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_invalid_area_floor_names(hass: HomeAssistant) -> None:
|
|
|
|
"""Test that we throw an intent handle error with invalid area/floor names."""
|
|
|
|
handler = intent.ServiceIntentHandler(
|
|
|
|
"TestType", "light", "turn_on", "Turned {} on"
|
|
|
|
)
|
|
|
|
intent.async_register(hass, handler)
|
|
|
|
|
|
|
|
with pytest.raises(intent.IntentHandleError):
|
|
|
|
await intent.async_handle(
|
|
|
|
hass,
|
|
|
|
"test",
|
|
|
|
"TestType",
|
|
|
|
slots={"area": {"value": "invalid area"}},
|
|
|
|
)
|
|
|
|
|
|
|
|
with pytest.raises(intent.IntentHandleError):
|
|
|
|
await intent.async_handle(
|
|
|
|
hass,
|
|
|
|
"test",
|
|
|
|
"TestType",
|
|
|
|
slots={"floor": {"value": "invalid floor"}},
|
|
|
|
)
|