aioamazondevices 10.0.0__tar.gz → 11.0.2__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.
Files changed (23) hide show
  1. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/PKG-INFO +1 -1
  2. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/pyproject.toml +1 -1
  3. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/__init__.py +1 -1
  4. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/api.py +13 -37
  5. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/devices.py +9 -0
  6. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/http.py +3 -2
  7. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/http_wrapper.py +65 -3
  8. aioamazondevices-11.0.2/src/aioamazondevices/implementation/dnd.py +56 -0
  9. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/implementation/notification.py +2 -1
  10. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/login.py +1 -57
  11. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/LICENSE +0 -0
  12. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/README.md +0 -0
  13. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/__init__.py +0 -0
  14. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/metadata.py +0 -0
  15. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/queries.py +0 -0
  16. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/schedules.py +0 -0
  17. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/const/sounds.py +0 -0
  18. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/exceptions.py +0 -0
  19. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/implementation/__init__.py +0 -0
  20. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/implementation/sequence.py +0 -0
  21. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/py.typed +0 -0
  22. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/structures.py +0 -0
  23. {aioamazondevices-10.0.0 → aioamazondevices-11.0.2}/src/aioamazondevices/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioamazondevices
3
- Version: 10.0.0
3
+ Version: 11.0.2
4
4
  Summary: Python library to control Amazon devices
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "10.0.0"
3
+ version = "11.0.2"
4
4
  requires-python = ">=3.12"
5
5
  description = "Python library to control Amazon devices"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "10.0.0"
3
+ __version__ = "11.0.2"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -16,8 +16,8 @@ from .const.devices import (
16
16
  from .const.http import (
17
17
  ARRAY_WRAPPER,
18
18
  DEFAULT_SITE,
19
+ REQUEST_AGENT,
19
20
  URI_DEVICES,
20
- URI_DND,
21
21
  URI_NEXUS_GRAPHQL,
22
22
  )
23
23
  from .const.metadata import SENSORS
@@ -31,6 +31,7 @@ from .exceptions import (
31
31
  CannotRetrieveData,
32
32
  )
33
33
  from .http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
34
+ from .implementation.dnd import AmazonDnDHandler
34
35
  from .implementation.notification import AmazonNotificationHandler
35
36
  from .implementation.sequence import AmazonSequenceHandler
36
37
  from .login import AmazonLogin
@@ -87,6 +88,10 @@ class AmazonEchoApi:
87
88
  session_state_data=self._session_state_data,
88
89
  )
89
90
 
91
+ self._dnd_handler = AmazonDnDHandler(
92
+ http_wrapper=self._http_wrapper, session_state_data=self._session_state_data
93
+ )
94
+
90
95
  self._final_devices: dict[str, AmazonDevice] = {}
91
96
  self._endpoints: dict[str, str] = {} # endpoint ID to serial number map
92
97
 
@@ -127,6 +132,7 @@ class AmazonEchoApi:
127
132
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
128
133
  input_data=payload,
129
134
  json_data=True,
135
+ extended_headers={"User-Agent": REQUEST_AGENT["Amazon"]},
130
136
  )
131
137
 
132
138
  sensors_state = await self._http_wrapper.response_to_json(raw_resp, "sensors")
@@ -236,6 +242,7 @@ class AmazonEchoApi:
236
242
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
237
243
  input_data=payload,
238
244
  json_data=True,
245
+ extended_headers={"User-Agent": REQUEST_AGENT["Amazon"]},
239
246
  )
240
247
 
241
248
  endpoint_data = await self._http_wrapper.response_to_json(raw_resp, "endpoint")
@@ -293,7 +300,7 @@ class AmazonEchoApi:
293
300
 
294
301
  async def _get_sensor_data(self) -> None:
295
302
  devices_sensors = await self._get_sensors_states()
296
- dnd_sensors = await self._get_dnd_status()
303
+ dnd_sensors = await self._dnd_handler.get_do_not_disturb_status()
297
304
  notifications = await self._notification_handler.get_notifications()
298
305
  for device in self._final_devices.values():
299
306
  # Update sensors
@@ -489,41 +496,6 @@ class AmazonEchoApi:
489
496
  """Call Info skill. See ALEXA_INFO_SKILLS . const."""
490
497
  await self._sequence_handler.send_message(device, info_skill_name, "")
491
498
 
492
- async def set_do_not_disturb(self, device: AmazonDevice, state: bool) -> None:
493
- """Set do_not_disturb flag."""
494
- payload = {
495
- "deviceSerialNumber": device.serial_number,
496
- "deviceType": device.device_type,
497
- "enabled": state,
498
- }
499
- url = f"https://alexa.amazon.{self._session_state_data.domain}/api/dnd/status"
500
- await self._http_wrapper.session_request(
501
- method="PUT",
502
- url=url,
503
- input_data=payload,
504
- json_data=True,
505
- )
506
-
507
- async def _get_dnd_status(self) -> dict[str, AmazonDeviceSensor]:
508
- dnd_status: dict[str, AmazonDeviceSensor] = {}
509
- _, raw_resp = await self._http_wrapper.session_request(
510
- method=HTTPMethod.GET,
511
- url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_DND}",
512
- )
513
-
514
- dnd_data = await self._http_wrapper.response_to_json(raw_resp, "dnd")
515
-
516
- for dnd in dnd_data.get("doNotDisturbDeviceStatusList", {}):
517
- dnd_status[dnd.get("deviceSerialNumber")] = AmazonDeviceSensor(
518
- name="dnd",
519
- value=dnd.get("enabled"),
520
- error=False,
521
- error_type=None,
522
- error_msg=None,
523
- scale=None,
524
- )
525
- return dnd_status
526
-
527
499
  async def _format_human_error(self, sensors_state: dict) -> bool:
528
500
  """Format human readable error from malformed data."""
529
501
  if sensors_state.get(ARRAY_WRAPPER):
@@ -538,3 +510,7 @@ class AmazonEchoApi:
538
510
  path = error[0].get("path", "Unknown path")
539
511
  _LOGGER.error("Error retrieving devices state: %s for path %s", msg, path)
540
512
  return True
513
+
514
+ async def set_do_not_disturb(self, device: AmazonDevice, enable: bool) -> None:
515
+ """Set Do Not Disturb status for a device."""
516
+ await self._dnd_handler.set_do_not_disturb(device, enable)
@@ -417,4 +417,13 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
417
417
  "model": "Ford SYNC 4",
418
418
  "hw_version": None,
419
419
  },
420
+ "A1J16TEDOYCZTN": {
421
+ "model": "Fire Tablet 7",
422
+ "hw_version": "Gen7",
423
+ },
424
+ "A18TCD9FP10WJ9": {
425
+ "manufacturer": "Netgear",
426
+ "model": "Orbi Voice (RBS40V)",
427
+ "hw_version": None,
428
+ },
420
429
  }
@@ -30,7 +30,8 @@ REFRESH_ACCESS_TOKEN = "access_token" # noqa: S105
30
30
  REFRESH_AUTH_COOKIES = "auth_cookies"
31
31
 
32
32
  URI_DEVICES = "/api/devices-v2/device"
33
- URI_DND = "/api/dnd/device-status-list"
33
+ URI_DND_STATUS_ALL = "/api/dnd/device-status-list"
34
+ URI_DND_STATUS_DEVICE = "/api/dnd/status"
35
+ URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
34
36
  URI_NOTIFICATIONS = "/api/notifications"
35
37
  URI_SIGNIN = "/ap/signin"
36
- URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
@@ -4,7 +4,7 @@ import asyncio
4
4
  import base64
5
5
  import secrets
6
6
  from collections.abc import Callable, Coroutine
7
- from http import HTTPStatus
7
+ from http import HTTPMethod, HTTPStatus
8
8
  from http.cookies import Morsel
9
9
  from typing import Any, cast
10
10
 
@@ -23,13 +23,17 @@ from . import __version__
23
23
  from .const.http import (
24
24
  AMAZON_APP_BUNDLE_ID,
25
25
  AMAZON_APP_ID,
26
+ AMAZON_APP_NAME,
26
27
  AMAZON_APP_VERSION,
28
+ AMAZON_CLIENT_OS,
27
29
  AMAZON_DEVICE_SOFTWARE_VERSION,
28
30
  ARRAY_WRAPPER,
29
31
  CSRF_COOKIE,
30
32
  DEFAULT_HEADERS,
31
33
  HTTP_ERROR_199,
32
34
  HTTP_ERROR_299,
35
+ REFRESH_ACCESS_TOKEN,
36
+ REFRESH_AUTH_COOKIES,
33
37
  REQUEST_AGENT,
34
38
  URI_SIGNIN,
35
39
  )
@@ -214,13 +218,66 @@ class AmazonHttpWrapper:
214
218
 
215
219
  return HTTPStatus(error).phrase
216
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
+
217
274
  async def session_request(
218
275
  self,
219
276
  method: str,
220
277
  url: str,
221
278
  input_data: dict[str, Any] | list[dict[str, Any]] | None = None,
222
279
  json_data: bool = False,
223
- agent: str = "Amazon",
280
+ extended_headers: dict[str, str] | None = None,
224
281
  ) -> tuple[BeautifulSoup, ClientResponse]:
225
282
  """Return request response context data."""
226
283
  _LOGGER.debug(
@@ -232,11 +289,15 @@ class AmazonHttpWrapper:
232
289
  )
233
290
 
234
291
  headers = DEFAULT_HEADERS.copy()
235
- headers.update({"User-Agent": REQUEST_AGENT[agent]})
292
+ headers.update({"User-Agent": REQUEST_AGENT["Browser"]})
236
293
  headers.update({"Accept-Language": self._session_state_data.language})
237
294
  headers.update({"x-amzn-client": "github.com/chemelli74/aioamazondevices"})
238
295
  headers.update({"x-amzn-build-version": __version__})
239
296
 
297
+ if extended_headers:
298
+ _LOGGER.debug("Adding to headers: %s", extended_headers)
299
+ headers.update(extended_headers)
300
+
240
301
  if self._csrf_cookie:
241
302
  csrf = {CSRF_COOKIE: self._csrf_cookie}
242
303
  _LOGGER.debug("Adding to headers: %s", csrf)
@@ -310,6 +371,7 @@ class AmazonHttpWrapper:
310
371
  ]:
311
372
  raise CannotAuthenticate(await self.http_phrase_error(resp.status))
312
373
  if not await self._ignore_ap_signin_error(resp):
374
+ _LOGGER.debug("Error response content: %s", await resp.text())
313
375
  raise CannotRetrieveData(
314
376
  f"Request failed: {await self.http_phrase_error(resp.status)}"
315
377
  )
@@ -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
+ )
@@ -8,7 +8,7 @@ from dateutil.parser import parse
8
8
  from dateutil.rrule import rrulestr
9
9
 
10
10
  from aioamazondevices.const.devices import DEVICE_TO_IGNORE
11
- from aioamazondevices.const.http import URI_NOTIFICATIONS
11
+ from aioamazondevices.const.http import REQUEST_AGENT, URI_NOTIFICATIONS
12
12
  from aioamazondevices.const.schedules import (
13
13
  COUNTRY_GROUPS,
14
14
  NOTIFICATION_ALARM,
@@ -45,6 +45,7 @@ class AmazonNotificationHandler:
45
45
  _, raw_resp = await self._http_wrapper.session_request(
46
46
  HTTPMethod.GET,
47
47
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NOTIFICATIONS}",
48
+ extended_headers={"User-Agent": REQUEST_AGENT["Browser"]},
48
49
  )
49
50
  except CannotRetrieveData:
50
51
  _LOGGER.warning(
@@ -10,20 +10,17 @@ from http import HTTPMethod, HTTPStatus
10
10
  from typing import Any, cast
11
11
  from urllib.parse import parse_qs, urlencode
12
12
 
13
- import orjson
14
13
  from bs4 import BeautifulSoup, Tag
15
14
  from multidict import MultiDictProxy
16
15
  from yarl import URL
17
16
 
18
17
  from .const.http import (
19
- AMAZON_APP_BUNDLE_ID,
20
18
  AMAZON_APP_NAME,
21
19
  AMAZON_APP_VERSION,
22
20
  AMAZON_CLIENT_OS,
23
21
  AMAZON_DEVICE_SOFTWARE_VERSION,
24
22
  AMAZON_DEVICE_TYPE,
25
23
  DEFAULT_SITE,
26
- REFRESH_ACCESS_TOKEN,
27
24
  REFRESH_AUTH_COOKIES,
28
25
  URI_DEVICES,
29
26
  URI_SIGNIN,
@@ -362,7 +359,7 @@ class AmazonLogin:
362
359
 
363
360
  async def _refresh_auth_cookies(self) -> None:
364
361
  """Refresh cookies after domain swap."""
365
- _, json_token_resp = await self._refresh_data(REFRESH_AUTH_COOKIES)
362
+ _, json_token_resp = await self._http_wrapper.refresh_data(REFRESH_AUTH_COOKIES)
366
363
 
367
364
  # Need to take cookies from response and create them as cookies
368
365
  website_cookies = self._session_state_data.login_stored_data[
@@ -394,59 +391,6 @@ class AmazonLogin:
394
391
  await self._http_wrapper.clear_csrf_cookie()
395
392
  await self._refresh_auth_cookies()
396
393
 
397
- async def _refresh_data(self, data_type: str) -> tuple[bool, dict]:
398
- """Refresh data."""
399
- if not self._session_state_data.login_stored_data:
400
- _LOGGER.debug("No login data available, cannot refresh")
401
- return False, {}
402
-
403
- data = {
404
- "app_name": AMAZON_APP_NAME,
405
- "app_version": AMAZON_APP_VERSION,
406
- "di.sdk.version": "6.12.4",
407
- "source_token": self._session_state_data.login_stored_data["refresh_token"],
408
- "package_name": AMAZON_APP_BUNDLE_ID,
409
- "di.hw.version": "iPhone",
410
- "platform": "iOS",
411
- "requested_token_type": data_type,
412
- "source_token_type": "refresh_token",
413
- "di.os.name": "iOS",
414
- "di.os.version": AMAZON_CLIENT_OS,
415
- "current_version": "6.12.4",
416
- "previous_version": "6.12.4",
417
- "domain": f"www.amazon.{self._session_state_data.domain}",
418
- }
419
-
420
- _, raw_resp = await self._http_wrapper.session_request(
421
- method=HTTPMethod.POST,
422
- url="https://api.amazon.com/auth/token",
423
- input_data=data,
424
- json_data=False,
425
- )
426
- _LOGGER.debug(
427
- "Refresh data response %s with payload %s",
428
- raw_resp.status,
429
- orjson.dumps(data),
430
- )
431
-
432
- if raw_resp.status != HTTPStatus.OK:
433
- _LOGGER.debug("Failed to refresh data")
434
- return False, {}
435
-
436
- json_response = await self._http_wrapper.response_to_json(raw_resp, data_type)
437
-
438
- if data_type == REFRESH_ACCESS_TOKEN and (
439
- new_token := json_response.get(REFRESH_ACCESS_TOKEN)
440
- ):
441
- self._session_state_data.login_stored_data[REFRESH_ACCESS_TOKEN] = new_token
442
- return True, json_response
443
-
444
- if data_type == REFRESH_AUTH_COOKIES:
445
- return True, json_response
446
-
447
- _LOGGER.debug("Unexpected refresh data response")
448
- return False, {}
449
-
450
394
  async def obtain_account_customer_id(self) -> None:
451
395
  """Find account customer id."""
452
396
  for retry_count in range(MAX_CUSTOMER_ACCOUNT_RETRIES):