diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 590761752c5..ff6981cfc9b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -643,11 +643,16 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): playlists = soco.get_sonos_playlists(complete_result=True) playlist = next((p for p in playlists if p.title == media_id), None) if not playlist: - _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) - else: - soco.clear_queue() - soco.add_to_queue(playlist, timeout=LONG_SERVICE_TIMEOUT) - soco.play_from_queue(0) + raise ServiceValidationError( + translation_domain=SONOS_DOMAIN, + translation_key="invalid_sonos_playlist", + translation_placeholders={ + "name": media_id, + }, + ) + soco.clear_queue() + soco.add_to_queue(playlist, timeout=LONG_SERVICE_TIMEOUT) + soco.play_from_queue(0) elif media_type in PLAYABLE_MEDIA_TYPES: item = media_browser.get_media(self.media.library, media_id, media_type) diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 6521302b007..5833e5a27f2 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -177,6 +177,9 @@ "exceptions": { "invalid_favorite": { "message": "Could not find a Sonos favorite: {name}" + }, + "invalid_sonos_playlist": { + "message": "Could not find Sonos playlist: {name}" } } } diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index cf75f23c322..996fd4c6663 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo from soco.alarms import Alarms -from soco.data_structures import DidlFavorite, SearchResult +from soco.data_structures import DidlFavorite, DidlPlaylistContainer, SearchResult from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf @@ -184,6 +184,7 @@ class SoCoMockFactory: current_track_info_empty, battery_info, alarm_clock, + sonos_playlists: SearchResult, ) -> None: """Initialize the mock factory.""" self.mock_list: dict[str, MockSoCo] = {} @@ -192,6 +193,7 @@ class SoCoMockFactory: self.current_track_info = current_track_info_empty self.battery_info = battery_info self.alarm_clock = alarm_clock + self.sonos_playlists = sonos_playlists def cache_mock( self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A" @@ -204,6 +206,7 @@ class SoCoMockFactory: mock_soco.music_library = self.music_library mock_soco.get_current_track_info.return_value = self.current_track_info mock_soco.music_source_from_uri = SoCo.music_source_from_uri + mock_soco.get_sonos_playlists.return_value = self.sonos_playlists my_speaker_info = self.speaker_info.copy() my_speaker_info["zone_name"] = name my_speaker_info["uid"] = mock_soco.uid @@ -254,11 +257,21 @@ def soco_sharelink(): @pytest.fixture(name="soco_factory") def soco_factory( - music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock + music_library, + speaker_info, + current_track_info_empty, + battery_info, + alarm_clock, + sonos_playlists: SearchResult, ): """Create factory for instantiating SoCo mocks.""" factory = SoCoMockFactory( - music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock + music_library, + speaker_info, + current_track_info_empty, + battery_info, + alarm_clock, + sonos_playlists, ) with ( patch("homeassistant.components.sonos.SoCo", new=factory.get_mock), @@ -335,6 +348,14 @@ def sonos_favorites_fixture() -> SearchResult: return SearchResult(favorite_list, "favorites", 3, 3, 1) +@pytest.fixture(name="sonos_playlists") +def sonos_playlists_fixture() -> SearchResult: + """Create sonos playlist fixture.""" + playlists = load_json_value_fixture("sonos_playlists.json", "sonos") + playlists_list = [DidlPlaylistContainer.from_dict(pl) for pl in playlists] + return SearchResult(playlists_list, "sonos_playlists", 1, 1, 0) + + class MockMusicServiceItem: """Mocks a Soco MusicServiceItem.""" diff --git a/tests/components/sonos/fixtures/sonos_playlists.json b/tests/components/sonos/fixtures/sonos_playlists.json new file mode 100644 index 00000000000..f0731467697 --- /dev/null +++ b/tests/components/sonos/fixtures/sonos_playlists.json @@ -0,0 +1,13 @@ +[ + { + "title": "sample playlist", + "parent_id": "SQ:", + "item_id": "SQ:0", + "resources": [ + { + "uri": "file:///jffs/settings/savedqueues.rsq#0", + "protocol_info": "file:*:audio/mpegurl:*" + } + ] + } +] diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d96ceb900e7..b29ae1b6cfb 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,10 +1,10 @@ """Tests for the Sonos Media Player platform.""" -import logging from typing import Any from unittest.mock import patch import pytest +from soco.data_structures import SearchResult from syrupy import SnapshotAssertion from homeassistant.components.media_player import ( @@ -535,8 +535,10 @@ async def test_play_media_music_library_playlist_dne( soco_mock = soco_factory.mock_list.get("192.168.42.2") soco_mock.music_library.get_playlists.return_value = _mock_playlists - with caplog.at_level(logging.ERROR): - caplog.clear() + with pytest.raises( + ServiceValidationError, + match=f"Could not find Sonos playlist: {media_content_id}", + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -548,8 +550,53 @@ async def test_play_media_music_library_playlist_dne( blocking=True, ) assert soco_mock.play_uri.call_count == 0 - assert media_content_id in caplog.text - assert "playlist" in caplog.text + + +async def test_play_sonos_playlist( + hass: HomeAssistant, + async_autosetup_sonos, + soco: MockSoCo, + sonos_playlists: SearchResult, +) -> None: + """Test that sonos playlists can be played.""" + + # Test a successful call + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: "sample playlist", + }, + blocking=True, + ) + assert soco.clear_queue.call_count == 1 + assert soco.add_to_queue.call_count == 1 + soco.add_to_queue.asset_called_with( + sonos_playlists[0], timeout=LONG_SERVICE_TIMEOUT + ) + + # Test playing a non-existent playlist + soco.clear_queue.reset_mock() + soco.add_to_queue.reset_mock() + media_content_id: str = "bad playlist" + with pytest.raises( + ServiceValidationError, + match=f"Could not find Sonos playlist: {media_content_id}", + ): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: media_content_id, + }, + blocking=True, + ) + assert soco.clear_queue.call_count == 0 + assert soco.add_to_queue.call_count == 0 @pytest.mark.parametrize(