diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 3bb65bd1a0a..93666958919 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -82,6 +82,7 @@ CONF_ADBKEY = "adbkey" CONF_ADB_SERVER_IP = "adb_server_ip" CONF_ADB_SERVER_PORT = "adb_server_port" CONF_APPS = "apps" +CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" CONF_GET_SOURCES = "get_sources" CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_TURN_ON_COMMAND = "turn_on_command" @@ -134,12 +135,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_ADB_SERVER_IP): cv.string, vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional(CONF_APPS, default=dict()): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_APPS, default=dict()): vol.Schema( + {cv.string: vol.Any(cv.string, None)} + ), vol.Optional(CONF_TURN_ON_COMMAND): cv.string, vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( {cv.string: ha_state_detection_rules_validator(vol.Invalid)} ), + vol.Optional(CONF_EXCLUDE_UNNAMED_APPS, default=False): cv.boolean, } ) @@ -232,6 +236,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_GET_SOURCES], config.get(CONF_TURN_ON_COMMAND), config.get(CONF_TURN_OFF_COMMAND), + config[CONF_EXCLUDE_UNNAMED_APPS], ] if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: @@ -367,7 +372,14 @@ class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + self, + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv @@ -375,7 +387,7 @@ class ADBDevice(MediaPlayerDevice): 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() + value: key for key, value in self._app_id_to_name.items() if value } self._get_sources = get_sources self._keys = KEYS @@ -386,6 +398,8 @@ class ADBDevice(MediaPlayerDevice): self.turn_on_command = turn_on_command self.turn_off_command = turn_off_command + self._exclude_unnamed_apps = exclude_unnamed_apps + # ADB exceptions to catch if not self.aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) @@ -561,11 +575,24 @@ class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + self, + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, ): """Initialize the Android TV device.""" super().__init__( - aftv, name, apps, get_sources, turn_on_command, turn_off_command + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, ) self._is_volume_muted = None @@ -603,9 +630,13 @@ class AndroidTVDevice(ADBDevice): self._available = False if running_apps: - self._sources = [ - self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + sources = [ + self._app_id_to_name.get( + app_id, app_id if not self._exclude_unnamed_apps else None + ) + for app_id in running_apps ] + self._sources = [source for source in sources if source] else: self._sources = None @@ -678,9 +709,13 @@ class FireTVDevice(ADBDevice): self._available = False if running_apps: - self._sources = [ - self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + sources = [ + self._app_id_to_name.get( + app_id, app_id if not self._exclude_unnamed_apps else None + ) + for app_id in running_apps ] + self._sources = [source for source in sources if source] else: self._sources = None diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index f076b461119..82287877eaf 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.androidtv.media_player import ( CONF_ADB_SERVER_IP, CONF_ADBKEY, CONF_APPS, + CONF_EXCLUDE_UNNAMED_APPS, KEYS, SERVICE_ADB_COMMAND, SERVICE_DOWNLOAD, @@ -300,7 +301,11 @@ async def test_setup_with_adbkey(hass): async def _test_sources(hass, config0): """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" config = config0.copy() - config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + config[DOMAIN][CONF_APPS] = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": "", + } patch_key, entity_id = _setup(config) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ @@ -316,14 +321,16 @@ async def _test_sources(hass, config0): patch_update = patchers.patch_androidtv_update( "playing", "com.app.test1", - ["com.app.test1", "com.app.test2"], + ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], "hdmi", False, 1, ) else: patch_update = patchers.patch_firetv_update( - "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + "playing", + "com.app.test1", + ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], ) with patch_update: @@ -332,20 +339,22 @@ async def _test_sources(hass, config0): 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"] + assert sorted(state.attributes["source_list"]) == ["TEST 1", "com.app.test2"] if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": patch_update = patchers.patch_androidtv_update( "playing", "com.app.test2", - ["com.app.test2", "com.app.test1"], + ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], "hdmi", True, 0, ) else: patch_update = patchers.patch_firetv_update( - "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + "playing", + "com.app.test2", + ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], ) with patch_update: @@ -354,7 +363,7 @@ async def _test_sources(hass, config0): 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"] + assert sorted(state.attributes["source_list"]) == ["TEST 1", "com.app.test2"] return True @@ -369,10 +378,82 @@ async def test_firetv_sources(hass): assert await _test_sources(hass, CONFIG_FIRETV_ADB_SERVER) +async def _test_exclude_sources(hass, config0, expected_sources): + """Test that sources (i.e., apps) are handled correctly when the `exclude_unnamed_apps` config parameter is provided.""" + config = config0.copy() + config[DOMAIN][CONF_APPS] = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": "", + } + patch_key, entity_id = _setup(config) + + with patchers.PATCH_ADB_DEVICE_TCP, 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 + + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + [ + "com.app.test1", + "com.app.test2", + "com.app.test3", + "com.app.test4", + "com.app.test5", + ], + "hdmi", + False, + 1, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", + "com.app.test1", + [ + "com.app.test1", + "com.app.test2", + "com.app.test3", + "com.app.test4", + "com.app.test5", + ], + ) + + with patch_update: + 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 sorted(state.attributes["source_list"]) == expected_sources + + return True + + +async def test_androidtv_exclude_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Android TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" + config = CONFIG_ANDROIDTV_ADB_SERVER.copy() + config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True + assert await _test_exclude_sources(hass, config, ["TEST 1"]) + + +async def test_firetv_exclude_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True + assert await _test_exclude_sources(hass, config, ["TEST 1"]) + + async def _test_select_source(hass, config0, source, expected_arg, method_patch): """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" config = config0.copy() - config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1", "com.app.test3": None} patch_key, entity_id = _setup(config) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ @@ -429,6 +510,17 @@ async def test_androidtv_select_source_launch_app_id_no_name(hass): ) +async def test_androidtv_select_source_launch_app_hidden(hass): + """Test that an app can be launched using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test3", + "com.app.test3", + patchers.PATCH_LAUNCH_APP, + ) + + async def test_androidtv_select_source_stop_app_id(hass): """Test that an app can be stopped using its app ID.""" assert await _test_select_source( @@ -462,6 +554,17 @@ async def test_androidtv_select_source_stop_app_id_no_name(hass): ) +async def test_androidtv_select_source_stop_app_hidden(hass): + """Test that an app can be stopped using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test3", + "com.app.test3", + patchers.PATCH_STOP_APP, + ) + + async def test_firetv_select_source_launch_app_id(hass): """Test that an app can be launched using its app ID.""" assert await _test_select_source( @@ -495,6 +598,17 @@ async def test_firetv_select_source_launch_app_id_no_name(hass): ) +async def test_firetv_select_source_launch_app_hidden(hass): + """Test that an app can be launched using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "com.app.test3", + "com.app.test3", + 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_select_source( @@ -528,6 +642,17 @@ async def test_firetv_select_source_stop_app_id_no_name(hass): ) +async def test_firetv_select_source_stop_hidden(hass): + """Test that an app can be stopped using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test3", + "com.app.test3", + patchers.PATCH_STOP_APP, + ) + + async def _test_setup_fail(hass, config): """Test that the entity is not created when the ADB connection is not established.""" patch_key, entity_id = _setup(config)