From 392e897170755283952c15115bc7c793724f5f2f Mon Sep 17 00:00:00 2001 From: Jeremy Herbison Date: Fri, 10 Dec 2021 10:15:35 -0700 Subject: [PATCH 01/66] Update constants.py (#274) Add Crave, CBC Gem and Global TV (popular Canadian apps) --- androidtv/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/androidtv/constants.py b/androidtv/constants.py index 161cac3a..ba097881 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -537,7 +537,9 @@ APP_APPLE_TV_PLUS_SONY = "com.apple.atve.sony.appletv" APP_ATV_LAUNCHER = "com.google.android.tvlauncher" APP_BELL_FIBE = "com.quickplay.android.bellmediaplayer" +APP_CBC_GEM = "ca.cbc.android.cbctv" APP_COMEDY_CENTRAL = "com.vmn.android.comedycentral" +APP_CRAVE = "ca.bellmedia.cravetv" APP_DAILYMOTION = "com.dailymotion.dailymotion" APP_DEEZER = "deezer.android.tv" APP_DISNEY_PLUS = "com.disney.disneyplus" @@ -553,6 +555,7 @@ APP_FIRETV_STORE = "com.amazon.venezia" APP_FOOD_NETWORK_GO = "tv.accedo.foodnetwork" APP_FRANCE_TV = "fr.francetv.pluzz" +APP_GLOBAL_TV = "com.shawmedia.smglobal" APP_GOOGLE_CAST = "com.google.android.apps.mediashell" APP_GOOGLE_TV_LAUNCHER = "com.google.android.apps.tv.launcherx" APP_HAYSTACK_NEWS = "com.haystack.android" @@ -636,7 +639,9 @@ APP_APPLE_TV_PLUS_SONY: "Apple TV+ (Sony)", APP_ATV_LAUNCHER: "Android TV Launcher", APP_BELL_FIBE: "Bell Fibe", + APP_CBC_GEM: "CBC Gem", APP_COMEDY_CENTRAL: "Comedy Central", + APP_CRAVE: "Crave", APP_DAILYMOTION: "Dailymotion", APP_DEEZER: "Deezer", APP_DISNEY_PLUS: "Disney+", @@ -650,6 +655,7 @@ APP_FIRETV_STORE: "FireTV Store", APP_FOOD_NETWORK_GO: "Food Network GO", APP_FRANCE_TV: "France TV", + APP_GLOBAL_TV: "Global TV", APP_GOOGLE_CAST: "Google Cast", APP_GOOGLE_TV_LAUNCHER: "Google TV Launcher", APP_HAYSTACK_NEWS: "Haystack News", From 9f0455d0c268926943c442ed4ae86b70a71b4ed0 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 13 Jan 2022 08:50:32 -0800 Subject: [PATCH 02/66] Add Python 3.10 to GitHub CI (#276) * Add Python 3.10 to GitHub CI * Black --- .github/workflows/python-package.yml | 10 +++++----- tests/async_wrapper.py | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4697128a..bebb43d9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9'] + python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 @@ -30,18 +30,18 @@ jobs: if python --version 2>&1 | grep -q "Python 2"; then pip install mock rsa==4.0; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install . - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then pip install aiofiles adb-shell[usb]; fi + if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install aiofiles adb-shell[usb]; fi - name: Check formatting with black run: | - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then pip install black && make lint-black; fi + if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install black && make lint-black; fi - name: Lint with pylint and flake8 run: | if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5" || python --version 2>&1 | grep -q "Python 3.6"; then flake8 androidtv/ --exclude="androidtv/setup_async.py,androidtv/basetv/basetv_async.py,androidtv/androidtv/androidtv_async.py,androidtv/firetv/firetv_async.py,androidtv/adb_manager/adb_manager_async.py" && pylint --ignore="setup_async.py,basetv_async.py,androidtv_async.py,firetv_async.py,adb_manager_async.py" androidtv/; fi - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then flake8 androidtv/ && pylint androidtv/; fi + if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then flake8 androidtv/ && pylint androidtv/; fi - name: Test with unittest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_SERVICE_NAME: github run: | if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5" || python --version 2>&1 | grep -q "Python 3.6" ; then for synctest in $(cd tests && ls test*.py | grep -v async); do python -m unittest discover -s tests/ -t . -p "$synctest" || exit 1; done; fi - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"; then coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m && coveralls; fi + if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"|| python --version 2>&1 | grep -q "Python 3.10"; then coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m && coveralls; fi diff --git a/tests/async_wrapper.py b/tests/async_wrapper.py index 8df6d5c0..759f0d57 100644 --- a/tests/async_wrapper.py +++ b/tests/async_wrapper.py @@ -3,8 +3,12 @@ def _await(coro): + """Create a new event loop, run the coroutine, then close the event loop.""" + loop = asyncio.new_event_loop() + with warnings.catch_warnings(record=True) as warns: - ret = asyncio.get_event_loop().run_until_complete(coro) + ret = loop.run_until_complete(coro) + loop.close() if warns: raise RuntimeError From 35b882f0fa164233e014c35dd969de96bbd538a0 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 13 Jan 2022 19:16:22 -0800 Subject: [PATCH 03/66] Add `screen_on_awake_wake_lock_size()` method (#278) --- androidtv/basetv/basetv.py | 30 ++++++++++++++++++++++++++++++ androidtv/basetv/basetv_async.py | 17 +++++++++++++++++ androidtv/basetv/basetv_sync.py | 17 +++++++++++++++++ androidtv/constants.py | 5 +++++ tests/test_basetv_async.py | 18 ++++++++++++++++++ tests/test_basetv_sync.py | 17 +++++++++++++++++ 6 files changed, 104 insertions(+) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index f8217322..d30e6c45 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -532,6 +532,36 @@ def _running_apps(running_apps_response): return None + @staticmethod + def _screen_on_awake_wake_lock_size(output): + """Check if the screen is on and the device is awake, and get the wake lock size. + + Parameters + ---------- + output : str, None + The output from :py:const:`androidtv.constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE` + + Returns + ------- + bool + Whether or not the device is on + bool + Whether or not the device is awake (screensaver is not running) + int, None + The size of the current wake lock, or ``None`` if it could not be determined + + """ + if not output: + return False, False, None + + if output == "1": + return True, False, None + + if output == "11": + return True, True, None + + return True, True, BaseTV._wake_lock_size(output[2:]) + def _volume(self, stream_music, audio_output_device): """Get the absolute volume level from the ``STREAM_MUSIC`` block from ``adb shell dumpsys audio``. diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index c463491b..1bba4665 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -334,6 +334,23 @@ async def screen_on(self): """ return await self._adb.shell(constants.CMD_SCREEN_ON + constants.CMD_SUCCESS1_FAILURE0) == "1" + async def screen_on_awake_wake_lock_size(self): + """Check if the screen is on and the device is awake, and get the wake lock size. + + Returns + ------- + bool + Whether or not the device is on + bool + Whether or not the device is awake (screensaver is not running) + int, None + The size of the current wake lock, or ``None`` if it could not be determined + + """ + output = await self._adb.shell(constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE) + + return self._screen_on_awake_wake_lock_size(output) + async def volume(self): """Get the absolute volume level. diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 38be410f..88697cfe 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -334,6 +334,23 @@ def screen_on(self): """ return self._adb.shell(constants.CMD_SCREEN_ON + constants.CMD_SUCCESS1_FAILURE0) == "1" + def screen_on_awake_wake_lock_size(self): + """Check if the screen is on and the device is awake, and get the wake lock size. + + Returns + ------- + bool + Whether or not the device is on + bool + Whether or not the device is awake (screensaver is not running) + int, None + The size of the current wake lock, or ``None`` if it could not be determined + + """ + output = self._adb.shell(constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE) + + return self._screen_on_awake_wake_lock_size(output) + def volume(self): """Get the absolute volume level. diff --git a/androidtv/constants.py b/androidtv/constants.py index ba097881..336aaa0a 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -93,6 +93,11 @@ #: Get the wake lock size CMD_WAKE_LOCK_SIZE = "dumpsys power | grep Locks | grep 'size='" +#: Determine if the device is on, the screen is on, and get the wake lock size +CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE = ( + CMD_SCREEN_ON + CMD_SUCCESS1 + " && " + CMD_AWAKE + CMD_SUCCESS1 + " && " + CMD_WAKE_LOCK_SIZE +) + #: Get the properties for an Android TV device (``lazy=True, get_running_apps=True``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS = ( CMD_SCREEN_ON diff --git a/tests/test_basetv_async.py b/tests/test_basetv_async.py index 96b3dbfc..e6e870d3 100644 --- a/tests/test_basetv_async.py +++ b/tests/test_basetv_async.py @@ -404,6 +404,24 @@ async def test_screen_on(self): with async_patchers.patch_shell("1")[self.PATCH_KEY]: self.assertTrue(await self.btv.screen_on()) + @awaiter + async def test_screen_on_awake_wake_lock_size(self): + """Check that the ``screen_on_awake_wake_lock_size`` property works correctly.""" + with async_patchers.patch_shell(None)[self.PATCH_KEY]: + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) + + with async_patchers.patch_shell("")[self.PATCH_KEY]: + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) + + with async_patchers.patch_shell("1")[self.PATCH_KEY]: + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, False, None)) + + with async_patchers.patch_shell("11")[self.PATCH_KEY]: + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, True, None)) + + with async_patchers.patch_shell("11Wake Locks: size=2")[self.PATCH_KEY]: + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) + @awaiter async def test_wake_lock_size(self): """Check that the ``wake_lock_size`` property works correctly.""" diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 0c011ae2..715a0416 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -562,6 +562,23 @@ def test_screen_on(self): with patchers.patch_shell("1")[self.PATCH_KEY]: self.assertTrue(self.btv.screen_on()) + def test_screen_on_awake_wake_lock_size(self): + """Check that the ``screen_on_awake_wake_lock_size`` property works correctly.""" + with patchers.patch_shell(None)[self.PATCH_KEY]: + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) + + with patchers.patch_shell("")[self.PATCH_KEY]: + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) + + with patchers.patch_shell("1")[self.PATCH_KEY]: + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, False, None)) + + with patchers.patch_shell("11")[self.PATCH_KEY]: + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, None)) + + with patchers.patch_shell("11Wake Locks: size=2")[self.PATCH_KEY]: + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) + def test_state_detection_rules_validator(self): """Check that the ``state_detection_rules_validator`` function works correctly.""" with patchers.patch_connect(True)["python"], patchers.patch_shell("")["python"]: From 957f5b3c98c8eb90c5f69f4d1ca8896a76413221 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 13 Jan 2022 19:50:21 -0800 Subject: [PATCH 04/66] Add `current_app_media_session_state()` method (#279) --- androidtv/basetv/basetv_async.py | 19 ++++++++++++++++--- androidtv/basetv/basetv_sync.py | 19 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 1bba4665..9d6ec996 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -271,6 +271,21 @@ async def current_app(self): return self._current_app(current_app_response) + async def current_app_media_session_state(self): + """Get the current app and the state from the output of ``dumpsys media_session``. + + Returns + ------- + str, None + The current app, or ``None`` if it could not be determined + int, None + The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined + + """ + media_session_state_response = await self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) + + return self._current_app_media_session_state(media_session_state_response) + async def get_hdmi_input(self): """Get the HDMI input from the output of :py:const:`androidtv.constants.CMD_HDMI_INPUT`. @@ -317,9 +332,7 @@ async def media_session_state(self): The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined """ - media_session_state_response = await self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) - - _, media_session_state = self._current_app_media_session_state(media_session_state_response) + _, media_session_state = await self.current_app_media_session_state() return media_session_state diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 88697cfe..c99cf0af 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -271,6 +271,21 @@ def current_app(self): return self._current_app(current_app_response) + def current_app_media_session_state(self): + """Get the current app and the state from the output of ``dumpsys media_session``. + + Returns + ------- + str, None + The current app, or ``None`` if it could not be determined + int, None + The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined + + """ + media_session_state_response = self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) + + return self._current_app_media_session_state(media_session_state_response) + def get_hdmi_input(self): """Get the HDMI input from the output of :py:const:`androidtv.constants.CMD_HDMI_INPUT`. @@ -317,9 +332,7 @@ def media_session_state(self): The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined """ - media_session_state_response = self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) - - _, media_session_state = self._current_app_media_session_state(media_session_state_response) + _, media_session_state = self.current_app_media_session_state() return media_session_state From 6ac4ebf9d5c2734a1bedf1368da5338250778818 Mon Sep 17 00:00:00 2001 From: Berend Haan Date: Fri, 14 Jan 2022 15:20:19 +0100 Subject: [PATCH 05/66] Map states from nl.nlziet app (#277) * Map states from nl.nlziet app * Add missing comment * Update androidtv/androidtv/base_androidtv.py * Add test to assert NLZIET detection changes Co-authored-by: Jeff Irion --- androidtv/androidtv/base_androidtv.py | 6 ++++++ tests/test_androidtv_sync.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/androidtv/androidtv/base_androidtv.py b/androidtv/androidtv/base_androidtv.py index cac54206..04823f47 100644 --- a/androidtv/androidtv/base_androidtv.py +++ b/androidtv/androidtv/base_androidtv.py @@ -171,6 +171,12 @@ def _update( else: state = constants.STATE_IDLE + # NLZIET + elif current_app == constants.APP_NLZIET: + if wake_lock_size == 1: + state = constants.STATE_PAUSED + elif wake_lock_size == 2: + state = constants.STATE_PLAYING # Plex elif current_app == constants.APP_PLEX: if media_session_state == 3: diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index 1eda7bc6..ae73688a 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -901,6 +901,17 @@ def test_state_detection(self): (constants.STATE_IDLE, constants.APP_NETFLIX, [constants.APP_NETFLIX], "hmdi_arc", False, 0.5, None), ) + # NLZIET + self.assertUpdate( + [True, True, constants.STATE_IDLE, 1, constants.APP_NLZIET, 2, "hmdi_arc", False, 30, None, None], + (constants.STATE_PAUSED, constants.APP_NLZIET, [constants.APP_NLZIET], "hmdi_arc", False, 0.5, None), + ) + + self.assertUpdate( + [True, True, constants.STATE_IDLE, 2, constants.APP_NLZIET, 2, "hmdi_arc", False, 30, None, None], + (constants.STATE_PLAYING, constants.APP_NLZIET, [constants.APP_NLZIET], "hmdi_arc", False, 0.5, None), + ) + # Plex self.assertUpdate( [True, True, constants.STATE_IDLE, 2, constants.APP_PLEX, 4, "hmdi_arc", False, 30, None, None], From e6dbd3d11c58c6688b42bf6625b944cc0e13bc36 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 14 Jan 2022 20:46:01 -0800 Subject: [PATCH 06/66] Add `stream_music_properties()` method (#280) * Add `stream_music_properties()` method * Update androidtv/basetv/basetv.py * Fix tests --- androidtv/basetv/basetv.py | 12 +-- androidtv/basetv/basetv_async.py | 24 ++++++ androidtv/basetv/basetv_sync.py | 24 ++++++ androidtv/constants.py | 2 +- tests/test_androidtv_async.py | 47 ++++++----- tests/test_androidtv_sync.py | 135 ++++++++----------------------- tests/test_basetv_async.py | 2 +- tests/test_basetv_sync.py | 2 +- 8 files changed, 113 insertions(+), 135 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index d30e6c45..f95c9d85 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -554,13 +554,11 @@ def _screen_on_awake_wake_lock_size(output): if not output: return False, False, None - if output == "1": - return True, False, None + screen_on = output[0] == "1" + awake = None if len(output) < 2 else output[1] == "1" + wake_lock_size = None if len(output) < 3 else BaseTV._wake_lock_size(output[2:]) - if output == "11": - return True, True, None - - return True, True, BaseTV._wake_lock_size(output[2:]) + return screen_on, awake, wake_lock_size def _volume(self, stream_music, audio_output_device): """Get the absolute volume level from the ``STREAM_MUSIC`` block from ``adb shell dumpsys audio``. @@ -585,8 +583,6 @@ def _volume(self, stream_music, audio_output_device): max_volume_matches = re.findall(constants.MAX_VOLUME_REGEX_PATTERN, stream_music, re.DOTALL | re.MULTILINE) if max_volume_matches: self.max_volume = float(max_volume_matches[0]) - else: - self.max_volume = 15.0 if not audio_output_device: return None diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 9d6ec996..5085ff78 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -282,6 +282,7 @@ async def current_app_media_session_state(self): The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined """ + # This needs to use different commands depending on the device media_session_state_response = await self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) return self._current_app_media_session_state(media_session_state_response) @@ -364,6 +365,29 @@ async def screen_on_awake_wake_lock_size(self): return self._screen_on_awake_wake_lock_size(output) + async def stream_music_properties(self): + """Get various properties from the "STREAM_MUSIC" block from ``dumpsys audio``.. + + Returns + ------- + audio_output_device : str, None + The current audio playback device, or ``None`` if it could not be determined + is_volume_muted : bool, None + Whether or not the volume is muted, or ``None`` if it could not be determined + volume : int, None + The absolute volume level, or ``None`` if it could not be determined + volume_level : float, None + The volume level (between 0 and 1), or ``None`` if it could not be determined + + """ + stream_music = await self._get_stream_music() + audio_output_device = self._audio_output_device(stream_music) + volume = self._volume(stream_music, audio_output_device) + volume_level = self._volume_level(volume) + is_volume_muted = self._is_volume_muted(stream_music) + + return audio_output_device, is_volume_muted, volume, volume_level + async def volume(self): """Get the absolute volume level. diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index c99cf0af..ac17d717 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -282,6 +282,7 @@ def current_app_media_session_state(self): The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined """ + # This needs to use different commands depending on the device media_session_state_response = self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) return self._current_app_media_session_state(media_session_state_response) @@ -364,6 +365,29 @@ def screen_on_awake_wake_lock_size(self): return self._screen_on_awake_wake_lock_size(output) + def stream_music_properties(self): + """Get various properties from the "STREAM_MUSIC" block from ``dumpsys audio``.. + + Returns + ------- + audio_output_device : str, None + The current audio playback device, or ``None`` if it could not be determined + is_volume_muted : bool, None + Whether or not the volume is muted, or ``None`` if it could not be determined + volume : int, None + The absolute volume level, or ``None`` if it could not be determined + volume_level : float, None + The volume level (between 0 and 1), or ``None`` if it could not be determined + + """ + stream_music = self._get_stream_music() + audio_output_device = self._audio_output_device(stream_music) + volume = self._volume(stream_music, audio_output_device) + volume_level = self._volume_level(volume) + is_volume_muted = self._is_volume_muted(stream_music) + + return audio_output_device, is_volume_muted, volume, volume_level + def volume(self): """Get the absolute volume level. diff --git a/androidtv/constants.py b/androidtv/constants.py index 336aaa0a..42731eef 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -95,7 +95,7 @@ #: Determine if the device is on, the screen is on, and get the wake lock size CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE = ( - CMD_SCREEN_ON + CMD_SUCCESS1 + " && " + CMD_AWAKE + CMD_SUCCESS1 + " && " + CMD_WAKE_LOCK_SIZE + CMD_SCREEN_ON + CMD_SUCCESS1_FAILURE0 + " && " + CMD_AWAKE + CMD_SUCCESS1_FAILURE0 + " && " + CMD_WAKE_LOCK_SIZE ) #: Get the properties for an Android TV device (``lazy=True, get_running_apps=True``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` diff --git a/tests/test_androidtv_async.py b/tests/test_androidtv_async.py index 062f0d35..0175f92b 100644 --- a/tests/test_androidtv_async.py +++ b/tests/test_androidtv_async.py @@ -106,36 +106,35 @@ async def test_running_apps(self): assert patched.called @awaiter - async def test_audio_output_device(self): - """Check that the ``device`` property works correctly.""" + async def test_stream_music_properties(self): + """Check that the ``stream_music_properties`` method works correctly.""" with async_patchers.patch_shell(None)[self.PATCH_KEY]: - with patch_calls(self.atv, self.atv._audio_output_device) as patched: + with patch_calls(self.atv, self.atv._audio_output_device) as audio_output_device, patch_calls( + self.atv, self.atv._is_volume_muted + ) as is_volume_muted, patch_calls(self.atv, self.atv._volume) as volume, patch_calls( + self.atv, self.atv._volume_level + ) as volume_level: + await self.atv.stream_music_properties() + assert audio_output_device.called + assert is_volume_muted.called + assert volume.called + assert volume_level.called + + with patch_calls(self.atv, self.atv._audio_output_device) as audio_output_device: await self.atv.audio_output_device() - assert patched.called + assert audio_output_device.called - @awaiter - async def test_volume(self): - """Check that the ``volume`` property works correctly.""" - with async_patchers.patch_shell(None)[self.PATCH_KEY]: - with patch_calls(self.atv, self.atv._volume) as patched: + with patch_calls(self.atv, self.atv._is_volume_muted) as is_volume_muted: + await self.atv.is_volume_muted() + assert is_volume_muted.called + + with patch_calls(self.atv, self.atv._volume) as volume: await self.atv.volume() - assert patched.called + assert volume.called - @awaiter - async def test_volume_level(self): - """Check that the ``volume_level`` property works correctly.""" - with async_patchers.patch_shell(None)[self.PATCH_KEY]: - with patch_calls(self.atv, self.atv._volume_level) as patched: + with patch_calls(self.atv, self.atv._volume_level) as volume_level: await self.atv.volume_level() - assert patched.called - - @awaiter - async def test_is_volume_muted(self): - """Check that the ``is_volume_muted`` property works correctly.""" - with async_patchers.patch_shell(None)[self.PATCH_KEY]: - with patch_calls(self.atv, self.atv._is_volume_muted) as patched: - await self.atv.is_volume_muted() - assert patched.called + assert volume_level.called @awaiter async def test_set_volume_level(self): diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index ae73688a..a6ae6fa0 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -473,122 +473,57 @@ def test_running_apps(self): running_apps = self.atv.running_apps() self.assertListEqual(running_apps, RUNNING_APPS_LIST) - def test_audio_output_device(self): - """Check that the ``device`` property works correctly.""" + def test_stream_music_properties(self): + """Check that the ``stream_music_properties`` method works correctly.""" with patchers.patch_shell(None)[self.PATCH_KEY]: - audio_output_device = self.atv.audio_output_device() - self.assertIsNone(audio_output_device) + self.assertTupleEqual(self.atv.stream_music_properties(), (None, None, None, None)) + self.assertIsNone(self.atv.audio_output_device()) + self.assertIsNone(self.atv.is_volume_muted()) + self.assertIsNone(self.atv.volume()) + self.assertIsNone(self.atv.volume_level()) with patchers.patch_shell("")[self.PATCH_KEY]: - audio_output_device = self.atv.audio_output_device() - self.assertIsNone(audio_output_device) + self.assertTupleEqual(self.atv.stream_music_properties(), (None, None, None, None)) + self.assertIsNone(self.atv.audio_output_device()) + self.assertIsNone(self.atv.is_volume_muted()) + self.assertIsNone(self.atv.volume()) + self.assertIsNone(self.atv.volume_level()) with patchers.patch_shell(" ")[self.PATCH_KEY]: - audio_output_device = self.atv.audio_output_device() - self.assertIsNone(audio_output_device) + self.assertTupleEqual(self.atv.stream_music_properties(), (None, None, None, None)) + self.assertIsNone(self.atv.audio_output_device()) + self.assertIsNone(self.atv.is_volume_muted()) + self.assertIsNone(self.atv.volume()) + self.assertIsNone(self.atv.volume_level()) with patchers.patch_shell(STREAM_MUSIC_EMPTY)[self.PATCH_KEY]: - audio_output_device = self.atv.audio_output_device() - self.assertIsNone(audio_output_device) - - with patchers.patch_shell(STREAM_MUSIC_OFF)[self.PATCH_KEY]: - audio_output_device = self.atv.audio_output_device() - self.assertEqual("speaker", audio_output_device) - - with patchers.patch_shell(STREAM_MUSIC_ON)[self.PATCH_KEY]: - audio_output_device = self.atv.audio_output_device() - self.assertEqual("hmdi_arc", audio_output_device) - - def test_volume(self): - """Check that the ``volume`` property works correctly.""" - with patchers.patch_shell(None)[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertIsNone(volume) - - with patchers.patch_shell("")[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertIsNone(volume) - - with patchers.patch_shell(" ")[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertIsNone(volume) - - with patchers.patch_shell(STREAM_MUSIC_EMPTY)[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertIsNone(volume) - - with patchers.patch_shell(STREAM_MUSIC_NO_VOLUME)[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertIsNone(volume) - - self.atv.max_volume = None - with patchers.patch_shell(STREAM_MUSIC_OFF)[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertEqual(volume, 20) - self.assertEqual(self.atv.max_volume, 60.0) - - self.atv.max_volume = None - with patchers.patch_shell(STREAM_MUSIC_ON)[self.PATCH_KEY]: - volume = self.atv.volume() - self.assertEqual(volume, 22) - self.assertEqual(self.atv.max_volume, 60.0) - - def test_volume_level(self): - """Check that the ``volume_level`` property works correctly.""" - with patchers.patch_shell(None)[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertIsNone(volume_level) - - with patchers.patch_shell("")[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertIsNone(volume_level) - - with patchers.patch_shell(" ")[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertIsNone(volume_level) - - with patchers.patch_shell(STREAM_MUSIC_EMPTY)[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertIsNone(volume_level) + self.assertTupleEqual(self.atv.stream_music_properties(), (None, None, None, None)) + self.assertIsNone(self.atv.audio_output_device()) + self.assertIsNone(self.atv.is_volume_muted()) + self.assertIsNone(self.atv.volume()) + self.assertIsNone(self.atv.volume_level()) with patchers.patch_shell(STREAM_MUSIC_NO_VOLUME)[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertIsNone(volume_level) + self.assertTupleEqual(self.atv.stream_music_properties(), ("speaker", False, None, None)) + self.assertEqual("speaker", self.atv.audio_output_device()) + self.assertFalse(self.atv.is_volume_muted()) + self.assertIsNone(self.atv.volume()) + self.assertIsNone(self.atv.volume_level()) - self.atv.max_volume = None with patchers.patch_shell(STREAM_MUSIC_OFF)[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertEqual(volume_level, 20.0 / 60) + self.assertTupleEqual(self.atv.stream_music_properties(), ("speaker", False, 20, 20 / 60.0)) + self.assertEqual("speaker", self.atv.audio_output_device()) + self.assertFalse(self.atv.is_volume_muted()) + self.assertEqual(self.atv.volume(), 20) self.assertEqual(self.atv.max_volume, 60.0) - self.atv.max_volume = None with patchers.patch_shell(STREAM_MUSIC_ON)[self.PATCH_KEY]: - volume_level = self.atv.volume_level() - self.assertEqual(volume_level, 22.0 / 60) + self.assertTupleEqual(self.atv.stream_music_properties(), ("hmdi_arc", False, 22, 22 / 60.0)) + self.assertEqual("hmdi_arc", self.atv.audio_output_device()) + self.assertFalse(self.atv.is_volume_muted()) + self.assertEqual(self.atv.volume(), 22) self.assertEqual(self.atv.max_volume, 60.0) - def test_is_volume_muted(self): - """Check that the ``is_volume_muted`` property works correctly.""" - with patchers.patch_shell(None)[self.PATCH_KEY]: - is_volume_muted = self.atv.is_volume_muted() - self.assertIsNone(is_volume_muted) - - with patchers.patch_shell("")[self.PATCH_KEY]: - is_volume_muted = self.atv.is_volume_muted() - self.assertIsNone(is_volume_muted) - - with patchers.patch_shell(" ")[self.PATCH_KEY]: - is_volume_muted = self.atv.is_volume_muted() - self.assertIsNone(is_volume_muted) - - with patchers.patch_shell(STREAM_MUSIC_EMPTY)[self.PATCH_KEY]: - is_volume_muted = self.atv.is_volume_muted() - self.assertIsNone(is_volume_muted) - - with patchers.patch_shell(STREAM_MUSIC_OFF)[self.PATCH_KEY]: - is_volume_muted = self.atv.is_volume_muted() - self.assertFalse(is_volume_muted) - def test_set_volume_level(self): """Check that the ``set_volume_level`` method works correctly.""" with patchers.patch_shell(None)[self.PATCH_KEY]: diff --git a/tests/test_basetv_async.py b/tests/test_basetv_async.py index e6e870d3..a2a185c1 100644 --- a/tests/test_basetv_async.py +++ b/tests/test_basetv_async.py @@ -414,7 +414,7 @@ async def test_screen_on_awake_wake_lock_size(self): self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) with async_patchers.patch_shell("1")[self.PATCH_KEY]: - self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, False, None)) + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, None, None)) with async_patchers.patch_shell("11")[self.PATCH_KEY]: self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, True, None)) diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 715a0416..c7825583 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -571,7 +571,7 @@ def test_screen_on_awake_wake_lock_size(self): self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) with patchers.patch_shell("1")[self.PATCH_KEY]: - self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, False, None)) + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, None, None)) with patchers.patch_shell("11")[self.PATCH_KEY]: self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, None)) From 043610a362edbe99831cc0e013f46eccf2298e0a Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 15 Jan 2022 13:37:55 -0800 Subject: [PATCH 07/66] Split `get_properties()` for Fire TV (#281) * Split `get_properties()` for Fire TV * Remove test_homeassistant_sync.py * Fix test_update * Fix async get_properties test * Fix tests * Cleanup * Cleanup * Cleanup * Cleanup * Add back test_homeassistant_sync.py --- androidtv/basetv/basetv.py | 13 +- androidtv/constants.py | 74 -------- androidtv/firetv/base_firetv.py | 87 --------- androidtv/firetv/firetv_async.py | 30 ++- androidtv/firetv/firetv_sync.py | 30 ++- tests/test_basetv_async.py | 2 +- tests/test_basetv_sync.py | 2 +- tests/test_constants.py | 24 --- tests/test_firetv_async.py | 62 ++++++- tests/test_firetv_sync.py | 303 ++++++------------------------- 10 files changed, 139 insertions(+), 488 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index f95c9d85..45f47f90 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -543,15 +543,18 @@ def _screen_on_awake_wake_lock_size(output): Returns ------- - bool - Whether or not the device is on - bool - Whether or not the device is awake (screensaver is not running) + bool, None + Whether or not the device is on, or ``None`` if it could not be determined + bool, None + Whether or not the device is awake (screensaver is not running), or ``None`` if it could not be determined int, None The size of the current wake lock, or ``None`` if it could not be determined """ - if not output: + if output is None: + return None, None, None + + if output == "": return False, False, None screen_on = output[0] == "1" diff --git a/androidtv/constants.py b/androidtv/constants.py index 42731eef..942e4e0b 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -274,80 +274,6 @@ + CMD_STREAM_MUSIC ) -#: Get the properties for a Fire TV device (``lazy=True, get_running_apps=True``); see :py:meth:`androidtv.firetv.firetv_sync.FireTVSync.get_properties` and :py:meth:`androidtv.firetv.firetv_async.FireTVAsync.get_properties` -CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1 - + " && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_RUNNING_APPS_FIRETV -) - -#: Get the properties for a Fire TV device (``lazy=True, get_running_apps=False``); see :py:meth:`androidtv.firetv.firetv_sync.FireTVSync.get_properties` and :py:meth:`androidtv.firetv.firetv_async.FireTVAsync.get_properties` -CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1 - + " && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo)" -) - -#: Get the properties for a Fire TV device (``lazy=False, get_running_apps=True``); see :py:meth:`androidtv.firetv.firetv_sync.FireTVSync.get_properties` and :py:meth:`androidtv.firetv.firetv_async.FireTVAsync.get_properties` -CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_RUNNING_APPS_FIRETV -) - -#: Get the properties for a Fire TV device (``lazy=False, get_running_apps=False``); see :py:meth:`androidtv.firetv.firetv_sync.FireTVSync.get_properties` and :py:meth:`androidtv.firetv.firetv_async.FireTVAsync.get_properties` -CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo)" -) - # `getprop` commands CMD_MANUFACTURER = "getprop ro.product.manufacturer" CMD_MODEL = "getprop ro.product.model" diff --git a/androidtv/firetv/base_firetv.py b/androidtv/firetv/base_firetv.py index 74c75360..6a641795 100644 --- a/androidtv/firetv/base_firetv.py +++ b/androidtv/firetv/base_firetv.py @@ -39,10 +39,6 @@ def __init__(self, host, port=5555, adbkey="", adb_server_ip="", adb_server_port def _fill_in_commands(self): """Fill in commands that are specific to Fire TV devices.""" - self._cmd_get_properties_lazy_running_apps = constants.CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS - self._cmd_get_properties_lazy_no_running_apps = constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS - self._cmd_get_properties_not_lazy_running_apps = constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS - self._cmd_get_properties_not_lazy_no_running_apps = constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS self._cmd_current_app = constants.CMD_CURRENT_APP self._cmd_launch_app = constants.CMD_LAUNCH_APP @@ -223,86 +219,3 @@ def _update(self, screen_on, awake, wake_lock_size, current_app, media_session_s state = constants.STATE_PAUSED return state, current_app, running_apps, hdmi_input - - # ======================================================================= # - # # - # Properties # - # # - # ======================================================================= # - def _get_properties(self, output, get_running_apps=True): - """Get the properties needed for Home Assistant updates. - - This will send one of the following ADB commands: - - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS` - - Parameters - ---------- - output : str, None - The output of the ADB command used to retrieve the properties - get_running_apps : bool - Whether or not to get the ``running_apps`` property - - Returns - ------- - screen_on : bool, None - Whether or not the device is on, or ``None`` if it was not determined - awake : bool, None - Whether or not the device is awake (screensaver is not running), or ``None`` if it was not determined - wake_lock_size : int, None - The size of the current wake lock, or ``None`` if it was not determined - current_app : str, None - The current app property, or ``None`` if it was not determined - media_session_state : int, None - The state from the output of ``dumpsys media_session``, or ``None`` if it was not determined - running_apps : list, None - A list of the running apps, or ``None`` if it was not determined - hdmi_input : str, None - The HDMI input, or ``None`` if it could not be determined - - """ - # ADB command was unsuccessful - if output is None: - return None, None, None, None, None, None, None - - # `screen_on` property - if not output: - return False, False, -1, None, None, None, None - screen_on = output[0] == "1" - - # `awake` property - if len(output) < 2: - return screen_on, False, -1, None, None, None, None - awake = output[1] == "1" - - lines = output.strip().splitlines() - - # `wake_lock_size` property - if len(lines[0]) < 3: - return screen_on, awake, -1, None, None, None, None - wake_lock_size = self._wake_lock_size(lines[0]) - - # `current_app` property - if len(lines) < 2: - return screen_on, awake, wake_lock_size, None, None, None, None - current_app = self._current_app(lines[1]) - - # `media_session_state` property - if len(lines) < 3: - return screen_on, awake, wake_lock_size, current_app, None, None, None - media_session_state = self._media_session_state(lines[2], current_app) - - # HDMI input property - if len(lines) < 4: - return screen_on, awake, wake_lock_size, current_app, media_session_state, None, None - hdmi_input = self._get_hdmi_input(lines[3]) - - # `running_apps` property - if not get_running_apps or len(lines) < 5: - return screen_on, awake, wake_lock_size, current_app, media_session_state, None, hdmi_input - running_apps = self._running_apps(lines[4:]) - - return screen_on, awake, wake_lock_size, current_app, media_session_state, running_apps, hdmi_input diff --git a/androidtv/firetv/firetv_async.py b/androidtv/firetv/firetv_async.py index 4d059616..202eeeba 100644 --- a/androidtv/firetv/firetv_async.py +++ b/androidtv/firetv/firetv_async.py @@ -100,13 +100,6 @@ async def update(self, get_running_apps=True, lazy=True): async def get_properties(self, get_running_apps=True, lazy=False): """Get the properties needed for Home Assistant updates. - This will send one of the following ADB commands: - - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS` - Parameters ---------- get_running_apps : bool @@ -132,19 +125,20 @@ async def get_properties(self, get_running_apps=True, lazy=False): The HDMI input, or ``None`` if it could not be determined """ - if lazy: - if get_running_apps: - output = await self._adb.shell(self._cmd_get_properties_lazy_running_apps) - else: - output = await self._adb.shell(self._cmd_get_properties_lazy_no_running_apps) + screen_on, awake, wake_lock_size = await self.screen_on_awake_wake_lock_size() + if lazy and not (screen_on and awake): + return screen_on, awake, wake_lock_size, None, None, None, None + + current_app, media_session_state = await self.current_app_media_session_state() + + if get_running_apps: + running_apps = await self.running_apps() else: - if get_running_apps: - output = await self._adb.shell(self._cmd_get_properties_not_lazy_running_apps) - else: - output = await self._adb.shell(self._cmd_get_properties_not_lazy_no_running_apps) - _LOGGER.debug("Fire TV %s:%d `get_properties` response: %s", self.host, self.port, output) + running_apps = None + + hdmi_input = await self.get_hdmi_input() - return self._get_properties(output, get_running_apps) + return screen_on, awake, wake_lock_size, current_app, media_session_state, running_apps, hdmi_input async def get_properties_dict(self, get_running_apps=True, lazy=True): """Get the properties needed for Home Assistant updates and return them as a dictionary. diff --git a/androidtv/firetv/firetv_sync.py b/androidtv/firetv/firetv_sync.py index 6bcb2722..66cb194b 100644 --- a/androidtv/firetv/firetv_sync.py +++ b/androidtv/firetv/firetv_sync.py @@ -100,13 +100,6 @@ def update(self, get_running_apps=True, lazy=True): def get_properties(self, get_running_apps=True, lazy=False): """Get the properties needed for Home Assistant updates. - This will send one of the following ADB commands: - - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS` - Parameters ---------- get_running_apps : bool @@ -132,19 +125,20 @@ def get_properties(self, get_running_apps=True, lazy=False): The HDMI input, or ``None`` if it could not be determined """ - if lazy: - if get_running_apps: - output = self._adb.shell(self._cmd_get_properties_lazy_running_apps) - else: - output = self._adb.shell(self._cmd_get_properties_lazy_no_running_apps) + screen_on, awake, wake_lock_size = self.screen_on_awake_wake_lock_size() + if lazy and not (screen_on and awake): + return screen_on, awake, wake_lock_size, None, None, None, None + + current_app, media_session_state = self.current_app_media_session_state() + + if get_running_apps: + running_apps = self.running_apps() else: - if get_running_apps: - output = self._adb.shell(self._cmd_get_properties_not_lazy_running_apps) - else: - output = self._adb.shell(self._cmd_get_properties_not_lazy_no_running_apps) - _LOGGER.debug("Fire TV %s:%d `get_properties` response: %s", self.host, self.port, output) + running_apps = None + + hdmi_input = self.get_hdmi_input() - return self._get_properties(output, get_running_apps) + return screen_on, awake, wake_lock_size, current_app, media_session_state, running_apps, hdmi_input def get_properties_dict(self, get_running_apps=True, lazy=True): """Get the properties needed for Home Assistant updates and return them as a dictionary. diff --git a/tests/test_basetv_async.py b/tests/test_basetv_async.py index a2a185c1..36b84ed8 100644 --- a/tests/test_basetv_async.py +++ b/tests/test_basetv_async.py @@ -408,7 +408,7 @@ async def test_screen_on(self): async def test_screen_on_awake_wake_lock_size(self): """Check that the ``screen_on_awake_wake_lock_size`` property works correctly.""" with async_patchers.patch_shell(None)[self.PATCH_KEY]: - self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (None, None, None)) with async_patchers.patch_shell("")[self.PATCH_KEY]: self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index c7825583..b6160ce8 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -565,7 +565,7 @@ def test_screen_on(self): def test_screen_on_awake_wake_lock_size(self): """Check that the ``screen_on_awake_wake_lock_size`` property works correctly.""" with patchers.patch_shell(None)[self.PATCH_KEY]: - self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (None, None, None)) with patchers.patch_shell("")[self.PATCH_KEY]: self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (False, False, None)) diff --git a/tests/test_constants.py b/tests/test_constants.py index 04fba4f3..afe9dcc2 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -119,30 +119,6 @@ def test_constants(self): r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11", ) - # CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS - self.assertEqual( - constants.CMD_FIRETV_PROPERTIES_LAZY_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && ps | grep u0_a", - ) - - # CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS - self.assertEqual( - constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo)", - ) - - # CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS - self.assertEqual( - constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && ps | grep u0_a", - ) - - # CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS - self.assertEqual( - constants.CMD_FIRETV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo)", - ) - def test_current_app_extraction_atv_launcher(self): dumpsys_output = """ mCurrentFocus=Window{e74bb23 u0 com.google.android.tvlauncher/com.google.android.tvlauncher.MainActivity} diff --git a/tests/test_firetv_async.py b/tests/test_firetv_async.py index e1e51540..90fc0003 100644 --- a/tests/test_firetv_async.py +++ b/tests/test_firetv_async.py @@ -24,9 +24,6 @@ async def setUp(self): ], async_patchers.patch_shell("")[self.PATCH_KEY]: self.ftv = FireTVAsync("HOST", 5555) await self.ftv.adb_connect() - self.assertEqual( - self.ftv._cmd_get_properties_lazy_no_running_apps, constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS - ) @awaiter async def test_turn_on_off(self): @@ -86,17 +83,64 @@ async def test_running_apps(self): async def test_get_properties(self): """Check that ``get_properties()`` works correctly.""" with async_patchers.patch_shell(None)[self.PATCH_KEY]: - for get_running_apps in [True, False]: - for lazy in [True, False]: - with patch_calls(self.ftv, self.ftv._get_properties) as patched: - await self.ftv.get_properties_dict(get_running_apps, lazy) - assert patched.called + with patch_calls( + self.ftv, self.ftv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patch_calls( + self.ftv, self.ftv.current_app_media_session_state + ) as current_app_media_session_state, patch_calls( + self.ftv, self.ftv.running_apps + ) as running_apps, patch_calls( + self.ftv, self.ftv.get_hdmi_input + ) as get_hdmi_input: + await self.ftv.get_properties(lazy=True) + assert screen_on_awake_wake_lock_size.called + assert not current_app_media_session_state.called + assert not running_apps.called + assert not get_hdmi_input.called + + with patch_calls( + self.ftv, self.ftv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patch_calls( + self.ftv, self.ftv.current_app_media_session_state + ) as current_app_media_session_state, patch_calls( + self.ftv, self.ftv.running_apps + ) as running_apps, patch_calls( + self.ftv, self.ftv.get_hdmi_input + ) as get_hdmi_input: + await self.ftv.get_properties(lazy=False, get_running_apps=True) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert running_apps.called + assert get_hdmi_input.called + + with patch_calls( + self.ftv, self.ftv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patch_calls( + self.ftv, self.ftv.current_app_media_session_state + ) as current_app_media_session_state, patch_calls( + self.ftv, self.ftv.running_apps + ) as running_apps, patch_calls( + self.ftv, self.ftv.get_hdmi_input + ) as get_hdmi_input: + await self.ftv.get_properties(lazy=False, get_running_apps=False) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert not running_apps.called + assert get_hdmi_input.called + + @awaiter + async def test_get_properties_dict(self): + """Check that ``get_properties_dict()`` works correctly.""" + with async_patchers.patch_shell(None)[self.PATCH_KEY]: + with patch_calls(self.ftv, self.ftv.get_properties) as get_properties: + await self.ftv.get_properties_dict() + assert get_properties.called @awaiter async def test_update(self): """Check that the ``update`` method works correctly.""" with async_patchers.patch_shell(None)[self.PATCH_KEY]: - with patch_calls(self.ftv, self.ftv._get_properties) as patched: + with patch_calls(self.ftv, self.ftv.get_properties) as patched: await self.ftv.update() assert patched.called diff --git a/tests/test_firetv_sync.py b/tests/test_firetv_sync.py index cc86c105..7dc61c05 100644 --- a/tests/test_firetv_sync.py +++ b/tests/test_firetv_sync.py @@ -25,152 +25,6 @@ RUNNING_APPS_LIST = ["com.netflix.ninja", "com.amazon.device.controllermanager"] -GET_PROPERTIES_OUTPUT1 = "" -GET_PROPERTIES_DICT1 = { - "screen_on": False, - "awake": False, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -STATE1 = (constants.STATE_OFF, None, None, None) - -GET_PROPERTIES_OUTPUT2 = "1" -GET_PROPERTIES_DICT2 = { - "screen_on": True, - "awake": False, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -STATE2 = (constants.STATE_STANDBY, None, None, None) - -GET_PROPERTIES_OUTPUT3 = """11Wake Locks: size=2 -com.amazon.tv.launcher - - -u0_a2 17243 197 998628 24932 ffffffff 00000000 S com.amazon.device.controllermanager -u0_a2 17374 197 995368 20764 ffffffff 00000000 S com.amazon.device.controllermanager:BluetoothReceiver""" -GET_PROPERTIES_DICT3 = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "running_apps": ["com.amazon.device.controllermanager", "com.amazon.device.controllermanager:BluetoothReceiver"], - "hdmi_input": None, -} -STATE3 = ( - constants.STATE_IDLE, - "com.amazon.tv.launcher", - ["com.amazon.device.controllermanager", "com.amazon.device.controllermanager:BluetoothReceiver"], - None, -) - -GET_PROPERTIES_OUTPUT3A = GET_PROPERTIES_OUTPUT3[0] -GET_PROPERTIES_OUTPUT3B = GET_PROPERTIES_OUTPUT3[:2] -GET_PROPERTIES_OUTPUT3C = GET_PROPERTIES_OUTPUT3.splitlines()[0] -GET_PROPERTIES_OUTPUT3D = "\n".join(GET_PROPERTIES_OUTPUT3.splitlines()[:2]) -GET_PROPERTIES_OUTPUT3E = "\n".join(GET_PROPERTIES_OUTPUT3.splitlines()[:3]) -GET_PROPERTIES_OUTPUT3F = "\n".join(GET_PROPERTIES_OUTPUT3.splitlines()[:4]) + "HW2" - -GET_PROPERTIES_DICT3A = { - "screen_on": True, - "awake": False, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3B = { - "screen_on": True, - "awake": True, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3C = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": None, - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3D = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3E = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3F = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "running_apps": None, - "hdmi_input": "HW2", -} - -GET_PROPERTIES_OUTPUT4 = """11Wake Locks: size=2 -com.amazon.tv.launcher -state=PlaybackState {state=2, position=0, buffered position=0, speed=0.0, updated=65749, actions=240640, custom actions=[], active item id=-1, error=null}""" -GET_PROPERTIES_DICT4 = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": 2, - "running_apps": None, - "hdmi_input": None, -} - -GET_PROPERTIES_OUTPUT5 = """11Wake Locks: size=2 -com.amazon.tv.launcher -state=PlaybackState {state=2, position=0, buffered position=0, speed=0.0, updated=65749, actions=240640, custom actions=[], active item id=-1, error=null} - -u0_a2 17243 197 998628 24932 ffffffff 00000000 S com.amazon.device.controllermanager -u0_a2 17374 197 995368 20764 ffffffff 00000000 S com.amazon.device.controllermanager:BluetoothReceiver""" -GET_PROPERTIES_DICT5 = { - "screen_on": True, - "awake": True, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": 2, - "running_apps": ["com.amazon.device.controllermanager", "com.amazon.device.controllermanager:BluetoothReceiver"], - "hdmi_input": None, -} - -GET_PROPERTIES_DICT_NONE = { - "screen_on": None, - "awake": None, - "wake_lock_size": None, - "media_session_state": None, - "current_app": None, - "running_apps": None, - "hdmi_input": None, -} STATE_NONE = (None, None, None, None) STATE_DETECTION_RULES1 = {"com.amazon.tv.launcher": ["off"]} @@ -190,9 +44,6 @@ def setUp(self): ]: self.ftv = FireTVSync("HOST", 5555) self.ftv.adb_connect() - self.assertEqual( - self.ftv._cmd_get_properties_lazy_no_running_apps, constants.CMD_FIRETV_PROPERTIES_LAZY_NO_RUNNING_APPS - ) def test_turn_on_off(self): """Test that the ``FireTVSync.turn_on`` and ``FireTVSync.turn_off`` methods work correctly.""" @@ -254,114 +105,64 @@ def test_running_apps(self): def test_get_properties(self): """Check that ``get_properties()`` works correctly.""" with patchers.patch_shell(None)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT_NONE) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT1)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT1) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT2)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT2) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3A)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3A) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3B)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3B) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3C)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3C) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3D)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3D) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3E)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3E) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3E)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True, get_running_apps=False) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3E) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3E)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=False, get_running_apps=False) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3E) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3F)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT3F) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT4)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT4) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT4)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True, get_running_apps=False) - self.assertDictEqual(properties, GET_PROPERTIES_DICT4) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT5)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=True) - self.assertDictEqual(properties, GET_PROPERTIES_DICT5) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT5)[self.PATCH_KEY]: - properties = self.ftv.get_properties_dict(lazy=False) - self.assertDictEqual(properties, GET_PROPERTIES_DICT5) + with patchers.patch_calls( + self.ftv, self.ftv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patchers.patch_calls( + self.ftv, self.ftv.current_app_media_session_state + ) as current_app_media_session_state, patchers.patch_calls( + self.ftv, self.ftv.running_apps + ) as running_apps, patchers.patch_calls( + self.ftv, self.ftv.get_hdmi_input + ) as get_hdmi_input: + self.ftv.get_properties(lazy=True) + assert screen_on_awake_wake_lock_size.called + assert not current_app_media_session_state.called + assert not running_apps.called + assert not get_hdmi_input.called + + with patchers.patch_calls( + self.ftv, self.ftv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patchers.patch_calls( + self.ftv, self.ftv.current_app_media_session_state + ) as current_app_media_session_state, patchers.patch_calls( + self.ftv, self.ftv.running_apps + ) as running_apps, patchers.patch_calls( + self.ftv, self.ftv.get_hdmi_input + ) as get_hdmi_input: + self.ftv.get_properties(lazy=False, get_running_apps=True) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert running_apps.called + assert get_hdmi_input.called + + with patchers.patch_calls( + self.ftv, self.ftv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patchers.patch_calls( + self.ftv, self.ftv.current_app_media_session_state + ) as current_app_media_session_state, patchers.patch_calls( + self.ftv, self.ftv.running_apps + ) as running_apps, patchers.patch_calls( + self.ftv, self.ftv.get_hdmi_input + ) as get_hdmi_input: + self.ftv.get_properties(lazy=False, get_running_apps=False) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert not running_apps.called + assert get_hdmi_input.called + + def test_get_properties_dict(self): + """Check that ``get_properties_dict()`` works correctly.""" + with patchers.patch_shell(None)[self.PATCH_KEY]: + with patchers.patch_calls(self.ftv, self.ftv.get_properties) as get_properties: + self.ftv.get_properties_dict() + assert get_properties.called def test_update(self): """Check that the ``update`` method works correctly.""" with patchers.patch_connect(False)[self.PATCH_KEY]: self.ftv.adb_connect() - state = self.ftv.update() - self.assertTupleEqual(state, STATE_NONE) - - with patchers.patch_connect(True)[self.PATCH_KEY]: - self.assertTrue(self.ftv.adb_connect()) - - with patchers.patch_shell(None)[self.PATCH_KEY]: - state = self.ftv.update() - self.assertTupleEqual(state, STATE_NONE) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT1)[self.PATCH_KEY]: - state = self.ftv.update() - self.assertTupleEqual(state, STATE1) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT2)[self.PATCH_KEY]: - state = self.ftv.update() - self.assertTupleEqual(state, STATE2) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3)[self.PATCH_KEY]: - state = self.ftv.update() - self.assertTupleEqual(state, STATE3) - - self.ftv._state_detection_rules = STATE_DETECTION_RULES1 - state = self.ftv.update() - self.assertEqual(state[0], constants.STATE_OFF) - - self.ftv._state_detection_rules = STATE_DETECTION_RULES2 - state = self.ftv.update() - self.assertEqual(state[0], constants.STATE_OFF) - - self.ftv._state_detection_rules = STATE_DETECTION_RULES3 - state = self.ftv.update() - self.assertEqual(state[0], constants.STATE_STANDBY) - - self.ftv._state_detection_rules = STATE_DETECTION_RULES4 - state = self.ftv.update() - self.assertEqual(state[0], constants.STATE_PAUSED) - self.ftv._state_detection_rules = STATE_DETECTION_RULES5 - state = self.ftv.update() - self.assertEqual(state[0], constants.STATE_IDLE) + self.assertTupleEqual(self.ftv.update(), STATE_NONE) def assertUpdate(self, get_properties, update): """Check that the results of the `update` method are as expected.""" From a242320a3bf69d05e8d134a9f525585a60b70dbb Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 16 Jan 2022 13:49:37 -0800 Subject: [PATCH 08/66] Split `get_properties()` for Android TV (#282) * Split `get_properties()` for Android TV * Uncomment * Fix test * Fix * Cleanup * Copy & paste async tests * Remove _get_properties() method * Fix tests * 100% coverage * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup * Cleanup --- androidtv/androidtv/androidtv_async.py | 38 +- androidtv/androidtv/androidtv_sync.py | 38 +- androidtv/androidtv/base_androidtv.py | 156 -------- androidtv/basetv/basetv.py | 36 +- tests/test_androidtv_async.py | 65 ++- tests/test_androidtv_sync.py | 523 ++++--------------------- 6 files changed, 192 insertions(+), 664 deletions(-) diff --git a/androidtv/androidtv/androidtv_async.py b/androidtv/androidtv/androidtv_async.py index 9887dde0..40e391db 100644 --- a/androidtv/androidtv/androidtv_async.py +++ b/androidtv/androidtv/androidtv_async.py @@ -158,19 +158,35 @@ async def get_properties(self, get_running_apps=True, lazy=False): The HDMI input, or ``None`` if it could not be determined """ - if lazy: - if get_running_apps: - output = await self._adb.shell(self._cmd_get_properties_lazy_running_apps) - else: - output = await self._adb.shell(self._cmd_get_properties_lazy_no_running_apps) + screen_on, awake, wake_lock_size = await self.screen_on_awake_wake_lock_size() + + if lazy and not (screen_on and awake): + return screen_on, awake, None, wake_lock_size, None, None, None, None, None, None, None + + audio_state = await self.audio_state() + current_app, media_session_state = await self.current_app_media_session_state() + audio_output_device, is_volume_muted, volume, _ = await self.stream_music_properties() + + if get_running_apps: + running_apps = await self.running_apps() else: - if get_running_apps: - output = await self._adb.shell(self._cmd_get_properties_not_lazy_running_apps) - else: - output = await self._adb.shell(self._cmd_get_properties_not_lazy_no_running_apps) - _LOGGER.debug("Android TV %s:%d `get_properties` response: %s", self.host, self.port, output) + running_apps = [current_app] if current_app else None + + hdmi_input = await self.get_hdmi_input() - return self._get_properties(output, get_running_apps) + return ( + screen_on, + awake, + audio_state, + wake_lock_size, + current_app, + media_session_state, + audio_output_device, + is_volume_muted, + volume, + running_apps, + hdmi_input, + ) async def get_properties_dict(self, get_running_apps=True, lazy=True): """Get the properties needed for Home Assistant updates and return them as a dictionary. diff --git a/androidtv/androidtv/androidtv_sync.py b/androidtv/androidtv/androidtv_sync.py index 7ee7147f..101807ec 100644 --- a/androidtv/androidtv/androidtv_sync.py +++ b/androidtv/androidtv/androidtv_sync.py @@ -160,19 +160,35 @@ def get_properties(self, get_running_apps=True, lazy=False): The HDMI input, or ``None`` if it could not be determined """ - if lazy: - if get_running_apps: - output = self._adb.shell(self._cmd_get_properties_lazy_running_apps) - else: - output = self._adb.shell(self._cmd_get_properties_lazy_no_running_apps) + screen_on, awake, wake_lock_size = self.screen_on_awake_wake_lock_size() + + if lazy and not (screen_on and awake): + return screen_on, awake, None, wake_lock_size, None, None, None, None, None, None, None + + audio_state = self.audio_state() + current_app, media_session_state = self.current_app_media_session_state() + audio_output_device, is_volume_muted, volume, _ = self.stream_music_properties() + + if get_running_apps: + running_apps = self.running_apps() else: - if get_running_apps: - output = self._adb.shell(self._cmd_get_properties_not_lazy_running_apps) - else: - output = self._adb.shell(self._cmd_get_properties_not_lazy_no_running_apps) - _LOGGER.debug("Android TV %s:%d `get_properties` response: %s", self.host, self.port, output) + running_apps = [current_app] if current_app else None + + hdmi_input = self.get_hdmi_input() - return self._get_properties(output, get_running_apps) + return ( + screen_on, + awake, + audio_state, + wake_lock_size, + current_app, + media_session_state, + audio_output_device, + is_volume_muted, + volume, + running_apps, + hdmi_input, + ) def get_properties_dict(self, get_running_apps=True, lazy=True): """Get the properties needed for Home Assistant updates and return them as a dictionary. diff --git a/androidtv/androidtv/base_androidtv.py b/androidtv/androidtv/base_androidtv.py index 04823f47..5034374d 100644 --- a/androidtv/androidtv/base_androidtv.py +++ b/androidtv/androidtv/base_androidtv.py @@ -241,159 +241,3 @@ def _update( state = constants.STATE_IDLE return state, current_app, running_apps, audio_output_device, is_volume_muted, volume_level, hdmi_input - - # ======================================================================= # - # # - # Properties # - # # - # ======================================================================= # - def _get_properties(self, output, get_running_apps): - """Get the properties needed for Home Assistant updates. - - Parameters - ---------- - output : str, None - The output of the ADB command used to retrieve the properties - get_running_apps : bool - Whether or not to get the ``running_apps`` property - - Returns - ------- - screen_on : bool, None - Whether or not the device is on, or ``None`` if it was not determined - awake : bool, None - Whether or not the device is awake (screensaver is not running), or ``None`` if it was not determined - audio_state : str, None - The audio state, as determined from "dumpsys audio", or ``None`` if it was not determined - wake_lock_size : int, None - The size of the current wake lock, or ``None`` if it was not determined - current_app : str, None - The current app property, or ``None`` if it was not determined - media_session_state : int, None - The state from the output of ``dumpsys media_session``, or ``None`` if it was not determined - audio_output_device : str, None - The current audio playback device, or ``None`` if it was not determined - is_volume_muted : bool, None - Whether or not the volume is muted, or ``None`` if it was not determined - volume : int, None - The absolute volume level, or ``None`` if it was not determined - running_apps : list, None - A list of the running apps, or ``None`` if it was not determined - hdmi_input : str, None - The HDMI input, or ``None`` if it could not be determined - - """ - # ADB command was unsuccessful - if output is None: - return None, None, None, None, None, None, None, None, None, None, None - - # `screen_on` property - if not output: - return False, False, None, -1, None, None, None, None, None, None, None - screen_on = output[0] == "1" - - # `awake` property - if len(output) < 2: - return screen_on, False, None, -1, None, None, None, None, None, None, None - awake = output[1] == "1" - - # `audio_state` property - if len(output) < 3: - return screen_on, awake, None, -1, None, None, None, None, None, None, None - audio_state = self._audio_state(output[2]) - - lines = output.strip().splitlines() - - # `wake_lock_size` property - if len(lines[0]) < 4: - return screen_on, awake, audio_state, -1, None, None, None, None, None, None, None - wake_lock_size = self._wake_lock_size(lines[0]) - - # `current_app` property - if len(lines) < 2: - return screen_on, awake, audio_state, wake_lock_size, None, None, None, None, None, None, None - current_app = self._current_app(lines[1]) - - # `media_session_state` property - if len(lines) < 3: - return screen_on, awake, audio_state, wake_lock_size, current_app, None, None, None, None, None, None - media_session_state = self._media_session_state(lines[2], current_app) - - # HDMI input property - if len(lines) < 4: - return ( - screen_on, - awake, - audio_state, - wake_lock_size, - current_app, - media_session_state, - None, - None, - None, - None, - None, - ) - hdmi_input = self._get_hdmi_input(lines[3]) - - # "STREAM_MUSIC" block - if len(lines) < 5: - return ( - screen_on, - awake, - audio_state, - wake_lock_size, - current_app, - media_session_state, - None, - None, - None, - None, - hdmi_input, - ) - - # reconstruct the output of `constants.CMD_STREAM_MUSIC` - stream_music_raw = "\n".join(lines[4:]) - - # the "STREAM_MUSIC" block from `adb shell dumpsys audio` - stream_music = self._parse_stream_music(stream_music_raw) - - # `audio_output_device` property - audio_output_device = self._audio_output_device(stream_music) - - # `volume` property - volume = self._volume(stream_music, audio_output_device) - - # `is_volume_muted` property - is_volume_muted = self._is_volume_muted(stream_music) - - # `running_apps` property - if not get_running_apps or len(lines) < 17: - return ( - screen_on, - awake, - audio_state, - wake_lock_size, - current_app, - media_session_state, - audio_output_device, - is_volume_muted, - volume, - None, - hdmi_input, - ) - running_apps = self._running_apps(lines[16:]) - - return ( - screen_on, - awake, - audio_state, - wake_lock_size, - current_app, - media_session_state, - audio_output_device, - is_volume_muted, - volume, - running_apps, - hdmi_input, - ) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 45f47f90..f5c5a010 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -391,11 +391,11 @@ def _current_app_media_session_state(self, media_session_state_response): current_app = self._current_app(lines[0].strip()) if len(lines) > 1: - media_session_state = self._media_session_state(lines[1], current_app) - else: - media_session_state = None + matches = constants.REGEX_MEDIA_SESSION_STATE.search(media_session_state_response) + if matches: + return current_app, int(matches.group("state")) - return current_app, media_session_state + return current_app, None @staticmethod def _get_hdmi_input(hdmi_response): @@ -460,32 +460,6 @@ def _is_volume_muted(stream_music): return None - @staticmethod - def _media_session_state(media_session_state_response, current_app): - """Get the state from the output of :py:const:`androidtv.constants.CMD_MEDIA_SESSION_STATE`. - - Parameters - ---------- - media_session_state_response : str, None - The output of :py:const:`androidtv.constants.CMD_MEDIA_SESSION_STATE` - current_app : str, None - The current app, or ``None`` if it could not be determined - - Returns - ------- - int, None - The state from the output of the ADB shell command, or ``None`` if it could not be determined - - """ - if not media_session_state_response or not current_app: - return None - - matches = constants.REGEX_MEDIA_SESSION_STATE.search(media_session_state_response) - if matches: - return int(matches.group("state")) - - return None - @staticmethod def _parse_stream_music(stream_music_raw): """Parse the output of the command :py:const:`androidtv.constants.CMD_STREAM_MUSIC`. @@ -526,8 +500,6 @@ def _running_apps(running_apps_response): """ if running_apps_response: - if isinstance(running_apps_response, list): - return [line.strip().rsplit(" ", 1)[-1] for line in running_apps_response if line.strip()] return [line.strip().rsplit(" ", 1)[-1] for line in running_apps_response.splitlines() if line.strip()] return None diff --git a/tests/test_androidtv_async.py b/tests/test_androidtv_async.py index 0175f92b..df165d7c 100644 --- a/tests/test_androidtv_async.py +++ b/tests/test_androidtv_async.py @@ -222,13 +222,66 @@ async def test_volume_down(self): @awaiter async def test_get_properties(self): - """Check that the ``get_properties`` method works correctly.""" + """Check that ``get_properties()`` works correctly.""" with async_patchers.patch_shell(None)[self.PATCH_KEY]: - for get_running_apps in [True, False]: - for lazy in [True, False]: - with patch_calls(self.atv, self.atv._get_properties) as patched: - await self.atv.get_properties_dict(get_running_apps, lazy) - assert patched.called + with patch_calls( + self.atv, self.atv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patch_calls( + self.atv, self.atv.current_app_media_session_state + ) as current_app_media_session_state, patch_calls( + self.atv, self.atv.stream_music_properties + ) as stream_music_properties, patch_calls( + self.atv, self.atv.running_apps + ) as running_apps, patch_calls( + self.atv, self.atv.get_hdmi_input + ) as get_hdmi_input: + await self.atv.get_properties(lazy=True) + assert screen_on_awake_wake_lock_size.called + assert not current_app_media_session_state.called + assert not running_apps.called + assert not get_hdmi_input.called + + with patch_calls( + self.atv, self.atv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patch_calls( + self.atv, self.atv.current_app_media_session_state + ) as current_app_media_session_state, patch_calls( + self.atv, self.atv.stream_music_properties + ) as stream_music_properties, patch_calls( + self.atv, self.atv.running_apps + ) as running_apps, patch_calls( + self.atv, self.atv.get_hdmi_input + ) as get_hdmi_input: + await self.atv.get_properties(lazy=False, get_running_apps=True) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert running_apps.called + assert get_hdmi_input.called + + with patch_calls( + self.atv, self.atv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patch_calls( + self.atv, self.atv.current_app_media_session_state + ) as current_app_media_session_state, patch_calls( + self.atv, self.atv.stream_music_properties + ) as stream_music_properties, patch_calls( + self.atv, self.atv.running_apps + ) as running_apps, patch_calls( + self.atv, self.atv.get_hdmi_input + ) as get_hdmi_input: + await self.atv.get_properties(lazy=False, get_running_apps=False) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert not running_apps.called + assert get_hdmi_input.called + + @awaiter + async def test_get_properties_dict(self): + """Check that ``get_properties_dict()`` works correctly.""" + with async_patchers.patch_shell(None)[self.PATCH_KEY]: + with patch_calls(self.atv, self.atv.get_properties) as get_properties: + await self.atv.get_properties_dict() + assert get_properties.called @awaiter async def test_update(self): diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index a6ae6fa0..c9ead005 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -67,352 +67,20 @@ RUNNING_APPS_LIST = ["com.netflix.ninja", "com.amazon.device.controllermanager"] -GET_PROPERTIES_OUTPUT1 = "" -GET_PROPERTIES_DICT1 = { - "screen_on": False, - "awake": False, - "audio_state": None, - "wake_lock_size": -1, - "media_session_state": None, - "current_app": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -STATE1 = (constants.STATE_OFF, None, None, None, None, None, None) - -GET_PROPERTIES_OUTPUT2 = "1" -GET_PROPERTIES_DICT2 = { - "screen_on": True, - "awake": False, - "audio_state": None, - "wake_lock_size": -1, - "media_session_state": None, - "current_app": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -STATE2 = (constants.STATE_STANDBY, None, None, None, None, None, None) - -GET_PROPERTIES_OUTPUT3 = ( - """110Wake Locks: size=2 -com.amazon.tv.launcher - -""" - + HDMI_INPUT_EMPTY - + STREAM_MUSIC_ON -) -GET_PROPERTIES_DICT3 = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "audio_output_device": "hmdi_arc", - "is_volume_muted": False, - "volume": 22, - "running_apps": None, - "hdmi_input": None, -} -STATE3 = ( - constants.STATE_PLAYING, +GET_PROPERTIES_OUTPUT = ( + True, + True, + constants.STATE_IDLE, + 2, "com.amazon.tv.launcher", - ["com.amazon.tv.launcher"], - "hmdi_arc", - False, - (22 / 60.0), None, -) - -GET_PROPERTIES_OUTPUT3A = GET_PROPERTIES_OUTPUT3[:1] -GET_PROPERTIES_OUTPUT3B = GET_PROPERTIES_OUTPUT3[:2] -GET_PROPERTIES_OUTPUT3C = GET_PROPERTIES_OUTPUT3[:3] -GET_PROPERTIES_OUTPUT3D = GET_PROPERTIES_OUTPUT3.splitlines()[0] -GET_PROPERTIES_OUTPUT3E = "\n".join(GET_PROPERTIES_OUTPUT3.splitlines()[:2]) -GET_PROPERTIES_OUTPUT3F = "\n".join(GET_PROPERTIES_OUTPUT3.splitlines()[:3]) -GET_PROPERTIES_OUTPUT3G = "\n".join(GET_PROPERTIES_OUTPUT3.splitlines()[:4]) + "HW2" - -GET_PROPERTIES_DICT3A = { - "screen_on": True, - "awake": False, - "audio_state": None, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3B = { - "screen_on": True, - "awake": True, - "audio_state": None, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3C = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": -1, - "current_app": None, - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3D = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 2, - "current_app": None, - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3E = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3F = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -GET_PROPERTIES_DICT3G = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": "HW2", -} - -GET_PROPERTIES_OUTPUT4 = """111Wake Locks: size=2 -com.amazon.tv.launcher -state=PlaybackState {state=1, position=0, buffered position=0, speed=0.0, updated=65749, actions=240640, custom actions=[], active item id=-1, error=null} -""" -GET_PROPERTIES_DICT4 = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_PAUSED, - "wake_lock_size": 2, - "current_app": "com.amazon.tv.launcher", - "media_session_state": 1, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} - -GET_PROPERTIES_DICT_NONE = { - "screen_on": None, - "awake": None, - "audio_state": None, - "wake_lock_size": None, - "media_session_state": None, - "current_app": None, - "audio_output_device": None, - "is_volume_muted": None, - "volume": None, - "running_apps": None, - "hdmi_input": None, -} -STATE_NONE = (None, None, None, None, None, None, None) - -# Source: https://community.home-assistant.io/t/new-chromecast-w-android-tv-integration-only-showing-as-off-or-idle/234424/17 -GET_PROPERTIES_OUTPUT_GOOGLE_TV = """111Wake Locks: size=4 -com.google.android.youtube.tv - state=PlaybackState {state=3, position=610102, buffered position=0, speed=1.0, updated=234649304, actions=379, custom actions=[], active item id=-1, error=null} - -- STREAM_MUSIC: - Muted: false - Min: 0 - Max: 25 - streamVolume:25 - Current: 4 (headset): 10, 8 (headphone): 10, 400 (hdmi): 25, 4000000 (usb_headset): 6, 40000000 (default): 20 - Devices: hdmi -- STREAM_ALARM: - Muted: false - Min: 1 - Max: 7 - streamVolume:6 -u0_a38 16522 16265 1348552 89960 0 0 S com.android.systemui -u0_a56 16765 16265 1343904 94124 0 0 S com.google.android.inputmethod.latin -u0_a42 16783 16265 1302956 67868 0 0 S com.google.android.tv.remote.service -""" - -GET_PROPERTIES_DICT_GOOGLE_TV = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_PAUSED, - "wake_lock_size": 4, - "current_app": "com.google.android.youtube.tv", - "media_session_state": 3, - "audio_output_device": "hdmi", - "is_volume_muted": False, - "volume": 25, - "running_apps": [ - "com.android.systemui", - "com.google.android.inputmethod.latin", - "com.google.android.tv.remote.service", - ], - "hdmi_input": None, -} - -# https://community.home-assistant.io/t/testers-needed-custom-state-detection-rules-for-android-tv-fire-tv/129493/6?u=jefflirion -STATE_DETECTION_RULES_PLEX = { - "com.plexapp.android": [ - {"playing": {"media_session_state": 3, "wake_lock_size": 3}}, - {"paused": {"media_session_state": 3, "wake_lock_size": 1}}, - "idle", - ] -} - -# Plex: idle -GET_PROPERTIES_OUTPUT_PLEX_IDLE = ( - """110Wake Locks: size=1 -com.plexapp.android - -""" - + HDMI_INPUT_EMPTY - + STREAM_MUSIC_ON -) - -GET_PROPERTIES_DICT_PLEX_IDLE = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 1, - "media_session_state": None, - "current_app": "com.plexapp.android", - "audio_output_device": "hmdi_arc", - "is_volume_muted": False, - "volume": 22, - "running_apps": None, - "hdmi_input": None, -} - -STATE_PLEX_IDLE = ( - constants.STATE_PLAYING, - "com.plexapp.android", - ["com.plexapp.android"], "hmdi_arc", False, - 22 / 60.0, + 22, None, -) - -# Plex: playing -GET_PROPERTIES_OUTPUT_PLEX_PLAYING = ( - """110Wake Locks: size=3 -com.plexapp.android -state=3 -""" - + HDMI_INPUT_EMPTY - + STREAM_MUSIC_ON -) - -GET_PROPERTIES_DICT_PLEX_PLAYING = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 3, - "media_session_state": 3, - "current_app": "com.plexapp.android", - "audio_output_device": "hmdi_arc", - "is_volume_muted": False, - "volume": 22, - "running_apps": None, - "hdmi_input": None, -} - -STATE_PLEX_PLAYING = ( - constants.STATE_PLAYING, - "com.plexapp.android", - ["com.plexapp.android"], - "hmdi_arc", - False, - 22 / 60.0, - None, -) - -# Plex: paused -GET_PROPERTIES_OUTPUT_PLEX_PAUSED = ( - """110Wake Locks: size=1 -com.plexapp.android -state=3 -""" - + HDMI_INPUT_EMPTY - + STREAM_MUSIC_ON -) - -GET_PROPERTIES_DICT_PLEX_PAUSED = { - "screen_on": True, - "awake": True, - "audio_state": constants.STATE_IDLE, - "wake_lock_size": 1, - "media_session_state": 3, - "current_app": "com.plexapp.android", - "audio_output_device": "hmdi_arc", - "is_volume_muted": False, - "volume": 22, - "running_apps": None, - "hdmi_input": None, -} - -STATE_PLEX_PAUSED = ( - constants.STATE_PAUSED, - "com.plexapp.android", - ["com.plexapp.android"], - "hmdi_arc", - False, - 22 / 60.0, None, ) +STATE_NONE = (None, None, None, None, None, None, None) STATE_DETECTION_RULES1 = {"com.amazon.tv.launcher": ["off"]} STATE_DETECTION_RULES2 = {"com.amazon.tv.launcher": ["media_session_state", "off"]} @@ -606,100 +274,74 @@ def test_volume_down(self): self.assertEqual(getattr(self.atv._adb, self.ADB_ATTR).shell_cmd, "input keyevent 25") def test_get_properties(self): - """Check that the ``get_properties`` method works correctly.""" + """Check that ``get_properties()`` works correctly.""" with patchers.patch_shell(None)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_NONE) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT1)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT1) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT2)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT2) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3A)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3A) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3B)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3B) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3C)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3C) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3D)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3D) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3E)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3E) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3F)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3F) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3G)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT3G) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT4)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT4) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT4)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=False) - self.assertEqual(properties, GET_PROPERTIES_DICT4) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT4)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=False, lazy=False) - self.assertEqual(properties, GET_PROPERTIES_DICT4) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_IDLE)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_PLEX_IDLE) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_PLAYING)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_PLEX_PLAYING) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_PAUSED)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_PLEX_PAUSED) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_PAUSED)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_PLEX_PAUSED) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_PAUSED)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=False, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_PLEX_PAUSED) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_PAUSED + RUNNING_APPS_OUTPUT)[self.PATCH_KEY]: - true_properties = GET_PROPERTIES_DICT_PLEX_PAUSED.copy() - true_properties["running_apps"] = RUNNING_APPS_LIST - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, true_properties) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_PLEX_PAUSED + RUNNING_APPS_OUTPUT)[self.PATCH_KEY]: - true_properties = GET_PROPERTIES_DICT_PLEX_PAUSED.copy() - true_properties["running_apps"] = RUNNING_APPS_LIST - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=False) - self.assertEqual(properties, true_properties) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT_GOOGLE_TV)[self.PATCH_KEY]: - properties = self.atv.get_properties_dict(get_running_apps=True, lazy=True) - self.assertEqual(properties, GET_PROPERTIES_DICT_GOOGLE_TV) + with patchers.patch_calls( + self.atv, self.atv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patchers.patch_calls( + self.atv, self.atv.current_app_media_session_state + ) as current_app_media_session_state, patchers.patch_calls( + self.atv, self.atv.stream_music_properties + ) as stream_music_properties, patchers.patch_calls( + self.atv, self.atv.running_apps + ) as running_apps, patchers.patch_calls( + self.atv, self.atv.get_hdmi_input + ) as get_hdmi_input: + self.atv.get_properties(lazy=True) + assert screen_on_awake_wake_lock_size.called + assert not current_app_media_session_state.called + assert not running_apps.called + assert not get_hdmi_input.called + + with patchers.patch_calls( + self.atv, self.atv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patchers.patch_calls( + self.atv, self.atv.current_app_media_session_state + ) as current_app_media_session_state, patchers.patch_calls( + self.atv, self.atv.stream_music_properties + ) as stream_music_properties, patchers.patch_calls( + self.atv, self.atv.running_apps + ) as running_apps, patchers.patch_calls( + self.atv, self.atv.get_hdmi_input + ) as get_hdmi_input: + self.atv.get_properties(lazy=False, get_running_apps=True) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert running_apps.called + assert get_hdmi_input.called + + with patchers.patch_calls( + self.atv, self.atv.screen_on_awake_wake_lock_size + ) as screen_on_awake_wake_lock_size, patchers.patch_calls( + self.atv, self.atv.current_app_media_session_state + ) as current_app_media_session_state, patchers.patch_calls( + self.atv, self.atv.stream_music_properties + ) as stream_music_properties, patchers.patch_calls( + self.atv, self.atv.running_apps + ) as running_apps, patchers.patch_calls( + self.atv, self.atv.get_hdmi_input + ) as get_hdmi_input: + self.atv.get_properties(lazy=False, get_running_apps=False) + assert screen_on_awake_wake_lock_size.called + assert current_app_media_session_state.called + assert not running_apps.called + assert get_hdmi_input.called + + def test_get_properties_dict(self): + """Check that ``get_properties_dict()`` works correctly.""" + with patchers.patch_shell(None)[self.PATCH_KEY]: + with patchers.patch_calls(self.atv, self.atv.get_properties) as get_properties: + self.atv.get_properties_dict() + assert get_properties.called def test_update(self): + """Check that the ``update`` method works correctly.""" + with patchers.patch_shell(None)[self.PATCH_KEY]: + with patchers.patch_calls(self.atv, self.atv._update) as patched: + self.atv.update() + assert patched.called + + def test_update2(self): """Check that the ``update`` method works correctly.""" with patchers.patch_connect(False)[self.PATCH_KEY]: self.atv.adb_connect() @@ -713,18 +355,9 @@ def test_update(self): state = self.atv.update() self.assertTupleEqual(state, STATE_NONE) - with patchers.patch_shell(GET_PROPERTIES_OUTPUT1)[self.PATCH_KEY]: - state = self.atv.update() - self.assertTupleEqual(state, STATE1) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT2)[self.PATCH_KEY]: - state = self.atv.update() - self.assertTupleEqual(state, STATE2) - - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3)[self.PATCH_KEY]: - state = self.atv.update() - self.assertTupleEqual(state, STATE3) - + with patch( + "androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties", return_value=GET_PROPERTIES_OUTPUT + ): self.atv._state_detection_rules = STATE_DETECTION_RULES1 state = self.atv.update() self.assertEqual(state[0], constants.STATE_OFF) @@ -745,12 +378,6 @@ def test_update(self): state = self.atv.update() self.assertEqual(state[0], constants.STATE_IDLE) - with patchers.patch_shell(GET_PROPERTIES_OUTPUT3 + RUNNING_APPS_OUTPUT)[self.PATCH_KEY]: - self.atv._state_detection_rules = None - state = self.atv.update(get_running_apps=True) - true_state = STATE3[:2] + (RUNNING_APPS_LIST,) + STATE3[3:] - self.assertTupleEqual(state, true_state) - def assertUpdate(self, get_properties, update): """Check that the results of the `update` method are as expected.""" with patch("androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties", return_value=get_properties): From 0cc329740005b4d56bfae13b9b396a99c8a5586d Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 16 Jan 2022 13:57:53 -0800 Subject: [PATCH 09/66] Enforce 100% coverage (#283) * Enforce 100% coverage * Update .github/workflows/python-package.yml * Update .github/workflows/python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bebb43d9..c18681c0 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -44,4 +44,4 @@ jobs: COVERALLS_SERVICE_NAME: github run: | if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5" || python --version 2>&1 | grep -q "Python 3.6" ; then for synctest in $(cd tests && ls test*.py | grep -v async); do python -m unittest discover -s tests/ -t . -p "$synctest" || exit 1; done; fi - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"|| python --version 2>&1 | grep -q "Python 3.10"; then coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m && coveralls; fi + if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"|| python --version 2>&1 | grep -q "Python 3.10"; then coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m --fail-under 100 && coveralls; fi From ff6105f2222d576a2caf75a2046065a6e6815c86 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 16 Jan 2022 14:27:09 -0800 Subject: [PATCH 10/66] Remove unused `CMD_*` constants (#284) * Cleanup * Cleanup --- androidtv/androidtv/androidtv_async.py | 7 - androidtv/androidtv/androidtv_sync.py | 7 - androidtv/androidtv/base_androidtv.py | 10 -- androidtv/constants.py | 176 ------------------------- tests/test_androidtv_async.py | 4 - tests/test_androidtv_sync.py | 4 - tests/test_basetv_sync.py | 4 +- tests/test_constants.py | 48 ------- 8 files changed, 2 insertions(+), 258 deletions(-) diff --git a/androidtv/androidtv/androidtv_async.py b/androidtv/androidtv/androidtv_async.py index 40e391db..dfb05943 100644 --- a/androidtv/androidtv/androidtv_async.py +++ b/androidtv/androidtv/androidtv_async.py @@ -118,13 +118,6 @@ async def update(self, get_running_apps=True, lazy=True): async def get_properties(self, get_running_apps=True, lazy=False): """Get the properties needed for Home Assistant updates. - This will send one of the following ADB commands: - - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS` - Parameters ---------- get_running_apps : bool diff --git a/androidtv/androidtv/androidtv_sync.py b/androidtv/androidtv/androidtv_sync.py index 101807ec..0d795fa6 100644 --- a/androidtv/androidtv/androidtv_sync.py +++ b/androidtv/androidtv/androidtv_sync.py @@ -120,13 +120,6 @@ def update(self, get_running_apps=True, lazy=True): def get_properties(self, get_running_apps=True, lazy=False): """Get the properties needed for Home Assistant updates. - This will send one of the following ADB commands: - - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS` - * :py:const:`androidtv.constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS`` - Parameters ---------- get_running_apps : bool diff --git a/androidtv/androidtv/base_androidtv.py b/androidtv/androidtv/base_androidtv.py index 5034374d..4a5ca846 100644 --- a/androidtv/androidtv/base_androidtv.py +++ b/androidtv/androidtv/base_androidtv.py @@ -43,20 +43,10 @@ def _fill_in_commands(self): if "Google" in self.device_properties.get("manufacturer", "") and "Chromecast" in self.device_properties.get( "model", "" ): - self._cmd_get_properties_lazy_running_apps = constants.CMD_GOOGLE_TV_PROPERTIES_LAZY_RUNNING_APPS - self._cmd_get_properties_lazy_no_running_apps = constants.CMD_GOOGLE_TV_PROPERTIES_LAZY_NO_RUNNING_APPS - self._cmd_get_properties_not_lazy_running_apps = constants.CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_RUNNING_APPS - self._cmd_get_properties_not_lazy_no_running_apps = ( - constants.CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS - ) self._cmd_current_app = constants.CMD_CURRENT_APP_GOOGLE_TV self._cmd_launch_app = constants.CMD_LAUNCH_APP_GOOGLE_TV return - self._cmd_get_properties_lazy_running_apps = constants.CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS - self._cmd_get_properties_lazy_no_running_apps = constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS - self._cmd_get_properties_not_lazy_running_apps = constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS - self._cmd_get_properties_not_lazy_no_running_apps = constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS self._cmd_current_app = constants.CMD_CURRENT_APP self._cmd_launch_app = constants.CMD_LAUNCH_APP diff --git a/androidtv/constants.py b/androidtv/constants.py index 942e4e0b..2139448c 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -98,182 +98,6 @@ CMD_SCREEN_ON + CMD_SUCCESS1_FAILURE0 + " && " + CMD_AWAKE + CMD_SUCCESS1_FAILURE0 + " && " + CMD_WAKE_LOCK_SIZE ) -#: Get the properties for an Android TV device (``lazy=True, get_running_apps=True``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC - + " && " - + CMD_RUNNING_APPS_ANDROIDTV -) - -#: Get the properties for an Android TV device (``lazy=True, get_running_apps=False``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC -) - -#: Get the properties for an Android TV device (``lazy=False, get_running_apps=True``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1_FAILURE0 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC - + " && " - + CMD_RUNNING_APPS_ANDROIDTV -) - -#: Get the properties for an Android TV device (``lazy=False, get_running_apps=False``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1_FAILURE0 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC -) - -#: Get the properties for a Google TV device (``lazy=True, get_running_apps=True``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_GOOGLE_TV_PROPERTIES_LAZY_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP_GOOGLE_TV - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC - + " && " - + CMD_RUNNING_APPS_ANDROIDTV -) - -#: Get the properties for a Google TV device (``lazy=True, get_running_apps=False``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_GOOGLE_TV_PROPERTIES_LAZY_NO_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP_GOOGLE_TV - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC -) - -#: Get the properties for a Google TV device (``lazy=False, get_running_apps=True``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1_FAILURE0 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP_GOOGLE_TV - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC - + " && " - + CMD_RUNNING_APPS_ANDROIDTV -) - -#: Get the properties for a Google TV device (``lazy=False, get_running_apps=False``); see :py:meth:`androidtv.androidtv.androidtv_sync.AndroidTVSync.get_properties` and :py:meth:`androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties` -CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS = ( - CMD_SCREEN_ON - + CMD_SUCCESS1_FAILURE0 - + " && " - + CMD_AWAKE - + CMD_SUCCESS1_FAILURE0 - + " && (" - + CMD_AUDIO_STATE - + ") && " - + CMD_WAKE_LOCK_SIZE - + " && " - + CMD_CURRENT_APP_GOOGLE_TV - + " && (" - + CMD_MEDIA_SESSION_STATE - + " || echo) && (" - + CMD_HDMI_INPUT - + " || echo) && " - + CMD_STREAM_MUSIC -) - # `getprop` commands CMD_MANUFACTURER = "getprop ro.product.manufacturer" CMD_MODEL = "getprop ro.product.model" diff --git a/tests/test_androidtv_async.py b/tests/test_androidtv_async.py index df165d7c..98fbc378 100644 --- a/tests/test_androidtv_async.py +++ b/tests/test_androidtv_async.py @@ -67,10 +67,6 @@ async def setUp(self): ], async_patchers.patch_shell("")[self.PATCH_KEY]: self.atv = AndroidTVAsync("HOST", 5555) await self.atv.adb_connect() - self.assertEqual( - self.atv._cmd_get_properties_lazy_no_running_apps, - constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS, - ) @awaiter async def test_turn_on_off(self): diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index c9ead005..3da6d085 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -99,10 +99,6 @@ def setUp(self): ]: self.atv = AndroidTVSync("HOST", 5555) self.atv.adb_connect() - self.assertEqual( - self.atv._cmd_get_properties_lazy_no_running_apps, - constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS, - ) def test_turn_on_off(self): """Test that the ``AndroidTVSync.turn_on`` and ``AndroidTVSync.turn_off`` methods work correctly.""" diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index b6160ce8..4e0b2705 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -468,8 +468,8 @@ def test_get_device_properties(self): device_properties = self.btv.get_device_properties() self.assertEqual(self.btv.device_properties["manufacturer"], "Google") self.assertEqual( - self.btv._cmd_get_properties_lazy_no_running_apps, - constants.CMD_GOOGLE_TV_PROPERTIES_LAZY_NO_RUNNING_APPS, + self.btv._cmd_current_app, + constants.CMD_CURRENT_APP_GOOGLE_TV, ) with patchers.patch_shell(DEVICE_PROPERTIES_OUTPUT_SONY_TV)[self.PATCH_KEY]: diff --git a/tests/test_constants.py b/tests/test_constants.py index afe9dcc2..69af13aa 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -71,54 +71,6 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) - # CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS - self.assertEqual( - constants.CMD_ANDROIDTV_PROPERTIES_LAZY_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11 && ps -A | grep u0_a", - ) - - # CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS - self.assertEqual( - constants.CMD_ANDROIDTV_PROPERTIES_LAZY_NO_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11", - ) - - # CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS - self.assertEqual( - constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11 && ps -A | grep u0_a", - ) - - # CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS - self.assertEqual( - constants.CMD_ANDROIDTV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11", - ) - - # CMD_GOOGLE_TV_PROPERTIES_LAZY_RUNNING_APPS - self.assertEqual( - constants.CMD_GOOGLE_TV_PROPERTIES_LAZY_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11 && ps -A | grep u0_a", - ) - - # CMD_GOOGLE_TV_PROPERTIES_LAZY_NO_RUNNING_APPS - self.assertEqual( - constants.CMD_GOOGLE_TV_PROPERTIES_LAZY_NO_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11", - ) - - # CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_RUNNING_APPS - self.assertEqual( - constants.CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11 && ps -A | grep u0_a", - ) - - # CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS - self.assertEqual( - constants.CMD_GOOGLE_TV_PROPERTIES_NOT_LAZY_NO_RUNNING_APPS, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && (dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')) && dumpsys power | grep Locks | grep 'size=' && CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && (dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {' || echo) && (dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]' || echo) && dumpsys audio | grep '\- STREAM_MUSIC:' -A 11", - ) - def test_current_app_extraction_atv_launcher(self): dumpsys_output = """ mCurrentFocus=Window{e74bb23 u0 com.google.android.tvlauncher/com.google.android.tvlauncher.MainActivity} From ccfa1d95081db2dd414341366ae1ea1f74814464 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 18 Jan 2022 07:49:56 -0800 Subject: [PATCH 11/66] Add `from_base()` methods (#286) --- androidtv/__init__.py | 4 ++-- androidtv/androidtv/androidtv_async.py | 30 ++++++++++++++++++++++++++ androidtv/androidtv/androidtv_sync.py | 30 ++++++++++++++++++++++++++ androidtv/firetv/firetv_async.py | 30 ++++++++++++++++++++++++++ androidtv/firetv/firetv_sync.py | 30 ++++++++++++++++++++++++++ androidtv/setup_async.py | 4 ++-- tests/test_basetv_sync.py | 2 +- 7 files changed, 125 insertions(+), 5 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index f3593764..b00afcd6 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -83,11 +83,11 @@ def setup( # Fire TV if aftv.device_properties.get("manufacturer") == "Amazon": - aftv.__class__ = FireTVSync + aftv = FireTVSync.from_base(aftv) # Android TV else: - aftv.__class__ = AndroidTVSync + aftv = AndroidTVSync.from_base(aftv) # Fill in commands that are specific to the device aftv._fill_in_commands() # pylint: disable=protected-access diff --git a/androidtv/androidtv/androidtv_async.py b/androidtv/androidtv/androidtv_async.py index dfb05943..9838b622 100644 --- a/androidtv/androidtv/androidtv_async.py +++ b/androidtv/androidtv/androidtv_async.py @@ -50,6 +50,36 @@ def __init__( # fill in commands that can vary based on the device BaseAndroidTV._fill_in_commands(self) + @classmethod + def from_base(cls, base_tv): + """Construct an `AndroidTVAsync` object from a `BaseTVAsync` object. + + Parameters + ---------- + base_tv : BaseTVAsync + The object that will be converted to an `AndroidTVAsync` object + + Returns + ------- + atv : AndroidTVAsync + The constructed `AndroidTVAsync` object + + """ + # pylint: disable=protected-access + atv = cls( + base_tv.host, + base_tv.port, + base_tv.adbkey, + base_tv.adb_server_ip, + base_tv.adb_server_port, + base_tv._state_detection_rules, + ) + atv._adb = base_tv._adb + atv.device_properties = base_tv.device_properties + atv.installed_apps = base_tv.installed_apps + atv.max_volume = base_tv.max_volume + return atv + # ======================================================================= # # # # Home Assistant Update # diff --git a/androidtv/androidtv/androidtv_sync.py b/androidtv/androidtv/androidtv_sync.py index 0d795fa6..34fb3a46 100644 --- a/androidtv/androidtv/androidtv_sync.py +++ b/androidtv/androidtv/androidtv_sync.py @@ -50,6 +50,36 @@ def __init__( # fill in commands that can vary based on the device BaseAndroidTV._fill_in_commands(self) + @classmethod + def from_base(cls, base_tv): + """Construct an `AndroidTVSync` object from a `BaseTVSync` object. + + Parameters + ---------- + base_tv : BaseTVSync + The object that will be converted to an `AndroidTVSync` object + + Returns + ------- + atv : AndroidTVSync + The constructed `AndroidTVSync` object + + """ + # pylint: disable=protected-access + atv = cls( + base_tv.host, + base_tv.port, + base_tv.adbkey, + base_tv.adb_server_ip, + base_tv.adb_server_port, + base_tv._state_detection_rules, + ) + atv._adb = base_tv._adb + atv.device_properties = base_tv.device_properties + atv.installed_apps = base_tv.installed_apps + atv.max_volume = base_tv.max_volume + return atv + # ======================================================================= # # # # Home Assistant Update # diff --git a/androidtv/firetv/firetv_async.py b/androidtv/firetv/firetv_async.py index 202eeeba..bde9aa5f 100644 --- a/androidtv/firetv/firetv_async.py +++ b/androidtv/firetv/firetv_async.py @@ -50,6 +50,36 @@ def __init__( # fill in commands that can vary based on the device BaseFireTV._fill_in_commands(self) + @classmethod + def from_base(cls, base_tv): + """Construct a `FireTVAsync` object from a `BaseTVAsync` object. + + Parameters + ---------- + base_tv : BaseTVAsync + The object that will be converted to a `FireTVAsync` object + + Returns + ------- + ftv : FireTVAsync + The constructed `FireTVAsync` object + + """ + # pylint: disable=protected-access + ftv = cls( + base_tv.host, + base_tv.port, + base_tv.adbkey, + base_tv.adb_server_ip, + base_tv.adb_server_port, + base_tv._state_detection_rules, + ) + ftv._adb = base_tv._adb + ftv.device_properties = base_tv.device_properties + ftv.installed_apps = base_tv.installed_apps + ftv.max_volume = base_tv.max_volume + return ftv + # ======================================================================= # # # # Home Assistant Update # diff --git a/androidtv/firetv/firetv_sync.py b/androidtv/firetv/firetv_sync.py index 66cb194b..0435753b 100644 --- a/androidtv/firetv/firetv_sync.py +++ b/androidtv/firetv/firetv_sync.py @@ -50,6 +50,36 @@ def __init__( # fill in commands that can vary based on the device BaseFireTV._fill_in_commands(self) + @classmethod + def from_base(cls, base_tv): + """Construct a `FireTVSync` object from a `BaseTVSync` object. + + Parameters + ---------- + base_tv : BaseTVSync + The object that will be converted to a `FireTVSync` object + + Returns + ------- + ftv : FireTVSync + The constructed `FireTVSync` object + + """ + # pylint: disable=protected-access + ftv = cls( + base_tv.host, + base_tv.port, + base_tv.adbkey, + base_tv.adb_server_ip, + base_tv.adb_server_port, + base_tv._state_detection_rules, + ) + ftv._adb = base_tv._adb + ftv.device_properties = base_tv.device_properties + ftv.installed_apps = base_tv.installed_apps + ftv.max_volume = base_tv.max_volume + return ftv + # ======================================================================= # # # # Home Assistant Update # diff --git a/androidtv/setup_async.py b/androidtv/setup_async.py index 3b458ca4..468ad40b 100644 --- a/androidtv/setup_async.py +++ b/androidtv/setup_async.py @@ -79,11 +79,11 @@ async def setup( # Fire TV if aftv.device_properties.get("manufacturer") == "Amazon": - aftv.__class__ = FireTVAsync + aftv = FireTVAsync.from_base(aftv) # Android TV else: - aftv.__class__ = AndroidTVAsync + aftv = AndroidTVAsync.from_base(aftv) # Fill in commands that are specific to the device aftv._fill_in_commands() # pylint: disable=protected-access diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 4e0b2705..60f79ac1 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -464,7 +464,7 @@ def test_get_device_properties(self): self.assertDictEqual({}, device_properties) with patchers.patch_shell(DEVICE_PROPERTIES_GOOGLE_TV)[self.PATCH_KEY]: - self.btv.__class__ = AndroidTVSync + self.btv = AndroidTVSync.from_base(self.btv) device_properties = self.btv.get_device_properties() self.assertEqual(self.btv.device_properties["manufacturer"], "Google") self.assertEqual( From dfbe15276bb9f84b8d05296a5a57aaa0af7f15da Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 18 Jan 2022 20:11:55 -0800 Subject: [PATCH 12/66] Enable device-specific ADB commands (#285) * Enable device-specific ADB commands * Consolidate running_apps() methods * WIP * WIP * WIP * WIP * Fixes * Cleanup --- androidtv/__init__.py | 10 +-- androidtv/androidtv/androidtv_async.py | 16 ---- androidtv/androidtv/androidtv_sync.py | 16 ---- androidtv/androidtv/base_androidtv.py | 14 +--- androidtv/basetv/basetv.py | 110 ++++++++++++++++++++----- androidtv/basetv/basetv_async.py | 20 ++++- androidtv/basetv/basetv_sync.py | 20 ++++- androidtv/constants.py | 24 +++++- androidtv/firetv/base_firetv.py | 6 +- androidtv/firetv/firetv_async.py | 16 ---- androidtv/firetv/firetv_sync.py | 16 ---- androidtv/setup_async.py | 10 +-- tests/test_basetv_async.py | 1 - tests/test_basetv_sync.py | 13 ++- tests/test_constants.py | 6 -- 15 files changed, 160 insertions(+), 138 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index b00afcd6..0c6dc5f6 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -83,16 +83,10 @@ def setup( # Fire TV if aftv.device_properties.get("manufacturer") == "Amazon": - aftv = FireTVSync.from_base(aftv) + return FireTVSync.from_base(aftv) # Android TV - else: - aftv = AndroidTVSync.from_base(aftv) - - # Fill in commands that are specific to the device - aftv._fill_in_commands() # pylint: disable=protected-access - - return aftv + return AndroidTVSync.from_base(aftv) def ha_state_detection_rules_validator(exc): diff --git a/androidtv/androidtv/androidtv_async.py b/androidtv/androidtv/androidtv_async.py index 9838b622..a2a52862 100644 --- a/androidtv/androidtv/androidtv_async.py +++ b/androidtv/androidtv/androidtv_async.py @@ -47,9 +47,6 @@ def __init__( ): # pylint: disable=super-init-not-called BaseTVAsync.__init__(self, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - # fill in commands that can vary based on the device - BaseAndroidTV._fill_in_commands(self) - @classmethod def from_base(cls, base_tv): """Construct an `AndroidTVAsync` object from a `BaseTVAsync` object. @@ -256,19 +253,6 @@ async def get_properties_dict(self, get_running_apps=True, lazy=True): "hdmi_input": hdmi_input, } - async def running_apps(self): - """Return a list of running user applications. - - Returns - ------- - list - A list of the running apps - - """ - running_apps_response = await self._adb.shell(constants.CMD_RUNNING_APPS_ANDROIDTV) - - return self._running_apps(running_apps_response) - # ======================================================================= # # # # turn on/off methods # diff --git a/androidtv/androidtv/androidtv_sync.py b/androidtv/androidtv/androidtv_sync.py index 34fb3a46..b820825d 100644 --- a/androidtv/androidtv/androidtv_sync.py +++ b/androidtv/androidtv/androidtv_sync.py @@ -47,9 +47,6 @@ def __init__( ): # pylint: disable=super-init-not-called BaseTVSync.__init__(self, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - # fill in commands that can vary based on the device - BaseAndroidTV._fill_in_commands(self) - @classmethod def from_base(cls, base_tv): """Construct an `AndroidTVSync` object from a `BaseTVSync` object. @@ -258,19 +255,6 @@ def get_properties_dict(self, get_running_apps=True, lazy=True): "hdmi_input": hdmi_input, } - def running_apps(self): - """Return a list of running user applications. - - Returns - ------- - list - A list of the running apps - - """ - running_apps_response = self._adb.shell(constants.CMD_RUNNING_APPS_ANDROIDTV) - - return self._running_apps(running_apps_response) - # ======================================================================= # # # # turn on/off methods # diff --git a/androidtv/androidtv/base_androidtv.py b/androidtv/androidtv/base_androidtv.py index 4a5ca846..8aa878ea 100644 --- a/androidtv/androidtv/base_androidtv.py +++ b/androidtv/androidtv/base_androidtv.py @@ -33,23 +33,11 @@ class BaseAndroidTV(BaseTV): # pylint: disable=too-few-public-methods """ DEVICE_CLASS = "androidtv" + DEVICE_ENUM = constants.DeviceEnum.ANDROID_TV def __init__(self, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None): BaseTV.__init__(self, None, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules) - def _fill_in_commands(self): - """Fill in commands that are specific to Android TV devices.""" - # Is this a Google Chromecast Android TV? - if "Google" in self.device_properties.get("manufacturer", "") and "Chromecast" in self.device_properties.get( - "model", "" - ): - self._cmd_current_app = constants.CMD_CURRENT_APP_GOOGLE_TV - self._cmd_launch_app = constants.CMD_LAUNCH_APP_GOOGLE_TV - return - - self._cmd_current_app = constants.CMD_CURRENT_APP - self._cmd_launch_app = constants.CMD_LAUNCH_APP - # ======================================================================= # # # # Home Assistant Update # diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index f5c5a010..dec43453 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -66,6 +66,8 @@ class BaseTV(object): # pylint: disable=too-few-public-methods """ + DEVICE_ENUM = constants.DeviceEnum.BASE_TV + def __init__( self, adb, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None ): @@ -79,14 +81,6 @@ def __init__( self.device_properties = {} self.installed_apps = [] - # commands that can vary based on the device - self._cmd_get_properties_lazy_running_apps = "" - self._cmd_get_properties_lazy_no_running_apps = "" - self._cmd_get_properties_not_lazy_running_apps = "" - self._cmd_get_properties_not_lazy_no_running_apps = "" - self._cmd_current_app = "" - self._cmd_launch_app = "" - # make sure the rules are valid if self._state_detection_rules: for app_id, rules in self._state_detection_rules.items(): @@ -97,12 +91,86 @@ def __init__( # the max volume level (determined when first getting the volume level) self.max_volume = None - def _fill_in_commands(self): - """Fill in commands that are specific to the device. + # ======================================================================= # + # # + # Device-specific ADB commands # + # # + # ======================================================================= # + def _cmd_current_app(self): + """Get the command used to retrieve the current app for this device. + + Returns + ------- + str + The device-specific ADB shell command used to determine the current app + + """ + # Is this a Google Chromecast Android TV? + if ( + self.DEVICE_ENUM == constants.DeviceEnum.ANDROID_TV + and "Google" in self.device_properties.get("manufacturer", "") + and "Chromecast" in self.device_properties.get("model", "") + ): + return constants.CMD_CURRENT_APP_GOOGLE_TV + + return constants.CMD_CURRENT_APP + + def _cmd_current_app_media_session_state(self): + """Get the command used to retrieve the current app and media session state for this device. + + Returns + ------- + str + The device-specific ADB shell command used to determine the current app and media session state + + """ + # Is this a Google Chromecast Android TV? + if ( + self.DEVICE_ENUM == constants.DeviceEnum.ANDROID_TV + and "Google" in self.device_properties.get("manufacturer", "") + and "Chromecast" in self.device_properties.get("model", "") + ): + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV + + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE + + def _cmd_launch_app(self, app): + """Get the command to launch the specified app for this device. + + Parameters + ---------- + app : str + The app that will be launched - This is implemented in the `BaseAndroidTV` and `BaseFireTV` classes. + Returns + ------- + str + The device-specific command to launch the app """ + # Is this a Google Chromecast Android TV? + if ( + self.DEVICE_ENUM == constants.DeviceEnum.ANDROID_TV + and "Google" in self.device_properties.get("manufacturer", "") + and "Chromecast" in self.device_properties.get("model", "") + ): + return constants.CMD_LAUNCH_APP_GOOGLE_TV.format(app) + + return constants.CMD_LAUNCH_APP.format(app) + + def _cmd_running_apps(self): + """Get the command used to retrieve the running apps for this device. + + Returns + ------- + str + The device-specific ADB shell command used to determine the running apps + + """ + if self.DEVICE_ENUM == constants.DeviceEnum.FIRE_TV: + return constants.CMD_RUNNING_APPS_FIRETV + + return constants.CMD_RUNNING_APPS_ANDROIDTV # ======================================================================= # # # @@ -193,8 +261,6 @@ def _parse_device_properties(self, properties): "ethmac": ethmac, } - self._fill_in_commands() - # ======================================================================= # # # # Custom state detection # @@ -349,12 +415,12 @@ def _audio_state(audio_state_response): @staticmethod def _current_app(current_app_response): - """Get the current app from the output of the command :py:const:`androidtv.constants.CMD_CURRENT_APP`. + """Get the current app from the output of the command `androidtv.basetv.basetv.BaseTV._cmd_current_app`. Parameters ---------- current_app_response : str, None - The output from the ADB command :py:const:`androidtv.constants.CMD_CURRENT_APP` + The output from the ADB command `androidtv.basetv.basetv.BaseTV._cmd_current_app` Returns ------- @@ -367,13 +433,13 @@ def _current_app(current_app_response): return current_app_response - def _current_app_media_session_state(self, media_session_state_response): - """Get the current app and the media session state properties from the output of :py:const:`androidtv.constants.CMD_MEDIA_SESSION_STATE_FULL`. + def _current_app_media_session_state(self, current_app_media_session_state_response): + """Get the current app and the media session state properties from the output of `androidtv.basetv.basetv.BaseTV._cmd_current_app_media_session_state`. Parameters ---------- - media_session_state_response : str, None - The output of :py:const:`androidtv.constants.CMD_MEDIA_SESSION_STATE_FULL` + current_app_media_session_state_response : str, None + The output of `androidtv.basetv.basetv.BaseTV._cmd_current_app_media_session_state` Returns ------- @@ -383,15 +449,15 @@ def _current_app_media_session_state(self, media_session_state_response): The state from the output of the ADB shell command, or ``None`` if it could not be determined """ - if not media_session_state_response: + if not current_app_media_session_state_response: return None, None - lines = media_session_state_response.splitlines() + lines = current_app_media_session_state_response.splitlines() current_app = self._current_app(lines[0].strip()) if len(lines) > 1: - matches = constants.REGEX_MEDIA_SESSION_STATE.search(media_session_state_response) + matches = constants.REGEX_MEDIA_SESSION_STATE.search(current_app_media_session_state_response) if matches: return current_app, int(matches.group("state")) diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 5085ff78..84b12643 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -267,7 +267,7 @@ async def current_app(self): The ID of the current app, or ``None`` if it could not be determined """ - current_app_response = await self._adb.shell(self._cmd_current_app) + current_app_response = await self._adb.shell(self._cmd_current_app()) return self._current_app(current_app_response) @@ -282,8 +282,7 @@ async def current_app_media_session_state(self): The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined """ - # This needs to use different commands depending on the device - media_session_state_response = await self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) + media_session_state_response = await self._adb.shell(self._cmd_current_app_media_session_state()) return self._current_app_media_session_state(media_session_state_response) @@ -337,6 +336,19 @@ async def media_session_state(self): return media_session_state + async def running_apps(self): + """Return a list of running user applications. + + Returns + ------- + list + A list of the running apps + + """ + running_apps_response = await self._adb.shell(self._cmd_running_apps()) + + return self._running_apps(running_apps_response) + async def screen_on(self): """Check if the screen is on. @@ -498,7 +510,7 @@ async def launch_app(self, app): The ID of the app that will be launched """ - await self._adb.shell(self._cmd_launch_app.format(app)) + await self._adb.shell(self._cmd_launch_app(app)) async def stop_app(self, app): """Stop an app. diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index ac17d717..3497398d 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -267,7 +267,7 @@ def current_app(self): The ID of the current app, or ``None`` if it could not be determined """ - current_app_response = self._adb.shell(self._cmd_current_app) + current_app_response = self._adb.shell(self._cmd_current_app()) return self._current_app(current_app_response) @@ -282,8 +282,7 @@ def current_app_media_session_state(self): The state from the output of the ADB shell command ``dumpsys media_session``, or ``None`` if it could not be determined """ - # This needs to use different commands depending on the device - media_session_state_response = self._adb.shell(constants.CMD_MEDIA_SESSION_STATE_FULL) + media_session_state_response = self._adb.shell(self._cmd_current_app_media_session_state()) return self._current_app_media_session_state(media_session_state_response) @@ -337,6 +336,19 @@ def media_session_state(self): return media_session_state + def running_apps(self): + """Return a list of running user applications. + + Returns + ------- + list + A list of the running apps + + """ + running_apps_response = self._adb.shell(self._cmd_running_apps()) + + return self._running_apps(running_apps_response) + def screen_on(self): """Check if the screen is on. @@ -498,7 +510,7 @@ def launch_app(self, app): The ID of the app that will be launched """ - self._adb.shell(self._cmd_launch_app.format(app)) + self._adb.shell(self._cmd_launch_app(app)) def stop_app(self, app): """Stop an app. diff --git a/androidtv/constants.py b/androidtv/constants.py index 2139448c..22236d5f 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -9,6 +9,25 @@ import re +import sys + +if sys.version_info[0] == 3 and sys.version_info[1] >= 5: + from enum import IntEnum, unique +else: # pragma: no cover + IntEnum = object + + def unique(cls): + """A class decorator that does nothing.""" + return cls + + +@unique +class DeviceEnum(IntEnum): + """An enum for the various device types.""" + + BASE_TV = 0 + ANDROID_TV = 1 + FIRE_TV = 2 # Intents @@ -71,7 +90,10 @@ CMD_MEDIA_SESSION_STATE = "dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'" #: Determine the current app and get the state from ``dumpsys media_session`` -CMD_MEDIA_SESSION_STATE_FULL = CMD_CURRENT_APP + " && " + CMD_MEDIA_SESSION_STATE +CMD_CURRENT_APP_MEDIA_SESSION_STATE = CMD_CURRENT_APP + " && " + CMD_MEDIA_SESSION_STATE + +#: Determine the current app and get the state from ``dumpsys media_session`` for a Google TV device +CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV = CMD_CURRENT_APP_GOOGLE_TV + " && " + CMD_MEDIA_SESSION_STATE #: Get the running apps for an Android TV device CMD_RUNNING_APPS_ANDROIDTV = "ps -A | grep u0_a" diff --git a/androidtv/firetv/base_firetv.py b/androidtv/firetv/base_firetv.py index 6a641795..409c2be7 100644 --- a/androidtv/firetv/base_firetv.py +++ b/androidtv/firetv/base_firetv.py @@ -33,15 +33,11 @@ class BaseFireTV(BaseTV): # pylint: disable=too-few-public-methods """ DEVICE_CLASS = "firetv" + DEVICE_ENUM = constants.DeviceEnum.FIRE_TV def __init__(self, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None): BaseTV.__init__(self, None, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules) - def _fill_in_commands(self): - """Fill in commands that are specific to Fire TV devices.""" - self._cmd_current_app = constants.CMD_CURRENT_APP - self._cmd_launch_app = constants.CMD_LAUNCH_APP - # ======================================================================= # # # # Home Assistant Update # diff --git a/androidtv/firetv/firetv_async.py b/androidtv/firetv/firetv_async.py index bde9aa5f..bee184bb 100644 --- a/androidtv/firetv/firetv_async.py +++ b/androidtv/firetv/firetv_async.py @@ -47,9 +47,6 @@ def __init__( ): # pylint: disable=super-init-not-called BaseTVAsync.__init__(self, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - # fill in commands that can vary based on the device - BaseFireTV._fill_in_commands(self) - @classmethod def from_base(cls, base_tv): """Construct a `FireTVAsync` object from a `BaseTVAsync` object. @@ -207,19 +204,6 @@ async def get_properties_dict(self, get_running_apps=True, lazy=True): "hdmi_input": hdmi_input, } - async def running_apps(self): - """Return a list of running user applications. - - Returns - ------- - list - A list of the running apps - - """ - running_apps_response = await self._adb.shell(constants.CMD_RUNNING_APPS_FIRETV) - - return self._running_apps(running_apps_response) - # ======================================================================= # # # # turn on/off methods # diff --git a/androidtv/firetv/firetv_sync.py b/androidtv/firetv/firetv_sync.py index 0435753b..1ede5816 100644 --- a/androidtv/firetv/firetv_sync.py +++ b/androidtv/firetv/firetv_sync.py @@ -47,9 +47,6 @@ def __init__( ): # pylint: disable=super-init-not-called BaseTVSync.__init__(self, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - # fill in commands that can vary based on the device - BaseFireTV._fill_in_commands(self) - @classmethod def from_base(cls, base_tv): """Construct a `FireTVSync` object from a `BaseTVSync` object. @@ -207,19 +204,6 @@ def get_properties_dict(self, get_running_apps=True, lazy=True): "hdmi_input": hdmi_input, } - def running_apps(self): - """Return a list of running user applications. - - Returns - ------- - list - A list of the running apps - - """ - running_apps_response = self._adb.shell(constants.CMD_RUNNING_APPS_FIRETV) - - return self._running_apps(running_apps_response) - # ======================================================================= # # # # turn on/off methods # diff --git a/androidtv/setup_async.py b/androidtv/setup_async.py index 468ad40b..3f3a1c02 100644 --- a/androidtv/setup_async.py +++ b/androidtv/setup_async.py @@ -79,13 +79,7 @@ async def setup( # Fire TV if aftv.device_properties.get("manufacturer") == "Amazon": - aftv = FireTVAsync.from_base(aftv) + return FireTVAsync.from_base(aftv) # Android TV - else: - aftv = AndroidTVAsync.from_base(aftv) - - # Fill in commands that are specific to the device - aftv._fill_in_commands() # pylint: disable=protected-access - - return aftv + return AndroidTVAsync.from_base(aftv) diff --git a/tests/test_basetv_async.py b/tests/test_basetv_async.py index 36b84ed8..1949d92d 100644 --- a/tests/test_basetv_async.py +++ b/tests/test_basetv_async.py @@ -28,7 +28,6 @@ async def setUp(self): ], async_patchers.patch_shell("")[self.PATCH_KEY]: self.btv = BaseTVAsync("HOST", 5555) await self.btv.adb_connect() - self.assertEqual(self.btv._cmd_launch_app, "") def test_available(self): """Test that the available property works correctly.""" diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 60f79ac1..4554297c 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -134,7 +134,6 @@ def setUp(self): ]: self.btv = BaseTVSync("HOST", 5555) self.btv.adb_connect() - self.assertEqual(self.btv._cmd_launch_app, "") def test_available(self): """Test that the available property works correctly.""" @@ -466,11 +465,21 @@ def test_get_device_properties(self): with patchers.patch_shell(DEVICE_PROPERTIES_GOOGLE_TV)[self.PATCH_KEY]: self.btv = AndroidTVSync.from_base(self.btv) device_properties = self.btv.get_device_properties() + assert "Chromecast" in self.btv.device_properties.get("model", "") + assert self.btv.DEVICE_ENUM == AndroidTVSync.DEVICE_ENUM self.assertEqual(self.btv.device_properties["manufacturer"], "Google") self.assertEqual( - self.btv._cmd_current_app, + self.btv._cmd_current_app(), constants.CMD_CURRENT_APP_GOOGLE_TV, ) + self.assertEqual( + self.btv._cmd_current_app_media_session_state(), + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, + ) + self.assertEqual( + self.btv._cmd_launch_app("TEST"), + constants.CMD_LAUNCH_APP_GOOGLE_TV.format("TEST"), + ) with patchers.patch_shell(DEVICE_PROPERTIES_OUTPUT_SONY_TV)[self.PATCH_KEY]: device_properties = self.btv.get_device_properties() diff --git a/tests/test_constants.py b/tests/test_constants.py index 69af13aa..93feea7e 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -65,12 +65,6 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LAUNCHER --pct-syskeys 0 1; fi", ) - # CMD_MEDIA_SESSION_STATE_FULL - self.assertEqual( - constants.CMD_MEDIA_SESSION_STATE_FULL, - r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", - ) - def test_current_app_extraction_atv_launcher(self): dumpsys_output = """ mCurrentFocus=Window{e74bb23 u0 com.google.android.tvlauncher/com.google.android.tvlauncher.MainActivity} From 3ca71f128b52e819255fe54157264c63c947a4bd Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 18 Jan 2022 20:29:58 -0800 Subject: [PATCH 13/66] Add more constants to test_constants.py (#287) --- tests/generate_test_constants.py | 25 ++++++++++ tests/test_constants.py | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 tests/generate_test_constants.py diff --git a/tests/generate_test_constants.py b/tests/generate_test_constants.py new file mode 100644 index 00000000..a3673036 --- /dev/null +++ b/tests/generate_test_constants.py @@ -0,0 +1,25 @@ +"""Helper script for generating the 'test_constants' test in test_constants.py.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from androidtv import constants + +EXCLUSIONS = { + "CMD_SUCCESS1", + "CMD_SUCCESS1_FAILURE0", + "CMD_PARSE_CURRENT_APP", + "CMD_DEFINE_CURRENT_APP_VARIABLE", + "CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV", + "CMD_LAUNCH_APP_CONDITION", +} + + +if __name__ == "__main__": + for var in sorted(dir(constants)): + if var.startswith("CMD_") and var not in EXCLUSIONS: + print(" # {}".format(var)) + print(' self.assertEqual(constants.{}, r"{}")'.format(var, getattr(constants, var))) + print() diff --git a/tests/test_constants.py b/tests/test_constants.py index 93feea7e..55d1a9b7 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -36,11 +36,22 @@ def test_apps(self): def test_constants(self): """Test ADB shell commands. + The contents of this test can be generated via the script tests/generate_test_constants.py. + This is basically a form of version control for constants. """ self.maxDiff = None + # CMD_AUDIO_STATE + self.assertEqual( + constants.CMD_AUDIO_STATE, + r"dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')", + ) + + # CMD_AWAKE + self.assertEqual(constants.CMD_AWAKE, r"dumpsys power | grep mWakefulness | grep -q Awake") + # CMD_CURRENT_APP self.assertEqual( constants.CMD_CURRENT_APP, @@ -53,6 +64,27 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP", ) + # CMD_CURRENT_APP_MEDIA_SESSION_STATE + self.assertEqual( + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE, + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) + + # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV + self.assertEqual( + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, + r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) + + # CMD_HDMI_INPUT + self.assertEqual( + constants.CMD_HDMI_INPUT, + r"dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]'", + ) + + # CMD_INSTALLED_APPS + self.assertEqual(constants.CMD_INSTALLED_APPS, r"pm list packages") + # CMD_LAUNCH_APP self.assertEqual( constants.CMD_LAUNCH_APP, @@ -65,6 +97,54 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LAUNCHER --pct-syskeys 0 1; fi", ) + # CMD_MAC_ETH0 + self.assertEqual(constants.CMD_MAC_ETH0, r"ip addr show eth0 | grep -m 1 ether") + + # CMD_MAC_WLAN0 + self.assertEqual(constants.CMD_MAC_WLAN0, r"ip addr show wlan0 | grep -m 1 ether") + + # CMD_MANUFACTURER + self.assertEqual(constants.CMD_MANUFACTURER, r"getprop ro.product.manufacturer") + + # CMD_MEDIA_SESSION_STATE + self.assertEqual( + constants.CMD_MEDIA_SESSION_STATE, + r"dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) + + # CMD_MODEL + self.assertEqual(constants.CMD_MODEL, r"getprop ro.product.model") + + # CMD_RUNNING_APPS_ANDROIDTV + self.assertEqual(constants.CMD_RUNNING_APPS_ANDROIDTV, r"ps -A | grep u0_a") + + # CMD_RUNNING_APPS_FIRETV + self.assertEqual(constants.CMD_RUNNING_APPS_FIRETV, r"ps | grep u0_a") + + # CMD_SCREEN_ON + self.assertEqual( + constants.CMD_SCREEN_ON, + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true')", + ) + + # CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE + self.assertEqual( + constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE, + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep Locks | grep 'size='", + ) + + # CMD_SERIALNO + self.assertEqual(constants.CMD_SERIALNO, r"getprop ro.serialno") + + # CMD_STREAM_MUSIC + self.assertEqual(constants.CMD_STREAM_MUSIC, r"dumpsys audio | grep '\- STREAM_MUSIC:' -A 11") + + # CMD_VERSION + self.assertEqual(constants.CMD_VERSION, r"getprop ro.build.version.release") + + # CMD_WAKE_LOCK_SIZE + self.assertEqual(constants.CMD_WAKE_LOCK_SIZE, r"dumpsys power | grep Locks | grep 'size='") + def test_current_app_extraction_atv_launcher(self): dumpsys_output = """ mCurrentFocus=Window{e74bb23 u0 com.google.android.tvlauncher/com.google.android.tvlauncher.MainActivity} From 3d3f0043225acb0ad8d6b4b845c6841cc8e5d3b6 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 19 Jan 2022 18:37:09 -0800 Subject: [PATCH 14/66] Consolidate `turn_on()` and `turn_off()` methods (#288) --- androidtv/androidtv/androidtv_async.py | 14 ------------- androidtv/androidtv/androidtv_sync.py | 14 ------------- androidtv/basetv/basetv.py | 28 ++++++++++++++++++++++++++ androidtv/basetv/basetv_async.py | 13 ++++++++++++ androidtv/basetv/basetv_sync.py | 13 ++++++++++++ androidtv/constants.py | 12 +++++++++++ androidtv/firetv/firetv_async.py | 17 ---------------- androidtv/firetv/firetv_sync.py | 17 ---------------- tests/test_constants.py | 24 ++++++++++++++++++++++ 9 files changed, 90 insertions(+), 62 deletions(-) diff --git a/androidtv/androidtv/androidtv_async.py b/androidtv/androidtv/androidtv_async.py index a2a52862..08fdeb6c 100644 --- a/androidtv/androidtv/androidtv_async.py +++ b/androidtv/androidtv/androidtv_async.py @@ -8,7 +8,6 @@ from .base_androidtv import BaseAndroidTV from ..basetv.basetv_async import BaseTVAsync -from .. import constants _LOGGER = logging.getLogger(__name__) @@ -252,16 +251,3 @@ async def get_properties_dict(self, get_running_apps=True, lazy=True): "running_apps": running_apps, "hdmi_input": hdmi_input, } - - # ======================================================================= # - # # - # turn on/off methods # - # # - # ======================================================================= # - async def turn_on(self): - """Send ``POWER`` action if the device is off.""" - await self._adb.shell(constants.CMD_SCREEN_ON + " || input keyevent {0}".format(constants.KEY_POWER)) - - async def turn_off(self): - """Send ``POWER`` action if the device is not off.""" - await self._adb.shell(constants.CMD_SCREEN_ON + " && input keyevent {0}".format(constants.KEY_POWER)) diff --git a/androidtv/androidtv/androidtv_sync.py b/androidtv/androidtv/androidtv_sync.py index b820825d..b5c6379c 100644 --- a/androidtv/androidtv/androidtv_sync.py +++ b/androidtv/androidtv/androidtv_sync.py @@ -8,7 +8,6 @@ from .base_androidtv import BaseAndroidTV from ..basetv.basetv_sync import BaseTVSync -from .. import constants _LOGGER = logging.getLogger(__name__) @@ -254,16 +253,3 @@ def get_properties_dict(self, get_running_apps=True, lazy=True): "running_apps": running_apps, "hdmi_input": hdmi_input, } - - # ======================================================================= # - # # - # turn on/off methods # - # # - # ======================================================================= # - def turn_on(self): - """Send ``POWER`` action if the device is off.""" - self._adb.shell(constants.CMD_SCREEN_ON + " || input keyevent {0}".format(constants.KEY_POWER)) - - def turn_off(self): - """Send ``POWER`` action if the device is not off.""" - self._adb.shell(constants.CMD_SCREEN_ON + " && input keyevent {0}".format(constants.KEY_POWER)) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index dec43453..5fb28c1a 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -172,6 +172,34 @@ def _cmd_running_apps(self): return constants.CMD_RUNNING_APPS_ANDROIDTV + def _cmd_turn_off(self): + """Get the command used to turn off this device. + + Returns + ------- + str + The device-specific ADB shell command used to turn off the device + + """ + if self.DEVICE_ENUM == constants.DeviceEnum.FIRE_TV: + return constants.CMD_TURN_OFF_FIRETV + + return constants.CMD_TURN_OFF_ANDROIDTV + + def _cmd_turn_on(self): + """Get the command used to turn on this device. + + Returns + ------- + str + The device-specific ADB shell command used to turn on the device + + """ + if self.DEVICE_ENUM == constants.DeviceEnum.FIRE_TV: + return constants.CMD_TURN_ON_FIRETV + + return constants.CMD_TURN_ON_ANDROIDTV + # ======================================================================= # # # # ADB methods # diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 84b12643..bd74f1a8 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -628,6 +628,19 @@ async def media_previous_track(self): """Send media previous action (results in rewind).""" await self._key(constants.KEY_PREVIOUS) + # ======================================================================= # + # # + # "key" methods: turn on/off commands # + # # + # ======================================================================= # + async def turn_on(self): + """Turn on the device.""" + await self._adb.shell(self._cmd_turn_on()) + + async def turn_off(self): + """Turn off the device.""" + await self._adb.shell(self._cmd_turn_off()) + # ======================================================================= # # # # "key" methods: alphanumeric commands # diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 3497398d..92515d3b 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -628,6 +628,19 @@ def media_previous_track(self): """Send media previous action (results in rewind).""" self._key(constants.KEY_PREVIOUS) + # ======================================================================= # + # # + # "key" methods: turn on/off commands # + # # + # ======================================================================= # + def turn_on(self): + """Turn on the device.""" + self._adb.shell(self._cmd_turn_on()) + + def turn_off(self): + """Turn off the device.""" + self._adb.shell(self._cmd_turn_off()) + # ======================================================================= # # # # "key" methods: alphanumeric commands # diff --git a/androidtv/constants.py b/androidtv/constants.py index 22236d5f..e3de2cba 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -112,6 +112,18 @@ class DeviceEnum(IntEnum): #: Get the "STREAM_MUSIC" block from ``dumpsys audio`` CMD_STREAM_MUSIC = r"dumpsys audio | grep '\- STREAM_MUSIC:' -A 11" +#: Turn off an Android TV device (note: `KEY_POWER = 26` is defined below) +CMD_TURN_OFF_ANDROIDTV = CMD_SCREEN_ON + " && input keyevent 26" + +#: Turn off a Fire TV device (note: `KEY_SLEEP = 223` is defined below) +CMD_TURN_OFF_FIRETV = CMD_SCREEN_ON + " && input keyevent 223" + +#: Turn on an Android TV device (note: `KEY_POWER = 26` is defined below) +CMD_TURN_ON_ANDROIDTV = CMD_SCREEN_ON + " || input keyevent 26" + +#: Turn on a Fire TV device (note: `KEY_POWER = 26` and `KEY_HOME = 3` are defined below) +CMD_TURN_ON_FIRETV = CMD_SCREEN_ON + " || (input keyevent 26 && input keyevent 3)" + #: Get the wake lock size CMD_WAKE_LOCK_SIZE = "dumpsys power | grep Locks | grep 'size='" diff --git a/androidtv/firetv/firetv_async.py b/androidtv/firetv/firetv_async.py index bee184bb..3493b07f 100644 --- a/androidtv/firetv/firetv_async.py +++ b/androidtv/firetv/firetv_async.py @@ -8,7 +8,6 @@ from .base_firetv import BaseFireTV from ..basetv.basetv_async import BaseTVAsync -from .. import constants _LOGGER = logging.getLogger(__name__) @@ -203,19 +202,3 @@ async def get_properties_dict(self, get_running_apps=True, lazy=True): "running_apps": running_apps, "hdmi_input": hdmi_input, } - - # ======================================================================= # - # # - # turn on/off methods # - # # - # ======================================================================= # - async def turn_on(self): - """Send ``POWER`` and ``HOME`` actions if the device is off.""" - await self._adb.shell( - constants.CMD_SCREEN_ON - + " || (input keyevent {0} && input keyevent {1})".format(constants.KEY_POWER, constants.KEY_HOME) - ) - - async def turn_off(self): - """Send ``SLEEP`` action if the device is not off.""" - await self._adb.shell(constants.CMD_SCREEN_ON + " && input keyevent {0}".format(constants.KEY_SLEEP)) diff --git a/androidtv/firetv/firetv_sync.py b/androidtv/firetv/firetv_sync.py index 1ede5816..5e9b0a62 100644 --- a/androidtv/firetv/firetv_sync.py +++ b/androidtv/firetv/firetv_sync.py @@ -8,7 +8,6 @@ from .base_firetv import BaseFireTV from ..basetv.basetv_sync import BaseTVSync -from .. import constants _LOGGER = logging.getLogger(__name__) @@ -203,19 +202,3 @@ def get_properties_dict(self, get_running_apps=True, lazy=True): "running_apps": running_apps, "hdmi_input": hdmi_input, } - - # ======================================================================= # - # # - # turn on/off methods # - # # - # ======================================================================= # - def turn_on(self): - """Send ``POWER`` and ``HOME`` actions if the device is off.""" - self._adb.shell( - constants.CMD_SCREEN_ON - + " || (input keyevent {0} && input keyevent {1})".format(constants.KEY_POWER, constants.KEY_HOME) - ) - - def turn_off(self): - """Send ``SLEEP`` action if the device is not off.""" - self._adb.shell(constants.CMD_SCREEN_ON + " && input keyevent {0}".format(constants.KEY_SLEEP)) diff --git a/tests/test_constants.py b/tests/test_constants.py index 55d1a9b7..7e127c50 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -139,6 +139,30 @@ def test_constants(self): # CMD_STREAM_MUSIC self.assertEqual(constants.CMD_STREAM_MUSIC, r"dumpsys audio | grep '\- STREAM_MUSIC:' -A 11") + # CMD_TURN_OFF_ANDROIDTV + self.assertEqual( + constants.CMD_TURN_OFF_ANDROIDTV, + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && input keyevent 26", + ) + + # CMD_TURN_OFF_FIRETV + self.assertEqual( + constants.CMD_TURN_OFF_FIRETV, + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && input keyevent 223", + ) + + # CMD_TURN_ON_ANDROIDTV + self.assertEqual( + constants.CMD_TURN_ON_ANDROIDTV, + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') || input keyevent 26", + ) + + # CMD_TURN_ON_FIRETV + self.assertEqual( + constants.CMD_TURN_ON_FIRETV, + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') || (input keyevent 26 && input keyevent 3)", + ) + # CMD_VERSION self.assertEqual(constants.CMD_VERSION, r"getprop ro.build.version.release") From 1577e22f0137007327595340f94d0428c63791cd Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 19 Jan 2022 20:05:28 -0800 Subject: [PATCH 15/66] Add a check that 'ANDROID_TV', 'BASE_TV', and 'FIRE_TV' do not appear in the code (#289) * Add a check that 'ANDROID_TV', 'BASE_TV', and 'FIRE_TV' do not appear in the code * Skip test with Python 2 --- androidtv/androidtv/base_androidtv.py | 2 +- androidtv/basetv/basetv.py | 14 +++++++------- androidtv/constants.py | 6 +++--- androidtv/firetv/base_firetv.py | 2 +- tests/test_constants.py | 10 ++++++++++ 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/androidtv/androidtv/base_androidtv.py b/androidtv/androidtv/base_androidtv.py index 8aa878ea..1694e76b 100644 --- a/androidtv/androidtv/base_androidtv.py +++ b/androidtv/androidtv/base_androidtv.py @@ -33,7 +33,7 @@ class BaseAndroidTV(BaseTV): # pylint: disable=too-few-public-methods """ DEVICE_CLASS = "androidtv" - DEVICE_ENUM = constants.DeviceEnum.ANDROID_TV + DEVICE_ENUM = constants.DeviceEnum.ANDROIDTV def __init__(self, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None): BaseTV.__init__(self, None, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 5fb28c1a..f94c118d 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -66,7 +66,7 @@ class BaseTV(object): # pylint: disable=too-few-public-methods """ - DEVICE_ENUM = constants.DeviceEnum.BASE_TV + DEVICE_ENUM = constants.DeviceEnum.BASETV def __init__( self, adb, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None @@ -107,7 +107,7 @@ def _cmd_current_app(self): """ # Is this a Google Chromecast Android TV? if ( - self.DEVICE_ENUM == constants.DeviceEnum.ANDROID_TV + self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and "Google" in self.device_properties.get("manufacturer", "") and "Chromecast" in self.device_properties.get("model", "") ): @@ -126,7 +126,7 @@ def _cmd_current_app_media_session_state(self): """ # Is this a Google Chromecast Android TV? if ( - self.DEVICE_ENUM == constants.DeviceEnum.ANDROID_TV + self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and "Google" in self.device_properties.get("manufacturer", "") and "Chromecast" in self.device_properties.get("model", "") ): @@ -150,7 +150,7 @@ def _cmd_launch_app(self, app): """ # Is this a Google Chromecast Android TV? if ( - self.DEVICE_ENUM == constants.DeviceEnum.ANDROID_TV + self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and "Google" in self.device_properties.get("manufacturer", "") and "Chromecast" in self.device_properties.get("model", "") ): @@ -167,7 +167,7 @@ def _cmd_running_apps(self): The device-specific ADB shell command used to determine the running apps """ - if self.DEVICE_ENUM == constants.DeviceEnum.FIRE_TV: + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_RUNNING_APPS_FIRETV return constants.CMD_RUNNING_APPS_ANDROIDTV @@ -181,7 +181,7 @@ def _cmd_turn_off(self): The device-specific ADB shell command used to turn off the device """ - if self.DEVICE_ENUM == constants.DeviceEnum.FIRE_TV: + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_TURN_OFF_FIRETV return constants.CMD_TURN_OFF_ANDROIDTV @@ -195,7 +195,7 @@ def _cmd_turn_on(self): The device-specific ADB shell command used to turn on the device """ - if self.DEVICE_ENUM == constants.DeviceEnum.FIRE_TV: + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_TURN_ON_FIRETV return constants.CMD_TURN_ON_ANDROIDTV diff --git a/androidtv/constants.py b/androidtv/constants.py index e3de2cba..9af9df0e 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -25,9 +25,9 @@ def unique(cls): class DeviceEnum(IntEnum): """An enum for the various device types.""" - BASE_TV = 0 - ANDROID_TV = 1 - FIRE_TV = 2 + BASETV = 0 + ANDROIDTV = 1 + FIRETV = 2 # Intents diff --git a/androidtv/firetv/base_firetv.py b/androidtv/firetv/base_firetv.py index 409c2be7..2b9eb6bf 100644 --- a/androidtv/firetv/base_firetv.py +++ b/androidtv/firetv/base_firetv.py @@ -33,7 +33,7 @@ class BaseFireTV(BaseTV): # pylint: disable=too-few-public-methods """ DEVICE_CLASS = "firetv" - DEVICE_ENUM = constants.DeviceEnum.FIRE_TV + DEVICE_ENUM = constants.DeviceEnum.FIRETV def __init__(self, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None): BaseTV.__init__(self, None, host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules) diff --git a/tests/test_constants.py b/tests/test_constants.py index 7e127c50..d458ea3e 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,5 +1,7 @@ import inspect import os +import shlex +import subprocess import sys import unittest @@ -169,6 +171,14 @@ def test_constants(self): # CMD_WAKE_LOCK_SIZE self.assertEqual(constants.CMD_WAKE_LOCK_SIZE, r"dumpsys power | grep Locks | grep 'size='") + @unittest.skipIf(sys.version_info.major == 2, "Test requires Python 3") + def test_no_underscores(self): + """Test that 'ANDROID_TV', 'BASE_TV', and 'FIRE_TV' do not appear in the code base.""" + cwd = os.path.join(os.path.dirname(__file__), "..") + for underscore_name in ["ANDROID_TV", "BASE_TV", "FIRE_TV"]: + with subprocess.Popen(shlex.split("git grep -l {} -- androidtv/".format(underscore_name)), cwd=cwd) as p: + self.assertEqual(p.wait(), 1) + def test_current_app_extraction_atv_launcher(self): dumpsys_output = """ mCurrentFocus=Window{e74bb23 u0 com.google.android.tvlauncher/com.google.android.tvlauncher.MainActivity} From 963a4e431a93862bc440d525d38390a77dd28663 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 28 Jan 2022 18:55:49 -0800 Subject: [PATCH 16/66] Add more `CMD_*` checks (#290) --- tests/generate_test_constants.py | 21 ++++++--- tests/test_constants.py | 74 ++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/tests/generate_test_constants.py b/tests/generate_test_constants.py index a3673036..f7aff0e1 100644 --- a/tests/generate_test_constants.py +++ b/tests/generate_test_constants.py @@ -17,9 +17,20 @@ } +def get_cmds(): + """Get the ``CMD_*`` constants. + + Returns + ------- + dict[str: str] + A dictionary where the keys are the names of the constants and the keys are their values + + """ + return {var: getattr(constants, var) for var in dir(constants) if var.startswith("CMD_") and var not in EXCLUSIONS} + + if __name__ == "__main__": - for var in sorted(dir(constants)): - if var.startswith("CMD_") and var not in EXCLUSIONS: - print(" # {}".format(var)) - print(' self.assertEqual(constants.{}, r"{}")'.format(var, getattr(constants, var))) - print() + for name, value in sorted(get_cmds().items(), key=lambda x: x[0]): + print(" # {}".format(name)) + print(' self.assertCommand(constants.{}, r"{}")'.format(name, value)) + print() diff --git a/tests/test_constants.py b/tests/test_constants.py index d458ea3e..7bd5c54a 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -9,8 +9,18 @@ from androidtv import constants +from .generate_test_constants import get_cmds + class TestConstants(unittest.TestCase): + def setUp(self): + self._cmds = [] + self._cmd_names = {} + + def assertCommand(self, value, expected_value): + self.assertEqual(value, expected_value) + self._cmds.append(self._cmd_names[value]) + @staticmethod def _exec(command): process = os.popen(command) @@ -45,131 +55,141 @@ def test_constants(self): """ self.maxDiff = None + # Generate a dictionary where the keys are the `CMD_*` strings and the values are their variable names + cmds = get_cmds() + self._cmd_names = {val: key for key, val in cmds.items()} + + # Check that each `CMD_*` is unique + self.assertEqual(len(cmds), len(self._cmd_names)) + # CMD_AUDIO_STATE - self.assertEqual( + self.assertCommand( constants.CMD_AUDIO_STATE, r"dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')", ) # CMD_AWAKE - self.assertEqual(constants.CMD_AWAKE, r"dumpsys power | grep mWakefulness | grep -q Awake") + self.assertCommand(constants.CMD_AWAKE, r"dumpsys power | grep mWakefulness | grep -q Awake") # CMD_CURRENT_APP - self.assertEqual( + self.assertCommand( constants.CMD_CURRENT_APP, r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP", ) # CMD_CURRENT_APP_GOOGLE_TV - self.assertEqual( + self.assertCommand( constants.CMD_CURRENT_APP_GOOGLE_TV, r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP", ) # CMD_CURRENT_APP_MEDIA_SESSION_STATE - self.assertEqual( + self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE, r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV - self.assertEqual( + self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) # CMD_HDMI_INPUT - self.assertEqual( + self.assertCommand( constants.CMD_HDMI_INPUT, r"dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]'", ) # CMD_INSTALLED_APPS - self.assertEqual(constants.CMD_INSTALLED_APPS, r"pm list packages") + self.assertCommand(constants.CMD_INSTALLED_APPS, r"pm list packages") # CMD_LAUNCH_APP - self.assertEqual( + self.assertCommand( constants.CMD_LAUNCH_APP, r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_LAUNCH_APP_GOOGLE_TV - self.assertEqual( + self.assertCommand( constants.CMD_LAUNCH_APP_GOOGLE_TV, r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_MAC_ETH0 - self.assertEqual(constants.CMD_MAC_ETH0, r"ip addr show eth0 | grep -m 1 ether") + self.assertCommand(constants.CMD_MAC_ETH0, r"ip addr show eth0 | grep -m 1 ether") # CMD_MAC_WLAN0 - self.assertEqual(constants.CMD_MAC_WLAN0, r"ip addr show wlan0 | grep -m 1 ether") + self.assertCommand(constants.CMD_MAC_WLAN0, r"ip addr show wlan0 | grep -m 1 ether") # CMD_MANUFACTURER - self.assertEqual(constants.CMD_MANUFACTURER, r"getprop ro.product.manufacturer") + self.assertCommand(constants.CMD_MANUFACTURER, r"getprop ro.product.manufacturer") # CMD_MEDIA_SESSION_STATE - self.assertEqual( + self.assertCommand( constants.CMD_MEDIA_SESSION_STATE, r"dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) # CMD_MODEL - self.assertEqual(constants.CMD_MODEL, r"getprop ro.product.model") + self.assertCommand(constants.CMD_MODEL, r"getprop ro.product.model") # CMD_RUNNING_APPS_ANDROIDTV - self.assertEqual(constants.CMD_RUNNING_APPS_ANDROIDTV, r"ps -A | grep u0_a") + self.assertCommand(constants.CMD_RUNNING_APPS_ANDROIDTV, r"ps -A | grep u0_a") # CMD_RUNNING_APPS_FIRETV - self.assertEqual(constants.CMD_RUNNING_APPS_FIRETV, r"ps | grep u0_a") + self.assertCommand(constants.CMD_RUNNING_APPS_FIRETV, r"ps | grep u0_a") # CMD_SCREEN_ON - self.assertEqual( + self.assertCommand( constants.CMD_SCREEN_ON, r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true')", ) # CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE - self.assertEqual( + self.assertCommand( constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE, r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep Locks | grep 'size='", ) # CMD_SERIALNO - self.assertEqual(constants.CMD_SERIALNO, r"getprop ro.serialno") + self.assertCommand(constants.CMD_SERIALNO, r"getprop ro.serialno") # CMD_STREAM_MUSIC - self.assertEqual(constants.CMD_STREAM_MUSIC, r"dumpsys audio | grep '\- STREAM_MUSIC:' -A 11") + self.assertCommand(constants.CMD_STREAM_MUSIC, r"dumpsys audio | grep '\- STREAM_MUSIC:' -A 11") # CMD_TURN_OFF_ANDROIDTV - self.assertEqual( + self.assertCommand( constants.CMD_TURN_OFF_ANDROIDTV, r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && input keyevent 26", ) # CMD_TURN_OFF_FIRETV - self.assertEqual( + self.assertCommand( constants.CMD_TURN_OFF_FIRETV, r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && input keyevent 223", ) # CMD_TURN_ON_ANDROIDTV - self.assertEqual( + self.assertCommand( constants.CMD_TURN_ON_ANDROIDTV, r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') || input keyevent 26", ) # CMD_TURN_ON_FIRETV - self.assertEqual( + self.assertCommand( constants.CMD_TURN_ON_FIRETV, r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') || (input keyevent 26 && input keyevent 3)", ) # CMD_VERSION - self.assertEqual(constants.CMD_VERSION, r"getprop ro.build.version.release") + self.assertCommand(constants.CMD_VERSION, r"getprop ro.build.version.release") # CMD_WAKE_LOCK_SIZE - self.assertEqual(constants.CMD_WAKE_LOCK_SIZE, r"dumpsys power | grep Locks | grep 'size='") + self.assertCommand(constants.CMD_WAKE_LOCK_SIZE, r"dumpsys power | grep Locks | grep 'size='") + + # Assert that the keys were checked in alphabetical order + self.assertEqual(self._cmds, sorted(cmds.keys())) @unittest.skipIf(sys.version_info.major == 2, "Test requires Python 3") def test_no_underscores(self): From e6b36495c27298ed5d013f4bbba801f0287a9d3f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 28 Jan 2022 19:15:02 -0800 Subject: [PATCH 17/66] Make repo 'Contributions Only' (#291) --- README.rst | 13 +++++++++++++ docs/source/index.rst | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3fcb5bd8..ec78d3e2 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,19 @@ python-androidtv :target: https://pepy.tech/project/androidtv :alt: Downloads + +Contributions Only +------------------ + +I no longer have the time to actively work on this project, and so all future development will be from pull requests submitted by the community. What I will do is: + +* review pull requests that pass all of the CI checks +* publish new releases upon request + + +About +----- + Documentation for this package can be found at `https://androidtv.readthedocs.io `_. ``androidtv`` is a Python package that provides state information and control of Android TV and Fire TV devices via ADB. This package is used by the `Android TV `_ integration in Home Assistant. diff --git a/docs/source/index.rst b/docs/source/index.rst index 3de5f680..f7c7edd8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,7 +15,7 @@ androidtv Documentation .. include:: ../../README.rst - :start-line: 15 + :start-line: 30 Indices and tables From e2e308cfd0c91ecdde3704d5b5287bec4ed1c856 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 30 Jan 2022 22:31:26 -0800 Subject: [PATCH 18/66] Enable customizing commands (#293) --- androidtv/basetv/basetv.py | 38 ++++++++++++++++++++++++++++++++++++ androidtv/constants.py | 16 +++++++++++++++ tests/test_androidtv_sync.py | 37 +++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index f94c118d..8d081b84 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -91,11 +91,31 @@ def __init__( # the max volume level (determined when first getting the volume level) self.max_volume = None + # Customizable commands + self._custom_commands = {} + # ======================================================================= # # # # Device-specific ADB commands # # # # ======================================================================= # + def customize_command(self, custom_command, value): + """Customize a command used to retrieve properties. + + Parameters + ---------- + custom_command : str + The name of the command that will be customized; it must be in `constants.CUSTOMIZABLE_COMMANDS` + value : str, None + The custom ADB command that will be used, or ``None`` if the custom command should be deleted + + """ + if custom_command in constants.CUSTOMIZABLE_COMMANDS: + if value is not None: + self._custom_commands[custom_command] = value + elif custom_command in self._custom_commands: + del self._custom_commands[custom_command] + def _cmd_current_app(self): """Get the command used to retrieve the current app for this device. @@ -105,6 +125,9 @@ def _cmd_current_app(self): The device-specific ADB shell command used to determine the current app """ + if constants.CUSTOM_CURRENT_APP in self._custom_commands: + return self._custom_commands[constants.CUSTOM_CURRENT_APP] + # Is this a Google Chromecast Android TV? if ( self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV @@ -124,6 +147,9 @@ def _cmd_current_app_media_session_state(self): The device-specific ADB shell command used to determine the current app and media session state """ + if constants.CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE in self._custom_commands: + return self._custom_commands[constants.CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE] + # Is this a Google Chromecast Android TV? if ( self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV @@ -148,6 +174,9 @@ def _cmd_launch_app(self, app): The device-specific command to launch the app """ + if constants.CUSTOM_LAUNCH_APP in self._custom_commands: + return self._custom_commands[constants.CUSTOM_LAUNCH_APP].format(app) + # Is this a Google Chromecast Android TV? if ( self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV @@ -167,6 +196,9 @@ def _cmd_running_apps(self): The device-specific ADB shell command used to determine the running apps """ + if constants.CUSTOM_RUNNING_APPS in self._custom_commands: + return self._custom_commands[constants.CUSTOM_RUNNING_APPS] + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_RUNNING_APPS_FIRETV @@ -181,6 +213,9 @@ def _cmd_turn_off(self): The device-specific ADB shell command used to turn off the device """ + if constants.CUSTOM_TURN_OFF in self._custom_commands: + return self._custom_commands[constants.CUSTOM_TURN_OFF] + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_TURN_OFF_FIRETV @@ -195,6 +230,9 @@ def _cmd_turn_on(self): The device-specific ADB shell command used to turn on the device """ + if constants.CUSTOM_TURN_ON in self._custom_commands: + return self._custom_commands[constants.CUSTOM_TURN_ON] + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_TURN_ON_FIRETV diff --git a/androidtv/constants.py b/androidtv/constants.py index 9af9df0e..db1ad2f6 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -34,6 +34,22 @@ class DeviceEnum(IntEnum): INTENT_LAUNCH = "android.intent.category.LAUNCHER" INTENT_HOME = "android.intent.category.HOME" +# Customizable commands +CUSTOM_CURRENT_APP = "current_app" +CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE = "current_app_media_session_state" +CUSTOM_LAUNCH_APP = "launch_app" +CUSTOM_RUNNING_APPS = "running_apps" +CUSTOM_TURN_OFF = "turn_off" +CUSTOM_TURN_ON = "turn_on" +CUSTOMIZABLE_COMMANDS = { + CUSTOM_CURRENT_APP, + CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, + CUSTOM_LAUNCH_APP, + CUSTOM_RUNNING_APPS, + CUSTOM_TURN_OFF, + CUSTOM_TURN_ON, +} + # echo '1' if the previous shell command was successful CMD_SUCCESS1 = r" && echo -e '1\c'" diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index 3da6d085..d3b39554 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -591,6 +591,43 @@ def test_state_detection(self): (constants.STATE_IDLE, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), ) + def test_customize_command(self): + self.atv.customize_command(constants.CUSTOM_CURRENT_APP, "1") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.current_app() + patched.assert_called_with("1") + + self.atv.customize_command(constants.CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, "2") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.current_app_media_session_state() + patched.assert_called_with("2") + + self.atv.customize_command(constants.CUSTOM_LAUNCH_APP, "this is a {}") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.launch_app("test") + patched.assert_called_with("this is a test") + + self.atv.customize_command(constants.CUSTOM_RUNNING_APPS, "3") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.running_apps() + patched.assert_called_with("3") + + self.atv.customize_command(constants.CUSTOM_TURN_OFF, "4") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.turn_off() + patched.assert_called_with("4") + + self.atv.customize_command(constants.CUSTOM_TURN_ON, "5") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.turn_on() + patched.assert_called_with("5") + + # Delete a custom command + self.atv.customize_command(constants.CUSTOM_TURN_ON, None) + with patch.object(self.atv._adb, "shell") as patched: + self.atv.turn_on() + patched.assert_called_with(constants.CMD_TURN_ON_ANDROIDTV) + class TestAndroidTVSyncServer(TestAndroidTVSyncPython): PATCH_KEY = "server" From 26a4d74cc92021e0448d9076642b6d6c094ba5e1 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 31 Jan 2022 08:33:45 -0800 Subject: [PATCH 19/66] Bump the version to 0.0.61 (#294) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- scripts/bumpversion.sh | 4 ++-- scripts/get_package_name.sh | 4 ++-- scripts/get_version.sh | 6 +++--- setup.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 0c6dc5f6..0d0ab4cf 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.60" +__version__ = "0.0.61" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index dd7d293d..cbcc0e78 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.60' +version = '0.0.61' # The full version, including alpha/beta/rc tags -release = '0.0.60' +release = '0.0.61' # -- General configuration --------------------------------------------------- diff --git a/scripts/bumpversion.sh b/scripts/bumpversion.sh index 7ee2b92b..8b7e2b0b 100755 --- a/scripts/bumpversion.sh +++ b/scripts/bumpversion.sh @@ -25,10 +25,10 @@ VERSION=$($DIR/get_version.sh) echo "Bumping the version from $VERSION to $1" # __init__.py -sed -i "s|__version__ = '$VERSION'|__version__ = '$1'|g" $DIR/../$PACKAGE/__init__.py +sed -i "s|__version__ = \"$VERSION\"|__version__ = \"$1\"|g" $DIR/../$PACKAGE/__init__.py # setup.py -sed -i "s|version='$VERSION',|version='$1',|g" $DIR/../setup.py +sed -i "s|version=\"$VERSION\",|version=\"$1\",|g" $DIR/../setup.py # conf.py sed -i "s|version = '$VERSION'|version = '$1'|g" $DIR/../docs/source/conf.py diff --git a/scripts/get_package_name.sh b/scripts/get_package_name.sh index b81914c9..ced6ccf6 100755 --- a/scripts/get_package_name.sh +++ b/scripts/get_package_name.sh @@ -5,8 +5,8 @@ set -e # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -RSTRIP="'*" -LSTRIP="*'" +RSTRIP="\"*" +LSTRIP="*\"" # get the package name PACKAGE_LINE=$(grep 'name=' $DIR/../setup.py || echo '') diff --git a/scripts/get_version.sh b/scripts/get_version.sh index b17f9606..ae39c7f3 100755 --- a/scripts/get_version.sh +++ b/scripts/get_version.sh @@ -5,15 +5,15 @@ set -e # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -RSTRIP="'*" -LSTRIP="*'" +RSTRIP="\"*" +LSTRIP="*\"" # get the package name PACKAGE=$($DIR/get_package_name.sh) # get the current version VERSION_LINE=$(grep '__version__' "$DIR/../$PACKAGE/__init__.py" || echo '') -VERSION_TEMP=${VERSION_LINE%"'"} +VERSION_TEMP=${VERSION_LINE%"\""} VERSION=${VERSION_TEMP##$LSTRIP} diff --git a/setup.py b/setup.py index 3727e975..ddc27bb6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.60", + version="0.0.61", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 4ecfe948b2d25d4e36366dfe54d4fd8902694353 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Feb 2022 08:43:59 -0800 Subject: [PATCH 20/66] Add `HA_CUSTOMIZABLE_COMMANDS` constant (#296) --- androidtv/constants.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/androidtv/constants.py b/androidtv/constants.py index db1ad2f6..acaa5f59 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -50,6 +50,14 @@ class DeviceEnum(IntEnum): CUSTOM_TURN_ON, } +#: The subset of `CUSTOMIZABLE_COMMANDS` that is potentially used in the ``update()`` method +HA_CUSTOMIZABLE_COMMANDS = ( + CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, + CUSTOM_LAUNCH_APP, + CUSTOM_RUNNING_APPS, + CUSTOM_TURN_OFF, + CUSTOM_TURN_ON, +) # echo '1' if the previous shell command was successful CMD_SUCCESS1 = r" && echo -e '1\c'" From 8003fbc1e8561da3812dbf884a35997275624d14 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Feb 2022 08:48:25 -0800 Subject: [PATCH 21/66] Bump the version to 0.0.62 (#297) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 0d0ab4cf..4b4d71f0 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.61" +__version__ = "0.0.62" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index cbcc0e78..9225fa57 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.61' +version = '0.0.62' # The full version, including alpha/beta/rc tags -release = '0.0.61' +release = '0.0.62' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index ddc27bb6..d2b76f62 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.61", + version="0.0.62", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 7d496ced19ffe630962c4b210113db4b6b567fcc Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Feb 2022 09:08:54 -0800 Subject: [PATCH 22/66] Create issue template (#298) --- .github/ISSUE_TEMPLATE/bug_report.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..e2aa1030 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,17 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + From be393dc93d76c7c94d10a55239bac1a2736846de Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 3 Feb 2022 12:39:16 -0800 Subject: [PATCH 23/66] Add `CMD_DEVICE_PROPERTIES` constant (#299) --- androidtv/basetv/basetv_async.py | 14 +------------- androidtv/basetv/basetv_sync.py | 14 +------------- androidtv/constants.py | 15 +++++++++++++++ tests/test_constants.py | 6 ++++++ 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index bd74f1a8..56ed0732 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -200,19 +200,7 @@ async def get_device_properties(self): A dictionary with keys ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` """ - properties = await self._adb.shell( - constants.CMD_MANUFACTURER - + " && " - + constants.CMD_MODEL - + " && " - + constants.CMD_SERIALNO - + " && " - + constants.CMD_VERSION - + " && " - + constants.CMD_MAC_WLAN0 - + " && " - + constants.CMD_MAC_ETH0 - ) + properties = await self._adb.shell(constants.CMD_DEVICE_PROPERTIES) self._parse_device_properties(properties) return self.device_properties diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 92515d3b..628eedfc 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -200,19 +200,7 @@ def get_device_properties(self): A dictionary with keys ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` """ - properties = self._adb.shell( - constants.CMD_MANUFACTURER - + " && " - + constants.CMD_MODEL - + " && " - + constants.CMD_SERIALNO - + " && " - + constants.CMD_VERSION - + " && " - + constants.CMD_MAC_WLAN0 - + " && " - + constants.CMD_MAC_ETH0 - ) + properties = self._adb.shell(constants.CMD_DEVICE_PROPERTIES) self._parse_device_properties(properties) return self.device_properties diff --git a/androidtv/constants.py b/androidtv/constants.py index acaa5f59..691fd6df 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -166,6 +166,21 @@ class DeviceEnum(IntEnum): CMD_MAC_WLAN0 = "ip addr show wlan0 | grep -m 1 ether" CMD_MAC_ETH0 = "ip addr show eth0 | grep -m 1 ether" +#: The command used for getting the device properties +CMD_DEVICE_PROPERTIES = ( + CMD_MANUFACTURER + + " && " + + CMD_MODEL + + " && " + + CMD_SERIALNO + + " && " + + CMD_VERSION + + " && " + + CMD_MAC_WLAN0 + + " && " + + CMD_MAC_ETH0 +) + # ADB key event codes # https://developer.android.com/reference/android/view/KeyEvent diff --git a/tests/test_constants.py b/tests/test_constants.py index 7bd5c54a..9ace7d93 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -95,6 +95,12 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) + # CMD_DEVICE_PROPERTIES + self.assertCommand( + constants.CMD_DEVICE_PROPERTIES, + r"getprop ro.product.manufacturer && getprop ro.product.model && getprop ro.serialno && getprop ro.build.version.release && ip addr show wlan0 | grep -m 1 ether && ip addr show eth0 | grep -m 1 ether", + ) + # CMD_HDMI_INPUT self.assertCommand( constants.CMD_HDMI_INPUT, From cfd9b0d2ba919d77c493a094db84479a749ba8de Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 3 Feb 2022 19:41:35 -0800 Subject: [PATCH 24/66] Fix the MAC address properties (#300) * Fix the `CMD_MAC_*` commands * Fix * Get the MAC addresses in separate ADB commands --- androidtv/basetv/basetv.py | 44 ++++++++++++-------- androidtv/basetv/basetv_async.py | 6 +++ androidtv/basetv/basetv_sync.py | 6 +++ androidtv/constants.py | 14 +------ tests/test_basetv_sync.py | 69 +++++++++++++++++++------------- tests/test_constants.py | 2 +- tests/test_setup_async.py | 8 ++-- tests/test_setup_sync.py | 8 ++-- 8 files changed, 90 insertions(+), 67 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 8d081b84..c6cc2372 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -286,7 +286,7 @@ def _parse_device_properties(self, properties): The output of the ADB command that retrieves the device properties This method fills in the ``device_properties`` attribute, which is a dictionary with keys - ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` + ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` """ _LOGGER.debug("%s:%d `get_device_properties` response: %s", self.host, self.port, properties) @@ -296,37 +296,47 @@ def _parse_device_properties(self, properties): return lines = properties.strip().splitlines() - if len(lines) != 6: + if len(lines) != 4: self.device_properties = {} return - manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output = lines + manufacturer, model, serialno, version = lines if not serialno.strip(): _LOGGER.warning("Could not obtain serialno for %s:%d, got: '%s'", self.host, self.port, serialno) serialno = None - mac_wlan0_matches = re.findall(constants.MAC_REGEX_PATTERN, mac_wlan0_output) - if mac_wlan0_matches: - wifimac = mac_wlan0_matches[0] - else: - wifimac = None - - mac_eth0_matches = re.findall(constants.MAC_REGEX_PATTERN, mac_eth0_output) - if mac_eth0_matches: - ethmac = mac_eth0_matches[0] - else: - ethmac = None - self.device_properties = { "manufacturer": manufacturer, "model": model, "serialno": serialno, "sw_version": version, - "wifimac": wifimac, - "ethmac": ethmac, } + @staticmethod + def _parse_mac_address(mac_response): + """Parse a MAC address from the ADB shell response. + + Parameters + ---------- + mac_response : str, None + The response from the MAC address ADB shell command + + Returns + ------- + str, None + The parsed MAC address, or ``None`` if it could not be determined + + """ + if not mac_response: + return None + + mac_matches = re.findall(constants.MAC_REGEX_PATTERN, mac_response) + if mac_matches: + return mac_matches[0] + + return None + # ======================================================================= # # # # Custom state detection # diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 56ed0732..db1abeaf 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -203,6 +203,12 @@ async def get_device_properties(self): properties = await self._adb.shell(constants.CMD_DEVICE_PROPERTIES) self._parse_device_properties(properties) + + ethmac_response = await self._adb.shell(constants.CMD_MAC_ETH0) + wifimac_response = await self._adb.shell(constants.CMD_MAC_WLAN0) + self.device_properties["ethmac"] = self._parse_mac_address(ethmac_response) + self.device_properties["wifimac"] = self._parse_mac_address(wifimac_response) + return self.device_properties # ======================================================================= # diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 628eedfc..a54258fd 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -203,6 +203,12 @@ def get_device_properties(self): properties = self._adb.shell(constants.CMD_DEVICE_PROPERTIES) self._parse_device_properties(properties) + + ethmac_response = self._adb.shell(constants.CMD_MAC_ETH0) + wifimac_response = self._adb.shell(constants.CMD_MAC_WLAN0) + self.device_properties["ethmac"] = self._parse_mac_address(ethmac_response) + self.device_properties["wifimac"] = self._parse_mac_address(wifimac_response) + return self.device_properties # ======================================================================= # diff --git a/androidtv/constants.py b/androidtv/constants.py index 691fd6df..e39a9ba0 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -167,19 +167,7 @@ class DeviceEnum(IntEnum): CMD_MAC_ETH0 = "ip addr show eth0 | grep -m 1 ether" #: The command used for getting the device properties -CMD_DEVICE_PROPERTIES = ( - CMD_MANUFACTURER - + " && " - + CMD_MODEL - + " && " - + CMD_SERIALNO - + " && " - + CMD_VERSION - + " && " - + CMD_MAC_WLAN0 - + " && " - + CMD_MAC_ETH0 -) +CMD_DEVICE_PROPERTIES = CMD_MANUFACTURER + " && " + CMD_MODEL + " && " + CMD_SERIALNO + " && " + CMD_VERSION # ADB key event codes diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 4554297c..1cfa0a25 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -19,10 +19,11 @@ AFTT SERIALNO 5.1.1 - link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff -Device "eth0" does not exist. """ +WIFIMAC_OUTPUT1 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" +ETHMAC_OUTPUT1 = 'Device "eth0" does not exist.' + DEVICE_PROPERTIES_DICT1 = { "manufacturer": "Amazon", "model": "AFTT", @@ -36,8 +37,6 @@ AFTT 5.1.1 - link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff -Device "eth0" does not exist. """ DEVICE_PROPERTIES_DICT2 = { @@ -53,27 +52,40 @@ AFTT SERIALNO 5.1.1 -Device "wlan0" does not exist. - link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff """ +WIFIMAC_OUTPUT3 = 'Device "wlan0" does not exist.' +ETHMAC_OUTPUT3 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" + +DEVICE_PROPERTIES_DICT3 = { + "manufacturer": "Not Amazon", + "model": "AFTT", + "serialno": "SERIALNO", + "sw_version": "5.1.1", + "wifimac": None, + "ethmac": "ab:cd:ef:gh:ij:kl", +} + + # Source: https://community.home-assistant.io/t/new-chromecast-w-android-tv-integration-only-showing-as-off-or-idle/234424/15 DEVICE_PROPERTIES_GOOGLE_TV = """Google Chromecast SERIALNO 10 - link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff -Device "eth0" does not exist. """ +WIFIMAC_GOOGLE = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" +ETHMAC_GOOGLE = 'Device "eth0" does not exist.' + DEVICE_PROPERTIES_OUTPUT_SONY_TV = """Sony BRAVIA 4K GB SERIALNO 8.0.0 - link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff - link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff """ +WIFIMAC_SONY = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" +ETHMAC_SONY = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" + DEVICE_PROPERTIES_DICT_SONY_TV = { "manufacturer": "Sony", "model": "BRAVIA 4K GB", @@ -92,15 +104,6 @@ "org.example.launcher", ] -DEVICE_PROPERTIES_DICT3 = { - "manufacturer": "Not Amazon", - "model": "AFTT", - "serialno": "SERIALNO", - "sw_version": "5.1.1", - "wifimac": None, - "ethmac": "ab:cd:ef:gh:ij:kl", -} - MEDIA_SESSION_STATE_OUTPUT = "com.amazon.tv.launcher\nstate=PlaybackState {state=2, position=0, buffered position=0, speed=0.0, updated=65749, actions=240640, custom actions=[], active item id=-1, error=null}" STATE_DETECTION_RULES1 = {"com.amazon.tv.launcher": ["off"]} @@ -442,27 +445,35 @@ def test_keys(self): def test_get_device_properties(self): """Check that ``get_device_properties`` works correctly.""" - with patchers.patch_shell(DEVICE_PROPERTIES_OUTPUT1)[self.PATCH_KEY]: + with patch.object( + self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT1, ETHMAC_OUTPUT1, WIFIMAC_OUTPUT1) + ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT1, device_properties) - with patchers.patch_shell(DEVICE_PROPERTIES_OUTPUT2)[self.PATCH_KEY]: + with patch.object( + self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT2, ETHMAC_OUTPUT1, WIFIMAC_OUTPUT1) + ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT2, device_properties) - with patchers.patch_shell(DEVICE_PROPERTIES_OUTPUT3)[self.PATCH_KEY]: + with patch.object( + self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT3, ETHMAC_OUTPUT3, WIFIMAC_OUTPUT3) + ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT3, device_properties) - with patchers.patch_shell("manufacturer")[self.PATCH_KEY]: + with patch.object(self.btv._adb, "shell", side_effect=("manufacturer", None, "No match")): device_properties = self.btv.get_device_properties() - self.assertDictEqual({}, device_properties) + self.assertDictEqual({"ethmac": None, "wifimac": None}, device_properties) - with patchers.patch_shell("")[self.PATCH_KEY]: + with patch.object(self.btv._adb, "shell", side_effect=("", None, "No match")): device_properties = self.btv.get_device_properties() - self.assertDictEqual({}, device_properties) + self.assertDictEqual({"ethmac": None, "wifimac": None}, device_properties) - with patchers.patch_shell(DEVICE_PROPERTIES_GOOGLE_TV)[self.PATCH_KEY]: + with patch.object( + self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_GOOGLE_TV, ETHMAC_GOOGLE, WIFIMAC_GOOGLE) + ): self.btv = AndroidTVSync.from_base(self.btv) device_properties = self.btv.get_device_properties() assert "Chromecast" in self.btv.device_properties.get("model", "") @@ -481,7 +492,9 @@ def test_get_device_properties(self): constants.CMD_LAUNCH_APP_GOOGLE_TV.format("TEST"), ) - with patchers.patch_shell(DEVICE_PROPERTIES_OUTPUT_SONY_TV)[self.PATCH_KEY]: + with patch.object( + self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT_SONY_TV, ETHMAC_SONY, WIFIMAC_SONY) + ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT_SONY_TV, device_properties) diff --git a/tests/test_constants.py b/tests/test_constants.py index 9ace7d93..44891e38 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -98,7 +98,7 @@ def test_constants(self): # CMD_DEVICE_PROPERTIES self.assertCommand( constants.CMD_DEVICE_PROPERTIES, - r"getprop ro.product.manufacturer && getprop ro.product.model && getprop ro.serialno && getprop ro.build.version.release && ip addr show wlan0 | grep -m 1 ether && ip addr show eth0 | grep -m 1 ether", + r"getprop ro.product.manufacturer && getprop ro.product.model && getprop ro.serialno && getprop ro.build.version.release", ) # CMD_HDMI_INPUT diff --git a/tests/test_setup_async.py b/tests/test_setup_async.py index eb365708..e4ac1c74 100644 --- a/tests/test_setup_async.py +++ b/tests/test_setup_async.py @@ -13,24 +13,24 @@ from .async_wrapper import awaiter -DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n\n\n." +DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n123" DEVICE_PROPERTIES_DICT1 = { "manufacturer": "Amazon", "model": "", "serialno": None, - "sw_version": "", + "sw_version": "123", "wifimac": None, "ethmac": None, } -DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n\n\n." +DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n456" DEVICE_PROPERTIES_DICT2 = { "manufacturer": "Not Amazon", "model": "", "serialno": None, - "sw_version": "", + "sw_version": "456", "wifimac": None, "ethmac": None, } diff --git a/tests/test_setup_sync.py b/tests/test_setup_sync.py index a57d48b8..8f9af3d9 100644 --- a/tests/test_setup_sync.py +++ b/tests/test_setup_sync.py @@ -17,24 +17,24 @@ from . import patchers -DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n\n\n." +DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n123" DEVICE_PROPERTIES_DICT1 = { "manufacturer": "Amazon", "model": "", "serialno": None, - "sw_version": "", + "sw_version": "123", "wifimac": None, "ethmac": None, } -DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n\n\n." +DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n456" DEVICE_PROPERTIES_DICT2 = { "manufacturer": "Not Amazon", "model": "", "serialno": None, - "sw_version": "", + "sw_version": "456", "wifimac": None, "ethmac": None, } From 8fe056c373f70a15f847661034b7371107a78c52 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 3 Feb 2022 19:48:06 -0800 Subject: [PATCH 25/66] Bump the version to 0.0.63 (#302) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 4b4d71f0..db58fb92 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.62" +__version__ = "0.0.63" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 9225fa57..922078da 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.62' +version = '0.0.63' # The full version, including alpha/beta/rc tags -release = '0.0.62' +release = '0.0.63' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index d2b76f62..42851bda 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.62", + version="0.0.63", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 90245ed5b8c561de6df1c24d27215fe5bc45bd95 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 14 Feb 2022 07:25:52 +0100 Subject: [PATCH 26/66] Add transport_timeout_s parameter for setup and adb_connect methods (#304) * Add connect_transport_timeout param for setup and adb_connect * Black formatting * Remove max limit for timeout --- androidtv/__init__.py | 11 +++++++---- androidtv/adb_manager/adb_manager_async.py | 22 ++++++++++++++++++---- androidtv/adb_manager/adb_manager_sync.py | 22 ++++++++++++++++++---- androidtv/basetv/basetv_async.py | 11 +++++++++-- androidtv/basetv/basetv_sync.py | 11 +++++++++-- androidtv/constants.py | 3 +++ androidtv/setup_async.py | 11 +++++++---- 7 files changed, 71 insertions(+), 20 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index db58fb92..48068e75 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -6,7 +6,7 @@ from .androidtv.androidtv_sync import AndroidTVSync from .basetv.basetv import state_detection_rules_validator from .basetv.basetv_sync import BaseTVSync -from .constants import DEFAULT_AUTH_TIMEOUT_S +from .constants import DEFAULT_AUTH_TIMEOUT_S, DEFAULT_TRANSPORT_TIMEOUT_S from .firetv.firetv_sync import FireTVSync @@ -23,6 +23,7 @@ def setup( device_class="auto", auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, signer=None, + transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, ): """Connect to a device and determine whether it's an Android TV or an Amazon Fire TV. @@ -46,6 +47,8 @@ def setup( Authentication timeout (in seconds) signer : PythonRSASigner, None The signer for the ADB keys, as loaded by :meth:`androidtv.adb_manager.adb_manager_sync.ADBPythonSync.load_adbkey` + transport_timeout_s : float + Transport timeout (in seconds) Returns ------- @@ -55,14 +58,14 @@ def setup( """ if device_class == "androidtv": atv = AndroidTVSync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - atv.adb_connect(auth_timeout_s=auth_timeout_s) + atv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) atv.get_device_properties() atv.get_installed_apps() return atv if device_class == "firetv": ftv = FireTVSync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - ftv.adb_connect(auth_timeout_s=auth_timeout_s) + ftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) ftv.get_device_properties() ftv.get_installed_apps() return ftv @@ -73,7 +76,7 @@ def setup( aftv = BaseTVSync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) # establish the ADB connection - aftv.adb_connect(auth_timeout_s=auth_timeout_s) + aftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) # get device properties aftv.device_properties = aftv.get_device_properties() diff --git a/androidtv/adb_manager/adb_manager_async.py b/androidtv/adb_manager/adb_manager_async.py index 50b71910..4996de7f 100644 --- a/androidtv/adb_manager/adb_manager_async.py +++ b/androidtv/adb_manager/adb_manager_async.py @@ -17,7 +17,12 @@ import aiofiles from ppadb.client import Client -from ..constants import DEFAULT_ADB_TIMEOUT_S, DEFAULT_AUTH_TIMEOUT_S, DEFAULT_LOCK_TIMEOUT_S +from ..constants import ( + DEFAULT_ADB_TIMEOUT_S, + DEFAULT_AUTH_TIMEOUT_S, + DEFAULT_LOCK_TIMEOUT_S, + DEFAULT_TRANSPORT_TIMEOUT_S, +) from ..exceptions import LockNotAcquiredException _LOGGER = logging.getLogger(__name__) @@ -224,7 +229,12 @@ async def close(self): """Close the ADB socket connection.""" await self._adb.close() - async def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S): + async def connect( + self, + always_log_errors=True, + auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, + transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, + ): """Connect to an Android TV / Fire TV device. Parameters @@ -233,6 +243,8 @@ async def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIME If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt auth_timeout_s : float Authentication timeout (in seconds) + transport_timeout_s : float + Transport timeout (in seconds) Returns ------- @@ -250,12 +262,14 @@ async def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIME self._signer = await self.load_adbkey(self.adbkey) await self._adb.connect( - rsa_keys=[self._signer], transport_timeout_s=1.0, auth_timeout_s=auth_timeout_s + rsa_keys=[self._signer], + transport_timeout_s=transport_timeout_s, + auth_timeout_s=auth_timeout_s, ) # Connect without authentication else: - await self._adb.connect(transport_timeout_s=1.0, auth_timeout_s=auth_timeout_s) + await self._adb.connect(transport_timeout_s=transport_timeout_s, auth_timeout_s=auth_timeout_s) # ADB connection successfully established _LOGGER.debug("ADB connection to %s:%d successfully established", self.host, self.port) diff --git a/androidtv/adb_manager/adb_manager_sync.py b/androidtv/adb_manager/adb_manager_sync.py index b075f9ce..e1a5d8c3 100644 --- a/androidtv/adb_manager/adb_manager_sync.py +++ b/androidtv/adb_manager/adb_manager_sync.py @@ -15,7 +15,12 @@ from adb_shell.auth.sign_pythonrsa import PythonRSASigner from ppadb.client import Client -from ..constants import DEFAULT_ADB_TIMEOUT_S, DEFAULT_AUTH_TIMEOUT_S, DEFAULT_LOCK_TIMEOUT_S +from ..constants import ( + DEFAULT_ADB_TIMEOUT_S, + DEFAULT_AUTH_TIMEOUT_S, + DEFAULT_LOCK_TIMEOUT_S, + DEFAULT_TRANSPORT_TIMEOUT_S, +) from ..exceptions import LockNotAcquiredException _LOGGER = logging.getLogger(__name__) @@ -108,7 +113,12 @@ def close(self): """Close the ADB socket connection.""" self._adb.close() - def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S): + def connect( + self, + always_log_errors=True, + auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, + transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, + ): """Connect to an Android TV / Fire TV device. Parameters @@ -117,6 +127,8 @@ def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S) If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt auth_timeout_s : float Authentication timeout (in seconds) + transport_timeout_s : float + Transport timeout (in seconds) Returns ------- @@ -134,12 +146,14 @@ def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S) self._signer = self.load_adbkey(self.adbkey) self._adb.connect( - rsa_keys=[self._signer], transport_timeout_s=1.0, auth_timeout_s=auth_timeout_s + rsa_keys=[self._signer], + transport_timeout_s=transport_timeout_s, + auth_timeout_s=auth_timeout_s, ) # Connect without authentication else: - self._adb.connect(transport_timeout_s=1.0, auth_timeout_s=auth_timeout_s) + self._adb.connect(transport_timeout_s=transport_timeout_s, auth_timeout_s=auth_timeout_s) # ADB connection successfully established _LOGGER.debug("ADB connection to %s:%d successfully established", self.host, self.port) diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index db1abeaf..3073e74e 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -157,7 +157,12 @@ async def adb_screencap(self): """ return await self._adb.screencap() - async def adb_connect(self, always_log_errors=True, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S): + async def adb_connect( + self, + always_log_errors=True, + auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, + transport_timeout_s=constants.DEFAULT_TRANSPORT_TIMEOUT_S, + ): """Connect to an Android TV / Fire TV device. Parameters @@ -166,6 +171,8 @@ async def adb_connect(self, always_log_errors=True, auth_timeout_s=constants.DEF If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt auth_timeout_s : float Authentication timeout (in seconds) + transport_timeout_s : float + Transport timeout (in seconds) Returns ------- @@ -174,7 +181,7 @@ async def adb_connect(self, always_log_errors=True, auth_timeout_s=constants.DEF """ if isinstance(self._adb, ADBPythonAsync): - return await self._adb.connect(always_log_errors, auth_timeout_s) + return await self._adb.connect(always_log_errors, auth_timeout_s, transport_timeout_s) return await self._adb.connect(always_log_errors) async def adb_close(self): diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index a54258fd..ec455d40 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -157,7 +157,12 @@ def adb_screencap(self): """ return self._adb.screencap() - def adb_connect(self, always_log_errors=True, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S): + def adb_connect( + self, + always_log_errors=True, + auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, + transport_timeout_s=constants.DEFAULT_TRANSPORT_TIMEOUT_S, + ): """Connect to an Android TV / Fire TV device. Parameters @@ -166,6 +171,8 @@ def adb_connect(self, always_log_errors=True, auth_timeout_s=constants.DEFAULT_A If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt auth_timeout_s : float Authentication timeout (in seconds) + transport_timeout_s : float + Transport timeout (in seconds) Returns ------- @@ -174,7 +181,7 @@ def adb_connect(self, always_log_errors=True, auth_timeout_s=constants.DEFAULT_A """ if isinstance(self._adb, ADBPythonSync): - return self._adb.connect(always_log_errors, auth_timeout_s) + return self._adb.connect(always_log_errors, auth_timeout_s, transport_timeout_s) return self._adb.connect(always_log_errors) def adb_close(self): diff --git a/androidtv/constants.py b/androidtv/constants.py index e39a9ba0..cbeca3fb 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -564,6 +564,9 @@ class DeviceEnum(IntEnum): #: Default authentication timeout (in s) for :meth:`adb_shell.handle.tcp_handle.TcpHandle.connect` and :meth:`adb_shell.handle.tcp_handle_async.TcpHandleAsync.connect` DEFAULT_AUTH_TIMEOUT_S = 10.0 +#: Default transport timeout (in s) for :meth:`adb_shell.handle.tcp_handle.TcpHandle.connect` and :meth:`adb_shell.handle.tcp_handle_async.TcpHandleAsync.connect` +DEFAULT_TRANSPORT_TIMEOUT_S = 1.0 + #: Default timeout (in s) for :class:`adb_shell.handle.tcp_handle.TcpHandle` and :class:`adb_shell.handle.tcp_handle_async.TcpHandleAsync` DEFAULT_ADB_TIMEOUT_S = 9.0 diff --git a/androidtv/setup_async.py b/androidtv/setup_async.py index 3f3a1c02..9fb53c2d 100644 --- a/androidtv/setup_async.py +++ b/androidtv/setup_async.py @@ -5,7 +5,7 @@ from .androidtv.androidtv_async import AndroidTVAsync from .basetv.basetv_async import BaseTVAsync -from .constants import DEFAULT_AUTH_TIMEOUT_S +from .constants import DEFAULT_AUTH_TIMEOUT_S, DEFAULT_TRANSPORT_TIMEOUT_S from .firetv.firetv_async import FireTVAsync @@ -19,6 +19,7 @@ async def setup( device_class="auto", auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, signer=None, + transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, ): """Connect to a device and determine whether it's an Android TV or an Amazon Fire TV. @@ -42,6 +43,8 @@ async def setup( Authentication timeout (in seconds) signer : PythonRSASigner, None The signer for the ADB keys, as loaded by :meth:`androidtv.adb_manager.adb_manager_async.ADBPythonAsync.load_adbkey` + transport_timeout_s : float + Transport timeout (in seconds) Returns ------- @@ -51,14 +54,14 @@ async def setup( """ if device_class == "androidtv": atv = AndroidTVAsync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - await atv.adb_connect(auth_timeout_s=auth_timeout_s) + await atv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) await atv.get_device_properties() await atv.get_installed_apps() return atv if device_class == "firetv": ftv = FireTVAsync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - await ftv.adb_connect(auth_timeout_s=auth_timeout_s) + await ftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) await ftv.get_device_properties() await ftv.get_installed_apps() return ftv @@ -69,7 +72,7 @@ async def setup( aftv = BaseTVAsync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) # establish the ADB connection - await aftv.adb_connect(auth_timeout_s=auth_timeout_s) + await aftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) # get device properties await aftv.get_device_properties() From 813cfbf17847ef867eefdcd14f88bb11f0f6bf6f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 14 Feb 2022 19:26:15 -0800 Subject: [PATCH 27/66] Bump the version to 0.0.64 (#305) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 48068e75..ac3fa647 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.63" +__version__ = "0.0.64" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 922078da..3bdb7e60 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.63' +version = '0.0.64' # The full version, including alpha/beta/rc tags -release = '0.0.63' +release = '0.0.64' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 42851bda..f8475dac 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.63", + version="0.0.64", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 4562db2ac6e034e97358f2a8e2b02da5dbec6432 Mon Sep 17 00:00:00 2001 From: Stephen Goodall Date: Fri, 18 Feb 2022 14:54:08 +1100 Subject: [PATCH 28/66] Enable custom Audio State command (#306) * Add Option for Custom Audio State command * Add Option for Custom Audio State command * Add Option for Custom Audio State command * Add Option for Custom Audio State command * Add Option for Custom Audio State command Correction to default return value * Formatting * Formatting * Formatting Fix * Add test for CUSTOM_AUDIO_STATE * Test correction * PR Suggestions (#1) * Reorder _cmd function * Code suggestion change * Code suggestion change * Re-Ordering * Update docs to refer to function * Update basetv.py Co-authored-by: Jeff Irion --- androidtv/basetv/basetv.py | 18 ++++++++++++++++-- androidtv/basetv/basetv_async.py | 4 ++-- androidtv/basetv/basetv_sync.py | 4 ++-- androidtv/constants.py | 3 +++ tests/test_androidtv_sync.py | 5 +++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index c6cc2372..7a34c068 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -116,6 +116,20 @@ def customize_command(self, custom_command, value): elif custom_command in self._custom_commands: del self._custom_commands[custom_command] + def _cmd_audio_state(self): + """Get the command used to retrieve the current audio state for this device. + + Returns + ------- + str + The device-specific ADB shell command used to determine the current audio state + + """ + if constants.CUSTOM_AUDIO_STATE in self._custom_commands: + return self._custom_commands[constants.CUSTOM_AUDIO_STATE] + + return constants.CMD_AUDIO_STATE + def _cmd_current_app(self): """Get the command used to retrieve the current app for this device. @@ -468,12 +482,12 @@ def _audio_output_device(stream_music): @staticmethod def _audio_state(audio_state_response): - """Parse the :meth:`audio_state` property from the output of the command :py:const:`androidtv.constants.CMD_AUDIO_STATE`. + """Parse the :meth:`audio_state` property from the ADB shell output. Parameters ---------- audio_state_response : str, None - The output of the command :py:const:`androidtv.constants.CMD_AUDIO_STATE` + The output from the ADB command `androidtv.basetv.basetv.BaseTV._cmd_audio_state`` Returns ------- diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 3073e74e..1cbac26d 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -242,10 +242,10 @@ async def audio_state(self): Returns ------- str, None - The audio state, as determined from the ADB shell command :py:const:`androidtv.constants.CMD_AUDIO_STATE`, or ``None`` if it could not be determined + The audio state, or ``None`` if it could not be determined """ - audio_state_response = await self._adb.shell(constants.CMD_AUDIO_STATE) + audio_state_response = await self._adb.shell(self._cmd_audio_state()) return self._audio_state(audio_state_response) async def awake(self): diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index ec455d40..e0c81bc7 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -242,10 +242,10 @@ def audio_state(self): Returns ------- str, None - The audio state, as determined from the ADB shell command :py:const:`androidtv.constants.CMD_AUDIO_STATE`, or ``None`` if it could not be determined + The audio state, or ``None`` if it could not be determined """ - audio_state_response = self._adb.shell(constants.CMD_AUDIO_STATE) + audio_state_response = self._adb.shell(self._cmd_audio_state()) return self._audio_state(audio_state_response) def awake(self): diff --git a/androidtv/constants.py b/androidtv/constants.py index cbeca3fb..1ecd87ad 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -35,6 +35,7 @@ class DeviceEnum(IntEnum): INTENT_HOME = "android.intent.category.HOME" # Customizable commands +CUSTOM_AUDIO_STATE = "audio_state" CUSTOM_CURRENT_APP = "current_app" CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE = "current_app_media_session_state" CUSTOM_LAUNCH_APP = "launch_app" @@ -42,6 +43,7 @@ class DeviceEnum(IntEnum): CUSTOM_TURN_OFF = "turn_off" CUSTOM_TURN_ON = "turn_on" CUSTOMIZABLE_COMMANDS = { + CUSTOM_AUDIO_STATE, CUSTOM_CURRENT_APP, CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, CUSTOM_LAUNCH_APP, @@ -52,6 +54,7 @@ class DeviceEnum(IntEnum): #: The subset of `CUSTOMIZABLE_COMMANDS` that is potentially used in the ``update()`` method HA_CUSTOMIZABLE_COMMANDS = ( + CUSTOM_AUDIO_STATE, CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, CUSTOM_LAUNCH_APP, CUSTOM_RUNNING_APPS, diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index d3b39554..46d1f42e 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -622,6 +622,11 @@ def test_customize_command(self): self.atv.turn_on() patched.assert_called_with("5") + self.atv.customize_command(constants.CUSTOM_AUDIO_STATE, "6") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.audio_state() + patched.assert_called_with("6") + # Delete a custom command self.atv.customize_command(constants.CUSTOM_TURN_ON, None) with patch.object(self.atv._adb, "shell") as patched: From fb4d7e6b0858e0943f7b0bcbd21aaf0252712ec6 Mon Sep 17 00:00:00 2001 From: Shawn Heide Date: Sun, 6 Mar 2022 09:31:01 -0800 Subject: [PATCH 29/66] Add custom hdmi input (#307) * Add ability to use custom HDMI input command * Fix test --- androidtv/basetv/basetv.py | 18 ++++++++++++++++-- androidtv/basetv/basetv_async.py | 3 ++- androidtv/basetv/basetv_sync.py | 3 ++- androidtv/constants.py | 3 +++ tests/test_androidtv_sync.py | 5 +++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 7a34c068..aa988a1b 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -174,6 +174,20 @@ def _cmd_current_app_media_session_state(self): return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE + def _cmd_hdmi_input(self): + """Get the command used to retrieve the current HDMI input for this device. + + Returns + ------- + str + The device-specific ADB shell command used to determine the current HDMI input + + """ + if constants.CUSTOM_HDMI_INPUT in self._custom_commands: + return self._custom_commands[constants.CUSTOM_HDMI_INPUT] + + return constants.CMD_HDMI_INPUT + def _cmd_launch_app(self, app): """Get the command to launch the specified app for this device. @@ -555,12 +569,12 @@ def _current_app_media_session_state(self, current_app_media_session_state_respo @staticmethod def _get_hdmi_input(hdmi_response): - """Get the HDMI input from the output of :py:const:`androidtv.constants.CMD_HDMI_INPUT`. + """Get the HDMI input from the from the ADB shell output`. Parameters ---------- hdmi_response : str, None - The output of :py:const:`androidtv.constants.CMD_HDMI_INPUT` + The output from the ADB command `androidtv.basetv.basetv.BaseTV._cmd_hdmi_input`` Returns ------- diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 1cbac26d..d2d56d7d 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -296,7 +296,8 @@ async def get_hdmi_input(self): The HDMI input, or ``None`` if it could not be determined """ - return self._get_hdmi_input(await self._adb.shell(constants.CMD_HDMI_INPUT)) + hdmi_input_response = await self._adb.shell(self._cmd_hdmi_input()) + return self._get_hdmi_input(hdmi_input_response) async def get_installed_apps(self): """Return a list of installed applications. diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index e0c81bc7..31c96c38 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -296,7 +296,8 @@ def get_hdmi_input(self): The HDMI input, or ``None`` if it could not be determined """ - return self._get_hdmi_input(self._adb.shell(constants.CMD_HDMI_INPUT)) + hdmi_input_response = self._adb.shell(self._cmd_hdmi_input()) + return self._get_hdmi_input(hdmi_input_response) def get_installed_apps(self): """Return a list of installed applications. diff --git a/androidtv/constants.py b/androidtv/constants.py index 1ecd87ad..f9889aaf 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -38,6 +38,7 @@ class DeviceEnum(IntEnum): CUSTOM_AUDIO_STATE = "audio_state" CUSTOM_CURRENT_APP = "current_app" CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE = "current_app_media_session_state" +CUSTOM_HDMI_INPUT = "hdmi_input" CUSTOM_LAUNCH_APP = "launch_app" CUSTOM_RUNNING_APPS = "running_apps" CUSTOM_TURN_OFF = "turn_off" @@ -46,6 +47,7 @@ class DeviceEnum(IntEnum): CUSTOM_AUDIO_STATE, CUSTOM_CURRENT_APP, CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, + CUSTOM_HDMI_INPUT, CUSTOM_LAUNCH_APP, CUSTOM_RUNNING_APPS, CUSTOM_TURN_OFF, @@ -56,6 +58,7 @@ class DeviceEnum(IntEnum): HA_CUSTOMIZABLE_COMMANDS = ( CUSTOM_AUDIO_STATE, CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE, + CUSTOM_HDMI_INPUT, CUSTOM_LAUNCH_APP, CUSTOM_RUNNING_APPS, CUSTOM_TURN_OFF, diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index 46d1f42e..b87a60be 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -627,6 +627,11 @@ def test_customize_command(self): self.atv.audio_state() patched.assert_called_with("6") + self.atv.customize_command(constants.CUSTOM_HDMI_INPUT, "7") + with patch.object(self.atv._adb, "shell") as patched: + self.atv.get_hdmi_input() + patched.assert_called_with("7") + # Delete a custom command self.atv.customize_command(constants.CUSTOM_TURN_ON, None) with patch.object(self.atv._adb, "shell") as patched: From e1c44665c0e5f03998c1d339113d124db0d22912 Mon Sep 17 00:00:00 2001 From: FilHarr <49679000+FilHarr@users.noreply.github.com> Date: Mon, 14 Mar 2022 19:43:49 +0000 Subject: [PATCH 30/66] Update launcher intent & Google Play app constants (#308) * Update INTENT_LAUNCH to use 'android.intent.category.LEANBACK_LAUNCHER'. Added new constants to launch apps on Fire TV (to keep using 'android.intent.category.LAUNCHER') Update constants for Android Play apps. * Update test files to reflect changes to launch constants * formatting * formatting * Fixed missing comma * Fixed test errors * Fixed another test error * More test fixes * Formatting (line length) fixes * More formatting changes * Added missing tests * Formatting (again....) * Apply suggestions from code review * Apply suggestions from code review * Removed redundant tests Co-authored-by: Jeff Irion --- .gitignore | 4 +++- androidtv/basetv/basetv.py | 3 +++ androidtv/constants.py | 17 ++++++++++++++--- tests/generate_test_constants.py | 1 + tests/test_androidtv_sync.py | 9 +++++++++ tests/test_constants.py | 8 +++++++- tests/test_firetv_async.py | 8 +++++--- tests/test_firetv_sync.py | 8 +++++--- 8 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 8eda1137..b87b6855 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,10 @@ **/__pycache__/ androidtv.egg-info -# Build files +# Build / development files .eggs/ +.vs/ +.vscode/ build/ dist/ diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index aa988a1b..faa400aa 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -213,6 +213,9 @@ def _cmd_launch_app(self, app): ): return constants.CMD_LAUNCH_APP_GOOGLE_TV.format(app) + if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: + return constants.CMD_LAUNCH_APP_FIRETV.format(app) + return constants.CMD_LAUNCH_APP.format(app) def _cmd_running_apps(self): diff --git a/androidtv/constants.py b/androidtv/constants.py index f9889aaf..5b6d20ae 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -31,7 +31,8 @@ class DeviceEnum(IntEnum): # Intents -INTENT_LAUNCH = "android.intent.category.LAUNCHER" +INTENT_LAUNCH = "android.intent.category.LEANBACK_LAUNCHER" +INTENT_LAUNCH_FIRETV = "android.intent.category.LAUNCHER" INTENT_HOME = "android.intent.category.HOME" # Customizable commands @@ -106,11 +107,21 @@ class DeviceEnum(IntEnum): "if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c " + INTENT_LAUNCH + " --pct-syskeys 0 1; fi" ) +#: Launch an app if it is not already the current app (assumes the variable ``CURRENT_APP`` has already been set) on a Fire TV +CMD_LAUNCH_APP_CONDITION_FIRETV = ( + "if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c " + INTENT_LAUNCH_FIRETV + " --pct-syskeys 0 1; fi" +) + #: Launch an app if it is not already the current app CMD_LAUNCH_APP = ( CMD_DEFINE_CURRENT_APP_VARIABLE.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION ) +#: Launch an app on a Fire TV device +CMD_LAUNCH_APP_FIRETV = ( + CMD_DEFINE_CURRENT_APP_VARIABLE.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION_FIRETV +) + #: Launch an app on a Google TV device CMD_LAUNCH_APP_GOOGLE_TV = ( CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION @@ -403,10 +414,10 @@ class DeviceEnum(IntEnum): APP_NOS = "nl.nos.app" APP_NPO = "nl.uitzendinggemist" APP_OCS = "com.orange.ocsgo" -APP_PLAY_GAMES = "com.google.android.games" +APP_PLAY_GAMES = "com.google.android.play.games" APP_PLAY_MUSIC = "com.google.android.music" APP_PLAY_STORE = "com.android.vending" -APP_PLAY_VIDEOS = "com.android.videos" +APP_PLAY_VIDEOS = "com.google.android.videos" APP_PLEX = "com.plexapp.android" APP_PRIME_VIDEO = "com.amazon.amazonvideo.livingroom" APP_PRIME_VIDEO_FIRETV = "com.amazon.firebat" diff --git a/tests/generate_test_constants.py b/tests/generate_test_constants.py index f7aff0e1..5158c744 100644 --- a/tests/generate_test_constants.py +++ b/tests/generate_test_constants.py @@ -14,6 +14,7 @@ "CMD_DEFINE_CURRENT_APP_VARIABLE", "CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV", "CMD_LAUNCH_APP_CONDITION", + "CMD_LAUNCH_APP_CONDITION_FIRETV", } diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index b87a60be..9e2435cf 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -123,6 +123,15 @@ def test_start_intent(self): getattr(self.atv._adb, self.ADB_ATTR).shell_cmd, "am start -a android.intent.action.VIEW -d TEST" ) + def test_launch_app_stop_app(self): + """Test that the ``AndroidTVSync.launch_app`` and ``AndroidTVSync.stop_app`` methods work correctly.""" + with patchers.patch_connect(True)[self.PATCH_KEY], patchers.patch_shell(None)[self.PATCH_KEY]: + self.atv.launch_app("TEST") + self.assertEqual(getattr(self.atv._adb, self.ADB_ATTR).shell_cmd, constants.CMD_LAUNCH_APP.format("TEST")) + + self.atv.stop_app("TEST") + self.assertEqual(getattr(self.atv._adb, self.ADB_ATTR).shell_cmd, "am force-stop TEST") + def test_running_apps(self): """Check that the ``running_apps`` property works correctly.""" with patchers.patch_shell(None)[self.PATCH_KEY]: diff --git a/tests/test_constants.py b/tests/test_constants.py index 44891e38..0a46cfa8 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -113,13 +113,19 @@ def test_constants(self): # CMD_LAUNCH_APP self.assertCommand( constants.CMD_LAUNCH_APP, + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + ) + + # CMD_LAUNCH_APP_FIRETV + self.assertCommand( + constants.CMD_LAUNCH_APP_FIRETV, r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_LAUNCH_APP_GOOGLE_TV self.assertCommand( constants.CMD_LAUNCH_APP_GOOGLE_TV, - r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LAUNCHER --pct-syskeys 0 1; fi", + r"CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_MAC_ETH0 diff --git a/tests/test_firetv_async.py b/tests/test_firetv_async.py index 90fc0003..b0aac7a8 100644 --- a/tests/test_firetv_async.py +++ b/tests/test_firetv_async.py @@ -46,7 +46,7 @@ async def test_turn_on_off(self): async def test_send_intent(self): """Test that the ``_send_intent`` method works correctly.""" with async_patchers.patch_shell("output\r\nretcode")[self.PATCH_KEY]: - result = await self.ftv._send_intent("TEST", constants.INTENT_LAUNCH) + result = await self.ftv._send_intent("TEST", constants.INTENT_LAUNCH_FIRETV) self.assertEqual( getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, "monkey -p TEST -c android.intent.category.LAUNCHER 1; echo $?", @@ -54,7 +54,7 @@ async def test_send_intent(self): self.assertDictEqual(result, {"output": "output", "retcode": "retcode"}) with async_patchers.patch_connect(True)[self.PATCH_KEY], async_patchers.patch_shell(None)[self.PATCH_KEY]: - result = await self.ftv._send_intent("TEST", constants.INTENT_LAUNCH) + result = await self.ftv._send_intent("TEST", constants.INTENT_LAUNCH_FIRETV) self.assertEqual( getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, "monkey -p TEST -c android.intent.category.LAUNCHER 1; echo $?", @@ -66,7 +66,9 @@ async def test_launch_app_stop_app(self): """Test that the ``FireTVAsync.launch_app`` and ``FireTVAsync.stop_app`` methods work correctly.""" with async_patchers.patch_shell("")[self.PATCH_KEY]: await self.ftv.launch_app("TEST") - self.assertEqual(getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, constants.CMD_LAUNCH_APP.format("TEST")) + self.assertEqual( + getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, constants.CMD_LAUNCH_APP_FIRETV.format("TEST") + ) await self.ftv.stop_app("TEST") self.assertEqual(getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, "am force-stop TEST") diff --git a/tests/test_firetv_sync.py b/tests/test_firetv_sync.py index 7dc61c05..f5f9906e 100644 --- a/tests/test_firetv_sync.py +++ b/tests/test_firetv_sync.py @@ -64,7 +64,7 @@ def test_turn_on_off(self): def test_send_intent(self): """Test that the ``_send_intent`` method works correctly.""" with patchers.patch_connect(True)[self.PATCH_KEY], patchers.patch_shell("output\r\nretcode")[self.PATCH_KEY]: - result = self.ftv._send_intent("TEST", constants.INTENT_LAUNCH) + result = self.ftv._send_intent("TEST", constants.INTENT_LAUNCH_FIRETV) self.assertEqual( getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, "monkey -p TEST -c android.intent.category.LAUNCHER 1; echo $?", @@ -72,7 +72,7 @@ def test_send_intent(self): self.assertDictEqual(result, {"output": "output", "retcode": "retcode"}) with patchers.patch_connect(True)[self.PATCH_KEY], patchers.patch_shell(None)[self.PATCH_KEY]: - result = self.ftv._send_intent("TEST", constants.INTENT_LAUNCH) + result = self.ftv._send_intent("TEST", constants.INTENT_LAUNCH_FIRETV) self.assertEqual( getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, "monkey -p TEST -c android.intent.category.LAUNCHER 1; echo $?", @@ -83,7 +83,9 @@ def test_launch_app_stop_app(self): """Test that the ``FireTVSync.launch_app`` and ``FireTVSync.stop_app`` methods work correctly.""" with patchers.patch_connect(True)[self.PATCH_KEY], patchers.patch_shell(None)[self.PATCH_KEY]: self.ftv.launch_app("TEST") - self.assertEqual(getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, constants.CMD_LAUNCH_APP.format("TEST")) + self.assertEqual( + getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, constants.CMD_LAUNCH_APP_FIRETV.format("TEST") + ) self.ftv.stop_app("TEST") self.assertEqual(getattr(self.ftv._adb, self.ADB_ATTR).shell_cmd, "am force-stop TEST") From 11b01d61399ad9bab8190d27de070484f8c3e48c Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 15 Mar 2022 08:56:21 -0700 Subject: [PATCH 31/66] Bump the version to 0.0.65 (#309) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index ac3fa647..a4aec576 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.64" +__version__ = "0.0.65" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 3bdb7e60..28c674f7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.64' +version = '0.0.65' # The full version, including alpha/beta/rc tags -release = '0.0.64' +release = '0.0.65' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index f8475dac..3fbbdb3b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.64", + version="0.0.65", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 466489013da8d137b4c716fbcb90f4ccac629768 Mon Sep 17 00:00:00 2001 From: deviant-aut <41020010+deviant-aut@users.noreply.github.com> Date: Sat, 26 Mar 2022 17:43:19 +0100 Subject: [PATCH 32/66] Android11 Basic Support (#310) * Android11 Basic Support * Update basetv.py * CI black black --check --safe --line-length 120 --target-version py35 * CI test constants * Update test_constants.py * Update test_basetv_sync.py * Update test_basetv_sync.py Test Device Specific Commands * Update test_basetv_sync.py assertion * Update test_basetv_sync.py * Create main.yml * Revert "Create main.yml" This reverts commit ca0d21b00bce16599afc060a89d1d74e2c54c06f. * Update basetv.py PR suggestion 2 * Update basetv.py PR suggestion 3 * update audio_state cmd PR suggestion * Update test_constants.py PR suggestion * changed audio state cmd * correction: return device specific cmds * Update test_basetv_sync.py _cmd_launch_app() takes exactly 2 arguments (1 given) * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Jeff Irion --- androidtv/basetv/basetv.py | 19 +++++++++++ androidtv/constants.py | 31 ++++++++++++++++++ tests/generate_test_constants.py | 2 ++ tests/test_basetv_sync.py | 54 ++++++++++++++++++++++++++++++++ tests/test_constants.py | 30 ++++++++++++++++++ 5 files changed, 136 insertions(+) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index faa400aa..0f895890 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -128,6 +128,9 @@ def _cmd_audio_state(self): if constants.CUSTOM_AUDIO_STATE in self._custom_commands: return self._custom_commands[constants.CUSTOM_AUDIO_STATE] + # Is this an Android 11 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": + return constants.CMD_AUDIO_STATE11 return constants.CMD_AUDIO_STATE def _cmd_current_app(self): @@ -150,6 +153,10 @@ def _cmd_current_app(self): ): return constants.CMD_CURRENT_APP_GOOGLE_TV + # Is this an Android 11 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": + return constants.CMD_CURRENT_APP11 + return constants.CMD_CURRENT_APP def _cmd_current_app_media_session_state(self): @@ -172,6 +179,10 @@ def _cmd_current_app_media_session_state(self): ): return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV + # Is this an Android 11 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11 + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE def _cmd_hdmi_input(self): @@ -186,6 +197,10 @@ def _cmd_hdmi_input(self): if constants.CUSTOM_HDMI_INPUT in self._custom_commands: return self._custom_commands[constants.CUSTOM_HDMI_INPUT] + # Is this an Android 11 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": + return constants.CMD_HDMI_INPUT11 + return constants.CMD_HDMI_INPUT def _cmd_launch_app(self, app): @@ -216,6 +231,10 @@ def _cmd_launch_app(self, app): if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: return constants.CMD_LAUNCH_APP_FIRETV.format(app) + # Is this an Android 11 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": + return constants.CMD_LAUNCH_APP11.format(app) + return constants.CMD_LAUNCH_APP.format(app) def _cmd_running_apps(self): diff --git a/androidtv/constants.py b/androidtv/constants.py index 5b6d20ae..f71bfbbb 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -75,20 +75,37 @@ class DeviceEnum(IntEnum): #: Get the audio state CMD_AUDIO_STATE = r"dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')" +#: Get the audio state for an Android 11 device +CMD_AUDIO_STATE11 = ( + "CURRENT_AUDIO_STATE=$(dumpsys audio | sed -r -n '/[0-9]{2}-[0-9]{2}.*player piid:.*state:.*$/h; ${x;p;}') && " + + r"echo $CURRENT_AUDIO_STATE | grep -q paused && echo -e '1\c' || { echo $CURRENT_AUDIO_STATE | grep -q started && echo '2\c' || echo '0\c' ; }" +) + #: Determine whether the device is awake CMD_AWAKE = "dumpsys power | grep mWakefulness | grep -q Awake" #: Parse current application identifier from dumpsys output and assign it to ``CURRENT_APP`` variable (assumes dumpsys output is momentarily set to ``CURRENT_APP`` variable) CMD_PARSE_CURRENT_APP = "CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\\}*}" +#: Parse current application for an Android 11 device +CMD_PARSE_CURRENT_APP11 = "CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* }" + #: Assign focused application identifier to ``CURRENT_APP`` variable CMD_DEFINE_CURRENT_APP_VARIABLE = ( "CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && " + CMD_PARSE_CURRENT_APP ) +#: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 11 device +CMD_DEFINE_CURRENT_APP_VARIABLE11 = ( + "CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && " + CMD_PARSE_CURRENT_APP11 +) + #: Output identifier for current/focused application CMD_CURRENT_APP = CMD_DEFINE_CURRENT_APP_VARIABLE + " && echo $CURRENT_APP" +#: Output identifier for current/focused application for an Android 11 device +CMD_CURRENT_APP11 = CMD_DEFINE_CURRENT_APP_VARIABLE11 + " && echo $CURRENT_APP" + #: Assign focused application identifier to ``CURRENT_APP`` variable (for a Google TV device) CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV = ( "CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && " + CMD_PARSE_CURRENT_APP @@ -102,6 +119,12 @@ class DeviceEnum(IntEnum): "dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]'" ) +#: Get the HDMI input for an Android 11 device +CMD_HDMI_INPUT11 = ( + "(HDMI=$(dumpsys tv_input | grep 'ResourceClientProfile {.*}' | grep -o -E '(hdmi_port=[0-9]|TV)') && { echo ${HDMI/hdmi_port=/HW} | cut -d' ' -f1 ; }) || " + + CMD_HDMI_INPUT +) + #: Launch an app if it is not already the current app (assumes the variable ``CURRENT_APP`` has already been set) CMD_LAUNCH_APP_CONDITION = ( "if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c " + INTENT_LAUNCH + " --pct-syskeys 0 1; fi" @@ -117,6 +140,11 @@ class DeviceEnum(IntEnum): CMD_DEFINE_CURRENT_APP_VARIABLE.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION ) +#: Launch an app if it is not already the current app on an Android 11 device +CMD_LAUNCH_APP11 = ( + CMD_DEFINE_CURRENT_APP_VARIABLE11.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION +) + #: Launch an app on a Fire TV device CMD_LAUNCH_APP_FIRETV = ( CMD_DEFINE_CURRENT_APP_VARIABLE.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION_FIRETV @@ -133,6 +161,9 @@ class DeviceEnum(IntEnum): #: Determine the current app and get the state from ``dumpsys media_session`` CMD_CURRENT_APP_MEDIA_SESSION_STATE = CMD_CURRENT_APP + " && " + CMD_MEDIA_SESSION_STATE +#: Determine the current app and get the state from ``dumpsys media_session`` for an Android 11 device +CMD_CURRENT_APP_MEDIA_SESSION_STATE11 = CMD_CURRENT_APP11 + " && " + CMD_MEDIA_SESSION_STATE + #: Determine the current app and get the state from ``dumpsys media_session`` for a Google TV device CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV = CMD_CURRENT_APP_GOOGLE_TV + " && " + CMD_MEDIA_SESSION_STATE diff --git a/tests/generate_test_constants.py b/tests/generate_test_constants.py index 5158c744..f1decd70 100644 --- a/tests/generate_test_constants.py +++ b/tests/generate_test_constants.py @@ -11,7 +11,9 @@ "CMD_SUCCESS1", "CMD_SUCCESS1_FAILURE0", "CMD_PARSE_CURRENT_APP", + "CMD_PARSE_CURRENT_APP11", "CMD_DEFINE_CURRENT_APP_VARIABLE", + "CMD_DEFINE_CURRENT_APP_VARIABLE11", "CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV", "CMD_LAUNCH_APP_CONDITION", "CMD_LAUNCH_APP_CONDITION_FIRETV", diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 1cfa0a25..0ba5525d 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -95,6 +95,24 @@ "ethmac": "ab:cd:ef:gh:ij:kl", } +DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_11 = """NVIDIA +SHIELD Android TV +0123456789012 +11 +""" + +WIFIMAC_SHIELD_TV_11 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" +ETHMAC_SHIELD_TV_11 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" + +DEVICE_PROPERTIES_DICT_SHIELD_TV_11 = { + "manufacturer": "NVIDIA", + "model": "SHIELD Android TV", + "serialno": "0123456789012", + "sw_version": "11", + "wifimac": "11:22:33:44:55:66", + "ethmac": "ab:cd:ef:gh:ij:kl", +} + INSTALLED_APPS_OUTPUT_1 = """package:org.example.app package:org.example.launcher """ @@ -498,6 +516,42 @@ def test_get_device_properties(self): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT_SONY_TV, device_properties) + with patch.object( + self.btv._adb, + "shell", + side_effect=(DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_11, ETHMAC_SHIELD_TV_11, WIFIMAC_SHIELD_TV_11), + ): + self.btv = AndroidTVSync.from_base(self.btv) + device_properties = self.btv.get_device_properties() + assert self.btv.device_properties.get("sw_version", "") == "11" + assert self.btv.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV + self.assertDictEqual(DEVICE_PROPERTIES_DICT_SHIELD_TV_11, device_properties) + # _cmd_audio_state + self.assertEqual( + self.btv._cmd_audio_state(), + constants.CMD_AUDIO_STATE11, + ) + # _cmd_current_app + self.assertEqual( + self.btv._cmd_current_app(), + constants.CMD_CURRENT_APP11, + ) + # _cmd_current_app_media_session_state + self.assertEqual( + self.btv._cmd_current_app_media_session_state(), + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, + ) + # _cmd_hdmi_input + self.assertEqual( + self.btv._cmd_hdmi_input(), + constants.CMD_HDMI_INPUT11, + ) + # _cmd_launch_app + self.assertEqual( + self.btv._cmd_launch_app("TEST"), + constants.CMD_LAUNCH_APP11.format("TEST"), + ) + def test_get_installed_apps(self): """ "Check that `get_installed_apps` works correctly.""" with patchers.patch_shell(INSTALLED_APPS_OUTPUT_1)[self.PATCH_KEY]: diff --git a/tests/test_constants.py b/tests/test_constants.py index 0a46cfa8..24bf5332 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -68,6 +68,12 @@ def test_constants(self): r"dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')", ) + # CMD_AUDIO_STATE11 + self.assertCommand( + constants.CMD_AUDIO_STATE11, + r"CURRENT_AUDIO_STATE=$(dumpsys audio | sed -r -n '/[0-9]{2}-[0-9]{2}.*player piid:.*state:.*$/h; ${x;p;}') && echo $CURRENT_AUDIO_STATE | grep -q paused && echo -e '1\c' || { echo $CURRENT_AUDIO_STATE | grep -q started && echo '2\c' || echo '0\c' ; }", + ) + # CMD_AWAKE self.assertCommand(constants.CMD_AWAKE, r"dumpsys power | grep mWakefulness | grep -q Awake") @@ -77,6 +83,12 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP", ) + # CMD_CURRENT_APP11 + self.assertCommand( + constants.CMD_CURRENT_APP11, + r"CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", + ) + # CMD_CURRENT_APP_GOOGLE_TV self.assertCommand( constants.CMD_CURRENT_APP_GOOGLE_TV, @@ -89,6 +101,12 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\}*} && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) + # CMD_CURRENT_APP_MEDIA_SESSION_STATE11 + self.assertCommand( + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, + r"CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) + # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, @@ -107,6 +125,12 @@ def test_constants(self): r"dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]'", ) + # CMD_HDMI_INPUT11 + self.assertCommand( + constants.CMD_HDMI_INPUT11, + r"(HDMI=$(dumpsys tv_input | grep 'ResourceClientProfile {.*}' | grep -o -E '(hdmi_port=[0-9]|TV)') && { echo ${HDMI/hdmi_port=/HW} | cut -d' ' -f1 ; }) || dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]'", + ) + # CMD_INSTALLED_APPS self.assertCommand(constants.CMD_INSTALLED_APPS, r"pm list packages") @@ -116,6 +140,12 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && CURRENT_APP=${{CURRENT_APP#*ActivityRecord{{* * }} && CURRENT_APP=${{CURRENT_APP#*{{* * }} && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP%\}}*}} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", ) + # CMD_LAUNCH_APP11 + self.assertCommand( + constants.CMD_LAUNCH_APP11, + r"CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + ) + # CMD_LAUNCH_APP_FIRETV self.assertCommand( constants.CMD_LAUNCH_APP_FIRETV, From 0af6b3bf206b2207523177333c7ab83fb5eb9977 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 26 Mar 2022 09:46:31 -0700 Subject: [PATCH 33/66] Bump the version to 0.0.66 (#311) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index a4aec576..c7b7a795 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.65" +__version__ = "0.0.66" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 28c674f7..55ea19d6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.65' +version = '0.0.66' # The full version, including alpha/beta/rc tags -release = '0.0.65' +release = '0.0.66' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 3fbbdb3b..a0891f9a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.65", + version="0.0.66", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From bcc83dbbf7073f341db769483090093873e7f994 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 8 Apr 2022 18:57:20 -0700 Subject: [PATCH 34/66] Remove unused STATE_* constants (#314) --- androidtv/constants.py | 2 -- tests/test_androidtv_sync.py | 32 +++++++++++++------------ tests/test_basetv_sync.py | 41 +++++++------------------------- tests/test_firetv_sync.py | 17 +++++++++---- tests/test_homeassistant_sync.py | 12 ++-------- 5 files changed, 40 insertions(+), 64 deletions(-) diff --git a/androidtv/constants.py b/androidtv/constants.py index f71bfbbb..be4b0af1 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -366,14 +366,12 @@ class DeviceEnum(IntEnum): # Android TV / Fire TV states -STATE_ON = "on" STATE_IDLE = "idle" STATE_OFF = "off" STATE_PLAYING = "playing" STATE_PAUSED = "paused" STATE_STANDBY = "standby" STATE_STOPPED = "stopped" -STATE_UNKNOWN = "unknown" #: States that are valid (used by :func:`~androidtv.basetv.state_detection_rules_validator`) VALID_STATES = (STATE_IDLE, STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_STANDBY) diff --git a/tests/test_androidtv_sync.py b/tests/test_androidtv_sync.py index 9e2435cf..d74c8ade 100644 --- a/tests/test_androidtv_sync.py +++ b/tests/test_androidtv_sync.py @@ -16,6 +16,8 @@ from . import patchers +UNKNOWN_APP = "unknown" + HDMI_INPUT_EMPTY = "\n" STREAM_MUSIC_EMPTY = "- STREAM_MUSIC:\n \n- STREAM" @@ -377,8 +379,8 @@ def test_update2(self): self.atv._state_detection_rules = STATE_DETECTION_RULES4 state = self.atv.update() - self.assertEqual(state[0], constants.STATE_PAUSED) + self.assertEqual(state[0], constants.STATE_PAUSED) self.atv._state_detection_rules = STATE_DETECTION_RULES5 state = self.atv.update() self.assertEqual(state[0], constants.STATE_IDLE) @@ -566,38 +568,38 @@ def test_state_detection(self): # Unknown app self.assertUpdate( - [True, True, constants.STATE_IDLE, 2, "unknown", 2, "hmdi_arc", False, 30, None, None], - (constants.STATE_PAUSED, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_IDLE, 2, UNKNOWN_APP, 2, "hmdi_arc", False, 30, None, None], + (constants.STATE_PAUSED, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) self.assertUpdate( - [True, True, constants.STATE_IDLE, 2, "unknown", 3, "hmdi_arc", False, 30, None, None], - (constants.STATE_PLAYING, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_IDLE, 2, UNKNOWN_APP, 3, "hmdi_arc", False, 30, None, None], + (constants.STATE_PLAYING, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) self.assertUpdate( - [True, True, constants.STATE_IDLE, 2, "unknown", 4, "hmdi_arc", False, 30, None, None], - (constants.STATE_IDLE, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_IDLE, 2, UNKNOWN_APP, 4, "hmdi_arc", False, 30, None, None], + (constants.STATE_IDLE, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) self.assertUpdate( - [True, True, constants.STATE_PLAYING, 2, "unknown", None, "hmdi_arc", False, 30, None, None], - (constants.STATE_PLAYING, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_PLAYING, 2, UNKNOWN_APP, None, "hmdi_arc", False, 30, None, None], + (constants.STATE_PLAYING, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) self.assertUpdate( - [True, True, constants.STATE_IDLE, 1, "unknown", None, "hmdi_arc", False, 30, None, None], - (constants.STATE_PAUSED, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_IDLE, 1, UNKNOWN_APP, None, "hmdi_arc", False, 30, None, None], + (constants.STATE_PAUSED, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) self.assertUpdate( - [True, True, constants.STATE_IDLE, 2, "unknown", None, "hmdi_arc", False, 30, None, None], - (constants.STATE_PLAYING, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_IDLE, 2, UNKNOWN_APP, None, "hmdi_arc", False, 30, None, None], + (constants.STATE_PLAYING, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) self.assertUpdate( - [True, True, constants.STATE_IDLE, 3, "unknown", None, "hmdi_arc", False, 30, None, None], - (constants.STATE_IDLE, "unknown", ["unknown"], "hmdi_arc", False, 0.5, None), + [True, True, constants.STATE_IDLE, 3, UNKNOWN_APP, None, "hmdi_arc", False, 30, None, None], + (constants.STATE_IDLE, UNKNOWN_APP, [UNKNOWN_APP], "hmdi_arc", False, 0.5, None), ) def test_customize_command(self): diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 0ba5525d..a0f7d219 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -117,10 +117,7 @@ package:org.example.launcher """ -INSTALLED_APPS_LIST = [ - "org.example.app", - "org.example.launcher", -] +INSTALLED_APPS_LIST = ["org.example.app", "org.example.launcher"] MEDIA_SESSION_STATE_OUTPUT = "com.amazon.tv.launcher\nstate=PlaybackState {state=2, position=0, buffered position=0, speed=0.0, updated=65749, actions=240640, custom actions=[], active item id=-1, error=null}" @@ -497,18 +494,11 @@ def test_get_device_properties(self): assert "Chromecast" in self.btv.device_properties.get("model", "") assert self.btv.DEVICE_ENUM == AndroidTVSync.DEVICE_ENUM self.assertEqual(self.btv.device_properties["manufacturer"], "Google") + self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP_GOOGLE_TV) self.assertEqual( - self.btv._cmd_current_app(), - constants.CMD_CURRENT_APP_GOOGLE_TV, - ) - self.assertEqual( - self.btv._cmd_current_app_media_session_state(), - constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, - ) - self.assertEqual( - self.btv._cmd_launch_app("TEST"), - constants.CMD_LAUNCH_APP_GOOGLE_TV.format("TEST"), + self.btv._cmd_current_app_media_session_state(), constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV ) + self.assertEqual(self.btv._cmd_launch_app("TEST"), constants.CMD_LAUNCH_APP_GOOGLE_TV.format("TEST")) with patch.object( self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT_SONY_TV, ETHMAC_SONY, WIFIMAC_SONY) @@ -527,30 +517,17 @@ def test_get_device_properties(self): assert self.btv.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV self.assertDictEqual(DEVICE_PROPERTIES_DICT_SHIELD_TV_11, device_properties) # _cmd_audio_state - self.assertEqual( - self.btv._cmd_audio_state(), - constants.CMD_AUDIO_STATE11, - ) + self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) # _cmd_current_app - self.assertEqual( - self.btv._cmd_current_app(), - constants.CMD_CURRENT_APP11, - ) + self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP11) # _cmd_current_app_media_session_state self.assertEqual( - self.btv._cmd_current_app_media_session_state(), - constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, + self.btv._cmd_current_app_media_session_state(), constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11 ) # _cmd_hdmi_input - self.assertEqual( - self.btv._cmd_hdmi_input(), - constants.CMD_HDMI_INPUT11, - ) + self.assertEqual(self.btv._cmd_hdmi_input(), constants.CMD_HDMI_INPUT11) # _cmd_launch_app - self.assertEqual( - self.btv._cmd_launch_app("TEST"), - constants.CMD_LAUNCH_APP11.format("TEST"), - ) + self.assertEqual(self.btv._cmd_launch_app("TEST"), constants.CMD_LAUNCH_APP11.format("TEST")) def test_get_installed_apps(self): """ "Check that `get_installed_apps` works correctly.""" diff --git a/tests/test_firetv_sync.py b/tests/test_firetv_sync.py index f5f9906e..9a68566d 100644 --- a/tests/test_firetv_sync.py +++ b/tests/test_firetv_sync.py @@ -16,6 +16,8 @@ from . import patchers +UNKNOWN_APP = "unknown" + HDMI_INPUT_EMPTY = "\n" CURRENT_APP_OUTPUT = "com.amazon.tv.launcher" @@ -363,23 +365,28 @@ def test_state_detection(self): # Unknown app self.assertUpdate( - [True, True, 1, "unknown", 3, ["unknown"], None], (constants.STATE_PLAYING, "unknown", ["unknown"], None) + [True, True, 1, UNKNOWN_APP, 3, [UNKNOWN_APP], None], + (constants.STATE_PLAYING, UNKNOWN_APP, [UNKNOWN_APP], None), ) self.assertUpdate( - [True, True, 1, "unknown", 2, ["unknown"], None], (constants.STATE_PAUSED, "unknown", ["unknown"], None) + [True, True, 1, UNKNOWN_APP, 2, [UNKNOWN_APP], None], + (constants.STATE_PAUSED, UNKNOWN_APP, [UNKNOWN_APP], None), ) self.assertUpdate( - [True, True, 1, "unknown", 1, ["unknown"], None], (constants.STATE_IDLE, "unknown", ["unknown"], None) + [True, True, 1, UNKNOWN_APP, 1, [UNKNOWN_APP], None], + (constants.STATE_IDLE, UNKNOWN_APP, [UNKNOWN_APP], None), ) self.assertUpdate( - [True, True, 1, "unknown", None, ["unknown"], None], (constants.STATE_PLAYING, "unknown", ["unknown"], None) + [True, True, 1, UNKNOWN_APP, None, [UNKNOWN_APP], None], + (constants.STATE_PLAYING, UNKNOWN_APP, [UNKNOWN_APP], None), ) self.assertUpdate( - [True, True, 2, "unknown", None, ["unknown"], None], (constants.STATE_PAUSED, "unknown", ["unknown"], None) + [True, True, 2, UNKNOWN_APP, None, [UNKNOWN_APP], None], + (constants.STATE_PAUSED, UNKNOWN_APP, [UNKNOWN_APP], None), ) diff --git a/tests/test_homeassistant_sync.py b/tests/test_homeassistant_sync.py index 8c7187fa..ef00e000 100644 --- a/tests/test_homeassistant_sync.py +++ b/tests/test_homeassistant_sync.py @@ -10,12 +10,7 @@ sys.path.insert(0, "..") -from adb_shell.exceptions import ( - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, -) +from adb_shell.exceptions import InvalidChecksumError, InvalidCommandError, InvalidResponseError, TcpTimeoutException from androidtv import setup from androidtv.constants import APPS, KEYS, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY from androidtv.exceptions import LockNotAcquiredException @@ -508,10 +503,7 @@ def test_reconnect(self): assert ( "ADB connection to {}:{} via ADB server {}:{} successfully established".format( - self.aftv.aftv.host, - self.aftv.aftv.port, - self.aftv.aftv.adb_server_ip, - self.aftv.aftv.adb_server_port, + self.aftv.aftv.host, self.aftv.aftv.port, self.aftv.aftv.adb_server_ip, self.aftv.aftv.adb_server_port ) in logs.output[0] ) From afa71345703eb41e29dcd34ee45640759540df03 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 8 Apr 2022 19:02:19 -0700 Subject: [PATCH 35/66] Simplify logging of connection attempts (#315) --- androidtv/adb_manager/adb_manager_async.py | 34 +++++++--------------- androidtv/adb_manager/adb_manager_sync.py | 34 +++++++--------------- androidtv/basetv/basetv_async.py | 10 +++---- androidtv/basetv/basetv_sync.py | 10 +++---- tests/test_adb_manager_async.py | 10 ------- tests/test_adb_manager_sync.py | 10 ------- tests/test_homeassistant_sync.py | 14 +++++++-- 7 files changed, 44 insertions(+), 78 deletions(-) diff --git a/androidtv/adb_manager/adb_manager_async.py b/androidtv/adb_manager/adb_manager_async.py index 4996de7f..e3c63d6f 100644 --- a/androidtv/adb_manager/adb_manager_async.py +++ b/androidtv/adb_manager/adb_manager_async.py @@ -207,9 +207,6 @@ def __init__(self, host, port, adbkey="", signer=None): self._signer = signer - # keep track of whether the ADB connection is intact - self._available = False - # use a lock to make sure that ADB commands don't overlap self._adb_lock = asyncio.Lock() @@ -231,7 +228,7 @@ async def close(self): async def connect( self, - always_log_errors=True, + log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, ): @@ -239,8 +236,8 @@ async def connect( Parameters ---------- - always_log_errors : bool - If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt + log_errors : bool + Whether errors should be logged auth_timeout_s : float Authentication timeout (in seconds) transport_timeout_s : float @@ -273,11 +270,10 @@ async def connect( # ADB connection successfully established _LOGGER.debug("ADB connection to %s:%d successfully established", self.host, self.port) - self._available = True return True except OSError as exc: - if self._available or always_log_errors: + if log_errors: if exc.strerror is None: exc.strerror = "Timed out trying to connect to ADB device." _LOGGER.warning( @@ -290,24 +286,21 @@ async def connect( # ADB connection attempt failed await self.close() - self._available = False return False except Exception as exc: # pylint: disable=broad-except - if self._available or always_log_errors: + if log_errors: _LOGGER.warning( "Couldn't connect to %s:%d. %s: %s", self.host, self.port, exc.__class__.__name__, exc ) # ADB connection attempt failed await self.close() - self._available = False return False except LockNotAcquiredException: _LOGGER.warning("Couldn't connect to %s:%d because adb-shell lock not acquired.", self.host, self.port) await self.close() - self._available = False return False @staticmethod @@ -470,9 +463,8 @@ def __init__(self, host, port=5555, adb_server_ip="", adb_server_port=5037): self._adb_client = None self._adb_device = None - # keep track of whether the ADB connection is/was intact + # keep track of whether the ADB connection is intact self._available = False - self._was_available = False # use a lock to make sure that ADB commands don't overlap self._adb_lock = asyncio.Lock() @@ -500,13 +492,13 @@ async def close(self): """ self._available = False - async def connect(self, always_log_errors=True): + async def connect(self, log_errors=True): """Connect to an Android TV / Fire TV device. Parameters ---------- - always_log_errors : bool - If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt + log_errors : bool + Whether errors should be logged Returns ------- @@ -531,11 +523,10 @@ async def connect(self, always_log_errors=True): self.adb_server_port, ) self._available = True - self._was_available = True return True # ADB connection attempt failed (without an exception) - if self._was_available or always_log_errors: + if log_errors: _LOGGER.warning( "Couldn't connect to %s:%d via ADB server %s:%d because the server is not connected to the device", self.host, @@ -546,12 +537,11 @@ async def connect(self, always_log_errors=True): await self.close() self._available = False - self._was_available = False return False # ADB connection attempt failed except Exception as exc: # noqa pylint: disable=broad-except - if self._was_available or always_log_errors: + if log_errors: _LOGGER.warning( "Couldn't connect to %s:%d via ADB server %s:%d, error: %s", self.host, @@ -563,7 +553,6 @@ async def connect(self, always_log_errors=True): await self.close() self._available = False - self._was_available = False return False except LockNotAcquiredException: @@ -576,7 +565,6 @@ async def connect(self, always_log_errors=True): ) await self.close() self._available = False - self._was_available = False return False async def pull(self, local_path, device_path): diff --git a/androidtv/adb_manager/adb_manager_sync.py b/androidtv/adb_manager/adb_manager_sync.py index e1a5d8c3..9647e227 100644 --- a/androidtv/adb_manager/adb_manager_sync.py +++ b/androidtv/adb_manager/adb_manager_sync.py @@ -91,9 +91,6 @@ def __init__(self, host, port, adbkey="", signer=None): self._signer = signer - # keep track of whether the ADB connection is intact - self._available = False - # use a lock to make sure that ADB commands don't overlap self._adb_lock = threading.Lock() @@ -115,7 +112,7 @@ def close(self): def connect( self, - always_log_errors=True, + log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, ): @@ -123,8 +120,8 @@ def connect( Parameters ---------- - always_log_errors : bool - If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt + log_errors : bool + Whether errors should be logged auth_timeout_s : float Authentication timeout (in seconds) transport_timeout_s : float @@ -157,11 +154,10 @@ def connect( # ADB connection successfully established _LOGGER.debug("ADB connection to %s:%d successfully established", self.host, self.port) - self._available = True return True except OSError as exc: - if self._available or always_log_errors: + if log_errors: if exc.strerror is None: exc.strerror = "Timed out trying to connect to ADB device." _LOGGER.warning( @@ -174,24 +170,21 @@ def connect( # ADB connection attempt failed self.close() - self._available = False return False except Exception as exc: # pylint: disable=broad-except - if self._available or always_log_errors: + if log_errors: _LOGGER.warning( "Couldn't connect to %s:%d. %s: %s", self.host, self.port, exc.__class__.__name__, exc ) # ADB connection attempt failed self.close() - self._available = False return False except LockNotAcquiredException: _LOGGER.warning("Couldn't connect to %s:%d because adb-shell lock not acquired.", self.host, self.port) self.close() - self._available = False return False @staticmethod @@ -354,9 +347,8 @@ def __init__(self, host, port=5555, adb_server_ip="", adb_server_port=5037): self._adb_client = None self._adb_device = None - # keep track of whether the ADB connection is/was intact + # keep track of whether the ADB connection is intact self._available = False - self._was_available = False # use a lock to make sure that ADB commands don't overlap self._adb_lock = threading.Lock() @@ -384,13 +376,13 @@ def close(self): """ self._available = False - def connect(self, always_log_errors=True): + def connect(self, log_errors=True): """Connect to an Android TV / Fire TV device. Parameters ---------- - always_log_errors : bool - If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt + log_errors : bool + Whether errors should be logged Returns ------- @@ -415,11 +407,10 @@ def connect(self, always_log_errors=True): self.adb_server_port, ) self._available = True - self._was_available = True return True # ADB connection attempt failed (without an exception) - if self._was_available or always_log_errors: + if log_errors: _LOGGER.warning( "Couldn't connect to %s:%d via ADB server %s:%d because the server is not connected to the device", self.host, @@ -430,12 +421,11 @@ def connect(self, always_log_errors=True): self.close() self._available = False - self._was_available = False return False # ADB connection attempt failed except Exception as exc: # noqa pylint: disable=broad-except - if self._was_available or always_log_errors: + if log_errors: _LOGGER.warning( "Couldn't connect to %s:%d via ADB server %s:%d, error: %s", self.host, @@ -447,7 +437,6 @@ def connect(self, always_log_errors=True): self.close() self._available = False - self._was_available = False return False except LockNotAcquiredException: @@ -460,7 +449,6 @@ def connect(self, always_log_errors=True): ) self.close() self._available = False - self._was_available = False return False def pull(self, local_path, device_path): diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index d2d56d7d..b9d5e288 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -159,7 +159,7 @@ async def adb_screencap(self): async def adb_connect( self, - always_log_errors=True, + log_errors=True, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, transport_timeout_s=constants.DEFAULT_TRANSPORT_TIMEOUT_S, ): @@ -167,8 +167,8 @@ async def adb_connect( Parameters ---------- - always_log_errors : bool - If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt + log_errors : bool + Whether errors should be logged auth_timeout_s : float Authentication timeout (in seconds) transport_timeout_s : float @@ -181,8 +181,8 @@ async def adb_connect( """ if isinstance(self._adb, ADBPythonAsync): - return await self._adb.connect(always_log_errors, auth_timeout_s, transport_timeout_s) - return await self._adb.connect(always_log_errors) + return await self._adb.connect(log_errors, auth_timeout_s, transport_timeout_s) + return await self._adb.connect(log_errors) async def adb_close(self): """Close the ADB connection. diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 31c96c38..d6639d71 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -159,7 +159,7 @@ def adb_screencap(self): def adb_connect( self, - always_log_errors=True, + log_errors=True, auth_timeout_s=constants.DEFAULT_AUTH_TIMEOUT_S, transport_timeout_s=constants.DEFAULT_TRANSPORT_TIMEOUT_S, ): @@ -167,8 +167,8 @@ def adb_connect( Parameters ---------- - always_log_errors : bool - If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt + log_errors : bool + Whether errors should be logged auth_timeout_s : float Authentication timeout (in seconds) transport_timeout_s : float @@ -181,8 +181,8 @@ def adb_connect( """ if isinstance(self._adb, ADBPythonSync): - return self._adb.connect(always_log_errors, auth_timeout_s, transport_timeout_s) - return self._adb.connect(always_log_errors) + return self._adb.connect(log_errors, auth_timeout_s, transport_timeout_s) + return self._adb.connect(log_errors) def adb_close(self): """Close the ADB connection. diff --git a/tests/test_adb_manager_async.py b/tests/test_adb_manager_async.py index 44d38ac0..d17c45e7 100644 --- a/tests/test_adb_manager_async.py +++ b/tests/test_adb_manager_async.py @@ -119,7 +119,6 @@ async def test_connect_success(self): with async_patchers.patch_connect(True)[self.PATCH_KEY]: self.assertTrue(await self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) @awaiter async def test_connect_fail(self): @@ -127,18 +126,15 @@ async def test_connect_fail(self): with async_patchers.patch_connect(False)[self.PATCH_KEY]: self.assertFalse(await self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) with async_patchers.patch_connect(True)[self.PATCH_KEY]: self.assertTrue(await self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) with async_patchers.patch_connect(True)[self.PATCH_KEY]: with async_patchers.PATCH_CONNECT_FAIL_CUSTOM_EXCEPTION[self.PATCH_KEY]: self.assertFalse(await self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) @awaiter async def test_connect_fail_lock(self): @@ -147,7 +143,6 @@ async def test_connect_fail_lock(self): with patch.object(self.adb, "_adb_lock", AsyncLockedLock()): self.assertFalse(await self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) @awaiter async def test_adb_shell_fail(self): @@ -322,7 +317,6 @@ async def test_connect_fail_server(self): with async_patchers.PATCH_ADB_SERVER_RUNTIME_ERROR: self.assertFalse(await self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) class TestADBPythonAsyncWithAuthentication(unittest.TestCase): @@ -343,13 +337,11 @@ async def test_connect_success_with_priv_key(self): ), patch("androidtv.adb_manager.adb_manager_async.PythonRSASigner", return_value="TEST"): self.assertTrue(await self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) with async_patchers.patch_connect(True)[self.PATCH_KEY]: with patch("androidtv.adb_manager.adb_manager_async.aiofiles.open") as patch_open: self.assertTrue(await self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) assert not patch_open.called @awaiter @@ -360,7 +352,6 @@ async def test_connect_success_with_priv_pub_key(self): ), patch("androidtv.adb_manager.adb_manager_async.PythonRSASigner", return_value=None): self.assertTrue(await self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) class TestADBPythonAsyncClose(unittest.TestCase): @@ -377,7 +368,6 @@ async def test_close(self): with async_patchers.patch_connect(True)[self.PATCH_KEY]: self.assertTrue(await self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) await self.adb.close() self.assertFalse(self.adb.available) diff --git a/tests/test_adb_manager_sync.py b/tests/test_adb_manager_sync.py index 4c6a4262..482df913 100644 --- a/tests/test_adb_manager_sync.py +++ b/tests/test_adb_manager_sync.py @@ -103,25 +103,21 @@ def test_connect_success(self): with patchers.patch_connect(True)[self.PATCH_KEY]: self.assertTrue(self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) def test_connect_fail(self): """Test when the connect attempt fails.""" with patchers.patch_connect(False)[self.PATCH_KEY]: self.assertFalse(self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) with patchers.patch_connect(True)[self.PATCH_KEY]: self.assertTrue(self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) with patchers.patch_connect(True)[self.PATCH_KEY]: with patchers.PATCH_CONNECT_FAIL_CUSTOM_EXCEPTION[self.PATCH_KEY]: self.assertFalse(self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) def test_connect_fail_lock(self): """Test when the connect attempt fails due to the lock.""" @@ -129,7 +125,6 @@ def test_connect_fail_lock(self): with patch.object(self.adb, "_adb_lock", LockedLock()): self.assertFalse(self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) def test_adb_shell_fail(self): """Test when an ADB shell command is not sent because the device is unavailable.""" @@ -286,7 +281,6 @@ def test_connect_fail_server(self): with patchers.PATCH_ADB_SERVER_RUNTIME_ERROR: self.assertFalse(self.adb.connect()) self.assertFalse(self.adb.available) - self.assertFalse(self.adb._available) class TestADBPythonSyncWithAuthentication(unittest.TestCase): @@ -306,13 +300,11 @@ def test_connect_success_with_priv_key(self): ), patch("androidtv.adb_manager.adb_manager_sync.PythonRSASigner", return_value="TEST"): self.assertTrue(self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) with patchers.patch_connect(True)[self.PATCH_KEY]: with patch("androidtv.adb_manager.adb_manager_sync.open") as patch_open: self.assertTrue(self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) assert not patch_open.called def test_connect_success_with_priv_pub_key(self): @@ -322,7 +314,6 @@ def test_connect_success_with_priv_pub_key(self): ), patch("androidtv.adb_manager.adb_manager_sync.PythonRSASigner", return_value=None): self.assertTrue(self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) class TestADBPythonSyncClose(unittest.TestCase): @@ -338,7 +329,6 @@ def test_close(self): with patchers.patch_connect(True)[self.PATCH_KEY]: self.assertTrue(self.adb.connect()) self.assertTrue(self.adb.available) - self.assertTrue(self.adb._available) self.adb.close() self.assertFalse(self.adb.available) diff --git a/tests/test_homeassistant_sync.py b/tests/test_homeassistant_sync.py index ef00e000..698ae53c 100644 --- a/tests/test_homeassistant_sync.py +++ b/tests/test_homeassistant_sync.py @@ -130,6 +130,8 @@ def __init__(self, aftv, name, apps, get_sources, turn_on_command, turn_off_comm self._sources = None self._state = None + self._failed_connect_count = 0 + @property def app_id(self): """Return the current app.""" @@ -283,7 +285,11 @@ def update(self): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.adb_connect(always_log_errors=False) + if self.aftv.adb_connect(log_errors=self._failed_connect_count == 0): + self._failed_connect_count = 0 + self._available = True + else: + self._failed_connect_count += 1 # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. @@ -353,7 +359,11 @@ def update(self): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.adb_connect(always_log_errors=False) + if self.aftv.adb_connect(log_errors=self._failed_connect_count == 0): + self._failed_connect_count = 0 + self._available = True + else: + self._failed_connect_count += 1 # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. From d4b8756ad8ada716eb123f486f410dcec9da37dd Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 8 Apr 2022 19:05:18 -0700 Subject: [PATCH 36/66] Bump the version to 0.0.67 (#316) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index c7b7a795..10cc027c 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.66" +__version__ = "0.0.67" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 55ea19d6..164e50b1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.66' +version = '0.0.67' # The full version, including alpha/beta/rc tags -release = '0.0.66' +release = '0.0.67' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index a0891f9a..f0f965e9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.66", + version="0.0.67", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 043bbb5bf72717f40fde7ea3d4bb776293c49005 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 14 Apr 2022 15:00:00 +0200 Subject: [PATCH 37/66] Add log_errors parameter in setup (#318) --- androidtv/__init__.py | 9 ++++++--- androidtv/setup_async.py | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 10cc027c..b9671696 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -24,6 +24,7 @@ def setup( auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, signer=None, transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, + log_errors=True, ): """Connect to a device and determine whether it's an Android TV or an Amazon Fire TV. @@ -49,6 +50,8 @@ def setup( The signer for the ADB keys, as loaded by :meth:`androidtv.adb_manager.adb_manager_sync.ADBPythonSync.load_adbkey` transport_timeout_s : float Transport timeout (in seconds) + log_errors: bool + Whether connection errors should be logged Returns ------- @@ -58,14 +61,14 @@ def setup( """ if device_class == "androidtv": atv = AndroidTVSync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - atv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) + atv.adb_connect(log_errors=log_errors, auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) atv.get_device_properties() atv.get_installed_apps() return atv if device_class == "firetv": ftv = FireTVSync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - ftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) + ftv.adb_connect(log_errors=log_errors, auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) ftv.get_device_properties() ftv.get_installed_apps() return ftv @@ -76,7 +79,7 @@ def setup( aftv = BaseTVSync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) # establish the ADB connection - aftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) + aftv.adb_connect(log_errors=log_errors, auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) # get device properties aftv.device_properties = aftv.get_device_properties() diff --git a/androidtv/setup_async.py b/androidtv/setup_async.py index 9fb53c2d..797969ae 100644 --- a/androidtv/setup_async.py +++ b/androidtv/setup_async.py @@ -20,6 +20,7 @@ async def setup( auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S, signer=None, transport_timeout_s=DEFAULT_TRANSPORT_TIMEOUT_S, + log_errors=True, ): """Connect to a device and determine whether it's an Android TV or an Amazon Fire TV. @@ -45,6 +46,8 @@ async def setup( The signer for the ADB keys, as loaded by :meth:`androidtv.adb_manager.adb_manager_async.ADBPythonAsync.load_adbkey` transport_timeout_s : float Transport timeout (in seconds) + log_errors: bool + Whether connection errors should be logged Returns ------- @@ -54,14 +57,18 @@ async def setup( """ if device_class == "androidtv": atv = AndroidTVAsync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - await atv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) + await atv.adb_connect( + log_errors=log_errors, auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s + ) await atv.get_device_properties() await atv.get_installed_apps() return atv if device_class == "firetv": ftv = FireTVAsync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) - await ftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) + await ftv.adb_connect( + log_errors=log_errors, auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s + ) await ftv.get_device_properties() await ftv.get_installed_apps() return ftv @@ -72,7 +79,9 @@ async def setup( aftv = BaseTVAsync(host, port, adbkey, adb_server_ip, adb_server_port, state_detection_rules, signer) # establish the ADB connection - await aftv.adb_connect(auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s) + await aftv.adb_connect( + log_errors=log_errors, auth_timeout_s=auth_timeout_s, transport_timeout_s=transport_timeout_s + ) # get device properties await aftv.get_device_properties() From f63687a96fc222eedb5f2fd852fc729f92d33a47 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 24 May 2022 07:39:50 -0700 Subject: [PATCH 38/66] Bump the version to 0.0.68 (#319) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index b9671696..9ef5e92e 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.67" +__version__ = "0.0.68" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 164e50b1..95f34b9c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.67' +version = '0.0.68' # The full version, including alpha/beta/rc tags -release = '0.0.67' +release = '0.0.68' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index f0f965e9..328b54d6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.67", + version="0.0.68", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From b5e1d918c9a1aa63ea5198d6d89ac213b61a4c49 Mon Sep 17 00:00:00 2001 From: Jo De Boeck Date: Thu, 13 Oct 2022 16:24:15 +0200 Subject: [PATCH 39/66] Fix for current application in android 11 (#321) Parsing of current app is broken incase google assitant gets triggered and closed again. Suddenly we have Windows #1 as `mOwnerUid=10050 showForAllUsers=false package=com.google.android.inputmethod.latin appop=NONE` this is not correct. In this pull request based on the link issue, I've moved the command to find the windows that has the inputfocus instead See: https://github.com/home-assistant/core/issues/69723 Signed-off-by: Jo De Boeck Signed-off-by: Jo De Boeck --- androidtv/constants.py | 2 +- tests/test_constants.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/constants.py b/androidtv/constants.py index be4b0af1..325270ef 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -96,7 +96,7 @@ class DeviceEnum(IntEnum): ) #: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 11 device CMD_DEFINE_CURRENT_APP_VARIABLE11 = ( - "CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && " + CMD_PARSE_CURRENT_APP11 + "CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && " + CMD_PARSE_CURRENT_APP11 ) diff --git a/tests/test_constants.py b/tests/test_constants.py index 24bf5332..57034c60 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -86,7 +86,7 @@ def test_constants(self): # CMD_CURRENT_APP11 self.assertCommand( constants.CMD_CURRENT_APP11, - r"CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", + r"CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", ) # CMD_CURRENT_APP_GOOGLE_TV @@ -104,7 +104,7 @@ def test_constants(self): # CMD_CURRENT_APP_MEDIA_SESSION_STATE11 self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, - r"CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + r"CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV @@ -143,7 +143,7 @@ def test_constants(self): # CMD_LAUNCH_APP11 self.assertCommand( constants.CMD_LAUNCH_APP11, - r"CURRENT_APP=$(dumpsys window windows | grep 'Window #1') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + r"CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_LAUNCH_APP_FIRETV From b1425b1b0d1d826e25948fad15ca3650433e3d5d Mon Sep 17 00:00:00 2001 From: Jo De Boeck Date: Sat, 15 Oct 2022 19:06:46 +0200 Subject: [PATCH 40/66] Bump the version to 0.0.69 (#322) Signed-off-by: Jo De Boeck Signed-off-by: Jo De Boeck --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 9ef5e92e..d71de04e 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.68" +__version__ = "0.0.69" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 95f34b9c..3b6cd8d9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.68' +version = '0.0.69' # The full version, including alpha/beta/rc tags -release = '0.0.68' +release = '0.0.69' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 328b54d6..f3dbdd7e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.68", + version="0.0.69", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 11afeab7b518d439d209f5252625e637ed6b4b8c Mon Sep 17 00:00:00 2001 From: Victor4X Date: Thu, 24 Nov 2022 18:38:11 +0100 Subject: [PATCH 41/66] Fix CMD_SCREEN_ON constant in accordance with #323 (#325) * Fix CMD_SCREEN_ON constant in accordance with #323 * Format constants.py with black * Update constant in unittest --- androidtv/constants.py | 4 +--- tests/test_constants.py | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/androidtv/constants.py b/androidtv/constants.py index 325270ef..175e4d81 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -177,9 +177,7 @@ class DeviceEnum(IntEnum): CMD_INSTALLED_APPS = "pm list packages" #: Determine if the device is on -CMD_SCREEN_ON = ( - "(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true')" -) +CMD_SCREEN_ON = "(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON')" #: Get the "STREAM_MUSIC" block from ``dumpsys audio`` CMD_STREAM_MUSIC = r"dumpsys audio | grep '\- STREAM_MUSIC:' -A 11" diff --git a/tests/test_constants.py b/tests/test_constants.py index 57034c60..5f3fb45d 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -185,13 +185,13 @@ def test_constants(self): # CMD_SCREEN_ON self.assertCommand( constants.CMD_SCREEN_ON, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true')", + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON')", ) # CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE self.assertCommand( constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep Locks | grep 'size='", + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON') && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep mWakefulness | grep -q Awake && echo -e '1\c' || echo -e '0\c' && dumpsys power | grep Locks | grep 'size='", ) # CMD_SERIALNO @@ -203,25 +203,25 @@ def test_constants(self): # CMD_TURN_OFF_ANDROIDTV self.assertCommand( constants.CMD_TURN_OFF_ANDROIDTV, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && input keyevent 26", + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON') && input keyevent 26", ) # CMD_TURN_OFF_FIRETV self.assertCommand( constants.CMD_TURN_OFF_FIRETV, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') && input keyevent 223", + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON') && input keyevent 223", ) # CMD_TURN_ON_ANDROIDTV self.assertCommand( constants.CMD_TURN_ON_ANDROIDTV, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') || input keyevent 26", + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON') || input keyevent 26", ) # CMD_TURN_ON_FIRETV self.assertCommand( constants.CMD_TURN_ON_FIRETV, - r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true') || (input keyevent 26 && input keyevent 3)", + r"(dumpsys power | grep 'Display Power' | grep -q 'state=ON' || dumpsys power | grep -q 'mScreenOn=true' || dumpsys display | grep -q 'mScreenState=ON') || (input keyevent 26 && input keyevent 3)", ) # CMD_VERSION From 127ce0580da0309e04fe56cc1da7f629ac4fdfca Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 9 Dec 2022 19:59:11 -0800 Subject: [PATCH 42/66] Fix CI (#327) * Fix CI * Fix CI (attempt 2) --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index c18681c0..f89bf372 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 From 428e835ee14ff7f0f274513a2d386c9cc22ab6b5 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 9 Dec 2022 20:01:18 -0800 Subject: [PATCH 43/66] Bump the version to 0.0.70 (#326) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index d71de04e..a7f7a18c 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.69" +__version__ = "0.0.70" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 3b6cd8d9..fa602ff1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.69' +version = '0.0.70' # The full version, including alpha/beta/rc tags -release = '0.0.69' +release = '0.0.70' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index f3dbdd7e..e8f23a5a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.69", + version="0.0.70", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 5dcac19c6d248f89ea757c143cb290ac2bab1fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Drobni=C4=8D?= Date: Wed, 5 Apr 2023 04:50:51 +0200 Subject: [PATCH 44/66] Retry power command on error (#330) * Retry power command on error * Only retry when output is not none * Add test for power retrying * Add power retry to sync version * Reformat --- androidtv/basetv/basetv_async.py | 7 +++++++ androidtv/basetv/basetv_sync.py | 7 +++++++ tests/async_patchers.py | 13 ++++++++++++- tests/patchers.py | 13 ++++++++++++- tests/test_basetv_async.py | 5 +++++ tests/test_basetv_sync.py | 5 +++++ 6 files changed, 48 insertions(+), 2 deletions(-) diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index b9d5e288..e57198b7 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -377,6 +377,13 @@ async def screen_on_awake_wake_lock_size(self): """ output = await self._adb.shell(constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE) + # Power service might sometimes reply with "Failed to write while dumping service". If this happens, + # retry the request, up to three times. + retries_left = 3 + while output is not None and "Failed to write while dumping service" in output and retries_left > 0: + output = await self._adb.shell(constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE) + retries_left -= 1 + return self._screen_on_awake_wake_lock_size(output) async def stream_music_properties(self): diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index d6639d71..dfb230df 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -377,6 +377,13 @@ def screen_on_awake_wake_lock_size(self): """ output = self._adb.shell(constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE) + # Power service might sometimes reply with "Failed to write while dumping service". If this happens, + # retry the request, up to three times. + retries_left = 3 + while output is not None and "Failed to write while dumping service" in output and retries_left > 0: + output = self._adb.shell(constants.CMD_SCREEN_ON_AWAKE_WAKE_LOCK_SIZE) + retries_left -= 1 + return self._screen_on_awake_wake_lock_size(output) def stream_music_properties(self): diff --git a/tests/async_patchers.py b/tests/async_patchers.py index 637ddd5e..3ac715f2 100644 --- a/tests/async_patchers.py +++ b/tests/async_patchers.py @@ -124,10 +124,21 @@ async def connect_fail_python(self, *args, **kwargs): def patch_shell(response=None, error=False): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" + if isinstance(response, list): + response_list = response + else: + response_list = [response] + + next_response = 0 + async def shell_success(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful.""" self.shell_cmd = cmd - return response + nonlocal next_response + nonlocal response_list + res = response_list[next_response] + next_response = (next_response + 1) % len(response_list) + return res async def shell_fail_python(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` method when it fails.""" diff --git a/tests/patchers.py b/tests/patchers.py index f9d2e1de..021bdfdf 100644 --- a/tests/patchers.py +++ b/tests/patchers.py @@ -116,10 +116,21 @@ def connect_fail_python(self, *args, **kwargs): def patch_shell(response=None, error=False): """Mock the `AdbDeviceTcpFake.shell` and `DeviceFake.shell` methods.""" + if isinstance(response, list): + response_list = response + else: + response_list = [response] + + next_response = 0 + def shell_success(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpFake.shell` and `DeviceFake.shell` methods when they are successful.""" self.shell_cmd = cmd - return response + nonlocal next_response + nonlocal response_list + res = response_list[next_response] + next_response = (next_response + 1) % len(response_list) + return res def shell_fail_python(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpFake.shell` method when it fails.""" diff --git a/tests/test_basetv_async.py b/tests/test_basetv_async.py index 1949d92d..96f60f88 100644 --- a/tests/test_basetv_async.py +++ b/tests/test_basetv_async.py @@ -421,6 +421,11 @@ async def test_screen_on_awake_wake_lock_size(self): with async_patchers.patch_shell("11Wake Locks: size=2")[self.PATCH_KEY]: self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) + with async_patchers.patch_shell( + ["Failed to write while dumping serviceWake Locks: size=2", "11Wake Locks: size=2"] + )[self.PATCH_KEY]: + self.assertTupleEqual(await self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) + @awaiter async def test_wake_lock_size(self): """Check that the ``wake_lock_size`` property works correctly.""" diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index a0f7d219..d8babddd 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -632,6 +632,11 @@ def test_screen_on_awake_wake_lock_size(self): with patchers.patch_shell("11Wake Locks: size=2")[self.PATCH_KEY]: self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) + with patchers.patch_shell(["Failed to write while dumping serviceWake Locks: size=2", "11Wake Locks: size=2"])[ + self.PATCH_KEY + ]: + self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) + def test_state_detection_rules_validator(self): """Check that the ``state_detection_rules_validator`` function works correctly.""" with patchers.patch_connect(True)["python"], patchers.patch_shell("")["python"]: From 610d256d1b40b35d73e446ceb2c006aa21ca1d92 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 5 Apr 2023 04:51:14 +0200 Subject: [PATCH 45/66] added emby player and magenta tv (#331) * Update constants.py added Emby and Magenta TV (from deutsche Telekom) * Update constants.py fixed missing semicolon * Update constants.py fixed alphabetical order... --- androidtv/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/androidtv/constants.py b/androidtv/constants.py index 175e4d81..ef4c12e3 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -406,6 +406,7 @@ class DeviceEnum(IntEnum): APP_DISNEY_PLUS_HOTSTAR = "in.startv.hotstar" APP_DS_PHOTO = "com.synology.dsphoto" APP_DS_VIDEO = "com.synology.dsvideo" +APP_EMBY = "tv.emby.embyatv" APP_ES_FILE_EXPLORER = "com.estrongs.android.pop" APP_FACEBOOK = "com.facebook.katana" APP_FAWESOME = "com.future.moviesByFawesomeAndroidTV" @@ -431,6 +432,7 @@ class DeviceEnum(IntEnum): APP_JIO_CINEMA = "com.jio.media.stb.ondemand" APP_KODI = "org.xbmc.kodi" APP_LIVE_CHANNELS = "com.google.android.tv" +APP_MAGENTA_TV = "de.telekom.magentatv.atv" APP_MIJN_RADIO = "org.samsonsen.nederlandse.radio.holland.nl" APP_MOLOTOV = "tv.molotov.app" APP_MRMC = "tv.mrmc.mrmc" @@ -508,6 +510,7 @@ class DeviceEnum(IntEnum): APP_DISNEY_PLUS_HOTSTAR: "Disney+ Hotstar", APP_DS_PHOTO: "DS photo", APP_DS_VIDEO: "DS video", + APP_EMBY: "Emby", APP_ES_FILE_EXPLORER: "ES File Explorer", APP_FACEBOOK: "Facebook Watch", APP_FAWESOME: "Fawsome", @@ -531,6 +534,7 @@ class DeviceEnum(IntEnum): APP_JIO_CINEMA: "Jio Cinema", APP_KODI: "Kodi", APP_LIVE_CHANNELS: "Live Channels", + APP_MAGENTA_TV: "Magenta TV", APP_MIJN_RADIO: "Mijn Radio", APP_MOLOTOV: "Molotov", APP_MRMC: "MrMC", From 3b9f7cbabab0049d21c9ddf6c5bcc49d7c415789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Drobni=C4=8D?= Date: Wed, 5 Apr 2023 04:53:03 +0200 Subject: [PATCH 46/66] also detect focused window by mInputMethodInputTarget (#329) * also detect focused window by mInputMethodInputTarget * Fix constant tests --- androidtv/constants.py | 2 +- tests/test_constants.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/constants.py b/androidtv/constants.py index ef4c12e3..05292539 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -96,7 +96,7 @@ class DeviceEnum(IntEnum): ) #: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 11 device CMD_DEFINE_CURRENT_APP_VARIABLE11 = ( - "CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && " + CMD_PARSE_CURRENT_APP11 + "CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && " + CMD_PARSE_CURRENT_APP11 ) diff --git a/tests/test_constants.py b/tests/test_constants.py index 5f3fb45d..8093b012 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -86,7 +86,7 @@ def test_constants(self): # CMD_CURRENT_APP11 self.assertCommand( constants.CMD_CURRENT_APP11, - r"CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", ) # CMD_CURRENT_APP_GOOGLE_TV @@ -104,7 +104,7 @@ def test_constants(self): # CMD_CURRENT_APP_MEDIA_SESSION_STATE11 self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, - r"CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV @@ -143,7 +143,7 @@ def test_constants(self): # CMD_LAUNCH_APP11 self.assertCommand( constants.CMD_LAUNCH_APP11, - r"CURRENT_APP=$(dumpsys window windows | grep 'mInputMethodTarget') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_LAUNCH_APP_FIRETV From f1272e0dc3c4c07b080364b6664b73febaf04727 Mon Sep 17 00:00:00 2001 From: sergeknystautas Date: Wed, 19 Jul 2023 20:53:44 -0700 Subject: [PATCH 47/66] Adding HBO, Hulu, Peacock, and Paramount+ (#328) --- androidtv/constants.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/androidtv/constants.py b/androidtv/constants.py index 05292539..c15d95e6 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -422,8 +422,10 @@ class DeviceEnum(IntEnum): APP_HAYSTACK_NEWS = "com.haystack.android" APP_HBO_GO = "eu.hbogo.androidtv.production" APP_HBO_GO_2 = "com.HBO" +APP_HBO_MAX = "com.hbo.hbonow" APP_HOICHOI = "com.viewlift.hoichoi" APP_HULU = "com.hulu.plus" +APP_HULU_2 = "com.hulu.livingroomplus" APP_HUNGAMA_PLAY = "com.hungama.movies.tv" APP_IMDB_TV = "com.amazon.imdb.tv.android.app" APP_IPTV = "ru.iptvremote.android.iptv" @@ -443,6 +445,8 @@ class DeviceEnum(IntEnum): APP_NOS = "nl.nos.app" APP_NPO = "nl.uitzendinggemist" APP_OCS = "com.orange.ocsgo" +APP_PARAMOUNT_PLUS = "com.cbs.ott" +APP_PEACOCK = "com.peacocktv.peacockandroid" APP_PLAY_GAMES = "com.google.android.play.games" APP_PLAY_MUSIC = "com.google.android.music" APP_PLAY_STORE = "com.android.vending" @@ -524,8 +528,10 @@ class DeviceEnum(IntEnum): APP_HAYSTACK_NEWS: "Haystack News", APP_HBO_GO: "HBO GO", APP_HBO_GO_2: "HBO GO (2)", + APP_HBO_MAX: "HBO Max", APP_HOICHOI: "Hoichoi", APP_HULU: "Hulu", + APP_HULU_2: "Hulu (2)", APP_HUNGAMA_PLAY: "Hungama Play", APP_IMDB_TV: "IMDb TV", APP_IPTV: "IPTV", @@ -545,6 +551,8 @@ class DeviceEnum(IntEnum): APP_NOS: "NOS", APP_NPO: "NPO", APP_OCS: "OCS", + APP_PARAMOUNT_PLUS: "Paramount+", + APP_PEACOCK: "Peacock", APP_PLAY_GAMES: "Play Games", APP_PLAY_MUSIC: "Play Music", APP_PLAY_STORE: "Play Store", From 7c1b752fc0bf368ac68c04518a102f148b3d9df3 Mon Sep 17 00:00:00 2001 From: Kevin <36297312+kevin-kraus@users.noreply.github.com> Date: Thu, 20 Jul 2023 05:54:24 +0200 Subject: [PATCH 48/66] add support for RTL PLUS (formerly TVNOW) (#335) * add support for TVNOW (formerly RTL Plus) * fix linting errors * fix wrong constant * fix tests * change naming to RTL Plus --- androidtv/constants.py | 2 ++ androidtv/firetv/base_firetv.py | 11 +++++++++++ tests/test_firetv_sync.py | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/androidtv/constants.py b/androidtv/constants.py index c15d95e6..320a0df1 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -478,6 +478,7 @@ class DeviceEnum(IntEnum): APP_TED = "com.ted.android.tv" APP_TUNEIN = "tunein.player" APP_TVHEADEND = "de.cyberdream.dreamepg.tvh.tv.player" +APP_TVNOW = "de.cbc.tvnow.firetv" APP_TWITCH = "tv.twitch.android.app" APP_TWITCH_FIRETV = "tv.twitch.android.viewer" APP_VEVO = "com.vevo.tv" @@ -584,6 +585,7 @@ class DeviceEnum(IntEnum): APP_TED: "TED", APP_TUNEIN: "TuneIn Radio", APP_TVHEADEND: "DreamPlayer TVHeadend", + APP_TVNOW: "RTL Plus", APP_TWITCH: "Twitch", APP_TWITCH_FIRETV: "Twitch (FireTV)", APP_VEVO: "Vevo", diff --git a/androidtv/firetv/base_firetv.py b/androidtv/firetv/base_firetv.py index 2b9eb6bf..285ce8f5 100644 --- a/androidtv/firetv/base_firetv.py +++ b/androidtv/firetv/base_firetv.py @@ -178,6 +178,17 @@ def _update(self, screen_on, awake, wake_lock_size, current_app, media_session_s else: state = constants.STATE_IDLE + # RTL Plus (Germany) + elif current_app == constants.APP_TVNOW: + if wake_lock_size == 3: + state = constants.STATE_PAUSED + elif wake_lock_size == 4: + state = constants.STATE_PLAYING + elif wake_lock_size == 5: + state = constants.STATE_PLAYING + else: + state = constants.STATE_IDLE + # Twitch elif current_app == constants.APP_TWITCH_FIRETV: if wake_lock_size == 2: diff --git a/tests/test_firetv_sync.py b/tests/test_firetv_sync.py index 9a68566d..4faa16c8 100644 --- a/tests/test_firetv_sync.py +++ b/tests/test_firetv_sync.py @@ -326,6 +326,27 @@ def test_state_detection(self): (constants.STATE_IDLE, constants.APP_SPOTIFY, [constants.APP_SPOTIFY], None), ) + # RTL Plus (Germany) + self.assertUpdate( + [True, True, 3, constants.APP_TVNOW, 1, [constants.APP_TVNOW], None], + (constants.STATE_PAUSED, constants.APP_TVNOW, [constants.APP_TVNOW], None), + ) + + self.assertUpdate( + [True, True, 4, constants.APP_TVNOW, 1, [constants.APP_TVNOW], None], + (constants.STATE_PLAYING, constants.APP_TVNOW, [constants.APP_TVNOW], None), + ) + + self.assertUpdate( + [True, True, 5, constants.APP_TVNOW, 1, [constants.APP_TVNOW], None], + (constants.STATE_PLAYING, constants.APP_TVNOW, [constants.APP_TVNOW], None), + ) + + self.assertUpdate( + [True, True, 6, constants.APP_TVNOW, 1, [constants.APP_TVNOW], None], + (constants.STATE_IDLE, constants.APP_TVNOW, [constants.APP_TVNOW], None), + ) + # Twitch self.assertUpdate( [True, True, 2, constants.APP_TWITCH_FIRETV, 3, [constants.APP_TWITCH_FIRETV], None], From 0994182a40dd6992ef495a18863b1d722d6b173f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 19 Jul 2023 20:58:46 -0700 Subject: [PATCH 49/66] Bump the version to 0.0.71 (#336) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index a7f7a18c..ba6f9afb 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.70" +__version__ = "0.0.71" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index fa602ff1..873bbd42 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.70' +version = '0.0.71' # The full version, including alpha/beta/rc tags -release = '0.0.70' +release = '0.0.71' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index e8f23a5a..60231405 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.70", + version="0.0.71", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From a7b1e51749802f797f5a57d44643cbb5232ea398 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Aug 2023 23:39:45 -0400 Subject: [PATCH 50/66] Switch usage of asyncio.wait_for to async_timeout (#337) * Switch usage of asyncio.wait_for to async_timeout `asyncio.wait_for` creates another tasks which leads to some race conditions in cancelation and a performance hit cpython 3.12 will change the underlying implementation of `asyncio.wait_for` to use `asyncio.wait` but that is still a long way off for many people: https://github.com/python/cpython/pull/98518 * adjust ci --- .github/workflows/python-package.yml | 2 +- androidtv/adb_manager/adb_manager_async.py | 4 +++- setup.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f89bf372..e4c25bfb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -30,7 +30,7 @@ jobs: if python --version 2>&1 | grep -q "Python 2"; then pip install mock rsa==4.0; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install . - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install aiofiles adb-shell[usb]; fi + if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install aiofiles async_timeout adb-shell[usb]; fi - name: Check formatting with black run: | if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install black && make lint-black; fi diff --git a/androidtv/adb_manager/adb_manager_async.py b/androidtv/adb_manager/adb_manager_async.py index e3c63d6f..7705a3f6 100644 --- a/androidtv/adb_manager/adb_manager_async.py +++ b/androidtv/adb_manager/adb_manager_async.py @@ -15,6 +15,7 @@ from adb_shell.auth.sign_pythonrsa import PythonRSASigner from adb_shell.constants import DEFAULT_PUSH_MODE, DEFAULT_READ_TIMEOUT_S import aiofiles +import async_timeout from ppadb.client import Client from ..constants import ( @@ -164,7 +165,8 @@ async def _acquire(lock, timeout=DEFAULT_LOCK_TIMEOUT_S): try: acquired = False try: - acquired = await asyncio.wait_for(lock.acquire(), timeout) + async with async_timeout.timeout(timeout): + acquired = await lock.acquire() if not acquired: raise LockNotAcquiredException yield acquired diff --git a/setup.py b/setup.py index 60231405..7900b688 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ author_email="jefflirion@users.noreply.github.com", packages=["androidtv", "androidtv.adb_manager", "androidtv.basetv", "androidtv.androidtv", "androidtv.firetv"], install_requires=["adb-shell>=0.4.0", "pure-python-adb>=0.3.0.dev0"], - extras_require={"async": ["aiofiles>=0.4.0"], "usb": ["adb-shell[usb]>=0.4.0"]}, + extras_require={"async": ["aiofiles>=0.4.0", "async_timeout>=3.0.0"], "usb": ["adb-shell[usb]>=0.4.0"]}, classifiers=[ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", From 94a3f01ce8246e051b4695b981093eabd6f1681f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 31 Aug 2023 20:49:18 -0700 Subject: [PATCH 51/66] Bump the version to 0.0.72 (#338) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index ba6f9afb..77a1f995 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.71" +__version__ = "0.0.72" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 873bbd42..567bb9d5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.71' +version = '0.0.72' # The full version, including alpha/beta/rc tags -release = '0.0.71' +release = '0.0.72' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 7900b688..0c16ddbf 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="androidtv", - version="0.0.71", + version="0.0.72", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From a74fa095c9e0d9a9aedab13dd4ff17b07fd29b4c Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 17 Oct 2023 20:44:42 -0700 Subject: [PATCH 52/66] More useful Makefile (#341) * More useful Makefile * Support async code for Python 3.1* * Add release target to Makefile * Run flake8 and pylint from venv/ in pre-commit.sh * Update python-package.yml * Add adb-shell[async] to venv_requirements.txt --- .github/workflows/python-package.yml | 22 +-- Makefile | 225 ++++++++++++++++++++++----- scripts/pre-commit.sh | 71 +++++++++ setup.py | 2 + tests/test_adb_manager_async_temp.py | 14 +- venv_requirements.txt | 12 ++ 6 files changed, 291 insertions(+), 55 deletions(-) create mode 100755 scripts/pre-commit.sh create mode 100644 venv_requirements.txt diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e4c25bfb..bf40e5ba 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,6 +9,9 @@ on: pull_request: branches: [ master ] +env: + ENV_GITHUB_ACTIONS: 'ENV_GITHUB_ACTIONS' + jobs: build: @@ -26,22 +29,13 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pylint coveralls - if python --version 2>&1 | grep -q "Python 2"; then pip install mock rsa==4.0; fi - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install . - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install aiofiles async_timeout adb-shell[usb]; fi - - name: Check formatting with black - run: | - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then pip install black && make lint-black; fi - - name: Lint with pylint and flake8 + make venv + - name: Linting checks with pylint, flake8, and black run: | - if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5" || python --version 2>&1 | grep -q "Python 3.6"; then flake8 androidtv/ --exclude="androidtv/setup_async.py,androidtv/basetv/basetv_async.py,androidtv/androidtv/androidtv_async.py,androidtv/firetv/firetv_async.py,androidtv/adb_manager/adb_manager_async.py" && pylint --ignore="setup_async.py,basetv_async.py,androidtv_async.py,firetv_async.py,adb_manager_async.py" androidtv/; fi - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9" || python --version 2>&1 | grep -q "Python 3.10"; then flake8 androidtv/ && pylint androidtv/; fi - - name: Test with unittest + make lint + - name: Test with pytest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_SERVICE_NAME: github run: | - if python --version 2>&1 | grep -q "Python 2" || python --version 2>&1 | grep -q "Python 3.5" || python --version 2>&1 | grep -q "Python 3.6" ; then for synctest in $(cd tests && ls test*.py | grep -v async); do python -m unittest discover -s tests/ -t . -p "$synctest" || exit 1; done; fi - if python --version 2>&1 | grep -q "Python 3.7" || python --version 2>&1 | grep -q "Python 3.8" || python --version 2>&1 | grep -q "Python 3.9"|| python --version 2>&1 | grep -q "Python 3.10"; then coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m --fail-under 100 && coveralls; fi + make coverage && coveralls diff --git a/Makefile b/Makefile index 21369688..d18a86e7 100644 --- a/Makefile +++ b/Makefile @@ -1,48 +1,199 @@ -.PHONY: release -release: - rm -rf dist - rm -rf build - scripts/git_tag.sh - python setup.py sdist bdist_wheel - twine upload dist/* +#-------------------- ONLY MODIFY CODE IN THIS SECTION --------------------# +PACKAGE_DIR := androidtv +TEST_DIR := tests +DOCS_DIR := docs -.PHONY: docs -docs: - @cd docs/source && rm -f androidtv*.rst - @cd docs && sphinx-apidoc -f -e -o source/ ../androidtv/ - @cd docs && make html && make html +# Change to false if you don't want to use pytest +USE_PYTEST := true -SYNCTESTS := $(shell cd tests && ls test*.py | grep -v async) +# Change this to false if you don't want to run linting checks on the tests +LINT_TEST_DIR := false +#-------------------- DO NOT MODIFY CODE BELOW!!!!!!!! --------------------# -.PHONY: test -test: - python --version 2>&1 | grep -q "Python 2" && (for synctest in $(SYNCTESTS); do python -m unittest discover -s tests/ -t . -p "$$synctest"; done) || true - python --version 2>&1 | grep -q "Python 3" && python -m unittest discover -s tests/ -t . || true +export PATH := $(abspath venv)/bin:${PATH} -.PHONY: coverage -coverage: - coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage html && coverage report -m +# Whether to include "*_async.py" files +INCLUDE_ASYNC = $(shell python --version | grep -q "Python 3.[7891]" && echo "true" || echo "false") -.PHONY: tdd -tdd: - coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m +# Async vs. Sync files +PACKAGE_ASYNC_FILES = $(shell ls -m $(PACKAGE_DIR)/*_async.py 2>/dev/null) +TEST_ASYNC_FILES = $(shell ls -m $(TEST_DIR)/*_async.py 2>/dev/null) +TEST_SYNC_FILES = $(shell cd $(TEST_DIR) && ls test*.py | grep -v async) -.PHONY: lint -lint: - flake8 androidtv/ && pylint androidtv/ +# Target prerequisites that may or may not exist +VENV_REQUIREMENTS_TXT := $(wildcard venv_requirements.txt) +SETUP_PY := $(wildcard setup.py) + +# A prerequisite for forcing targets to run +FORCE: + +# Help! +help: ## Show this help menu + @echo "\n\033[1mUsage:\033[0m"; \ + awk -F ':|##' '/^[^\t].+?:.*?##/ { printf "\033[36m make %-20s\033[0m %s\n", $$1, $$NF }' $(MAKEFILE_LIST) | grep -v "make venv/\." | sort + @echo "" + @echo "NOTES:" + @echo "- The 'venv/.bin' target may fail because newer Python versions include the 'venv' package. Follow the instructions to create the virtual environment manually." +ifneq ("$(wildcard scripts/pre-commit.sh)", "") + @echo "- To install the git pre-commit hook:\n\n scripts/pre-commit.sh\n" +endif + @echo "- You may need to activate the virtual environment prior to running any Make commands:\n\n source venv/bin/activate\n" + +# Virtual environment targets +.PHONY: clean-venv +clean-venv: ## Remove the virtual environment + rm -rf venv + +venv: venv/.bin venv/.requirements venv/.setup .git/hooks/pre-commit ## Create the virtual environment and install all necessary packages + +venv/.bin: ## Create the virtual environment + if [ -z "$$ENV_GITHUB_ACTIONS" ]; then \ + echo -e "If this target fails, you can perform this action manually via:\n\n make clean-venv && python3 -m venv venv && source venv/bin/activate && pip install -U setuptools && echo -e '*.*\n**/' > venv/.gitignore && touch venv/.bin\n\n"; \ + apt list -a --installed python3-venv 2>&1 | grep -q installed || (sudo apt update && sudo apt install python3-venv); \ + python3 -m venv venv; \ + pip install -U setuptools; \ + fi + mkdir -p venv + echo '*.*\n**/' > venv/.gitignore + touch venv/.bin + +venv/.requirements: venv/.bin $(VENV_REQUIREMENTS_TXT) ## Install the requirements from 'venv_requirements.txt' in the virtual environment +ifneq ("$(wildcard venv_requirements.txt)", "") + pip install -U -r venv_requirements.txt +endif + touch venv/.requirements + +# Install the package in the virtual environment +venv/.setup: venv/.bin $(SETUP_PY) +ifneq ("$(wildcard setup.py)", "") + pip install . +endif + touch venv/.setup + +.PHONY: uninstall +uninstall: + rm -f venv/.setup + +.PHONY: install +install: uninstall venv/.setup ## Install the package in the virtual environment + +# Create the pre-commit hook +.git/hooks/pre-commit: + ./scripts/pre-commit.sh MAKE_PRECOMMIT_HOOK + +.PHONY: pre-commit +pre-commit: .git/hooks/pre-commit ## Create the pre-commit hook + +# Linting and code analysis .PHONY: black -black: - black --safe --line-length 120 --target-version py35 androidtv - black --safe --line-length 120 --target-version py35 tests +black: venv ## Format the code using black + black --safe --line-length 120 --target-version py35 $(PACKAGE_DIR) + black --safe --line-length 120 --target-version py35 $(TEST_DIR) +ifneq ("$(wildcard setup.py)", "") black --safe --line-length 120 --target-version py35 setup.py +endif .PHONY: lint-black -lint-black: - black --check --safe --line-length 120 --target-version py35 androidtv - black --check --safe --line-length 120 --target-version py35 tests - black --check --safe --line-length 120 --target-version py35 setup.py - -.PHONY: alltests -alltests: - flake8 androidtv/ && pylint androidtv/ && coverage run --source androidtv -m unittest discover -s tests/ -t . && coverage report -m +lint-black: venv ## Check that the code is formatted using black + black --check --line-length 120 --safe --target-version py35 $(PACKAGE_DIR) + black --check --line-length 120 --safe --target-version py35 $(TEST_DIR) +ifneq ("$(wildcard setup.py)", "") + black --check --line-length 120 --safe --target-version py35 setup.py +endif + +.PHONY: lint-flake8 +lint-flake8: venv ## Check the code using flake8 +ifeq ($(INCLUDE_ASYNC), true) + flake8 $(PACKAGE_DIR) +ifeq ($(LINT_TEST_DIR), true) + flake8 $(TEST_DIR) +endif +else + flake8 $(PACKAGE_DIR) --exclude="$(PACKAGE_ASYNC_FILES)" +ifeq ($(LINT_TEST_DIR), true) + flake8 $(TEST_DIR) --exclude="$(TEST_ASYNC_FILES)" +endif +endif +ifneq ("$(wildcard setup.py)", "") + flake8 setup.py +endif + +.PHONY: lint-pylint +lint-pylint: venv ## Check the code using pylint +ifeq ($(INCLUDE_ASYNC), true) + pylint $(PACKAGE_DIR) +ifeq ($(LINT_TEST_DIR), true) + pylint $(TEST_DIR) +endif +else + pylint $(PACKAGE_DIR) --ignore="$(PACKAGE_ASYNC_FILES)" +ifeq ($(LINT_TEST_DIR), true) + pylint $(TEST_DIR) --ignore="$(TEST_ASYNC_FILES)" +endif +endif +ifneq ("$(wildcard setup.py)", "") + pylint setup.py +endif + +.PHONY: lint +lint: lint-black lint-flake8 lint-pylint ## Run all linting checks on the code + + +# Testing and coverage. +.PHONY: test +test: venv ## Run the unit tests +ifeq ($(INCLUDE_ASYNC), true) +ifeq ($(USE_PYTEST), true) + pytest $(TEST_DIR) +else + python -m unittest discover -s $(TEST_DIR)/ -t . +endif +else +ifeq ($(USE_PYTEST), true) + pytest $(TEST_DIR) --ignore-glob="*async.py" +else + for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && python -m unittest "$(TEST_DIR)/$$synctest"; done +endif +endif + +.PHONY: coverage +coverage: venv ## Run the unit tests and produce coverage info +ifeq ($(INCLUDE_ASYNC), true) +ifeq ($(USE_PYTEST), true) + coverage run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ && coverage report -m +else + coverage run --source $(PACKAGE_DIR) -m unittest discover -s $(TEST_DIR) -t . && coverage report -m +endif +else +ifeq ($(USE_PYTEST), true) + coverage run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ --ignore-glob="*async.py" && coverage report -m +else + for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && coverage run --source $(PACKAGE_DIR) -m unittest "$(TEST_DIR)/$$synctest"; done + coverage report -m +endif +endif + +.PHONY: htmlcov +htmlcov: coverage ## Produce a coverage report + coverage html + + +# Documentation +.PHONY: docs +docs: venv ## Build the documentation + rm -rf $(DOCS_DIR)/build + @cd $(DOCS_DIR) && sphinx-apidoc -f -e -o source/ $(CURDIR)/$(PACKAGE_DIR)/ + @cd $(DOCS_DIR) && make html && make html + + +.PHONY: release +release: ## Make a release and upload it to pypi + rm -rf dist + scripts/git_tag.sh + python setup.py sdist bdist_wheel + twine upload dist/* + + +.PHONY: all +all: lint htmlcov ## Run all linting checks and unit tests and produce a coverage report diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh new file mode 100755 index 00000000..2f6b53c4 --- /dev/null +++ b/scripts/pre-commit.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +set -e + +function make_pre_commit() { + # setup pre-commit hook + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + echo -e "#!/bin/bash\n\n./scripts/pre-commit.sh 'placeholder_argument'" > "$DIR/../.git/hooks/pre-commit" + chmod a+x "$DIR/../.git/hooks/pre-commit" + echo "pre-commit hook successfully configured" +} + +# if no arguments are passed, create the pre-commit hook +if [ "$#" -eq 0 ]; then + read -p "Do you want to setup the git pre-commit hook? [Y/n] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + make_pre_commit + else + echo "pre-commit hook not configured" + fi + exit 0 +fi + +# if the argument passed is "MAKE_PRECOMMIT_HOOK", then make the pre-commit hook +if [[ $1 == "MAKE_PRECOMMIT_HOOK" ]]; then + make_pre_commit + exit 0 +fi + +# THE PRE-COMMIT HOOK + +# get the directory of this script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +( + cd "$DIR/.." + + no_unstaged_changes=true + echo -e "\n\033[1m1. Checking for unstaged changes...\033[0m" + for staged_file in $(git diff --name-only --cached); do + git diff --name-only | grep -q "${staged_file}" && echo "You have unstaged changes in '${staged_file}'" && no_unstaged_changes=false || true + done + + # modified .py files + pyfiles=$(git diff --cached --name-only -- '*.py') + + # flake8 + flake8_pass=true + if [ "$pyfiles" != "" ]; then + echo -e "\n\033[1m2. Running flake8...\033[0m" + venv/bin/flake8 $pyfiles || flake8_pass=false + else + echo -e "\n\033[1m2. Skipping flake8.\033[0m" + fi + + # pylint + pylint_pass=true + if [ "$pyfiles" != "" ]; then + echo -e "\n\033[1m3. Running pylint...\033[0m" + venv/bin/pylint $pyfiles || pylint_pass=false + else + echo -e "\n\033[1m3. Skipping pylint.\033[0m\n" + fi + + if [ "$flake8_pass" != "true" ] || [ "$pylint_pass" != "true" ] || [ "$no_unstaged_changes" != "true" ]; then + echo -e "\033[1m\033[31mSome checks failed.\033[0m\n\n NOT RECOMMENDED: If you want to skip the pre-commit hook, use the --no-verify flag.\n" + exit 1 + fi + echo -e "\033[1m\033[32mAll checks passed.\033[0m\n" +) diff --git a/setup.py b/setup.py index 0c16ddbf..bb8a69d1 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +"""setup.py file for the androidtv package.""" + from setuptools import setup with open("README.rst") as f: diff --git a/tests/test_adb_manager_async_temp.py b/tests/test_adb_manager_async_temp.py index b67782d3..dd5c341b 100644 --- a/tests/test_adb_manager_async_temp.py +++ b/tests/test_adb_manager_async_temp.py @@ -1,10 +1,12 @@ -import sys +"""Tests for the ClientAsync and AdbDeviceUsbAsync classes.""" + import unittest from unittest.mock import patch -sys.path.insert(0, "..") - -from adb_shell.transport.usb_transport import UsbTransport +try: + from adb_shell.transport.usb_transport import UsbTransport +except (ImportError, OSError): + UsbTransport = None from androidtv.adb_manager.adb_manager_async import AdbDeviceUsbAsync, ClientAsync @@ -21,6 +23,7 @@ class TestAsyncClientDevice(unittest.TestCase): @awaiter async def test_async_client_device(self): + """Test the ClientAsync class.""" with patch("androidtv.adb_manager.adb_manager_async.Client", patchers.ClientFakeSuccess): client = ClientAsync("host", "port") @@ -40,6 +43,7 @@ async def test_async_client_device(self): @awaiter async def test_async_client_device_fail(self): + """Test the ClientAsync class when it fails.""" with patch("androidtv.adb_manager.adb_manager_async.Client", patchers.ClientFakeFail): client = ClientAsync("host", "port") @@ -48,6 +52,7 @@ async def test_async_client_device_fail(self): self.assertFalse(device) +@unittest.skipIf(UsbTransport is None, "UsbTransport cannot be imported") class TestAsyncUsb(unittest.TestCase): """Test the ``AdbDeviceUsbAsync`` class defined in ``adb_manager_async.py``. @@ -57,6 +62,7 @@ class TestAsyncUsb(unittest.TestCase): @awaiter async def test_async_usb(self): + """Test the AdbDeviceUsbAsync class.""" with patch("adb_shell.adb_device.UsbTransport.find_adb", return_value=UsbTransport("device", "setting")): device = AdbDeviceUsbAsync() diff --git a/venv_requirements.txt b/venv_requirements.txt new file mode 100644 index 00000000..836a04b3 --- /dev/null +++ b/venv_requirements.txt @@ -0,0 +1,12 @@ +adb-shell[async,usb] +aiofiles +black +coverage +coveralls +flake8 +pylint +pytest +pyyaml +sphinx +sphinx-rtd-theme +tk From a06ac9868a4670a9dcad07cc121e84fca77b9a90 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 18 Oct 2023 21:33:48 -0700 Subject: [PATCH 53/66] More Makefile improvements (#342) --- Makefile | 97 +++++++++++++++++++++++++++---------------- venv_requirements.txt | 10 +++-- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index d18a86e7..9c347328 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,19 @@ LINT_TEST_DIR := false export PATH := $(abspath venv)/bin:${PATH} +# Binaries to run +BLACK := $(abspath venv)/bin/black +COVERAGE := $(abspath venv)/bin/coverage +FLAKE8 := $(abspath venv)/bin/flake8 +PIP := $(abspath venv)/bin/pip3 +PYLINT := $(abspath venv)/bin/pylint +PYTEST := $(abspath venv)/bin/pytest +PYTHON := $(abspath venv)/bin/python +SPHINX_APIDOC := $(abspath venv)/bin/sphinx-apidoc +TWINE := $(abspath venv)/bin/twine + # Whether to include "*_async.py" files -INCLUDE_ASYNC = $(shell python --version | grep -q "Python 3.[7891]" && echo "true" || echo "false") +INCLUDE_ASYNC = $(shell $(PYTHON) --version | grep -q "Python 3.[7891]" && echo "true" || echo "false") # Async vs. Sync files PACKAGE_ASYNC_FILES = $(shell ls -m $(PACKAGE_DIR)/*_async.py 2>/dev/null) @@ -24,6 +35,7 @@ TEST_SYNC_FILES = $(shell cd $(TEST_DIR) && ls test*.py | grep -v async) VENV_REQUIREMENTS_TXT := $(wildcard venv_requirements.txt) SETUP_PY := $(wildcard setup.py) + # A prerequisite for forcing targets to run FORCE: @@ -49,25 +61,38 @@ venv: venv/.bin venv/.requirements venv/.setup .git/hooks/pre-commit ## Create venv/.bin: ## Create the virtual environment if [ -z "$$ENV_GITHUB_ACTIONS" ]; then \ - echo -e "If this target fails, you can perform this action manually via:\n\n make clean-venv && python3 -m venv venv && source venv/bin/activate && pip install -U setuptools && echo -e '*.*\n**/' > venv/.gitignore && touch venv/.bin\n\n"; \ - apt list -a --installed python3-venv 2>&1 | grep -q installed || (sudo apt update && sudo apt install python3-venv); \ + echo -e "If this target fails, you can perform this action manually via:\n\n make clean-venv && python3 -m venv venv && source venv/bin/activate && pip install -U setuptools && echo -e '*.*\\\n**/' > venv/.gitignore && touch venv/.bin\n\n"; \ + apt list -a --installed python3-venv 2>&1 | grep -q installed || sudo apt update && sudo apt install python3-venv; \ python3 -m venv venv; \ - pip install -U setuptools; \ + $(PIP) install -U setuptools; \ + else \ + mkdir -p venv/bin; \ + ln -s $$(which pip) $(PIP); \ + ln -s $$(which python) $(PYTHON); \ fi - mkdir -p venv + mkdir -p venv/bin echo '*.*\n**/' > venv/.gitignore touch venv/.bin venv/.requirements: venv/.bin $(VENV_REQUIREMENTS_TXT) ## Install the requirements from 'venv_requirements.txt' in the virtual environment ifneq ("$(wildcard venv_requirements.txt)", "") - pip install -U -r venv_requirements.txt + $(PIP) install -U -r venv_requirements.txt + if ! [ -z "$$ENV_GITHUB_ACTIONS" ]; then \ + ln -s $$(which black) $(BLACK); \ + ln -s $$(which coverage) $(COVERAGE); \ + ln -s $$(which flake8) $(FLAKE8); \ + ln -s $$(which pylint) $(PYLINT); \ + ln -s $$(which pytest) $(PYTEST); \ + ln -s $$(which sphinx-apidoc) $(SPHINX_APIDOC); \ + ln -s $$(which twine) $(TWINE); \ + fi endif touch venv/.requirements # Install the package in the virtual environment venv/.setup: venv/.bin $(SETUP_PY) ifneq ("$(wildcard setup.py)", "") - pip install . + $(PIP) install . endif touch venv/.setup @@ -88,52 +113,52 @@ pre-commit: .git/hooks/pre-commit ## Create the pre-commit hook # Linting and code analysis .PHONY: black black: venv ## Format the code using black - black --safe --line-length 120 --target-version py35 $(PACKAGE_DIR) - black --safe --line-length 120 --target-version py35 $(TEST_DIR) + $(BLACK) --safe --line-length 120 --target-version py35 $(PACKAGE_DIR) + $(BLACK) --safe --line-length 120 --target-version py35 $(TEST_DIR) ifneq ("$(wildcard setup.py)", "") - black --safe --line-length 120 --target-version py35 setup.py + $(BLACK) --safe --line-length 120 --target-version py35 setup.py endif .PHONY: lint-black lint-black: venv ## Check that the code is formatted using black - black --check --line-length 120 --safe --target-version py35 $(PACKAGE_DIR) - black --check --line-length 120 --safe --target-version py35 $(TEST_DIR) + $(BLACK) --check --line-length 120 --safe --target-version py35 $(PACKAGE_DIR) + $(BLACK) --check --line-length 120 --safe --target-version py35 $(TEST_DIR) ifneq ("$(wildcard setup.py)", "") - black --check --line-length 120 --safe --target-version py35 setup.py + $(BLACK) --check --line-length 120 --safe --target-version py35 setup.py endif .PHONY: lint-flake8 lint-flake8: venv ## Check the code using flake8 ifeq ($(INCLUDE_ASYNC), true) - flake8 $(PACKAGE_DIR) + $(FLAKE8) $(PACKAGE_DIR) ifeq ($(LINT_TEST_DIR), true) - flake8 $(TEST_DIR) + $(FLAKE8) $(TEST_DIR) endif else - flake8 $(PACKAGE_DIR) --exclude="$(PACKAGE_ASYNC_FILES)" + $(FLAKE8) $(PACKAGE_DIR) --exclude="$(PACKAGE_ASYNC_FILES)" ifeq ($(LINT_TEST_DIR), true) - flake8 $(TEST_DIR) --exclude="$(TEST_ASYNC_FILES)" + $(FLAKE8) $(TEST_DIR) --exclude="$(TEST_ASYNC_FILES)" endif endif ifneq ("$(wildcard setup.py)", "") - flake8 setup.py + $(FLAKE8) setup.py endif .PHONY: lint-pylint lint-pylint: venv ## Check the code using pylint ifeq ($(INCLUDE_ASYNC), true) - pylint $(PACKAGE_DIR) + $(PYLINT) $(PACKAGE_DIR) ifeq ($(LINT_TEST_DIR), true) - pylint $(TEST_DIR) + $(PYLINT) $(TEST_DIR) endif else - pylint $(PACKAGE_DIR) --ignore="$(PACKAGE_ASYNC_FILES)" + $(PYLINT) $(PACKAGE_DIR) --ignore="$(PACKAGE_ASYNC_FILES)" ifeq ($(LINT_TEST_DIR), true) - pylint $(TEST_DIR) --ignore="$(TEST_ASYNC_FILES)" + $(PYLINT) $(TEST_DIR) --ignore="$(TEST_ASYNC_FILES)" endif endif ifneq ("$(wildcard setup.py)", "") - pylint setup.py + $(PYLINT) setup.py endif .PHONY: lint @@ -145,15 +170,15 @@ lint: lint-black lint-flake8 lint-pylint ## Run all linting checks on the code test: venv ## Run the unit tests ifeq ($(INCLUDE_ASYNC), true) ifeq ($(USE_PYTEST), true) - pytest $(TEST_DIR) + $(PYTEST) $(TEST_DIR) else - python -m unittest discover -s $(TEST_DIR)/ -t . + $(PYTHON) -m unittest discover -s $(TEST_DIR)/ -t . endif else ifeq ($(USE_PYTEST), true) - pytest $(TEST_DIR) --ignore-glob="*async.py" + $(PYTEST) $(TEST_DIR) --ignore-glob="*async.py" else - for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && python -m unittest "$(TEST_DIR)/$$synctest"; done + for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && $(PYTHON) -m unittest "$(TEST_DIR)/$$synctest"; done endif endif @@ -161,29 +186,29 @@ endif coverage: venv ## Run the unit tests and produce coverage info ifeq ($(INCLUDE_ASYNC), true) ifeq ($(USE_PYTEST), true) - coverage run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ && coverage report -m + $(COVERAGE) run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ && $(COVERAGE) report -m else - coverage run --source $(PACKAGE_DIR) -m unittest discover -s $(TEST_DIR) -t . && coverage report -m + $(COVERAGE) run --source $(PACKAGE_DIR) -m unittest discover -s $(TEST_DIR) -t . && $(COVERAGE) report -m endif else ifeq ($(USE_PYTEST), true) - coverage run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ --ignore-glob="*async.py" && coverage report -m + $(COVERAGE) run --source $(PACKAGE_DIR) -m pytest $(TEST_DIR)/ --ignore-glob="*async.py" && $(COVERAGE) report -m else - for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && coverage run --source $(PACKAGE_DIR) -m unittest "$(TEST_DIR)/$$synctest"; done - coverage report -m + for synctest in $(TEST_SYNC_FILES); do echo "\033[1;32m$(TEST_DIR)/$$synctest\033[0m" && $(COVERAGE) run --source $(PACKAGE_DIR) -m unittest "$(TEST_DIR)/$$synctest"; done + $(COVERAGE) report -m endif endif .PHONY: htmlcov htmlcov: coverage ## Produce a coverage report - coverage html + $(COVERAGE) html # Documentation .PHONY: docs docs: venv ## Build the documentation rm -rf $(DOCS_DIR)/build - @cd $(DOCS_DIR) && sphinx-apidoc -f -e -o source/ $(CURDIR)/$(PACKAGE_DIR)/ + @cd $(DOCS_DIR) && $(SPHINX_APIDOC) -f -e -o source/ $(CURDIR)/$(PACKAGE_DIR)/ @cd $(DOCS_DIR) && make html && make html @@ -191,8 +216,8 @@ docs: venv ## Build the documentation release: ## Make a release and upload it to pypi rm -rf dist scripts/git_tag.sh - python setup.py sdist bdist_wheel - twine upload dist/* + $(PYTHON) setup.py sdist bdist_wheel + $(TWINE) upload dist/* .PHONY: all diff --git a/venv_requirements.txt b/venv_requirements.txt index 836a04b3..a11d971c 100644 --- a/venv_requirements.txt +++ b/venv_requirements.txt @@ -1,12 +1,14 @@ -adb-shell[async,usb] -aiofiles +# Standard requirements black coverage coveralls flake8 pylint pytest -pyyaml sphinx sphinx-rtd-theme -tk +twine + +# Specific requirements for this project +adb-shell[async,usb] +aiofiles From 4df518d41d51a9c35160ec5164e13314b80136a4 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 19 Oct 2023 20:03:27 -0700 Subject: [PATCH 54/66] Update scripts (#343) --- scripts/get_package_name.sh | 6 +++--- scripts/get_version.sh | 8 ++++---- scripts/git_tag.sh | 3 --- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/scripts/get_package_name.sh b/scripts/get_package_name.sh index ced6ccf6..ea213836 100755 --- a/scripts/get_package_name.sh +++ b/scripts/get_package_name.sh @@ -5,11 +5,11 @@ set -e # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -RSTRIP="\"*" -LSTRIP="*\"" +RSTRIP='"*' +LSTRIP='*"' # get the package name -PACKAGE_LINE=$(grep 'name=' $DIR/../setup.py || echo '') +PACKAGE_LINE=$(grep "name=" $DIR/../setup.py || echo '') PACKAGE_TEMP=${PACKAGE_LINE%$RSTRIP} PACKAGE=${PACKAGE_TEMP##$LSTRIP} diff --git a/scripts/get_version.sh b/scripts/get_version.sh index ae39c7f3..113c8eb5 100755 --- a/scripts/get_version.sh +++ b/scripts/get_version.sh @@ -5,15 +5,15 @@ set -e # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -RSTRIP="\"*" -LSTRIP="*\"" +RSTRIP='"*' +LSTRIP='*"' # get the package name PACKAGE=$($DIR/get_package_name.sh) # get the current version -VERSION_LINE=$(grep '__version__' "$DIR/../$PACKAGE/__init__.py" || echo '') -VERSION_TEMP=${VERSION_LINE%"\""} +VERSION_LINE=$(grep "__version__" "$DIR/../$PACKAGE/__init__.py" || echo '') +VERSION_TEMP=${VERSION_LINE%'"'} VERSION=${VERSION_TEMP##$LSTRIP} diff --git a/scripts/git_tag.sh b/scripts/git_tag.sh index c8c8d569..d79e29ee 100755 --- a/scripts/git_tag.sh +++ b/scripts/git_tag.sh @@ -3,9 +3,6 @@ # get the directory of this script DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# get the package name -PACKAGE=$($DIR/get_package_name.sh) - # get the current version VERSION=$($DIR/get_version.sh) From c1998f374545c09c29c5bb4dda8e62981925e798 Mon Sep 17 00:00:00 2001 From: Nortonko <52453201+Nortonko@users.noreply.github.com> Date: Fri, 20 Oct 2023 05:11:05 +0200 Subject: [PATCH 55/66] androidtv 12 & 13 (#340) * add support for androidtv 12 & 13 * reformated code by black formater * reformatted code by black again * fixed missing CMD_CURRENT_APP_MEDIA_SESSION_STATE12 * tried to fix the tests errors * fix missing CMD_VOLUME_SET_COMMAND11 * fixed errors * Stmts Miss * fix --- androidtv/basetv/basetv.py | 112 ++++++++- androidtv/basetv/basetv_async.py | 2 +- androidtv/constants.py | 48 +++- tests/generate_test_constants.py | 4 + tests/test_basetv_sync.py | 416 +++++++++++++++++++++++++------ tests/test_constants.py | 50 +++- 6 files changed, 542 insertions(+), 90 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 0f895890..8174d8ef 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -69,7 +69,14 @@ class BaseTV(object): # pylint: disable=too-few-public-methods DEVICE_ENUM = constants.DeviceEnum.BASETV def __init__( - self, adb, host, port=5555, adbkey="", adb_server_ip="", adb_server_port=5037, state_detection_rules=None + self, + adb, + host, + port=5555, + adbkey="", + adb_server_ip="", + adb_server_port=5037, + state_detection_rules=None, ): self._adb = adb self.host = host @@ -131,6 +138,14 @@ def _cmd_audio_state(self): # Is this an Android 11 device? if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": return constants.CMD_AUDIO_STATE11 + + # Is this an Android 12 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + return constants.CMD_AUDIO_STATE11 + + # Is this an Android 13 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + return constants.CMD_AUDIO_STATE11 return constants.CMD_AUDIO_STATE def _cmd_current_app(self): @@ -157,6 +172,14 @@ def _cmd_current_app(self): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": return constants.CMD_CURRENT_APP11 + # Is this an Android 12 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + return constants.CMD_CURRENT_APP12 + + # Is this an Android 13 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + return constants.CMD_CURRENT_APP13 + return constants.CMD_CURRENT_APP def _cmd_current_app_media_session_state(self): @@ -183,6 +206,14 @@ def _cmd_current_app_media_session_state(self): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11 + # Is this an Android 12 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE12 + + # Is this an Android 13 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE13 + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE def _cmd_hdmi_input(self): @@ -201,8 +232,39 @@ def _cmd_hdmi_input(self): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": return constants.CMD_HDMI_INPUT11 + # Is this an Android 12 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + return constants.CMD_HDMI_INPUT11 + + # Is this an Android 13 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + return constants.CMD_HDMI_INPUT11 + return constants.CMD_HDMI_INPUT + def _cmd_volume_set(self): + """Get the command used to set volume for this device. + + Returns + ------- + str + The device-specific ADB shell command used to set volume + + """ + # Is this an Android 11 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": + return constants.CMD_VOLUME_SET_COMMAND11 + + # Is this an Android 12 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + return constants.CMD_VOLUME_SET_COMMAND11 + + # Is this an Android 13 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + return constants.CMD_VOLUME_SET_COMMAND11 + + return constants.CMD_VOLUME_SET_COMMAND + def _cmd_launch_app(self, app): """Get the command to launch the specified app for this device. @@ -235,6 +297,14 @@ def _cmd_launch_app(self, app): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": return constants.CMD_LAUNCH_APP11.format(app) + # Is this an Android 12 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + return constants.CMD_LAUNCH_APP12.format(app) + + # Is this an Android 13 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + return constants.CMD_LAUNCH_APP13.format(app) + return constants.CMD_LAUNCH_APP.format(app) def _cmd_running_apps(self): @@ -339,7 +409,12 @@ def _parse_device_properties(self, properties): ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` """ - _LOGGER.debug("%s:%d `get_device_properties` response: %s", self.host, self.port, properties) + _LOGGER.debug( + "%s:%d `get_device_properties` response: %s", + self.host, + self.port, + properties, + ) if not properties: self.device_properties = {} @@ -353,7 +428,12 @@ def _parse_device_properties(self, properties): manufacturer, model, serialno, version = lines if not serialno.strip(): - _LOGGER.warning("Could not obtain serialno for %s:%d, got: '%s'", self.host, self.port, serialno) + _LOGGER.warning( + "Could not obtain serialno for %s:%d, got: '%s'", + self.host, + self.port, + serialno, + ) serialno = None self.device_properties = { @@ -393,7 +473,11 @@ def _parse_mac_address(mac_response): # # # ======================================================================= # def _custom_state_detection( - self, current_app=None, media_session_state=None, wake_lock_size=None, audio_state=None + self, + current_app=None, + media_session_state=None, + wake_lock_size=None, + audio_state=None, ): """Use the rules in ``self._state_detection_rules`` to determine the state. @@ -670,7 +754,11 @@ def _parse_stream_music(stream_music_raw): if not stream_music_raw: return None - matches = re.findall(constants.STREAM_MUSIC_REGEX_PATTERN, stream_music_raw, re.DOTALL | re.MULTILINE) + matches = re.findall( + constants.STREAM_MUSIC_REGEX_PATTERN, + stream_music_raw, + re.DOTALL | re.MULTILINE, + ) if matches: return matches[0] @@ -747,7 +835,11 @@ def _volume(self, stream_music, audio_output_device): return None if not self.max_volume: - max_volume_matches = re.findall(constants.MAX_VOLUME_REGEX_PATTERN, stream_music, re.DOTALL | re.MULTILINE) + max_volume_matches = re.findall( + constants.MAX_VOLUME_REGEX_PATTERN, + stream_music, + re.DOTALL | re.MULTILINE, + ) if max_volume_matches: self.max_volume = float(max_volume_matches[0]) @@ -755,7 +847,9 @@ def _volume(self, stream_music, audio_output_device): return None volume_matches = re.findall( - audio_output_device + constants.VOLUME_REGEX_PATTERN, stream_music, re.DOTALL | re.MULTILINE + audio_output_device + constants.VOLUME_REGEX_PATTERN, + stream_music, + re.DOTALL | re.MULTILINE, ) if volume_matches: return int(volume_matches[0]) @@ -899,7 +993,9 @@ def state_detection_rules_validator(rules, exc=KeyError): if not isinstance(value, constants.VALID_PROPERTIES_TYPES[prop]): raise exc( "Conditional value for property '{0}' must be of type {1}, not {2}".format( - prop, constants.VALID_PROPERTIES_TYPES[prop].__name__, type(value).__name__ + prop, + constants.VALID_PROPERTIES_TYPES[prop].__name__, + type(value).__name__, ) ) diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index e57198b7..b733fd87 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -830,7 +830,7 @@ async def set_volume_level(self, volume_level): new_volume = int(min(max(round(self.max_volume * volume_level), 0.0), self.max_volume)) - await self._adb.shell("media volume --show --stream 3 --set {}".format(new_volume)) + await self._adb.shell(self._cmd_volume_set().format(new_volume)) # return the new volume level return new_volume / self.max_volume diff --git a/androidtv/constants.py b/androidtv/constants.py index 320a0df1..c1c20a67 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -89,7 +89,6 @@ class DeviceEnum(IntEnum): #: Parse current application for an Android 11 device CMD_PARSE_CURRENT_APP11 = "CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* }" - #: Assign focused application identifier to ``CURRENT_APP`` variable CMD_DEFINE_CURRENT_APP_VARIABLE = ( "CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp') && " + CMD_PARSE_CURRENT_APP @@ -99,6 +98,17 @@ class DeviceEnum(IntEnum): "CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && " + CMD_PARSE_CURRENT_APP11 ) +#: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 12 device +CMD_DEFINE_CURRENT_APP_VARIABLE12 = ( + "CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp|mObscuringWindow') && " + + CMD_PARSE_CURRENT_APP11 +) + +#: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 13 device +CMD_DEFINE_CURRENT_APP_VARIABLE13 = ( + "CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && " + + CMD_PARSE_CURRENT_APP11 +) #: Output identifier for current/focused application CMD_CURRENT_APP = CMD_DEFINE_CURRENT_APP_VARIABLE + " && echo $CURRENT_APP" @@ -106,6 +116,12 @@ class DeviceEnum(IntEnum): #: Output identifier for current/focused application for an Android 11 device CMD_CURRENT_APP11 = CMD_DEFINE_CURRENT_APP_VARIABLE11 + " && echo $CURRENT_APP" +#: Output identifier for current/focused application for an Android 12 device +CMD_CURRENT_APP12 = CMD_DEFINE_CURRENT_APP_VARIABLE12 + " && echo $CURRENT_APP" + +#: Output identifier for current/focused application for an Android 13 device +CMD_CURRENT_APP13 = CMD_DEFINE_CURRENT_APP_VARIABLE13 + " && echo $CURRENT_APP" + #: Assign focused application identifier to ``CURRENT_APP`` variable (for a Google TV device) CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV = ( "CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && " + CMD_PARSE_CURRENT_APP @@ -114,6 +130,12 @@ class DeviceEnum(IntEnum): #: Output identifier for current/focused application (for a Google TV device) CMD_CURRENT_APP_GOOGLE_TV = CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV + " && echo $CURRENT_APP" +#: set volume +CMD_VOLUME_SET_COMMAND = "media volume --show --stream 3 --set {}" + +#: set volume for an Android 11 & 12 & 13 device +CMD_VOLUME_SET_COMMAND11 = "cmd media_session volume --show --stream 3 --set {}" + #: Get the HDMI input CMD_HDMI_INPUT = ( "dumpsys activity starter | grep -E -o '(ExternalTv|HDMI)InputService/HW[0-9]' -m 1 | grep -o 'HW[0-9]'" @@ -145,6 +167,16 @@ class DeviceEnum(IntEnum): CMD_DEFINE_CURRENT_APP_VARIABLE11.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION ) +#: Launch an app if it is not already the current app on an Android 12 device +CMD_LAUNCH_APP12 = ( + CMD_DEFINE_CURRENT_APP_VARIABLE12.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION +) + +#: Launch an app if it is not already the current app on an Android 11 device +CMD_LAUNCH_APP13 = ( + CMD_DEFINE_CURRENT_APP_VARIABLE13.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION +) + #: Launch an app on a Fire TV device CMD_LAUNCH_APP_FIRETV = ( CMD_DEFINE_CURRENT_APP_VARIABLE.replace("{", "{{").replace("}", "}}") + " && " + CMD_LAUNCH_APP_CONDITION_FIRETV @@ -164,6 +196,12 @@ class DeviceEnum(IntEnum): #: Determine the current app and get the state from ``dumpsys media_session`` for an Android 11 device CMD_CURRENT_APP_MEDIA_SESSION_STATE11 = CMD_CURRENT_APP11 + " && " + CMD_MEDIA_SESSION_STATE +#: Determine the current app and get the state from ``dumpsys media_session`` for an Android 12 device +CMD_CURRENT_APP_MEDIA_SESSION_STATE12 = CMD_CURRENT_APP12 + " && " + CMD_MEDIA_SESSION_STATE + +#: Determine the current app and get the state from ``dumpsys media_session`` for an Android 13 device +CMD_CURRENT_APP_MEDIA_SESSION_STATE13 = CMD_CURRENT_APP13 + " && " + CMD_MEDIA_SESSION_STATE + #: Determine the current app and get the state from ``dumpsys media_session`` for a Google TV device CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV = CMD_CURRENT_APP_GOOGLE_TV + " && " + CMD_MEDIA_SESSION_STATE @@ -381,7 +419,11 @@ class DeviceEnum(IntEnum): VALID_PROPERTIES = VALID_STATE_PROPERTIES + ("wake_lock_size",) #: The required type for each entry in :py:const:`VALID_PROPERTIES` (used by :func:`~androidtv.basetv.state_detection_rules_validator`) -VALID_PROPERTIES_TYPES = {"audio_state": str, "media_session_state": int, "wake_lock_size": int} +VALID_PROPERTIES_TYPES = { + "audio_state": str, + "media_session_state": int, + "wake_lock_size": int, +} # https://developer.android.com/reference/android/media/session/PlaybackState.html #: States for the :attr:`~androidtv.basetv.basetv.BaseTV.media_session_state` property @@ -483,6 +525,7 @@ class DeviceEnum(IntEnum): APP_TWITCH_FIRETV = "tv.twitch.android.viewer" APP_VEVO = "com.vevo.tv" APP_VH1 = "com.mtvn.vh1android" +APP_VIKI = "com.viki.android" APP_VIMEO = "com.vimeo.android.videoapp" APP_VLC = "org.videolan.vlc" APP_VOYO = "com.phonegap.voyo" @@ -590,6 +633,7 @@ class DeviceEnum(IntEnum): APP_TWITCH_FIRETV: "Twitch (FireTV)", APP_VEVO: "Vevo", APP_VH1: "VH1", + APP_VIKI: "Rakuten Viki", APP_VIMEO: "Vimeo", APP_VLC: "VLC", APP_VOYO: "VOYO", diff --git a/tests/generate_test_constants.py b/tests/generate_test_constants.py index f1decd70..74e5769d 100644 --- a/tests/generate_test_constants.py +++ b/tests/generate_test_constants.py @@ -12,8 +12,12 @@ "CMD_SUCCESS1_FAILURE0", "CMD_PARSE_CURRENT_APP", "CMD_PARSE_CURRENT_APP11", + "CMD_PARSE_CURRENT_APP12", + "CMD_PARSE_CURRENT_APP13", "CMD_DEFINE_CURRENT_APP_VARIABLE", "CMD_DEFINE_CURRENT_APP_VARIABLE11", + "CMD_DEFINE_CURRENT_APP_VARIABLE12", + "CMD_DEFINE_CURRENT_APP_VARIABLE13", "CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV", "CMD_LAUNCH_APP_CONDITION", "CMD_LAUNCH_APP_CONDITION_FIRETV", diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index d8babddd..073d676b 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -104,6 +104,24 @@ WIFIMAC_SHIELD_TV_11 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" ETHMAC_SHIELD_TV_11 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" +DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_12 = """NVIDIA +SHIELD Android TV +0123456789012 +12 +""" + +WIFIMAC_SHIELD_TV_12 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" +ETHMAC_SHIELD_TV_12 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" + +DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_13 = """NVIDIA +SHIELD Android TV +0123456789012 +13 +""" + +WIFIMAC_SHIELD_TV_13 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" +ETHMAC_SHIELD_TV_13 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" + DEVICE_PROPERTIES_DICT_SHIELD_TV_11 = { "manufacturer": "NVIDIA", "model": "SHIELD Android TV", @@ -113,6 +131,24 @@ "ethmac": "ab:cd:ef:gh:ij:kl", } +DEVICE_PROPERTIES_DICT_SHIELD_TV_12 = { + "manufacturer": "NVIDIA", + "model": "SHIELD Android TV", + "serialno": "0123456789012", + "sw_version": "12", + "wifimac": "11:22:33:44:55:66", + "ethmac": "ab:cd:ef:gh:ij:kl", +} + +DEVICE_PROPERTIES_DICT_SHIELD_TV_13 = { + "manufacturer": "NVIDIA", + "model": "SHIELD Android TV", + "serialno": "0123456789012", + "sw_version": "13", + "wifimac": "11:22:33:44:55:66", + "ethmac": "ab:cd:ef:gh:ij:kl", +} + INSTALLED_APPS_OUTPUT_1 = """package:org.example.app package:org.example.launcher """ @@ -190,290 +226,350 @@ def test_keys(self): self.btv.space() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_SPACE) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_SPACE), ) self.btv.key_0() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_0) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_0), ) self.btv.key_1() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_1) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_1), ) self.btv.key_2() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_2) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_2), ) self.btv.key_3() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_3) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_3), ) self.btv.key_4() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_4) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_4), ) self.btv.key_5() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_5) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_5), ) self.btv.key_6() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_6) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_6), ) self.btv.key_7() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_7) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_7), ) self.btv.key_8() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_8) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_8), ) self.btv.key_9() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_9) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_9), ) self.btv.key_a() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_A) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_A), ) self.btv.key_b() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_B) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_B), ) self.btv.key_c() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_C) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_C), ) self.btv.key_d() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_D) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_D), ) self.btv.key_e() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_E) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_E), ) self.btv.key_f() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_F) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_F), ) self.btv.key_g() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_G) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_G), ) self.btv.key_h() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_H) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_H), ) self.btv.key_i() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_I) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_I), ) self.btv.key_j() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_J) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_J), ) self.btv.key_k() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_K) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_K), ) self.btv.key_l() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_L) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_L), ) self.btv.key_m() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_M) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_M), ) self.btv.key_n() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_N) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_N), ) self.btv.key_o() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_O) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_O), ) self.btv.key_p() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_P) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_P), ) self.btv.key_q() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_Q) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_Q), ) self.btv.key_r() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_R) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_R), ) self.btv.key_s() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_S) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_S), ) self.btv.key_t() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_T) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_T), ) self.btv.key_u() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_U) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_U), ) self.btv.key_v() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_V) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_V), ) self.btv.key_w() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_W) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_W), ) self.btv.key_x() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_X) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_X), ) self.btv.key_y() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_Y) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_Y), ) self.btv.key_z() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_Z) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_Z), ) self.btv.power() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_POWER) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_POWER), ) self.btv.sleep() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_SLEEP) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_SLEEP), ) self.btv.home() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_HOME) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_HOME), ) self.btv.up() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_UP) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_UP), ) self.btv.down() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_DOWN) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_DOWN), ) self.btv.left() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_LEFT) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_LEFT), ) self.btv.right() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_RIGHT) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_RIGHT), ) self.btv.enter() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_ENTER) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_ENTER), ) self.btv.back() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_BACK) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_BACK), ) self.btv.menu() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_MENU) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_MENU), ) self.btv.mute_volume() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_MUTE) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_MUTE), ) self.btv.media_play() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_PLAY) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_PLAY), ) self.btv.media_pause() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_PAUSE) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_PAUSE), ) self.btv.media_play_pause() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_PLAY_PAUSE) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_PLAY_PAUSE), ) self.btv.media_stop() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_STOP) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_STOP), ) self.btv.media_next_track() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_NEXT) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_NEXT), ) self.btv.media_previous_track() self.assertEqual( - getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, "input keyevent {}".format(constants.KEY_PREVIOUS) + getattr(self.btv._adb, self.ADB_ATTR).shell_cmd, + "input keyevent {}".format(constants.KEY_PREVIOUS), ) def test_get_device_properties(self): """Check that ``get_device_properties`` works correctly.""" with patch.object( - self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT1, ETHMAC_OUTPUT1, WIFIMAC_OUTPUT1) + self.btv._adb, + "shell", + side_effect=(DEVICE_PROPERTIES_OUTPUT1, ETHMAC_OUTPUT1, WIFIMAC_OUTPUT1), ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT1, device_properties) with patch.object( - self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT2, ETHMAC_OUTPUT1, WIFIMAC_OUTPUT1) + self.btv._adb, + "shell", + side_effect=(DEVICE_PROPERTIES_OUTPUT2, ETHMAC_OUTPUT1, WIFIMAC_OUTPUT1), ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT2, device_properties) with patch.object( - self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT3, ETHMAC_OUTPUT3, WIFIMAC_OUTPUT3) + self.btv._adb, + "shell", + side_effect=(DEVICE_PROPERTIES_OUTPUT3, ETHMAC_OUTPUT3, WIFIMAC_OUTPUT3), ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT3, device_properties) @@ -487,7 +583,9 @@ def test_get_device_properties(self): self.assertDictEqual({"ethmac": None, "wifimac": None}, device_properties) with patch.object( - self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_GOOGLE_TV, ETHMAC_GOOGLE, WIFIMAC_GOOGLE) + self.btv._adb, + "shell", + side_effect=(DEVICE_PROPERTIES_GOOGLE_TV, ETHMAC_GOOGLE, WIFIMAC_GOOGLE), ): self.btv = AndroidTVSync.from_base(self.btv) device_properties = self.btv.get_device_properties() @@ -496,12 +594,18 @@ def test_get_device_properties(self): self.assertEqual(self.btv.device_properties["manufacturer"], "Google") self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP_GOOGLE_TV) self.assertEqual( - self.btv._cmd_current_app_media_session_state(), constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV + self.btv._cmd_current_app_media_session_state(), + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, + ) + self.assertEqual( + self.btv._cmd_launch_app("TEST"), + constants.CMD_LAUNCH_APP_GOOGLE_TV.format("TEST"), ) - self.assertEqual(self.btv._cmd_launch_app("TEST"), constants.CMD_LAUNCH_APP_GOOGLE_TV.format("TEST")) with patch.object( - self.btv._adb, "shell", side_effect=(DEVICE_PROPERTIES_OUTPUT_SONY_TV, ETHMAC_SONY, WIFIMAC_SONY) + self.btv._adb, + "shell", + side_effect=(DEVICE_PROPERTIES_OUTPUT_SONY_TV, ETHMAC_SONY, WIFIMAC_SONY), ): device_properties = self.btv.get_device_properties() self.assertDictEqual(DEVICE_PROPERTIES_DICT_SONY_TV, device_properties) @@ -509,7 +613,11 @@ def test_get_device_properties(self): with patch.object( self.btv._adb, "shell", - side_effect=(DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_11, ETHMAC_SHIELD_TV_11, WIFIMAC_SHIELD_TV_11), + side_effect=( + DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_11, + ETHMAC_SHIELD_TV_11, + WIFIMAC_SHIELD_TV_11, + ), ): self.btv = AndroidTVSync.from_base(self.btv) device_properties = self.btv.get_device_properties() @@ -518,16 +626,88 @@ def test_get_device_properties(self): self.assertDictEqual(DEVICE_PROPERTIES_DICT_SHIELD_TV_11, device_properties) # _cmd_audio_state self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) + # _cmd_volume_set + self.assertEqual(self.btv._cmd_volume_set(), constants.CMD_VOLUME_SET_COMMAND11) # _cmd_current_app self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP11) # _cmd_current_app_media_session_state self.assertEqual( - self.btv._cmd_current_app_media_session_state(), constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11 + self.btv._cmd_current_app_media_session_state(), + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, + ) + # _cmd_hdmi_input + self.assertEqual(self.btv._cmd_hdmi_input(), constants.CMD_HDMI_INPUT11) + # _cmd_launch_app + self.assertEqual( + self.btv._cmd_launch_app("TEST"), + constants.CMD_LAUNCH_APP11.format("TEST"), + ) + + with patch.object( + self.btv._adb, + "shell", + side_effect=( + DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_12, + ETHMAC_SHIELD_TV_12, + WIFIMAC_SHIELD_TV_12, + ), + ): + self.btv = AndroidTVSync.from_base(self.btv) + device_properties = self.btv.get_device_properties() + assert self.btv.device_properties.get("sw_version", "") == "12" + assert self.btv.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV + self.assertDictEqual(DEVICE_PROPERTIES_DICT_SHIELD_TV_12, device_properties) + # _cmd_audio_state + self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) + # _cmd_volume_set + self.assertEqual(self.btv._cmd_volume_set(), constants.CMD_VOLUME_SET_COMMAND11) + # _cmd_current_app + self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP12) + # _cmd_current_app_media_session_state + self.assertEqual( + self.btv._cmd_current_app_media_session_state(), + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE12, + ) + # _cmd_hdmi_input + self.assertEqual(self.btv._cmd_hdmi_input(), constants.CMD_HDMI_INPUT11) + # _cmd_launch_app + self.assertEqual( + self.btv._cmd_launch_app("TEST"), + constants.CMD_LAUNCH_APP12.format("TEST"), + ) + + with patch.object( + self.btv._adb, + "shell", + side_effect=( + DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_13, + ETHMAC_SHIELD_TV_13, + WIFIMAC_SHIELD_TV_13, + ), + ): + self.btv = AndroidTVSync.from_base(self.btv) + device_properties = self.btv.get_device_properties() + assert self.btv.device_properties.get("sw_version", "") == "13" + assert self.btv.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV + self.assertDictEqual(DEVICE_PROPERTIES_DICT_SHIELD_TV_13, device_properties) + # _cmd_audio_state + self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) + # _cmd_volume_set + self.assertEqual(self.btv._cmd_volume_set(), constants.CMD_VOLUME_SET_COMMAND11) + # _cmd_current_app + self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP13) + # _cmd_current_app_media_session_state + self.assertEqual( + self.btv._cmd_current_app_media_session_state(), + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE13, ) # _cmd_hdmi_input self.assertEqual(self.btv._cmd_hdmi_input(), constants.CMD_HDMI_INPUT11) # _cmd_launch_app - self.assertEqual(self.btv._cmd_launch_app("TEST"), constants.CMD_LAUNCH_APP11.format("TEST")) + self.assertEqual( + self.btv._cmd_launch_app("TEST"), + constants.CMD_LAUNCH_APP13.format("TEST"), + ) def test_get_installed_apps(self): """ "Check that `get_installed_apps` works correctly.""" @@ -632,9 +812,12 @@ def test_screen_on_awake_wake_lock_size(self): with patchers.patch_shell("11Wake Locks: size=2")[self.PATCH_KEY]: self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) - with patchers.patch_shell(["Failed to write while dumping serviceWake Locks: size=2", "11Wake Locks: size=2"])[ - self.PATCH_KEY - ]: + with patchers.patch_shell( + [ + "Failed to write while dumping serviceWake Locks: size=2", + "11Wake Locks: size=2", + ] + )[self.PATCH_KEY]: self.assertTupleEqual(self.btv.screen_on_awake_wake_lock_size(), (True, True, 2)) def test_state_detection_rules_validator(self): @@ -649,37 +832,114 @@ def test_state_detection_rules_validator(self): # Make sure that an error is raised when the state detection rules are invalid self.assertRaises( - TypeError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID1 + TypeError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID1, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID2 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID2, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID3 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID3, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID4 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID4, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID5 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID5, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID6 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID6, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID7 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID7, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID8 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID8, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID9 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID9, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID10 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID10, ) self.assertRaises( - KeyError, BaseTVSync, "HOST", 5555, "", "", 5037, state_detection_rules=STATE_DETECTION_RULES_INVALID11 + KeyError, + BaseTVSync, + "HOST", + 5555, + "", + "", + 5037, + state_detection_rules=STATE_DETECTION_RULES_INVALID11, ) def test_wake_lock_size(self): diff --git a/tests/test_constants.py b/tests/test_constants.py index 8093b012..ec9a5d16 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -89,6 +89,18 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", ) + # CMD_CURRENT_APP12 + self.assertCommand( + constants.CMD_CURRENT_APP12, + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp|mObscuringWindow') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", + ) + + # CMD_CURRENT_APP13 + self.assertCommand( + constants.CMD_CURRENT_APP13, + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", + ) + # CMD_CURRENT_APP_GOOGLE_TV self.assertCommand( constants.CMD_CURRENT_APP_GOOGLE_TV, @@ -107,6 +119,18 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) + # CMD_CURRENT_APP_MEDIA_SESSION_STATE12 + self.assertCommand( + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE12, + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp|mObscuringWindow') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) + + # CMD_CURRENT_APP_MEDIA_SESSION_STATE13 + self.assertCommand( + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE13, + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) + # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV, @@ -146,6 +170,18 @@ def test_constants(self): r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", ) + # CMD_LAUNCH_APP12 + self.assertCommand( + constants.CMD_LAUNCH_APP12, + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp|mObscuringWindow') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + ) + + # CMD_LAUNCH_APP13 + self.assertCommand( + constants.CMD_LAUNCH_APP13, + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + ) + # CMD_LAUNCH_APP_FIRETV self.assertCommand( constants.CMD_LAUNCH_APP_FIRETV, @@ -227,6 +263,15 @@ def test_constants(self): # CMD_VERSION self.assertCommand(constants.CMD_VERSION, r"getprop ro.build.version.release") + # CMD_VOLUME_SET_COMMAND + self.assertCommand(constants.CMD_VOLUME_SET_COMMAND, r"media volume --show --stream 3 --set {}") + + # CMD_VOLUME_SET_COMMAND11 + self.assertCommand( + constants.CMD_VOLUME_SET_COMMAND11, + r"cmd media_session volume --show --stream 3 --set {}", + ) + # CMD_WAKE_LOCK_SIZE self.assertCommand(constants.CMD_WAKE_LOCK_SIZE, r"dumpsys power | grep Locks | grep 'size='") @@ -238,7 +283,10 @@ def test_no_underscores(self): """Test that 'ANDROID_TV', 'BASE_TV', and 'FIRE_TV' do not appear in the code base.""" cwd = os.path.join(os.path.dirname(__file__), "..") for underscore_name in ["ANDROID_TV", "BASE_TV", "FIRE_TV"]: - with subprocess.Popen(shlex.split("git grep -l {} -- androidtv/".format(underscore_name)), cwd=cwd) as p: + with subprocess.Popen( + shlex.split("git grep -l {} -- androidtv/".format(underscore_name)), + cwd=cwd, + ) as p: self.assertEqual(p.wait(), 1) def test_current_app_extraction_atv_launcher(self): From ea0d1fb4c97bb21b3ddb728af5d41c76b408182b Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 19 Oct 2023 20:21:36 -0700 Subject: [PATCH 56/66] Android TV 12 & 13 follow-up (#344) --- androidtv/basetv/basetv.py | 15 ++++++++++----- androidtv/basetv/basetv_async.py | 2 +- androidtv/basetv/basetv_sync.py | 2 +- tests/test_basetv_sync.py | 8 ++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 8174d8ef..c4c86fb2 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -242,9 +242,14 @@ def _cmd_hdmi_input(self): return constants.CMD_HDMI_INPUT - def _cmd_volume_set(self): + def _cmd_volume_set(self, new_volume): """Get the command used to set volume for this device. + Parameters + ---------- + new_volume : int + The new volume level + Returns ------- str @@ -253,17 +258,17 @@ def _cmd_volume_set(self): """ # Is this an Android 11 device? if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": - return constants.CMD_VOLUME_SET_COMMAND11 + return constants.CMD_VOLUME_SET_COMMAND11.format(new_volume) # Is this an Android 12 device? if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": - return constants.CMD_VOLUME_SET_COMMAND11 + return constants.CMD_VOLUME_SET_COMMAND11.format(new_volume) # Is this an Android 13 device? if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": - return constants.CMD_VOLUME_SET_COMMAND11 + return constants.CMD_VOLUME_SET_COMMAND11.format(new_volume) - return constants.CMD_VOLUME_SET_COMMAND + return constants.CMD_VOLUME_SET_COMMAND.format(new_volume) def _cmd_launch_app(self, app): """Get the command to launch the specified app for this device. diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index b733fd87..13cc8d5c 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -830,7 +830,7 @@ async def set_volume_level(self, volume_level): new_volume = int(min(max(round(self.max_volume * volume_level), 0.0), self.max_volume)) - await self._adb.shell(self._cmd_volume_set().format(new_volume)) + await self._adb.shell(self._cmd_volume_set(new_volume)) # return the new volume level return new_volume / self.max_volume diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index dfb230df..8d4480a6 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -830,7 +830,7 @@ def set_volume_level(self, volume_level): new_volume = int(min(max(round(self.max_volume * volume_level), 0.0), self.max_volume)) - self._adb.shell("media volume --show --stream 3 --set {}".format(new_volume)) + self._adb.shell(self._cmd_volume_set(new_volume)) # return the new volume level return new_volume / self.max_volume diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index 073d676b..dbb909a9 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -35,7 +35,7 @@ DEVICE_PROPERTIES_OUTPUT2 = """Amazon AFTT - + 5.1.1 """ @@ -627,7 +627,7 @@ def test_get_device_properties(self): # _cmd_audio_state self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) # _cmd_volume_set - self.assertEqual(self.btv._cmd_volume_set(), constants.CMD_VOLUME_SET_COMMAND11) + self.assertEqual(self.btv._cmd_volume_set(5), constants.CMD_VOLUME_SET_COMMAND11.format(5)) # _cmd_current_app self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP11) # _cmd_current_app_media_session_state @@ -660,7 +660,7 @@ def test_get_device_properties(self): # _cmd_audio_state self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) # _cmd_volume_set - self.assertEqual(self.btv._cmd_volume_set(), constants.CMD_VOLUME_SET_COMMAND11) + self.assertEqual(self.btv._cmd_volume_set(5), constants.CMD_VOLUME_SET_COMMAND11.format(5)) # _cmd_current_app self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP12) # _cmd_current_app_media_session_state @@ -693,7 +693,7 @@ def test_get_device_properties(self): # _cmd_audio_state self.assertEqual(self.btv._cmd_audio_state(), constants.CMD_AUDIO_STATE11) # _cmd_volume_set - self.assertEqual(self.btv._cmd_volume_set(), constants.CMD_VOLUME_SET_COMMAND11) + self.assertEqual(self.btv._cmd_volume_set(5), constants.CMD_VOLUME_SET_COMMAND11.format(5)) # _cmd_current_app self.assertEqual(self.btv._cmd_current_app(), constants.CMD_CURRENT_APP13) # _cmd_current_app_media_session_state From 8a41ad2c9a04e18496e002f4eae9bc3e09c9dad0 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 19 Oct 2023 20:24:45 -0700 Subject: [PATCH 57/66] Bump the version to 0.0.73 (#345) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 77a1f995..fa543778 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.72" +__version__ = "0.0.73" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 567bb9d5..54c51c8b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.72' +version = '0.0.73' # The full version, including alpha/beta/rc tags -release = '0.0.72' +release = '0.0.73' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index bb8a69d1..7ada6524 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="androidtv", - version="0.0.72", + version="0.0.73", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 7aa7c6a84e77a4c6a8941abedd5ac60f949b57e9 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 25 Oct 2023 20:47:12 -0700 Subject: [PATCH 58/66] Fix Readthedocs (#346) * Fix .readthedocs.yml * More .readthedocs.yml changes * Checkout Makefile from master * Update docs/requirements.txt * Add sphinx-rtd-theme to docs/requirements.txt * Formatting --- .readthedocs.yml | 7 ++++++- docs/requirements.txt | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index b168698a..e0b542ac 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,6 +5,12 @@ # Required version: 2 +# Set the OS +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py @@ -14,6 +20,5 @@ formats: all # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 install: - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt index e402f438..a754375f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,6 @@ -sphinx==2.1.2 -androidtv -aiofiles +# Standard requirements +sphinx +sphinx-rtd-theme + +# Specific requirements for this project +androidtv[async] From d8a8f3a4810eec7180f0370750ea6d216dd8300f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 7 Apr 2024 20:56:43 -0700 Subject: [PATCH 59/66] Add Python 3.11 and 3.12 to CI (#350) * Add Python 3.11 and 3.12 to CI * black * Add setuptools to venv_requirements.txt --- .github/workflows/python-package.yml | 2 +- androidtv/adb_manager/adb_manager_async.py | 1 - androidtv/adb_manager/adb_manager_sync.py | 1 - androidtv/androidtv/androidtv_async.py | 1 - androidtv/androidtv/androidtv_sync.py | 1 - androidtv/androidtv/base_androidtv.py | 1 - androidtv/basetv/basetv.py | 1 - androidtv/basetv/basetv_async.py | 1 - androidtv/basetv/basetv_sync.py | 1 - androidtv/constants.py | 1 - androidtv/firetv/base_firetv.py | 1 - androidtv/firetv/firetv_async.py | 1 - androidtv/firetv/firetv_sync.py | 1 - venv_requirements.txt | 1 + 14 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bf40e5ba..5989973c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 diff --git a/androidtv/adb_manager/adb_manager_async.py b/androidtv/adb_manager/adb_manager_async.py index 7705a3f6..e5325291 100644 --- a/androidtv/adb_manager/adb_manager_async.py +++ b/androidtv/adb_manager/adb_manager_async.py @@ -5,7 +5,6 @@ """ - import asyncio from contextlib import asynccontextmanager import logging diff --git a/androidtv/adb_manager/adb_manager_sync.py b/androidtv/adb_manager/adb_manager_sync.py index 9647e227..e93283d3 100644 --- a/androidtv/adb_manager/adb_manager_sync.py +++ b/androidtv/adb_manager/adb_manager_sync.py @@ -5,7 +5,6 @@ """ - from contextlib import contextmanager import logging import sys diff --git a/androidtv/androidtv/androidtv_async.py b/androidtv/androidtv/androidtv_async.py index 08fdeb6c..d2493acb 100644 --- a/androidtv/androidtv/androidtv_async.py +++ b/androidtv/androidtv/androidtv_async.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from .base_androidtv import BaseAndroidTV diff --git a/androidtv/androidtv/androidtv_sync.py b/androidtv/androidtv/androidtv_sync.py index b5c6379c..fa0fe693 100644 --- a/androidtv/androidtv/androidtv_sync.py +++ b/androidtv/androidtv/androidtv_sync.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from .base_androidtv import BaseAndroidTV diff --git a/androidtv/androidtv/base_androidtv.py b/androidtv/androidtv/base_androidtv.py index 1694e76b..6180a931 100644 --- a/androidtv/androidtv/base_androidtv.py +++ b/androidtv/androidtv/base_androidtv.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from ..basetv.basetv import BaseTV diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index c4c86fb2..1d702ec4 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging import re diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 13cc8d5c..8173bf24 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from .basetv import BaseTV diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index 8d4480a6..f5f227f6 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from .basetv import BaseTV diff --git a/androidtv/constants.py b/androidtv/constants.py index c1c20a67..50d0c888 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -7,7 +7,6 @@ """ - import re import sys diff --git a/androidtv/firetv/base_firetv.py b/androidtv/firetv/base_firetv.py index 285ce8f5..59bb3530 100644 --- a/androidtv/firetv/base_firetv.py +++ b/androidtv/firetv/base_firetv.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from ..basetv.basetv import BaseTV diff --git a/androidtv/firetv/firetv_async.py b/androidtv/firetv/firetv_async.py index 3493b07f..87202fd2 100644 --- a/androidtv/firetv/firetv_async.py +++ b/androidtv/firetv/firetv_async.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from .base_firetv import BaseFireTV diff --git a/androidtv/firetv/firetv_sync.py b/androidtv/firetv/firetv_sync.py index 5e9b0a62..01ae1d74 100644 --- a/androidtv/firetv/firetv_sync.py +++ b/androidtv/firetv/firetv_sync.py @@ -3,7 +3,6 @@ ADB Debugging must be enabled. """ - import logging from .base_firetv import BaseFireTV diff --git a/venv_requirements.txt b/venv_requirements.txt index a11d971c..8f953308 100644 --- a/venv_requirements.txt +++ b/venv_requirements.txt @@ -5,6 +5,7 @@ coveralls flake8 pylint pytest +setuptools sphinx sphinx-rtd-theme twine From 88d76f5725cb38fed3f0018dc803638e1d0c467a Mon Sep 17 00:00:00 2001 From: mano3m Date: Sun, 27 Oct 2024 19:33:39 +0100 Subject: [PATCH 60/66] Add -A option to ps command to list all Fire TV processes (#348) * Added -A option to ps to list all processes. Fixes [#102565](https://github.com/home-assistant/core/issues/102565) and [https://github.com/JeffLIrion/python-androidtv/issues/292](https://github.com/JeffLIrion/python-androidtv/issues/292) * Revert "Added -A option to ps to list all processes. Fixes [#102565](https://github.com/home-assistant/core/issues/102565) and [https://github.com/JeffLIrion/python-androidtv/issues/292](https://github.com/JeffLIrion/python-androidtv/issues/292)" This reverts commit 86a27ae2b3fa00aa6daeace9ca0d59de4f494c7c. * Merged Android/Fire TV command for running process --- androidtv/basetv/basetv.py | 5 +---- androidtv/constants.py | 7 ++----- tests/test_constants.py | 7 ++----- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 1d702ec4..47d82ebe 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -323,10 +323,7 @@ def _cmd_running_apps(self): if constants.CUSTOM_RUNNING_APPS in self._custom_commands: return self._custom_commands[constants.CUSTOM_RUNNING_APPS] - if self.DEVICE_ENUM == constants.DeviceEnum.FIRETV: - return constants.CMD_RUNNING_APPS_FIRETV - - return constants.CMD_RUNNING_APPS_ANDROIDTV + return constants.CMD_RUNNING_APPS def _cmd_turn_off(self): """Get the command used to turn off this device. diff --git a/androidtv/constants.py b/androidtv/constants.py index 50d0c888..ba7cde96 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -204,11 +204,8 @@ class DeviceEnum(IntEnum): #: Determine the current app and get the state from ``dumpsys media_session`` for a Google TV device CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV = CMD_CURRENT_APP_GOOGLE_TV + " && " + CMD_MEDIA_SESSION_STATE -#: Get the running apps for an Android TV device -CMD_RUNNING_APPS_ANDROIDTV = "ps -A | grep u0_a" - -#: Get the running apps for a Fire TV device -CMD_RUNNING_APPS_FIRETV = "ps | grep u0_a" +#: Get the running apps for an Android/Fire TV device +CMD_RUNNING_APPS = "ps -A | grep u0_a" #: Get installed apps CMD_INSTALLED_APPS = "pm list packages" diff --git a/tests/test_constants.py b/tests/test_constants.py index ec9a5d16..9fa1dd88 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -212,11 +212,8 @@ def test_constants(self): # CMD_MODEL self.assertCommand(constants.CMD_MODEL, r"getprop ro.product.model") - # CMD_RUNNING_APPS_ANDROIDTV - self.assertCommand(constants.CMD_RUNNING_APPS_ANDROIDTV, r"ps -A | grep u0_a") - - # CMD_RUNNING_APPS_FIRETV - self.assertCommand(constants.CMD_RUNNING_APPS_FIRETV, r"ps | grep u0_a") + # CMD_RUNNING_APPS + self.assertCommand(constants.CMD_RUNNING_APPS, r"ps -A | grep u0_a") # CMD_SCREEN_ON self.assertCommand( From c2ca4c46a43a164d47ad5c840b5097f40324abff Mon Sep 17 00:00:00 2001 From: Liborsaf Date: Sun, 27 Oct 2024 19:33:56 +0100 Subject: [PATCH 61/66] Fix "Failed to write while dumping service window: Broken pipe" (#351) --- androidtv/constants.py | 2 +- tests/test_constants.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/constants.py b/androidtv/constants.py index ba7cde96..655f0689 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -94,7 +94,7 @@ class DeviceEnum(IntEnum): ) #: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 11 device CMD_DEFINE_CURRENT_APP_VARIABLE11 = ( - "CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && " + CMD_PARSE_CURRENT_APP11 + "CURRENT_APP=$(dumpsys window windows | grep -E 'mInputMethod(Input)?Target') && " + CMD_PARSE_CURRENT_APP11 ) #: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 12 device diff --git a/tests/test_constants.py b/tests/test_constants.py index 9fa1dd88..b2bf790a 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -86,7 +86,7 @@ def test_constants(self): # CMD_CURRENT_APP11 self.assertCommand( constants.CMD_CURRENT_APP11, - r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", ) # CMD_CURRENT_APP12 @@ -116,7 +116,7 @@ def test_constants(self): # CMD_CURRENT_APP_MEDIA_SESSION_STATE11 self.assertCommand( constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE11, - r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mInputMethod(Input)?Target') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) # CMD_CURRENT_APP_MEDIA_SESSION_STATE12 @@ -167,7 +167,7 @@ def test_constants(self): # CMD_LAUNCH_APP11 self.assertCommand( constants.CMD_LAUNCH_APP11, - r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'mInputMethod(Input)?Target') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", + r"CURRENT_APP=$(dumpsys window windows | grep -E 'mInputMethod(Input)?Target') && CURRENT_APP=${{CURRENT_APP%%/*}} && CURRENT_APP=${{CURRENT_APP##* }} && if [ $CURRENT_APP != '{0}' ]; then monkey -p {0} -c android.intent.category.LEANBACK_LAUNCHER --pct-syskeys 0 1; fi", ) # CMD_LAUNCH_APP12 From 78ecfd5e2036a6c7521077e943aef069f4e87cf3 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 28 Oct 2024 20:45:19 -0700 Subject: [PATCH 62/66] Pylint fixes (#352) * Pylint fixes * Revert "Pylint fixes" This reverts commit a3a24bf4b9606ba17cdc32a923e40c222933eee5. * Update .pylintrc * Add invalid-name to pylint disable list --- .pylintrc | 100 ++++-------------------------------------------------- 1 file changed, 6 insertions(+), 94 deletions(-) diff --git a/.pylintrc b/.pylintrc index d506a584..b12c4e3f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,102 +60,22 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - line-too-long, +disable=consider-using-f-string, duplicate-code, - fixme, + invalid-name, + line-too-long, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-nested-blocks, + too-many-positional-arguments, too-many-public-methods, too-many-return-statements, too-many-statements, - invalid-name, useless-object-inheritance, - unspecified-encoding, - consider-using-f-string, - bad-continuation + unspecified-encoding # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -233,13 +153,6 @@ max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no @@ -583,5 +496,4 @@ valid-metaclass-classmethod-first-arg=cls # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.Exception From ac4b9095ceac839d885fab788cf7d0f7f5019e45 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 28 Oct 2024 20:49:45 -0700 Subject: [PATCH 63/66] Bump the version to 0.0.74 (#353) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index fa543778..5c528339 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.73" +__version__ = "0.0.74" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 54c51c8b..5e9c0bb5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.73' +version = '0.0.74' # The full version, including alpha/beta/rc tags -release = '0.0.73' +release = '0.0.74' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 7ada6524..25a9d6e4 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="androidtv", - version="0.0.73", + version="0.0.74", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"], From 6f04990826fe0e5dccf7cae3d664451078148e4b Mon Sep 17 00:00:00 2001 From: prabhjotsbhatia-ca <56749856+prabhjotsbhatia-ca@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:13:18 -0500 Subject: [PATCH 64/66] Add support for Askey STI6130 (#354) * Add support for Askey STI6130 * Linting androidtv/basetv/basetv.py Co-authored-by: Jeff Irion update documentation for product_id more lint issues. * More linting --- androidtv/basetv/basetv.py | 23 ++++++++++++++++++++--- androidtv/basetv/basetv_async.py | 2 +- androidtv/basetv/basetv_sync.py | 2 +- androidtv/constants.py | 20 +++++++++++++++++++- tests/generate_test_constants.py | 1 + tests/test_basetv_sync.py | 15 +++++++++++++++ tests/test_constants.py | 15 ++++++++++++++- tests/test_setup_async.py | 6 ++++-- tests/test_setup_sync.py | 6 ++++-- 9 files changed, 79 insertions(+), 11 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 47d82ebe..3499abb9 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -167,6 +167,14 @@ def _cmd_current_app(self): ): return constants.CMD_CURRENT_APP_GOOGLE_TV + # Is this an Askey STI6130 Device? + if ( + self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV + and "askey" in self.device_properties.get("manufacturer", "") + and "sti6130" in self.device_properties.get("product_id", "") + ): + return constants.CMD_CURRENT_APP_ASKEY_STI6130 + # Is this an Android 11 device? if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": return constants.CMD_CURRENT_APP11 @@ -193,6 +201,14 @@ def _cmd_current_app_media_session_state(self): if constants.CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE in self._custom_commands: return self._custom_commands[constants.CUSTOM_CURRENT_APP_MEDIA_SESSION_STATE] + # Is this an Askey STI6130 Device? + if ( + self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV + and "askey" in self.device_properties.get("manufacturer", "") + and "sti6130" in self.device_properties.get("product_id", "") + ): + return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_ASKEY_STI6130 + # Is this a Google Chromecast Android TV? if ( self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV @@ -407,7 +423,7 @@ def _parse_device_properties(self, properties): The output of the ADB command that retrieves the device properties This method fills in the ``device_properties`` attribute, which is a dictionary with keys - ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` + ``'serialno'``, ``'manufacturer'``, ``'model'``, ``'sw_version'`` and ``'product_id'`` """ _LOGGER.debug( @@ -422,11 +438,11 @@ def _parse_device_properties(self, properties): return lines = properties.strip().splitlines() - if len(lines) != 4: + if len(lines) != 5: self.device_properties = {} return - manufacturer, model, serialno, version = lines + manufacturer, model, serialno, version, product_id = lines if not serialno.strip(): _LOGGER.warning( @@ -442,6 +458,7 @@ def _parse_device_properties(self, properties): "model": model, "serialno": serialno, "sw_version": version, + "product_id": product_id, } @staticmethod diff --git a/androidtv/basetv/basetv_async.py b/androidtv/basetv/basetv_async.py index 8173bf24..d901d10c 100644 --- a/androidtv/basetv/basetv_async.py +++ b/androidtv/basetv/basetv_async.py @@ -203,7 +203,7 @@ async def get_device_properties(self): Returns ------- props : dict - A dictionary with keys ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` + A dictionary with keys ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, ``'sw_version'`` and ``'product_id'`` """ properties = await self._adb.shell(constants.CMD_DEVICE_PROPERTIES) diff --git a/androidtv/basetv/basetv_sync.py b/androidtv/basetv/basetv_sync.py index f5f227f6..57ad0d84 100644 --- a/androidtv/basetv/basetv_sync.py +++ b/androidtv/basetv/basetv_sync.py @@ -203,7 +203,7 @@ def get_device_properties(self): Returns ------- props : dict - A dictionary with keys ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, and ``'sw_version'`` + A dictionary with keys ``'wifimac'``, ``'ethmac'``, ``'serialno'``, ``'manufacturer'``, ``'model'``, ``'sw_version'`` and ``'product_id'`` """ properties = self._adb.shell(constants.CMD_DEVICE_PROPERTIES) diff --git a/androidtv/constants.py b/androidtv/constants.py index 655f0689..b1729ec2 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -121,6 +121,18 @@ class DeviceEnum(IntEnum): #: Output identifier for current/focused application for an Android 13 device CMD_CURRENT_APP13 = CMD_DEFINE_CURRENT_APP_VARIABLE13 + " && echo $CURRENT_APP" + +#: Assign focused application identifier to ``CURRENT_APP`` variable (for a Askey STI6130 ) +#: Tested on Bell Streamer which is sti6130. May also work the same on Walmart Onn android stick, which is the same hardware, but labelled Askey STI6140 +CMD_DEFINE_CURRENT_APP_VARIABLE_ASKEY_STI6130 = ( + "CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && " + + CMD_PARSE_CURRENT_APP11 +) + +#: Output identifier for current/focused application (for a Askey STI6130) +CMD_CURRENT_APP_ASKEY_STI6130 = CMD_DEFINE_CURRENT_APP_VARIABLE_ASKEY_STI6130 + " && echo $CURRENT_APP " + + #: Assign focused application identifier to ``CURRENT_APP`` variable (for a Google TV device) CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV = ( "CURRENT_APP=$(dumpsys activity a . | grep mResumedActivity) && " + CMD_PARSE_CURRENT_APP @@ -201,6 +213,9 @@ class DeviceEnum(IntEnum): #: Determine the current app and get the state from ``dumpsys media_session`` for an Android 13 device CMD_CURRENT_APP_MEDIA_SESSION_STATE13 = CMD_CURRENT_APP13 + " && " + CMD_MEDIA_SESSION_STATE +#: Determine the current app and get the state from ``dumpsys media_session`` for a Askey STI6130 +CMD_CURRENT_APP_MEDIA_SESSION_STATE_ASKEY_STI6130 = CMD_CURRENT_APP_ASKEY_STI6130 + " && " + CMD_MEDIA_SESSION_STATE + #: Determine the current app and get the state from ``dumpsys media_session`` for a Google TV device CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV = CMD_CURRENT_APP_GOOGLE_TV + " && " + CMD_MEDIA_SESSION_STATE @@ -241,13 +256,16 @@ class DeviceEnum(IntEnum): CMD_MODEL = "getprop ro.product.model" CMD_SERIALNO = "getprop ro.serialno" CMD_VERSION = "getprop ro.build.version.release" +CMD_PRODUCT_ID = "getprop ro.product.vendor.device" # Commands for getting the MAC address CMD_MAC_WLAN0 = "ip addr show wlan0 | grep -m 1 ether" CMD_MAC_ETH0 = "ip addr show eth0 | grep -m 1 ether" #: The command used for getting the device properties -CMD_DEVICE_PROPERTIES = CMD_MANUFACTURER + " && " + CMD_MODEL + " && " + CMD_SERIALNO + " && " + CMD_VERSION +CMD_DEVICE_PROPERTIES = ( + CMD_MANUFACTURER + " && " + CMD_MODEL + " && " + CMD_SERIALNO + " && " + CMD_VERSION + " && " + CMD_PRODUCT_ID +) # ADB key event codes diff --git a/tests/generate_test_constants.py b/tests/generate_test_constants.py index 74e5769d..7bc203e1 100644 --- a/tests/generate_test_constants.py +++ b/tests/generate_test_constants.py @@ -18,6 +18,7 @@ "CMD_DEFINE_CURRENT_APP_VARIABLE11", "CMD_DEFINE_CURRENT_APP_VARIABLE12", "CMD_DEFINE_CURRENT_APP_VARIABLE13", + "CMD_DEFINE_CURRENT_APP_VARIABLE_ASKEY_STI6130", "CMD_DEFINE_CURRENT_APP_VARIABLE_GOOGLE_TV", "CMD_LAUNCH_APP_CONDITION", "CMD_LAUNCH_APP_CONDITION_FIRETV", diff --git a/tests/test_basetv_sync.py b/tests/test_basetv_sync.py index dbb909a9..ce6e7104 100644 --- a/tests/test_basetv_sync.py +++ b/tests/test_basetv_sync.py @@ -19,6 +19,7 @@ AFTT SERIALNO 5.1.1 +test123 """ WIFIMAC_OUTPUT1 = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" @@ -31,12 +32,14 @@ "sw_version": "5.1.1", "wifimac": "ab:cd:ef:gh:ij:kl", "ethmac": None, + "product_id": "test123", } DEVICE_PROPERTIES_OUTPUT2 = """Amazon AFTT 5.1.1 +test456 """ DEVICE_PROPERTIES_DICT2 = { @@ -46,12 +49,14 @@ "sw_version": "5.1.1", "wifimac": "ab:cd:ef:gh:ij:kl", "ethmac": None, + "product_id": "test456", } DEVICE_PROPERTIES_OUTPUT3 = """Not Amazon AFTT SERIALNO 5.1.1 +test678 """ WIFIMAC_OUTPUT3 = 'Device "wlan0" does not exist.' @@ -64,6 +69,7 @@ "sw_version": "5.1.1", "wifimac": None, "ethmac": "ab:cd:ef:gh:ij:kl", + "product_id": "test678", } @@ -72,6 +78,7 @@ Chromecast SERIALNO 10 +ccwgtv1 """ WIFIMAC_GOOGLE = " link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff" @@ -81,6 +88,7 @@ BRAVIA 4K GB SERIALNO 8.0.0 +sonytestproduct """ WIFIMAC_SONY = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" @@ -93,12 +101,14 @@ "sw_version": "8.0.0", "wifimac": "11:22:33:44:55:66", "ethmac": "ab:cd:ef:gh:ij:kl", + "product_id": "sonytestproduct", } DEVICE_PROPERTIES_OUTPUT_SHIELD_TV_11 = """NVIDIA SHIELD Android TV 0123456789012 11 +nvshield11 """ WIFIMAC_SHIELD_TV_11 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" @@ -108,6 +118,7 @@ SHIELD Android TV 0123456789012 12 +nvshield12 """ WIFIMAC_SHIELD_TV_12 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" @@ -117,6 +128,7 @@ SHIELD Android TV 0123456789012 13 +nvshield13 """ WIFIMAC_SHIELD_TV_13 = " link/ether 11:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff" @@ -129,6 +141,7 @@ "sw_version": "11", "wifimac": "11:22:33:44:55:66", "ethmac": "ab:cd:ef:gh:ij:kl", + "product_id": "nvshield11", } DEVICE_PROPERTIES_DICT_SHIELD_TV_12 = { @@ -138,6 +151,7 @@ "sw_version": "12", "wifimac": "11:22:33:44:55:66", "ethmac": "ab:cd:ef:gh:ij:kl", + "product_id": "nvshield12", } DEVICE_PROPERTIES_DICT_SHIELD_TV_13 = { @@ -147,6 +161,7 @@ "sw_version": "13", "wifimac": "11:22:33:44:55:66", "ethmac": "ab:cd:ef:gh:ij:kl", + "product_id": "nvshield13", } INSTALLED_APPS_OUTPUT_1 = """package:org.example.app diff --git a/tests/test_constants.py b/tests/test_constants.py index b2bf790a..59f0df71 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -100,6 +100,11 @@ def test_constants(self): constants.CMD_CURRENT_APP13, r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP", ) + # CMD_CURRENT_APP_ASKEY_STI6130 + self.assertCommand( + constants.CMD_CURRENT_APP_ASKEY_STI6130, + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP ", + ) # CMD_CURRENT_APP_GOOGLE_TV self.assertCommand( @@ -130,6 +135,11 @@ def test_constants(self): constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE13, r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", ) + # CMD_CURRENT_APP_MEDIA_SESSION_STATE_ASKEY_STI6130 + self.assertCommand( + constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE_ASKEY_STI6130, + r"CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* } && echo $CURRENT_APP && dumpsys media_session | grep -A 100 'Sessions Stack' | grep -A 100 $CURRENT_APP | grep -m 1 'state=PlaybackState {'", + ) # CMD_CURRENT_APP_MEDIA_SESSION_STATE_GOOGLE_TV self.assertCommand( @@ -140,7 +150,7 @@ def test_constants(self): # CMD_DEVICE_PROPERTIES self.assertCommand( constants.CMD_DEVICE_PROPERTIES, - r"getprop ro.product.manufacturer && getprop ro.product.model && getprop ro.serialno && getprop ro.build.version.release", + r"getprop ro.product.manufacturer && getprop ro.product.model && getprop ro.serialno && getprop ro.build.version.release && getprop ro.product.vendor.device", ) # CMD_HDMI_INPUT @@ -212,6 +222,9 @@ def test_constants(self): # CMD_MODEL self.assertCommand(constants.CMD_MODEL, r"getprop ro.product.model") + # CMD_PRODUCT_ID + self.assertCommand(constants.CMD_PRODUCT_ID, r"getprop ro.product.vendor.device") + # CMD_RUNNING_APPS self.assertCommand(constants.CMD_RUNNING_APPS, r"ps -A | grep u0_a") diff --git a/tests/test_setup_async.py b/tests/test_setup_async.py index e4ac1c74..09c33d79 100644 --- a/tests/test_setup_async.py +++ b/tests/test_setup_async.py @@ -13,7 +13,7 @@ from .async_wrapper import awaiter -DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n123" +DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n123\namazon123" DEVICE_PROPERTIES_DICT1 = { "manufacturer": "Amazon", @@ -22,9 +22,10 @@ "sw_version": "123", "wifimac": None, "ethmac": None, + "product_id": "amazon123", } -DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n456" +DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n456\nnotamazon456" DEVICE_PROPERTIES_DICT2 = { "manufacturer": "Not Amazon", @@ -33,6 +34,7 @@ "sw_version": "456", "wifimac": None, "ethmac": None, + "product_id": "notamazon456", } diff --git a/tests/test_setup_sync.py b/tests/test_setup_sync.py index 8f9af3d9..ad9f3105 100644 --- a/tests/test_setup_sync.py +++ b/tests/test_setup_sync.py @@ -17,7 +17,7 @@ from . import patchers -DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n123" +DEVICE_PROPERTIES_OUTPUT1 = "Amazon\n\n\n123\namazon123" DEVICE_PROPERTIES_DICT1 = { "manufacturer": "Amazon", @@ -26,9 +26,10 @@ "sw_version": "123", "wifimac": None, "ethmac": None, + "product_id": "amazon123", } -DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n456" +DEVICE_PROPERTIES_OUTPUT2 = "Not Amazon\n\n\n456\nnotamazon456" DEVICE_PROPERTIES_DICT2 = { "manufacturer": "Not Amazon", @@ -37,6 +38,7 @@ "sw_version": "456", "wifimac": None, "ethmac": None, + "product_id": "notamazon456", } From 99e7967c2403f7f73be1f026e6754588ac1f20a3 Mon Sep 17 00:00:00 2001 From: K900 Date: Sun, 10 Nov 2024 09:56:18 +0300 Subject: [PATCH 65/66] Add Android 14 support (#355) Only tested on the Google TV Streamer for now, so the HDMI input stuff may need further tweaking once this actually rolls out to things with HDMI inputs. --- androidtv/basetv/basetv.py | 71 +++++++++++++++++++------------------- androidtv/constants.py | 12 +++---- tests/test_constants.py | 2 +- 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/androidtv/basetv/basetv.py b/androidtv/basetv/basetv.py index 3499abb9..50bfecb0 100644 --- a/androidtv/basetv/basetv.py +++ b/androidtv/basetv/basetv.py @@ -134,17 +134,15 @@ def _cmd_audio_state(self): if constants.CUSTOM_AUDIO_STATE in self._custom_commands: return self._custom_commands[constants.CUSTOM_AUDIO_STATE] - # Is this an Android 11 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": - return constants.CMD_AUDIO_STATE11 - - # Is this an Android 12 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": + # Is this an Android 11-14 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") in [ + "11", + "12", + "13", + "14", + ]: return constants.CMD_AUDIO_STATE11 - # Is this an Android 13 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": - return constants.CMD_AUDIO_STATE11 return constants.CMD_AUDIO_STATE def _cmd_current_app(self): @@ -183,8 +181,11 @@ def _cmd_current_app(self): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": return constants.CMD_CURRENT_APP12 - # Is this an Android 13 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + # Is this an Android 13/14 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") in [ + "13", + "14", + ]: return constants.CMD_CURRENT_APP13 return constants.CMD_CURRENT_APP @@ -225,8 +226,11 @@ def _cmd_current_app_media_session_state(self): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE12 - # Is this an Android 13 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + # Is this an Android 13/14 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") in [ + "13", + "14", + ]: return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE13 return constants.CMD_CURRENT_APP_MEDIA_SESSION_STATE @@ -243,16 +247,13 @@ def _cmd_hdmi_input(self): if constants.CUSTOM_HDMI_INPUT in self._custom_commands: return self._custom_commands[constants.CUSTOM_HDMI_INPUT] - # Is this an Android 11 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": - return constants.CMD_HDMI_INPUT11 - - # Is this an Android 12 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": - return constants.CMD_HDMI_INPUT11 - - # Is this an Android 13 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + # Is this an Android 11-14 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") in [ + "11", + "12", + "13", + "14", + ]: return constants.CMD_HDMI_INPUT11 return constants.CMD_HDMI_INPUT @@ -271,16 +272,13 @@ def _cmd_volume_set(self, new_volume): The device-specific ADB shell command used to set volume """ - # Is this an Android 11 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "11": - return constants.CMD_VOLUME_SET_COMMAND11.format(new_volume) - - # Is this an Android 12 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": - return constants.CMD_VOLUME_SET_COMMAND11.format(new_volume) - - # Is this an Android 13 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + # Is this an Android 11-14 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") in [ + "11", + "12", + "13", + "14", + ]: return constants.CMD_VOLUME_SET_COMMAND11.format(new_volume) return constants.CMD_VOLUME_SET_COMMAND.format(new_volume) @@ -321,8 +319,11 @@ def _cmd_launch_app(self, app): if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "12": return constants.CMD_LAUNCH_APP12.format(app) - # Is this an Android 13 device? - if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") == "13": + # Is this an Android 13-14 device? + if self.DEVICE_ENUM == constants.DeviceEnum.ANDROIDTV and self.device_properties.get("sw_version", "") in [ + "13", + "14", + ]: return constants.CMD_LAUNCH_APP13.format(app) return constants.CMD_LAUNCH_APP.format(app) diff --git a/androidtv/constants.py b/androidtv/constants.py index b1729ec2..6fa36bb3 100644 --- a/androidtv/constants.py +++ b/androidtv/constants.py @@ -74,9 +74,9 @@ class DeviceEnum(IntEnum): #: Get the audio state CMD_AUDIO_STATE = r"dumpsys audio | grep paused | grep -qv 'Buffer Queue' && echo -e '1\c' || (dumpsys audio | grep started | grep -qv 'Buffer Queue' && echo '2\c' || echo '0\c')" -#: Get the audio state for an Android 11 device +#: Get the audio state for an Android 11+ device CMD_AUDIO_STATE11 = ( - "CURRENT_AUDIO_STATE=$(dumpsys audio | sed -r -n '/[0-9]{2}-[0-9]{2}.*player piid:.*state:.*$/h; ${x;p;}') && " + "CURRENT_AUDIO_STATE=$(dumpsys audio | sed -r -n '/[0-9]{2}-[0-9]{2}.*player piid:.*(state|event):(started|paused|stopped).*$/h; ${x;p;}') && " + r"echo $CURRENT_AUDIO_STATE | grep -q paused && echo -e '1\c' || { echo $CURRENT_AUDIO_STATE | grep -q started && echo '2\c' || echo '0\c' ; }" ) @@ -86,7 +86,7 @@ class DeviceEnum(IntEnum): #: Parse current application identifier from dumpsys output and assign it to ``CURRENT_APP`` variable (assumes dumpsys output is momentarily set to ``CURRENT_APP`` variable) CMD_PARSE_CURRENT_APP = "CURRENT_APP=${CURRENT_APP#*ActivityRecord{* * } && CURRENT_APP=${CURRENT_APP#*{* * } && CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP%\\}*}" -#: Parse current application for an Android 11 device +#: Parse current application for an Android 11+ device CMD_PARSE_CURRENT_APP11 = "CURRENT_APP=${CURRENT_APP%%/*} && CURRENT_APP=${CURRENT_APP##* }" #: Assign focused application identifier to ``CURRENT_APP`` variable CMD_DEFINE_CURRENT_APP_VARIABLE = ( @@ -103,7 +103,7 @@ class DeviceEnum(IntEnum): + CMD_PARSE_CURRENT_APP11 ) -#: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 13 device +#: Assign focused application identifier to ``CURRENT_APP`` variable for an Android 13+ device CMD_DEFINE_CURRENT_APP_VARIABLE13 = ( "CURRENT_APP=$(dumpsys window windows | grep -E -m 1 'imeLayeringTarget|imeInputTarget|imeControlTarget') && " + CMD_PARSE_CURRENT_APP11 @@ -118,7 +118,7 @@ class DeviceEnum(IntEnum): #: Output identifier for current/focused application for an Android 12 device CMD_CURRENT_APP12 = CMD_DEFINE_CURRENT_APP_VARIABLE12 + " && echo $CURRENT_APP" -#: Output identifier for current/focused application for an Android 13 device +#: Output identifier for current/focused application for an Android 13+ device CMD_CURRENT_APP13 = CMD_DEFINE_CURRENT_APP_VARIABLE13 + " && echo $CURRENT_APP" @@ -144,7 +144,7 @@ class DeviceEnum(IntEnum): #: set volume CMD_VOLUME_SET_COMMAND = "media volume --show --stream 3 --set {}" -#: set volume for an Android 11 & 12 & 13 device +#: set volume for an Android 11+ device CMD_VOLUME_SET_COMMAND11 = "cmd media_session volume --show --stream 3 --set {}" #: Get the HDMI input diff --git a/tests/test_constants.py b/tests/test_constants.py index 59f0df71..d2f9ca5f 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -71,7 +71,7 @@ def test_constants(self): # CMD_AUDIO_STATE11 self.assertCommand( constants.CMD_AUDIO_STATE11, - r"CURRENT_AUDIO_STATE=$(dumpsys audio | sed -r -n '/[0-9]{2}-[0-9]{2}.*player piid:.*state:.*$/h; ${x;p;}') && echo $CURRENT_AUDIO_STATE | grep -q paused && echo -e '1\c' || { echo $CURRENT_AUDIO_STATE | grep -q started && echo '2\c' || echo '0\c' ; }", + r"CURRENT_AUDIO_STATE=$(dumpsys audio | sed -r -n '/[0-9]{2}-[0-9]{2}.*player piid:.*(state|event):(started|paused|stopped).*$/h; ${x;p;}') && echo $CURRENT_AUDIO_STATE | grep -q paused && echo -e '1\c' || { echo $CURRENT_AUDIO_STATE | grep -q started && echo '2\c' || echo '0\c' ; }", ) # CMD_AWAKE From 343b74ea7bb3d159f8a715190b4b9d8c00c2c0fd Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 9 Nov 2024 23:00:36 -0800 Subject: [PATCH 66/66] Bump the version to 0.0.75 (#356) --- androidtv/__init__.py | 2 +- docs/source/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/androidtv/__init__.py b/androidtv/__init__.py index 5c528339..dca64c5e 100644 --- a/androidtv/__init__.py +++ b/androidtv/__init__.py @@ -10,7 +10,7 @@ from .firetv.firetv_sync import FireTVSync -__version__ = "0.0.74" +__version__ = "0.0.75" def setup( diff --git a/docs/source/conf.py b/docs/source/conf.py index 5e9c0bb5..01c51b74 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,9 +26,9 @@ author = 'Jeff Irion' # The short X.Y version -version = '0.0.74' +version = '0.0.75' # The full version, including alpha/beta/rc tags -release = '0.0.74' +release = '0.0.75' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 25a9d6e4..969696d0 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="androidtv", - version="0.0.74", + version="0.0.75", description="Communicate with an Android TV or Fire TV device via ADB over a network.", long_description=readme, keywords=["adb", "android", "androidtv", "firetv"],