diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index 9174e8399f3..633b09f987c 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -1,9 +1,16 @@ """Intents for the cover integration.""" -from homeassistant.const import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER + +import voluptuous as vol + +from homeassistant.const import ( + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from . import DOMAIN +from . import ATTR_POSITION, DOMAIN INTENT_OPEN_COVER = "HassOpenCover" INTENT_CLOSE_COVER = "HassCloseCover" @@ -23,3 +30,12 @@ async def async_setup_intents(hass: HomeAssistant) -> None: INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" ), ) + intent.async_register( + hass, + intent.ServiceIntentHandler( + intent.INTENT_SET_POSITION, + DOMAIN, + SERVICE_SET_COVER_POSITION, + extra_slots={ATTR_POSITION: vol.All(vol.Range(min=0, max=100))}, + ), + ) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index e960b5616cb..3c8e1d57d7c 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -20,6 +20,11 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) +from homeassistant.components.valve import ( + DOMAIN as VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, @@ -82,10 +87,10 @@ class IntentPlatformProtocol(Protocol): class OnOffIntentHandler(intent.ServiceIntentHandler): - """Intent handler for on/off that handles covers too.""" + """Intent handler for on/off that also supports covers, valves, locks, etc.""" async def async_call_service(self, intent_obj: intent.Intent, state: State) -> None: - """Call service on entity with special case for covers.""" + """Call service on entity with handling for special cases.""" hass = intent_obj.hass if state.domain == COVER_DOMAIN: @@ -130,6 +135,27 @@ class OnOffIntentHandler(intent.ServiceIntentHandler): ) return + if state.domain == VALVE_DOMAIN: + # on = opened + # off = closed + if self.service == SERVICE_TURN_ON: + service_name = SERVICE_OPEN_VALVE + else: + service_name = SERVICE_CLOSE_VALVE + + await self._run_then_background( + hass.async_create_task( + hass.services.async_call( + VALVE_DOMAIN, + service_name, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + blocking=True, + ) + ) + ) + return + if not hass.services.has_service(state.domain, self.service): raise intent.IntentHandleError( f"Service {self.service} does not support entity {state.entity_id}" diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py new file mode 100644 index 00000000000..b0c0e7f559e --- /dev/null +++ b/homeassistant/components/media_player/intent.py @@ -0,0 +1,50 @@ +"""Intents for the media_player integration.""" + +import voluptuous as vol + +from homeassistant.const import ( + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_VOLUME_SET, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import ATTR_MEDIA_VOLUME_LEVEL, DOMAIN + +INTENT_MEDIA_PAUSE = "HassMediaPause" +INTENT_MEDIA_UNPAUSE = "HassMediaUnpause" +INTENT_MEDIA_NEXT = "HassMediaNext" +INTENT_SET_VOLUME = "HassSetVolume" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the media_player intents.""" + intent.async_register( + hass, + intent.ServiceIntentHandler(INTENT_MEDIA_UNPAUSE, DOMAIN, SERVICE_MEDIA_PLAY), + ) + intent.async_register( + hass, + intent.ServiceIntentHandler(INTENT_MEDIA_PAUSE, DOMAIN, SERVICE_MEDIA_PAUSE), + ) + intent.async_register( + hass, + intent.ServiceIntentHandler( + INTENT_MEDIA_NEXT, DOMAIN, SERVICE_MEDIA_NEXT_TRACK + ), + ) + intent.async_register( + hass, + intent.ServiceIntentHandler( + INTENT_SET_VOLUME, + DOMAIN, + SERVICE_VOLUME_SET, + extra_slots={ + ATTR_MEDIA_VOLUME_LEVEL: vol.All( + vol.Range(min=0, max=100), lambda val: val / 100 + ) + }, + ), + ) diff --git a/homeassistant/components/vacuum/intent.py b/homeassistant/components/vacuum/intent.py new file mode 100644 index 00000000000..c485686aa23 --- /dev/null +++ b/homeassistant/components/vacuum/intent.py @@ -0,0 +1,24 @@ +"""Intents for the vacuum integration.""" + + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START + +INTENT_VACUUM_START = "HassVacuumStart" +INTENT_VACUUM_RETURN_TO_BASE = "HassVacuumReturnToBase" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the vacuum intents.""" + intent.async_register( + hass, + intent.ServiceIntentHandler(INTENT_VACUUM_START, DOMAIN, SERVICE_START), + ) + intent.async_register( + hass, + intent.ServiceIntentHandler( + INTENT_VACUUM_RETURN_TO_BASE, DOMAIN, SERVICE_RETURN_TO_BASE + ), + ) diff --git a/homeassistant/components/valve/intent.py b/homeassistant/components/valve/intent.py new file mode 100644 index 00000000000..1b77bdce343 --- /dev/null +++ b/homeassistant/components/valve/intent.py @@ -0,0 +1,22 @@ +"""Intents for the valve integration.""" + +import voluptuous as vol + +from homeassistant.const import SERVICE_SET_VALVE_POSITION +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import ATTR_POSITION, DOMAIN + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the valve intents.""" + intent.async_register( + hass, + intent.ServiceIntentHandler( + intent.INTENT_SET_POSITION, + DOMAIN, + SERVICE_SET_VALVE_POSITION, + extra_slots={ATTR_POSITION: vol.All(vol.Range(min=0, max=100))}, + ), + ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 5217a55bec5..2fd745c35fa 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -7,6 +7,7 @@ from collections.abc import Collection, Coroutine, Iterable import dataclasses from dataclasses import dataclass from enum import Enum +from functools import cached_property import logging from typing import Any, TypeVar @@ -33,6 +34,7 @@ INTENT_TURN_ON = "HassTurnOn" INTENT_TOGGLE = "HassToggle" INTENT_GET_STATE = "HassGetState" INTENT_NEVERMIND = "HassNevermind" +INTENT_SET_POSITION = "HassSetPosition" SLOT_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) @@ -347,7 +349,6 @@ class IntentHandler: intent_type: str | None = None slot_schema: vol.Schema | None = None - _slot_schema: vol.Schema | None = None platforms: Iterable[str] | None = [] @callback @@ -361,17 +362,20 @@ class IntentHandler: if self.slot_schema is None: return slots - if self._slot_schema is None: - self._slot_schema = vol.Schema( - { - key: SLOT_SCHEMA.extend({"value": validator}) - for key, validator in self.slot_schema.items() - }, - extra=vol.ALLOW_EXTRA, - ) - return self._slot_schema(slots) # type: ignore[no-any-return] + @cached_property + def _slot_schema(self) -> vol.Schema: + """Create validation schema for slots.""" + assert self.slot_schema is not None + return vol.Schema( + { + key: SLOT_SCHEMA.extend({"value": validator}) + for key, validator in self.slot_schema.items() + }, + extra=vol.ALLOW_EXTRA, + ) + async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the intent.""" raise NotImplementedError() @@ -398,13 +402,44 @@ class ServiceIntentHandler(IntentHandler): service_timeout: float = 0.2 def __init__( - self, intent_type: str, domain: str, service: str, speech: str | None = None + self, + intent_type: str, + domain: str, + service: str, + speech: str | None = None, + extra_slots: dict[str, vol.Schema] | None = None, ) -> None: """Create Service Intent Handler.""" self.intent_type = intent_type self.domain = domain self.service = service self.speech = speech + self.extra_slots = extra_slots + + @cached_property + def _slot_schema(self) -> vol.Schema: + """Create validation schema for slots (with extra required slots).""" + if self.slot_schema is None: + raise ValueError("Slot schema is not defined") + + if self.extra_slots: + slot_schema = { + **self.slot_schema, + **{ + vol.Required(key): schema + for key, schema in self.extra_slots.items() + }, + } + else: + slot_schema = self.slot_schema + + return vol.Schema( + { + key: SLOT_SCHEMA.extend({"value": validator}) + for key, validator in slot_schema.items() + }, + extra=vol.ALLOW_EXTRA, + ) async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the hass intent.""" @@ -467,6 +502,9 @@ class ServiceIntentHandler(IntentHandler): area=area_name or area_id, ) + # Update intent slots to include any transformations done by the schemas + intent_obj.slots = slots + response = await self.async_handle_states(intent_obj, states, area) # Make the matched states available in the response @@ -539,12 +577,19 @@ class ServiceIntentHandler(IntentHandler): async def async_call_service(self, intent_obj: Intent, state: State) -> None: """Call service on entity.""" hass = intent_obj.hass + + service_data: dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id} + if self.extra_slots: + service_data.update( + {key: intent_obj.slots[key]["value"] for key in self.extra_slots} + ) + await self._run_then_background( hass.async_create_task( hass.services.async_call( self.domain, self.service, - {ATTR_ENTITY_ID: state.entity_id}, + service_data, context=intent_obj.context, blocking=True, ), diff --git a/tests/components/cover/test_intent.py b/tests/components/cover/test_intent.py index 1de7772c370..7705dc1c5a9 100644 --- a/tests/components/cover/test_intent.py +++ b/tests/components/cover/test_intent.py @@ -1,9 +1,14 @@ """The tests for the cover platform.""" + from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, intent as cover_intent, ) +from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.core import HomeAssistant from homeassistant.helpers import intent @@ -14,37 +19,66 @@ async def test_open_cover_intent(hass: HomeAssistant) -> None: """Test HassOpenCover intent.""" await cover_intent.async_setup_intents(hass) - hass.states.async_set("cover.garage_door", "closed") - calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + hass.states.async_set(f"{DOMAIN}.garage_door", STATE_CLOSED) + calls = async_mock_service(hass, DOMAIN, SERVICE_OPEN_COVER) response = await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + hass, "test", cover_intent.INTENT_OPEN_COVER, {"name": {"value": "garage door"}} ) await hass.async_block_till_done() assert response.speech["plain"]["speech"] == "Opened garage door" assert len(calls) == 1 call = calls[0] - assert call.domain == "cover" - assert call.service == "open_cover" - assert call.data == {"entity_id": "cover.garage_door"} + assert call.domain == DOMAIN + assert call.service == SERVICE_OPEN_COVER + assert call.data == {"entity_id": f"{DOMAIN}.garage_door"} async def test_close_cover_intent(hass: HomeAssistant) -> None: """Test HassCloseCover intent.""" await cover_intent.async_setup_intents(hass) - hass.states.async_set("cover.garage_door", "open") - calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) + hass.states.async_set(f"{DOMAIN}.garage_door", STATE_OPEN) + calls = async_mock_service(hass, DOMAIN, SERVICE_CLOSE_COVER) response = await intent.async_handle( - hass, "test", "HassCloseCover", {"name": {"value": "garage door"}} + hass, + "test", + cover_intent.INTENT_CLOSE_COVER, + {"name": {"value": "garage door"}}, ) await hass.async_block_till_done() assert response.speech["plain"]["speech"] == "Closed garage door" assert len(calls) == 1 call = calls[0] - assert call.domain == "cover" - assert call.service == "close_cover" - assert call.data == {"entity_id": "cover.garage_door"} + assert call.domain == DOMAIN + assert call.service == SERVICE_CLOSE_COVER + assert call.data == {"entity_id": f"{DOMAIN}.garage_door"} + + +async def test_set_cover_position(hass: HomeAssistant) -> None: + """Test HassSetPosition intent for covers.""" + await cover_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_cover" + hass.states.async_set( + entity_id, STATE_CLOSED, attributes={ATTR_CURRENT_POSITION: 0} + ) + calls = async_mock_service(hass, DOMAIN, SERVICE_SET_COVER_POSITION) + + response = await intent.async_handle( + hass, + "test", + intent.INTENT_SET_POSITION, + {"name": {"value": "test cover"}, "position": {"value": 50}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_SET_COVER_POSITION + assert call.data == {"entity_id": entity_id, "position": 50} diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py new file mode 100644 index 00000000000..b0ea7fe8e94 --- /dev/null +++ b/tests/components/media_player/test_intent.py @@ -0,0 +1,111 @@ +"""The tests for the media_player platform.""" + +from homeassistant.components.media_player import ( + DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_VOLUME_SET, + intent as media_player_intent, +) +from homeassistant.const import STATE_IDLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from tests.common import async_mock_service + + +async def test_pause_media_player_intent(hass: HomeAssistant) -> None: + """Test HassMediaPause intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + hass.states.async_set(entity_id, STATE_IDLE) + calls = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PAUSE) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_PAUSE, + {"name": {"value": "test media player"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_MEDIA_PAUSE + assert call.data == {"entity_id": entity_id} + + +async def test_unpause_media_player_intent(hass: HomeAssistant) -> None: + """Test HassMediaUnpause intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + hass.states.async_set(entity_id, STATE_IDLE) + calls = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PLAY) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_UNPAUSE, + {"name": {"value": "test media player"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_MEDIA_PLAY + assert call.data == {"entity_id": entity_id} + + +async def test_next_media_player_intent(hass: HomeAssistant) -> None: + """Test HassMediaNext intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + hass.states.async_set(entity_id, STATE_IDLE) + calls = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_NEXT_TRACK) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_NEXT, + {"name": {"value": "test media player"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_MEDIA_NEXT_TRACK + assert call.data == {"entity_id": entity_id} + + +async def test_volume_media_player_intent(hass: HomeAssistant) -> None: + """Test HassSetVolume intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + hass.states.async_set(entity_id, STATE_IDLE) + calls = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_SET) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME, + {"name": {"value": "test media player"}, "volume_level": {"value": 50}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_VOLUME_SET + assert call.data == {"entity_id": entity_id, "volume_level": 0.5} diff --git a/tests/components/vacuum/test_intent.py b/tests/components/vacuum/test_intent.py new file mode 100644 index 00000000000..cf96d32ad49 --- /dev/null +++ b/tests/components/vacuum/test_intent.py @@ -0,0 +1,61 @@ +"""The tests for the vacuum platform.""" + +from homeassistant.components.vacuum import ( + DOMAIN, + SERVICE_RETURN_TO_BASE, + SERVICE_START, + intent as vacuum_intent, +) +from homeassistant.const import STATE_IDLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from tests.common import async_mock_service + + +async def test_start_vacuum_intent(hass: HomeAssistant) -> None: + """Test HassTurnOn intent for vacuums.""" + await vacuum_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_vacuum" + hass.states.async_set(entity_id, STATE_IDLE) + calls = async_mock_service(hass, DOMAIN, SERVICE_START) + + response = await intent.async_handle( + hass, + "test", + vacuum_intent.INTENT_VACUUM_START, + {"name": {"value": "test vacuum"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_START + assert call.data == {"entity_id": entity_id} + + +async def test_stop_vacuum_intent(hass: HomeAssistant) -> None: + """Test HassTurnOff intent for vacuums.""" + await vacuum_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_vacuum" + hass.states.async_set(entity_id, STATE_IDLE) + calls = async_mock_service(hass, DOMAIN, SERVICE_RETURN_TO_BASE) + + response = await intent.async_handle( + hass, + "test", + vacuum_intent.INTENT_VACUUM_RETURN_TO_BASE, + {"name": {"value": "test vacuum"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_RETURN_TO_BASE + assert call.data == {"entity_id": entity_id} diff --git a/tests/components/valve/test_intent.py b/tests/components/valve/test_intent.py new file mode 100644 index 00000000000..049bb21c722 --- /dev/null +++ b/tests/components/valve/test_intent.py @@ -0,0 +1,84 @@ +"""The tests for the valve platform.""" + +from homeassistant.components.valve import ( + ATTR_CURRENT_POSITION, + DOMAIN, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + SERVICE_SET_VALVE_POSITION, + intent as valve_intent, +) +from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +async def test_open_valve_intent(hass: HomeAssistant) -> None: + """Test HassTurnOn intent for valves.""" + assert await async_setup_component(hass, "intent", {}) + + entity_id = f"{DOMAIN}.test_valve" + hass.states.async_set(entity_id, STATE_CLOSED) + calls = async_mock_service(hass, DOMAIN, SERVICE_OPEN_VALVE) + + response = await intent.async_handle( + hass, "test", intent.INTENT_TURN_ON, {"name": {"value": "test valve"}} + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_OPEN_VALVE + assert call.data == {"entity_id": entity_id} + + +async def test_close_valve_intent(hass: HomeAssistant) -> None: + """Test HassTurnOff intent for valves.""" + assert await async_setup_component(hass, "intent", {}) + + entity_id = f"{DOMAIN}.test_valve" + hass.states.async_set(entity_id, STATE_OPEN) + calls = async_mock_service(hass, DOMAIN, SERVICE_CLOSE_VALVE) + + response = await intent.async_handle( + hass, "test", intent.INTENT_TURN_OFF, {"name": {"value": "test valve"}} + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_CLOSE_VALVE + assert call.data == {"entity_id": entity_id} + + +async def test_set_valve_position(hass: HomeAssistant) -> None: + """Test HassSetPosition intent for valves.""" + await valve_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_valve" + hass.states.async_set( + entity_id, STATE_CLOSED, attributes={ATTR_CURRENT_POSITION: 0} + ) + calls = async_mock_service(hass, DOMAIN, SERVICE_SET_VALVE_POSITION) + + response = await intent.async_handle( + hass, + "test", + intent.INTENT_SET_POSITION, + {"name": {"value": "test valve"}, "position": {"value": 50}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_SET_VALVE_POSITION + assert call.data == {"entity_id": entity_id, "position": 50} diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 0486211417c..1bc01c28cf2 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -1,4 +1,5 @@ """Tests for the intent helpers.""" + import asyncio from unittest.mock import MagicMock, patch @@ -176,6 +177,14 @@ def test_async_validate_slots() -> None: ) +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"} + } + + async def test_cant_turn_on_lock(hass: HomeAssistant) -> None: """Test that we can't turn on entities that don't support it.""" assert await async_setup_component(hass, "homeassistant", {})