Add play media capability to Cambridge Audio (#129002)
This commit is contained in:
parent
1663d8dfa9
commit
5f839ad3ee
6 changed files with 239 additions and 1 deletions
|
@ -17,3 +17,7 @@ STREAM_MAGIC_EXCEPTIONS = (
|
|||
)
|
||||
|
||||
CONNECT_TIMEOUT = 5
|
||||
|
||||
CAMBRIDGE_MEDIA_TYPE_PRESET = "preset"
|
||||
CAMBRIDGE_MEDIA_TYPE_AIRABLE = "airable"
|
||||
CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO = "internet_radio"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from aiostreammagic import (
|
||||
RepeatMode as CambridgeRepeatMode,
|
||||
|
@ -21,14 +22,22 @@ from homeassistant.components.media_player import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CAMBRIDGE_MEDIA_TYPE_AIRABLE,
|
||||
CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
|
||||
CAMBRIDGE_MEDIA_TYPE_PRESET,
|
||||
DOMAIN,
|
||||
)
|
||||
from .entity import CambridgeAudioEntity, command
|
||||
|
||||
BASE_FEATURES = (
|
||||
MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
)
|
||||
|
||||
PREAMP_FEATURES = (
|
||||
|
@ -285,3 +294,48 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
|||
if repeat in {RepeatMode.ALL, RepeatMode.ONE}:
|
||||
repeat_mode = CambridgeRepeatMode.ALL
|
||||
await self.client.set_repeat(repeat_mode)
|
||||
|
||||
@command
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play media on the Cambridge Audio device."""
|
||||
|
||||
if media_type not in {
|
||||
CAMBRIDGE_MEDIA_TYPE_PRESET,
|
||||
CAMBRIDGE_MEDIA_TYPE_AIRABLE,
|
||||
CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
|
||||
}:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unsupported_media_type",
|
||||
translation_placeholders={"media_type": media_type},
|
||||
)
|
||||
|
||||
if media_type == CAMBRIDGE_MEDIA_TYPE_PRESET:
|
||||
try:
|
||||
preset_id = int(media_id)
|
||||
except ValueError as ve:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="preset_non_integer",
|
||||
translation_placeholders={"preset_id": media_id},
|
||||
) from ve
|
||||
preset = None
|
||||
for _preset in self.client.preset_list.presets:
|
||||
if _preset.preset_id == preset_id:
|
||||
preset = _preset
|
||||
if not preset:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_preset",
|
||||
translation_placeholders={"preset_id": media_id},
|
||||
)
|
||||
await self.client.recall_preset(preset.preset_id)
|
||||
|
||||
if media_type == CAMBRIDGE_MEDIA_TYPE_AIRABLE:
|
||||
preset_id = int(media_id)
|
||||
await self.client.play_radio_airable("Radio", preset_id)
|
||||
|
||||
if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
|
||||
await self.client.play_radio_url("Radio", media_id)
|
||||
|
|
|
@ -34,5 +34,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"unsupported_media_type": {
|
||||
"message": "Unsupported media type for Cambridge Audio device: {media_type}"
|
||||
},
|
||||
"missing_preset": {
|
||||
"message": "Missing preset for media_id: {preset_id}"
|
||||
},
|
||||
"preset_non_integer": {
|
||||
"message": "Preset must be an integer, got: {preset_id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,15 @@
|
|||
from collections.abc import Generator
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiostreammagic.models import Display, Info, NowPlaying, PlayState, Source, State
|
||||
from aiostreammagic.models import (
|
||||
Display,
|
||||
Info,
|
||||
NowPlaying,
|
||||
PlayState,
|
||||
PresetList,
|
||||
Source,
|
||||
State,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cambridge_audio.const import DOMAIN
|
||||
|
@ -51,6 +59,9 @@ def mock_stream_magic_client() -> Generator[AsyncMock]:
|
|||
load_fixture("get_now_playing.json", DOMAIN)
|
||||
)
|
||||
client.display = Display.from_json(load_fixture("get_display.json", DOMAIN))
|
||||
client.preset_list = PresetList.from_json(
|
||||
load_fixture("get_presets_list.json", DOMAIN)
|
||||
)
|
||||
client.is_connected = Mock(return_value=True)
|
||||
client.position_last_updated = client.play_state.position
|
||||
client.unregister_state_update_callbacks = AsyncMock(return_value=True)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"start": 1,
|
||||
"end": 99,
|
||||
"max_presets": 99,
|
||||
"presettable": true,
|
||||
"presets": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Chicago House Radio",
|
||||
"type": "Radio",
|
||||
"class": "stream.radio",
|
||||
"state": "OK",
|
||||
"is_playing": false,
|
||||
"art_url": "https://static.airable.io/43/68/432868.png",
|
||||
"airable_radio_id": 5317566146608442
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Spotify: Good & Evil",
|
||||
"type": "Spotify",
|
||||
"class": "stream.service.spotify",
|
||||
"state": "OK",
|
||||
"is_playing": true,
|
||||
"art_url": "https://i.scdn.co/image/ab67616d0000b27325a5a1ed28871e8e53e62d59"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Unknown Preset Type",
|
||||
"type": "Unknown",
|
||||
"class": "stream.unknown",
|
||||
"state": "OK"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -11,10 +11,13 @@ from aiostreammagic.models import CallbackType
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_REPEAT,
|
||||
ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_MEDIA_SHUFFLE,
|
||||
DOMAIN as MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
MediaPlayerEntityFeature,
|
||||
RepeatMode,
|
||||
)
|
||||
|
@ -40,6 +43,7 @@ from homeassistant.const import (
|
|||
STATE_STANDBY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from . import setup_integration
|
||||
from .const import ENTITY_ID
|
||||
|
@ -301,3 +305,123 @@ async def test_media_seek(
|
|||
)
|
||||
|
||||
mock_stream_magic_client.media_seek.assert_called_once_with(100)
|
||||
|
||||
|
||||
async def test_play_media_preset_item_id(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_stream_magic_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,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "preset",
|
||||
ATTR_MEDIA_CONTENT_ID: "1",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_stream_magic_client.recall_preset.call_count == 1
|
||||
assert mock_stream_magic_client.recall_preset.call_args_list[0].args[0] == 1
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="Missing preset for media_id: 10"):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
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,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "preset",
|
||||
ATTR_MEDIA_CONTENT_ID: "UNKNOWN_PRESET",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_play_media_airable_radio_id(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_stream_magic_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test playing media with an airable radio id."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "airable",
|
||||
ATTR_MEDIA_CONTENT_ID: "12345678",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_stream_magic_client.play_radio_airable.call_count == 1
|
||||
call_args = mock_stream_magic_client.play_radio_airable.call_args_list[0].args
|
||||
assert call_args[0] == "Radio"
|
||||
assert call_args[1] == 12345678
|
||||
|
||||
|
||||
async def test_play_media_internet_radio(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_stream_magic_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test playing media with a url."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "internet_radio",
|
||||
ATTR_MEDIA_CONTENT_ID: "https://example.com",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_stream_magic_client.play_radio_url.call_count == 1
|
||||
call_args = mock_stream_magic_client.play_radio_url.call_args_list[0].args
|
||||
assert call_args[0] == "Radio"
|
||||
assert call_args[1] == "https://example.com"
|
||||
|
||||
|
||||
async def test_play_media_unknown_type(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_stream_magic_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 Cambridge Audio device: unsupported_content_type",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "unsupported_content_type",
|
||||
ATTR_MEDIA_CONTENT_ID: "1",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue