diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 35c6be3fa6b..e9fbb152b7a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -39,7 +39,7 @@ from homeassistant.components.plex.services import process_plex_payload from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -432,7 +432,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): fav = [fav for fav in self.speaker.favorites if fav.title == name] if len(fav) != 1: - return + raise ServiceValidationError( + translation_domain=SONOS_DOMAIN, + translation_key="invalid_favorite", + translation_placeholders={ + "name": name, + }, + ) src = fav.pop() self._play_favorite(src) @@ -445,7 +451,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): MUSIC_SRC_RADIO, MUSIC_SRC_LINE_IN, ]: - soco.play_uri(uri, title=favorite.title) + soco.play_uri(uri, title=favorite.title, timeout=LONG_SERVICE_TIMEOUT) else: soco.clear_queue() soco.add_to_queue(favorite.reference, timeout=LONG_SERVICE_TIMEOUT) diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 6f45195c46b..6521302b007 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -173,5 +173,10 @@ } } } + }, + "exceptions": { + "invalid_favorite": { + "message": "Could not find a Sonos favorite: {name}" + } } } diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 3da0dd5c983..465ac6e2728 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -9,6 +9,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.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf @@ -17,7 +18,7 @@ from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture class SonosMockEventListener: @@ -304,6 +305,14 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.2"]}}} +@pytest.fixture(name="sonos_favorites") +def sonos_favorites_fixture() -> SearchResult: + """Create sonos favorites fixture.""" + favorites = load_json_value_fixture("sonos_favorites.json", "sonos") + favorite_list = [DidlFavorite.from_dict(fav) for fav in favorites] + return SearchResult(favorite_list, "favorites", 3, 3, 1) + + class MockMusicServiceItem: """Mocks a Soco MusicServiceItem.""" @@ -408,10 +417,10 @@ def mock_get_music_library_information( @pytest.fixture(name="music_library") -def music_library_fixture(): +def music_library_fixture(sonos_favorites: SearchResult) -> Mock: """Create music_library fixture.""" music_library = MagicMock() - music_library.get_sonos_favorites.return_value.update_id = 1 + music_library.get_sonos_favorites.return_value = sonos_favorites music_library.browse_by_idstring = mock_browse_by_idstring music_library.get_music_library_information = mock_get_music_library_information return music_library diff --git a/tests/components/sonos/fixtures/sonos_favorites.json b/tests/components/sonos/fixtures/sonos_favorites.json new file mode 100644 index 00000000000..21ee68f4872 --- /dev/null +++ b/tests/components/sonos/fixtures/sonos_favorites.json @@ -0,0 +1,38 @@ +[ + { + "title": "66 - Watercolors", + "parent_id": "FV:2", + "item_id": "FV:2/4", + "resource_meta_data": "66 - Watercolorsobject.item.audioItem.audioBroadcastSA_RINCON9479_X_#Svc9479-99999999-Token", + "resources": [ + { + "uri": "x-sonosapi-hls:Api%3atune%3aliveAudio%3ajazzcafe%3aetc", + "protocol_info": "a:b:c:d" + } + ] + }, + { + "title": "James Taylor Radio", + "parent_id": "FV:2", + "item_id": "FV:2/13", + "resource_meta_data": "James Taylor Radioobject.item.audioItem.audioBroadcast.#stationSA_RINCON60423_X_#Svc60423-99999999-Token", + "resources": [ + { + "uri": "x-sonosapi-radio:ST%3aetc", + "protocol_info": "a:b:c:d" + } + ] + }, + { + "title": "1984", + "parent_id": "FV:2", + "item_id": "FV:2/8", + "resource_meta_data": "1984object.container.album.musicAlbumRINCON_AssociatedZPUDN", + "resources": [ + { + "uri": "x-rincon-playlist:RINCON_test#A:ALBUMARTIST/Aerosmith/1984", + "protocol_info": "a:b:c:d" + } + ] + } +] diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 976d3480429..9fb8444a696 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,6 +1,7 @@ """Tests for the Sonos Media Player platform.""" import logging +from typing import Any import pytest @@ -9,10 +10,15 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, MediaPlayerEnqueue, ) -from homeassistant.components.media_player.const import ATTR_MEDIA_ENQUEUE +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ENQUEUE, + SERVICE_SELECT_SOURCE, +) +from homeassistant.components.sonos.const import SOURCE_LINEIN, SOURCE_TV from homeassistant.components.sonos.media_player import LONG_SERVICE_TIMEOUT from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_UPNP, @@ -272,3 +278,154 @@ async def test_play_media_music_library_playlist_dne( assert soco_mock.play_uri.call_count == 0 assert media_content_id in caplog.text assert "playlist" in caplog.text + + +@pytest.mark.parametrize( + ("source", "result"), + [ + ( + SOURCE_LINEIN, + { + "switch_to_line_in": 1, + }, + ), + ( + SOURCE_TV, + { + "switch_to_tv": 1, + }, + ), + ], +) +async def test_select_source_line_in_tv( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + source: str, + result: dict[str, Any], +) -> None: + """Test the select_source method with a variety of inputs.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + { + "entity_id": "media_player.zone_a", + "source": source, + }, + blocking=True, + ) + assert soco_mock.switch_to_line_in.call_count == result.get("switch_to_line_in", 0) + assert soco_mock.switch_to_tv.call_count == result.get("switch_to_tv", 0) + + +@pytest.mark.parametrize( + ("source", "result"), + [ + ( + "James Taylor Radio", + { + "play_uri": 1, + "play_uri_uri": "x-sonosapi-radio:ST%3aetc", + "play_uri_title": "James Taylor Radio", + }, + ), + ( + "66 - Watercolors", + { + "play_uri": 1, + "play_uri_uri": "x-sonosapi-hls:Api%3atune%3aliveAudio%3ajazzcafe%3aetc", + "play_uri_title": "66 - Watercolors", + }, + ), + ], +) +async def test_select_source_play_uri( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + source: str, + result: dict[str, Any], +) -> None: + """Test the select_source method with a variety of inputs.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + { + "entity_id": "media_player.zone_a", + "source": source, + }, + blocking=True, + ) + assert soco_mock.play_uri.call_count == result.get("play_uri") + soco_mock.play_uri.assert_called_with( + result.get("play_uri_uri"), + title=result.get("play_uri_title"), + timeout=LONG_SERVICE_TIMEOUT, + ) + + +@pytest.mark.parametrize( + ("source", "result"), + [ + ( + "1984", + { + "add_to_queue": 1, + "add_to_queue_item_id": "A:ALBUMARTIST/Aerosmith/1984", + "clear_queue": 1, + "play_from_queue": 1, + }, + ), + ], +) +async def test_select_source_play_queue( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + source: str, + result: dict[str, Any], +) -> None: + """Test the select_source method with a variety of inputs.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + { + "entity_id": "media_player.zone_a", + "source": source, + }, + blocking=True, + ) + assert soco_mock.clear_queue.call_count == result.get("clear_queue") + assert soco_mock.add_to_queue.call_count == result.get("add_to_queue") + assert soco_mock.add_to_queue.call_args_list[0].args[0].item_id == result.get( + "add_to_queue_item_id" + ) + assert ( + soco_mock.add_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert soco_mock.play_from_queue.call_count == result.get("play_from_queue") + soco_mock.play_from_queue.assert_called_with(0) + + +async def test_select_source_error( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, +) -> None: + """Test the select_source method with a variety of inputs.""" + with pytest.raises(ServiceValidationError) as sve: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + { + "entity_id": "media_player.zone_a", + "source": "invalid_source", + }, + blocking=True, + ) + assert "invalid_source" in str(sve.value) + assert "Could not find a Sonos favorite" in str(sve.value)