"""The tests for the DirecTV Media player platform.""" from datetime import datetime, timedelta from unittest.mock import call, patch import pytest import requests 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.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, DOMAIN, MEDIA_TYPE_TVSHOW, SERVICE_PLAY_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, ) 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 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 patch( "homeassistant.components.directv.media_player.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 patch( "homeassistant.components.directv.media_player.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 patch( "homeassistant.components.directv.media_player.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 patch( "homeassistant.components.directv.media_player.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 patch( "homeassistant.components.directv.media_player.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