Add media player test to Cambridge Audio (#125780)

* Add media player tests to Cambridge Audio

* Add media player tests to Cambridge Audio

* Remove unnecessary test case

* Move state_update call out of mock

* Update tests/components/cambridge_audio/test_media_player.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Noah Husby 2024-09-11 16:06:03 -04:00 committed by GitHub
parent 98728d37a6
commit 610e9239a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 381 additions and 2 deletions

View file

@ -3,13 +3,13 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from aiostreammagic.models import Info from aiostreammagic.models import Info, NowPlaying, PlayState, Source, State
import pytest import pytest
from homeassistant.components.cambridge_audio.const import DOMAIN from homeassistant.components.cambridge_audio.const import DOMAIN
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_fixture, load_json_array_fixture
from tests.components.smhi.common import AsyncMock from tests.components.smhi.common import AsyncMock
@ -39,7 +39,20 @@ def mock_stream_magic_client() -> Generator[AsyncMock]:
client = mock_client.return_value client = mock_client.return_value
client.host = "192.168.20.218" client.host = "192.168.20.218"
client.info = Info.from_json(load_fixture("get_info.json", DOMAIN)) client.info = Info.from_json(load_fixture("get_info.json", DOMAIN))
client.sources = [
Source.from_dict(x)
for x in load_json_array_fixture("get_sources.json", DOMAIN)
]
client.state = State.from_json(load_fixture("get_state.json", DOMAIN))
client.play_state = PlayState.from_json(
load_fixture("get_play_state.json", DOMAIN)
)
client.now_playing = NowPlaying.from_json(
load_fixture("get_now_playing.json", DOMAIN)
)
client.is_connected = Mock(return_value=True) client.is_connected = Mock(return_value=True)
client.position_last_updated = client.play_state.position
client.unregister_state_update_callbacks = AsyncMock(return_value=True)
yield client yield client

View file

@ -0,0 +1,6 @@
"""Constants for Cambridge Audio integration tests."""
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
DEVICE_NAME = "cambridge_audio_cxnv2"
ENTITY_ID = f"{MP_DOMAIN}.{DEVICE_NAME}"

View file

@ -0,0 +1,25 @@
{
"state": "PLAYING",
"source": {
"id": "AIRPLAY",
"name": "AirPlay"
},
"allow_apd": false,
"listening_on": "Listening on Cambridge Audio CXNv2 - AirPlay",
"display": {
"line1": "Holiday",
"line2": "Green Day",
"line3": "Greatest Hits: God's Favorite Band",
"format": "44.1kHz/16bit ALAC",
"mqa": "none",
"playback_source": "iPhone",
"class": "stream.service.airplay",
"art_file": "/tmp/current/AlbumArtFile-811-363",
"art_url": "http://192.168.20.218:80/album-art-2d89?id=1:246",
"progress": {
"position": 216,
"duration": 232
}
},
"controls": ["play_pause", "track_next", "track_previous"]
}

View file

@ -0,0 +1,22 @@
{
"state": "play",
"position": 179,
"presettable": false,
"mode_repeat": "off",
"mode_shuffle": "off",
"metadata": {
"class": "md.track",
"source": "AIRPLAY",
"name": "AirPlay",
"duration": 232,
"album": "Greatest Hits: God's Favorite Band",
"artist": "Green Day",
"title": "Holiday",
"art_url": "http://192.168.20.218:80/album-art-2d89?id=1:246",
"mqa": "none",
"codec": "ALAC",
"lossless": true,
"sample_rate": 44100,
"bit_depth": 16
}
}

View file

@ -0,0 +1,113 @@
[
{
"id": "IR",
"name": "Internet Radio",
"default_name": "Internet Radio",
"class": "stream.radio",
"nameable": false,
"ui_selectable": false,
"description": "Internet Radio",
"description_locale": "Internet Radio",
"preferred_order": 9
},
{
"id": "USB_AUDIO",
"name": "USB Audio",
"default_name": "USB Audio",
"class": "digital.usb",
"nameable": true,
"ui_selectable": true,
"description": "USB Audio",
"description_locale": "USB Audio",
"preferred_order": 1
},
{
"id": "SPDIF_COAX",
"name": "D2",
"default_name": "D2",
"class": "digital.coax",
"nameable": true,
"ui_selectable": false,
"description": "Digital Co-axial",
"description_locale": "Digital Co-axial",
"preferred_order": 3
},
{
"id": "SPDIF_TOSLINK",
"name": "D1",
"default_name": "D1",
"class": "digital.toslink",
"nameable": true,
"ui_selectable": false,
"description": "Digital Optical",
"description_locale": "Digital Optical",
"preferred_order": 2
},
{
"id": "MEDIA_PLAYER",
"name": "Media Library",
"default_name": "Media Library",
"class": "stream.media",
"nameable": false,
"ui_selectable": true,
"description": "Media Player",
"description_locale": "Media Player",
"preferred_order": 10
},
{
"id": "AIRPLAY",
"name": "AirPlay",
"default_name": "AirPlay",
"class": "stream.service.airplay",
"nameable": false,
"ui_selectable": true,
"description": "AirPlay",
"description_locale": "AirPlay",
"preferred_order": 11
},
{
"id": "SPOTIFY",
"name": "Spotify",
"default_name": "Spotify",
"class": "stream.service.spotify",
"nameable": false,
"ui_selectable": true,
"description": "Spotify",
"description_locale": "Spotify",
"preferred_order": 6,
"normalisation": "off"
},
{
"id": "CAST",
"name": "Chromecast built-in",
"default_name": "Chromecast built-in",
"class": "stream.service.cast",
"nameable": false,
"ui_selectable": true,
"description": "Chromecast built-in",
"description_locale": "Chromecast built-in",
"preferred_order": 8
},
{
"id": "ROON",
"name": "Roon Ready",
"default_name": "Roon Ready",
"class": "stream.service.roon",
"nameable": false,
"ui_selectable": false,
"description": "Roon Ready",
"description_locale": "Roon Ready",
"preferred_order": 5
},
{
"id": "TIDAL",
"name": "TIDAL Connect",
"default_name": "TIDAL Connect",
"class": "stream.service.tidal",
"nameable": false,
"ui_selectable": false,
"description": "TIDAL",
"description_locale": "TIDAL",
"preferred_order": 7
}
]

View file

@ -0,0 +1,7 @@
{
"source": "AIRPLAY",
"power": true,
"pre_amp_mode": false,
"pre_amp_state": "disabled_user",
"cbus": "off"
}

View file

@ -0,0 +1,193 @@
"""Tests for the Cambridge Audio integration."""
from unittest.mock import AsyncMock
from aiostreammagic import TransportControl
import pytest
from homeassistant.components.media_player import (
DOMAIN as MP_DOMAIN,
MediaPlayerEntityFeature,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
STATE_BUFFERING,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
STATE_STANDBY,
)
from homeassistant.core import HomeAssistant
from . import setup_integration
from .const import ENTITY_ID
from tests.common import MockConfigEntry
async def mock_state_update(client: AsyncMock) -> None:
"""Trigger a callback in the media player."""
await client.register_state_update_callbacks.call_args[0][0](client)
async def test_entity_supported_features(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test entity attributes."""
await setup_integration(hass, mock_config_entry)
await mock_state_update(mock_stream_magic_client)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
attrs = state.attributes
# Ensure volume isn't available when pre-amp is disabled
assert not mock_stream_magic_client.state.pre_amp_mode
assert (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
not in attrs[ATTR_SUPPORTED_FEATURES]
)
# Check for basic media controls
assert {
TransportControl.PLAY_PAUSE,
TransportControl.TRACK_NEXT,
TransportControl.TRACK_PREVIOUS,
}.issubset(mock_stream_magic_client.now_playing.controls)
assert (
MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PREVIOUS_TRACK
in attrs[ATTR_SUPPORTED_FEATURES]
)
assert (
MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.SEEK
not in attrs[ATTR_SUPPORTED_FEATURES]
)
mock_stream_magic_client.now_playing.controls = [
TransportControl.TOGGLE_REPEAT,
TransportControl.TOGGLE_SHUFFLE,
TransportControl.SEEK,
]
await mock_state_update(mock_stream_magic_client)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
attrs = state.attributes
assert (
MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.SEEK
in attrs[ATTR_SUPPORTED_FEATURES]
)
mock_stream_magic_client.state.pre_amp_mode = True
await mock_state_update(mock_stream_magic_client)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
attrs = state.attributes
assert (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
in attrs[ATTR_SUPPORTED_FEATURES]
)
@pytest.mark.parametrize(
("power_state", "play_state", "media_player_state"),
[
(True, "NETWORK", STATE_STANDBY),
(False, "NETWORK", STATE_STANDBY),
(False, "play", STATE_OFF),
(True, "play", STATE_PLAYING),
(True, "pause", STATE_PAUSED),
(True, "connecting", STATE_BUFFERING),
(True, "stop", STATE_IDLE),
(True, "ready", STATE_IDLE),
],
)
async def test_entity_state(
hass: HomeAssistant,
mock_stream_magic_client: AsyncMock,
mock_config_entry: MockConfigEntry,
power_state: bool,
play_state: str,
media_player_state: str,
) -> None:
"""Test media player state."""
await setup_integration(hass, mock_config_entry)
mock_stream_magic_client.state.power = power_state
mock_stream_magic_client.play_state.state = play_state
await mock_state_update(mock_stream_magic_client)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.state == media_player_state
async def test_media_play_pause_stop(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_stream_magic_client: AsyncMock,
) -> None:
"""Test media next previous track service."""
await setup_integration(hass, mock_config_entry)
data = {ATTR_ENTITY_ID: ENTITY_ID}
# Test for play/pause command when separate play and pause controls are unavailable
await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PAUSE, data, True)
mock_stream_magic_client.play_pause.assert_called_once()
await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True)
assert mock_stream_magic_client.play_pause.call_count == 2
# Test for separate play and pause controls
mock_stream_magic_client.now_playing.controls = [
TransportControl.PLAY,
TransportControl.PAUSE,
]
await mock_state_update(mock_stream_magic_client)
await hass.async_block_till_done()
await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PAUSE, data, True)
mock_stream_magic_client.pause.assert_called_once()
await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True)
mock_stream_magic_client.play.assert_called_once()
async def test_media_next_previous_track(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_stream_magic_client: AsyncMock,
) -> None:
"""Test media next previous track service."""
await setup_integration(hass, mock_config_entry)
data = {ATTR_ENTITY_ID: ENTITY_ID}
await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data, True)
mock_stream_magic_client.next_track.assert_called_once()
await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data, True)
mock_stream_magic_client.previous_track.assert_called_once()