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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 3.0.5
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": "True"
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": "True"
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
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aioamazondevices"
3
- version = "3.0.5"
3
+ version = "3.0.7"
4
4
  description = "Python library to control Amazon devices"
5
5
  authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "3.0.5"
3
+ __version__ = "3.0.7"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -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 as YARL_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 | HttpxClientSession
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: YARL_URL | HTTPX_URL) -> str:
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 (%s)", LIBRARY)
253
- if LIBRARY == "httpx":
254
- self.session = HttpxClientSession(
255
- headers=DEFAULT_HEADERS,
256
- cookies=self._cookies,
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 | HttpxClientResponseWrapper]:
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
- _url,
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="POST",
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(method="GET", url=login_url)
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 (%s)", LIBRARY)
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="GET",
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="GET",
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="POST",
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 Dot",
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)