aioamazondevices 3.1.17rc1__py3-none-any.whl → 3.1.22__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 +50 -27
- aioamazondevices/const.py +21 -0
- aioamazondevices/utils.py +60 -0
- {aioamazondevices-3.1.17rc1.dist-info → aioamazondevices-3.1.22.dist-info}/METADATA +2 -2
- aioamazondevices-3.1.22.dist-info/RECORD +11 -0
- aioamazondevices-3.1.17rc1.dist-info/RECORD +0 -10
- {aioamazondevices-3.1.17rc1.dist-info → aioamazondevices-3.1.22.dist-info}/LICENSE +0 -0
- {aioamazondevices-3.1.17rc1.dist-info → aioamazondevices-3.1.22.dist-info}/WHEEL +0 -0
aioamazondevices/__init__.py
CHANGED
aioamazondevices/api.py
CHANGED
@@ -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
|
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(
|
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(
|
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
|
-
|
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"],
|
aioamazondevices/const.py
CHANGED
@@ -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
|
@@ -1,12 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 3.1.
|
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 ::
|
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
|
@@ -0,0 +1,11 @@
|
|
1
|
+
aioamazondevices/__init__.py,sha256=1zTgfVFS9BIXxS-1ghlWzAXzlEs643PyJVEr2SeZxws,277
|
2
|
+
aioamazondevices/api.py,sha256=6R7eHx48qRqWmLmC657R7vvED75mqS7WvCFh01ixLzk,37149
|
3
|
+
aioamazondevices/const.py,sha256=w4tyAkGRbMX0s1NA1iOeuQ2yAB2v9Xqa56jLa4sQ9vg,8561
|
4
|
+
aioamazondevices/exceptions.py,sha256=JDnSFi_7oEhqK31sHXf0S_cyMoMjiRJuLp4ow7mYgLY,643
|
5
|
+
aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
aioamazondevices/sounds.py,sha256=01pVCDFIuhrLypXInw4JNuHsC6zjMLsuKocet1R6we8,13409
|
7
|
+
aioamazondevices/utils.py,sha256=RzuKRhnq_8ymCoJMoQJ2vBYyuew06RSWpqQWmqdNczE,2019
|
8
|
+
aioamazondevices-3.1.22.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
|
9
|
+
aioamazondevices-3.1.22.dist-info/METADATA,sha256=NKuOQKxX6VK34vpsmrajPQlOa4b0pR-glSj3zaoKGiY,5235
|
10
|
+
aioamazondevices-3.1.22.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
11
|
+
aioamazondevices-3.1.22.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
aioamazondevices/__init__.py,sha256=-DEB2sLPnC-46esN6l_NTZB1KlUE4XuMdEVWFW5-y1o,277
|
2
|
-
aioamazondevices/api.py,sha256=sLfD1-K2H32zgR03UvcBP-xBdwrF1b0VHs0ttfar7nY,36110
|
3
|
-
aioamazondevices/const.py,sha256=sNtoAN22sawIKES6TghRA_kvfe061Oliw_VvYJNklNA,8234
|
4
|
-
aioamazondevices/exceptions.py,sha256=JDnSFi_7oEhqK31sHXf0S_cyMoMjiRJuLp4ow7mYgLY,643
|
5
|
-
aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
aioamazondevices/sounds.py,sha256=01pVCDFIuhrLypXInw4JNuHsC6zjMLsuKocet1R6we8,13409
|
7
|
-
aioamazondevices-3.1.17rc1.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
|
8
|
-
aioamazondevices-3.1.17rc1.dist-info/METADATA,sha256=-7MfUws8OCh7TwQwyd_HEU684aMqV697LRC_6ReCPRE,5243
|
9
|
-
aioamazondevices-3.1.17rc1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
-
aioamazondevices-3.1.17rc1.dist-info/RECORD,,
|
File without changes
|
File without changes
|