diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 8b68f089617..47768cbd4dc 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.8", - "androidtv==0.0.34", + "adb-shell==0.0.9", + "androidtv==0.0.35", "pure-python-adb==0.2.2.dev0" ], "dependencies": [], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 8b7f1880264..a1fb4cea9cd 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -55,6 +55,7 @@ SUPPORT_ANDROIDTV = ( | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP @@ -199,6 +200,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): aftv, config[CONF_NAME], config[CONF_APPS], + config[CONF_GET_SOURCES], config.get(CONF_TURN_ON_COMMAND), config.get(CONF_TURN_OFF_COMMAND), ) @@ -287,7 +289,9 @@ def adb_decorator(override_available=False): class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name @@ -296,6 +300,7 @@ class ADBDevice(MediaPlayerDevice): self._app_name_to_id = { value: key for key, value in self._app_id_to_name.items() } + self._get_sources = get_sources self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -325,6 +330,7 @@ class ADBDevice(MediaPlayerDevice): self._adb_response = None self._available = True self._current_app = None + self._sources = None self._state = None @property @@ -357,6 +363,16 @@ class ADBDevice(MediaPlayerDevice): """Device should be polled.""" return True + @property + def source(self): + """Return the 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._sources + @property def state(self): """Return the state of the player.""" @@ -408,6 +424,20 @@ class ADBDevice(MediaPlayerDevice): """Send next track command (results in fast-forward).""" self.aftv.media_next_track() + @adb_decorator() + def select_source(self, source): + """Select input source. + + If the source starts with a '!', then it will close the app instead of + opening it. + """ + if isinstance(source, str): + if not source.startswith("!"): + self.aftv.launch_app(self._app_name_to_id.get(source, source)) + else: + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) + @adb_decorator() def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" @@ -436,11 +466,14 @@ class ADBDevice(MediaPlayerDevice): class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) + super().__init__( + aftv, name, apps, get_sources, turn_on_command, turn_off_command + ) - self._device = None self._is_volume_muted = None self._volume_level = None @@ -465,25 +498,28 @@ class AndroidTVDevice(ADBDevice): ( state, self._current_app, - self._device, + running_apps, + _, self._is_volume_muted, self._volume_level, - ) = self.aftv.update() + ) = 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 is_volume_muted(self): """Boolean if volume is currently muted.""" return self._is_volume_muted - @property - def source(self): - """Return the current playback device.""" - return self._device - @property def supported_features(self): """Flag media player features that are supported.""" @@ -518,15 +554,6 @@ class AndroidTVDevice(ADBDevice): class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" - def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command - ): - """Initialize the Fire TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) - - self._get_sources = get_sources - self._sources = None - @adb_decorator(override_available=True) def update(self): """Update the device state and, if necessary, re-connect.""" @@ -558,16 +585,6 @@ class FireTVDevice(ADBDevice): else: self._sources = None - @property - def source(self): - """Return the 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._sources - @property def supported_features(self): """Flag media player features that are supported.""" @@ -577,17 +594,3 @@ class FireTVDevice(ADBDevice): def media_stop(self): """Send stop (back) command.""" self.aftv.back() - - @adb_decorator() - def select_source(self, source): - """Select input source. - - If the source starts with a '!', then it will close the app instead of - opening it. - """ - if isinstance(source, str): - if not source.startswith("!"): - self.aftv.launch_app(self._app_name_to_id.get(source, source)) - else: - source_ = source[1:].lstrip() - self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/requirements_all.txt b/requirements_all.txt index 46c1b0f2ade..f93db416543 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.8 +adb-shell==0.0.9 # homeassistant.components.adguard adguardhome==0.3.0 @@ -215,7 +215,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.34 +androidtv==0.0.35 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb02be746de..812af1a2b13 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -29,7 +29,7 @@ YesssSMS==0.4.1 abodepy==0.16.7 # homeassistant.components.androidtv -adb-shell==0.0.8 +adb-shell==0.0.9 # homeassistant.components.adguard adguardhome==0.3.0 @@ -84,7 +84,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.34 +androidtv==0.0.35 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 0549ad995e1..bd05cab2a74 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -149,5 +149,22 @@ def patch_firetv_update(state, current_app, running_apps): ) -PATCH_LAUNCH_APP = patch("androidtv.firetv.FireTV.launch_app") -PATCH_STOP_APP = patch("androidtv.firetv.FireTV.stop_app") +def patch_androidtv_update( + state, current_app, running_apps, device, is_volume_muted, volume_level +): + """Patch the `AndroidTV.update()` method.""" + return patch( + "androidtv.androidtv.AndroidTV.update", + return_value=( + state, + current_app, + running_apps, + device, + is_volume_muted, + volume_level, + ), + ) + + +PATCH_LAUNCH_APP = patch("androidtv.basetv.BaseTV.launch_app") +PATCH_STOP_APP = patch("androidtv.basetv.BaseTV.stop_app") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index d7a6a8c1ce6..860b8738607 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -33,6 +33,7 @@ CONFIG_ANDROIDTV_PYTHON_ADB = { CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", CONF_NAME: "Android TV", + CONF_DEVICE_CLASS: "androidtv", } } @@ -42,6 +43,7 @@ CONFIG_ANDROIDTV_ADB_SERVER = { CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", CONF_NAME: "Android TV", + CONF_DEVICE_CLASS: "androidtv", CONF_ADB_SERVER_IP: "127.0.0.1", } } @@ -284,9 +286,9 @@ async def test_setup_with_adbkey(hass): 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() +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"} patch_key, entity_id = _setup(hass, config) @@ -299,9 +301,21 @@ async def test_firetv_sources(hass): 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"] - ): + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + ["com.app.test1", "com.app.test2"], + "hdmi", + False, + 1, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + ) + + with patch_update: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -309,9 +323,21 @@ async def test_firetv_sources(hass): 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"] - ): + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test2", + ["com.app.test2", "com.app.test1"], + "hdmi", + True, + 0, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + ) + + with patch_update: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -319,10 +345,22 @@ async def test_firetv_sources(hass): assert state.attributes["source"] == "com.app.test2" assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + return True -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() + +async def test_androidtv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Android TV devices.""" + assert await _test_sources(hass, CONFIG_ANDROIDTV_ADB_SERVER) + + +async def test_firetv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" + assert await _test_sources(hass, CONFIG_FIRETV_ADB_SERVER) + + +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"} patch_key, entity_id = _setup(hass, config) @@ -347,43 +385,133 @@ async def _test_firetv_select_source(hass, source, expected_arg, method_patch): return True +async def test_androidtv_select_source_launch_app_id(hass): + """Test that an app can be launched using its app ID.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_launch_app_name(hass): + """Test that an app can be launched using its friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "TEST 1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_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_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test2", + "com.app.test2", + 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( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test1", + "com.app.test1", + patchers.PATCH_STOP_APP, + ) + + +async def test_androidtv_select_source_stop_app_name(hass): + """Test that an app can be stopped using its friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!TEST 1", + "com.app.test1", + patchers.PATCH_STOP_APP, + ) + + +async def test_androidtv_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_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test2", + "com.app.test2", + 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_firetv_select_source( - hass, "com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "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 + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "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 + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "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 + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!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 + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!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 + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test2", + "com.app.test2", + patchers.PATCH_STOP_APP, )