"""The tests for the DirecTV Media player platform.""" from unittest.mock import call, patch from datetime import datetime, timedelta import requests import pytest from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, MEDIA_TYPE_TVSHOW, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_DURATION, ATTR_MEDIA_TITLE, ATTR_MEDIA_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_CHANNEL, ATTR_INPUT_SOURCE, ATTR_MEDIA_POSITION_UPDATED_AT, DOMAIN, SERVICE_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY) from homeassistant.components.directv.media_player import ( ATTR_MEDIA_CURRENTLY_RECORDING, ATTR_MEDIA_RATING, ATTR_MEDIA_RECORDED, ATTR_MEDIA_START_TIME, DEFAULT_DEVICE, DEFAULT_PORT) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import MockDependency, async_fire_time_changed CLIENT_ENTITY_ID = 'media_player.client_dvr' MAIN_ENTITY_ID = 'media_player.main_dvr' IP_ADDRESS = '127.0.0.1' DISCOVERY_INFO = { 'host': IP_ADDRESS, 'serial': 1234 } LIVE = { "callsign": "HASSTV", "date": "20181110", "duration": 3600, "isOffAir": False, "isPclocked": 1, "isPpv": False, "isRecording": False, "isVod": False, "major": 202, "minor": 65535, "offset": 1, "programId": "102454523", "rating": "No Rating", "startTime": 1541876400, "stationId": 3900947, "title": "Using Home Assistant to automate your home" } LOCATIONS = [ { 'locationName': 'Main DVR', 'clientAddr': DEFAULT_DEVICE } ] RECORDING = { "callsign": "HASSTV", "date": "20181110", "duration": 3600, "isOffAir": False, "isPclocked": 1, "isPpv": False, "isRecording": True, "isVod": False, "major": 202, "minor": 65535, "offset": 1, "programId": "102454523", "rating": "No Rating", "startTime": 1541876400, "stationId": 3900947, "title": "Using Home Assistant to automate your home", 'uniqueId': '12345', 'episodeTitle': 'Configure DirecTV platform.' } WORKING_CONFIG = { 'media_player': { 'platform': 'directv', CONF_HOST: IP_ADDRESS, CONF_NAME: 'Main DVR', CONF_PORT: DEFAULT_PORT, CONF_DEVICE: DEFAULT_DEVICE } } @pytest.fixture def client_dtv(): """Fixture for a client device.""" mocked_dtv = MockDirectvClass('mock_ip') mocked_dtv.attributes = RECORDING mocked_dtv._standby = False return mocked_dtv @pytest.fixture def main_dtv(): """Fixture for main DVR.""" return MockDirectvClass('mock_ip') @pytest.fixture def dtv_side_effect(client_dtv, main_dtv): """Fixture to create DIRECTV instance for main and client.""" def mock_dtv(ip, port, client_addr): if client_addr != '0': mocked_dtv = client_dtv else: mocked_dtv = main_dtv mocked_dtv._host = ip mocked_dtv._port = port mocked_dtv._device = client_addr return mocked_dtv return mock_dtv @pytest.fixture def mock_now(): """Fixture for dtutil.now.""" return dt_util.utcnow() @pytest.fixture def platforms(hass, dtv_side_effect, mock_now): """Fixture for setting up test platforms.""" config = { 'media_player': [{ 'platform': 'directv', 'name': 'Main DVR', 'host': IP_ADDRESS, 'port': DEFAULT_PORT, 'device': DEFAULT_DEVICE }, { 'platform': 'directv', 'name': 'Client DVR', 'host': IP_ADDRESS, 'port': DEFAULT_PORT, 'device': '1' }] } with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', side_effect=dtv_side_effect), \ patch('homeassistant.util.dt.utcnow', return_value=mock_now): hass.loop.run_until_complete(async_setup_component( hass, DOMAIN, config)) hass.loop.run_until_complete(hass.async_block_till_done()) yield async def async_turn_on(hass, entity_id=None): """Turn on specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) async def async_turn_off(hass, entity_id=None): """Turn off specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) async def async_media_pause(hass, entity_id=None): """Send the media player the command for pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_MEDIA_PAUSE, data) async def async_media_play(hass, entity_id=None): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_MEDIA_PLAY, data) async def async_media_stop(hass, entity_id=None): """Send the media player the command for stop.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_MEDIA_STOP, data) async def async_media_next_track(hass, entity_id=None): """Send the media player the command for next track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) async def async_media_previous_track(hass, entity_id=None): """Send the media player the command for prev track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) async def async_play_media(hass, media_type, media_id, entity_id=None, enqueue=None): """Send the media player the command for playing media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} if entity_id: data[ATTR_ENTITY_ID] = entity_id if enqueue: data[ATTR_MEDIA_ENQUEUE] = enqueue await hass.services.async_call(DOMAIN, SERVICE_PLAY_MEDIA, data) class MockDirectvClass: """A fake DirecTV DVR device.""" def __init__(self, ip, port=8080, clientAddr='0'): """Initialize the fake DirecTV device.""" self._host = ip self._port = port self._device = clientAddr self._standby = True self._play = False self._locations = LOCATIONS self.attributes = LIVE def get_locations(self): """Mock for get_locations method.""" test_locations = { 'locations': self._locations, 'status': { 'code': 200, 'commandResult': 0, 'msg': 'OK.', 'query': '/info/getLocations' } } return test_locations def get_standby(self): """Mock for get_standby method.""" return self._standby def get_tuned(self): """Mock for get_tuned method.""" if self._play: self.attributes['offset'] = self.attributes['offset']+1 test_attributes = self.attributes test_attributes['status'] = { "code": 200, "commandResult": 0, "msg": "OK.", "query": "/tv/getTuned" } return test_attributes def key_press(self, keypress): """Mock for key_press method.""" if keypress == 'poweron': self._standby = False self._play = True elif keypress == 'poweroff': self._standby = True self._play = False elif keypress == 'play': self._play = True elif keypress == 'pause' or keypress == 'stop': self._play = False def tune_channel(self, source): """Mock for tune_channel method.""" self.attributes['major'] = int(source) async def test_setup_platform_config(hass): """Test setting up the platform from configuration.""" with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state assert len(hass.states.async_entity_ids('media_player')) == 1 async def test_setup_platform_discover(hass): """Test setting up the platform from discovery.""" with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): hass.async_create_task( async_load_platform(hass, DOMAIN, 'directv', DISCOVERY_INFO, {'media_player': {}}) ) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state assert len(hass.states.async_entity_ids('media_player')) == 1 async def test_setup_platform_discover_duplicate(hass): """Test setting up the platform from discovery.""" with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() hass.async_create_task( async_load_platform(hass, DOMAIN, 'directv', DISCOVERY_INFO, {'media_player': {}}) ) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state assert len(hass.states.async_entity_ids('media_player')) == 1 async def test_setup_platform_discover_client(hass): """Test setting up the platform from discovery.""" LOCATIONS.append({ 'locationName': 'Client 1', 'clientAddr': '1' }) LOCATIONS.append({ 'locationName': 'Client 2', 'clientAddr': '2' }) with MockDependency('DirectPy'), \ patch('DirectPy.DIRECTV', new=MockDirectvClass): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() hass.async_create_task( async_load_platform(hass, DOMAIN, 'directv', DISCOVERY_INFO, {'media_player': {}}) ) await hass.async_block_till_done() del LOCATIONS[-1] del LOCATIONS[-1] state = hass.states.get(MAIN_ENTITY_ID) assert state state = hass.states.get('media_player.client_1') assert state state = hass.states.get('media_player.client_2') assert state assert len(hass.states.async_entity_ids('media_player')) == 3 async def test_supported_features(hass, platforms): """Test supported features.""" # Features supported for main DVR state = hass.states.get(MAIN_ENTITY_ID) assert SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF |\ SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK |\ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY ==\ state.attributes.get('supported_features') # Feature supported for clients. state = hass.states.get(CLIENT_ENTITY_ID) assert SUPPORT_PAUSE |\ SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK |\ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY ==\ state.attributes.get('supported_features') async def test_check_attributes(hass, platforms, mock_now): """Test attributes.""" next_update = mock_now + timedelta(minutes=5) with patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() # Start playing TV with patch('homeassistant.util.dt.utcnow', return_value=next_update): await async_media_play(hass, CLIENT_ENTITY_ID) await hass.async_block_till_done() state = hass.states.get(CLIENT_ENTITY_ID) assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == \ RECORDING['programId'] assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == \ MEDIA_TYPE_TVSHOW assert state.attributes.get(ATTR_MEDIA_DURATION) == \ RECORDING['duration'] assert state.attributes.get(ATTR_MEDIA_POSITION) == 2 assert state.attributes.get( ATTR_MEDIA_POSITION_UPDATED_AT) == next_update assert state.attributes.get(ATTR_MEDIA_TITLE) == RECORDING['title'] assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == \ RECORDING['episodeTitle'] assert state.attributes.get(ATTR_MEDIA_CHANNEL) == \ "{} ({})".format(RECORDING['callsign'], RECORDING['major']) assert state.attributes.get(ATTR_INPUT_SOURCE) == RECORDING['major'] assert state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) == \ RECORDING['isRecording'] assert state.attributes.get(ATTR_MEDIA_RATING) == RECORDING['rating'] assert state.attributes.get(ATTR_MEDIA_RECORDED) assert state.attributes.get(ATTR_MEDIA_START_TIME) == \ datetime(2018, 11, 10, 19, 0, tzinfo=dt_util.UTC) # Test to make sure that ATTR_MEDIA_POSITION_UPDATED_AT is not # updated if TV is paused. with patch('homeassistant.util.dt.utcnow', return_value=next_update + timedelta(minutes=5)): await async_media_pause(hass, CLIENT_ENTITY_ID) await hass.async_block_till_done() state = hass.states.get(CLIENT_ENTITY_ID) assert state.state == STATE_PAUSED assert state.attributes.get( ATTR_MEDIA_POSITION_UPDATED_AT) == next_update async def test_main_services(hass, platforms, main_dtv, mock_now): """Test the different services.""" next_update = mock_now + timedelta(minutes=5) with patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() # DVR starts in off state. state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_OFF # All these should call key_press in our class. with patch.object(main_dtv, 'key_press', wraps=main_dtv.key_press) as mock_key_press, \ patch.object(main_dtv, 'tune_channel', wraps=main_dtv.tune_channel) as mock_tune_channel, \ patch.object(main_dtv, 'get_tuned', wraps=main_dtv.get_tuned) as mock_get_tuned, \ patch.object(main_dtv, 'get_standby', wraps=main_dtv.get_standby) as mock_get_standby: # Turn main DVR on. When turning on DVR is playing. await async_turn_on(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() assert mock_key_press.called assert mock_key_press.call_args == call('poweron') state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_PLAYING # Pause live TV. await async_media_pause(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() assert mock_key_press.called assert mock_key_press.call_args == call('pause') state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_PAUSED # Start play again for live TV. await async_media_play(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() assert mock_key_press.called assert mock_key_press.call_args == call('play') state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_PLAYING # Change channel, currently it should be 202 assert state.attributes.get('source') == 202 await async_play_media(hass, 'channel', 7, MAIN_ENTITY_ID) await hass.async_block_till_done() assert mock_tune_channel.called assert mock_tune_channel.call_args == call('7') state = hass.states.get(MAIN_ENTITY_ID) assert state.attributes.get('source') == 7 # Stop live TV. await async_media_stop(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() assert mock_key_press.called assert mock_key_press.call_args == call('stop') state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_PAUSED # Turn main DVR off. await async_turn_off(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() assert mock_key_press.called assert mock_key_press.call_args == call('poweroff') state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_OFF # There should have been 6 calls to check if DVR is in standby assert main_dtv.get_standby.call_count == 6 assert mock_get_standby.call_count == 6 # There should be 5 calls to get current info (only 1 time it will # not be called as DVR is in standby.) assert main_dtv.get_tuned.call_count == 5 assert mock_get_tuned.call_count == 5 async def test_available(hass, platforms, main_dtv, mock_now): """Test available status.""" next_update = mock_now + timedelta(minutes=5) with patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() # Confirm service is currently set to available. state = hass.states.get(MAIN_ENTITY_ID) assert state.state != STATE_UNAVAILABLE # Make update fail 1st time next_update = next_update + timedelta(minutes=5) with patch.object( main_dtv, 'get_standby', side_effect=requests.RequestException), \ patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state.state != STATE_UNAVAILABLE # Make update fail 2nd time within 1 minute next_update = next_update + timedelta(seconds=30) with patch.object( main_dtv, 'get_standby', side_effect=requests.RequestException), \ patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state.state != STATE_UNAVAILABLE # Make update fail 3rd time more then a minute after 1st failure next_update = next_update + timedelta(minutes=1) with patch.object( main_dtv, 'get_standby', side_effect=requests.RequestException), \ patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_UNAVAILABLE # Recheck state, update should work again. next_update = next_update + timedelta(minutes=5) with patch('homeassistant.util.dt.utcnow', return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() state = hass.states.get(MAIN_ENTITY_ID) assert state.state != STATE_UNAVAILABLE