diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 60f423c3dee..7540973ea19 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -287,8 +287,11 @@ class ADBDevice(MediaPlayerDevice): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name - self._apps = APPS.copy() - self._apps.update(apps) + self._app_id_to_name = APPS.copy() + self._app_id_to_name.update(apps) + self._app_name_to_id = { + value: key for key, value in self._app_id_to_name.items() + } self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -328,7 +331,7 @@ class ADBDevice(MediaPlayerDevice): @property def app_name(self): """Return the friendly name of the current app.""" - return self._apps.get(self._current_app, self._current_app) + return self._app_id_to_name.get(self._current_app, self._current_app) @property def available(self): @@ -518,7 +521,7 @@ class FireTVDevice(ADBDevice): super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._get_sources = get_sources - self._running_apps = None + self._sources = None @adb_decorator(override_available=True) def update(self): @@ -538,23 +541,28 @@ class FireTVDevice(ADBDevice): return # Get the `state`, `current_app`, and `running_apps`. - state, self._current_app, self._running_apps = self.aftv.update( - self._get_sources - ) + state, self._current_app, running_apps = self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: self._available = False + if running_apps: + self._sources = [ + self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + ] + else: + self._sources = None + @property def source(self): """Return the current app.""" - return self._current_app + return self._app_id_to_name.get(self._current_app, self._current_app) @property def source_list(self): """Return a list of running apps.""" - return self._running_apps + return self._sources @property def supported_features(self): @@ -575,6 +583,7 @@ class FireTVDevice(ADBDevice): """ if isinstance(source, str): if not source.startswith("!"): - self.aftv.launch_app(source) + self.aftv.launch_app(self._app_name_to_id.get(source, source)) else: - self.aftv.stop_app(source[1:].lstrip()) + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 5fc6bc754fa..986180bf214 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -140,3 +140,15 @@ def isfile(filepath): PATCH_ISFILE = patch("os.path.isfile", isfile) PATCH_ACCESS = patch("os.access", return_value=True) + + +def patch_firetv_update(state, current_app, running_apps): + """Patch the `FireTV.update()` method.""" + return patch( + "androidtv.firetv.FireTV.update", + return_value=(state, current_app, running_apps), + ) + + +PATCH_LAUNCH_APP = patch("androidtv.firetv.FireTV.launch_app") +PATCH_STOP_APP = patch("androidtv.firetv.FireTV.stop_app") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 85f562a3500..04b0bebf447 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -6,15 +6,22 @@ from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, CONF_ADBKEY, + CONF_APPS, +) +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + DOMAIN, + SERVICE_SELECT_SOURCE, ) -from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PLATFORM, STATE_IDLE, STATE_OFF, + STATE_PLAYING, STATE_UNAVAILABLE, ) @@ -276,3 +283,108 @@ async def test_setup_with_adbkey(hass): state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF + + +async def test_firetv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with patchers.patch_firetv_update( + "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "TEST 1" + assert state.attributes["source_list"] == ["TEST 1", "com.app.test2"] + + with patchers.patch_firetv_update( + "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "com.app.test2" + assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + + +async def _test_firetv_select_source(hass, source, expected_arg, method_patch): + """Test that the `FireTV.launch_app` and `FireTV.stop_app` methods are called with the right parameter.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with method_patch as method_patch_: + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: source}, + blocking=True, + ) + method_patch_.assert_called_with(expected_arg) + + return True + + +async def test_firetv_select_source_launch_app_id(hass): + """Test that an app can be launched using its app ID.""" + assert await _test_firetv_select_source( + hass, "com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_launch_app_name(hass): + """Test that an app can be launched using its friendly name.""" + assert await _test_firetv_select_source( + hass, "TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_launch_app_id_no_name(hass): + """Test that an app can be launched using its app ID when it has no friendly name.""" + assert await _test_firetv_select_source( + hass, "com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_stop_app_id(hass): + """Test that an app can be stopped using its app ID.""" + assert await _test_firetv_select_source( + hass, "!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP + ) + + +async def test_firetv_select_source_stop_app_name(hass): + """Test that an app can be stopped using its friendly name.""" + assert await _test_firetv_select_source( + hass, "!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP + ) + + +async def test_firetv_select_source_stop_app_id_no_name(hass): + """Test that an app can be stopped using its app ID when it has no friendly name.""" + assert await _test_firetv_select_source( + hass, "!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP + )