From 6d940f476a41a6833f79402c0f7f604921030bfd Mon Sep 17 00:00:00 2001 From: anishsane Date: Wed, 1 Oct 2025 01:07:19 +0530 Subject: [PATCH] Add support for Media player Mute/Unmute intents (#150508) --- .../components/media_player/intent.py | 42 ++++++++++ tests/components/media_player/test_intent.py | 83 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index c45dc83e872..2cca51af4ad 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -14,6 +14,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, STATE_PLAYING, ) @@ -27,6 +28,7 @@ from .browse_media import SearchMedia from .const import ( ATTR_MEDIA_FILTER_CLASSES, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SEARCH_MEDIA, @@ -39,6 +41,8 @@ INTENT_MEDIA_PAUSE = "HassMediaPause" INTENT_MEDIA_UNPAUSE = "HassMediaUnpause" INTENT_MEDIA_NEXT = "HassMediaNext" INTENT_MEDIA_PREVIOUS = "HassMediaPrevious" +INTENT_PLAYER_MUTE = "HassMediaPlayerMute" +INTENT_PLAYER_UNMUTE = "HassMediaPlayerUnmute" INTENT_SET_VOLUME = "HassSetVolume" INTENT_SET_VOLUME_RELATIVE = "HassSetVolumeRelative" INTENT_MEDIA_SEARCH_AND_PLAY = "HassMediaSearchAndPlay" @@ -130,6 +134,8 @@ async def async_setup_intents(hass: HomeAssistant) -> None: ), ) intent.async_register(hass, MediaSetVolumeRelativeHandler()) + intent.async_register(hass, MediaPlayerMuteUnmuteHandler(True)) + intent.async_register(hass, MediaPlayerMuteUnmuteHandler(False)) intent.async_register(hass, MediaSearchAndPlayHandler()) @@ -231,6 +237,42 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler): ) +class MediaPlayerMuteUnmuteHandler(intent.ServiceIntentHandler): + """Handle Mute/Unmute intents.""" + + def __init__(self, is_volume_muted: bool) -> None: + """Initialize the mute/unmute handler objects.""" + + super().__init__( + (INTENT_PLAYER_MUTE if is_volume_muted else INTENT_PLAYER_UNMUTE), + DOMAIN, + SERVICE_VOLUME_MUTE, + required_domains={DOMAIN}, + required_features=MediaPlayerEntityFeature.VOLUME_MUTE, + optional_slots={ + ATTR_MEDIA_VOLUME_MUTED: intent.IntentSlotInfo( + description="Whether the media player should be muted or unmuted", + value_schema=vol.Boolean(), + ), + }, + description=( + "Mutes a media player" if is_volume_muted else "Unmutes a media player" + ), + platforms={DOMAIN}, + device_classes={MediaPlayerDeviceClass}, + ) + self.is_volume_muted = is_volume_muted + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the intent.""" + + intent_obj.slots["is_volume_muted"] = { + "value": self.is_volume_muted, + "text": str(self.is_volume_muted), + } + return await super().async_handle(intent_obj) + + class MediaSearchAndPlayHandler(intent.IntentHandler): """Handle HassMediaSearchAndPlay intents.""" diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index 2b585319826..3fb12b1a90d 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -13,6 +13,7 @@ from homeassistant.components.media_player import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_PLAY_MEDIA, SERVICE_SEARCH_MEDIA, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, BrowseMedia, MediaClass, @@ -265,6 +266,88 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None: ) +async def test_media_player_mute_intent(hass: HomeAssistant) -> None: + """Test HassMediaPlayerMute intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + attributes = {ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_MUTE} + + hass.states.async_set(entity_id, STATE_PLAYING, attributes=attributes) + calls = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_PLAYER_MUTE, + {}, + ) + 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_MUTE + assert call.data == {"entity_id": entity_id, "is_volume_muted": True} + + # Test feature not supported + hass.states.async_set( + entity_id, + STATE_PLAYING, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)}, + ) + + with pytest.raises(intent.MatchFailedError): + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_PLAYER_MUTE, + {}, + ) + + +async def test_media_player_unmute_intent(hass: HomeAssistant) -> None: + """Test HassMediaPlayerMute intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + attributes = {ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_MUTE} + + hass.states.async_set(entity_id, STATE_PLAYING, attributes=attributes) + calls = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_PLAYER_UNMUTE, + {}, + ) + 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_MUTE + assert call.data == {"entity_id": entity_id, "is_volume_muted": False} + + # Test feature not supported + hass.states.async_set( + entity_id, + STATE_PLAYING, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)}, + ) + + with pytest.raises(intent.MatchFailedError): + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_PLAYER_UNMUTE, + {}, + ) + + async def test_multiple_media_players( hass: HomeAssistant, area_registry: ar.AreaRegistry,