aioamazondevices 6.0.0__tar.gz → 6.1.0__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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 6.0.0
3
+ Version: 6.1.0
4
4
  Summary: Python library to control Amazon devices
5
5
  License: Apache-2.0
6
6
  Author: Simone Chemelli
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "6.0.0"
3
+ version = "6.1.0"
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__ = "6.0.0"
3
+ __version__ = "6.1.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -48,18 +48,12 @@ from .const import (
48
48
  HTTP_ERROR_199,
49
49
  HTTP_ERROR_299,
50
50
  JSON_EXTENSION,
51
- NODE_BLUETOOTH,
52
- NODE_DEVICES,
53
- NODE_DO_NOT_DISTURB,
54
- NODE_IDENTIFIER,
55
- NODE_PREFERENCES,
56
51
  REFRESH_ACCESS_TOKEN,
57
52
  REFRESH_AUTH_COOKIES,
58
53
  SAVE_PATH,
59
54
  SENSORS,
60
- URI_IDS,
61
- URI_QUERIES,
62
- URI_SENSORS,
55
+ URI_DEVICES,
56
+ URI_NEXUS_GRAPHQL,
63
57
  URI_SIGNIN,
64
58
  )
65
59
  from .exceptions import (
@@ -69,6 +63,7 @@ from .exceptions import (
69
63
  CannotRetrieveData,
70
64
  WrongMethod,
71
65
  )
66
+ from .query import QUERY_DEVICE_STATE
72
67
  from .utils import obfuscate_email, scrub_fields
73
68
 
74
69
 
@@ -94,11 +89,8 @@ class AmazonDevice:
94
89
  online: bool
95
90
  serial_number: str
96
91
  software_version: str
97
- do_not_disturb: bool
98
- response_style: str | None
99
- bluetooth_state: bool
100
- entity_id: str
101
- appliance_id: str
92
+ entity_id: str | None
93
+ endpoint_id: str | None
102
94
  sensors: dict[str, AmazonDeviceSensor]
103
95
 
104
96
 
@@ -326,14 +318,6 @@ class AmazonEchoApi:
326
318
  )
327
319
  return False
328
320
 
329
- async def _ignore_phoenix_error(self, response: ClientResponse) -> bool:
330
- """Return true if error is due to phoenix endpoint."""
331
- # Endpoint URI_IDS replies with error 199 or 299
332
- # during maintenance
333
- return response.status in [HTTP_ERROR_199, HTTP_ERROR_299] and (
334
- URI_IDS in response.url.path
335
- )
336
-
337
321
  async def _http_phrase_error(self, error: int) -> str:
338
322
  """Convert numeric error in human phrase."""
339
323
  if error == HTTP_ERROR_199:
@@ -444,9 +428,7 @@ class AmazonEchoApi:
444
428
  HTTPStatus.UNAUTHORIZED,
445
429
  ]:
446
430
  raise CannotAuthenticate(await self._http_phrase_error(resp.status))
447
- if not await self._ignore_ap_signin_error(
448
- resp
449
- ) and not await self._ignore_phoenix_error(resp):
431
+ if not await self._ignore_ap_signin_error(resp):
450
432
  raise CannotRetrieveData(
451
433
  f"Request failed: {await self._http_phrase_error(resp.status)}"
452
434
  )
@@ -597,127 +579,80 @@ class AmazonEchoApi:
597
579
  _LOGGER.info("Register device: %s", scrub_fields(login_data))
598
580
  return login_data
599
581
 
600
- async def _get_devices_ids(self) -> list[dict[str, str]]:
601
- """Retrieve devices entityId and applianceId."""
602
- _, raw_resp = await self._session_request(
603
- "GET",
604
- url=f"https://alexa.amazon.{self._domain}{URI_IDS}",
605
- amazon_user_agent=False,
606
- )
607
-
608
- # Sensors data not available
609
- if raw_resp.status != HTTPStatus.OK:
610
- _LOGGER.warning(
611
- "Sensors data not available [%s error '%s'], skipping",
612
- URI_IDS,
613
- await self._http_phrase_error(raw_resp.status),
614
- )
615
- self._sensors_available = False
616
- return []
617
-
618
- json_data = await raw_resp.json()
619
-
620
- network_detail = orjson.loads(json_data["networkDetail"])
621
- # Navigate through the nested structure step by step
622
- location_details = network_detail["locationDetails"]["locationDetails"]
623
- default_location = location_details["Default_Location"]
624
- amazon_bridge = default_location["amazonBridgeDetails"]["amazonBridgeDetails"]
625
-
626
- # New devices are based on LambdaBridge_AAA structure
627
- lambda_bridge_aaa = amazon_bridge.get("LambdaBridge_AAA/SonarCloudService")
628
- appliance_details_aaa = (
629
- lambda_bridge_aaa["applianceDetails"]["applianceDetails"]
630
- if lambda_bridge_aaa
631
- else {}
632
- )
582
+ async def _get_devices_state(
583
+ self,
584
+ ) -> dict[str, Any]:
585
+ """Get Device State."""
586
+ payload = {
587
+ "operationName": "getDevicesState",
588
+ "variables": {
589
+ "latencyTolerance": "LOW",
590
+ },
591
+ "query": QUERY_DEVICE_STATE,
592
+ }
633
593
 
634
- entity_ids_list: list[dict[str, str]] = await self._get_entities_ids(
635
- appliance_details_aaa, "AAA_SonarCloudService"
594
+ _, raw_resp = await self._session_request(
595
+ method=HTTPMethod.POST,
596
+ url=f"https://alexa.amazon.{self._domain}{URI_NEXUS_GRAPHQL}",
597
+ input_data=payload,
598
+ json_data=True,
636
599
  )
637
600
 
638
- # Old devices are based on LambdaBridge_AlexaBridge structure
639
- for bridge_key, bridge_value in amazon_bridge.items():
640
- if "LambdaBridge_AlexaBridge/" in bridge_key:
641
- # Value key: "LambdaBridge_AlexaBridge/XXXXXXXXXXXXXX@XXXXXXXXXXXXXX"
642
- # Value subkey: "AlexaBridge_XXXXXXXXXXXXXX@XXXXXXXXXXXXXX_XXXXXXXXXXXX"
643
- subkey = bridge_key.split("_")[1].replace("/", "_")
644
-
645
- appliance_details_alexa = bridge_value["applianceDetails"][
646
- "applianceDetails"
647
- ]
648
- entity_ids_list.extend(
649
- await self._get_entities_ids(appliance_details_alexa, subkey)
650
- )
651
-
652
- return entity_ids_list
653
-
654
- async def _get_entities_ids(
655
- self, appliance_details: dict[str, Any], searchkey: str
656
- ) -> list[dict[str, str]]:
657
- """Extract entityId and applianceId."""
658
- entity_ids_list: list[dict[str, str]] = []
659
- # Process each appliance that starts with "searchkey"
660
- for appliance_key, appliance_data in appliance_details.items():
661
- if not appliance_key.startswith(searchkey):
662
- continue
663
-
664
- entity_id = appliance_data["entityId"]
665
- appliance_id = appliance_data["applianceId"]
666
-
667
- # Create identifier object for this appliance
668
- identifier = {
669
- "entityId": entity_id,
670
- "applianceId": appliance_id,
671
- }
672
-
673
- # Update device information for each device in the identifier list
674
- for device_identifier in appliance_data["alexaDeviceIdentifierList"]:
675
- serial_number = device_identifier["dmsDeviceSerialNumber"]
676
-
677
- # Add identifier information to the device
678
- # but only if the device was previously found
679
- if serial_number in self._devices:
680
- self._devices[serial_number] |= {NODE_IDENTIFIER: identifier}
681
-
682
- # Add to entity IDs list for sensor retrieval
683
- entity_ids_list.append({"entityId": entity_id, "entityType": "ENTITY"})
684
-
685
- return entity_ids_list
601
+ return cast("dict", await raw_resp.json())
686
602
 
687
603
  async def _get_sensors_states(
688
- self, entity_ids_list: list[dict[str, str]]
689
- ) -> dict[str, dict[str, AmazonDeviceSensor]]:
604
+ self,
605
+ ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, AmazonDeviceSensor]]]:
690
606
  """Retrieve devices sensors states."""
691
- _data = {"stateRequests": entity_ids_list}
692
- _, raw_resp = await self._session_request(
693
- "POST",
694
- url=f"https://alexa.amazon.{self._domain}{URI_SENSORS}",
695
- input_data=_data,
696
- json_data=True,
697
- )
698
- json_data = await raw_resp.json()
607
+ devices_state = await self._get_devices_state()
608
+ devices_sensors: dict[str, dict[str, AmazonDeviceSensor]] = {}
609
+ devices_endpoints: dict[str, dict[str, Any]] = {}
610
+
611
+ endpoints = devices_state["data"]["listEndpoints"]
612
+ for endpoint in endpoints.get("endpoints"):
613
+ serial_number = (
614
+ endpoint["serialNumber"]["value"]["text"]
615
+ if endpoint["serialNumber"]
616
+ else None
617
+ )
618
+ if serial_number in self._devices:
619
+ devices_sensors[serial_number] = self._get_device_sensor_state(endpoint)
620
+ devices_endpoints[serial_number] = endpoint
621
+
622
+ return devices_endpoints, devices_sensors
623
+
624
+ def _get_device_sensor_state(
625
+ self, endpoint: dict[str, Any]
626
+ ) -> dict[str, AmazonDeviceSensor]:
627
+ device_sensors: dict[str, AmazonDeviceSensor] = {}
628
+ if (
629
+ endpoint_dnd := endpoint.get("settings", {}).get("doNotDisturb")
630
+ ) and not endpoint_dnd["error"]:
631
+ device_sensors["dnd"] = AmazonDeviceSensor(
632
+ "dnd", endpoint_dnd.get("toggleValue"), None
633
+ )
634
+ for feature in endpoint.get("features", {}):
635
+ first_property = (feature.get("properties") or [None])[0] or {}
636
+ if (
637
+ first_property.get("type") != "RETRIEVABLE"
638
+ or (sensor := SENSORS.get(feature["name"])) is None
639
+ ):
640
+ continue
699
641
 
700
- final_sensors: dict[str, dict[str, AmazonDeviceSensor]] = {}
701
- for sensors in json_data["deviceStates"]:
702
- _id = sensors["entity"]["entityId"]
703
- dict_sensors: dict[str, AmazonDeviceSensor] = {}
704
- for sensor in sensors["capabilityStates"]:
705
- sensor_json = orjson.loads(sensor)
706
- if sensor_json["name"] in SENSORS:
707
- _value = sensor_json["value"]
708
- _value_dict = isinstance(_value, dict)
709
- _name = sensor_json["name"]
710
- dict_sensors.update(
711
- {
712
- _name: AmazonDeviceSensor(
713
- name=_name,
714
- value=(_value["value"] if _value_dict else _value),
715
- scale=_value.get("scale") if _value_dict else None,
716
- )
717
- }
718
- )
719
- final_sensors.update({_id: dict_sensors})
720
- return final_sensors
642
+ if not (name := sensor["name"]):
643
+ raise TypeError("Unable to read sensor template")
644
+
645
+ value = first_property[sensor["key"]]
646
+ scale = value["scale"] if sensor["scale"] else None
647
+ if subkey := sensor["subkey"]:
648
+ value = value[subkey]
649
+ device_sensors[name] = AmazonDeviceSensor(
650
+ name,
651
+ value,
652
+ scale,
653
+ )
654
+
655
+ return device_sensors
721
656
 
722
657
  async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
723
658
  """Login to Amazon interactively via OTP."""
@@ -857,68 +792,49 @@ class AmazonEchoApi:
857
792
  ) -> dict[str, AmazonDevice]:
858
793
  """Get Amazon devices data."""
859
794
  self._devices = {}
860
- for key in URI_QUERIES:
861
- _, raw_resp = await self._session_request(
862
- method=HTTPMethod.GET,
863
- url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
864
- )
865
-
866
- response_data = await raw_resp.text()
867
- json_data = {} if len(response_data) == 0 else await raw_resp.json()
795
+ _, raw_resp = await self._session_request(
796
+ method=HTTPMethod.GET,
797
+ url=f"https://alexa.amazon.{self._domain}{URI_DEVICES}",
798
+ )
868
799
 
869
- _LOGGER.debug("JSON data: |%s|", scrub_fields(json_data))
800
+ response_data = await raw_resp.text()
801
+ json_data = {} if len(response_data) == 0 else await raw_resp.json()
870
802
 
871
- for data in json_data[key]:
872
- dev_serial = data.get("serialNumber") or data.get("deviceSerialNumber")
873
- if previous_data := self._devices.get(dev_serial):
874
- self._devices[dev_serial] = previous_data | {key: data}
875
- else:
876
- self._devices[dev_serial] = {key: data}
803
+ _LOGGER.debug("JSON devices data: %s", scrub_fields(json_data))
877
804
 
878
- devices_sensors: dict[str, dict[str, AmazonDeviceSensor]] = {}
805
+ for data in json_data["devices"]:
806
+ dev_serial = data.get("serialNumber")
807
+ self._devices[dev_serial] = data
879
808
 
880
- if self._sensors_available and (
881
- entity_ids_list := await self._get_devices_ids()
882
- ):
883
- devices_sensors = await self._get_sensors_states(entity_ids_list)
809
+ devices_endpoints, devices_sensors = await self._get_sensors_states()
884
810
 
885
811
  final_devices_list: dict[str, AmazonDevice] = {}
886
812
  for device in self._devices.values():
887
813
  # Remove stale, orphaned and virtual devices
888
- devices_node = device.get(NODE_DEVICES)
889
- if not devices_node or (devices_node.get("deviceType") in DEVICE_TO_IGNORE):
814
+ if not device or (device.get("deviceType") in DEVICE_TO_IGNORE):
890
815
  continue
891
816
 
892
- preferences_node = device.get(NODE_PREFERENCES, {})
893
- do_not_disturb_node = device[NODE_DO_NOT_DISTURB]
894
- bluetooth_node = device[NODE_BLUETOOTH]
895
- identifier_node = device.get(NODE_IDENTIFIER, {})
896
-
817
+ serial_number: str = device["serialNumber"]
897
818
  # Add sensors
898
- sensors = {}
899
- if identifier_node:
900
- for _device_id, _device_sensors in devices_sensors.items():
901
- if _device_id == identifier_node["entityId"]:
902
- sensors = _device_sensors
819
+ sensors = devices_sensors.get(serial_number, {})
820
+ device_endpoint = devices_endpoints.get(serial_number, {})
903
821
 
904
- serial_number: str = devices_node["serialNumber"]
905
822
  final_devices_list[serial_number] = AmazonDevice(
906
- account_name=devices_node["accountName"],
907
- capabilities=devices_node["capabilities"],
908
- device_family=devices_node["deviceFamily"],
909
- device_type=devices_node["deviceType"],
910
- device_owner_customer_id=devices_node["deviceOwnerCustomerId"],
911
- device_cluster_members=(
912
- devices_node["clusterMembers"] or [serial_number]
913
- ),
914
- online=devices_node["online"],
823
+ account_name=device["accountName"],
824
+ capabilities=device["capabilities"],
825
+ device_family=device["deviceFamily"],
826
+ device_type=device["deviceType"],
827
+ device_owner_customer_id=device["deviceOwnerCustomerId"],
828
+ device_cluster_members=(device["clusterMembers"] or [serial_number]),
829
+ online=device["online"],
915
830
  serial_number=serial_number,
916
- software_version=devices_node["softwareVersion"],
917
- do_not_disturb=do_not_disturb_node["enabled"],
918
- response_style=preferences_node.get("responseStyle"),
919
- bluetooth_state=bluetooth_node["online"],
920
- entity_id=identifier_node.get("entityId"),
921
- appliance_id=identifier_node.get("applianceId"),
831
+ software_version=device["softwareVersion"],
832
+ entity_id=device_endpoint["legacyIdentifiers"]["chrsIdentifier"][
833
+ "entityId"
834
+ ]
835
+ if device_endpoint
836
+ else None,
837
+ endpoint_id=device_endpoint["endpointId"] if device_endpoint else None,
922
838
  sensors=sensors,
923
839
  )
924
840
 
@@ -54,41 +54,10 @@ CSRF_COOKIE = "csrf"
54
54
  REFRESH_ACCESS_TOKEN = "access_token" # noqa: S105
55
55
  REFRESH_AUTH_COOKIES = "auth_cookies"
56
56
 
57
- NODE_DEVICES = "devices"
58
- NODE_DO_NOT_DISTURB = "doNotDisturbDeviceStatusList"
59
- NODE_PREFERENCES = "devicePreferences"
60
- NODE_BLUETOOTH = "bluetoothStates"
61
- NODE_IDENTIFIER = "identifier"
62
- NODE_SENSORS = "sensors"
63
-
64
- URI_QUERIES = {
65
- NODE_DEVICES: "/api/devices-v2/device",
66
- NODE_DO_NOT_DISTURB: "/api/dnd/device-status-list",
67
- NODE_PREFERENCES: "/api/device-preferences",
68
- NODE_BLUETOOTH: "/api/bluetooth",
69
- # "/api/ping"
70
- # "/api/np/command"
71
- # "/api/np/player"
72
- # "/api/device-wifi-details"
73
- # "/api/activities"
74
- # "/api/behaviors/v2/automations"
75
- # "/api/notifications"
76
- }
77
-
57
+ URI_DEVICES = "/api/devices-v2/device"
78
58
  URI_SIGNIN = "/ap/signin"
79
- URI_IDS = "/api/phoenix"
80
- URI_SENSORS = "/api/phoenix/state"
59
+ URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
81
60
 
82
- SENSORS = [
83
- "babyCryDetectionState",
84
- "beepingApplianceDetectionState",
85
- "coughDetectionState",
86
- "dogBarkDetectionState",
87
- "humanPresenceDetectionState",
88
- "illuminance",
89
- "temperature",
90
- "waterSoundsDetectionState",
91
- ]
92
61
  SENSOR_STATE_OFF = "NOT_DETECTED"
93
62
 
94
63
  # File extensions
@@ -100,6 +69,32 @@ BIN_EXTENSION = ".bin"
100
69
  SPEAKER_GROUP_FAMILY = "WHA"
101
70
  SPEAKER_GROUP_MODEL = "Speaker Group"
102
71
 
72
+ SENSORS: dict[str, dict[str, str | None]] = {
73
+ "temperatureSensor": {
74
+ "name": "temperature",
75
+ "key": "value",
76
+ "subkey": "value",
77
+ "scale": "scale",
78
+ },
79
+ "motionSensor": {
80
+ "name": "motion",
81
+ "key": "detectionStateValue",
82
+ "subkey": None,
83
+ "scale": None,
84
+ },
85
+ "lightSensor": {
86
+ "name": "illuminance",
87
+ "key": "illuminanceValue",
88
+ "subkey": "value",
89
+ "scale": None,
90
+ },
91
+ "speaker": {
92
+ "name": "volume",
93
+ "key": "value",
94
+ "subkey": "volValue",
95
+ "scale": None,
96
+ },
97
+ }
103
98
  DEVICE_TO_IGNORE: list[str] = [
104
99
  AMAZON_DEVICE_TYPE, # Alexa App for iOS
105
100
  "A2TF17PFR55MTB", # Alexa App for Android
@@ -0,0 +1,91 @@
1
+ """GraphQL Queries."""
2
+
3
+ QUERY_DEVICE_STATE = """
4
+ query getDevicesState ($latencyTolerance: LatencyToleranceValue) {
5
+ listEndpoints(listEndpointsInput: {}) {
6
+ endpoints {
7
+ endpointId: id
8
+ friendlyNameObject { value { text } }
9
+ manufacturer { value { text } }
10
+ model { value { text} }
11
+ serialNumber { value { text } }
12
+ softwareVersion { value { text } }
13
+ creationTime
14
+ enablement
15
+ settings {
16
+ doNotDisturb {
17
+ id
18
+ endpointId
19
+ name
20
+ toggleValue
21
+ error {
22
+ type
23
+ message
24
+ }
25
+ }
26
+ }
27
+ displayCategories {
28
+ all { value }
29
+ primary { value }
30
+ }
31
+ alexaEnabledMetadata {
32
+ iconId
33
+ isVisible
34
+ category
35
+ capabilities
36
+ }
37
+ legacyIdentifiers {
38
+ dmsIdentifier {
39
+ deviceType { value { text } }
40
+ }
41
+ chrsIdentifier { entityId }
42
+ }
43
+ legacyAppliance { applianceId }
44
+ associatedUnits { id }
45
+ connections {
46
+ type
47
+ macAddress
48
+ bleMeshDeviceUuid
49
+ }
50
+ features(latencyToleranceValue: $latencyTolerance) {
51
+ name
52
+ instance
53
+ properties {
54
+ name
55
+ type
56
+ accuracy
57
+ error { message }
58
+ __typename
59
+ ... on Illuminance {
60
+ illuminanceValue { value }
61
+ timeOfSample
62
+ timeOfLastChange
63
+ }
64
+ ... on Reachability {
65
+ reachabilityStatusValue
66
+ timeOfSample
67
+ timeOfLastChange
68
+ }
69
+ ... on DetectionState {
70
+ detectionStateValue
71
+ timeOfSample
72
+ timeOfLastChange
73
+ }
74
+ ... on Volume {
75
+ value { volValue: value }
76
+ }
77
+ ... on TemperatureSensor {
78
+ name
79
+ value {
80
+ value
81
+ scale
82
+ }
83
+ timeOfSample
84
+ timeOfLastChange
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ """