aioamazondevices 3.1.17rc1__tar.gz → 3.1.22__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,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 3.1.17rc1
3
+ Version: 3.1.22
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
8
  Requires-Python: >=3.12
9
- Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Operating System :: OS Independent
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "3.1.17-rc.1"
3
+ version = "3.1.22"
4
4
  requires-python = ">=3.12"
5
5
  description = "Python library to control Amazon devices"
6
6
  authors = [
@@ -10,7 +10,7 @@ license = "Apache-2.0"
10
10
  readme = "README.md"
11
11
  repository = "https://github.com/chemelli74/aioamazondevices"
12
12
  classifiers = [
13
- "Development Status :: 2 - Pre-Alpha",
13
+ "Development Status :: 4 - Beta",
14
14
  "Intended Audience :: Developers",
15
15
  "Natural Language :: English",
16
16
  "Operating System :: OS Independent",
@@ -38,7 +38,7 @@ pytest = "^8.4"
38
38
  pytest-cov = ">=5,<7"
39
39
 
40
40
  [tool.semantic_release]
41
- version_toml = ["pyproject.toml:tool.poetry.version"]
41
+ version_toml = ["pyproject.toml:project.version"]
42
42
  version_variables = [
43
43
  "src/aioamazondevices/__init__.py:__version__",
44
44
  ]
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "3.1.15"
3
+ __version__ = "3.1.22"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -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,16 +474,11 @@ 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}")
480
481
 
481
- await self._save_to_file(
482
- await resp.text(),
483
- url=register_url,
484
- extension=JSON_EXTENSION,
485
- )
486
482
  success_response = resp_json["response"]["success"]
487
483
 
488
484
  tokens = success_response["tokens"]
@@ -529,16 +525,43 @@ class AmazonEchoApi:
529
525
  location_details = network_detail["locationDetails"]["locationDetails"]
530
526
  default_location = location_details["Default_Location"]
531
527
  amazon_bridge = default_location["amazonBridgeDetails"]["amazonBridgeDetails"]
532
- lambda_bridge = amazon_bridge.get("LambdaBridge_AAA/SonarCloudService")
533
- if not lambda_bridge:
534
- # Some very old devices lack the key for sensors data
535
- return []
536
- appliance_details = lambda_bridge["applianceDetails"]["applianceDetails"]
537
528
 
529
+ # New devices are based on LambdaBridge_AAA structure
530
+ lambda_bridge_aaa = amazon_bridge.get("LambdaBridge_AAA/SonarCloudService")
531
+ appliance_details_aaa = (
532
+ lambda_bridge_aaa["applianceDetails"]["applianceDetails"]
533
+ if lambda_bridge_aaa
534
+ else {}
535
+ )
536
+
537
+ entity_ids_list: list[dict[str, str]] = await self._get_entities_ids(
538
+ appliance_details_aaa, "AAA_SonarCloudService"
539
+ )
540
+
541
+ # Old devices are based on LambdaBridge_AlexaBridge structure
542
+ for bridge_key, bridge_value in amazon_bridge.items():
543
+ if "LambdaBridge_AlexaBridge/" in bridge_key:
544
+ # Value key: "LambdaBridge_AlexaBridge/XXXXXXXXXXXXXX@XXXXXXXXXXXXXX"
545
+ # Value subkey: "AlexaBridge_XXXXXXXXXXXXXX@XXXXXXXXXXXXXX_XXXXXXXXXXXX"
546
+ subkey = bridge_key.split("_")[1].replace("/", "_")
547
+
548
+ appliance_details_alexa = bridge_value["applianceDetails"][
549
+ "applianceDetails"
550
+ ]
551
+ entity_ids_list.extend(
552
+ await self._get_entities_ids(appliance_details_alexa, subkey)
553
+ )
554
+
555
+ return entity_ids_list
556
+
557
+ async def _get_entities_ids(
558
+ self, appliance_details: dict[str, Any], searchkey: str
559
+ ) -> list[dict[str, str]]:
560
+ """Extract entityId and applianceId."""
538
561
  entity_ids_list: list[dict[str, str]] = []
539
- # Process each appliance that starts with AAA_SonarCloudService
562
+ # Process each appliance that starts with "searchkey"
540
563
  for appliance_key, appliance_data in appliance_details.items():
541
- if not appliance_key.startswith("AAA_SonarCloudService"):
564
+ if not appliance_key.startswith(searchkey):
542
565
  continue
543
566
 
544
567
  entity_id = appliance_data["entityId"]
@@ -601,7 +624,11 @@ class AmazonEchoApi:
601
624
 
602
625
  async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
603
626
  """Login to Amazon interactively via OTP."""
604
- _LOGGER.debug("Logging-in for %s [otp code %s]", self._login_email, otp_code)
627
+ _LOGGER.debug(
628
+ "Logging-in for %s [otp code: %s]",
629
+ obfuscate_email(self._login_email),
630
+ bool(otp_code),
631
+ )
605
632
  self._client_session()
606
633
 
607
634
  code_verifier = self._create_code_verifier()
@@ -657,7 +684,7 @@ class AmazonEchoApi:
657
684
  register_device = await self._register_device(device_login_data)
658
685
  self._login_stored_data = register_device
659
686
 
660
- _LOGGER.info("Register device: %s", register_device)
687
+ _LOGGER.info("Register device: %s", scrub_fields(register_device))
661
688
  return register_device
662
689
 
663
690
  async def login_mode_stored_data(self) -> dict[str, Any]:
@@ -671,7 +698,7 @@ class AmazonEchoApi:
671
698
 
672
699
  _LOGGER.debug(
673
700
  "Logging-in for %s with stored data",
674
- self._login_email,
701
+ obfuscate_email(self._login_email),
675
702
  )
676
703
 
677
704
  self._client_session()
@@ -699,7 +726,6 @@ class AmazonEchoApi:
699
726
  _LOGGER.debug("Response code: |%s|", response_code)
700
727
 
701
728
  response_data = await raw_resp.text()
702
- _LOGGER.debug("Response data: |%s|", response_data)
703
729
 
704
730
  if not self._csrf_cookie:
705
731
  self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE, Morsel()).value
@@ -707,7 +733,7 @@ class AmazonEchoApi:
707
733
 
708
734
  json_data = {} if len(response_data) == 0 else await raw_resp.json()
709
735
 
710
- _LOGGER.debug("JSON data: |%s|", json_data)
736
+ _LOGGER.debug("JSON data: |%s|", scrub_fields(json_data))
711
737
 
712
738
  for data in json_data[key]:
713
739
  dev_serial = data.get("serialNumber") or data.get("deviceSerialNumber")
@@ -723,7 +749,11 @@ class AmazonEchoApi:
723
749
 
724
750
  final_devices_list: dict[str, AmazonDevice] = {}
725
751
  for device in self._devices.values():
726
- devices_node = device[NODE_DEVICES]
752
+ # Remove stale, orphaned and virtual devices
753
+ devices_node = device.get(NODE_DEVICES)
754
+ if not devices_node or (devices_node.get("deviceType") in DEVICE_TO_IGNORE):
755
+ continue
756
+
727
757
  preferences_node = device.get(NODE_PREFERENCES)
728
758
  do_not_disturb_node = device[NODE_DO_NOT_DISTURB]
729
759
  bluetooth_node = device[NODE_BLUETOOTH]
@@ -736,13 +766,6 @@ class AmazonEchoApi:
736
766
  if _device_id == identifier_node["entityId"]:
737
767
  sensors = _device_sensors
738
768
 
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
769
  serial_number: str = devices_node["serialNumber"]
747
770
  final_devices_list[serial_number] = AmazonDevice(
748
771
  account_name=devices_node["accountName"],
@@ -6,6 +6,27 @@ _LOGGER = logging.getLogger(__package__)
6
6
 
7
7
  DEFAULT_ASSOC_HANDLE = "amzn_dp_project_dee_ios"
8
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
+ }
29
+
9
30
  DOMAIN_BY_ISO3166_COUNTRY = {
10
31
  "ar": {
11
32
  "domain": "com",
@@ -0,0 +1,60 @@
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 == "email":
41
+ result[k] = obfuscate_email(v)
42
+ elif k in field_names:
43
+ result[k] = replacement
44
+ else:
45
+ # Otherwise keep walking
46
+ result[k] = scrub_fields(v, field_names, replacement)
47
+ return result
48
+
49
+ if isinstance(obj, list):
50
+ return [scrub_fields(item, field_names, replacement) for item in obj]
51
+
52
+ if isinstance(obj, tuple):
53
+ return tuple(scrub_fields(item, field_names, replacement) for item in obj)
54
+
55
+ if isinstance(obj, set):
56
+ # Note: a set cannot contain mutable/unhashable items like dicts,
57
+ # so we assume its members are hashable after scrubbing.
58
+ return {scrub_fields(item, field_names, replacement) for item in obj}
59
+
60
+ return obj