Skip to content

Commit

Permalink
Fix Sonos select_source timeout error (#115640)
Browse files Browse the repository at this point in the history
  • Loading branch information
PeteRager committed May 7, 2024
1 parent b5cd0e6 commit 731fe17
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 7 deletions.
12 changes: 9 additions & 3 deletions homeassistant/components/sonos/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
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
Expand Down Expand Up @@ -432,7 +432,13 @@ def _play_favorite_by_name(self, name: str) -> None:
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)
Expand All @@ -445,7 +451,7 @@ def _play_favorite(self, favorite: DidlFavorite) -> None:
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)
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/sonos/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,10 @@
}
}
}
},
"exceptions": {
"invalid_favorite": {
"message": "Could not find a Sonos favorite: {name}"
}
}
}
15 changes: 12 additions & 3 deletions tests/components/sonos/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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
Expand All @@ -17,7 +18,7 @@
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:
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions tests/components/sonos/fixtures/sonos_favorites.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"title": "66 - Watercolors",
"parent_id": "FV:2",
"item_id": "FV:2/4",
"resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"10090120Api%3atune%3aliveAudio%3ajazzcafe%3ae4b5402c-9999-9999-9999-4bc8e2cdccce\" parentID=\"10086064live%3f93b0b9cb-9999-9999-9999-bcf75971fcfe\" restricted=\"false\"><dc:title>66 - Watercolors</dc:title><upnp:class>object.item.audioItem.audioBroadcast</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">SA_RINCON9479_X_#Svc9479-99999999-Token</desc></item></DIDL-Lite>",
"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": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"100c2068ST%3a1683194971234567890\" parentID=\"10fe2064myStations\" restricted=\"true\"><dc:title>James Taylor Radio</dc:title><upnp:class>object.item.audioItem.audioBroadcast.#station</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">SA_RINCON60423_X_#Svc60423-99999999-Token</desc></item></DIDL-Lite>",
"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": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"A:ALBUMARTIST/Aerosmith/1984\" parentID=\"A:ALBUMARTIST/Aerosmith\" restricted=\"true\"><dc:title>1984</dc:title><upnp:class>object.container.album.musicAlbum</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>",
"resources": [
{
"uri": "x-rincon-playlist:RINCON_test#A:ALBUMARTIST/Aerosmith/1984",
"protocol_info": "a:b:c:d"
}
]
}
]
159 changes: 158 additions & 1 deletion tests/components/sonos/test_media_player.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for the Sonos Media Player platform."""

import logging
from typing import Any

import pytest

Expand All @@ -9,10 +10,15 @@
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,
Expand Down Expand Up @@ -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)

0 comments on commit 731fe17

Please sign in to comment.