androidtvremote2 0.2.3__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {androidtvremote2-0.2.3/src/androidtvremote2.egg-info → androidtvremote2-0.3.0}/PKG-INFO +17 -10
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/README.md +9 -8
- androidtvremote2-0.3.0/pyproject.toml +98 -0
- androidtvremote2-0.3.0/setup.cfg +4 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/__init__.py +2 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/androidtv_remote.py +60 -60
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/base.py +6 -6
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/pairing.py +6 -20
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/remote.py +140 -21
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/remotemessage_pb2.py +42 -42
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/remotemessage_pb2.pyi +25 -0
- androidtvremote2-0.3.0/src/androidtvremote2/voice_stream.py +74 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0/src/androidtvremote2.egg-info}/PKG-INFO +17 -10
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2.egg-info/SOURCES.txt +1 -1
- androidtvremote2-0.3.0/src/androidtvremote2.egg-info/requires.txt +11 -0
- androidtvremote2-0.3.0/src/demo.py +408 -0
- androidtvremote2-0.2.3/pyproject.toml +0 -79
- androidtvremote2-0.2.3/setup.cfg +0 -16
- androidtvremote2-0.2.3/src/androidtvremote2.egg-info/requires.txt +0 -3
- androidtvremote2-0.2.3/src/demo.py +0 -248
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/LICENSE +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/certificate_generator.py +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/const.py +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/exceptions.py +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/model.py +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/polo_pb2.py +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/polo_pb2.pyi +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2/py.typed +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2.egg-info/dependency_links.txt +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/src/androidtvremote2.egg-info/top_level.txt +0 -0
- {androidtvremote2-0.2.3 → androidtvremote2-0.3.0}/tests/test_androidtv_remote.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidtvremote2
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python library for interacting with Android TV using the Android TV Remote protocol v2
|
|
5
5
|
Author-email: tronikos <tronikos@gmail.com>
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/tronikos/androidtvremote2
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/tronikos/androidtvremote2/issues
|
|
9
9
|
Requires-Python: >=3.10
|
|
@@ -12,6 +12,12 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: aiofiles>=0.8
|
|
13
13
|
Requires-Dist: cryptography>=3
|
|
14
14
|
Requires-Dist: protobuf>=4.21
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest; extra == "test"
|
|
17
|
+
Provides-Extra: demo
|
|
18
|
+
Requires-Dist: pyaudio; extra == "demo"
|
|
19
|
+
Requires-Dist: pynput; extra == "demo"
|
|
20
|
+
Requires-Dist: zeroconf; extra == "demo"
|
|
15
21
|
Dynamic: license-file
|
|
16
22
|
|
|
17
23
|
# androidtvremote2
|
|
@@ -22,6 +28,8 @@ For a list of the most common commands you can send to the Android TV see: [TvKe
|
|
|
22
28
|
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/b4c49ac03043b1b9c40c2f2960e466d5a3b8bd67/src/androidtvremote2/remotemessage.proto#L90).
|
|
23
29
|
In addition to commands you can send URLs to open apps registered to handle them. See [this guide](https://community.home-assistant.io/t/android-tv-remote-app-links-deep-linking-guide/567921) for how to find deep links for apps.
|
|
24
30
|
|
|
31
|
+
Voice commands can also be sent as PCM 16-bit mono 8 kHz audio data.
|
|
32
|
+
|
|
25
33
|
## Credits
|
|
26
34
|
|
|
27
35
|
- Official [implementation](https://android.googlesource.com/platform/external/google-tv-pairing-protocol/+/refs/heads/master) of the pairing protocol in Java
|
|
@@ -45,7 +53,7 @@ source .venv/bin/activate
|
|
|
45
53
|
|
|
46
54
|
# Install dependencies
|
|
47
55
|
python -m pip install --upgrade pip
|
|
48
|
-
python -m pip install .
|
|
56
|
+
python -m pip install -e .
|
|
49
57
|
|
|
50
58
|
# Generate *_pb2.py from *.proto
|
|
51
59
|
python -m pip install grpcio-tools mypy-protobuf
|
|
@@ -53,23 +61,22 @@ python -m grpc_tools.protoc src/androidtvremote2/*.proto --python_out=src/androi
|
|
|
53
61
|
|
|
54
62
|
# Run pre-commit
|
|
55
63
|
python -m pip install pre-commit
|
|
56
|
-
pre-commit autoupdate
|
|
57
64
|
pre-commit install
|
|
58
65
|
pre-commit run --all-files
|
|
59
66
|
|
|
60
|
-
# Alternative: run formatter, lint, and type checking
|
|
61
|
-
python -m pip install isort black flake8 ruff mypy
|
|
62
|
-
isort . ; black . ; flake8 . ; ruff check . --fix ; mypy --install-types .
|
|
63
|
-
|
|
64
67
|
# Run tests
|
|
65
|
-
python -m pip install
|
|
68
|
+
python -m pip install -e ".[test]"
|
|
66
69
|
pytest
|
|
67
70
|
|
|
68
71
|
# Run demo
|
|
69
|
-
python -m pip install
|
|
72
|
+
python -m pip install -e ".[demo]"
|
|
70
73
|
python src/demo.py
|
|
71
74
|
|
|
72
75
|
# Build package
|
|
73
76
|
python -m pip install build
|
|
74
77
|
python -m build
|
|
75
78
|
```
|
|
79
|
+
|
|
80
|
+
The voice demo requires the [PyAudio](https://pypi.org/project/PyAudio/) library.
|
|
81
|
+
Depending on the target platform, [PortAudio](https://www.portaudio.com/) might have to be installed manually,
|
|
82
|
+
see [PyAudio installation](https://people.csail.mit.edu/hubert/pyaudio/) for more information.
|
|
@@ -6,6 +6,8 @@ For a list of the most common commands you can send to the Android TV see: [TvKe
|
|
|
6
6
|
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/b4c49ac03043b1b9c40c2f2960e466d5a3b8bd67/src/androidtvremote2/remotemessage.proto#L90).
|
|
7
7
|
In addition to commands you can send URLs to open apps registered to handle them. See [this guide](https://community.home-assistant.io/t/android-tv-remote-app-links-deep-linking-guide/567921) for how to find deep links for apps.
|
|
8
8
|
|
|
9
|
+
Voice commands can also be sent as PCM 16-bit mono 8 kHz audio data.
|
|
10
|
+
|
|
9
11
|
## Credits
|
|
10
12
|
|
|
11
13
|
- Official [implementation](https://android.googlesource.com/platform/external/google-tv-pairing-protocol/+/refs/heads/master) of the pairing protocol in Java
|
|
@@ -29,7 +31,7 @@ source .venv/bin/activate
|
|
|
29
31
|
|
|
30
32
|
# Install dependencies
|
|
31
33
|
python -m pip install --upgrade pip
|
|
32
|
-
python -m pip install .
|
|
34
|
+
python -m pip install -e .
|
|
33
35
|
|
|
34
36
|
# Generate *_pb2.py from *.proto
|
|
35
37
|
python -m pip install grpcio-tools mypy-protobuf
|
|
@@ -37,23 +39,22 @@ python -m grpc_tools.protoc src/androidtvremote2/*.proto --python_out=src/androi
|
|
|
37
39
|
|
|
38
40
|
# Run pre-commit
|
|
39
41
|
python -m pip install pre-commit
|
|
40
|
-
pre-commit autoupdate
|
|
41
42
|
pre-commit install
|
|
42
43
|
pre-commit run --all-files
|
|
43
44
|
|
|
44
|
-
# Alternative: run formatter, lint, and type checking
|
|
45
|
-
python -m pip install isort black flake8 ruff mypy
|
|
46
|
-
isort . ; black . ; flake8 . ; ruff check . --fix ; mypy --install-types .
|
|
47
|
-
|
|
48
45
|
# Run tests
|
|
49
|
-
python -m pip install
|
|
46
|
+
python -m pip install -e ".[test]"
|
|
50
47
|
pytest
|
|
51
48
|
|
|
52
49
|
# Run demo
|
|
53
|
-
python -m pip install
|
|
50
|
+
python -m pip install -e ".[demo]"
|
|
54
51
|
python src/demo.py
|
|
55
52
|
|
|
56
53
|
# Build package
|
|
57
54
|
python -m pip install build
|
|
58
55
|
python -m build
|
|
59
56
|
```
|
|
57
|
+
|
|
58
|
+
The voice demo requires the [PyAudio](https://pypi.org/project/PyAudio/) library.
|
|
59
|
+
Depending on the target platform, [PortAudio](https://www.portaudio.com/) might have to be installed manually,
|
|
60
|
+
see [PyAudio installation](https://people.csail.mit.edu/hubert/pyaudio/) for more information.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "androidtvremote2"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
license = "Apache-2.0"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name="tronikos", email="tronikos@gmail.com" },
|
|
7
|
+
]
|
|
8
|
+
description = "A Python library for interacting with Android TV using the Android TV Remote protocol v2"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"aiofiles>=0.8",
|
|
13
|
+
"cryptography>=3",
|
|
14
|
+
"protobuf>=4.21",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
test = [
|
|
19
|
+
"pytest",
|
|
20
|
+
]
|
|
21
|
+
demo = [
|
|
22
|
+
"pyaudio",
|
|
23
|
+
"pynput",
|
|
24
|
+
"zeroconf",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
"Homepage" = "https://github.com/tronikos/androidtvremote2"
|
|
30
|
+
"Bug Tracker" = "https://github.com/tronikos/androidtvremote2/issues"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["setuptools"]
|
|
34
|
+
build-backend = "setuptools.build_meta"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
target-version = "py310"
|
|
38
|
+
line-length = 127
|
|
39
|
+
exclude = [
|
|
40
|
+
".git", ".mypy_cache", ".ruff_cache", ".venv", "__pypackages__",
|
|
41
|
+
"build", "dist", "venv", "*_pb2.py", "*_pb2_grpc.py", "*.pyi"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = [
|
|
46
|
+
"A", # flake8-builtins
|
|
47
|
+
"ASYNC", # flake8-async
|
|
48
|
+
"B", # flake8-bugbear
|
|
49
|
+
"C", # flake8-mccabe
|
|
50
|
+
"C4", # flake8-comprehensions
|
|
51
|
+
"D", # pydocstyle
|
|
52
|
+
"E", # pycodestyle (error)
|
|
53
|
+
"F", # pyflakes
|
|
54
|
+
"G", # flake8-logging-format
|
|
55
|
+
"I", # isort
|
|
56
|
+
"PGH", # pygrep-hooks
|
|
57
|
+
"PL", # Pylint
|
|
58
|
+
"RET", # flake8-return
|
|
59
|
+
"RUF", # Ruff-specific rules
|
|
60
|
+
"S", # flake8-bandit (security)
|
|
61
|
+
"SIM", # flake8-simplify
|
|
62
|
+
"T20", # flake8-print
|
|
63
|
+
"TCH", # flake8-type-checking
|
|
64
|
+
"TRY", # tryceratops
|
|
65
|
+
"UP", # pyupgrade
|
|
66
|
+
"W", # pycodestyle (warning)
|
|
67
|
+
]
|
|
68
|
+
ignore = [
|
|
69
|
+
"S101", # Ignore assert
|
|
70
|
+
"D203", # 1 blank line required before class docstring
|
|
71
|
+
"D213", # Multi-line docstring summary should start at the second line
|
|
72
|
+
"PLR0913", # Too many arguments in function definition
|
|
73
|
+
"PLR1714", # Consider merging multiple comparisons
|
|
74
|
+
"PLR2004", # Magic value used in comparison
|
|
75
|
+
"TRY003", # Avoid specifying long messages outside the exception class
|
|
76
|
+
]
|
|
77
|
+
fixable = ["ALL"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.mccabe]
|
|
80
|
+
max-complexity = 25
|
|
81
|
+
|
|
82
|
+
[tool.mypy]
|
|
83
|
+
python_version = "3.10"
|
|
84
|
+
exclude = [".venv", "venv", "build"]
|
|
85
|
+
pretty = true
|
|
86
|
+
strict = true
|
|
87
|
+
show_error_codes = true
|
|
88
|
+
warn_incomplete_stub = true
|
|
89
|
+
enable_error_code = ["ignore-without-code", "redundant-self", "truthy-iterable"]
|
|
90
|
+
|
|
91
|
+
[[tool.mypy.overrides]]
|
|
92
|
+
module = ["pyaudio"]
|
|
93
|
+
ignore_missing_imports = true
|
|
94
|
+
|
|
95
|
+
[tool.codespell]
|
|
96
|
+
skip = "*.json,*.csv,*.lock,./.git/*,./.venv/*"
|
|
97
|
+
check-filenames = true
|
|
98
|
+
check-hidden = true
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .androidtv_remote import AndroidTVRemote
|
|
4
4
|
from .exceptions import CannotConnect, ConnectionClosed, InvalidAuth
|
|
5
5
|
from .model import DeviceInfo, VolumeInfo
|
|
6
|
+
from .voice_stream import VoiceStream
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"AndroidTVRemote",
|
|
@@ -10,5 +11,6 @@ __all__ = [
|
|
|
10
11
|
"ConnectionClosed",
|
|
11
12
|
"DeviceInfo",
|
|
12
13
|
"InvalidAuth",
|
|
14
|
+
"VoiceStream",
|
|
13
15
|
"VolumeInfo",
|
|
14
16
|
]
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from collections.abc import Callable
|
|
7
6
|
import os
|
|
8
7
|
import ssl
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
9
|
from urllib.parse import urlparse
|
|
10
10
|
|
|
11
11
|
import aiofiles
|
|
@@ -14,10 +14,15 @@ from cryptography import x509
|
|
|
14
14
|
from .certificate_generator import generate_selfsigned_cert
|
|
15
15
|
from .const import LOGGER
|
|
16
16
|
from .exceptions import CannotConnect, ConnectionClosed, InvalidAuth
|
|
17
|
-
from .model import DeviceInfo, VolumeInfo
|
|
18
17
|
from .pairing import PairingProtocol
|
|
19
|
-
from .remote import RemoteProtocol
|
|
18
|
+
from .remote import VOICE_SESSION_TIMEOUT, RemoteProtocol
|
|
20
19
|
from .remotemessage_pb2 import RemoteDirection
|
|
20
|
+
from .voice_stream import VoiceStream
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
from .model import DeviceInfo, VolumeInfo
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def _load_cert_chain(certfile: str, keyfile: str) -> ssl.SSLContext:
|
|
@@ -41,6 +46,7 @@ class AndroidTVRemote:
|
|
|
41
46
|
pair_port: int = 6467,
|
|
42
47
|
loop: asyncio.AbstractEventLoop | None = None,
|
|
43
48
|
enable_ime: bool = True,
|
|
49
|
+
enable_voice: bool = False,
|
|
44
50
|
) -> None:
|
|
45
51
|
"""Initialize.
|
|
46
52
|
|
|
@@ -53,6 +59,7 @@ class AndroidTVRemote:
|
|
|
53
59
|
:param loop: event loop. Used for connections and futures.
|
|
54
60
|
:param enable_ime: Needed for getting current_app.
|
|
55
61
|
Disable for devices that show 'Use keyboard on mobile device screen'.
|
|
62
|
+
:param enable_voice: Enable sending voice commands to the device.
|
|
56
63
|
"""
|
|
57
64
|
self._client_name = client_name
|
|
58
65
|
self._certfile = certfile
|
|
@@ -62,6 +69,7 @@ class AndroidTVRemote:
|
|
|
62
69
|
self._pair_port = pair_port
|
|
63
70
|
self._loop = loop or asyncio.get_running_loop()
|
|
64
71
|
self._enable_ime = enable_ime
|
|
72
|
+
self._enable_voice = enable_voice
|
|
65
73
|
self._transport: asyncio.Transport | None = None
|
|
66
74
|
self._remote_message_protocol: RemoteProtocol | None = None
|
|
67
75
|
self._pairing_message_protocol: PairingProtocol | None = None
|
|
@@ -121,6 +129,17 @@ class AndroidTVRemote:
|
|
|
121
129
|
return None
|
|
122
130
|
return self._remote_message_protocol.volume_info
|
|
123
131
|
|
|
132
|
+
@property
|
|
133
|
+
def is_voice_enabled(self) -> bool | None:
|
|
134
|
+
"""Whether voice commands are enabled on the Android TV.
|
|
135
|
+
|
|
136
|
+
Depends on the requested feature at AndroidTVRemote initialization and the supported
|
|
137
|
+
features of the device.
|
|
138
|
+
"""
|
|
139
|
+
if not self._remote_message_protocol:
|
|
140
|
+
return None
|
|
141
|
+
return self._remote_message_protocol.is_voice_enabled
|
|
142
|
+
|
|
124
143
|
def add_is_on_updated_callback(self, callback: Callable[[bool], None]) -> None:
|
|
125
144
|
"""Add a callback for when is_on is updated."""
|
|
126
145
|
self._is_on_updated_callbacks.append(callback)
|
|
@@ -136,39 +155,29 @@ class AndroidTVRemote:
|
|
|
136
155
|
"""Add a callback for when current_app is updated."""
|
|
137
156
|
self._current_app_updated_callbacks.append(callback)
|
|
138
157
|
|
|
139
|
-
def remove_current_app_updated_callback(
|
|
140
|
-
self, callback: Callable[[str], None]
|
|
141
|
-
) -> None:
|
|
158
|
+
def remove_current_app_updated_callback(self, callback: Callable[[str], None]) -> None:
|
|
142
159
|
"""Remove a callback previously added via add_current_app_updated_callback.
|
|
143
160
|
|
|
144
161
|
:raises ValueError: if callback not previously added.
|
|
145
162
|
"""
|
|
146
163
|
self._current_app_updated_callbacks.remove(callback)
|
|
147
164
|
|
|
148
|
-
def add_volume_info_updated_callback(
|
|
149
|
-
self, callback: Callable[[VolumeInfo], None]
|
|
150
|
-
) -> None:
|
|
165
|
+
def add_volume_info_updated_callback(self, callback: Callable[[VolumeInfo], None]) -> None:
|
|
151
166
|
"""Add a callback for when volume_info is updated."""
|
|
152
167
|
self._volume_info_updated_callbacks.append(callback)
|
|
153
168
|
|
|
154
|
-
def remove_volume_info_updated_callback(
|
|
155
|
-
self, callback: Callable[[VolumeInfo], None]
|
|
156
|
-
) -> None:
|
|
169
|
+
def remove_volume_info_updated_callback(self, callback: Callable[[VolumeInfo], None]) -> None:
|
|
157
170
|
"""Remove a callback previously added via add_volume_info_updated_callback.
|
|
158
171
|
|
|
159
172
|
:raises ValueError: if callback not previously added.
|
|
160
173
|
"""
|
|
161
174
|
self._volume_info_updated_callbacks.remove(callback)
|
|
162
175
|
|
|
163
|
-
def add_is_available_updated_callback(
|
|
164
|
-
self, callback: Callable[[bool], None]
|
|
165
|
-
) -> None:
|
|
176
|
+
def add_is_available_updated_callback(self, callback: Callable[[bool], None]) -> None:
|
|
166
177
|
"""Add a callback for when the Android TV is ready to receive commands or is unavailable."""
|
|
167
178
|
self._is_available_updated_callbacks.append(callback)
|
|
168
179
|
|
|
169
|
-
def remove_is_available_updated_callback(
|
|
170
|
-
self, callback: Callable[[bool], None]
|
|
171
|
-
) -> None:
|
|
180
|
+
def remove_is_available_updated_callback(self, callback: Callable[[bool], None]) -> None:
|
|
172
181
|
"""Remove a callback previously added via add_is_available_updated_callback.
|
|
173
182
|
|
|
174
183
|
:raises ValueError: if callback not previously added.
|
|
@@ -193,9 +202,7 @@ class AndroidTVRemote:
|
|
|
193
202
|
if self._ssl_context:
|
|
194
203
|
return self._ssl_context
|
|
195
204
|
try:
|
|
196
|
-
ssl_context = await self._loop.run_in_executor(
|
|
197
|
-
None, _load_cert_chain, self._certfile, self._keyfile
|
|
198
|
-
)
|
|
205
|
+
ssl_context = await self._loop.run_in_executor(None, _load_cert_chain, self._certfile, self._keyfile)
|
|
199
206
|
except FileNotFoundError as exc:
|
|
200
207
|
LOGGER.debug("Missing certificate. Error: %s", exc)
|
|
201
208
|
raise InvalidAuth from exc
|
|
@@ -225,24 +232,19 @@ class AndroidTVRemote:
|
|
|
225
232
|
self._on_volume_info_updated,
|
|
226
233
|
self._loop,
|
|
227
234
|
self._enable_ime,
|
|
235
|
+
self._enable_voice,
|
|
228
236
|
),
|
|
229
237
|
self.host,
|
|
230
238
|
self._api_port,
|
|
231
239
|
ssl=ssl_context,
|
|
232
240
|
)
|
|
233
241
|
except OSError as exc:
|
|
234
|
-
LOGGER.debug(
|
|
235
|
-
"Couldn't connect to %s:%s. Error: %s", self.host, self._api_port, exc
|
|
236
|
-
)
|
|
242
|
+
LOGGER.debug("Couldn't connect to %s:%s. Error: %s", self.host, self._api_port, exc)
|
|
237
243
|
if isinstance(exc, ssl.SSLError):
|
|
238
244
|
raise InvalidAuth("Need to pair") from exc
|
|
239
|
-
raise CannotConnect(
|
|
240
|
-
f"Couldn't connect to {self.host}:{self._api_port}"
|
|
241
|
-
) from exc
|
|
245
|
+
raise CannotConnect(f"Couldn't connect to {self.host}:{self._api_port}") from exc
|
|
242
246
|
|
|
243
|
-
await asyncio.wait(
|
|
244
|
-
(on_con_lost, on_remote_started), return_when=asyncio.FIRST_COMPLETED
|
|
245
|
-
)
|
|
247
|
+
await asyncio.wait((on_con_lost, on_remote_started), return_when=asyncio.FIRST_COMPLETED)
|
|
246
248
|
if on_con_lost.done():
|
|
247
249
|
con_lost_exc = on_con_lost.result()
|
|
248
250
|
LOGGER.debug(
|
|
@@ -255,17 +257,13 @@ class AndroidTVRemote:
|
|
|
255
257
|
raise InvalidAuth("Need to pair again") from con_lost_exc
|
|
256
258
|
raise ConnectionClosed("Connection closed") from con_lost_exc
|
|
257
259
|
|
|
258
|
-
async def _async_reconnect(
|
|
259
|
-
self, invalid_auth_callback: Callable[[], None] | None = None
|
|
260
|
-
) -> None:
|
|
260
|
+
async def _async_reconnect(self, invalid_auth_callback: Callable[[], None] | None = None) -> None:
|
|
261
261
|
while self._remote_message_protocol:
|
|
262
262
|
exc = await self._remote_message_protocol.on_con_lost
|
|
263
263
|
self._on_is_available_updated(False)
|
|
264
264
|
LOGGER.debug("Disconnected from %s. Error: %s", self.host, exc)
|
|
265
265
|
delay_seconds = 0.1
|
|
266
|
-
LOGGER.debug(
|
|
267
|
-
"Trying to reconnect to %s in %s seconds", self.host, delay_seconds
|
|
268
|
-
)
|
|
266
|
+
LOGGER.debug("Trying to reconnect to %s in %s seconds", self.host, delay_seconds)
|
|
269
267
|
while self._remote_message_protocol:
|
|
270
268
|
await asyncio.sleep(delay_seconds)
|
|
271
269
|
try:
|
|
@@ -290,13 +288,9 @@ class AndroidTVRemote:
|
|
|
290
288
|
invalid_auth_callback()
|
|
291
289
|
return
|
|
292
290
|
|
|
293
|
-
def keep_reconnecting(
|
|
294
|
-
self, invalid_auth_callback: Callable[[], None] | None = None
|
|
295
|
-
) -> None:
|
|
291
|
+
def keep_reconnecting(self, invalid_auth_callback: Callable[[], None] | None = None) -> None:
|
|
296
292
|
"""Create a task to keep reconnecting whenever connection is lost."""
|
|
297
|
-
self._reconnect_task = self._loop.create_task(
|
|
298
|
-
self._async_reconnect(invalid_auth_callback)
|
|
299
|
-
)
|
|
293
|
+
self._reconnect_task = self._loop.create_task(self._async_reconnect(invalid_auth_callback))
|
|
300
294
|
|
|
301
295
|
def disconnect(self) -> None:
|
|
302
296
|
"""Disconnect any open connections."""
|
|
@@ -318,17 +312,11 @@ class AndroidTVRemote:
|
|
|
318
312
|
"""
|
|
319
313
|
ssl_context = await self._create_ssl_context()
|
|
320
314
|
try:
|
|
321
|
-
_, writer = await asyncio.open_connection(
|
|
322
|
-
self.host, self._pair_port, ssl=ssl_context
|
|
323
|
-
)
|
|
315
|
+
_, writer = await asyncio.open_connection(self.host, self._pair_port, ssl=ssl_context)
|
|
324
316
|
except OSError as exc:
|
|
325
|
-
LOGGER.debug(
|
|
326
|
-
"Couldn't connect to %s:%s. %s", self.host, self._pair_port, exc
|
|
327
|
-
)
|
|
317
|
+
LOGGER.debug("Couldn't connect to %s:%s. %s", self.host, self._pair_port, exc)
|
|
328
318
|
raise CannotConnect from exc
|
|
329
|
-
server_cert_bytes = writer.transport.get_extra_info("ssl_object").getpeercert(
|
|
330
|
-
True
|
|
331
|
-
)
|
|
319
|
+
server_cert_bytes = writer.transport.get_extra_info("ssl_object").getpeercert(True)
|
|
332
320
|
writer.close()
|
|
333
321
|
server_cert = x509.load_der_x509_certificate(server_cert_bytes)
|
|
334
322
|
# NVIDIA SHIELD example:
|
|
@@ -370,9 +358,7 @@ class AndroidTVRemote:
|
|
|
370
358
|
ssl=ssl_context,
|
|
371
359
|
)
|
|
372
360
|
except OSError as exc:
|
|
373
|
-
LOGGER.debug(
|
|
374
|
-
"Couldn't connect to %s:%s. %s", self.host, self._pair_port, exc
|
|
375
|
-
)
|
|
361
|
+
LOGGER.debug("Couldn't connect to %s:%s. %s", self.host, self._pair_port, exc)
|
|
376
362
|
raise CannotConnect from exc
|
|
377
363
|
await self._pairing_message_protocol.async_start_pairing()
|
|
378
364
|
|
|
@@ -389,9 +375,7 @@ class AndroidTVRemote:
|
|
|
389
375
|
await self._pairing_message_protocol.async_finish_pairing(pairing_code)
|
|
390
376
|
self.disconnect()
|
|
391
377
|
|
|
392
|
-
def send_key_command(
|
|
393
|
-
self, key_code: int | str, direction: int | str = RemoteDirection.SHORT
|
|
394
|
-
) -> None:
|
|
378
|
+
def send_key_command(self, key_code: int | str, direction: int | str = RemoteDirection.SHORT) -> None:
|
|
395
379
|
"""Send a key press to Android TV.
|
|
396
380
|
|
|
397
381
|
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
|
@@ -431,6 +415,22 @@ class AndroidTVRemote:
|
|
|
431
415
|
LOGGER.debug("Called send_launch_app_command after disconnect")
|
|
432
416
|
raise ConnectionClosed("Called send_launch_app_command after disconnect")
|
|
433
417
|
prefix = "" if urlparse(app_link_or_app_id).scheme else "market://launch?id="
|
|
434
|
-
self._remote_message_protocol.send_launch_app_command(
|
|
435
|
-
|
|
436
|
-
|
|
418
|
+
self._remote_message_protocol.send_launch_app_command(f"{prefix}{app_link_or_app_id}")
|
|
419
|
+
|
|
420
|
+
async def start_voice(self, timeout: float = VOICE_SESSION_TIMEOUT) -> VoiceStream:
|
|
421
|
+
"""Start a streaming voice session.
|
|
422
|
+
|
|
423
|
+
A ``VoiceStream`` session wrapper is returned if the voice session can be established
|
|
424
|
+
within the given timeout. The session needs to be closed with ``end()`` (or through the
|
|
425
|
+
asynchronous context manager) before a new session is started.
|
|
426
|
+
|
|
427
|
+
:param timeout: optional timeout for session readiness. Defaults to 2 seconds.
|
|
428
|
+
:raises ConnectionClosed: if client is disconnected.
|
|
429
|
+
:raises asyncio.TimeoutError: if the device does not begin voice in time, or a voice
|
|
430
|
+
session is already in progress.
|
|
431
|
+
"""
|
|
432
|
+
if not self._remote_message_protocol:
|
|
433
|
+
LOGGER.debug("Called start_voice after disconnect")
|
|
434
|
+
raise ConnectionClosed("Called start_voice after disconnect")
|
|
435
|
+
session_id = await self._remote_message_protocol.start_voice(timeout)
|
|
436
|
+
return VoiceStream(self._remote_message_protocol, session_id)
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from typing import cast
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
7
|
|
|
8
8
|
from google.protobuf import text_format
|
|
9
9
|
from google.protobuf.internal.decoder import _DecodeVarint # type: ignore[attr-defined]
|
|
10
10
|
from google.protobuf.internal.encoder import _EncodeVarint # type: ignore[attr-defined]
|
|
11
|
-
from google.protobuf.message import Message
|
|
12
11
|
|
|
13
12
|
from .const import LOGGER
|
|
14
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from google.protobuf.message import Message
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
class ProtobufProtocol(asyncio.Protocol):
|
|
17
19
|
"""Protocol for receiving and sending protobuf messages."""
|
|
@@ -29,7 +31,7 @@ class ProtobufProtocol(asyncio.Protocol):
|
|
|
29
31
|
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
|
30
32
|
"""Store transport when a connection is made."""
|
|
31
33
|
LOGGER.debug("Connected to %s", transport.get_extra_info("peername"))
|
|
32
|
-
self.transport = cast(asyncio.Transport, transport)
|
|
34
|
+
self.transport = cast("asyncio.Transport", transport)
|
|
33
35
|
|
|
34
36
|
def connection_lost(self, exc: Exception | None) -> None:
|
|
35
37
|
"""Notify on_con_lost when the connection is lost or closed."""
|
|
@@ -69,9 +71,7 @@ class ProtobufProtocol(asyncio.Protocol):
|
|
|
69
71
|
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
|
70
72
|
"""
|
|
71
73
|
if should_debug_log:
|
|
72
|
-
LOGGER.debug(
|
|
73
|
-
"Sending: %s", text_format.MessageToString(msg, as_one_line=True)
|
|
74
|
-
)
|
|
74
|
+
LOGGER.debug("Sending: %s", text_format.MessageToString(msg, as_one_line=True))
|
|
75
75
|
if not self.transport or self.transport.is_closing():
|
|
76
76
|
LOGGER.debug("Connection is closed!")
|
|
77
77
|
return
|
|
@@ -104,9 +104,7 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
104
104
|
client_modulus, client_exponent = _get_modulus_and_exponent(client_cert)
|
|
105
105
|
|
|
106
106
|
assert self.transport
|
|
107
|
-
server_cert_bytes = self.transport.get_extra_info("ssl_object").getpeercert(
|
|
108
|
-
True
|
|
109
|
-
)
|
|
107
|
+
server_cert_bytes = self.transport.get_extra_info("ssl_object").getpeercert(True)
|
|
110
108
|
server_cert = x509.load_der_x509_certificate(server_cert_bytes)
|
|
111
109
|
server_modulus, server_exponent = _get_modulus_and_exponent(server_cert)
|
|
112
110
|
|
|
@@ -131,13 +129,9 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
131
129
|
finally:
|
|
132
130
|
self._on_pairing_finished = None
|
|
133
131
|
|
|
134
|
-
async def _async_wait_for_future_or_con_lost(
|
|
135
|
-
self, future: asyncio.Future[Any]
|
|
136
|
-
) -> None:
|
|
132
|
+
async def _async_wait_for_future_or_con_lost(self, future: asyncio.Future[Any]) -> None:
|
|
137
133
|
"""Wait for future to finish or connection to be lost."""
|
|
138
|
-
await asyncio.wait(
|
|
139
|
-
(self.on_con_lost, future), return_when=asyncio.FIRST_COMPLETED
|
|
140
|
-
)
|
|
134
|
+
await asyncio.wait((self.on_con_lost, future), return_when=asyncio.FIRST_COMPLETED)
|
|
141
135
|
if future.done():
|
|
142
136
|
if future.exception():
|
|
143
137
|
raise ConnectionClosed(future.exception())
|
|
@@ -180,9 +174,7 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
180
174
|
enc.symbol_length = 6
|
|
181
175
|
elif msg.HasField("options"):
|
|
182
176
|
new_msg.configuration.client_role = Options.RoleType.ROLE_TYPE_INPUT
|
|
183
|
-
new_msg.configuration.encoding.type =
|
|
184
|
-
Options.Encoding.ENCODING_TYPE_HEXADECIMAL
|
|
185
|
-
)
|
|
177
|
+
new_msg.configuration.encoding.type = Options.Encoding.ENCODING_TYPE_HEXADECIMAL
|
|
186
178
|
new_msg.configuration.encoding.symbol_length = 6
|
|
187
179
|
elif msg.HasField("configuration_ack"):
|
|
188
180
|
if self._on_pairing_started:
|
|
@@ -193,14 +185,8 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
193
185
|
self._on_pairing_finished.set_result(True)
|
|
194
186
|
return
|
|
195
187
|
else:
|
|
196
|
-
LOGGER.debug(
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
self._handle_error(
|
|
200
|
-
Exception(
|
|
201
|
-
f"Unhandled msg: {text_format.MessageToString(msg, as_one_line=True)}"
|
|
202
|
-
)
|
|
203
|
-
)
|
|
188
|
+
LOGGER.debug("Unhandled msg: %s", text_format.MessageToString(msg, as_one_line=True))
|
|
189
|
+
self._handle_error(Exception(f"Unhandled msg: {text_format.MessageToString(msg, as_one_line=True)}"))
|
|
204
190
|
return
|
|
205
191
|
|
|
206
192
|
self._send_message(new_msg)
|