Skip to content

Commit c9351a0

Browse files
Add HassStopMoving intent for covers and valves (home-assistant#155267)
Co-authored-by: Artur Pragacz <[email protected]>
1 parent 4e8a31a commit c9351a0

File tree

4 files changed

+109
-2
lines changed

4 files changed

+109
-2
lines changed

homeassistant/components/intent/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
SERVICE_CLOSE_COVER,
2323
SERVICE_OPEN_COVER,
2424
SERVICE_SET_COVER_POSITION,
25+
SERVICE_STOP_COVER,
2526
CoverDeviceClass,
2627
)
2728
from homeassistant.components.http.data_validator import RequestDataValidator
@@ -38,6 +39,7 @@
3839
SERVICE_CLOSE_VALVE,
3940
SERVICE_OPEN_VALVE,
4041
SERVICE_SET_VALVE_POSITION,
42+
SERVICE_STOP_VALVE,
4143
ValveDeviceClass,
4244
)
4345
from homeassistant.const import (
@@ -143,6 +145,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
143145
NevermindIntentHandler(),
144146
)
145147
intent.async_register(hass, SetPositionIntentHandler())
148+
intent.async_register(hass, StopMovingIntentHandler())
146149
intent.async_register(hass, StartTimerIntentHandler())
147150
intent.async_register(hass, CancelTimerIntentHandler())
148151
intent.async_register(hass, CancelAllTimersIntentHandler())
@@ -433,6 +436,31 @@ def get_domain_and_service(
433436
raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
434437

435438

439+
class StopMovingIntentHandler(intent.DynamicServiceIntentHandler):
440+
"""Intent handler for stopping covers and valves."""
441+
442+
def __init__(self) -> None:
443+
"""Create stop moving handler."""
444+
super().__init__(
445+
intent.INTENT_STOP_MOVING,
446+
description="Stops a moving device or entity",
447+
platforms={COVER_DOMAIN, VALVE_DOMAIN},
448+
device_classes={CoverDeviceClass, ValveDeviceClass},
449+
)
450+
451+
def get_domain_and_service(
452+
self, intent_obj: intent.Intent, state: State
453+
) -> tuple[str, str]:
454+
"""Get the domain and service name to call."""
455+
if state.domain == COVER_DOMAIN:
456+
return (COVER_DOMAIN, SERVICE_STOP_COVER)
457+
458+
if state.domain == VALVE_DOMAIN:
459+
return (VALVE_DOMAIN, SERVICE_STOP_VALVE)
460+
461+
raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
462+
463+
436464
class GetCurrentDateIntentHandler(intent.IntentHandler):
437465
"""Gets the current date."""
438466

homeassistant/helpers/intent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
INTENT_GET_STATE = "HassGetState"
4949
INTENT_NEVERMIND = "HassNevermind"
5050
INTENT_SET_POSITION = "HassSetPosition"
51+
INTENT_STOP_MOVING = "HassStopMoving"
5152
INTENT_START_TIMER = "HassStartTimer"
5253
INTENT_CANCEL_TIMER = "HassCancelTimer"
5354
INTENT_CANCEL_ALL_TIMERS = "HassCancelAllTimers"

tests/components/intent/test_init.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
"""Tests for Intent component."""
22

3+
from typing import Any
4+
35
import pytest
46

57
from homeassistant.components.button import SERVICE_PRESS
6-
from homeassistant.components.cover import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER
8+
from homeassistant.components.cover import (
9+
DOMAIN as COVER_DOMAIN,
10+
SERVICE_CLOSE_COVER,
11+
SERVICE_OPEN_COVER,
12+
SERVICE_STOP_COVER,
13+
CoverState,
14+
)
715
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
8-
from homeassistant.components.valve import SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE
16+
from homeassistant.components.valve import (
17+
DOMAIN as VALVE_DOMAIN,
18+
SERVICE_CLOSE_VALVE,
19+
SERVICE_OPEN_VALVE,
20+
SERVICE_STOP_VALVE,
21+
ValveState,
22+
)
923
from homeassistant.const import (
1024
ATTR_DEVICE_CLASS,
1125
ATTR_FRIENDLY_NAME,
@@ -594,3 +608,66 @@ async def test_intents_respond_intent(hass: HomeAssistant) -> None:
594608
hass, "test", intent.INTENT_RESPOND, {"response": {"value": "Hello World"}}
595609
)
596610
assert response.speech["plain"]["speech"] == "Hello World"
611+
612+
613+
async def test_stop_moving_valve(hass: HomeAssistant) -> None:
614+
"""Test HassStopMoving intent for valves."""
615+
assert await async_setup_component(hass, "intent", {})
616+
617+
entity_id = f"{VALVE_DOMAIN}.test_valve"
618+
hass.states.async_set(entity_id, ValveState.OPEN)
619+
calls = async_mock_service(hass, VALVE_DOMAIN, SERVICE_STOP_VALVE)
620+
621+
response = await intent.async_handle(
622+
hass, "test", intent.INTENT_STOP_MOVING, {"name": {"value": "test valve"}}
623+
)
624+
await hass.async_block_till_done()
625+
626+
assert response.response_type == intent.IntentResponseType.ACTION_DONE
627+
assert len(calls) == 1
628+
call = calls[0]
629+
assert call.domain == VALVE_DOMAIN
630+
assert call.service == SERVICE_STOP_VALVE
631+
assert call.data == {"entity_id": entity_id}
632+
633+
634+
@pytest.mark.parametrize(
635+
("slots"),
636+
[
637+
({"name": {"value": "test cover"}}),
638+
({"device_class": {"value": "shade"}}),
639+
],
640+
)
641+
async def test_stop_moving_cover(hass: HomeAssistant, slots: dict[str, Any]) -> None:
642+
"""Test HassStopMoving intent for covers."""
643+
assert await async_setup_component(hass, "intent", {})
644+
645+
entity_id = f"{COVER_DOMAIN}.test_cover"
646+
hass.states.async_set(
647+
entity_id, CoverState.OPEN, attributes={"device_class": "shade"}
648+
)
649+
calls = async_mock_service(hass, COVER_DOMAIN, SERVICE_STOP_COVER)
650+
651+
response = await intent.async_handle(hass, "test", intent.INTENT_STOP_MOVING, slots)
652+
await hass.async_block_till_done()
653+
654+
assert response.response_type == intent.IntentResponseType.ACTION_DONE
655+
assert len(calls) == 1
656+
call = calls[0]
657+
assert call.domain == COVER_DOMAIN
658+
assert call.service == SERVICE_STOP_COVER
659+
assert call.data == {"entity_id": entity_id}
660+
661+
662+
async def test_stop_moving_intent_unsupported_domain(hass: HomeAssistant) -> None:
663+
"""Test that HassStopMoving intent fails with unsupported domain."""
664+
assert await async_setup_component(hass, "homeassistant", {})
665+
assert await async_setup_component(hass, "intent", {})
666+
667+
# Can't stop lights
668+
hass.states.async_set("light.test_light", "on")
669+
670+
with pytest.raises(intent.IntentHandleError):
671+
await intent.async_handle(
672+
hass, "test", intent.INTENT_STOP_MOVING, {"name": {"value": "test light"}}
673+
)

tests/helpers/test_llm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ class MyIntentHandler(intent.IntentHandler):
369369
"HassTurnOn",
370370
"HassTurnOff",
371371
"HassSetPosition",
372+
"HassStopMoving",
372373
"HassStartTimer",
373374
"HassCancelTimer",
374375
"HassCancelAllTimers",

0 commit comments

Comments
 (0)