androidtvremote2 0.2.1__tar.gz → 0.2.3__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.1/src/androidtvremote2.egg-info → androidtvremote2-0.2.3}/PKG-INFO +2 -2
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/README.md +1 -1
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/pyproject.toml +4 -5
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/__init__.py +3 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/androidtv_remote.py +33 -19
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/base.py +1 -1
- androidtvremote2-0.2.3/src/androidtvremote2/model.py +21 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/pairing.py +7 -4
- androidtvremote2-0.2.3/src/androidtvremote2/py.typed +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/remote.py +18 -12
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3/src/androidtvremote2.egg-info}/PKG-INFO +2 -2
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/SOURCES.txt +2 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/demo.py +30 -26
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/LICENSE +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/setup.cfg +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/certificate_generator.py +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/const.py +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/exceptions.py +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/polo_pb2.py +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/polo_pb2.pyi +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/remotemessage_pb2.py +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/remotemessage_pb2.pyi +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/dependency_links.txt +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/requires.txt +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/top_level.txt +0 -0
- {androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/tests/test_androidtv_remote.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidtvremote2
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
6
|
License: Apache-2.0
|
|
@@ -19,7 +19,7 @@ Dynamic: license-file
|
|
|
19
19
|
A Python library for interacting with Android TV using the Android TV Remote protocol v2. This is the same protocol the Google TV mobile app is using. It doesn't require ADB or enabling developer tools on the Android TV device. It only requires the [Android TV Remote Service](https://play.google.com/store/apps/details?id=com.google.android.tv.remote.service) that comes pre-installed on most Android TV devices.
|
|
20
20
|
|
|
21
21
|
For a list of the most common commands you can send to the Android TV see: [TvKeys](https://github.com/tronikos/androidtvremote2/blob/main/TvKeys.txt).
|
|
22
|
-
For a full list see [
|
|
22
|
+
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/b4c49ac03043b1b9c40c2f2960e466d5a3b8bd67/src/androidtvremote2/remotemessage.proto#L90).
|
|
23
23
|
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
24
|
|
|
25
25
|
## Credits
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A Python library for interacting with Android TV using the Android TV Remote protocol v2. This is the same protocol the Google TV mobile app is using. It doesn't require ADB or enabling developer tools on the Android TV device. It only requires the [Android TV Remote Service](https://play.google.com/store/apps/details?id=com.google.android.tv.remote.service) that comes pre-installed on most Android TV devices.
|
|
4
4
|
|
|
5
5
|
For a list of the most common commands you can send to the Android TV see: [TvKeys](https://github.com/tronikos/androidtvremote2/blob/main/TvKeys.txt).
|
|
6
|
-
For a full list see [
|
|
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
9
|
## Credits
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "androidtvremote2"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
license = {text = "Apache-2.0"}
|
|
5
5
|
authors = [
|
|
6
6
|
{ name="tronikos", email="tronikos@gmail.com" },
|
|
@@ -32,12 +32,11 @@ combine_as_imports = true
|
|
|
32
32
|
extend_skip_glob = ["*_pb2.py", "*_pb2_grpc.py"]
|
|
33
33
|
|
|
34
34
|
[tool.ruff]
|
|
35
|
-
target-version = "
|
|
35
|
+
target-version = "py310"
|
|
36
36
|
exclude = ["*_pb2.py", "*_pb2_grpc.py", "*.pyi"]
|
|
37
37
|
line-length = 127
|
|
38
38
|
|
|
39
|
-
[
|
|
40
|
-
select = [
|
|
39
|
+
lint.select = [
|
|
41
40
|
"B007", # Loop control variable {name} not used within loop body
|
|
42
41
|
"B014", # Exception handler with duplicate exception
|
|
43
42
|
"C", # complexity
|
|
@@ -61,7 +60,7 @@ select = [
|
|
|
61
60
|
"W", # pycodestyle
|
|
62
61
|
]
|
|
63
62
|
|
|
64
|
-
ignore = [
|
|
63
|
+
lint.ignore = [
|
|
65
64
|
"D203", # 1 blank line required before class docstring
|
|
66
65
|
"D213", # Multi-line docstring summary should start at the second line
|
|
67
66
|
# keep-runtime-annotations
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from .androidtv_remote import AndroidTVRemote
|
|
4
4
|
from .exceptions import CannotConnect, ConnectionClosed, InvalidAuth
|
|
5
|
+
from .model import DeviceInfo, VolumeInfo
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"AndroidTVRemote",
|
|
8
9
|
"CannotConnect",
|
|
9
10
|
"ConnectionClosed",
|
|
11
|
+
"DeviceInfo",
|
|
10
12
|
"InvalidAuth",
|
|
13
|
+
"VolumeInfo",
|
|
11
14
|
]
|
|
@@ -14,6 +14,7 @@ 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
|
|
17
18
|
from .pairing import PairingProtocol
|
|
18
19
|
from .remote import RemoteProtocol
|
|
19
20
|
from .remotemessage_pb2 import RemoteDirection
|
|
@@ -64,12 +65,12 @@ class AndroidTVRemote:
|
|
|
64
65
|
self._transport: asyncio.Transport | None = None
|
|
65
66
|
self._remote_message_protocol: RemoteProtocol | None = None
|
|
66
67
|
self._pairing_message_protocol: PairingProtocol | None = None
|
|
67
|
-
self._reconnect_task: asyncio.Task | None = None
|
|
68
|
+
self._reconnect_task: asyncio.Task[None] | None = None
|
|
68
69
|
self._ssl_context: ssl.SSLContext | None = None
|
|
69
|
-
self._is_on_updated_callbacks: list[Callable] = []
|
|
70
|
-
self._current_app_updated_callbacks: list[Callable] = []
|
|
71
|
-
self._volume_info_updated_callbacks: list[Callable] = []
|
|
72
|
-
self._is_available_updated_callbacks: list[Callable] = []
|
|
70
|
+
self._is_on_updated_callbacks: list[Callable[[bool], None]] = []
|
|
71
|
+
self._current_app_updated_callbacks: list[Callable[[str], None]] = []
|
|
72
|
+
self._volume_info_updated_callbacks: list[Callable[[VolumeInfo], None]] = []
|
|
73
|
+
self._is_available_updated_callbacks: list[Callable[[bool], None]] = []
|
|
73
74
|
|
|
74
75
|
def is_on_updated(is_on: bool) -> None:
|
|
75
76
|
for callback in self._is_on_updated_callbacks:
|
|
@@ -79,7 +80,7 @@ class AndroidTVRemote:
|
|
|
79
80
|
for callback in self._current_app_updated_callbacks:
|
|
80
81
|
callback(current_app)
|
|
81
82
|
|
|
82
|
-
def volume_info_updated(volume_info:
|
|
83
|
+
def volume_info_updated(volume_info: VolumeInfo) -> None:
|
|
83
84
|
for callback in self._volume_info_updated_callbacks:
|
|
84
85
|
callback(volume_info)
|
|
85
86
|
|
|
@@ -107,57 +108,67 @@ class AndroidTVRemote:
|
|
|
107
108
|
return self._remote_message_protocol.current_app
|
|
108
109
|
|
|
109
110
|
@property
|
|
110
|
-
def device_info(self) ->
|
|
111
|
+
def device_info(self) -> DeviceInfo | None:
|
|
111
112
|
"""Device info (manufacturer, model, sw_version)."""
|
|
112
113
|
if not self._remote_message_protocol:
|
|
113
114
|
return None
|
|
114
115
|
return self._remote_message_protocol.device_info
|
|
115
116
|
|
|
116
117
|
@property
|
|
117
|
-
def volume_info(self) ->
|
|
118
|
+
def volume_info(self) -> VolumeInfo | None:
|
|
118
119
|
"""Volume info (level, max, muted)."""
|
|
119
120
|
if not self._remote_message_protocol:
|
|
120
121
|
return None
|
|
121
122
|
return self._remote_message_protocol.volume_info
|
|
122
123
|
|
|
123
|
-
def add_is_on_updated_callback(self, callback: Callable) -> None:
|
|
124
|
+
def add_is_on_updated_callback(self, callback: Callable[[bool], None]) -> None:
|
|
124
125
|
"""Add a callback for when is_on is updated."""
|
|
125
126
|
self._is_on_updated_callbacks.append(callback)
|
|
126
127
|
|
|
127
|
-
def remove_is_on_updated_callback(self, callback: Callable) -> None:
|
|
128
|
+
def remove_is_on_updated_callback(self, callback: Callable[[bool], None]) -> None:
|
|
128
129
|
"""Remove a callback previously added via add_is_on_updated_callback.
|
|
129
130
|
|
|
130
131
|
:raises ValueError: if callback not previously added.
|
|
131
132
|
"""
|
|
132
133
|
self._is_on_updated_callbacks.remove(callback)
|
|
133
134
|
|
|
134
|
-
def add_current_app_updated_callback(self, callback: Callable) -> None:
|
|
135
|
+
def add_current_app_updated_callback(self, callback: Callable[[str], None]) -> None:
|
|
135
136
|
"""Add a callback for when current_app is updated."""
|
|
136
137
|
self._current_app_updated_callbacks.append(callback)
|
|
137
138
|
|
|
138
|
-
def remove_current_app_updated_callback(
|
|
139
|
+
def remove_current_app_updated_callback(
|
|
140
|
+
self, callback: Callable[[str], None]
|
|
141
|
+
) -> None:
|
|
139
142
|
"""Remove a callback previously added via add_current_app_updated_callback.
|
|
140
143
|
|
|
141
144
|
:raises ValueError: if callback not previously added.
|
|
142
145
|
"""
|
|
143
146
|
self._current_app_updated_callbacks.remove(callback)
|
|
144
147
|
|
|
145
|
-
def add_volume_info_updated_callback(
|
|
148
|
+
def add_volume_info_updated_callback(
|
|
149
|
+
self, callback: Callable[[VolumeInfo], None]
|
|
150
|
+
) -> None:
|
|
146
151
|
"""Add a callback for when volume_info is updated."""
|
|
147
152
|
self._volume_info_updated_callbacks.append(callback)
|
|
148
153
|
|
|
149
|
-
def remove_volume_info_updated_callback(
|
|
154
|
+
def remove_volume_info_updated_callback(
|
|
155
|
+
self, callback: Callable[[VolumeInfo], None]
|
|
156
|
+
) -> None:
|
|
150
157
|
"""Remove a callback previously added via add_volume_info_updated_callback.
|
|
151
158
|
|
|
152
159
|
:raises ValueError: if callback not previously added.
|
|
153
160
|
"""
|
|
154
161
|
self._volume_info_updated_callbacks.remove(callback)
|
|
155
162
|
|
|
156
|
-
def add_is_available_updated_callback(
|
|
163
|
+
def add_is_available_updated_callback(
|
|
164
|
+
self, callback: Callable[[bool], None]
|
|
165
|
+
) -> None:
|
|
157
166
|
"""Add a callback for when the Android TV is ready to receive commands or is unavailable."""
|
|
158
167
|
self._is_available_updated_callbacks.append(callback)
|
|
159
168
|
|
|
160
|
-
def remove_is_available_updated_callback(
|
|
169
|
+
def remove_is_available_updated_callback(
|
|
170
|
+
self, callback: Callable[[bool], None]
|
|
171
|
+
) -> None:
|
|
161
172
|
"""Remove a callback previously added via add_is_available_updated_callback.
|
|
162
173
|
|
|
163
174
|
:raises ValueError: if callback not previously added.
|
|
@@ -245,7 +256,7 @@ class AndroidTVRemote:
|
|
|
245
256
|
raise ConnectionClosed("Connection closed") from con_lost_exc
|
|
246
257
|
|
|
247
258
|
async def _async_reconnect(
|
|
248
|
-
self, invalid_auth_callback: Callable | None = None
|
|
259
|
+
self, invalid_auth_callback: Callable[[], None] | None = None
|
|
249
260
|
) -> None:
|
|
250
261
|
while self._remote_message_protocol:
|
|
251
262
|
exc = await self._remote_message_protocol.on_con_lost
|
|
@@ -279,7 +290,9 @@ class AndroidTVRemote:
|
|
|
279
290
|
invalid_auth_callback()
|
|
280
291
|
return
|
|
281
292
|
|
|
282
|
-
def keep_reconnecting(
|
|
293
|
+
def keep_reconnecting(
|
|
294
|
+
self, invalid_auth_callback: Callable[[], None] | None = None
|
|
295
|
+
) -> None:
|
|
283
296
|
"""Create a task to keep reconnecting whenever connection is lost."""
|
|
284
297
|
self._reconnect_task = self._loop.create_task(
|
|
285
298
|
self._async_reconnect(invalid_auth_callback)
|
|
@@ -384,7 +397,8 @@ class AndroidTVRemote:
|
|
|
384
397
|
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
|
385
398
|
|
|
386
399
|
:param key_code: int (e.g. 26) or str (e.g. "KEYCODE_POWER" or just "POWER") from the enum
|
|
387
|
-
RemoteKeyCode in remotemessage.proto
|
|
400
|
+
RemoteKeyCode in remotemessage.proto or str prefixed with "text:" to pass
|
|
401
|
+
to send_text.
|
|
388
402
|
:param direction: "SHORT" (default) or "START_LONG" or "END_LONG".
|
|
389
403
|
:raises ValueError: if key_code in str or direction isn't known.
|
|
390
404
|
:raises ConnectionClosed: if client is disconnected.
|
|
@@ -16,7 +16,7 @@ from .const import LOGGER
|
|
|
16
16
|
class ProtobufProtocol(asyncio.Protocol):
|
|
17
17
|
"""Protocol for receiving and sending protobuf messages."""
|
|
18
18
|
|
|
19
|
-
def __init__(self, on_con_lost: asyncio.Future) -> None:
|
|
19
|
+
def __init__(self, on_con_lost: asyncio.Future[Exception | None]) -> None:
|
|
20
20
|
"""Initialize.
|
|
21
21
|
|
|
22
22
|
:param on_con_lost: callback for when the connection is lost or closed.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Data models for AndroidTVRemote."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeviceInfo(TypedDict):
|
|
9
|
+
"""A TypedDict for device information."""
|
|
10
|
+
|
|
11
|
+
manufacturer: str
|
|
12
|
+
model: str
|
|
13
|
+
sw_version: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VolumeInfo(TypedDict):
|
|
17
|
+
"""A TypedDict for volume information."""
|
|
18
|
+
|
|
19
|
+
level: int
|
|
20
|
+
max: int
|
|
21
|
+
muted: bool
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import hashlib
|
|
12
|
+
from typing import Any
|
|
12
13
|
|
|
13
14
|
import aiofiles
|
|
14
15
|
from cryptography import x509
|
|
@@ -45,7 +46,7 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
45
46
|
|
|
46
47
|
def __init__(
|
|
47
48
|
self,
|
|
48
|
-
on_con_lost: asyncio.Future,
|
|
49
|
+
on_con_lost: asyncio.Future[Exception | None],
|
|
49
50
|
client_name: str,
|
|
50
51
|
certfile: str,
|
|
51
52
|
loop: asyncio.AbstractEventLoop,
|
|
@@ -62,8 +63,8 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
62
63
|
self._client_name = client_name
|
|
63
64
|
self._certfile = certfile
|
|
64
65
|
self._loop = loop
|
|
65
|
-
self._on_pairing_started: asyncio.Future | None = None
|
|
66
|
-
self._on_pairing_finished: asyncio.Future | None = None
|
|
66
|
+
self._on_pairing_started: asyncio.Future[bool] | None = None
|
|
67
|
+
self._on_pairing_finished: asyncio.Future[bool] | None = None
|
|
67
68
|
|
|
68
69
|
async def async_start_pairing(self) -> None:
|
|
69
70
|
"""Start the pairing process.
|
|
@@ -130,7 +131,9 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
130
131
|
finally:
|
|
131
132
|
self._on_pairing_finished = None
|
|
132
133
|
|
|
133
|
-
async def _async_wait_for_future_or_con_lost(
|
|
134
|
+
async def _async_wait_for_future_or_con_lost(
|
|
135
|
+
self, future: asyncio.Future[Any]
|
|
136
|
+
) -> None:
|
|
134
137
|
"""Wait for future to finish or connection to be lost."""
|
|
135
138
|
await asyncio.wait(
|
|
136
139
|
(self.on_con_lost, future), return_when=asyncio.FIRST_COMPLETED
|
|
File without changes
|
|
@@ -15,6 +15,7 @@ from google.protobuf.message import DecodeError
|
|
|
15
15
|
|
|
16
16
|
from .base import ProtobufProtocol
|
|
17
17
|
from .const import LOGGER
|
|
18
|
+
from .model import DeviceInfo, VolumeInfo
|
|
18
19
|
from .remotemessage_pb2 import (
|
|
19
20
|
RemoteDirection,
|
|
20
21
|
RemoteEditInfo,
|
|
@@ -30,6 +31,8 @@ ERROR_SUGGESTION_MSG = (
|
|
|
30
31
|
"On the Android TV device, go to Settings > Apps > See all apps > Show system apps. "
|
|
31
32
|
"Then, select Android TV Remote Service > Storage > Clear data/storage."
|
|
32
33
|
)
|
|
34
|
+
KEYCODE_PREFIX = "KEYCODE_"
|
|
35
|
+
TEXT_PREFIX = "text:"
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class Feature(IntFlag):
|
|
@@ -53,11 +56,11 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
53
56
|
|
|
54
57
|
def __init__(
|
|
55
58
|
self,
|
|
56
|
-
on_con_lost: asyncio.Future,
|
|
57
|
-
on_remote_started: asyncio.Future,
|
|
58
|
-
on_is_on_updated: Callable,
|
|
59
|
-
on_current_app_updated: Callable,
|
|
60
|
-
on_volume_info_updated: Callable,
|
|
59
|
+
on_con_lost: asyncio.Future[Exception | None],
|
|
60
|
+
on_remote_started: asyncio.Future[bool],
|
|
61
|
+
on_is_on_updated: Callable[[bool], None],
|
|
62
|
+
on_current_app_updated: Callable[[str], None],
|
|
63
|
+
on_volume_info_updated: Callable[[VolumeInfo], None],
|
|
61
64
|
loop: asyncio.AbstractEventLoop,
|
|
62
65
|
enable_ime: bool,
|
|
63
66
|
) -> None:
|
|
@@ -87,12 +90,12 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
87
90
|
)
|
|
88
91
|
self.is_on = False
|
|
89
92
|
self.current_app = ""
|
|
90
|
-
self.device_info:
|
|
91
|
-
self.volume_info:
|
|
93
|
+
self.device_info: DeviceInfo | None = None
|
|
94
|
+
self.volume_info: VolumeInfo | None = None
|
|
92
95
|
self.ime_counter: int = 0
|
|
93
96
|
self.ime_field_counter: int = 0
|
|
94
97
|
self._loop = loop
|
|
95
|
-
self._idle_disconnect_task: asyncio.Task | None = None
|
|
98
|
+
self._idle_disconnect_task: asyncio.Task[None] | None = None
|
|
96
99
|
self._reset_idle_disconnect_task()
|
|
97
100
|
|
|
98
101
|
def send_key_command(
|
|
@@ -102,16 +105,19 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
102
105
|
|
|
103
106
|
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
|
104
107
|
|
|
105
|
-
:param key_code: int (e.g. 26) or str (e.g. KEYCODE_POWER or just "POWER") from the enum
|
|
106
|
-
RemoteKeyCode in remotemessage.proto
|
|
108
|
+
:param key_code: int (e.g. 26) or str (e.g. "KEYCODE_POWER" or just "POWER") from the enum
|
|
109
|
+
RemoteKeyCode in remotemessage.proto or str prefixed with "text:" to pass
|
|
110
|
+
to send_text.
|
|
107
111
|
:param direction: "SHORT" (default) or "START_LONG" or "END_LONG".
|
|
108
112
|
:raises ValueError: if key_code in str or direction isn't known.
|
|
109
113
|
"""
|
|
110
114
|
self._reset_idle_disconnect_task()
|
|
111
115
|
msg = RemoteMessage()
|
|
112
116
|
if isinstance(key_code, str):
|
|
113
|
-
if
|
|
114
|
-
key_code
|
|
117
|
+
if key_code.lower().startswith(TEXT_PREFIX):
|
|
118
|
+
return self.send_text(key_code[len(TEXT_PREFIX) :])
|
|
119
|
+
if not key_code.startswith(KEYCODE_PREFIX):
|
|
120
|
+
key_code = KEYCODE_PREFIX + key_code
|
|
115
121
|
key_code = RemoteKeyCode.Value(key_code)
|
|
116
122
|
if isinstance(direction, str):
|
|
117
123
|
direction = RemoteDirection.Value(direction)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidtvremote2
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
6
|
License: Apache-2.0
|
|
@@ -19,7 +19,7 @@ Dynamic: license-file
|
|
|
19
19
|
A Python library for interacting with Android TV using the Android TV Remote protocol v2. This is the same protocol the Google TV mobile app is using. It doesn't require ADB or enabling developer tools on the Android TV device. It only requires the [Android TV Remote Service](https://play.google.com/store/apps/details?id=com.google.android.tv.remote.service) that comes pre-installed on most Android TV devices.
|
|
20
20
|
|
|
21
21
|
For a list of the most common commands you can send to the Android TV see: [TvKeys](https://github.com/tronikos/androidtvremote2/blob/main/TvKeys.txt).
|
|
22
|
-
For a full list see [
|
|
22
|
+
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/b4c49ac03043b1b9c40c2f2960e466d5a3b8bd67/src/androidtvremote2/remotemessage.proto#L90).
|
|
23
23
|
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
24
|
|
|
25
25
|
## Credits
|
|
@@ -9,9 +9,11 @@ src/androidtvremote2/base.py
|
|
|
9
9
|
src/androidtvremote2/certificate_generator.py
|
|
10
10
|
src/androidtvremote2/const.py
|
|
11
11
|
src/androidtvremote2/exceptions.py
|
|
12
|
+
src/androidtvremote2/model.py
|
|
12
13
|
src/androidtvremote2/pairing.py
|
|
13
14
|
src/androidtvremote2/polo_pb2.py
|
|
14
15
|
src/androidtvremote2/polo_pb2.pyi
|
|
16
|
+
src/androidtvremote2/py.typed
|
|
15
17
|
src/androidtvremote2/remote.py
|
|
16
18
|
src/androidtvremote2/remotemessage_pb2.py
|
|
17
19
|
src/androidtvremote2/remotemessage_pb2.pyi
|
|
@@ -14,6 +14,7 @@ from androidtvremote2 import (
|
|
|
14
14
|
CannotConnect,
|
|
15
15
|
ConnectionClosed,
|
|
16
16
|
InvalidAuth,
|
|
17
|
+
VolumeInfo,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -51,8 +52,8 @@ async def _bind_keyboard(remote: AndroidTVRemote) -> None:
|
|
|
51
52
|
keyboard.Key.delete: "POWER",
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
def transmit_keys() -> asyncio.Queue:
|
|
55
|
-
queue: asyncio.Queue = asyncio.Queue()
|
|
55
|
+
def transmit_keys() -> asyncio.Queue[keyboard.Key | keyboard.KeyCode | None]:
|
|
56
|
+
queue: asyncio.Queue[keyboard.Key | keyboard.KeyCode | None] = asyncio.Queue()
|
|
56
57
|
loop = asyncio.get_event_loop()
|
|
57
58
|
|
|
58
59
|
def on_press(key: keyboard.Key | keyboard.KeyCode | None) -> None:
|
|
@@ -64,30 +65,33 @@ async def _bind_keyboard(remote: AndroidTVRemote) -> None:
|
|
|
64
65
|
key_queue = transmit_keys()
|
|
65
66
|
while True:
|
|
66
67
|
key = await key_queue.get()
|
|
68
|
+
if key is None:
|
|
69
|
+
continue
|
|
67
70
|
if key in key_mappings:
|
|
68
71
|
remote.send_key_command(key_mappings[key])
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
if not isinstance(key, keyboard.KeyCode):
|
|
73
|
+
continue
|
|
74
|
+
if key.char == "q":
|
|
75
|
+
remote.disconnect()
|
|
76
|
+
return
|
|
77
|
+
elif key.char == "m":
|
|
78
|
+
remote.send_key_command("MUTE")
|
|
79
|
+
elif key.char == "+":
|
|
80
|
+
remote.send_key_command("VOLUME_UP")
|
|
81
|
+
elif key.char == "-":
|
|
82
|
+
remote.send_key_command("VOLUME_DOWN")
|
|
83
|
+
elif key.char == "y":
|
|
84
|
+
remote.send_launch_app_command("https://www.youtube.com")
|
|
85
|
+
elif key.char == "n":
|
|
86
|
+
remote.send_launch_app_command("com.netflix.ninja")
|
|
87
|
+
elif key.char == "d":
|
|
88
|
+
remote.send_launch_app_command("com.disney.disneyplus")
|
|
89
|
+
elif key.char == "a":
|
|
90
|
+
remote.send_launch_app_command("com.amazon.amazonvideo.livingroom")
|
|
91
|
+
elif key.char == "k":
|
|
92
|
+
remote.send_launch_app_command("org.xbmc.kodi")
|
|
93
|
+
elif key.char == "t":
|
|
94
|
+
remote.send_text("Hello World!")
|
|
91
95
|
|
|
92
96
|
|
|
93
97
|
async def _host_from_zeroconf(timeout: float) -> str:
|
|
@@ -114,7 +118,7 @@ async def _host_from_zeroconf(timeout: float) -> str:
|
|
|
114
118
|
for addr in info.parsed_scoped_addresses()
|
|
115
119
|
]
|
|
116
120
|
print(f" Name: {name}")
|
|
117
|
-
print(f" Addresses: {
|
|
121
|
+
print(f" Addresses: {', '.join(addresses)}")
|
|
118
122
|
if info.properties:
|
|
119
123
|
print(" Properties:")
|
|
120
124
|
for key, value in info.properties.items():
|
|
@@ -227,7 +231,7 @@ async def _main() -> None:
|
|
|
227
231
|
def current_app_updated(current_app: str) -> None:
|
|
228
232
|
_LOGGER.info("Notified that current_app: %s", current_app)
|
|
229
233
|
|
|
230
|
-
def volume_info_updated(volume_info:
|
|
234
|
+
def volume_info_updated(volume_info: VolumeInfo) -> None:
|
|
231
235
|
_LOGGER.info("Notified that volume_info: %s", volume_info)
|
|
232
236
|
|
|
233
237
|
def is_available_updated(is_available: bool) -> None:
|
|
File without changes
|
|
File without changes
|
{androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/certificate_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2/remotemessage_pb2.pyi
RENAMED
|
File without changes
|
{androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/requires.txt
RENAMED
|
File without changes
|
{androidtvremote2-0.2.1 → androidtvremote2-0.2.3}/src/androidtvremote2.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|