From 731fe172249b88630f5ad9864c1341712b670f5b Mon Sep 17 00:00:00 2001
From: Pete Sage <76050312+PeteRager@users.noreply.github.com>
Date: Tue, 7 May 2024 04:08:12 -0400
Subject: [PATCH] Fix Sonos select_source timeout error (#115640)
---
.../components/sonos/media_player.py | 12 +-
homeassistant/components/sonos/strings.json | 5 +
tests/components/sonos/conftest.py | 15 +-
.../sonos/fixtures/sonos_favorites.json | 38 +++++
tests/components/sonos/test_media_player.py | 159 +++++++++++++++++-
5 files changed, 222 insertions(+), 7 deletions(-)
create mode 100644 tests/components/sonos/fixtures/sonos_favorites.json
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)