From a9fd744247443e3d74abbae83aa63fdb64de0bfc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Mar 2022 13:16:22 -0700 Subject: [PATCH] Add media source support to unifiprotect (#67570) --- .../components/unifiprotect/media_player.py | 30 ++++++++++-- .../unifiprotect/test_media_player.py | 46 ++++++++++++++++--- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index a6ed8f65681..4de59c8252a 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -7,13 +7,19 @@ from typing import Any from pyunifiprotect.data import Camera from pyunifiprotect.exceptions import StreamError +from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityDescription, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_SET, @@ -74,7 +80,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self._attr_name = f"{self.device.name} Speaker" self._attr_supported_features = ( - SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | SUPPORT_STOP + SUPPORT_PLAY_MEDIA + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_STOP + | SUPPORT_BROWSE_MEDIA ) self._attr_media_content_type = MEDIA_TYPE_MUSIC @@ -112,16 +122,20 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = async_process_play_media_url(self.hass, play_item.url) if media_type != MEDIA_TYPE_MUSIC: - raise ValueError("Only music media type is supported") + raise HomeAssistantError("Only music media type is supported") _LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name) await self.async_media_stop() try: await self.device.play_audio(media_id, blocking=False) except StreamError as err: - raise HomeAssistantError from err + raise HomeAssistantError(err) from err else: # update state after starting player self._async_updated_event() @@ -129,3 +143,13 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): await self.device.wait_until_audio_completes() self._async_updated_event() + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 4d83da3fabb..c4586eb7880 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations from copy import copy -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest from pyunifiprotect.data import Camera @@ -80,7 +80,7 @@ async def test_media_player_setup( assert state assert state.state == STATE_IDLE assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 5636 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 136708 assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == "music" assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == expected_volume @@ -166,7 +166,6 @@ async def test_media_player_play( camera: tuple[Camera, str], ): """Test media_player entity test play_media.""" - camera[0].__fields__["stop_audio"] = Mock() camera[0].__fields__["play_audio"] = Mock() camera[0].__fields__["wait_until_audio_completes"] = Mock() @@ -179,13 +178,48 @@ async def test_media_player_play( "play_media", { ATTR_ENTITY_ID: camera[1], - "media_content_id": "/test.mp3", + "media_content_id": "http://example.com/test.mp3", "media_content_type": "music", }, blocking=True, ) - camera[0].play_audio.assert_called_once_with("/test.mp3", blocking=False) + camera[0].play_audio.assert_called_once_with( + "http://example.com/test.mp3", blocking=False + ) + camera[0].wait_until_audio_completes.assert_called_once() + + +async def test_media_player_play_media_source( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity test play_media.""" + camera[0].__fields__["stop_audio"] = Mock() + camera[0].__fields__["play_audio"] = Mock() + camera[0].__fields__["wait_until_audio_completes"] = Mock() + camera[0].stop_audio = AsyncMock() + camera[0].play_audio = AsyncMock() + camera[0].wait_until_audio_completes = AsyncMock() + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=Mock(url="http://example.com/test.mp3"), + ): + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: camera[1], + "media_content_id": "media-source://some_source/some_id", + "media_content_type": "audio/mpeg", + }, + blocking=True, + ) + + camera[0].play_audio.assert_called_once_with( + "http://example.com/test.mp3", blocking=False + ) camera[0].wait_until_audio_completes.assert_called_once() @@ -198,7 +232,7 @@ async def test_media_player_play_invalid( camera[0].__fields__["play_audio"] = Mock() camera[0].play_audio = AsyncMock() - with pytest.raises(ValueError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( "media_player", "play_media",