aioamazondevices 6.4.6__tar.gz → 6.5.1__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.4
2
2
  Name: aioamazondevices
3
- Version: 6.4.6
3
+ Version: 6.5.1
4
4
  Summary: Python library to control Amazon devices
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -17,6 +17,7 @@ Requires-Dist: beautifulsoup4
17
17
  Requires-Dist: colorlog
18
18
  Requires-Dist: langcodes
19
19
  Requires-Dist: orjson (>=3.10,<4)
20
+ Requires-Dist: python-dateutil
20
21
  Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
21
22
  Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
22
23
  Project-URL: Homepage, https://github.com/chemelli74/aioamazondevices
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "6.4.6"
3
+ version = "6.5.1"
4
4
  requires-python = ">=3.12"
5
5
  description = "Python library to control Amazon devices"
6
6
  authors = [
@@ -25,6 +25,7 @@ dependencies = [
25
25
  "colorlog",
26
26
  "langcodes",
27
27
  "orjson (>=3.10,<4)",
28
+ "python-dateutil",
28
29
  ]
29
30
 
30
31
  [project.urls]
@@ -35,6 +36,7 @@ dependencies = [
35
36
  [tool.poetry.group.dev.dependencies]
36
37
  pytest = "^8.4"
37
38
  pytest-cov = ">=5,<8"
39
+ types-python-dateutil = "^2.9.0.20250822"
38
40
 
39
41
  [tool.semantic_release]
40
42
  version_toml = ["pyproject.toml:project.version"]
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "6.4.6"
3
+ __version__ = "6.5.1"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -23,6 +23,8 @@ from aiohttp import (
23
23
  ContentTypeError,
24
24
  )
25
25
  from bs4 import BeautifulSoup, Tag
26
+ from dateutil.parser import parse
27
+ from dateutil.rrule import rrulestr
26
28
  from langcodes import Language, standardize_tag
27
29
  from multidict import MultiDictProxy
28
30
  from yarl import URL
@@ -38,7 +40,9 @@ from .const import (
38
40
  AMAZON_CLIENT_OS,
39
41
  AMAZON_DEVICE_SOFTWARE_VERSION,
40
42
  AMAZON_DEVICE_TYPE,
43
+ ARRAY_WRAPPER,
41
44
  BIN_EXTENSION,
45
+ COUNTRY_GROUPS,
42
46
  CSRF_COOKIE,
43
47
  DEFAULT_HEADERS,
44
48
  DEFAULT_SITE,
@@ -48,6 +52,11 @@ from .const import (
48
52
  HTTP_ERROR_199,
49
53
  HTTP_ERROR_299,
50
54
  JSON_EXTENSION,
55
+ NOTIFICATION_ALARM,
56
+ NOTIFICATION_MUSIC_ALARM,
57
+ NOTIFICATION_REMINDER,
58
+ NOTIFICATION_TIMER,
59
+ RECURRING_PATTERNS,
51
60
  REFRESH_ACCESS_TOKEN,
52
61
  REFRESH_AUTH_COOKIES,
53
62
  REQUEST_AGENT,
@@ -56,7 +65,9 @@ from .const import (
56
65
  URI_DEVICES,
57
66
  URI_DND,
58
67
  URI_NEXUS_GRAPHQL,
68
+ URI_NOTIFICATIONS,
59
69
  URI_SIGNIN,
70
+ WEEKEND_EXCEPTIONS,
60
71
  )
61
72
  from .exceptions import (
62
73
  CannotAuthenticate,
@@ -81,6 +92,16 @@ class AmazonDeviceSensor:
81
92
  scale: str | None
82
93
 
83
94
 
95
+ @dataclass
96
+ class AmazonSchedule:
97
+ """Amazon schedule class."""
98
+
99
+ type: str # alarm, reminder, timer
100
+ status: str
101
+ label: str
102
+ next_occurrence: datetime | None
103
+
104
+
84
105
  @dataclass
85
106
  class AmazonDevice:
86
107
  """Amazon device class."""
@@ -98,6 +119,7 @@ class AmazonDevice:
98
119
  entity_id: str | None
99
120
  endpoint_id: str | None
100
121
  sensors: dict[str, AmazonDeviceSensor]
122
+ notifications: dict[str, AmazonSchedule]
101
123
 
102
124
 
103
125
  class AmazonSequenceType(StrEnum):
@@ -173,6 +195,7 @@ class AmazonEchoApi:
173
195
  lang_object = Language.make(territory=country_code.upper())
174
196
  lang_maximized = lang_object.maximize()
175
197
 
198
+ self._country_code: str = country_code
176
199
  self._domain: str = domain
177
200
  language = f"{lang_maximized.language}-{lang_maximized.territory}"
178
201
  self._language = standardize_tag(language)
@@ -584,20 +607,19 @@ class AmazonEchoApi:
584
607
  _LOGGER.info("Register device: %s", scrub_fields(login_data))
585
608
  return login_data
586
609
 
587
- async def _get_sensors_state(
588
- self, endpoint_id_list: list[str]
589
- ) -> dict[str, Any] | list[dict[str, Any]]:
590
- """Get sensor State."""
610
+ async def _get_sensors_states(self) -> dict[str, dict[str, AmazonDeviceSensor]]:
611
+ """Retrieve devices sensors states."""
612
+ devices_sensors: dict[str, dict[str, AmazonDeviceSensor]] = {}
613
+
614
+ endpoint_ids = list(self._endpoints.keys())
591
615
  payload = [
592
616
  {
593
617
  "operationName": "getEndpointState",
594
618
  "variables": {
595
- "endpointId": endpoint_id,
596
- "latencyTolerance": "LOW",
619
+ "endpointIds": endpoint_ids,
597
620
  },
598
621
  "query": QUERY_SENSOR_STATE,
599
622
  }
600
- for endpoint_id in endpoint_id_list
601
623
  ]
602
624
 
603
625
  _, raw_resp = await self._session_request(
@@ -607,47 +629,29 @@ class AmazonEchoApi:
607
629
  json_data=True,
608
630
  )
609
631
 
610
- return await self._response_to_json(raw_resp)
632
+ sensors_state = await self._response_to_json(raw_resp)
633
+ _LOGGER.debug("Sensor data - %s", sensors_state)
611
634
 
612
- async def _get_sensors_states(self) -> dict[str, dict[str, AmazonDeviceSensor]]:
613
- """Retrieve devices sensors states."""
614
- devices_sensors: dict[str, dict[str, AmazonDeviceSensor]] = {}
615
-
616
- # batch endpoints into groups of 3 to reduce number of requests
617
- endpoint_ids = list(self._endpoints.keys())
618
- batches = [endpoint_ids[i : i + 3] for i in range(0, len(endpoint_ids), 3)]
619
- for endpoint_id_batch in batches:
620
- sensors_state = await self._get_sensors_state(endpoint_id_batch)
621
- _LOGGER.debug("Sensor data - %s", sensors_state)
635
+ if await self._format_human_error(sensors_state):
636
+ # Explicit error in returned data
637
+ return {}
622
638
 
623
- if not isinstance(sensors_state, list) and (
624
- error := sensors_state.get("errors")
625
- ):
626
- if isinstance(error, list):
627
- error = error[0]
628
- msg = error.get("message", "Unknown error")
629
- path = error.get("path", "Unknown path")
630
- _LOGGER.error(
631
- "Error retrieving devices state: %s for path %s", msg, path
632
- )
633
- return {}
639
+ if (
640
+ not (arr := sensors_state.get(ARRAY_WRAPPER))
641
+ or not (data := arr[0].get("data"))
642
+ or not (endpoints_list := data.get("listEndpoints"))
643
+ or not (endpoints := endpoints_list.get("endpoints"))
644
+ ):
645
+ _LOGGER.error("Malformed sensor state data received: %s", sensors_state)
646
+ return {}
634
647
 
635
- for endpoint_data in sensors_state:
636
- if (
637
- not isinstance(endpoint_data, dict)
638
- or not (data := endpoint_data.get("data"))
639
- or not (endpoint := data.get("endpoint"))
640
- ):
641
- _LOGGER.error(
642
- "Malformed sensor state data received: %s", endpoint_data
643
- )
644
- return {}
645
- serial_number = self._endpoints[endpoint.get("endpointId")]
648
+ for endpoint in endpoints:
649
+ serial_number = self._endpoints[endpoint.get("endpointId")]
646
650
 
647
- if serial_number in self._final_devices:
648
- devices_sensors[serial_number] = self._get_device_sensor_state(
649
- endpoint, serial_number
650
- )
651
+ if serial_number in self._final_devices:
652
+ devices_sensors[serial_number] = self._get_device_sensor_state(
653
+ endpoint, serial_number
654
+ )
651
655
 
652
656
  return devices_sensors
653
657
 
@@ -736,7 +740,7 @@ class AmazonEchoApi:
736
740
  endpoint_data = await self._response_to_json(raw_resp)
737
741
 
738
742
  if not (data := endpoint_data.get("data")) or not data.get("listEndpoints"):
739
- _LOGGER.error("Malformed endpoint data received: %s", endpoint_data)
743
+ await self._format_human_error(endpoint_data)
740
744
  return {}
741
745
 
742
746
  endpoints = data["listEndpoints"]
@@ -760,12 +764,173 @@ class AmazonEchoApi:
760
764
  if not data:
761
765
  _LOGGER.warning("Empty JSON data received")
762
766
  data = {}
767
+ if isinstance(data, list):
768
+ # if anonymous array is returned wrap it inside
769
+ # generated key to convert list to dict
770
+ data = {ARRAY_WRAPPER: data}
763
771
  return cast("dict[str, Any]", data)
764
772
  except ContentTypeError as exc:
765
773
  raise ValueError("Response not in JSON format") from exc
766
774
  except orjson.JSONDecodeError as exc:
767
775
  raise ValueError("Response with corrupted JSON format") from exc
768
776
 
777
+ async def _get_notifications(self) -> dict[str, dict[str, AmazonSchedule]]:
778
+ final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
779
+
780
+ _, raw_resp = await self._session_request(
781
+ HTTPMethod.GET,
782
+ url=f"https://alexa.amazon.{self._domain}{URI_NOTIFICATIONS}",
783
+ )
784
+ notifications = await self._response_to_json(raw_resp)
785
+ for schedule in notifications["notifications"]:
786
+ schedule_type: str = schedule["type"]
787
+ schedule_device_serial = schedule["deviceSerialNumber"]
788
+ if schedule_type == NOTIFICATION_MUSIC_ALARM:
789
+ # Structure is the same as standard Alarm
790
+ schedule_type = NOTIFICATION_ALARM
791
+ schedule["type"] = NOTIFICATION_ALARM
792
+ label_desc = schedule_type.lower() + "Label"
793
+ if (schedule_status := schedule["status"]) == "ON" and (
794
+ next_occurrence := await self._parse_next_occurence(schedule)
795
+ ):
796
+ schedule_notification_list = final_notifications.get(
797
+ schedule_device_serial, {}
798
+ )
799
+ schedule_notification_by_type = schedule_notification_list.get(
800
+ schedule_type
801
+ )
802
+ # Replace if no existing notification
803
+ # or if existing.next_occurrence is None
804
+ # or if new next_occurrence is earlier
805
+ if (
806
+ not schedule_notification_by_type
807
+ or schedule_notification_by_type.next_occurrence is None
808
+ or next_occurrence < schedule_notification_by_type.next_occurrence
809
+ ):
810
+ final_notifications.update(
811
+ {
812
+ schedule_device_serial: {
813
+ **schedule_notification_list
814
+ | {
815
+ schedule_type: AmazonSchedule(
816
+ type=schedule_type,
817
+ status=schedule_status,
818
+ label=schedule[label_desc],
819
+ next_occurrence=next_occurrence,
820
+ ),
821
+ }
822
+ }
823
+ }
824
+ )
825
+
826
+ return final_notifications
827
+
828
+ async def _parse_next_occurence(
829
+ self,
830
+ schedule: dict[str, Any],
831
+ ) -> datetime | None:
832
+ """Parse RFC5545 rule set for next iteration."""
833
+ # Local timezone
834
+ tzinfo = datetime.now().astimezone().tzinfo
835
+ # Current time
836
+ actual_time = datetime.now(tz=tzinfo)
837
+ # Reference start date
838
+ today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
839
+ # Reference time (1 minute ago to avoid edge cases)
840
+ now_reference = actual_time - timedelta(minutes=1)
841
+
842
+ # Schedule data
843
+ original_date = schedule.get("originalDate")
844
+ original_time = schedule.get("originalTime")
845
+
846
+ recurring_rules: list[str] = []
847
+ if schedule.get("rRuleData"):
848
+ recurring_rules = schedule["rRuleData"]["recurrenceRules"]
849
+ if schedule.get("recurringPattern"):
850
+ recurring_rules.append(schedule["recurringPattern"])
851
+
852
+ # Recurring events
853
+ if recurring_rules:
854
+ next_candidates: list[datetime] = []
855
+ for recurring_rule in recurring_rules:
856
+ # Already in RFC5545 format
857
+ if "FREQ=" in recurring_rule:
858
+ rule = await self._add_hours_minutes(recurring_rule, original_time)
859
+
860
+ # Add date to candidates list
861
+ next_candidates.append(
862
+ rrulestr(rule, dtstart=today_midnight).after(
863
+ now_reference, True
864
+ ),
865
+ )
866
+ continue
867
+
868
+ if recurring_rule not in RECURRING_PATTERNS:
869
+ _LOGGER.warning("Unknown recurring rule: %s", recurring_rule)
870
+ return None
871
+
872
+ # Adjust recurring rules for country specific weekend exceptions
873
+ recurring_pattern = RECURRING_PATTERNS.copy()
874
+ for group, countries in COUNTRY_GROUPS.items():
875
+ if self._country_code in countries:
876
+ recurring_pattern |= WEEKEND_EXCEPTIONS[group]
877
+ break
878
+
879
+ rule = await self._add_hours_minutes(
880
+ recurring_pattern[recurring_rule], original_time
881
+ )
882
+
883
+ # Add date to candidates list
884
+ next_candidates.append(
885
+ rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
886
+ )
887
+
888
+ return min(next_candidates) if next_candidates else None
889
+
890
+ # Single events
891
+ if schedule["type"] == NOTIFICATION_ALARM:
892
+ timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
893
+
894
+ elif schedule["type"] == NOTIFICATION_TIMER:
895
+ # API returns triggerTime in milliseconds since epoch
896
+ timestamp = datetime.fromtimestamp(
897
+ schedule["triggerTime"] / 1000, tz=tzinfo
898
+ )
899
+
900
+ elif schedule["type"] == NOTIFICATION_REMINDER:
901
+ # API returns alarmTime in milliseconds since epoch
902
+ timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
903
+
904
+ else:
905
+ _LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
906
+ return None
907
+
908
+ if timestamp > now_reference:
909
+ return timestamp
910
+
911
+ return None
912
+
913
+ async def _add_hours_minutes(
914
+ self,
915
+ recurring_rule: str,
916
+ original_time: str | None,
917
+ ) -> str:
918
+ """Add hours and minutes to a RFC5545 string."""
919
+ rule = recurring_rule.removesuffix(";")
920
+
921
+ if not original_time:
922
+ return rule
923
+
924
+ # Add missing BYHOUR, BYMINUTE if needed (Alarms only)
925
+ if "BYHOUR=" not in recurring_rule:
926
+ hour = int(original_time.split(":")[0])
927
+ rule += f";BYHOUR={hour}"
928
+ if "BYMINUTE=" not in recurring_rule:
929
+ minute = int(original_time.split(":")[1])
930
+ rule += f";BYMINUTE={minute}"
931
+
932
+ return rule
933
+
769
934
  async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
770
935
  """Login to Amazon interactively via OTP."""
771
936
  _LOGGER.debug(
@@ -965,6 +1130,7 @@ class AmazonEchoApi:
965
1130
  async def _get_sensor_data(self) -> None:
966
1131
  devices_sensors = await self._get_sensors_states()
967
1132
  dnd_sensors = await self._get_dnd_status()
1133
+ notifications = await self._get_notifications()
968
1134
  for device in self._final_devices.values():
969
1135
  # Update sensors
970
1136
  sensors = devices_sensors.get(device.serial_number, {})
@@ -976,6 +1142,26 @@ class AmazonEchoApi:
976
1142
  if device_dnd := dnd_sensors.get(device.serial_number):
977
1143
  device.sensors["dnd"] = device_dnd
978
1144
 
1145
+ # Update notifications
1146
+ device_notifications = notifications.get(device.serial_number, {})
1147
+
1148
+ # Add only supported notification types
1149
+ for capability, notification_type in [
1150
+ ("REMINDERS", NOTIFICATION_REMINDER),
1151
+ ("TIMERS_AND_ALARMS", NOTIFICATION_ALARM),
1152
+ ("TIMERS_AND_ALARMS", NOTIFICATION_TIMER),
1153
+ ]:
1154
+ if (
1155
+ capability in device.capabilities
1156
+ and notification_type in device_notifications
1157
+ and (
1158
+ notification_object := device_notifications.get(
1159
+ notification_type
1160
+ )
1161
+ )
1162
+ ):
1163
+ device.notifications[notification_type] = notification_object
1164
+
979
1165
  async def _set_device_endpoints_data(self) -> None:
980
1166
  """Set device endpoint data."""
981
1167
  devices_endpoints = await self._get_devices_endpoint_data()
@@ -1039,6 +1225,7 @@ class AmazonEchoApi:
1039
1225
  entity_id=None,
1040
1226
  endpoint_id=None,
1041
1227
  sensors={},
1228
+ notifications={},
1042
1229
  )
1043
1230
 
1044
1231
  self._list_for_clusters.update(
@@ -1375,3 +1562,18 @@ class AmazonEchoApi:
1375
1562
  scale=None,
1376
1563
  )
1377
1564
  return dnd_status
1565
+
1566
+ async def _format_human_error(self, sensors_state: dict) -> bool:
1567
+ """Format human readable error from malformed data."""
1568
+ if sensors_state.get(ARRAY_WRAPPER):
1569
+ error = sensors_state[ARRAY_WRAPPER][0].get("errors", [])
1570
+ else:
1571
+ error = sensors_state.get("errors", [])
1572
+
1573
+ if not error:
1574
+ return False
1575
+
1576
+ msg = error[0].get("message", "Unknown error")
1577
+ path = error[0].get("path", "Unknown path")
1578
+ _LOGGER.error("Error retrieving devices state: %s for path %s", msg, path)
1579
+ return True
@@ -4,6 +4,8 @@ import logging
4
4
 
5
5
  _LOGGER = logging.getLogger(__package__)
6
6
 
7
+ ARRAY_WRAPPER = "generatedArrayWrapper"
8
+
7
9
  HTTP_ERROR_199 = 199
8
10
  HTTP_ERROR_299 = 299
9
11
 
@@ -54,6 +56,7 @@ REFRESH_AUTH_COOKIES = "auth_cookies"
54
56
 
55
57
  URI_DEVICES = "/api/devices-v2/device"
56
58
  URI_DND = "/api/dnd/device-status-list"
59
+ URI_NOTIFICATIONS = "/api/notifications"
57
60
  URI_SIGNIN = "/ap/signin"
58
61
  URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
59
62
 
@@ -477,3 +480,57 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
477
480
  "hw_version": "Gen2",
478
481
  },
479
482
  }
483
+
484
+ RECURRING_PATTERNS: dict[str, str] = {
485
+ "XXXX-WD": "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
486
+ "XXXX-WE": "FREQ=WEEKLY;BYDAY=SA,SU",
487
+ "XXXX-WXX-1": "FREQ=WEEKLY;BYDAY=MO",
488
+ "XXXX-WXX-2": "FREQ=WEEKLY;BYDAY=TU",
489
+ "XXXX-WXX-3": "FREQ=WEEKLY;BYDAY=WE",
490
+ "XXXX-WXX-4": "FREQ=WEEKLY;BYDAY=TH",
491
+ "XXXX-WXX-5": "FREQ=WEEKLY;BYDAY=FR",
492
+ "XXXX-WXX-6": "FREQ=WEEKLY;BYDAY=SA",
493
+ "XXXX-WXX-7": "FREQ=WEEKLY;BYDAY=SU",
494
+ }
495
+
496
+ WEEKEND_EXCEPTIONS = {
497
+ "TH-FR": {
498
+ "XXXX-WD": "FREQ=WEEKLY;BYDAY=MO,TU,WE,SA,SU",
499
+ "XXXX-WE": "FREQ=WEEKLY;BYDAY=TH,FR",
500
+ },
501
+ "FR-SA": {
502
+ "XXXX-WD": "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,SU",
503
+ "XXXX-WE": "FREQ=WEEKLY;BYDAY=FR,SA",
504
+ },
505
+ }
506
+
507
+ # Countries grouped by their weekend type
508
+ COUNTRY_GROUPS = {
509
+ "TH-FR": ["IR"],
510
+ "FR-SA": [
511
+ "AF",
512
+ "BD",
513
+ "BH",
514
+ "DZ",
515
+ "EG",
516
+ "IL",
517
+ "IQ",
518
+ "JO",
519
+ "KW",
520
+ "LY",
521
+ "MV",
522
+ "MY",
523
+ "OM",
524
+ "PS",
525
+ "QA",
526
+ "SA",
527
+ "SD",
528
+ "SY",
529
+ "YE",
530
+ ],
531
+ }
532
+
533
+ NOTIFICATION_ALARM = "Alarm"
534
+ NOTIFICATION_MUSIC_ALARM = "MusicAlarm"
535
+ NOTIFICATION_REMINDER = "Reminder"
536
+ NOTIFICATION_TIMER = "Timer"
@@ -41,44 +41,57 @@ query getDevicesBaseData {
41
41
  """
42
42
 
43
43
  QUERY_SENSOR_STATE = """
44
- query getEndpointState($endpointId: String!, $latencyTolerance: LatencyToleranceValue) {
45
- endpoint(id: $endpointId) {
46
- endpointId: id
47
- features(latencyToleranceValue: $latencyTolerance) {
44
+ fragment EndpointState on Endpoint {
45
+ endpointId: id
46
+ friendlyNameObject { value { text } }
47
+ features {
48
+ name
49
+ properties {
48
50
  name
49
- instance
50
- properties {
51
+ type
52
+ accuracy
53
+ error { type message }
54
+ __typename
55
+ ... on Illuminance {
56
+ illuminanceValue { value }
57
+ timeOfSample
58
+ timeOfLastChange
59
+ }
60
+ ... on Reachability {
61
+ reachabilityStatusValue
62
+ timeOfSample
63
+ timeOfLastChange
64
+ }
65
+ ... on DetectionState {
66
+ detectionStateValue
67
+ timeOfSample
68
+ timeOfLastChange
69
+ }
70
+ ... on TemperatureSensor {
51
71
  name
52
- type
53
- accuracy
54
- error { type message }
55
- __typename
56
- ... on Illuminance {
57
- illuminanceValue { value }
58
- timeOfSample
59
- timeOfLastChange
60
- }
61
- ... on Reachability {
62
- reachabilityStatusValue
63
- timeOfSample
64
- timeOfLastChange
65
- }
66
- ... on DetectionState {
67
- detectionStateValue
68
- timeOfSample
69
- timeOfLastChange
70
- }
71
- ... on TemperatureSensor {
72
- name
73
- value {
74
- value
75
- scale
76
- }
77
- timeOfSample
78
- timeOfLastChange
72
+ value {
73
+ value
74
+ scale
79
75
  }
76
+ timeOfSample
77
+ timeOfLastChange
80
78
  }
81
79
  }
82
80
  }
83
81
  }
82
+
83
+
84
+ query getEndpointState($endpointIds: [String]!) {
85
+ listEndpoints(
86
+ listEndpointsInput: {
87
+ latencyTolerance: LOW,
88
+ endpointIds: $endpointIds,
89
+ includeHouseholdDevices: true
90
+ }
91
+ ) {
92
+ endpoints {
93
+ ...EndpointState
94
+ }
95
+ }
96
+ }
84
97
  """