Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Hansen
4e141a2747 Merge branch 'dev' into synesthesiam-20260303-intent-media-tests 2026-03-24 08:56:56 -05:00
Michael Hansen
3d47bed74d Test media intent match errors 2026-03-03 14:44:16 -06:00
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 153 additions and 16 deletions

View File

@@ -2,6 +2,7 @@
import asyncio
from collections.abc import Iterable
import dataclasses
from dataclasses import dataclass, field
import logging
import time
@@ -481,7 +482,9 @@ class MediaSetVolumeRelativeHandler(intent.IntentHandler):
result=intent.MatchTargetsResult(
is_match=False, no_match_reason=intent.MatchFailedReason.STATE
),
constraints=match_constraints,
constraints=dataclasses.replace(
match_constraints, states=[MediaPlayerState.PLAYING]
),
preferences=match_preferences,
)

View File

@@ -78,13 +78,21 @@ async def test_pause_media_player_intent(hass: HomeAssistant) -> None:
# Test if not playing
hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_MEDIA_PAUSE,
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.STATE
assert match_failed_error.constraints.states
assert list(match_failed_error.constraints.states) == [MediaPlayerState.PLAYING]
# Test feature not supported
hass.states.async_set(
entity_id,
@@ -92,13 +100,20 @@ async def test_pause_media_player_intent(hass: HomeAssistant) -> None:
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_MEDIA_PAUSE,
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert match_failed_error.constraints.features == MediaPlayerEntityFeature.PAUSE
async def test_unpause_media_player_intent(hass: HomeAssistant) -> None:
"""Test HassMediaUnpause intent for media players."""
@@ -151,13 +166,21 @@ async def test_next_media_player_intent(hass: HomeAssistant) -> None:
# Test if not playing
hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_MEDIA_NEXT,
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.STATE
assert match_failed_error.constraints.states
assert list(match_failed_error.constraints.states) == [MediaPlayerState.PLAYING]
# Test feature not supported
hass.states.async_set(
entity_id,
@@ -165,7 +188,7 @@ async def test_next_media_player_intent(hass: HomeAssistant) -> None:
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
@@ -173,6 +196,15 @@ async def test_next_media_player_intent(hass: HomeAssistant) -> None:
{"name": {"value": "test media player"}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features == MediaPlayerEntityFeature.NEXT_TRACK
)
async def test_previous_media_player_intent(hass: HomeAssistant) -> None:
"""Test HassMediaPrevious intent for media players."""
@@ -202,13 +234,21 @@ async def test_previous_media_player_intent(hass: HomeAssistant) -> None:
# Test if not playing
hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_MEDIA_PREVIOUS,
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.STATE
assert match_failed_error.constraints.states
assert list(match_failed_error.constraints.states) == [MediaPlayerState.PLAYING]
# Test feature not supported
hass.states.async_set(
entity_id,
@@ -216,7 +256,7 @@ async def test_previous_media_player_intent(hass: HomeAssistant) -> None:
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
@@ -224,6 +264,16 @@ async def test_previous_media_player_intent(hass: HomeAssistant) -> None:
{"name": {"value": "test media player"}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features
== MediaPlayerEntityFeature.PREVIOUS_TRACK
)
async def test_volume_media_player_intent(hass: HomeAssistant) -> None:
"""Test HassSetVolume intent for media players."""
@@ -257,7 +307,7 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None:
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
@@ -265,6 +315,15 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None:
{"volume_level": {"value": 50}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features == MediaPlayerEntityFeature.VOLUME_SET
)
async def test_media_player_mute_intent(hass: HomeAssistant) -> None:
"""Test HassMediaPlayerMute intent for media players."""
@@ -298,7 +357,7 @@ async def test_media_player_mute_intent(hass: HomeAssistant) -> None:
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
@@ -306,6 +365,15 @@ async def test_media_player_mute_intent(hass: HomeAssistant) -> None:
{},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features == MediaPlayerEntityFeature.VOLUME_MUTE
)
async def test_media_player_unmute_intent(hass: HomeAssistant) -> None:
"""Test HassMediaPlayerMute intent for media players."""
@@ -339,7 +407,7 @@ async def test_media_player_unmute_intent(hass: HomeAssistant) -> None:
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
@@ -347,6 +415,15 @@ async def test_media_player_unmute_intent(hass: HomeAssistant) -> None:
{},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features == MediaPlayerEntityFeature.VOLUME_MUTE
)
async def test_multiple_media_players(
hass: HomeAssistant,
@@ -462,7 +539,7 @@ async def test_multiple_media_players(
# -----
# There are multiple TV's currently playing
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
response = await intent.async_handle(
hass,
"test",
@@ -470,6 +547,16 @@ async def test_multiple_media_players(
{"name": {"value": "TV"}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert (
match_failed_error.result.no_match_reason
== intent.MatchFailedReason.DUPLICATE_NAME
)
assert match_failed_error.result.no_match_name == "TV"
# Pause the upstairs TV
calls = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PAUSE)
response = await intent.async_handle(
@@ -826,7 +913,7 @@ async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None:
STATE_IDLE,
attributes={},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
await intent.async_handle(
hass,
"test",
@@ -834,13 +921,23 @@ async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None:
{"search_query": {"value": "test query"}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features
== MediaPlayerEntityFeature.SEARCH_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
)
# Test feature not supported (missing SEARCH_MEDIA)
hass.states.async_set(
entity_id,
STATE_IDLE,
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY_MEDIA},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
await intent.async_handle(
hass,
"test",
@@ -848,6 +945,16 @@ async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None:
{"search_query": {"value": "test query"}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features
== MediaPlayerEntityFeature.SEARCH_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
)
# Test play media service errors
search_results.append(search_result_item)
hass.states.async_set(
@@ -862,7 +969,7 @@ async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None:
SERVICE_PLAY_MEDIA,
raise_exception=HomeAssistantError("Play failed"),
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
await intent.async_handle(
hass,
"test",
@@ -870,6 +977,16 @@ async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None:
{"search_query": {"value": "play error query"}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features
== MediaPlayerEntityFeature.SEARCH_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
)
# Test search service error
hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes)
async_mock_service(
@@ -1105,7 +1222,7 @@ async def test_volume_relative_media_player_intent(
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_SET},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
await intent.async_handle(
hass,
"test",
@@ -1113,6 +1230,14 @@ async def test_volume_relative_media_player_intent(
{"volume_step": {"value": direction}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.STATE
assert match_failed_error.constraints.states
assert list(match_failed_error.constraints.states) == [MediaPlayerState.PLAYING]
# Test feature not supported
for entity_id in (idle_entity.entity_id, playing_entity.entity_id):
hass.states.async_set(
@@ -1121,10 +1246,19 @@ async def test_volume_relative_media_player_intent(
attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)},
)
with pytest.raises(intent.MatchFailedError):
with pytest.raises(intent.MatchFailedError) as error_wrapper:
await intent.async_handle(
hass,
"test",
media_player_intent.INTENT_SET_VOLUME_RELATIVE,
{"volume_step": {"value": direction}},
)
# Verify match failure reason and info
match_failed_error = error_wrapper.value
assert isinstance(match_failed_error, intent.MatchFailedError)
assert not match_failed_error.result.is_match
assert match_failed_error.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert (
match_failed_error.constraints.features == MediaPlayerEntityFeature.VOLUME_SET
)