aioamazondevices 3.1.14__tar.gz → 3.1.19__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,29 +1,24 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 3.1.14
3
+ Version: 3.1.19
4
4
  Summary: Python library to control Amazon devices
5
5
  License: Apache-2.0
6
6
  Author: Simone Chemelli
7
7
  Author-email: simone.chemelli@gmail.com
8
- Requires-Python: >=3.12,<4.0
9
- Classifier: Development Status :: 2 - Pre-Alpha
8
+ Requires-Python: >=3.12
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: Apache Software License
12
11
  Classifier: Natural Language :: English
13
12
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
13
  Classifier: Topic :: Software Development :: Libraries
18
14
  Requires-Dist: aiohttp
19
- Requires-Dist: babel
20
15
  Requires-Dist: beautifulsoup4
21
16
  Requires-Dist: colorlog
17
+ Requires-Dist: langcodes
22
18
  Requires-Dist: orjson
23
19
  Requires-Dist: yarl
24
20
  Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
25
21
  Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
26
- Project-URL: Repository, https://github.com/chemelli74/aioamazondevices
27
22
  Description-Content-Type: text/markdown
28
23
 
29
24
  # aioamazondevices
@@ -1,13 +1,16 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "aioamazondevices"
3
- version = "3.1.14"
3
+ version = "3.1.19"
4
+ requires-python = ">=3.12"
4
5
  description = "Python library to control Amazon devices"
5
- authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
6
+ authors = [
7
+ { name = "Simone Chemelli", email = "simone.chemelli@gmail.com" },
8
+ ]
6
9
  license = "Apache-2.0"
7
10
  readme = "README.md"
8
11
  repository = "https://github.com/chemelli74/aioamazondevices"
9
12
  classifiers = [
10
- "Development Status :: 2 - Pre-Alpha",
13
+ "Development Status :: 4 - Beta",
11
14
  "Intended Audience :: Developers",
12
15
  "Natural Language :: English",
13
16
  "Operating System :: OS Independent",
@@ -16,22 +19,22 @@ classifiers = [
16
19
  packages = [
17
20
  { include = "aioamazondevices", from = "src" },
18
21
  ]
22
+ dependencies = [
23
+ "aiohttp",
24
+ "beautifulsoup4",
25
+ "colorlog",
26
+ "langcodes",
27
+ "orjson",
28
+ "yarl",
29
+ ]
19
30
 
20
- [tool.poetry.urls]
31
+ [project.urls]
21
32
  "Bug Tracker" = "https://github.com/chemelli74/aioamazondevices/issues"
22
33
  "Changelog" = "https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md"
23
34
 
24
- [tool.poetry.dependencies]
25
- aiohttp = "*"
26
- python = "^3.12"
27
- babel = "*"
28
- beautifulsoup4 = "*"
29
- colorlog = "*"
30
- orjson = "*"
31
- yarl = "*"
32
35
 
33
36
  [tool.poetry.group.dev.dependencies]
34
- pytest = "^8.1"
37
+ pytest = "^8.4"
35
38
  pytest-cov = ">=5,<7"
36
39
 
37
40
  [tool.semantic_release]
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "3.1.14"
3
+ __version__ = "3.1.19"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -16,8 +16,8 @@ from urllib.parse import parse_qs, urlencode
16
16
 
17
17
  import orjson
18
18
  from aiohttp import ClientConnectorError, ClientResponse, ClientSession
19
- from babel import Locale
20
19
  from bs4 import BeautifulSoup, Tag
20
+ from langcodes import Language
21
21
  from multidict import CIMultiDictProxy, MultiDictProxy
22
22
  from yarl import URL
23
23
 
@@ -58,6 +58,7 @@ from .exceptions import (
58
58
  CannotRetrieveData,
59
59
  WrongMethod,
60
60
  )
61
+ from .utils import obfuscate_email, scrub_fields
61
62
 
62
63
 
63
64
  @dataclass
@@ -317,7 +318,7 @@ class AmazonEchoApi:
317
318
  "%s request: %s with payload %s [json=%s]",
318
319
  method,
319
320
  url,
320
- input_data,
321
+ scrub_fields(input_data) if input_data else None,
321
322
  json_data,
322
323
  )
323
324
 
@@ -473,7 +474,7 @@ class AmazonEchoApi:
473
474
  msg = resp_json["response"]["error"]["message"]
474
475
  _LOGGER.error(
475
476
  "Cannot register device for %s: %s",
476
- self._login_email,
477
+ obfuscate_email(self._login_email),
477
478
  msg,
478
479
  )
479
480
  raise CannotRegisterDevice(f"{HTTPStatus(resp.status).phrase}: {msg}")
@@ -601,7 +602,11 @@ class AmazonEchoApi:
601
602
 
602
603
  async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
603
604
  """Login to Amazon interactively via OTP."""
604
- _LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
605
+ _LOGGER.debug(
606
+ "Logging-in for %s [otp code: %s]",
607
+ obfuscate_email(self._login_email),
608
+ bool(otp_code),
609
+ )
605
610
  self._client_session()
606
611
 
607
612
  code_verifier = self._create_code_verifier()
@@ -671,7 +676,7 @@ class AmazonEchoApi:
671
676
 
672
677
  _LOGGER.debug(
673
678
  "Logging-in for %s with stored data",
674
- self._login_email,
679
+ obfuscate_email(self._login_email),
675
680
  )
676
681
 
677
682
  self._client_session()
@@ -723,7 +728,11 @@ class AmazonEchoApi:
723
728
 
724
729
  final_devices_list: dict[str, AmazonDevice] = {}
725
730
  for device in self._devices.values():
726
- devices_node = device[NODE_DEVICES]
731
+ # Remove stale, orphaned and virtual devices
732
+ devices_node = device.get(NODE_DEVICES)
733
+ if not devices_node or (devices_node.get("deviceType") in DEVICE_TO_IGNORE):
734
+ continue
735
+
727
736
  preferences_node = device.get(NODE_PREFERENCES)
728
737
  do_not_disturb_node = device[NODE_DO_NOT_DISTURB]
729
738
  bluetooth_node = device[NODE_BLUETOOTH]
@@ -736,13 +745,6 @@ class AmazonEchoApi:
736
745
  if _device_id == identifier_node["entityId"]:
737
746
  sensors = _device_sensors
738
747
 
739
- # Remove stale, orphaned and virtual devices
740
- if (
741
- NODE_DEVICES not in device
742
- or devices_node.get("deviceType") in DEVICE_TO_IGNORE
743
- ):
744
- continue
745
-
746
748
  serial_number: str = devices_node["serialNumber"]
747
749
  final_devices_list[serial_number] = AmazonDevice(
748
750
  account_name=devices_node["accountName"],
@@ -819,8 +821,9 @@ class AmazonEchoApi:
819
821
  message_source: AmazonMusicSource | None = None,
820
822
  ) -> None:
821
823
  """Send message to specific device."""
822
- locale_data = Locale.parse(f"und_{self._login_country_code}")
823
- locale = f"{locale_data.language}-{locale_data.language}"
824
+ lang_object = Language.make(territory=self._login_country_code.upper())
825
+ lang_maximized = lang_object.maximize()
826
+ locale = f"{lang_maximized.language}-{lang_maximized.region}"
824
827
 
825
828
  if not self._login_stored_data:
826
829
  _LOGGER.warning("Trying to send message before login")
@@ -6,6 +6,25 @@ _LOGGER = logging.getLogger(__package__)
6
6
 
7
7
  DEFAULT_ASSOC_HANDLE = "amzn_dp_project_dee_ios"
8
8
 
9
+ TO_REDACT = {
10
+ "address1",
11
+ "address2",
12
+ "address3",
13
+ "city",
14
+ "county",
15
+ "deviceAccountId",
16
+ "deviceAddress",
17
+ "deviceOwnerCustomerId",
18
+ "given_name",
19
+ "name",
20
+ "password",
21
+ "postalCode",
22
+ "searchCustomerId",
23
+ "state",
24
+ "street",
25
+ "user_id",
26
+ }
27
+
9
28
  DOMAIN_BY_ISO3166_COUNTRY = {
10
29
  "ar": {
11
30
  "domain": "com",
@@ -115,7 +134,8 @@ SPEAKER_GROUP_FAMILY = "WHA"
115
134
  SPEAKER_GROUP_MODEL = "Speaker Group"
116
135
 
117
136
  DEVICE_TO_IGNORE: list[str] = [
118
- AMAZON_DEVICE_TYPE, # Alexa App for Mobile
137
+ AMAZON_DEVICE_TYPE, # Alexa App for iOS
138
+ "A2TF17PFR55MTB", # Alexa App for Android
119
139
  "A1RTAM01W29CUP", # Alexa App for PC
120
140
  ]
121
141
 
@@ -140,6 +160,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
140
160
  "model": "Echo Show 15",
141
161
  "hw_version": "Gen1",
142
162
  },
163
+ "A1NL4BVLQ4L3N3": {
164
+ "model": "Echo Show",
165
+ "hw_version": "Gen1",
166
+ },
143
167
  "A1Q6UGEXJZWJQ0": {
144
168
  "model": "Fire TV Stick 4K",
145
169
  "hw_version": "Gen2",
@@ -165,6 +189,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
165
189
  "model": "FireTV 4k MAX",
166
190
  "hw_version": "Gen2",
167
191
  },
192
+ "A1XWJRHALS1REP": {
193
+ "model": "Echo Show 5",
194
+ "hw_version": "Gen2",
195
+ },
168
196
  "A1Z88NGR2BK6A2": {
169
197
  "model": "Echo Show 8",
170
198
  "hw_version": "Gen1",
@@ -197,10 +225,18 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
197
225
  "model": "Fire TV Stick",
198
226
  "hw_version": "Gen2",
199
227
  },
228
+ "A2M35JJZWCQOMZ": {
229
+ "model": "Echo Plus",
230
+ "hw_version": "Gen1",
231
+ },
200
232
  "A2M4YX06LWP8WI": {
201
233
  "model": "Fire Tablet 7",
202
234
  "hw_version": "Gen5",
203
235
  },
236
+ "A2N49KXGVA18AR": {
237
+ "model": "Fire HD 10 Plus",
238
+ "hw_version": "Gen11",
239
+ },
204
240
  "A2U21SRK4QGSE1": {
205
241
  "model": "Echo Dot",
206
242
  "hw_version": "Gen4",
@@ -250,6 +286,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
250
286
  "model": "Sonos Beam",
251
287
  "hw_version": None,
252
288
  },
289
+ "A3RBAYBE7VM004": {
290
+ "model": "Echo Studio",
291
+ "hw_version": None,
292
+ },
253
293
  "A3RMGO6LYLH7YN": {
254
294
  "model": "Echo Dot",
255
295
  "hw_version": "Gen4",
@@ -299,6 +339,10 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
299
339
  "model": "Fire TV Stick 4K",
300
340
  "hw_version": "Gen1",
301
341
  },
342
+ "AP1F6KUH00XPV": {
343
+ "model": "Echo Stereo Pair",
344
+ "hw_version": "Virtual",
345
+ },
302
346
  "ASQZWP4GPYUT7": {
303
347
  "model": "Echo pop",
304
348
  "hw_version": "Gen1",
@@ -0,0 +1,58 @@
1
+ """Utils module for Amazon devices."""
2
+
3
+ from collections.abc import Collection
4
+ from typing import Any
5
+
6
+ from .const import TO_REDACT
7
+
8
+
9
+ def obfuscate_email(email: str) -> str:
10
+ """Obfuscate an email address partially."""
11
+ try:
12
+ username, domain = email.split("@")
13
+ domain_name, domain_ext = domain.rsplit(".", 1)
14
+
15
+ def obfuscate_part(part: str, visible: int = 1) -> str:
16
+ if len(part) <= visible:
17
+ return "*" * len(part)
18
+ return part[:visible] + "*" * (len(part) - visible)
19
+
20
+ # Obfuscate username and domain parts
21
+ obf_user = ".".join(obfuscate_part(u, 1) for u in username.split("."))
22
+ obf_domain = obfuscate_part(domain_name, 1)
23
+
24
+ except (SyntaxError, ValueError):
25
+ return "[invalid email]"
26
+ else:
27
+ return f"{obf_user}@{obf_domain}.{domain_ext}"
28
+
29
+
30
+ def scrub_fields(
31
+ obj: Any, # noqa: ANN401
32
+ field_names: Collection[str] = TO_REDACT,
33
+ replacement: str = "[REDACTED]",
34
+ ) -> Any: # noqa: ANN401
35
+ """Return a deep-copied version of *obj* with redacted keys."""
36
+ if isinstance(obj, dict):
37
+ result = {}
38
+ for k, v in obj.items():
39
+ # If the key itself is sensitive → overwrite its value
40
+ if k in field_names:
41
+ result[k] = replacement
42
+ else:
43
+ # Otherwise keep walking
44
+ result[k] = scrub_fields(v, field_names, replacement)
45
+ return result
46
+
47
+ if isinstance(obj, list):
48
+ return [scrub_fields(item, field_names, replacement) for item in obj]
49
+
50
+ if isinstance(obj, tuple):
51
+ return tuple(scrub_fields(item, field_names, replacement) for item in obj)
52
+
53
+ if isinstance(obj, set):
54
+ # Note: a set cannot contain mutable/unhashable items like dicts,
55
+ # so we assume its members are hashable after scrubbing.
56
+ return {scrub_fields(item, field_names, replacement) for item in obj}
57
+
58
+ return obj