aioamazondevices 6.4.5__py3-none-any.whl → 9.0.2__py3-none-any.whl
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/__init__.py +1 -1
- aioamazondevices/api.py +333 -825
- aioamazondevices/const/__init__.py +1 -0
- aioamazondevices/{const.py → const/devices.py} +47 -106
- aioamazondevices/const/http.py +36 -0
- aioamazondevices/const/metadata.py +44 -0
- aioamazondevices/const/queries.py +97 -0
- aioamazondevices/const/schedules.py +61 -0
- aioamazondevices/{sounds.py → const/sounds.py} +2 -1
- aioamazondevices/http_wrapper.py +349 -0
- aioamazondevices/login.py +445 -0
- aioamazondevices/structures.py +65 -0
- aioamazondevices/utils.py +23 -1
- {aioamazondevices-6.4.5.dist-info → aioamazondevices-9.0.2.dist-info}/METADATA +18 -4
- aioamazondevices-9.0.2.dist-info/RECORD +19 -0
- aioamazondevices/query.py +0 -84
- aioamazondevices-6.4.5.dist-info/RECORD +0 -12
- {aioamazondevices-6.4.5.dist-info → aioamazondevices-9.0.2.dist-info}/WHEEL +0 -0
- {aioamazondevices-6.4.5.dist-info → aioamazondevices-9.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Support for Amazon login."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import secrets
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import UTC, datetime, timedelta
|
|
9
|
+
from http import HTTPMethod, HTTPStatus
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
from urllib.parse import parse_qs, urlencode
|
|
12
|
+
|
|
13
|
+
import orjson
|
|
14
|
+
from bs4 import BeautifulSoup, Tag
|
|
15
|
+
from multidict import MultiDictProxy
|
|
16
|
+
from yarl import URL
|
|
17
|
+
|
|
18
|
+
from .const.http import (
|
|
19
|
+
AMAZON_APP_BUNDLE_ID,
|
|
20
|
+
AMAZON_APP_NAME,
|
|
21
|
+
AMAZON_APP_VERSION,
|
|
22
|
+
AMAZON_CLIENT_OS,
|
|
23
|
+
AMAZON_DEVICE_SOFTWARE_VERSION,
|
|
24
|
+
AMAZON_DEVICE_TYPE,
|
|
25
|
+
DEFAULT_SITE,
|
|
26
|
+
REFRESH_ACCESS_TOKEN,
|
|
27
|
+
REFRESH_AUTH_COOKIES,
|
|
28
|
+
URI_SIGNIN,
|
|
29
|
+
)
|
|
30
|
+
from .exceptions import (
|
|
31
|
+
CannotAuthenticate,
|
|
32
|
+
CannotRegisterDevice,
|
|
33
|
+
WrongMethod,
|
|
34
|
+
)
|
|
35
|
+
from .http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
|
|
36
|
+
from .utils import _LOGGER, obfuscate_email, scrub_fields
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AmazonLogin:
|
|
40
|
+
"""Amazon login for Echo devices."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
http_wrapper: AmazonHttpWrapper,
|
|
45
|
+
session_state_data: AmazonSessionStateData,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Login to Amazon."""
|
|
48
|
+
self._session_state_data = session_state_data
|
|
49
|
+
self._http_wrapper = http_wrapper
|
|
50
|
+
|
|
51
|
+
self._serial = self._serial_number()
|
|
52
|
+
|
|
53
|
+
def _serial_number(self) -> str:
|
|
54
|
+
"""Get or calculate device serial number."""
|
|
55
|
+
if not self._session_state_data.login_stored_data:
|
|
56
|
+
# Create a new serial number
|
|
57
|
+
_LOGGER.debug("Cannot find previous login data, creating new serial number")
|
|
58
|
+
return uuid.uuid4().hex.upper()
|
|
59
|
+
|
|
60
|
+
_LOGGER.debug("Found previous login data, loading serial number")
|
|
61
|
+
return cast(
|
|
62
|
+
"str",
|
|
63
|
+
self._session_state_data.login_stored_data["device_info"][
|
|
64
|
+
"device_serial_number"
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _create_code_verifier(self, length: int = 32) -> bytes:
|
|
69
|
+
"""Create code verifier."""
|
|
70
|
+
verifier = secrets.token_bytes(length)
|
|
71
|
+
return base64.urlsafe_b64encode(verifier).rstrip(b"=")
|
|
72
|
+
|
|
73
|
+
def _create_s256_code_challenge(self, verifier: bytes) -> bytes:
|
|
74
|
+
"""Create S256 code challenge."""
|
|
75
|
+
m = hashlib.sha256(verifier)
|
|
76
|
+
return base64.urlsafe_b64encode(m.digest()).rstrip(b"=")
|
|
77
|
+
|
|
78
|
+
def _build_client_id(self) -> str:
|
|
79
|
+
"""Build client ID."""
|
|
80
|
+
client_id = self._serial.encode() + b"#" + AMAZON_DEVICE_TYPE.encode("utf-8")
|
|
81
|
+
return client_id.hex()
|
|
82
|
+
|
|
83
|
+
def _build_oauth_url(
|
|
84
|
+
self,
|
|
85
|
+
code_verifier: bytes,
|
|
86
|
+
client_id: str,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Build the url to login to Amazon as a Mobile device."""
|
|
89
|
+
code_challenge = self._create_s256_code_challenge(code_verifier)
|
|
90
|
+
|
|
91
|
+
oauth_params = {
|
|
92
|
+
"openid.return_to": "https://www.amazon.com/ap/maplanding",
|
|
93
|
+
"openid.oa2.code_challenge_method": "S256",
|
|
94
|
+
"openid.assoc_handle": "amzn_dp_project_dee_ios",
|
|
95
|
+
"openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
|
|
96
|
+
"pageId": "amzn_dp_project_dee_ios",
|
|
97
|
+
"accountStatusPolicy": "P1",
|
|
98
|
+
"openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
|
|
99
|
+
"openid.mode": "checkid_setup",
|
|
100
|
+
"openid.ns.oa2": "http://www.amazon.com/ap/ext/oauth/2",
|
|
101
|
+
"openid.oa2.client_id": f"device:{client_id}",
|
|
102
|
+
"language": self._session_state_data.language.replace("-", "_"),
|
|
103
|
+
"openid.ns.pape": "http://specs.openid.net/extensions/pape/1.0",
|
|
104
|
+
"openid.oa2.code_challenge": code_challenge,
|
|
105
|
+
"openid.oa2.scope": "device_auth_access",
|
|
106
|
+
"openid.ns": "http://specs.openid.net/auth/2.0",
|
|
107
|
+
"openid.pape.max_auth_age": "0",
|
|
108
|
+
"openid.oa2.response_type": "code",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return f"https://www.amazon.com{URI_SIGNIN}?{urlencode(oauth_params)}"
|
|
112
|
+
|
|
113
|
+
def _get_inputs_from_soup(self, soup: BeautifulSoup) -> dict[str, str]:
|
|
114
|
+
"""Extract hidden form input fields from a Amazon login page."""
|
|
115
|
+
form = soup.find("form", {"name": "signIn"}) or soup.find("form")
|
|
116
|
+
|
|
117
|
+
if not isinstance(form, Tag):
|
|
118
|
+
raise CannotAuthenticate("Unable to find form in login response")
|
|
119
|
+
|
|
120
|
+
inputs = {}
|
|
121
|
+
for field in form.find_all("input"):
|
|
122
|
+
if isinstance(field, Tag) and field.get("type", "") == "hidden":
|
|
123
|
+
inputs[field["name"]] = field.get("value", "")
|
|
124
|
+
|
|
125
|
+
return inputs
|
|
126
|
+
|
|
127
|
+
def _get_request_from_soup(self, soup: BeautifulSoup) -> tuple[str, str]:
|
|
128
|
+
"""Extract URL and method for the next request."""
|
|
129
|
+
_LOGGER.debug("Get request data from HTML source")
|
|
130
|
+
form = soup.find("form", {"name": "signIn"}) or soup.find("form")
|
|
131
|
+
if isinstance(form, Tag):
|
|
132
|
+
method = form.get("method")
|
|
133
|
+
url = form.get("action")
|
|
134
|
+
if isinstance(method, str) and isinstance(url, str):
|
|
135
|
+
return method, url
|
|
136
|
+
raise CannotAuthenticate("Unable to extract form data from response")
|
|
137
|
+
|
|
138
|
+
def _extract_code_from_url(self, url: URL) -> str:
|
|
139
|
+
"""Extract the access token from url query after login."""
|
|
140
|
+
parsed_url: dict[str, list[str]] = {}
|
|
141
|
+
if isinstance(url.query, bytes):
|
|
142
|
+
parsed_url = parse_qs(url.query.decode())
|
|
143
|
+
elif isinstance(url.query, MultiDictProxy):
|
|
144
|
+
for key, value in url.query.items():
|
|
145
|
+
parsed_url[key] = [value]
|
|
146
|
+
else:
|
|
147
|
+
raise CannotAuthenticate(
|
|
148
|
+
f"Unable to extract authorization code from url: {url}"
|
|
149
|
+
)
|
|
150
|
+
return parsed_url["openid.oa2.authorization_code"][0]
|
|
151
|
+
|
|
152
|
+
async def _register_device(
|
|
153
|
+
self,
|
|
154
|
+
data: dict[str, Any],
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
"""Register a dummy Alexa device."""
|
|
157
|
+
authorization_code: str = data["authorization_code"]
|
|
158
|
+
code_verifier: bytes = data["code_verifier"]
|
|
159
|
+
|
|
160
|
+
body = {
|
|
161
|
+
"requested_extensions": ["device_info", "customer_info"],
|
|
162
|
+
"cookies": {
|
|
163
|
+
"website_cookies": [],
|
|
164
|
+
"domain": f".amazon.{self._session_state_data.domain}",
|
|
165
|
+
},
|
|
166
|
+
"registration_data": {
|
|
167
|
+
"domain": "Device",
|
|
168
|
+
"app_version": AMAZON_APP_VERSION,
|
|
169
|
+
"device_type": AMAZON_DEVICE_TYPE,
|
|
170
|
+
"device_name": (
|
|
171
|
+
f"%FIRST_NAME%\u0027s%DUPE_STRATEGY_1ST%{AMAZON_APP_NAME}"
|
|
172
|
+
),
|
|
173
|
+
"os_version": AMAZON_CLIENT_OS,
|
|
174
|
+
"device_serial": self._serial,
|
|
175
|
+
"device_model": "iPhone",
|
|
176
|
+
"app_name": AMAZON_APP_NAME,
|
|
177
|
+
"software_version": AMAZON_DEVICE_SOFTWARE_VERSION,
|
|
178
|
+
},
|
|
179
|
+
"auth_data": {
|
|
180
|
+
"use_global_authentication": "true",
|
|
181
|
+
"client_id": self._build_client_id(),
|
|
182
|
+
"authorization_code": authorization_code,
|
|
183
|
+
"code_verifier": code_verifier.decode(),
|
|
184
|
+
"code_algorithm": "SHA-256",
|
|
185
|
+
"client_domain": "DeviceLegacy",
|
|
186
|
+
},
|
|
187
|
+
"user_context_map": {"frc": self._http_wrapper.cookies["frc"]},
|
|
188
|
+
"requested_token_type": [
|
|
189
|
+
"bearer",
|
|
190
|
+
"mac_dms",
|
|
191
|
+
"website_cookies",
|
|
192
|
+
"store_authentication_cookie",
|
|
193
|
+
],
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
register_url = "https://api.amazon.com/auth/register"
|
|
197
|
+
_, raw_resp = await self._http_wrapper.session_request(
|
|
198
|
+
method=HTTPMethod.POST,
|
|
199
|
+
url=register_url,
|
|
200
|
+
input_data=body,
|
|
201
|
+
json_data=True,
|
|
202
|
+
)
|
|
203
|
+
resp_json = await self._http_wrapper.response_to_json(raw_resp)
|
|
204
|
+
|
|
205
|
+
if raw_resp.status != HTTPStatus.OK:
|
|
206
|
+
msg = resp_json["response"]["error"]["message"]
|
|
207
|
+
_LOGGER.error(
|
|
208
|
+
"Cannot register device for %s: %s",
|
|
209
|
+
obfuscate_email(self._session_state_data.login_email),
|
|
210
|
+
msg,
|
|
211
|
+
)
|
|
212
|
+
raise CannotRegisterDevice(
|
|
213
|
+
f"{await self._http_wrapper.http_phrase_error(raw_resp.status)}: {msg}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
success_response = resp_json["response"]["success"]
|
|
217
|
+
|
|
218
|
+
tokens = success_response["tokens"]
|
|
219
|
+
adp_token = tokens["mac_dms"]["adp_token"]
|
|
220
|
+
device_private_key = tokens["mac_dms"]["device_private_key"]
|
|
221
|
+
store_authentication_cookie = tokens["store_authentication_cookie"]
|
|
222
|
+
access_token = tokens["bearer"]["access_token"]
|
|
223
|
+
refresh_token = tokens["bearer"]["refresh_token"]
|
|
224
|
+
expires_s = int(tokens["bearer"]["expires_in"])
|
|
225
|
+
expires = (datetime.now(UTC) + timedelta(seconds=expires_s)).timestamp()
|
|
226
|
+
|
|
227
|
+
extensions = success_response["extensions"]
|
|
228
|
+
device_info = extensions["device_info"]
|
|
229
|
+
customer_info = extensions["customer_info"]
|
|
230
|
+
|
|
231
|
+
website_cookies = {}
|
|
232
|
+
for cookie in tokens["website_cookies"]:
|
|
233
|
+
website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r"")
|
|
234
|
+
|
|
235
|
+
login_data = {
|
|
236
|
+
"adp_token": adp_token,
|
|
237
|
+
"device_private_key": device_private_key,
|
|
238
|
+
"access_token": access_token,
|
|
239
|
+
"refresh_token": refresh_token,
|
|
240
|
+
"expires": expires,
|
|
241
|
+
"website_cookies": website_cookies,
|
|
242
|
+
"store_authentication_cookie": store_authentication_cookie,
|
|
243
|
+
"device_info": device_info,
|
|
244
|
+
"customer_info": customer_info,
|
|
245
|
+
}
|
|
246
|
+
_LOGGER.info("Register device: %s", scrub_fields(login_data))
|
|
247
|
+
return login_data
|
|
248
|
+
|
|
249
|
+
async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
|
|
250
|
+
"""Login to Amazon interactively via OTP."""
|
|
251
|
+
_LOGGER.debug(
|
|
252
|
+
"Logging-in for %s [otp code: %s]",
|
|
253
|
+
obfuscate_email(self._session_state_data.login_email),
|
|
254
|
+
bool(otp_code),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
device_login_data = await self._login_mode_interactive_oauth(otp_code)
|
|
258
|
+
|
|
259
|
+
login_data = await self._register_device(device_login_data)
|
|
260
|
+
self._session_state_data.load_login_stored_data(login_data)
|
|
261
|
+
|
|
262
|
+
await self._domain_refresh_auth_cookies()
|
|
263
|
+
|
|
264
|
+
self._session_state_data.login_stored_data.update(
|
|
265
|
+
{"site": f"https://www.amazon.{self._session_state_data.domain}"}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Can take a little while to register device but we need it
|
|
269
|
+
# to be able to pickout account customer ID
|
|
270
|
+
await asyncio.sleep(2)
|
|
271
|
+
|
|
272
|
+
return self._session_state_data.login_stored_data
|
|
273
|
+
|
|
274
|
+
async def _login_mode_interactive_oauth(
|
|
275
|
+
self, otp_code: str
|
|
276
|
+
) -> dict[str, str | bytes]:
|
|
277
|
+
"""Login interactive via oauth URL."""
|
|
278
|
+
code_verifier = self._create_code_verifier()
|
|
279
|
+
client_id = self._build_client_id()
|
|
280
|
+
|
|
281
|
+
_LOGGER.debug("Build oauth URL")
|
|
282
|
+
login_url = self._build_oauth_url(code_verifier, client_id)
|
|
283
|
+
|
|
284
|
+
login_soup, _ = await self._http_wrapper.session_request(
|
|
285
|
+
method=HTTPMethod.GET,
|
|
286
|
+
url=login_url,
|
|
287
|
+
)
|
|
288
|
+
login_method, login_url = self._get_request_from_soup(login_soup)
|
|
289
|
+
login_inputs = self._get_inputs_from_soup(login_soup)
|
|
290
|
+
login_inputs["email"] = self._session_state_data.login_email
|
|
291
|
+
login_inputs["password"] = self._session_state_data.login_password
|
|
292
|
+
|
|
293
|
+
_LOGGER.debug("Register at %s", login_url)
|
|
294
|
+
login_soup, _ = await self._http_wrapper.session_request(
|
|
295
|
+
method=login_method,
|
|
296
|
+
url=login_url,
|
|
297
|
+
input_data=login_inputs,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if not login_soup.find("input", id="auth-mfa-otpcode"):
|
|
301
|
+
_LOGGER.debug(
|
|
302
|
+
'Cannot find "auth-mfa-otpcode" in html source [%s]', login_url
|
|
303
|
+
)
|
|
304
|
+
raise CannotAuthenticate("MFA OTP code not found on login page")
|
|
305
|
+
|
|
306
|
+
login_method, login_url = self._get_request_from_soup(login_soup)
|
|
307
|
+
|
|
308
|
+
login_inputs = self._get_inputs_from_soup(login_soup)
|
|
309
|
+
login_inputs["otpCode"] = otp_code
|
|
310
|
+
login_inputs["mfaSubmit"] = "Submit"
|
|
311
|
+
login_inputs["rememberDevice"] = "false"
|
|
312
|
+
|
|
313
|
+
login_soup, login_resp = await self._http_wrapper.session_request(
|
|
314
|
+
method=login_method,
|
|
315
|
+
url=login_url,
|
|
316
|
+
input_data=login_inputs,
|
|
317
|
+
)
|
|
318
|
+
_LOGGER.debug("Login response url:%s", login_resp.url)
|
|
319
|
+
|
|
320
|
+
authcode = self._extract_code_from_url(login_resp.url)
|
|
321
|
+
_LOGGER.debug("Login extracted authcode: %s", authcode)
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
"authorization_code": authcode,
|
|
325
|
+
"code_verifier": code_verifier,
|
|
326
|
+
"domain": self._session_state_data.domain,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async def login_mode_stored_data(self) -> dict[str, Any]:
|
|
330
|
+
"""Login to Amazon using previously stored data."""
|
|
331
|
+
if not self._session_state_data.login_stored_data:
|
|
332
|
+
_LOGGER.debug(
|
|
333
|
+
"Cannot find previous login data,\
|
|
334
|
+
use login_mode_interactive() method instead",
|
|
335
|
+
)
|
|
336
|
+
raise WrongMethod
|
|
337
|
+
|
|
338
|
+
_LOGGER.debug(
|
|
339
|
+
"Logging-in for %s with stored data",
|
|
340
|
+
obfuscate_email(self._session_state_data.login_email),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return self._session_state_data.login_stored_data
|
|
344
|
+
|
|
345
|
+
async def _get_alexa_domain(self) -> str:
|
|
346
|
+
"""Get the Alexa domain."""
|
|
347
|
+
_LOGGER.debug("Retrieve Alexa domain")
|
|
348
|
+
_, raw_resp = await self._http_wrapper.session_request(
|
|
349
|
+
method=HTTPMethod.GET,
|
|
350
|
+
url=f"https://alexa.amazon.{self._session_state_data.domain}/api/welcome",
|
|
351
|
+
)
|
|
352
|
+
json_data = await self._http_wrapper.response_to_json(raw_resp)
|
|
353
|
+
return cast(
|
|
354
|
+
"str",
|
|
355
|
+
json_data.get(
|
|
356
|
+
"alexaHostName", f"alexa.amazon.{self._session_state_data.domain}"
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
async def _refresh_auth_cookies(self) -> None:
|
|
361
|
+
"""Refresh cookies after domain swap."""
|
|
362
|
+
_, json_token_resp = await self._refresh_data(REFRESH_AUTH_COOKIES)
|
|
363
|
+
|
|
364
|
+
# Need to take cookies from response and create them as cookies
|
|
365
|
+
website_cookies = self._session_state_data.login_stored_data[
|
|
366
|
+
"website_cookies"
|
|
367
|
+
] = {}
|
|
368
|
+
await self._http_wrapper.clear_cookies()
|
|
369
|
+
|
|
370
|
+
cookie_json = json_token_resp["response"]["tokens"]["cookies"]
|
|
371
|
+
for cookie_domain in cookie_json:
|
|
372
|
+
for cookie in cookie_json[cookie_domain]:
|
|
373
|
+
new_cookie_value = cookie["Value"].replace(r'"', r"")
|
|
374
|
+
new_cookie = {cookie["Name"]: new_cookie_value}
|
|
375
|
+
await self._http_wrapper.set_cookies(new_cookie, URL(cookie_domain))
|
|
376
|
+
website_cookies.update(new_cookie)
|
|
377
|
+
if cookie["Name"] == "session-token":
|
|
378
|
+
self._session_state_data.login_stored_data[
|
|
379
|
+
"store_authentication_cookie"
|
|
380
|
+
] = {"cookie": new_cookie_value}
|
|
381
|
+
|
|
382
|
+
async def _domain_refresh_auth_cookies(self) -> None:
|
|
383
|
+
"""Refresh cookies after domain swap."""
|
|
384
|
+
_LOGGER.debug("Refreshing auth cookies after domain change")
|
|
385
|
+
|
|
386
|
+
# Get the new Alexa domain
|
|
387
|
+
user_domain = (await self._get_alexa_domain()).replace("alexa", "https://www")
|
|
388
|
+
if user_domain != DEFAULT_SITE:
|
|
389
|
+
_LOGGER.debug("User domain changed to %s", user_domain)
|
|
390
|
+
self._session_state_data.country_specific_data(user_domain)
|
|
391
|
+
await self._http_wrapper.clear_csrf_cookie()
|
|
392
|
+
await self._refresh_auth_cookies()
|
|
393
|
+
|
|
394
|
+
async def _refresh_data(self, data_type: str) -> tuple[bool, dict]:
|
|
395
|
+
"""Refresh data."""
|
|
396
|
+
if not self._session_state_data.login_stored_data:
|
|
397
|
+
_LOGGER.debug("No login data available, cannot refresh")
|
|
398
|
+
return False, {}
|
|
399
|
+
|
|
400
|
+
data = {
|
|
401
|
+
"app_name": AMAZON_APP_NAME,
|
|
402
|
+
"app_version": AMAZON_APP_VERSION,
|
|
403
|
+
"di.sdk.version": "6.12.4",
|
|
404
|
+
"source_token": self._session_state_data.login_stored_data["refresh_token"],
|
|
405
|
+
"package_name": AMAZON_APP_BUNDLE_ID,
|
|
406
|
+
"di.hw.version": "iPhone",
|
|
407
|
+
"platform": "iOS",
|
|
408
|
+
"requested_token_type": data_type,
|
|
409
|
+
"source_token_type": "refresh_token",
|
|
410
|
+
"di.os.name": "iOS",
|
|
411
|
+
"di.os.version": AMAZON_CLIENT_OS,
|
|
412
|
+
"current_version": "6.12.4",
|
|
413
|
+
"previous_version": "6.12.4",
|
|
414
|
+
"domain": f"www.amazon.{self._session_state_data.domain}",
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_, raw_resp = await self._http_wrapper.session_request(
|
|
418
|
+
method=HTTPMethod.POST,
|
|
419
|
+
url="https://api.amazon.com/auth/token",
|
|
420
|
+
input_data=data,
|
|
421
|
+
json_data=False,
|
|
422
|
+
)
|
|
423
|
+
_LOGGER.debug(
|
|
424
|
+
"Refresh data response %s with payload %s",
|
|
425
|
+
raw_resp.status,
|
|
426
|
+
orjson.dumps(data),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if raw_resp.status != HTTPStatus.OK:
|
|
430
|
+
_LOGGER.debug("Failed to refresh data")
|
|
431
|
+
return False, {}
|
|
432
|
+
|
|
433
|
+
json_response = await self._http_wrapper.response_to_json(raw_resp, data_type)
|
|
434
|
+
|
|
435
|
+
if data_type == REFRESH_ACCESS_TOKEN and (
|
|
436
|
+
new_token := json_response.get(REFRESH_ACCESS_TOKEN)
|
|
437
|
+
):
|
|
438
|
+
self._session_state_data.login_stored_data[REFRESH_ACCESS_TOKEN] = new_token
|
|
439
|
+
return True, json_response
|
|
440
|
+
|
|
441
|
+
if data_type == REFRESH_AUTH_COOKIES:
|
|
442
|
+
return True, json_response
|
|
443
|
+
|
|
444
|
+
_LOGGER.debug("Unexpected refresh data response")
|
|
445
|
+
return False, {}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""aioamazondevices structures module."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AmazonDeviceSensor:
|
|
10
|
+
"""Amazon device sensor class."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
value: str | int | float
|
|
14
|
+
error: bool
|
|
15
|
+
error_type: str | None
|
|
16
|
+
error_msg: str | None
|
|
17
|
+
scale: str | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AmazonSchedule:
|
|
22
|
+
"""Amazon schedule class."""
|
|
23
|
+
|
|
24
|
+
type: str # alarm, reminder, timer
|
|
25
|
+
status: str
|
|
26
|
+
label: str
|
|
27
|
+
next_occurrence: datetime | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AmazonDevice:
|
|
32
|
+
"""Amazon device class."""
|
|
33
|
+
|
|
34
|
+
account_name: str
|
|
35
|
+
capabilities: list[str]
|
|
36
|
+
device_family: str
|
|
37
|
+
device_type: str
|
|
38
|
+
device_owner_customer_id: str
|
|
39
|
+
household_device: bool
|
|
40
|
+
device_cluster_members: list[str]
|
|
41
|
+
online: bool
|
|
42
|
+
serial_number: str
|
|
43
|
+
software_version: str
|
|
44
|
+
entity_id: str | None
|
|
45
|
+
endpoint_id: str | None
|
|
46
|
+
sensors: dict[str, AmazonDeviceSensor]
|
|
47
|
+
notifications: dict[str, AmazonSchedule]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AmazonSequenceType(StrEnum):
|
|
51
|
+
"""Amazon sequence types."""
|
|
52
|
+
|
|
53
|
+
Announcement = "AlexaAnnouncement"
|
|
54
|
+
Speak = "Alexa.Speak"
|
|
55
|
+
Sound = "Alexa.Sound"
|
|
56
|
+
Music = "Alexa.Music.PlaySearchPhrase"
|
|
57
|
+
TextCommand = "Alexa.TextCommand"
|
|
58
|
+
LaunchSkill = "Alexa.Operation.SkillConnections.Launch"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AmazonMusicSource(StrEnum):
|
|
62
|
+
"""Amazon music sources."""
|
|
63
|
+
|
|
64
|
+
Radio = "TUNEIN"
|
|
65
|
+
AmazonMusic = "AMAZON_MUSIC"
|
aioamazondevices/utils.py
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
"""Utils module for Amazon devices."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from collections.abc import Collection
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
_LOGGER = logging.getLogger(__package__)
|
|
8
|
+
|
|
9
|
+
TO_REDACT = {
|
|
10
|
+
"address",
|
|
11
|
+
"address1",
|
|
12
|
+
"address2",
|
|
13
|
+
"address3",
|
|
14
|
+
"city",
|
|
15
|
+
"county",
|
|
16
|
+
"customerId",
|
|
17
|
+
"deviceAccountId",
|
|
18
|
+
"deviceAddress",
|
|
19
|
+
"deviceOwnerCustomerId",
|
|
20
|
+
"given_name",
|
|
21
|
+
"name",
|
|
22
|
+
"password",
|
|
23
|
+
"postalCode",
|
|
24
|
+
"searchCustomerId",
|
|
25
|
+
"state",
|
|
26
|
+
"street",
|
|
27
|
+
"user_id",
|
|
28
|
+
}
|
|
7
29
|
|
|
8
30
|
|
|
9
31
|
def obfuscate_email(email: str) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aioamazondevices
|
|
3
|
-
Version:
|
|
3
|
+
Version: 9.0.2
|
|
4
4
|
Summary: Python library to control Amazon devices
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,6 +17,7 @@ Requires-Dist: beautifulsoup4
|
|
|
17
17
|
Requires-Dist: colorlog
|
|
18
18
|
Requires-Dist: langcodes
|
|
19
19
|
Requires-Dist: orjson (>=3.10,<4)
|
|
20
|
+
Requires-Dist: python-dateutil
|
|
20
21
|
Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
|
|
21
22
|
Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
|
|
22
23
|
Project-URL: Homepage, https://github.com/chemelli74/aioamazondevices
|
|
@@ -80,7 +81,6 @@ The script accept command line arguments or a library_test.json config file:
|
|
|
80
81
|
"single_device_name": "Echo Dot Livingroom",
|
|
81
82
|
"cluster_device_name": "Everywhere",
|
|
82
83
|
"login_data_file": "out/login_data.json",
|
|
83
|
-
"save_raw_data": true,
|
|
84
84
|
"test": true
|
|
85
85
|
}
|
|
86
86
|
```
|
|
@@ -134,12 +134,28 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
134
134
|
<sub><b>Ivan F. Martinez</b></sub>
|
|
135
135
|
</a>
|
|
136
136
|
</td>
|
|
137
|
+
<td align="center">
|
|
138
|
+
<a href="https://github.com/eyadkobatte">
|
|
139
|
+
<img src="https://avatars.githubusercontent.com/u/16541074?v=4" width="100;" alt="eyadkobatte"/>
|
|
140
|
+
<br />
|
|
141
|
+
<sub><b>Eyad Kobatte</b></sub>
|
|
142
|
+
</a>
|
|
143
|
+
</td>
|
|
137
144
|
<td align="center">
|
|
138
145
|
<a href="https://github.com/AzonInc">
|
|
139
146
|
<img src="https://avatars.githubusercontent.com/u/11911587?v=4" width="100;" alt="AzonInc"/>
|
|
140
147
|
<br />
|
|
141
148
|
<sub><b>Flo</b></sub>
|
|
142
149
|
</a>
|
|
150
|
+
</td>
|
|
151
|
+
</tr>
|
|
152
|
+
<tr>
|
|
153
|
+
<td align="center">
|
|
154
|
+
<a href="https://github.com/francescolf">
|
|
155
|
+
<img src="https://avatars.githubusercontent.com/u/14892143?v=4" width="100;" alt="francescolf"/>
|
|
156
|
+
<br />
|
|
157
|
+
<sub><b>Francesco Lo Faro</b></sub>
|
|
158
|
+
</a>
|
|
143
159
|
</td>
|
|
144
160
|
<td align="center">
|
|
145
161
|
<a href="https://github.com/lchavezcuu">
|
|
@@ -148,8 +164,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
148
164
|
<sub><b>Luis Chavez</b></sub>
|
|
149
165
|
</a>
|
|
150
166
|
</td>
|
|
151
|
-
</tr>
|
|
152
|
-
<tr>
|
|
153
167
|
<td align="center">
|
|
154
168
|
<a href="https://github.com/maxmati">
|
|
155
169
|
<img src="https://avatars.githubusercontent.com/u/509560?v=4" width="100;" alt="maxmati"/>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
aioamazondevices/__init__.py,sha256=-N24hqmuP5X5yZml6w5ASarPp5QOcX9uWEHMHmbHpVY,276
|
|
2
|
+
aioamazondevices/api.py,sha256=Jwl6yx6gLjfPto6TNuP_e9rkl6qskzRPzDuGmV0kOAc,32729
|
|
3
|
+
aioamazondevices/const/__init__.py,sha256=xQt8Smq2Ojjo30KKdev_gxDkcpY9PJlArTUXKel8oqs,38
|
|
4
|
+
aioamazondevices/const/devices.py,sha256=yy4bKdvsy4HUIMVlCe-1SJboZqH060nyhcf_ZuES23Y,11056
|
|
5
|
+
aioamazondevices/const/http.py,sha256=PMgfVl1K2vr_W3jkbD719x03aYte5mTObcPhRBayh7U,1151
|
|
6
|
+
aioamazondevices/const/metadata.py,sha256=KbH184fSBZ5AvZAjas92qiAEwiSwWz4xSssznWhOWbI,1066
|
|
7
|
+
aioamazondevices/const/queries.py,sha256=weCYmUJedNyx1P8z_tG_6cHGMjICUVo6KOckkl4P_-w,1900
|
|
8
|
+
aioamazondevices/const/schedules.py,sha256=GohhoXSGxXEq_fEycSBsjaDJdIxuMtvspLVqaCmJjOU,1378
|
|
9
|
+
aioamazondevices/const/sounds.py,sha256=hdrXYKEuSOCw7fDdylvqLcDqD5poHzhgXEnUwq_TwDE,1923
|
|
10
|
+
aioamazondevices/exceptions.py,sha256=gRYrxNAJnrV6uRuMx5e76VMvtNKyceXd09q84pDBBrI,638
|
|
11
|
+
aioamazondevices/http_wrapper.py,sha256=0gTjxXiqgK-lFSN5KW2PyxuQipp8JTfRPLYUsNnyp3E,11828
|
|
12
|
+
aioamazondevices/login.py,sha256=Imkh78JsOiQEtTYXdU1yPC0CSh0nRGpxwLOQzCwfBiA,17247
|
|
13
|
+
aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
aioamazondevices/structures.py,sha256=cGDUSsC9gNcA_rvAZhUwM5gCQ5U2nLl9HwMI4Mhnmyc,1413
|
|
15
|
+
aioamazondevices/utils.py,sha256=V6b5_CNJ5LtVBl9KSitr14nNle4mNDkZVojGhKfy60A,2373
|
|
16
|
+
aioamazondevices-9.0.2.dist-info/METADATA,sha256=pSXcilz1pm-goAp1GJvt5rLyDfXyT5qHLM00OmHmA00,8307
|
|
17
|
+
aioamazondevices-9.0.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
18
|
+
aioamazondevices-9.0.2.dist-info/licenses/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
|
|
19
|
+
aioamazondevices-9.0.2.dist-info/RECORD,,
|