aioamazondevices 0.9.0__tar.gz → 0.11.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.
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/PKG-INFO +3 -1
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/pyproject.toml +3 -1
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/src/aioamazondevices/__init__.py +2 -2
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/src/aioamazondevices/api.py +58 -13
- aioamazondevices-0.11.0/src/aioamazondevices/auth.py +315 -0
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/src/aioamazondevices/const.py +23 -10
- aioamazondevices-0.11.0/src/aioamazondevices/exceptions.py +51 -0
- aioamazondevices-0.9.0/src/aioamazondevices/exceptions.py +0 -23
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/LICENSE +0 -0
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/README.md +0 -0
- {aioamazondevices-0.9.0 → aioamazondevices-0.11.0}/src/aioamazondevices/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.0
|
4
4
|
Summary: Python library to control Amazon devices
|
5
5
|
Home-page: https://github.com/chemelli74/aioamazondevices
|
6
6
|
License: Apache-2.0
|
@@ -20,6 +20,8 @@ Requires-Dist: beautifulsoup4
|
|
20
20
|
Requires-Dist: colorlog
|
21
21
|
Requires-Dist: httpx
|
22
22
|
Requires-Dist: orjson
|
23
|
+
Requires-Dist: pyasn1
|
24
|
+
Requires-Dist: rsa
|
23
25
|
Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
|
24
26
|
Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
|
25
27
|
Project-URL: Repository, https://github.com/chemelli74/aioamazondevices
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "aioamazondevices"
|
3
|
-
version = "0.
|
3
|
+
version = "0.11.0"
|
4
4
|
description = "Python library to control Amazon devices"
|
5
5
|
authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
|
6
6
|
license = "Apache-2.0"
|
@@ -27,6 +27,8 @@ beautifulsoup4 = "*"
|
|
27
27
|
colorlog = "*"
|
28
28
|
httpx = "*"
|
29
29
|
orjson = "*"
|
30
|
+
pyasn1 = "*"
|
31
|
+
rsa = "*"
|
30
32
|
|
31
33
|
[tool.poetry.group.dev.dependencies]
|
32
34
|
pytest = "^8.1"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""aioamazondevices library."""
|
2
2
|
|
3
|
-
__version__ = "0.
|
3
|
+
__version__ = "0.11.0"
|
4
4
|
|
5
5
|
|
6
6
|
from .api import AmazonDevice, AmazonEchoApi
|
@@ -12,6 +12,6 @@ from .exceptions import (
|
|
12
12
|
__all__ = [
|
13
13
|
"AmazonDevice",
|
14
14
|
"AmazonEchoApi",
|
15
|
-
"CannotConnect",
|
16
15
|
"CannotAuthenticate",
|
16
|
+
"CannotConnect",
|
17
17
|
]
|
@@ -15,8 +15,9 @@ from urllib.parse import parse_qs, urlencode
|
|
15
15
|
|
16
16
|
import orjson
|
17
17
|
from bs4 import BeautifulSoup, Tag
|
18
|
-
from httpx import URL, AsyncClient, Response
|
18
|
+
from httpx import URL, AsyncClient, Auth, Response
|
19
19
|
|
20
|
+
from .auth import Authenticator
|
20
21
|
from .const import (
|
21
22
|
_LOGGER,
|
22
23
|
AMAZON_APP_BUNDLE_ID,
|
@@ -35,7 +36,7 @@ from .const import (
|
|
35
36
|
SAVE_PATH,
|
36
37
|
URI_QUERIES,
|
37
38
|
)
|
38
|
-
from .exceptions import CannotAuthenticate, CannotRegisterDevice
|
39
|
+
from .exceptions import CannotAuthenticate, CannotRegisterDevice, WrongMethod
|
39
40
|
|
40
41
|
|
41
42
|
@dataclass
|
@@ -59,6 +60,7 @@ class AmazonEchoApi:
|
|
59
60
|
login_country_code: str,
|
60
61
|
login_email: str,
|
61
62
|
login_password: str,
|
63
|
+
login_data_file: str | None,
|
62
64
|
save_raw_data: bool = False,
|
63
65
|
) -> None:
|
64
66
|
"""Initialize the scanner."""
|
@@ -81,23 +83,43 @@ class AmazonEchoApi:
|
|
81
83
|
self._cookies = self._build_init_cookies()
|
82
84
|
self._headers = DEFAULT_HEADERS
|
83
85
|
self._save_raw_data = save_raw_data
|
86
|
+
self._login_stored_data: dict[str, Any] = self._load_data_file(login_data_file)
|
84
87
|
self._serial = self._serial_number()
|
88
|
+
self._website_cookies: dict[str, Any] = self._load_website_cookies()
|
85
89
|
|
86
90
|
self.session: AsyncClient
|
87
91
|
|
92
|
+
def _load_data_file(self, data_file: str | None) -> dict[str, Any]:
|
93
|
+
"""Load stored login data from file."""
|
94
|
+
if not data_file or not (file := Path(data_file)).exists():
|
95
|
+
_LOGGER.debug(
|
96
|
+
"Cannot find previous login data file <%s>",
|
97
|
+
data_file,
|
98
|
+
)
|
99
|
+
return {}
|
100
|
+
|
101
|
+
with Path.open(file, "rb") as f:
|
102
|
+
return cast(dict[str, Any], json.load(f))
|
103
|
+
|
104
|
+
def _load_website_cookies(self) -> dict[str, Any]:
|
105
|
+
"""Get website cookies, if avaliables."""
|
106
|
+
if not self._login_stored_data:
|
107
|
+
return {}
|
108
|
+
|
109
|
+
return cast(dict, self._login_stored_data["website_cookies"])
|
110
|
+
|
88
111
|
def _serial_number(self) -> str:
|
89
112
|
"""Get or calculate device serial number."""
|
90
|
-
|
91
|
-
if not fullpath.exists():
|
113
|
+
if not self._login_stored_data:
|
92
114
|
# Create a new serial number
|
93
115
|
_LOGGER.debug("Cannot find previous login data, creating new serial number")
|
94
116
|
return uuid.uuid4().hex.upper()
|
95
117
|
|
96
|
-
with Path.open(fullpath, "rb") as file:
|
97
|
-
data = json.load(file)
|
98
|
-
|
99
118
|
_LOGGER.debug("Found previous login data, loading serial number")
|
100
|
-
return cast(
|
119
|
+
return cast(
|
120
|
+
str,
|
121
|
+
self._login_stored_data["device_info"]["device_serial_number"],
|
122
|
+
)
|
101
123
|
|
102
124
|
def _build_init_cookies(self) -> dict[str, str]:
|
103
125
|
"""Build initial cookies to prevent captcha in most cases."""
|
@@ -193,7 +215,7 @@ class AmazonEchoApi:
|
|
193
215
|
parsed_url = parse_qs(url.query.decode())
|
194
216
|
return parsed_url["openid.oa2.authorization_code"][0]
|
195
217
|
|
196
|
-
def _client_session(self) -> None:
|
218
|
+
def _client_session(self, auth: Auth | None = None) -> None:
|
197
219
|
"""Create httpx ClientSession."""
|
198
220
|
if not hasattr(self, "session") or self.session.is_closed:
|
199
221
|
_LOGGER.debug("Creating HTTP ClientSession")
|
@@ -202,6 +224,7 @@ class AmazonEchoApi:
|
|
202
224
|
headers=DEFAULT_HEADERS,
|
203
225
|
cookies=self._cookies,
|
204
226
|
follow_redirects=True,
|
227
|
+
auth=auth,
|
205
228
|
)
|
206
229
|
|
207
230
|
async def _session_request(
|
@@ -216,6 +239,7 @@ class AmazonEchoApi:
|
|
216
239
|
method,
|
217
240
|
url,
|
218
241
|
data=input_data,
|
242
|
+
cookies=self._website_cookies,
|
219
243
|
)
|
220
244
|
content_type: str = resp.headers.get("Content-Type", "")
|
221
245
|
_LOGGER.debug("Response content type: %s", content_type)
|
@@ -367,8 +391,8 @@ class AmazonEchoApi:
|
|
367
391
|
await self._save_to_file(login_data, "login_data", JSON_EXTENSION)
|
368
392
|
return login_data
|
369
393
|
|
370
|
-
async def
|
371
|
-
"""Login to Amazon."""
|
394
|
+
async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
|
395
|
+
"""Login to Amazon interactively via OTP."""
|
372
396
|
_LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
|
373
397
|
self._client_session()
|
374
398
|
|
@@ -432,6 +456,28 @@ class AmazonEchoApi:
|
|
432
456
|
_LOGGER.info("Register device: %s", register_device)
|
433
457
|
return register_device
|
434
458
|
|
459
|
+
async def login_mode_stored_data(self) -> dict[str, Any]:
|
460
|
+
"""Login to Amazon using previously stored data."""
|
461
|
+
if not self._login_stored_data:
|
462
|
+
_LOGGER.debug(
|
463
|
+
"Cannot find previous login data,\
|
464
|
+
use login_interactive() method instead",
|
465
|
+
)
|
466
|
+
raise WrongMethod
|
467
|
+
|
468
|
+
_LOGGER.debug(
|
469
|
+
"Logging-in for %s with stored data",
|
470
|
+
self._login_email,
|
471
|
+
)
|
472
|
+
|
473
|
+
auth = Authenticator.from_dict(
|
474
|
+
self._login_stored_data,
|
475
|
+
self._domain,
|
476
|
+
)
|
477
|
+
self._client_session(auth)
|
478
|
+
|
479
|
+
return self._login_stored_data
|
480
|
+
|
435
481
|
async def close(self) -> None:
|
436
482
|
"""Close httpx session."""
|
437
483
|
if hasattr(self, "session"):
|
@@ -467,8 +513,7 @@ class AmazonEchoApi:
|
|
467
513
|
|
468
514
|
# Remove stale, orphaned and virtual devices
|
469
515
|
final_devices_list: dict[str, Any] = devices.copy()
|
470
|
-
for serial in devices:
|
471
|
-
device = devices[serial]
|
516
|
+
for serial, device in devices.items():
|
472
517
|
if (
|
473
518
|
DEVICES not in device
|
474
519
|
or device[DEVICES].get("deviceType") == AMAZON_DEVICE_TYPE
|
@@ -0,0 +1,315 @@
|
|
1
|
+
"""Custom authentication module for httpx."""
|
2
|
+
|
3
|
+
import base64
|
4
|
+
from collections.abc import Generator
|
5
|
+
from datetime import UTC, datetime, timedelta
|
6
|
+
from typing import (
|
7
|
+
Any,
|
8
|
+
cast,
|
9
|
+
)
|
10
|
+
|
11
|
+
import httpx
|
12
|
+
from httpx import Cookies
|
13
|
+
from pyasn1.codec.der import decoder
|
14
|
+
from pyasn1.type import namedtype, univ
|
15
|
+
from rsa import PrivateKey, pkcs1
|
16
|
+
|
17
|
+
from .const import _LOGGER, AMAZON_APP_NAME, AMAZON_APP_VERSION
|
18
|
+
from .exceptions import (
|
19
|
+
AuthFlowError,
|
20
|
+
AuthMissingAccessToken,
|
21
|
+
AuthMissingRefreshToken,
|
22
|
+
AuthMissingSigningData,
|
23
|
+
AuthMissingTimestamp,
|
24
|
+
AuthMissingWebsiteCookies,
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
def base64_der_to_pkcs1(base64_key: str) -> str:
|
29
|
+
"""Convert DER private key to PEM format."""
|
30
|
+
|
31
|
+
class PrivateKeyAlgorithm(univ.Sequence): # type: ignore[misc]
|
32
|
+
component_type = namedtype.NamedTypes(
|
33
|
+
namedtype.NamedType("algorithm", univ.ObjectIdentifier()),
|
34
|
+
namedtype.NamedType("parameters", univ.Any()),
|
35
|
+
)
|
36
|
+
|
37
|
+
class PrivateKeyInfo(univ.Sequence): # type: ignore[misc]
|
38
|
+
component_type = namedtype.NamedTypes(
|
39
|
+
namedtype.NamedType("version", univ.Integer()),
|
40
|
+
namedtype.NamedType("pkalgo", PrivateKeyAlgorithm()),
|
41
|
+
namedtype.NamedType("key", univ.OctetString()),
|
42
|
+
)
|
43
|
+
|
44
|
+
encoded_key = base64.b64decode(base64_key)
|
45
|
+
(key_info, _) = decoder.decode(encoded_key, asn1Spec=PrivateKeyInfo())
|
46
|
+
key_octet_string = key_info.components[2]
|
47
|
+
key = PrivateKey.load_pkcs1(key_octet_string, format="DER")
|
48
|
+
return cast(str, key.save_pkcs1().decode("utf-8"))
|
49
|
+
|
50
|
+
|
51
|
+
def refresh_access_token(
|
52
|
+
refresh_token: str,
|
53
|
+
domain: str,
|
54
|
+
) -> dict[str, Any]:
|
55
|
+
"""Refresh an access token."""
|
56
|
+
body = {
|
57
|
+
"app_name": AMAZON_APP_NAME,
|
58
|
+
"app_version": AMAZON_APP_VERSION,
|
59
|
+
"source_token": refresh_token,
|
60
|
+
"requested_token_type": "access_token",
|
61
|
+
"source_token_type": "refresh_token",
|
62
|
+
}
|
63
|
+
|
64
|
+
resp = httpx.post(f"https://api.amazon.{domain}/auth/token", data=body)
|
65
|
+
resp.raise_for_status()
|
66
|
+
resp_dict = resp.json()
|
67
|
+
|
68
|
+
expires_in_sec = int(resp_dict["expires_in"])
|
69
|
+
expires = (datetime.now(UTC) + timedelta(seconds=expires_in_sec)).timestamp()
|
70
|
+
|
71
|
+
return {"access_token": resp_dict["access_token"], "expires": expires}
|
72
|
+
|
73
|
+
|
74
|
+
def refresh_website_cookies(
|
75
|
+
refresh_token: str,
|
76
|
+
domain: str,
|
77
|
+
) -> dict[str, str]:
|
78
|
+
"""Fetch website cookies for a specific domain."""
|
79
|
+
url = f"https://www.amazon.{domain}/ap/exchangetoken/cookies"
|
80
|
+
|
81
|
+
body = {
|
82
|
+
"app_name": AMAZON_APP_NAME,
|
83
|
+
"app_version": AMAZON_APP_VERSION,
|
84
|
+
"source_token": refresh_token,
|
85
|
+
"requested_token_type": "auth_cookies",
|
86
|
+
"source_token_type": "refresh_token",
|
87
|
+
"domain": f".amazon.{domain}",
|
88
|
+
}
|
89
|
+
|
90
|
+
resp = httpx.post(url, data=body)
|
91
|
+
resp.raise_for_status()
|
92
|
+
resp_dict = resp.json()
|
93
|
+
|
94
|
+
raw_cookies = resp_dict["response"]["tokens"]["cookies"]
|
95
|
+
website_cookies = {}
|
96
|
+
for domain_cookies in raw_cookies:
|
97
|
+
for cookie in raw_cookies[domain_cookies]:
|
98
|
+
website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r"")
|
99
|
+
|
100
|
+
return website_cookies
|
101
|
+
|
102
|
+
|
103
|
+
def user_profile(access_token: str, domain: str) -> dict[str, Any]:
|
104
|
+
"""Return Amazon user profile from Amazon."""
|
105
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
106
|
+
|
107
|
+
resp = httpx.get(f"https://api.amazon.{domain}/user/profile", headers=headers)
|
108
|
+
resp.raise_for_status()
|
109
|
+
profile: dict[str, Any] = resp.json()
|
110
|
+
|
111
|
+
if "user_id" not in profile:
|
112
|
+
raise ValueError("Malformed user profile response.")
|
113
|
+
|
114
|
+
return profile
|
115
|
+
|
116
|
+
|
117
|
+
def sign_request(
|
118
|
+
method: str,
|
119
|
+
path: str,
|
120
|
+
body: bytes,
|
121
|
+
adp_token: str,
|
122
|
+
private_key: str,
|
123
|
+
) -> dict[str, str]:
|
124
|
+
"""Create signed headers for http requests."""
|
125
|
+
date = datetime.now(UTC).isoformat("T") + "Z"
|
126
|
+
str_body = body.decode("utf-8")
|
127
|
+
|
128
|
+
data = f"{method}\n{path}\n{date}\n{str_body}\n{adp_token}"
|
129
|
+
|
130
|
+
key = PrivateKey.load_pkcs1(private_key.encode("utf-8"))
|
131
|
+
cipher = pkcs1.sign(data.encode(), key, "SHA-256")
|
132
|
+
signed_encoded = base64.b64encode(cipher)
|
133
|
+
|
134
|
+
signature = f"{signed_encoded.decode()}:{date}"
|
135
|
+
|
136
|
+
return {
|
137
|
+
"x-adp-token": adp_token,
|
138
|
+
"x-adp-alg": "SHA256withRSA:1.0",
|
139
|
+
"x-adp-signature": signature,
|
140
|
+
}
|
141
|
+
|
142
|
+
|
143
|
+
class Authenticator(httpx.Auth): # type: ignore[misc]
|
144
|
+
"""Amazon Authenticator class."""
|
145
|
+
|
146
|
+
access_token: str | None = None
|
147
|
+
activation_bytes: str | None = None
|
148
|
+
adp_token: str | None = None
|
149
|
+
customer_info: dict[str, Any] | None = None
|
150
|
+
device_info: dict[str, Any] | None = None
|
151
|
+
device_private_key: str | None = None
|
152
|
+
expires: float | None = None
|
153
|
+
domain: str
|
154
|
+
refresh_token: str | None = None
|
155
|
+
store_authentication_cookie: dict[str, Any] | None = None
|
156
|
+
website_cookies: dict[str, Any] | None = None
|
157
|
+
requires_request_body: bool = True
|
158
|
+
_forbid_new_attrs: bool = True
|
159
|
+
_apply_test_convert: bool = True
|
160
|
+
|
161
|
+
def update_attrs(self, **kwargs: Any) -> None: # noqa: ANN401
|
162
|
+
"""Update attributes from dict."""
|
163
|
+
for attr, value in kwargs.items():
|
164
|
+
setattr(self, attr, value)
|
165
|
+
|
166
|
+
@classmethod
|
167
|
+
def from_dict(
|
168
|
+
cls,
|
169
|
+
data: dict[str, Any],
|
170
|
+
domain: str,
|
171
|
+
) -> "Authenticator":
|
172
|
+
"""Instantiate an Authenticator from authentication dictionary."""
|
173
|
+
auth = cls()
|
174
|
+
|
175
|
+
auth.domain = domain
|
176
|
+
|
177
|
+
if "login_cookies" in data:
|
178
|
+
auth.website_cookies = data.pop("login_cookies")
|
179
|
+
|
180
|
+
if data["device_private_key"].startswith("MII"):
|
181
|
+
pk_der = data["device_private_key"]
|
182
|
+
data["device_private_key"] = base64_der_to_pkcs1(pk_der)
|
183
|
+
|
184
|
+
auth.update_attrs(**data)
|
185
|
+
|
186
|
+
_LOGGER.info("load data from dictionary for domain %s", auth.domain)
|
187
|
+
|
188
|
+
return auth
|
189
|
+
|
190
|
+
def auth_flow(
|
191
|
+
self,
|
192
|
+
request: httpx.Request,
|
193
|
+
) -> Generator[httpx.Request, httpx.Response, None]:
|
194
|
+
"""Auth flow to be executed on every request by :mod:`httpx`."""
|
195
|
+
available_modes = self.available_auth_modes
|
196
|
+
|
197
|
+
_LOGGER.debug("Auth flow modes: %s", available_modes)
|
198
|
+
if "signing" in available_modes:
|
199
|
+
self._apply_signing_auth_flow(request)
|
200
|
+
elif "bearer" in available_modes:
|
201
|
+
self._apply_bearer_auth_flow(request)
|
202
|
+
else:
|
203
|
+
message = "signing or bearer auth flow are not available."
|
204
|
+
_LOGGER.critical(message)
|
205
|
+
raise AuthFlowError(message)
|
206
|
+
|
207
|
+
yield request
|
208
|
+
|
209
|
+
def _apply_signing_auth_flow(self, request: httpx.Request) -> None:
|
210
|
+
if self.adp_token is None or self.device_private_key is None:
|
211
|
+
raise AuthMissingSigningData
|
212
|
+
|
213
|
+
headers = sign_request(
|
214
|
+
method=request.method,
|
215
|
+
path=request.url.raw_path.decode(),
|
216
|
+
body=request.content,
|
217
|
+
adp_token=self.adp_token,
|
218
|
+
private_key=self.device_private_key,
|
219
|
+
)
|
220
|
+
|
221
|
+
request.headers.update(headers)
|
222
|
+
_LOGGER.info("signing auth flow applied to request")
|
223
|
+
|
224
|
+
def _apply_bearer_auth_flow(self, request: httpx.Request) -> None:
|
225
|
+
if self.access_token_expired:
|
226
|
+
self.refresh_access_token()
|
227
|
+
|
228
|
+
if self.access_token is None:
|
229
|
+
raise AuthMissingAccessToken
|
230
|
+
|
231
|
+
headers = {"Authorization": "Bearer " + self.access_token, "client-id": "0"}
|
232
|
+
request.headers.update(headers)
|
233
|
+
_LOGGER.info("bearer auth flow applied to request")
|
234
|
+
|
235
|
+
def _apply_cookies_auth_flow(self, request: httpx.Request) -> None:
|
236
|
+
if self.website_cookies is None:
|
237
|
+
raise AuthMissingWebsiteCookies
|
238
|
+
cookies = self.website_cookies.copy()
|
239
|
+
|
240
|
+
Cookies(cookies).set_cookie_header(request)
|
241
|
+
_LOGGER.info("cookies auth flow applied to request")
|
242
|
+
|
243
|
+
@property
|
244
|
+
def available_auth_modes(self) -> list[str]:
|
245
|
+
"""List available authentication modes."""
|
246
|
+
available_modes = []
|
247
|
+
|
248
|
+
if self.adp_token and self.device_private_key:
|
249
|
+
available_modes.append("signing")
|
250
|
+
|
251
|
+
if self.access_token:
|
252
|
+
if self.access_token_expired and not self.refresh_token:
|
253
|
+
pass
|
254
|
+
else:
|
255
|
+
available_modes.append("bearer")
|
256
|
+
|
257
|
+
if self.website_cookies:
|
258
|
+
available_modes.append("cookies")
|
259
|
+
|
260
|
+
return available_modes
|
261
|
+
|
262
|
+
def refresh_access_token(self, force: bool = False) -> None:
|
263
|
+
"""Refresh access token."""
|
264
|
+
if force or self.access_token_expired:
|
265
|
+
if self.refresh_token is None:
|
266
|
+
message = "No refresh token found. Can't refresh access token."
|
267
|
+
_LOGGER.critical(message)
|
268
|
+
raise AuthMissingRefreshToken(message)
|
269
|
+
|
270
|
+
refresh_data = refresh_access_token(
|
271
|
+
refresh_token=self.refresh_token,
|
272
|
+
domain=self.domain,
|
273
|
+
)
|
274
|
+
|
275
|
+
self.update_attrs(**refresh_data)
|
276
|
+
else:
|
277
|
+
_LOGGER.info(
|
278
|
+
"Access Token not expired. No refresh necessary. "
|
279
|
+
"To force refresh please use force=True",
|
280
|
+
)
|
281
|
+
|
282
|
+
def set_website_cookies_for_country(self) -> None:
|
283
|
+
"""Set website cookies for country."""
|
284
|
+
if self.refresh_token is None:
|
285
|
+
raise AuthMissingRefreshToken
|
286
|
+
|
287
|
+
self.website_cookies = refresh_website_cookies(
|
288
|
+
self.refresh_token,
|
289
|
+
self.domain,
|
290
|
+
)
|
291
|
+
|
292
|
+
def user_profile(self) -> dict[str, Any]:
|
293
|
+
"""Get user profile."""
|
294
|
+
if self.access_token is None:
|
295
|
+
raise AuthMissingAccessToken
|
296
|
+
|
297
|
+
return user_profile(access_token=self.access_token, domain=self.domain)
|
298
|
+
|
299
|
+
@property
|
300
|
+
def access_token_expires(self) -> timedelta:
|
301
|
+
"""Time to access token expiration."""
|
302
|
+
if self.expires is None:
|
303
|
+
raise AuthMissingTimestamp
|
304
|
+
return datetime.fromtimestamp(self.expires, UTC) - datetime.now(
|
305
|
+
UTC,
|
306
|
+
)
|
307
|
+
|
308
|
+
@property
|
309
|
+
def access_token_expired(self) -> bool:
|
310
|
+
"""Return True if access token is expired."""
|
311
|
+
if self.expires is None:
|
312
|
+
raise AuthMissingTimestamp
|
313
|
+
return datetime.fromtimestamp(self.expires, UTC) <= datetime.now(
|
314
|
+
UTC,
|
315
|
+
)
|
@@ -26,9 +26,18 @@ DOMAIN_BY_COUNTRY = {
|
|
26
26
|
},
|
27
27
|
}
|
28
28
|
|
29
|
+
# Amazon APP info
|
30
|
+
AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
|
31
|
+
AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
|
32
|
+
AMAZON_APP_NAME = "AioAmazonDevices"
|
33
|
+
AMAZON_APP_VERSION = "2.2.556530.0"
|
34
|
+
AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
|
35
|
+
AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
|
36
|
+
AMAZON_CLIENT_OS = "16.6"
|
37
|
+
|
29
38
|
DEFAULT_HEADERS = {
|
30
39
|
"User-Agent": (
|
31
|
-
"Mozilla/5.0 (iPhone; CPU iPhone OS
|
40
|
+
f"Mozilla/5.0 (iPhone; CPU iPhone OS {AMAZON_CLIENT_OS.replace('.', '_')} like Mac OS X) " # noqa: E501
|
32
41
|
"AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
|
33
42
|
),
|
34
43
|
"Accept-Language": "en-US",
|
@@ -43,16 +52,20 @@ URI_QUERIES = {
|
|
43
52
|
"bluetoothStates": "/api/bluetooth",
|
44
53
|
}
|
45
54
|
|
46
|
-
# Amazon APP info
|
47
|
-
AMAZON_APP_BUNDLE_ID = "com.amazon.echo"
|
48
|
-
AMAZON_APP_ID = "MAPiOSLib/6.0/ToHideRetailLink"
|
49
|
-
AMAZON_APP_NAME = "AioAmazonDevices"
|
50
|
-
AMAZON_APP_VERSION = "2.2.556530.0"
|
51
|
-
AMAZON_DEVICE_SOFTWARE_VERSION = "35602678"
|
52
|
-
AMAZON_DEVICE_TYPE = "A2IVLV5VM2W81"
|
53
|
-
AMAZON_CLIENT_OS = "16.6"
|
54
|
-
|
55
55
|
# File extensions
|
56
56
|
SAVE_PATH = "out"
|
57
57
|
HTML_EXTENSION = ".html"
|
58
58
|
JSON_EXTENSION = ".json"
|
59
|
+
|
60
|
+
DEVICE_TYPE_TO_MODEL = {
|
61
|
+
"A1RABVCI4QCIKC": "Echo Dot (Gen3)",
|
62
|
+
"A2DS1Q2TPDJ48U": "Echo Dot Clock (Gen5)",
|
63
|
+
"A2H4LV5GIZ1JFT": "Echo Dot Clock (Gen4)",
|
64
|
+
"A2U21SRK4QGSE1": "Echo Dot Clock (Gen4)",
|
65
|
+
"A32DDESGESSHZA": "Echo Dot (Gen3)",
|
66
|
+
"A32DOYMUN6DTXA": "Echo Dot (Gen3)",
|
67
|
+
"A3RMGO6LYLH7YN": "Echo Dot (Gen4)",
|
68
|
+
"A3S5BH2HU6VAYF": "Echo Dot (Gen2)",
|
69
|
+
"A4ZXE0RM7LQ7A": "Echo Dot (Gen5)",
|
70
|
+
"AKNO1N0KSFN8L": "Echo Dot (Gen1)",
|
71
|
+
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""Comelit SimpleHome library exceptions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
|
6
|
+
class AmazonError(Exception):
|
7
|
+
"""Base class for aioamazondevices errors."""
|
8
|
+
|
9
|
+
|
10
|
+
class CannotConnect(AmazonError):
|
11
|
+
"""Exception raised when connection fails."""
|
12
|
+
|
13
|
+
|
14
|
+
class CannotAuthenticate(AmazonError):
|
15
|
+
"""Exception raised when credentials are incorrect."""
|
16
|
+
|
17
|
+
|
18
|
+
class CannotRetrieveData(AmazonError):
|
19
|
+
"""Exception raised when data retrieval fails."""
|
20
|
+
|
21
|
+
|
22
|
+
class CannotRegisterDevice(AmazonError):
|
23
|
+
"""Exception raised when device registration fails."""
|
24
|
+
|
25
|
+
|
26
|
+
class WrongMethod(AmazonError):
|
27
|
+
"""Exception raised when the wrong login metho is used."""
|
28
|
+
|
29
|
+
|
30
|
+
class AuthFlowError(AmazonError):
|
31
|
+
"""Exception raised when auth flow fails."""
|
32
|
+
|
33
|
+
|
34
|
+
class AuthMissingTimestamp(AmazonError):
|
35
|
+
"""Exception raised when expires timestamp is missing."""
|
36
|
+
|
37
|
+
|
38
|
+
class AuthMissingAccessToken(AmazonError):
|
39
|
+
"""Exception raised when access token is missing."""
|
40
|
+
|
41
|
+
|
42
|
+
class AuthMissingRefreshToken(AmazonError):
|
43
|
+
"""Exception raised when refresh token is missing."""
|
44
|
+
|
45
|
+
|
46
|
+
class AuthMissingSigningData(AmazonError):
|
47
|
+
"""Exception raised when some data for signing are missing."""
|
48
|
+
|
49
|
+
|
50
|
+
class AuthMissingWebsiteCookies(AmazonError):
|
51
|
+
"""Exception raised when website cookies are missing."""
|
@@ -1,23 +0,0 @@
|
|
1
|
-
"""Comelit SimpleHome library exceptions."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
|
6
|
-
class AmazonError(Exception):
|
7
|
-
"""Base class for aioamazondevices errors."""
|
8
|
-
|
9
|
-
|
10
|
-
class CannotConnect(AmazonError):
|
11
|
-
"""Exception raised when connection fails."""
|
12
|
-
|
13
|
-
|
14
|
-
class CannotAuthenticate(AmazonError):
|
15
|
-
"""Exception raised when credentials are incorrect."""
|
16
|
-
|
17
|
-
|
18
|
-
class CannotRetrieveData(AmazonError):
|
19
|
-
"""Exception raised when data retrieval fails."""
|
20
|
-
|
21
|
-
|
22
|
-
class CannotRegisterDevice(AmazonError):
|
23
|
-
"""Exception raised when device registration fails."""
|
File without changes
|
File without changes
|
File without changes
|