mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Add play media support to Russound RIO (#148240)
This commit is contained in:
parent
6d0891e970
commit
d44b822295
@ -6,6 +6,10 @@ from aiorussound import CommandError
|
||||
|
||||
DOMAIN = "russound_rio"
|
||||
|
||||
RUSSOUND_MEDIA_TYPE_PRESET = "preset"
|
||||
|
||||
SELECT_SOURCE_DELAY = 0.5
|
||||
|
||||
RUSSOUND_RIO_EXCEPTIONS = (
|
||||
CommandError,
|
||||
ConnectionRefusedError,
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiorussound import Controller
|
||||
from aiorussound.const import FeatureFlag
|
||||
@ -19,9 +20,11 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import RussoundConfigEntry
|
||||
from .const import DOMAIN, RUSSOUND_MEDIA_TYPE_PRESET, SELECT_SOURCE_DELAY
|
||||
from .entity import RussoundBaseEntity, command
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -45,6 +48,17 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
def _parse_preset_source_id(media_id: str) -> tuple[int | None, int]:
|
||||
source_id = None
|
||||
if "," in media_id:
|
||||
source_id_str, preset_id_str = media_id.split(",", maxsplit=1)
|
||||
source_id = int(source_id_str.strip())
|
||||
preset_id = int(preset_id_str.strip())
|
||||
else:
|
||||
preset_id = int(media_id)
|
||||
return source_id, preset_id
|
||||
|
||||
|
||||
class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
"""Representation of a Russound Zone."""
|
||||
|
||||
@ -58,6 +72,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
)
|
||||
_attr_name = None
|
||||
|
||||
@ -215,3 +230,37 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Seek to a position in the current media."""
|
||||
await self._zone.set_seek_time(int(position))
|
||||
|
||||
@command
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play media on the Russound zone."""
|
||||
|
||||
if media_type != RUSSOUND_MEDIA_TYPE_PRESET:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unsupported_media_type",
|
||||
translation_placeholders={
|
||||
"media_type": media_type,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
source_id, preset_id = _parse_preset_source_id(media_id)
|
||||
except ValueError as ve:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="preset_non_integer",
|
||||
translation_placeholders={"preset_id": media_id},
|
||||
) from ve
|
||||
if source_id:
|
||||
await self._zone.select_source(source_id)
|
||||
await asyncio.sleep(SELECT_SOURCE_DELAY)
|
||||
if not self._source.presets or preset_id not in self._source.presets:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_preset",
|
||||
translation_placeholders={"preset_id": media_id},
|
||||
)
|
||||
await self._zone.restore_preset(preset_id)
|
||||
|
@ -67,6 +67,15 @@
|
||||
},
|
||||
"command_error": {
|
||||
"message": "Error executing {function_name} on entity {entity_id}"
|
||||
},
|
||||
"unsupported_media_type": {
|
||||
"message": "Unsupported media type for Russound zone: {media_type}"
|
||||
},
|
||||
"missing_preset": {
|
||||
"message": "The specified preset is not available for this source: {preset_id}"
|
||||
},
|
||||
"preset_non_integer": {
|
||||
"message": "Preset must be an integer, got: {preset_id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ def mock_russound_client() -> Generator[AsyncMock]:
|
||||
zone.set_treble = AsyncMock()
|
||||
zone.set_turn_on_volume = AsyncMock()
|
||||
zone.set_loudness = AsyncMock()
|
||||
zone.restore_preset = AsyncMock()
|
||||
|
||||
client.controllers = {
|
||||
1: Controller(
|
||||
|
@ -1,7 +1,14 @@
|
||||
{
|
||||
"1": {
|
||||
"name": "Aux",
|
||||
"type": "Miscellaneous Audio"
|
||||
"type": "RNET AM/FM Tuner (Internal)",
|
||||
"presets": {
|
||||
"1": "WOOD",
|
||||
"2": "89.7 MHz FM",
|
||||
"7": "WWKR",
|
||||
"8": "WKLA",
|
||||
"11": "WGN"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"name": "Spotify",
|
||||
|
@ -9,10 +9,13 @@ import pytest
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
DOMAIN as MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@ -32,7 +35,7 @@ from homeassistant.const import (
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from . import mock_state_update, setup_integration
|
||||
from .const import ENTITY_ID_ZONE_1
|
||||
@ -253,3 +256,94 @@ async def test_media_seek(
|
||||
mock_russound_client.controllers[1].zones[1].set_seek_time.assert_called_once_with(
|
||||
100
|
||||
)
|
||||
|
||||
|
||||
async def test_play_media_preset_item_id(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_russound_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test playing media with a preset item id."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID_ZONE_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "preset",
|
||||
ATTR_MEDIA_CONTENT_ID: "1",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_russound_client.controllers[1].zones[1].restore_preset.assert_called_once_with(
|
||||
1
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID_ZONE_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "preset",
|
||||
ATTR_MEDIA_CONTENT_ID: "1,2",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_russound_client.controllers[1].zones[1].select_source.assert_called_once_with(
|
||||
1
|
||||
)
|
||||
mock_russound_client.controllers[1].zones[1].restore_preset.assert_called_with(2)
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="The specified preset is not available for this source: 10",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID_ZONE_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "preset",
|
||||
ATTR_MEDIA_CONTENT_ID: "10",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match="Preset must be an integer, got: UNKNOWN_PRESET"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID_ZONE_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "preset",
|
||||
ATTR_MEDIA_CONTENT_ID: "UNKNOWN_PRESET",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_play_media_unknown_type(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_russound_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test playing media with an unsupported content type."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Unsupported media type for Russound zone: unsupported_content_type",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID_ZONE_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "unsupported_content_type",
|
||||
ATTR_MEDIA_CONTENT_ID: "1",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user