Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Sonos select_source timeout error #115640

Merged
merged 30 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
af11baa
Initial Draft
PeteRager Apr 11, 2024
7c0cccd
Update tests for select_source
PeteRager Apr 11, 2024
ca03957
Update tests for select_source
PeteRager Apr 12, 2024
d2a8cd7
Update tests for select_source
PeteRager Apr 12, 2024
1577887
Mock the reference item id correctly
PeteRager Apr 12, 2024
4a0eb20
Mock the reference item id correctly
PeteRager Apr 12, 2024
887963f
Raise exception on missing favorite
PeteRager Apr 13, 2024
e47c4e6
Refactor mock media
PeteRager Apr 13, 2024
73f1328
Refactor mock media
PeteRager Apr 13, 2024
ca0aa96
refactor test data into fixture
PeteRager Apr 14, 2024
70e8a94
refactor test data into fixture
PeteRager Apr 14, 2024
d31dad4
refactor test data into fixture
PeteRager Apr 14, 2024
b16851f
Add sonos favoritres json
PeteRager Apr 14, 2024
6cb6ab5
json update
PeteRager Apr 14, 2024
57eb3bb
simplify code
PeteRager Apr 14, 2024
dee3ddd
update comment
PeteRager Apr 14, 2024
263064d
simplify test
PeteRager Apr 14, 2024
1f5fa7e
add play_uri_timeout
PeteRager Apr 15, 2024
8721ac8
Merge branch 'home-assistant:dev' into select_source_timeout
PeteRager Apr 15, 2024
977edcf
remove unneeded line
PeteRager Apr 15, 2024
5080534
add typing to tests
PeteRager Apr 24, 2024
8c06181
add translation key for exception
PeteRager Apr 24, 2024
6be350e
Merge branch 'dev' into select_source_timeout
PeteRager Apr 24, 2024
140f140
fix merge issues
PeteRager Apr 24, 2024
95e03d7
Merge branch 'dev' into select_source_timeout
PeteRager Apr 24, 2024
a3f823d
refactor tests
PeteRager Apr 30, 2024
bc077ec
refactor tests
PeteRager Apr 30, 2024
56ff05b
refactor tests
PeteRager Apr 30, 2024
0aeb03c
Merge branch 'dev' into select_source_timeout
PeteRager Apr 30, 2024
cd229cc
Merge branch 'dev' into select_source_timeout
PeteRager Apr 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)