Add HassStopMoving intent for covers and valves (#155267)

Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
This commit is contained in:
Ezra Freedman
2025-12-04 16:23:49 -05:00
committed by GitHub
parent 4e8a31a4e2
commit c9351a022e
4 changed files with 109 additions and 2 deletions

View File

@@ -22,6 +22,7 @@ from homeassistant.components.cover import (
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER,
CoverDeviceClass,
)
from homeassistant.components.http.data_validator import RequestDataValidator
@@ -38,6 +39,7 @@ from homeassistant.components.valve import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_SET_VALVE_POSITION,
SERVICE_STOP_VALVE,
ValveDeviceClass,
)
from homeassistant.const import (
@@ -143,6 +145,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
NevermindIntentHandler(),
)
intent.async_register(hass, SetPositionIntentHandler())
intent.async_register(hass, StopMovingIntentHandler())
intent.async_register(hass, StartTimerIntentHandler())
intent.async_register(hass, CancelTimerIntentHandler())
intent.async_register(hass, CancelAllTimersIntentHandler())
@@ -433,6 +436,31 @@ class SetPositionIntentHandler(intent.DynamicServiceIntentHandler):
raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
class StopMovingIntentHandler(intent.DynamicServiceIntentHandler):
"""Intent handler for stopping covers and valves."""
def __init__(self) -> None:
"""Create stop moving handler."""
super().__init__(
intent.INTENT_STOP_MOVING,
description="Stops a moving device or entity",
platforms={COVER_DOMAIN, VALVE_DOMAIN},
device_classes={CoverDeviceClass, ValveDeviceClass},
)
def get_domain_and_service(
self, intent_obj: intent.Intent, state: State
) -> tuple[str, str]:
"""Get the domain and service name to call."""
if state.domain == COVER_DOMAIN:
return (COVER_DOMAIN, SERVICE_STOP_COVER)
if state.domain == VALVE_DOMAIN:
return (VALVE_DOMAIN, SERVICE_STOP_VALVE)
raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
class GetCurrentDateIntentHandler(intent.IntentHandler):
"""Gets the current date."""

View File

@@ -48,6 +48,7 @@ INTENT_TOGGLE = "HassToggle"
INTENT_GET_STATE = "HassGetState"
INTENT_NEVERMIND = "HassNevermind"
INTENT_SET_POSITION = "HassSetPosition"
INTENT_STOP_MOVING = "HassStopMoving"
INTENT_START_TIMER = "HassStartTimer"
INTENT_CANCEL_TIMER = "HassCancelTimer"
INTENT_CANCEL_ALL_TIMERS = "HassCancelAllTimers"

View File

@@ -1,11 +1,25 @@
"""Tests for Intent component."""
from typing import Any
import pytest
from homeassistant.components.button import SERVICE_PRESS
from homeassistant.components.cover import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_STOP_COVER,
CoverState,
)
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
from homeassistant.components.valve import SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE
from homeassistant.components.valve import (
DOMAIN as VALVE_DOMAIN,
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_STOP_VALVE,
ValveState,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
@@ -594,3 +608,66 @@ async def test_intents_respond_intent(hass: HomeAssistant) -> None:
hass, "test", intent.INTENT_RESPOND, {"response": {"value": "Hello World"}}
)
assert response.speech["plain"]["speech"] == "Hello World"
async def test_stop_moving_valve(hass: HomeAssistant) -> None:
"""Test HassStopMoving intent for valves."""
assert await async_setup_component(hass, "intent", {})
entity_id = f"{VALVE_DOMAIN}.test_valve"
hass.states.async_set(entity_id, ValveState.OPEN)
calls = async_mock_service(hass, VALVE_DOMAIN, SERVICE_STOP_VALVE)
response = await intent.async_handle(
hass, "test", intent.INTENT_STOP_MOVING, {"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 == VALVE_DOMAIN
assert call.service == SERVICE_STOP_VALVE
assert call.data == {"entity_id": entity_id}
@pytest.mark.parametrize(
("slots"),
[
({"name": {"value": "test cover"}}),
({"device_class": {"value": "shade"}}),
],
)
async def test_stop_moving_cover(hass: HomeAssistant, slots: dict[str, Any]) -> None:
"""Test HassStopMoving intent for covers."""
assert await async_setup_component(hass, "intent", {})
entity_id = f"{COVER_DOMAIN}.test_cover"
hass.states.async_set(
entity_id, CoverState.OPEN, attributes={"device_class": "shade"}
)
calls = async_mock_service(hass, COVER_DOMAIN, SERVICE_STOP_COVER)
response = await intent.async_handle(hass, "test", intent.INTENT_STOP_MOVING, slots)
await hass.async_block_till_done()
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert len(calls) == 1
call = calls[0]
assert call.domain == COVER_DOMAIN
assert call.service == SERVICE_STOP_COVER
assert call.data == {"entity_id": entity_id}
async def test_stop_moving_intent_unsupported_domain(hass: HomeAssistant) -> None:
"""Test that HassStopMoving intent fails with unsupported domain."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "intent", {})
# Can't stop lights
hass.states.async_set("light.test_light", "on")
with pytest.raises(intent.IntentHandleError):
await intent.async_handle(
hass, "test", intent.INTENT_STOP_MOVING, {"name": {"value": "test light"}}
)

View File

@@ -369,6 +369,7 @@ async def test_assist_api_tools(
"HassTurnOn",
"HassTurnOff",
"HassSetPosition",
"HassStopMoving",
"HassStartTimer",
"HassCancelTimer",
"HassCancelAllTimers",