core/tests/components/cast/test_media_player.py

590 lines
22 KiB
Python
Raw Normal View History

2016-03-09 09:25:50 +00:00
"""The tests for the Cast Media player platform."""
# pylint: disable=protected-access
import asyncio
from typing import Optional
from unittest.mock import patch, MagicMock, Mock
from uuid import UUID
import attr
import pytest
2016-02-02 05:07:33 +00:00
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.components.cast.media_player import ChromecastInfo
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.cast import media_player as cast
from homeassistant.setup import async_setup_component
2016-02-02 05:07:33 +00:00
2018-06-25 19:59:05 +00:00
from tests.common import MockConfigEntry, mock_coro
2016-02-02 05:07:33 +00:00
@pytest.fixture(autouse=True)
def cast_mock():
"""Mock pychromecast."""
with patch.dict('sys.modules', {
'pychromecast': MagicMock(),
'pychromecast.controllers.multizone': MagicMock(),
}):
yield
# pylint: disable=invalid-name
FakeUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e2')
FakeGroupUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e3')
def get_fake_chromecast(info: ChromecastInfo):
"""Generate a Fake Chromecast object with the specified arguments."""
mock = MagicMock(host=info.host, port=info.port, uuid=info.uuid)
mock.media_controller.status = None
return mock
def get_fake_chromecast_info(host='192.168.178.42', port=8009,
uuid: Optional[UUID] = FakeUUID):
"""Generate a Fake ChromecastInfo with the specified arguments."""
return ChromecastInfo(host=host, port=port, uuid=uuid,
friendly_name="Speaker", service='the-service')
async def async_setup_cast(hass, config=None, discovery_info=None):
"""Set up the cast platform."""
if config is None:
config = {}
add_entities = Mock()
await cast.async_setup_platform(hass, config, add_entities,
discovery_info=discovery_info)
await hass.async_block_till_done()
return add_entities
async def async_setup_cast_internal_discovery(hass, config=None,
discovery_info=None):
2018-08-19 20:29:08 +00:00
"""Set up the cast platform and the discovery."""
listener = MagicMock(services={})
browser = MagicMock(zc={})
with patch('pychromecast.start_discovery',
return_value=(listener, browser)) as start_discovery:
add_entities = await async_setup_cast(hass, config, discovery_info)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert start_discovery.call_count == 1
discovery_callback = start_discovery.call_args[0][0]
def discover_chromecast(service_name: str, info: ChromecastInfo) -> None:
"""Discover a chromecast device."""
2018-08-29 13:46:09 +00:00
listener.services[service_name] = (
info.host, info.port, info.uuid, info.model_name,
info.friendly_name
)
discovery_callback(service_name)
return discover_chromecast, add_entities
async def async_setup_media_player_cast(hass: HomeAssistantType,
info: ChromecastInfo):
2018-08-19 20:29:08 +00:00
"""Set up the cast platform with async_setup_component."""
chromecast = get_fake_chromecast(info)
cast.CastStatusListener = MagicMock()
with patch('pychromecast._get_chromecast_from_host',
return_value=chromecast) as get_chromecast:
await async_setup_component(hass, 'media_player', {
'media_player': {'platform': 'cast', 'host': info.host}})
await hass.async_block_till_done()
assert get_chromecast.call_count == 1
assert cast.CastStatusListener.call_count == 1
entity = cast.CastStatusListener.call_args[0][0]
return chromecast, entity
@asyncio.coroutine
def test_start_discovery_called_once(hass):
"""Test pychromecast.start_discovery called exactly once."""
with patch('pychromecast.start_discovery',
return_value=(None, None)) as start_discovery:
yield from async_setup_cast(hass)
assert start_discovery.call_count == 1
yield from async_setup_cast(hass)
assert start_discovery.call_count == 1
@asyncio.coroutine
def test_stop_discovery_called_on_stop(hass):
"""Test pychromecast.stop_discovery called on shutdown."""
browser = MagicMock(zc={})
with patch('pychromecast.start_discovery',
return_value=(None, browser)) as start_discovery:
# start_discovery should be called with empty config
yield from async_setup_cast(hass, {})
assert start_discovery.call_count == 1
with patch('pychromecast.stop_discovery') as stop_discovery:
# stop discovery should be called on shutdown
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
yield from hass.async_block_till_done()
stop_discovery.assert_called_once_with(browser)
with patch('pychromecast.start_discovery',
return_value=(None, browser)) as start_discovery:
# start_discovery should be called again on re-startup
yield from async_setup_cast(hass)
assert start_discovery.call_count == 1
async def test_internal_discovery_callback_fill_out(hass):
"""Test internal discovery automatically filling out information."""
import pychromecast # imports mock pychromecast
pychromecast.ChromecastConnectionError = IOError
discover_cast, _ = await async_setup_cast_internal_discovery(hass)
info = get_fake_chromecast_info(uuid=None)
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
signal = MagicMock()
async_dispatcher_connect(hass, 'cast_discovered', signal)
discover_cast('the-service', info)
await hass.async_block_till_done()
# when called with incomplete info, it should use HTTP to get missing
discover = signal.mock_calls[0][1][0]
2018-08-29 13:46:09 +00:00
assert discover == full_info
async def test_create_cast_device_without_uuid(hass):
"""Test create a cast device with no UUId should still create an entity."""
info = get_fake_chromecast_info(uuid=None)
cast_device = cast._async_create_cast_device(hass, info)
assert cast_device is not None
async def test_create_cast_device_with_uuid(hass):
"""Test create cast devices with UUID creates entities."""
added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = set()
info = get_fake_chromecast_info()
cast_device = cast._async_create_cast_device(hass, info)
assert cast_device is not None
assert info.uuid in added_casts
# Sending second time should not create new entity
cast_device = cast._async_create_cast_device(hass, info)
assert cast_device is None
async def test_normal_chromecast_not_starting_discovery(hass):
"""Test cast platform not starting discovery when not required."""
# pylint: disable=no-member
with patch('homeassistant.components.cast.media_player.'
'_setup_internal_discovery') as setup_discovery:
# normal (non-group) chromecast shouldn't start discovery.
add_entities = await async_setup_cast(hass, {'host': 'host1'})
await hass.async_block_till_done()
assert add_entities.call_count == 1
assert setup_discovery.call_count == 0
# Same entity twice
add_entities = await async_setup_cast(hass, {'host': 'host1'})
await hass.async_block_till_done()
assert add_entities.call_count == 0
assert setup_discovery.call_count == 0
hass.data[cast.ADDED_CAST_DEVICES_KEY] = set()
add_entities = await async_setup_cast(
hass, discovery_info={'host': 'host1', 'port': 8009})
await hass.async_block_till_done()
assert add_entities.call_count == 1
assert setup_discovery.call_count == 0
# group should start discovery.
hass.data[cast.ADDED_CAST_DEVICES_KEY] = set()
add_entities = await async_setup_cast(
hass, discovery_info={'host': 'host1', 'port': 42})
await hass.async_block_till_done()
assert add_entities.call_count == 0
assert setup_discovery.call_count == 1
async def test_replay_past_chromecasts(hass):
"""Test cast platform re-playing past chromecasts when adding new one."""
cast_group1 = get_fake_chromecast_info(host='host1', port=42)
cast_group2 = get_fake_chromecast_info(host='host2', port=42, uuid=UUID(
'9462202c-e747-4af5-a66b-7dce0e1ebc09'))
discover_cast, add_dev1 = await async_setup_cast_internal_discovery(
hass, discovery_info={'host': 'host1', 'port': 42})
discover_cast('service2', cast_group2)
await hass.async_block_till_done()
assert add_dev1.call_count == 0
discover_cast('service1', cast_group1)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
assert add_dev1.call_count == 1
add_dev2 = await async_setup_cast(
hass, discovery_info={'host': 'host2', 'port': 42})
await hass.async_block_till_done()
assert add_dev2.call_count == 1
async def test_entity_media_states(hass: HomeAssistantType):
"""Test various entity media states."""
info = get_fake_chromecast_info()
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state is not None
assert state.name == 'Speaker'
assert state.state == 'unknown'
assert entity.unique_id == full_info.uuid
media_status = MagicMock(images=None)
media_status.player_is_playing = True
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'playing'
media_status.player_is_playing = False
media_status.player_is_paused = True
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'paused'
media_status.player_is_paused = False
media_status.player_is_idle = True
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'idle'
media_status.player_is_idle = False
chromecast.is_idle = True
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'off'
chromecast.is_idle = False
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'unknown'
async def test_group_media_states(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state is not None
assert state.name == 'Speaker'
assert state.state == 'unknown'
assert entity.unique_id == full_info.uuid
group_media_status = MagicMock(images=None)
player_media_status = MagicMock(images=None)
# Player has no state, group is playing -> Should report 'playing'
group_media_status.player_is_playing = True
entity.multizone_new_media_status(str(FakeGroupUUID), group_media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'playing'
# Player is paused, group is playing -> Should report 'paused'
player_media_status.player_is_playing = False
player_media_status.player_is_paused = True
entity.new_media_status(player_media_status)
await hass.async_block_till_done()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'paused'
# Player is in unknown state, group is playing -> Should report 'playing'
player_media_status.player_state = "UNKNOWN"
entity.new_media_status(player_media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'playing'
async def test_dynamic_group_media_states(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state is not None
assert state.name == 'Speaker'
assert state.state == 'unknown'
assert entity.unique_id == full_info.uuid
group_media_status = MagicMock(images=None)
player_media_status = MagicMock(images=None)
# Player has no state, dynamic group is playing -> Should report 'playing'
entity._dynamic_group_cast = MagicMock()
group_media_status.player_is_playing = True
entity.new_dynamic_group_media_status(group_media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'playing'
# Player is paused, dynamic group is playing -> Should report 'paused'
player_media_status.player_is_playing = False
player_media_status.player_is_paused = True
entity.new_media_status(player_media_status)
await hass.async_block_till_done()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'paused'
# Player is in unknown state, dynamic group is playing -> Should report
# 'playing'
player_media_status.player_state = "UNKNOWN"
entity.new_media_status(player_media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.state == 'playing'
async def test_group_media_control(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state is not None
assert state.name == 'Speaker'
assert state.state == 'unknown'
assert entity.unique_id == full_info.uuid
group_media_status = MagicMock(images=None)
player_media_status = MagicMock(images=None)
# Player has no state, group is playing -> Should forward calls to group
group_media_status.player_is_playing = True
entity.multizone_new_media_status(str(FakeGroupUUID), group_media_status)
entity.media_play()
grp_media = entity.mz_mgr.get_multizone_mediacontroller(str(FakeGroupUUID))
assert grp_media.play.called
assert not chromecast.media_controller.play.called
# Player is paused, group is playing -> Should not forward
player_media_status.player_is_playing = False
player_media_status.player_is_paused = True
entity.new_media_status(player_media_status)
entity.media_pause()
grp_media = entity.mz_mgr.get_multizone_mediacontroller(str(FakeGroupUUID))
assert not grp_media.pause.called
assert chromecast.media_controller.pause.called
# Player is in unknown state, group is playing -> Should forward to group
player_media_status.player_state = "UNKNOWN"
entity.new_media_status(player_media_status)
entity.media_stop()
grp_media = entity.mz_mgr.get_multizone_mediacontroller(str(FakeGroupUUID))
assert grp_media.stop.called
assert not chromecast.media_controller.stop.called
# Verify play_media is not forwarded
entity.play_media(None, None)
assert not grp_media.play_media.called
assert chromecast.media_controller.play_media.called
async def test_dynamic_group_media_control(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
entity._dynamic_group_cast = MagicMock()
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state is not None
assert state.name == 'Speaker'
assert state.state == 'unknown'
assert entity.unique_id == full_info.uuid
group_media_status = MagicMock(images=None)
player_media_status = MagicMock(images=None)
# Player has no state, dynamic group is playing -> Should forward
group_media_status.player_is_playing = True
entity.new_dynamic_group_media_status(group_media_status)
entity.media_previous_track()
assert entity._dynamic_group_cast.media_controller.queue_prev.called
assert not chromecast.media_controller.queue_prev.called
# Player is paused, dynamic group is playing -> Should not forward
player_media_status.player_is_playing = False
player_media_status.player_is_paused = True
entity.new_media_status(player_media_status)
entity.media_next_track()
assert not entity._dynamic_group_cast.media_controller.queue_next.called
assert chromecast.media_controller.queue_next.called
# Player is in unknown state, dynamic group is playing -> Should forward
player_media_status.player_state = "UNKNOWN"
entity.new_media_status(player_media_status)
entity.media_seek(None)
assert entity._dynamic_group_cast.media_controller.seek.called
assert not chromecast.media_controller.seek.called
# Verify play_media is not forwarded
entity.play_media(None, None)
assert not entity._dynamic_group_cast.media_controller.play_media.called
assert chromecast.media_controller.play_media.called
async def test_disconnect_on_stop(hass: HomeAssistantType):
"""Test cast device disconnects socket on stop."""
info = get_fake_chromecast_info()
with patch('pychromecast.dial.get_device_status', return_value=info):
chromecast, _ = await async_setup_media_player_cast(hass, info)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert chromecast.disconnect.call_count == 1
2018-06-25 19:59:05 +00:00
async def test_entry_setup_no_config(hass: HomeAssistantType):
"""Test setting up entry with no config.."""
await async_setup_component(hass, 'cast', {})
with patch(
'homeassistant.components.cast.media_player._async_setup_platform',
2018-06-25 19:59:05 +00:00
return_value=mock_coro()) as mock_setup:
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == {}
async def test_entry_setup_single_config(hass: HomeAssistantType):
"""Test setting up entry and having a single config option."""
await async_setup_component(hass, 'cast', {
'cast': {
'media_player': {
'host': 'bla'
}
}
})
with patch(
'homeassistant.components.cast.media_player._async_setup_platform',
2018-06-25 19:59:05 +00:00
return_value=mock_coro()) as mock_setup:
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}
async def test_entry_setup_list_config(hass: HomeAssistantType):
"""Test setting up entry and having multiple config options."""
await async_setup_component(hass, 'cast', {
'cast': {
'media_player': [
{'host': 'bla'},
{'host': 'blu'},
]
}
})
with patch(
'homeassistant.components.cast.media_player._async_setup_platform',
2018-06-25 19:59:05 +00:00
return_value=mock_coro()) as mock_setup:
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 2
assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}
assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'}
async def test_entry_setup_platform_not_ready(hass: HomeAssistantType):
"""Test failed setting up entry will raise PlatformNotReady."""
await async_setup_component(hass, 'cast', {
'cast': {
'media_player': {
'host': 'bla'
}
}
})
with patch(
'homeassistant.components.cast.media_player._async_setup_platform',
return_value=mock_coro(exception=Exception)) as mock_setup:
with pytest.raises(PlatformNotReady):
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}