aioamazondevices 6.5.1__py3-none-any.whl → 11.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 +150 -1213
- aioamazondevices/const/__init__.py +1 -0
- aioamazondevices/{const.py → const/devices.py} +54 -161
- aioamazondevices/const/http.py +37 -0
- aioamazondevices/const/metadata.py +46 -0
- aioamazondevices/{query.py → const/queries.py} +1 -1
- aioamazondevices/const/schedules.py +61 -0
- aioamazondevices/{sounds.py → const/sounds.py} +2 -1
- aioamazondevices/http_wrapper.py +422 -0
- aioamazondevices/implementation/__init__.py +1 -0
- aioamazondevices/implementation/dnd.py +56 -0
- aioamazondevices/implementation/notification.py +224 -0
- aioamazondevices/implementation/sequence.py +159 -0
- aioamazondevices/login.py +439 -0
- aioamazondevices/structures.py +65 -0
- aioamazondevices/utils.py +23 -1
- {aioamazondevices-6.5.1.dist-info → aioamazondevices-11.0.2.dist-info}/METADATA +17 -4
- aioamazondevices-11.0.2.dist-info/RECORD +23 -0
- aioamazondevices-6.5.1.dist-info/RECORD +0 -12
- {aioamazondevices-6.5.1.dist-info → aioamazondevices-11.0.2.dist-info}/WHEEL +0 -0
- {aioamazondevices-6.5.1.dist-info → aioamazondevices-11.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""aioamazondevices HTTP wrapper module."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import secrets
|
|
6
|
+
from collections.abc import Callable, Coroutine
|
|
7
|
+
from http import HTTPMethod, HTTPStatus
|
|
8
|
+
from http.cookies import Morsel
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
import orjson
|
|
12
|
+
from aiohttp import (
|
|
13
|
+
ClientConnectorError,
|
|
14
|
+
ClientResponse,
|
|
15
|
+
ClientSession,
|
|
16
|
+
ContentTypeError,
|
|
17
|
+
)
|
|
18
|
+
from bs4 import BeautifulSoup
|
|
19
|
+
from langcodes import Language, standardize_tag
|
|
20
|
+
from yarl import URL
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .const.http import (
|
|
24
|
+
AMAZON_APP_BUNDLE_ID,
|
|
25
|
+
AMAZON_APP_ID,
|
|
26
|
+
AMAZON_APP_NAME,
|
|
27
|
+
AMAZON_APP_VERSION,
|
|
28
|
+
AMAZON_CLIENT_OS,
|
|
29
|
+
AMAZON_DEVICE_SOFTWARE_VERSION,
|
|
30
|
+
ARRAY_WRAPPER,
|
|
31
|
+
CSRF_COOKIE,
|
|
32
|
+
DEFAULT_HEADERS,
|
|
33
|
+
HTTP_ERROR_199,
|
|
34
|
+
HTTP_ERROR_299,
|
|
35
|
+
REFRESH_ACCESS_TOKEN,
|
|
36
|
+
REFRESH_AUTH_COOKIES,
|
|
37
|
+
REQUEST_AGENT,
|
|
38
|
+
URI_SIGNIN,
|
|
39
|
+
)
|
|
40
|
+
from .exceptions import (
|
|
41
|
+
CannotAuthenticate,
|
|
42
|
+
CannotConnect,
|
|
43
|
+
CannotRetrieveData,
|
|
44
|
+
)
|
|
45
|
+
from .utils import _LOGGER, scrub_fields
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AmazonSessionStateData:
|
|
49
|
+
"""Amazon session state data class."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
domain: str,
|
|
54
|
+
login_email: str,
|
|
55
|
+
login_password: str,
|
|
56
|
+
login_data: dict[str, Any] | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Init state class."""
|
|
59
|
+
self._login_email: str = login_email
|
|
60
|
+
self._login_password: str = login_password
|
|
61
|
+
self._login_stored_data: dict[str, Any] = login_data or {}
|
|
62
|
+
self.country_specific_data(domain)
|
|
63
|
+
self._account_customer_id: str | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def country_code(self) -> str:
|
|
67
|
+
"""Return country code."""
|
|
68
|
+
return self._country_code
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def domain(self) -> str:
|
|
72
|
+
"""Return domain."""
|
|
73
|
+
return self._domain
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def language(self) -> str:
|
|
77
|
+
"""Return language."""
|
|
78
|
+
return self._language
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def login_email(self) -> str:
|
|
82
|
+
"""Return login email."""
|
|
83
|
+
return self._login_email
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def login_password(self) -> str:
|
|
87
|
+
"""Return login password."""
|
|
88
|
+
return self._login_password
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def login_stored_data(self) -> dict[str, Any]:
|
|
92
|
+
"""Return login stored data."""
|
|
93
|
+
return self._login_stored_data
|
|
94
|
+
|
|
95
|
+
@login_stored_data.setter
|
|
96
|
+
def login_stored_data(self, data: dict[str, Any]) -> None:
|
|
97
|
+
"""Set login stored data."""
|
|
98
|
+
self._login_stored_data = data
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def account_customer_id(self) -> str | None:
|
|
102
|
+
"""Return account customer id."""
|
|
103
|
+
return self._account_customer_id
|
|
104
|
+
|
|
105
|
+
@account_customer_id.setter
|
|
106
|
+
def account_customer_id(self, customer_id: str | None) -> None:
|
|
107
|
+
"""Set account customer id."""
|
|
108
|
+
self._account_customer_id = customer_id
|
|
109
|
+
|
|
110
|
+
def country_specific_data(self, domain: str) -> None:
|
|
111
|
+
"""Set country specific data."""
|
|
112
|
+
# Force lower case
|
|
113
|
+
domain = domain.replace("https://www.amazon.", "").lower()
|
|
114
|
+
country_code = domain.split(".")[-1] if domain != "com" else "us"
|
|
115
|
+
|
|
116
|
+
lang_object = Language.make(territory=country_code.upper())
|
|
117
|
+
lang_maximized = lang_object.maximize()
|
|
118
|
+
|
|
119
|
+
self._country_code: str = country_code
|
|
120
|
+
self._domain: str = domain
|
|
121
|
+
language = f"{lang_maximized.language}-{lang_maximized.territory}"
|
|
122
|
+
self._language: str = standardize_tag(language)
|
|
123
|
+
|
|
124
|
+
_LOGGER.debug(
|
|
125
|
+
"Initialize country <%s>: domain <amazon.%s>, language <%s>",
|
|
126
|
+
country_code.upper(),
|
|
127
|
+
self._domain,
|
|
128
|
+
self._language,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AmazonHttpWrapper:
|
|
133
|
+
"""Amazon HTTP wrapper class."""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
client_session: ClientSession,
|
|
138
|
+
session_state_data: AmazonSessionStateData,
|
|
139
|
+
save_to_file: Callable[[str | dict, str, str], Coroutine[Any, Any, None]]
|
|
140
|
+
| None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Initialize HTTP wrapper."""
|
|
143
|
+
self._session = client_session
|
|
144
|
+
self._session_state_data: AmazonSessionStateData = session_state_data
|
|
145
|
+
self._save_to_file = save_to_file
|
|
146
|
+
|
|
147
|
+
self._csrf_cookie: str | None = None
|
|
148
|
+
self._cookies: dict[str, str] = self._build_init_cookies()
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def cookies(self) -> dict[str, str]:
|
|
152
|
+
"""Return the current cookies."""
|
|
153
|
+
return self._cookies
|
|
154
|
+
|
|
155
|
+
def _load_website_cookies(self, language: str) -> dict[str, str]:
|
|
156
|
+
"""Get website cookies, if available."""
|
|
157
|
+
if not self._session_state_data.login_stored_data:
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
website_cookies: dict[str, Any] = self._session_state_data.login_stored_data[
|
|
161
|
+
"website_cookies"
|
|
162
|
+
]
|
|
163
|
+
website_cookies.update(
|
|
164
|
+
{
|
|
165
|
+
"session-token": self._session_state_data.login_stored_data[
|
|
166
|
+
"store_authentication_cookie"
|
|
167
|
+
]["cookie"]
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
website_cookies.update({"lc-acbit": language})
|
|
171
|
+
|
|
172
|
+
return website_cookies
|
|
173
|
+
|
|
174
|
+
def _build_init_cookies(self) -> dict[str, str]:
|
|
175
|
+
"""Build initial cookies to prevent captcha in most cases."""
|
|
176
|
+
token_bytes = secrets.token_bytes(313)
|
|
177
|
+
frc = base64.b64encode(token_bytes).decode("ascii").rstrip("=")
|
|
178
|
+
|
|
179
|
+
map_md_dict = {
|
|
180
|
+
"device_user_dictionary": [],
|
|
181
|
+
"device_registration_data": {
|
|
182
|
+
"software_version": AMAZON_DEVICE_SOFTWARE_VERSION,
|
|
183
|
+
},
|
|
184
|
+
"app_identifier": {
|
|
185
|
+
"app_version": AMAZON_APP_VERSION,
|
|
186
|
+
"bundle_id": AMAZON_APP_BUNDLE_ID,
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
map_md_str = orjson.dumps(map_md_dict).decode("utf-8")
|
|
190
|
+
map_md = base64.b64encode(map_md_str.encode()).decode().rstrip("=")
|
|
191
|
+
|
|
192
|
+
return {"amzn-app-id": AMAZON_APP_ID, "frc": frc, "map-md": map_md}
|
|
193
|
+
|
|
194
|
+
async def _ignore_ap_signin_error(self, response: ClientResponse) -> bool:
|
|
195
|
+
"""Return true if error is due to signin endpoint."""
|
|
196
|
+
# Endpoint URI_SIGNIN replies with error 404
|
|
197
|
+
# but reports the needed parameters anyway
|
|
198
|
+
if history := response.history:
|
|
199
|
+
return (
|
|
200
|
+
response.status == HTTPStatus.NOT_FOUND
|
|
201
|
+
and URI_SIGNIN in history[0].request_info.url.path
|
|
202
|
+
)
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
async def set_session_state_data(
|
|
206
|
+
self, session_state_data: AmazonSessionStateData
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Set the current session state data."""
|
|
209
|
+
self._session_state_data = session_state_data
|
|
210
|
+
|
|
211
|
+
async def http_phrase_error(self, error: int) -> str:
|
|
212
|
+
"""Convert numeric error in human phrase."""
|
|
213
|
+
if error == HTTP_ERROR_199:
|
|
214
|
+
return "Miscellaneous Warning"
|
|
215
|
+
|
|
216
|
+
if error == HTTP_ERROR_299:
|
|
217
|
+
return "Miscellaneous Persistent Warning"
|
|
218
|
+
|
|
219
|
+
return HTTPStatus(error).phrase
|
|
220
|
+
|
|
221
|
+
async def refresh_data(self, data_type: str) -> tuple[bool, dict]:
|
|
222
|
+
"""Refresh data."""
|
|
223
|
+
if not self._session_state_data.login_stored_data:
|
|
224
|
+
_LOGGER.debug("No login data available, cannot refresh")
|
|
225
|
+
return False, {}
|
|
226
|
+
|
|
227
|
+
data = {
|
|
228
|
+
"app_name": AMAZON_APP_NAME,
|
|
229
|
+
"app_version": AMAZON_APP_VERSION,
|
|
230
|
+
"di.sdk.version": "6.12.4",
|
|
231
|
+
"source_token": self._session_state_data.login_stored_data["refresh_token"],
|
|
232
|
+
"package_name": AMAZON_APP_BUNDLE_ID,
|
|
233
|
+
"di.hw.version": "iPhone",
|
|
234
|
+
"platform": "iOS",
|
|
235
|
+
"requested_token_type": data_type,
|
|
236
|
+
"source_token_type": "refresh_token",
|
|
237
|
+
"di.os.name": "iOS",
|
|
238
|
+
"di.os.version": AMAZON_CLIENT_OS,
|
|
239
|
+
"current_version": "6.12.4",
|
|
240
|
+
"previous_version": "6.12.4",
|
|
241
|
+
"domain": f"www.amazon.{self._session_state_data.domain}",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_, raw_resp = await self.session_request(
|
|
245
|
+
method=HTTPMethod.POST,
|
|
246
|
+
url="https://api.amazon.com/auth/token",
|
|
247
|
+
input_data=data,
|
|
248
|
+
json_data=False,
|
|
249
|
+
)
|
|
250
|
+
_LOGGER.debug(
|
|
251
|
+
"Refresh data response %s with payload %s",
|
|
252
|
+
raw_resp.status,
|
|
253
|
+
orjson.dumps(data),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if raw_resp.status != HTTPStatus.OK:
|
|
257
|
+
_LOGGER.debug("Failed to refresh data")
|
|
258
|
+
return False, {}
|
|
259
|
+
|
|
260
|
+
json_response = await self.response_to_json(raw_resp, data_type)
|
|
261
|
+
|
|
262
|
+
if data_type == REFRESH_ACCESS_TOKEN and (
|
|
263
|
+
new_token := json_response.get(REFRESH_ACCESS_TOKEN)
|
|
264
|
+
):
|
|
265
|
+
self._session_state_data.login_stored_data[REFRESH_ACCESS_TOKEN] = new_token
|
|
266
|
+
return True, json_response
|
|
267
|
+
|
|
268
|
+
if data_type == REFRESH_AUTH_COOKIES:
|
|
269
|
+
return True, json_response
|
|
270
|
+
|
|
271
|
+
_LOGGER.debug("Unexpected refresh data response")
|
|
272
|
+
return False, {}
|
|
273
|
+
|
|
274
|
+
async def session_request(
|
|
275
|
+
self,
|
|
276
|
+
method: str,
|
|
277
|
+
url: str,
|
|
278
|
+
input_data: dict[str, Any] | list[dict[str, Any]] | None = None,
|
|
279
|
+
json_data: bool = False,
|
|
280
|
+
extended_headers: dict[str, str] | None = None,
|
|
281
|
+
) -> tuple[BeautifulSoup, ClientResponse]:
|
|
282
|
+
"""Return request response context data."""
|
|
283
|
+
_LOGGER.debug(
|
|
284
|
+
"%s request: %s with payload %s [json=%s]",
|
|
285
|
+
method,
|
|
286
|
+
url,
|
|
287
|
+
scrub_fields(input_data) if input_data else None,
|
|
288
|
+
json_data,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
headers = DEFAULT_HEADERS.copy()
|
|
292
|
+
headers.update({"User-Agent": REQUEST_AGENT["Browser"]})
|
|
293
|
+
headers.update({"Accept-Language": self._session_state_data.language})
|
|
294
|
+
headers.update({"x-amzn-client": "github.com/chemelli74/aioamazondevices"})
|
|
295
|
+
headers.update({"x-amzn-build-version": __version__})
|
|
296
|
+
|
|
297
|
+
if extended_headers:
|
|
298
|
+
_LOGGER.debug("Adding to headers: %s", extended_headers)
|
|
299
|
+
headers.update(extended_headers)
|
|
300
|
+
|
|
301
|
+
if self._csrf_cookie:
|
|
302
|
+
csrf = {CSRF_COOKIE: self._csrf_cookie}
|
|
303
|
+
_LOGGER.debug("Adding to headers: %s", csrf)
|
|
304
|
+
headers.update(csrf)
|
|
305
|
+
|
|
306
|
+
if json_data:
|
|
307
|
+
json_header = {"Content-Type": "application/json; charset=utf-8"}
|
|
308
|
+
_LOGGER.debug("Adding to headers: %s", json_header)
|
|
309
|
+
headers.update(json_header)
|
|
310
|
+
|
|
311
|
+
_cookies = (
|
|
312
|
+
self._load_website_cookies(self._session_state_data.language)
|
|
313
|
+
if self._session_state_data.login_stored_data
|
|
314
|
+
else self._cookies
|
|
315
|
+
)
|
|
316
|
+
self._session.cookie_jar.update_cookies(
|
|
317
|
+
_cookies, URL(f"amazon.{self._session_state_data.domain}")
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
resp: ClientResponse | None = None
|
|
321
|
+
for delay in [0, 1, 2, 5, 8, 12, 21]:
|
|
322
|
+
if delay:
|
|
323
|
+
_LOGGER.info(
|
|
324
|
+
"Sleeping for %s seconds before retrying API call to %s", delay, url
|
|
325
|
+
)
|
|
326
|
+
await asyncio.sleep(delay)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
resp = await self._session.request(
|
|
330
|
+
method,
|
|
331
|
+
URL(url, encoded=True),
|
|
332
|
+
data=input_data if not json_data else orjson.dumps(input_data),
|
|
333
|
+
headers=headers,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
except (TimeoutError, ClientConnectorError) as exc:
|
|
337
|
+
_LOGGER.warning("Connection error to %s: %s", url, repr(exc))
|
|
338
|
+
raise CannotConnect(f"Connection error during {method}") from exc
|
|
339
|
+
|
|
340
|
+
# Retry with a delay only for specific HTTP status
|
|
341
|
+
# that can benefits of a back-off
|
|
342
|
+
if resp.status not in [
|
|
343
|
+
HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
344
|
+
HTTPStatus.SERVICE_UNAVAILABLE,
|
|
345
|
+
HTTPStatus.TOO_MANY_REQUESTS,
|
|
346
|
+
]:
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
if resp is None:
|
|
350
|
+
_LOGGER.error("No response received from %s", url)
|
|
351
|
+
raise CannotConnect(f"No response received from {url}")
|
|
352
|
+
|
|
353
|
+
if csrf := resp.cookies.get(CSRF_COOKIE, Morsel()).value:
|
|
354
|
+
self._csrf_cookie = csrf
|
|
355
|
+
_LOGGER.debug("CSRF cookie value: <%s> [%s]", self._csrf_cookie, url)
|
|
356
|
+
|
|
357
|
+
content_type: str = resp.headers.get("Content-Type", "")
|
|
358
|
+
_LOGGER.debug(
|
|
359
|
+
"Response for url %s :\nstatus : %s \
|
|
360
|
+
\ncontent type: %s ",
|
|
361
|
+
url,
|
|
362
|
+
resp.status,
|
|
363
|
+
content_type,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if resp.status != HTTPStatus.OK:
|
|
367
|
+
if resp.status in [
|
|
368
|
+
HTTPStatus.FORBIDDEN,
|
|
369
|
+
HTTPStatus.PROXY_AUTHENTICATION_REQUIRED,
|
|
370
|
+
HTTPStatus.UNAUTHORIZED,
|
|
371
|
+
]:
|
|
372
|
+
raise CannotAuthenticate(await self.http_phrase_error(resp.status))
|
|
373
|
+
if not await self._ignore_ap_signin_error(resp):
|
|
374
|
+
_LOGGER.debug("Error response content: %s", await resp.text())
|
|
375
|
+
raise CannotRetrieveData(
|
|
376
|
+
f"Request failed: {await self.http_phrase_error(resp.status)}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
raw_content = await resp.read()
|
|
380
|
+
|
|
381
|
+
if self._save_to_file:
|
|
382
|
+
await self._save_to_file(
|
|
383
|
+
raw_content.decode("utf-8"),
|
|
384
|
+
url,
|
|
385
|
+
content_type,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return BeautifulSoup(raw_content or "", "html.parser"), resp
|
|
389
|
+
|
|
390
|
+
async def response_to_json(
|
|
391
|
+
self, raw_resp: ClientResponse, description: str | None = None
|
|
392
|
+
) -> dict[str, Any]:
|
|
393
|
+
"""Convert response to JSON, if possible."""
|
|
394
|
+
try:
|
|
395
|
+
data = await raw_resp.json(loads=orjson.loads)
|
|
396
|
+
if not data:
|
|
397
|
+
_LOGGER.warning("Empty JSON data received")
|
|
398
|
+
data = {}
|
|
399
|
+
if isinstance(data, list):
|
|
400
|
+
# if anonymous array is returned wrap it inside
|
|
401
|
+
# generated key to convert list to dict
|
|
402
|
+
data = {ARRAY_WRAPPER: data}
|
|
403
|
+
if description:
|
|
404
|
+
_LOGGER.debug("JSON '%s' data: %s", description, scrub_fields(data))
|
|
405
|
+
return cast("dict[str, Any]", data)
|
|
406
|
+
except ContentTypeError as exc:
|
|
407
|
+
raise ValueError("Response not in JSON format") from exc
|
|
408
|
+
except orjson.JSONDecodeError as exc:
|
|
409
|
+
raise ValueError("Response with corrupted JSON format") from exc
|
|
410
|
+
|
|
411
|
+
async def clear_cookies(self) -> None:
|
|
412
|
+
"""Clear session cookies."""
|
|
413
|
+
self._session.cookie_jar.clear()
|
|
414
|
+
await self.clear_csrf_cookie()
|
|
415
|
+
|
|
416
|
+
async def clear_csrf_cookie(self) -> None:
|
|
417
|
+
"""Clear CSRF cookie."""
|
|
418
|
+
self._csrf_cookie = None
|
|
419
|
+
|
|
420
|
+
async def set_cookies(self, cookies: dict[str, str], domain_url: URL) -> None:
|
|
421
|
+
"""Set session cookies."""
|
|
422
|
+
self._session.cookie_jar.update_cookies(cookies, domain_url)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""aioamazondevices implementation package."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Module to handle Alexa do not disturb setting."""
|
|
2
|
+
|
|
3
|
+
from http import HTTPMethod
|
|
4
|
+
|
|
5
|
+
from aioamazondevices.const.http import URI_DND_STATUS_ALL, URI_DND_STATUS_DEVICE
|
|
6
|
+
from aioamazondevices.http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
|
|
7
|
+
from aioamazondevices.structures import AmazonDevice, AmazonDeviceSensor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AmazonDnDHandler:
|
|
11
|
+
"""Class to handle Alexa Do Not Disturb functionality."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
http_wrapper: AmazonHttpWrapper,
|
|
16
|
+
session_state_data: AmazonSessionStateData,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Initialize AmazonDnDHandler class."""
|
|
19
|
+
self._domain = session_state_data.domain
|
|
20
|
+
self._http_wrapper = http_wrapper
|
|
21
|
+
|
|
22
|
+
async def get_do_not_disturb_status(self) -> dict[str, AmazonDeviceSensor]:
|
|
23
|
+
"""Get do_not_disturb status for all devices."""
|
|
24
|
+
dnd_status: dict[str, AmazonDeviceSensor] = {}
|
|
25
|
+
_, raw_resp = await self._http_wrapper.session_request(
|
|
26
|
+
method=HTTPMethod.GET,
|
|
27
|
+
url=f"https://alexa.amazon.{self._domain}{URI_DND_STATUS_ALL}",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
dnd_data = await self._http_wrapper.response_to_json(raw_resp, "dnd")
|
|
31
|
+
|
|
32
|
+
for dnd in dnd_data.get("doNotDisturbDeviceStatusList", {}):
|
|
33
|
+
dnd_status[dnd.get("deviceSerialNumber")] = AmazonDeviceSensor(
|
|
34
|
+
name="dnd",
|
|
35
|
+
value=dnd.get("enabled"),
|
|
36
|
+
error=False,
|
|
37
|
+
error_type=None,
|
|
38
|
+
error_msg=None,
|
|
39
|
+
scale=None,
|
|
40
|
+
)
|
|
41
|
+
return dnd_status
|
|
42
|
+
|
|
43
|
+
async def set_do_not_disturb(self, device: AmazonDevice, enable: bool) -> None:
|
|
44
|
+
"""Set do_not_disturb flag."""
|
|
45
|
+
payload = {
|
|
46
|
+
"deviceSerialNumber": device.serial_number,
|
|
47
|
+
"deviceType": device.device_type,
|
|
48
|
+
"enabled": enable,
|
|
49
|
+
}
|
|
50
|
+
url = f"https://alexa.amazon.{self._domain}{URI_DND_STATUS_DEVICE}"
|
|
51
|
+
await self._http_wrapper.session_request(
|
|
52
|
+
method=HTTPMethod.PUT,
|
|
53
|
+
url=url,
|
|
54
|
+
input_data=payload,
|
|
55
|
+
json_data=True,
|
|
56
|
+
)
|