aioamazondevices 3.0.5__tar.gz → 3.0.7__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.5 → aioamazondevices-3.0.7}/PKG-INFO +6 -2
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/README.md +5 -1
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/pyproject.toml +1 -1
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/src/aioamazondevices/api.py +19 -33
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/src/aioamazondevices/const.py +22 -1
- aioamazondevices-3.0.5/src/aioamazondevices/httpx.py +0 -159
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/LICENSE +0 -0
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/src/aioamazondevices/py.typed +0 -0
- {aioamazondevices-3.0.5 → aioamazondevices-3.0.7}/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.7
|
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,7 +8,7 @@ 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
|
11
|
+
from http import HTTPMethod, HTTPStatus
|
12
12
|
from http.cookies import Morsel, SimpleCookie
|
13
13
|
from pathlib import Path
|
14
14
|
from typing import Any, cast
|
@@ -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 httpx import URL as HTTPX_URL
|
22
21
|
from multidict import CIMultiDictProxy, MultiDictProxy
|
23
|
-
from yarl import URL
|
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 = "aiohttp"
|
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,11 @@ 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
|
-
follow_redirects=True,
|
258
|
-
)
|
259
|
-
else:
|
260
|
-
self.session = ClientSession(
|
261
|
-
headers=DEFAULT_HEADERS,
|
262
|
-
cookies=self._cookies,
|
263
|
-
)
|
247
|
+
_LOGGER.debug("Creating HTTP session (aiohttp)")
|
248
|
+
self.session = ClientSession(
|
249
|
+
headers=DEFAULT_HEADERS,
|
250
|
+
cookies=self._cookies,
|
251
|
+
)
|
264
252
|
|
265
253
|
async def _parse_cookies_from_headers(
|
266
254
|
self, headers: CIMultiDictProxy[str]
|
@@ -285,7 +273,7 @@ class AmazonEchoApi:
|
|
285
273
|
url: str,
|
286
274
|
input_data: dict[str, Any] | None = None,
|
287
275
|
json_data: bool = False,
|
288
|
-
) -> tuple[BeautifulSoup, ClientResponse
|
276
|
+
) -> tuple[BeautifulSoup, ClientResponse]:
|
289
277
|
"""Return request response context data."""
|
290
278
|
_LOGGER.debug(
|
291
279
|
"%s request: %s with payload %s [json=%s]",
|
@@ -309,13 +297,9 @@ class AmazonEchoApi:
|
|
309
297
|
_cookies = (
|
310
298
|
self._load_website_cookies() if self._login_stored_data else self._cookies
|
311
299
|
)
|
312
|
-
_url: YARL_URL | str = url
|
313
|
-
if LIBRARY == "aiohttp":
|
314
|
-
_url = YARL_URL(url, encoded=True)
|
315
|
-
|
316
300
|
resp = await self.session.request(
|
317
301
|
method,
|
318
|
-
|
302
|
+
URL(url, encoded=True),
|
319
303
|
data=input_data if not json_data else orjson.dumps(input_data),
|
320
304
|
cookies=_cookies,
|
321
305
|
headers=headers,
|
@@ -424,7 +408,7 @@ class AmazonEchoApi:
|
|
424
408
|
|
425
409
|
register_url = f"https://api.amazon.{self._domain}/auth/register"
|
426
410
|
_, resp = await self._session_request(
|
427
|
-
method=
|
411
|
+
method=HTTPMethod.POST,
|
428
412
|
url=register_url,
|
429
413
|
input_data=body,
|
430
414
|
json_data=True,
|
@@ -488,7 +472,9 @@ class AmazonEchoApi:
|
|
488
472
|
_LOGGER.debug("Build oauth URL")
|
489
473
|
login_url = self._build_oauth_url(code_verifier, client_id)
|
490
474
|
|
491
|
-
login_soup, _ = await self._session_request(
|
475
|
+
login_soup, _ = await self._session_request(
|
476
|
+
method=HTTPMethod.GET, url=login_url
|
477
|
+
)
|
492
478
|
login_method, login_url = self._get_request_from_soup(login_soup)
|
493
479
|
login_inputs = self._get_inputs_from_soup(login_soup)
|
494
480
|
login_inputs["email"] = self._login_email
|
@@ -557,7 +543,7 @@ class AmazonEchoApi:
|
|
557
543
|
async def close(self) -> None:
|
558
544
|
"""Close http client session."""
|
559
545
|
if hasattr(self, "session"):
|
560
|
-
_LOGGER.debug("Closing HTTP session (
|
546
|
+
_LOGGER.debug("Closing HTTP session (aiohttp)")
|
561
547
|
await self.session.close()
|
562
548
|
|
563
549
|
async def get_devices_data(
|
@@ -567,7 +553,7 @@ class AmazonEchoApi:
|
|
567
553
|
devices: dict[str, Any] = {}
|
568
554
|
for key in URI_QUERIES:
|
569
555
|
_, raw_resp = await self._session_request(
|
570
|
-
method=
|
556
|
+
method=HTTPMethod.GET,
|
571
557
|
url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
572
558
|
)
|
573
559
|
_LOGGER.debug("Response URL: %s", raw_resp.url)
|
@@ -637,7 +623,7 @@ class AmazonEchoApi:
|
|
637
623
|
async def auth_check_status(self) -> bool:
|
638
624
|
"""Check AUTH status."""
|
639
625
|
_, raw_resp = await self._session_request(
|
640
|
-
method=
|
626
|
+
method=HTTPMethod.GET,
|
641
627
|
url=f"https://alexa.amazon.{self._domain}/api/bootstrap?version=0",
|
642
628
|
)
|
643
629
|
if raw_resp.status != HTTPStatus.OK:
|
@@ -782,7 +768,7 @@ class AmazonEchoApi:
|
|
782
768
|
|
783
769
|
_LOGGER.debug("Preview data payload: %s", node_data)
|
784
770
|
await self._session_request(
|
785
|
-
method=
|
771
|
+
method=HTTPMethod.POST,
|
786
772
|
url=f"https://alexa.amazon.{self._domain}/api/behaviors/preview",
|
787
773
|
input_data=node_data,
|
788
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",
|
@@ -168,6 +184,11 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
|
|
168
184
|
"model": "Fire Tablet HD 10",
|
169
185
|
"hw_version": "Gen11",
|
170
186
|
},
|
187
|
+
"AUPUQSVCVHXP0": {
|
188
|
+
"manufacturer": "ecobee Inc.",
|
189
|
+
"model": "ecobee Switch+",
|
190
|
+
"hw_version": None,
|
191
|
+
},
|
171
192
|
"AVU7CPPF2ZRAS": {
|
172
193
|
"model": "Fire Tablet HD 8 Plus",
|
173
194
|
"hw_version": "Gen10",
|
@@ -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
|