aioamazondevices 3.0.4__tar.gz → 3.0.6__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.
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/PKG-INFO +6 -2
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/README.md +5 -1
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/pyproject.toml +1 -1
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/src/aioamazondevices/api.py +44 -36
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/src/aioamazondevices/const.py +17 -1
- aioamazondevices-3.0.4/src/aioamazondevices/httpx.py +0 -159
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/LICENSE +0 -0
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/src/aioamazondevices/py.typed +0 -0
- {aioamazondevices-3.0.4 → aioamazondevices-3.0.6}/src/aioamazondevices/sounds.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.6
|
4
4
|
Summary: Python library to control Amazon devices
|
5
5
|
License: Apache-2.0
|
6
6
|
Author: Simone Chemelli
|
@@ -86,7 +86,8 @@ The script accept command line arguments or a library_test.json config file:
|
|
86
86
|
"single_device_name": "Echo Dot Livingroom",
|
87
87
|
"cluster_device_name": "Everywhere",
|
88
88
|
"login_data_file": "out/login_data.json",
|
89
|
-
"save_raw_data":
|
89
|
+
"save_raw_data": true,
|
90
|
+
"test": true
|
90
91
|
}
|
91
92
|
```
|
92
93
|
|
@@ -97,8 +98,11 @@ Library logs a warning if an unknown device type is linked to your Amazon accoun
|
|
97
98
|
Please open an issue [here](https://github.com/chemelli74/aioamazondevices/issues) and provide the following information:
|
98
99
|
|
99
100
|
- device type
|
101
|
+
- brand
|
100
102
|
- model
|
101
103
|
- generation
|
104
|
+
- entities are available in Home Assistant
|
105
|
+
- entities work in Home Assistant
|
102
106
|
|
103
107
|
Current device list: `DEVICE_TYPE_TO_MODEL` from [const.py](https://github.com/chemelli74/aioamazondevices/blob/main/src/aioamazondevices/const.py)
|
104
108
|
|
@@ -57,7 +57,8 @@ The script accept command line arguments or a library_test.json config file:
|
|
57
57
|
"single_device_name": "Echo Dot Livingroom",
|
58
58
|
"cluster_device_name": "Everywhere",
|
59
59
|
"login_data_file": "out/login_data.json",
|
60
|
-
"save_raw_data":
|
60
|
+
"save_raw_data": true,
|
61
|
+
"test": true
|
61
62
|
}
|
62
63
|
```
|
63
64
|
|
@@ -68,8 +69,11 @@ Library logs a warning if an unknown device type is linked to your Amazon accoun
|
|
68
69
|
Please open an issue [here](https://github.com/chemelli74/aioamazondevices/issues) and provide the following information:
|
69
70
|
|
70
71
|
- device type
|
72
|
+
- brand
|
71
73
|
- model
|
72
74
|
- generation
|
75
|
+
- entities are available in Home Assistant
|
76
|
+
- entities work in Home Assistant
|
73
77
|
|
74
78
|
Current device list: `DEVICE_TYPE_TO_MODEL` from [const.py](https://github.com/chemelli74/aioamazondevices/blob/main/src/aioamazondevices/const.py)
|
75
79
|
|
@@ -8,8 +8,8 @@ import uuid
|
|
8
8
|
from dataclasses import dataclass
|
9
9
|
from datetime import UTC, datetime, timedelta
|
10
10
|
from enum import StrEnum
|
11
|
-
from http import HTTPStatus
|
12
|
-
from http.cookies import Morsel
|
11
|
+
from http import HTTPMethod, HTTPStatus
|
12
|
+
from http.cookies import Morsel, SimpleCookie
|
13
13
|
from pathlib import Path
|
14
14
|
from typing import Any, cast
|
15
15
|
from urllib.parse import parse_qs, urlencode
|
@@ -18,9 +18,8 @@ import orjson
|
|
18
18
|
from aiohttp import ClientResponse, ClientSession
|
19
19
|
from babel import Locale
|
20
20
|
from bs4 import BeautifulSoup, Tag
|
21
|
-
from
|
22
|
-
from
|
23
|
-
from yarl import URL as YARL_URL
|
21
|
+
from multidict import CIMultiDictProxy, MultiDictProxy
|
22
|
+
from yarl import URL
|
24
23
|
|
25
24
|
from .const import (
|
26
25
|
_LOGGER,
|
@@ -47,10 +46,6 @@ from .const import (
|
|
47
46
|
URI_QUERIES,
|
48
47
|
)
|
49
48
|
from .exceptions import CannotAuthenticate, CannotRegisterDevice, WrongMethod
|
50
|
-
from .httpx import HttpxClientResponseWrapper, HttpxClientSession
|
51
|
-
|
52
|
-
# Values: "aiohttp", or "httpx"
|
53
|
-
LIBRARY = "httpx"
|
54
49
|
|
55
50
|
|
56
51
|
@dataclass
|
@@ -123,7 +118,7 @@ class AmazonEchoApi:
|
|
123
118
|
self._serial = self._serial_number()
|
124
119
|
self._list_for_clusters: dict[str, str] = {}
|
125
120
|
|
126
|
-
self.session: ClientSession
|
121
|
+
self.session: ClientSession
|
127
122
|
|
128
123
|
def _load_website_cookies(self) -> dict[str, str]:
|
129
124
|
"""Get website cookies, if avaliables."""
|
@@ -234,7 +229,7 @@ class AmazonEchoApi:
|
|
234
229
|
return method, url
|
235
230
|
raise TypeError("Unable to extract form data from response")
|
236
231
|
|
237
|
-
def _extract_code_from_url(self, url:
|
232
|
+
def _extract_code_from_url(self, url: URL) -> str:
|
238
233
|
"""Extract the access token from url query after login."""
|
239
234
|
parsed_url: dict[str, list[str]] = {}
|
240
235
|
if isinstance(url.query, bytes):
|
@@ -249,18 +244,28 @@ class AmazonEchoApi:
|
|
249
244
|
def _client_session(self) -> None:
|
250
245
|
"""Create HTTP client session."""
|
251
246
|
if not hasattr(self, "session") or self.session.closed:
|
252
|
-
_LOGGER.debug("Creating HTTP session (
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
247
|
+
_LOGGER.debug("Creating HTTP session (aiohttp)")
|
248
|
+
self.session = ClientSession(
|
249
|
+
headers=DEFAULT_HEADERS,
|
250
|
+
cookies=self._cookies,
|
251
|
+
)
|
252
|
+
|
253
|
+
async def _parse_cookies_from_headers(
|
254
|
+
self, headers: CIMultiDictProxy[str]
|
255
|
+
) -> dict[str, str]:
|
256
|
+
"""Parse cookies with a value from headers."""
|
257
|
+
cookies_with_value: dict[str, str] = {}
|
258
|
+
|
259
|
+
for value in headers.getall("Set-Cookie", ()):
|
260
|
+
cookie = SimpleCookie()
|
261
|
+
cookie.load(value)
|
262
|
+
|
263
|
+
for name, morsel in cookie.items():
|
264
|
+
if morsel.value and morsel.value not in ("-", ""):
|
265
|
+
cookies_with_value[name] = morsel.value
|
266
|
+
|
267
|
+
_LOGGER.debug("Cookies from headers: %s", cookies_with_value)
|
268
|
+
return cookies_with_value
|
264
269
|
|
265
270
|
async def _session_request(
|
266
271
|
self,
|
@@ -268,7 +273,7 @@ class AmazonEchoApi:
|
|
268
273
|
url: str,
|
269
274
|
input_data: dict[str, Any] | None = None,
|
270
275
|
json_data: bool = False,
|
271
|
-
) -> tuple[BeautifulSoup, ClientResponse
|
276
|
+
) -> tuple[BeautifulSoup, ClientResponse]:
|
272
277
|
"""Return request response context data."""
|
273
278
|
_LOGGER.debug(
|
274
279
|
"%s request: %s with payload %s [json=%s]",
|
@@ -289,17 +294,18 @@ class AmazonEchoApi:
|
|
289
294
|
_LOGGER.debug("Adding %s to headers", json_header)
|
290
295
|
headers.update(json_header)
|
291
296
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
297
|
+
_cookies = (
|
298
|
+
self._load_website_cookies() if self._login_stored_data else self._cookies
|
299
|
+
)
|
296
300
|
resp = await self.session.request(
|
297
301
|
method,
|
298
|
-
|
302
|
+
URL(url, encoded=True),
|
299
303
|
data=input_data if not json_data else orjson.dumps(input_data),
|
300
|
-
cookies=
|
304
|
+
cookies=_cookies,
|
301
305
|
headers=headers,
|
302
306
|
)
|
307
|
+
self._cookies.update(**await self._parse_cookies_from_headers(resp.headers))
|
308
|
+
|
303
309
|
content_type: str = resp.headers.get("Content-Type", "")
|
304
310
|
_LOGGER.debug(
|
305
311
|
"Response %s for url %s with content type: %s",
|
@@ -402,7 +408,7 @@ class AmazonEchoApi:
|
|
402
408
|
|
403
409
|
register_url = f"https://api.amazon.{self._domain}/auth/register"
|
404
410
|
_, resp = await self._session_request(
|
405
|
-
method=
|
411
|
+
method=HTTPMethod.POST,
|
406
412
|
url=register_url,
|
407
413
|
input_data=body,
|
408
414
|
json_data=True,
|
@@ -466,7 +472,9 @@ class AmazonEchoApi:
|
|
466
472
|
_LOGGER.debug("Build oauth URL")
|
467
473
|
login_url = self._build_oauth_url(code_verifier, client_id)
|
468
474
|
|
469
|
-
login_soup, _ = await self._session_request(
|
475
|
+
login_soup, _ = await self._session_request(
|
476
|
+
method=HTTPMethod.GET, url=login_url
|
477
|
+
)
|
470
478
|
login_method, login_url = self._get_request_from_soup(login_soup)
|
471
479
|
login_inputs = self._get_inputs_from_soup(login_soup)
|
472
480
|
login_inputs["email"] = self._login_email
|
@@ -535,7 +543,7 @@ class AmazonEchoApi:
|
|
535
543
|
async def close(self) -> None:
|
536
544
|
"""Close http client session."""
|
537
545
|
if hasattr(self, "session"):
|
538
|
-
_LOGGER.debug("Closing HTTP session (
|
546
|
+
_LOGGER.debug("Closing HTTP session (aiohttp)")
|
539
547
|
await self.session.close()
|
540
548
|
|
541
549
|
async def get_devices_data(
|
@@ -545,7 +553,7 @@ class AmazonEchoApi:
|
|
545
553
|
devices: dict[str, Any] = {}
|
546
554
|
for key in URI_QUERIES:
|
547
555
|
_, raw_resp = await self._session_request(
|
548
|
-
method=
|
556
|
+
method=HTTPMethod.GET,
|
549
557
|
url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
550
558
|
)
|
551
559
|
_LOGGER.debug("Response URL: %s", raw_resp.url)
|
@@ -615,7 +623,7 @@ class AmazonEchoApi:
|
|
615
623
|
async def auth_check_status(self) -> bool:
|
616
624
|
"""Check AUTH status."""
|
617
625
|
_, raw_resp = await self._session_request(
|
618
|
-
method=
|
626
|
+
method=HTTPMethod.GET,
|
619
627
|
url=f"https://alexa.amazon.{self._domain}/api/bootstrap?version=0",
|
620
628
|
)
|
621
629
|
if raw_resp.status != HTTPStatus.OK:
|
@@ -760,7 +768,7 @@ class AmazonEchoApi:
|
|
760
768
|
|
761
769
|
_LOGGER.debug("Preview data payload: %s", node_data)
|
762
770
|
await self._session_request(
|
763
|
-
method=
|
771
|
+
method=HTTPMethod.POST,
|
764
772
|
url=f"https://alexa.amazon.{self._domain}/api/behaviors/preview",
|
765
773
|
input_data=node_data,
|
766
774
|
json_data=True,
|
@@ -84,6 +84,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
|
|
84
84
|
"model": "Echo Dot",
|
85
85
|
"hw_version": "Gen3",
|
86
86
|
},
|
87
|
+
"A1Z88NGR2BK6A2": {
|
88
|
+
"model": "Echo Show 8",
|
89
|
+
"hw_version": "Gen1",
|
90
|
+
},
|
87
91
|
"A271DR1789MXDS": {
|
88
92
|
"model": "Fire Tablet 7",
|
89
93
|
"hw_version": "Gen12",
|
@@ -96,6 +100,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
|
|
96
100
|
"model": "Echo Dot Clock",
|
97
101
|
"hw_version": "Gen4",
|
98
102
|
},
|
103
|
+
"A2JKHJ0PX4J3L3": {
|
104
|
+
"model": "Fire TV Cube",
|
105
|
+
"hw_version": "Gen2",
|
106
|
+
},
|
99
107
|
"A2LWARUGJLBYEW": {
|
100
108
|
"model": "Fire TV Stick",
|
101
109
|
"hw_version": "Gen2",
|
@@ -108,6 +116,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
|
|
108
116
|
"model": "Echo Dot Clock",
|
109
117
|
"hw_version": "Gen4",
|
110
118
|
},
|
119
|
+
"A2UONLFQW0PADH": {
|
120
|
+
"model": "Echo Show 8",
|
121
|
+
"hw_version": "Gen3",
|
122
|
+
},
|
111
123
|
"A303PJF6ISQ7IC": {
|
112
124
|
"model": "Echo Auto",
|
113
125
|
"hw_version": "Gen1",
|
@@ -149,13 +161,17 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
|
|
149
161
|
"hw_version": "Gen5",
|
150
162
|
},
|
151
163
|
"A7WXQPH584YP": {
|
152
|
-
"model": "Echo
|
164
|
+
"model": "Echo",
|
153
165
|
"hw_version": "Gen2",
|
154
166
|
},
|
155
167
|
"AB72C64C86AW2": {
|
156
168
|
"model": "Echo Dot",
|
157
169
|
"hw_version": "Gen2",
|
158
170
|
},
|
171
|
+
"AIPK7MM90V7TB": {
|
172
|
+
"model": "Echo Show 10",
|
173
|
+
"hw_version": "Gen3",
|
174
|
+
},
|
159
175
|
"AKNO1N0KSFN8L": {
|
160
176
|
"model": "Echo Dot",
|
161
177
|
"hw_version": "Gen1",
|
@@ -1,159 +0,0 @@
|
|
1
|
-
"""Wrapper around httpx.Response to mimic aiohttp.ClientResponse."""
|
2
|
-
|
3
|
-
import types
|
4
|
-
from http.cookies import SimpleCookie
|
5
|
-
from typing import Any, Self, cast
|
6
|
-
|
7
|
-
from httpx import AsyncClient, Cookies, Response
|
8
|
-
from httpx._types import RequestData
|
9
|
-
|
10
|
-
|
11
|
-
def convert_httpx_cookies_to_simplecookie(httpx_cookies: Cookies) -> SimpleCookie:
|
12
|
-
"""Convert an httpx.Cookies object to a single SimpleCookie."""
|
13
|
-
simple_cookie = SimpleCookie()
|
14
|
-
|
15
|
-
for name, value in httpx_cookies.items():
|
16
|
-
simple_cookie[name] = value
|
17
|
-
|
18
|
-
return simple_cookie
|
19
|
-
|
20
|
-
|
21
|
-
class HttpxClientResponseWrapper:
|
22
|
-
"""aiohttp-like Wrapper for httpx.Response."""
|
23
|
-
|
24
|
-
def __init__(self, response: Response) -> None:
|
25
|
-
"""Init wrapper."""
|
26
|
-
self._response = response
|
27
|
-
|
28
|
-
# Basic aiohttp-like attributes
|
29
|
-
self.status = response.status_code
|
30
|
-
self.headers = response.headers
|
31
|
-
self.url = response.url
|
32
|
-
self.reason = response.reason_phrase
|
33
|
-
self.cookies = convert_httpx_cookies_to_simplecookie(response.cookies)
|
34
|
-
self.ok = response.is_success
|
35
|
-
|
36
|
-
# Aiohttp compatibility
|
37
|
-
self.content_type = response.headers.get("Content-Type", "")
|
38
|
-
self.real_url = str(response.url)
|
39
|
-
self.request_info = types.SimpleNamespace(
|
40
|
-
real_url=self.url,
|
41
|
-
method=response.request.method,
|
42
|
-
headers=response.request.headers,
|
43
|
-
)
|
44
|
-
|
45
|
-
# History (aiohttp returns redirects as history)
|
46
|
-
self.history = (
|
47
|
-
[HttpxClientResponseWrapper(r) for r in response.history]
|
48
|
-
if response.history
|
49
|
-
else []
|
50
|
-
)
|
51
|
-
|
52
|
-
async def text(self, encoding: str | None = None) -> str:
|
53
|
-
"""Text."""
|
54
|
-
# httpx auto-decodes
|
55
|
-
if encoding:
|
56
|
-
return cast("str", self._response.content.decode(encoding))
|
57
|
-
return cast("str", self._response.text)
|
58
|
-
|
59
|
-
async def json(self) -> Any: # noqa: ANN401
|
60
|
-
"""Json."""
|
61
|
-
return self._response.json()
|
62
|
-
|
63
|
-
async def read(self) -> bytes:
|
64
|
-
"""Read."""
|
65
|
-
return cast("bytes", self._response.content)
|
66
|
-
|
67
|
-
async def release(self) -> None:
|
68
|
-
"""Release."""
|
69
|
-
# No-op: aiohttp requires this, httpx does not
|
70
|
-
|
71
|
-
def raise_for_status(self) -> Response:
|
72
|
-
"""Raise for status."""
|
73
|
-
return self._response.raise_for_status()
|
74
|
-
|
75
|
-
def __repr__(self) -> str:
|
76
|
-
"""Repr."""
|
77
|
-
return f"<HttpxClientResponseWrapper [{self.status} {self.reason}]>"
|
78
|
-
|
79
|
-
async def __aenter__(self) -> Self:
|
80
|
-
"""Aenter."""
|
81
|
-
return self
|
82
|
-
|
83
|
-
async def __aexit__(
|
84
|
-
self,
|
85
|
-
exc_type: type[BaseException] | None,
|
86
|
-
exc_val: BaseException | None,
|
87
|
-
exc_tb: types.TracebackType | None,
|
88
|
-
) -> None:
|
89
|
-
"""Aexit."""
|
90
|
-
await self.release()
|
91
|
-
|
92
|
-
|
93
|
-
class HttpxClientSession:
|
94
|
-
"""aiohttp-like Wrapper for httpx.AsyncClient."""
|
95
|
-
|
96
|
-
def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
|
97
|
-
"""Init session."""
|
98
|
-
# Allow passing any httpx.AsyncClient init kwargs (e.g., headers, cookies)
|
99
|
-
self._client = AsyncClient(**kwargs)
|
100
|
-
|
101
|
-
@property
|
102
|
-
def closed(self) -> bool:
|
103
|
-
"""Indicates whether the underlying client session is closed."""
|
104
|
-
return cast("bool", self._client.is_closed)
|
105
|
-
|
106
|
-
async def request(
|
107
|
-
self,
|
108
|
-
method: str,
|
109
|
-
url: str,
|
110
|
-
*,
|
111
|
-
params: dict[str, str | int | float] | None = None,
|
112
|
-
data: Any = None, # noqa: ANN401
|
113
|
-
json: Any = None, # noqa: ANN401
|
114
|
-
**kwargs: Any, # noqa: ANN401
|
115
|
-
) -> HttpxClientResponseWrapper:
|
116
|
-
"""Make a generic request method for any HTTP verb."""
|
117
|
-
response = await self._client.request(
|
118
|
-
method=method.upper(),
|
119
|
-
url=url,
|
120
|
-
params=params,
|
121
|
-
data=data,
|
122
|
-
json=json,
|
123
|
-
**kwargs,
|
124
|
-
)
|
125
|
-
return HttpxClientResponseWrapper(response)
|
126
|
-
|
127
|
-
async def get(self, url: str, **kwargs: Any) -> HttpxClientResponseWrapper: # noqa: ANN401
|
128
|
-
"""Get."""
|
129
|
-
response = await self._client.get(url, **kwargs)
|
130
|
-
return HttpxClientResponseWrapper(response)
|
131
|
-
|
132
|
-
async def post(
|
133
|
-
self,
|
134
|
-
url: str,
|
135
|
-
data: RequestData | None = None,
|
136
|
-
json: Any = None, # noqa: ANN401
|
137
|
-
**kwargs: Any, # noqa: ANN401
|
138
|
-
) -> HttpxClientResponseWrapper:
|
139
|
-
"""Post."""
|
140
|
-
response = await self._client.post(url, data=data, json=json, **kwargs)
|
141
|
-
return HttpxClientResponseWrapper(response)
|
142
|
-
|
143
|
-
async def close(self) -> None:
|
144
|
-
"""Close."""
|
145
|
-
await self._client.aclose()
|
146
|
-
|
147
|
-
async def __aenter__(self) -> Self:
|
148
|
-
"""AEnter."""
|
149
|
-
await self._client.__aenter__()
|
150
|
-
return self
|
151
|
-
|
152
|
-
async def __aexit__(
|
153
|
-
self,
|
154
|
-
exc_type: type[BaseException] | None,
|
155
|
-
exc_val: BaseException | None,
|
156
|
-
exc_tb: types.TracebackType | None,
|
157
|
-
) -> None:
|
158
|
-
"""AExit."""
|
159
|
-
await self._client.__aexit__(exc_type, exc_val, exc_tb)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|