diff --git a/homeassistant/components/russound_rio/media_browser.py b/homeassistant/components/russound_rio/media_browser.py new file mode 100644 index 00000000000..7e5ca741f90 --- /dev/null +++ b/homeassistant/components/russound_rio/media_browser.py @@ -0,0 +1,97 @@ +"""Support for Russound media browsing.""" + +from aiorussound import RussoundClient, Zone +from aiorussound.const import FeatureFlag +from aiorussound.util import is_feature_supported + +from homeassistant.components.media_player import BrowseMedia, MediaClass +from homeassistant.core import HomeAssistant + + +async def async_browse_media( + hass: HomeAssistant, + client: RussoundClient, + media_content_id: str | None, + media_content_type: str | None, + zone: Zone, +) -> BrowseMedia: + """Browse media.""" + if media_content_type == "presets": + return await _presets_payload(_find_presets_by_zone(client, zone)) + + return await _root_payload(hass, _find_presets_by_zone(client, zone)) + + +async def _root_payload( + hass: HomeAssistant, presets_by_zone: dict[int, dict[int, str]] +) -> BrowseMedia: + """Return root payload for Russound RIO.""" + children: list[BrowseMedia] = [] + + if presets_by_zone: + children.append( + BrowseMedia( + title="Presets", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="presets", + thumbnail="https://brands.home-assistant.io/_/russound_rio/logo.png", + can_play=False, + can_expand=True, + ) + ) + + return BrowseMedia( + title="Russound", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="root", + can_play=False, + can_expand=True, + children=children, + ) + + +async def _presets_payload(presets_by_zone: dict[int, dict[int, str]]) -> BrowseMedia: + """Create payload to list presets.""" + children: list[BrowseMedia] = [] + for source_id, presets in presets_by_zone.items(): + for preset_id, preset_name in presets.items(): + children.append( + BrowseMedia( + title=preset_name, + media_class=MediaClass.CHANNEL, + media_content_id=f"{source_id},{preset_id}", + media_content_type="preset", + can_play=True, + can_expand=False, + ) + ) + + return BrowseMedia( + title="Presets", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="presets", + can_play=False, + can_expand=True, + children=children, + ) + + +def _find_presets_by_zone( + client: RussoundClient, zone: Zone +) -> dict[int, dict[int, str]]: + """Returns a dict by {source_id: {preset_id: preset_name}}.""" + assert client.rio_version + return { + source_id: source.presets + for source_id, source in client.sources.items() + if source.presets + and ( + not is_feature_supported( + client.rio_version, FeatureFlag.SUPPORT_ZONE_SOURCE_EXCLUSION + ) + or source_id in zone.enabled_sources + ) + } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index a4b86a85e94..a09c663a983 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -13,6 +13,7 @@ from aiorussound.models import PlayStatus, Source from aiorussound.util import is_feature_supported from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -23,7 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import RussoundConfigEntry +from . import RussoundConfigEntry, media_browser from .const import DOMAIN, RUSSOUND_MEDIA_TYPE_PRESET, SELECT_SOURCE_DELAY from .entity import RussoundBaseEntity, command @@ -65,7 +66,8 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): _attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( - MediaPlayerEntityFeature.VOLUME_SET + MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.TURN_ON @@ -264,3 +266,13 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): translation_placeholders={"preset_id": media_id}, ) await self._zone.restore_preset(preset_id) + + async def async_browse_media( + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the media browsing helper.""" + return await media_browser.async_browse_media( + self.hass, self._client, media_content_id, media_content_type, self._zone + ) diff --git a/tests/components/russound_rio/fixtures/get_zones.json b/tests/components/russound_rio/fixtures/get_zones.json index e1077944593..b51c93875f1 100644 --- a/tests/components/russound_rio/fixtures/get_zones.json +++ b/tests/components/russound_rio/fixtures/get_zones.json @@ -7,7 +7,8 @@ "volume": "10", "status": "ON", "enabled": "True", - "current_source": "1" + "current_source": "1", + "enabled_sources": [1, 2] }, "2": { "name": "Kitchen", diff --git a/tests/components/russound_rio/snapshots/test_media_browser.ambr b/tests/components/russound_rio/snapshots/test_media_browser.ambr new file mode 100644 index 00000000000..7c3df31a69b --- /dev/null +++ b/tests/components/russound_rio/snapshots/test_media_browser.ambr @@ -0,0 +1,75 @@ +# serializer version: 1 +# name: test_browse_media_root + list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': '', + 'media_content_type': 'presets', + 'thumbnail': 'https://brands.home-assistant.io/_/russound_rio/logo.png', + 'title': 'Presets', + }), + ]) +# --- +# name: test_browse_presets + list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,1', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WOOD', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,2', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': '89.7 MHz FM', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,7', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WWKR', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,8', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WKLA', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,11', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WGN', + }), + ]) +# --- diff --git a/tests/components/russound_rio/test_media_browser.py b/tests/components/russound_rio/test_media_browser.py new file mode 100644 index 00000000000..d2d67e70aeb --- /dev/null +++ b/tests/components/russound_rio/test_media_browser.py @@ -0,0 +1,61 @@ +"""Tests for the Russound RIO media browser.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_integration +from .const import ENTITY_ID_ZONE_1 + +from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator + + +async def test_browse_media_root( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the root browse page.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": ENTITY_ID_ZONE_1, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot + + +async def test_browse_presets( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the presets browse page.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": ENTITY_ID_ZONE_1, + "media_content_type": "presets", + "media_content_id": "", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot