aioamazondevices 6.4.5__py3-none-any.whl → 6.5.0__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.

Potentially problematic release.


This version of aioamazondevices might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "6.4.5"
3
+ __version__ = "6.5.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
aioamazondevices/api.py CHANGED
@@ -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
@@ -39,6 +41,7 @@ from .const import (
39
41
  AMAZON_DEVICE_SOFTWARE_VERSION,
40
42
  AMAZON_DEVICE_TYPE,
41
43
  BIN_EXTENSION,
44
+ COUNTRY_GROUPS,
42
45
  CSRF_COOKIE,
43
46
  DEFAULT_HEADERS,
44
47
  DEFAULT_SITE,
@@ -48,6 +51,11 @@ from .const import (
48
51
  HTTP_ERROR_199,
49
52
  HTTP_ERROR_299,
50
53
  JSON_EXTENSION,
54
+ NOTIFICATION_ALARM,
55
+ NOTIFICATION_MUSIC_ALARM,
56
+ NOTIFICATION_REMINDER,
57
+ NOTIFICATION_TIMER,
58
+ RECURRING_PATTERNS,
51
59
  REFRESH_ACCESS_TOKEN,
52
60
  REFRESH_AUTH_COOKIES,
53
61
  REQUEST_AGENT,
@@ -56,7 +64,9 @@ from .const import (
56
64
  URI_DEVICES,
57
65
  URI_DND,
58
66
  URI_NEXUS_GRAPHQL,
67
+ URI_NOTIFICATIONS,
59
68
  URI_SIGNIN,
69
+ WEEKEND_EXCEPTIONS,
60
70
  )
61
71
  from .exceptions import (
62
72
  CannotAuthenticate,
@@ -81,6 +91,16 @@ class AmazonDeviceSensor:
81
91
  scale: str | None
82
92
 
83
93
 
94
+ @dataclass
95
+ class AmazonSchedule:
96
+ """Amazon schedule class."""
97
+
98
+ type: str # alarm, reminder, timer
99
+ status: str
100
+ label: str
101
+ next_occurrence: datetime | None
102
+
103
+
84
104
  @dataclass
85
105
  class AmazonDevice:
86
106
  """Amazon device class."""
@@ -98,6 +118,7 @@ class AmazonDevice:
98
118
  entity_id: str | None
99
119
  endpoint_id: str | None
100
120
  sensors: dict[str, AmazonDeviceSensor]
121
+ notifications: dict[str, AmazonSchedule]
101
122
 
102
123
 
103
124
  class AmazonSequenceType(StrEnum):
@@ -173,6 +194,7 @@ class AmazonEchoApi:
173
194
  lang_object = Language.make(territory=country_code.upper())
174
195
  lang_maximized = lang_object.maximize()
175
196
 
197
+ self._country_code: str = country_code
176
198
  self._domain: str = domain
177
199
  language = f"{lang_maximized.language}-{lang_maximized.territory}"
178
200
  self._language = standardize_tag(language)
@@ -766,6 +788,163 @@ class AmazonEchoApi:
766
788
  except orjson.JSONDecodeError as exc:
767
789
  raise ValueError("Response with corrupted JSON format") from exc
768
790
 
791
+ async def _get_notifications(self) -> dict[str, dict[str, AmazonSchedule]]:
792
+ final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
793
+
794
+ _, raw_resp = await self._session_request(
795
+ HTTPMethod.GET,
796
+ url=f"https://alexa.amazon.{self._domain}{URI_NOTIFICATIONS}",
797
+ )
798
+ notifications = await self._response_to_json(raw_resp)
799
+ for schedule in notifications["notifications"]:
800
+ schedule_type: str = schedule["type"]
801
+ schedule_device_serial = schedule["deviceSerialNumber"]
802
+ if schedule_type == NOTIFICATION_MUSIC_ALARM:
803
+ # Structure is the same as standard Alarm
804
+ schedule_type = NOTIFICATION_ALARM
805
+ schedule["type"] = NOTIFICATION_ALARM
806
+ label_desc = schedule_type.lower() + "Label"
807
+ if (schedule_status := schedule["status"]) == "ON" and (
808
+ next_occurrence := await self._parse_next_occurence(schedule)
809
+ ):
810
+ schedule_notification_list = final_notifications.get(
811
+ schedule_device_serial, {}
812
+ )
813
+ schedule_notification_by_type = schedule_notification_list.get(
814
+ schedule_type
815
+ )
816
+ # Replace if no existing notification
817
+ # or if existing.next_occurrence is None
818
+ # or if new next_occurrence is earlier
819
+ if (
820
+ not schedule_notification_by_type
821
+ or schedule_notification_by_type.next_occurrence is None
822
+ or next_occurrence < schedule_notification_by_type.next_occurrence
823
+ ):
824
+ final_notifications.update(
825
+ {
826
+ schedule_device_serial: {
827
+ **schedule_notification_list
828
+ | {
829
+ schedule_type: AmazonSchedule(
830
+ type=schedule_type,
831
+ status=schedule_status,
832
+ label=schedule[label_desc],
833
+ next_occurrence=next_occurrence,
834
+ ),
835
+ }
836
+ }
837
+ }
838
+ )
839
+
840
+ return final_notifications
841
+
842
+ async def _parse_next_occurence(
843
+ self,
844
+ schedule: dict[str, Any],
845
+ ) -> datetime | None:
846
+ """Parse RFC5545 rule set for next iteration."""
847
+ # Local timezone
848
+ tzinfo = datetime.now().astimezone().tzinfo
849
+ # Current time
850
+ actual_time = datetime.now(tz=tzinfo)
851
+ # Reference start date
852
+ today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
853
+ # Reference time (1 minute ago to avoid edge cases)
854
+ now_reference = actual_time - timedelta(minutes=1)
855
+
856
+ # Schedule data
857
+ original_date = schedule.get("originalDate")
858
+ original_time = schedule.get("originalTime")
859
+
860
+ recurring_rules: list[str] = []
861
+ if schedule.get("rRuleData"):
862
+ recurring_rules = schedule["rRuleData"]["recurrenceRules"]
863
+ if schedule.get("recurringPattern"):
864
+ recurring_rules.append(schedule["recurringPattern"])
865
+
866
+ # Recurring events
867
+ if recurring_rules:
868
+ next_candidates: list[datetime] = []
869
+ for recurring_rule in recurring_rules:
870
+ # Already in RFC5545 format
871
+ if "FREQ=" in recurring_rule:
872
+ rule = await self._add_hours_minutes(recurring_rule, original_time)
873
+
874
+ # Add date to candidates list
875
+ next_candidates.append(
876
+ rrulestr(rule, dtstart=today_midnight).after(
877
+ now_reference, True
878
+ ),
879
+ )
880
+ continue
881
+
882
+ if recurring_rule not in RECURRING_PATTERNS:
883
+ _LOGGER.warning("Unknown recurring rule: %s", recurring_rule)
884
+ return None
885
+
886
+ # Adjust recurring rules for country specific weekend exceptions
887
+ recurring_pattern = RECURRING_PATTERNS.copy()
888
+ for group, countries in COUNTRY_GROUPS.items():
889
+ if self._country_code in countries:
890
+ recurring_pattern |= WEEKEND_EXCEPTIONS[group]
891
+ break
892
+
893
+ rule = await self._add_hours_minutes(
894
+ recurring_pattern[recurring_rule], original_time
895
+ )
896
+
897
+ # Add date to candidates list
898
+ next_candidates.append(
899
+ rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
900
+ )
901
+
902
+ return min(next_candidates) if next_candidates else None
903
+
904
+ # Single events
905
+ if schedule["type"] == NOTIFICATION_ALARM:
906
+ timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
907
+
908
+ elif schedule["type"] == NOTIFICATION_TIMER:
909
+ # API returns triggerTime in milliseconds since epoch
910
+ timestamp = datetime.fromtimestamp(
911
+ schedule["triggerTime"] / 1000, tz=tzinfo
912
+ )
913
+
914
+ elif schedule["type"] == NOTIFICATION_REMINDER:
915
+ # API returns alarmTime in milliseconds since epoch
916
+ timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
917
+
918
+ else:
919
+ _LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
920
+ return None
921
+
922
+ if timestamp > now_reference:
923
+ return timestamp
924
+
925
+ return None
926
+
927
+ async def _add_hours_minutes(
928
+ self,
929
+ recurring_rule: str,
930
+ original_time: str | None,
931
+ ) -> str:
932
+ """Add hours and minutes to a RFC5545 string."""
933
+ rule = recurring_rule.removesuffix(";")
934
+
935
+ if not original_time:
936
+ return rule
937
+
938
+ # Add missing BYHOUR, BYMINUTE if needed (Alarms only)
939
+ if "BYHOUR=" not in recurring_rule:
940
+ hour = int(original_time.split(":")[0])
941
+ rule += f";BYHOUR={hour}"
942
+ if "BYMINUTE=" not in recurring_rule:
943
+ minute = int(original_time.split(":")[1])
944
+ rule += f";BYMINUTE={minute}"
945
+
946
+ return rule
947
+
769
948
  async def login_mode_interactive(self, otp_code: str) -> dict[str, Any]:
770
949
  """Login to Amazon interactively via OTP."""
771
950
  _LOGGER.debug(
@@ -943,8 +1122,13 @@ class AmazonEchoApi:
943
1122
  await self._get_base_devices()
944
1123
  self._last_devices_refresh = datetime.now(UTC)
945
1124
 
1125
+ # Only refresh endpoint data if we have no endpoints yet
946
1126
  delta_endpoints = datetime.now(UTC) - self._last_endpoint_refresh
947
- if delta_endpoints >= timedelta(minutes=30):
1127
+ endpoint_refresh_needed = delta_endpoints >= timedelta(days=1)
1128
+ endpoints_recently_checked = delta_endpoints < timedelta(minutes=30)
1129
+ if (
1130
+ not self._endpoints and not endpoints_recently_checked
1131
+ ) or endpoint_refresh_needed:
948
1132
  _LOGGER.debug(
949
1133
  "Refreshing endpoint data after %s",
950
1134
  str(timedelta(minutes=round(delta_endpoints.total_seconds() / 60))),
@@ -960,6 +1144,7 @@ class AmazonEchoApi:
960
1144
  async def _get_sensor_data(self) -> None:
961
1145
  devices_sensors = await self._get_sensors_states()
962
1146
  dnd_sensors = await self._get_dnd_status()
1147
+ notifications = await self._get_notifications()
963
1148
  for device in self._final_devices.values():
964
1149
  # Update sensors
965
1150
  sensors = devices_sensors.get(device.serial_number, {})
@@ -971,7 +1156,28 @@ class AmazonEchoApi:
971
1156
  if device_dnd := dnd_sensors.get(device.serial_number):
972
1157
  device.sensors["dnd"] = device_dnd
973
1158
 
1159
+ # Update notifications
1160
+ device_notifications = notifications.get(device.serial_number, {})
1161
+
1162
+ # Add only supported notification types
1163
+ for capability, notification_type in [
1164
+ ("REMINDERS", NOTIFICATION_REMINDER),
1165
+ ("TIMERS_AND_ALARMS", NOTIFICATION_ALARM),
1166
+ ("TIMERS_AND_ALARMS", NOTIFICATION_TIMER),
1167
+ ]:
1168
+ if (
1169
+ capability in device.capabilities
1170
+ and notification_type in device_notifications
1171
+ and (
1172
+ notification_object := device_notifications.get(
1173
+ notification_type
1174
+ )
1175
+ )
1176
+ ):
1177
+ device.notifications[notification_type] = notification_object
1178
+
974
1179
  async def _set_device_endpoints_data(self) -> None:
1180
+ """Set device endpoint data."""
975
1181
  devices_endpoints = await self._get_devices_endpoint_data()
976
1182
  for serial_number in self._final_devices:
977
1183
  device_endpoint = devices_endpoints.get(serial_number, {})
@@ -1033,6 +1239,7 @@ class AmazonEchoApi:
1033
1239
  entity_id=None,
1034
1240
  endpoint_id=None,
1035
1241
  sensors={},
1242
+ notifications={},
1036
1243
  )
1037
1244
 
1038
1245
  self._list_for_clusters.update(
aioamazondevices/const.py CHANGED
@@ -54,6 +54,7 @@ REFRESH_AUTH_COOKIES = "auth_cookies"
54
54
 
55
55
  URI_DEVICES = "/api/devices-v2/device"
56
56
  URI_DND = "/api/dnd/device-status-list"
57
+ URI_NOTIFICATIONS = "/api/notifications"
57
58
  URI_SIGNIN = "/ap/signin"
58
59
  URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
59
60
 
@@ -477,3 +478,57 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
477
478
  "hw_version": "Gen2",
478
479
  },
479
480
  }
481
+
482
+ RECURRING_PATTERNS: dict[str, str] = {
483
+ "XXXX-WD": "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
484
+ "XXXX-WE": "FREQ=WEEKLY;BYDAY=SA,SU",
485
+ "XXXX-WXX-1": "FREQ=WEEKLY;BYDAY=MO",
486
+ "XXXX-WXX-2": "FREQ=WEEKLY;BYDAY=TU",
487
+ "XXXX-WXX-3": "FREQ=WEEKLY;BYDAY=WE",
488
+ "XXXX-WXX-4": "FREQ=WEEKLY;BYDAY=TH",
489
+ "XXXX-WXX-5": "FREQ=WEEKLY;BYDAY=FR",
490
+ "XXXX-WXX-6": "FREQ=WEEKLY;BYDAY=SA",
491
+ "XXXX-WXX-7": "FREQ=WEEKLY;BYDAY=SU",
492
+ }
493
+
494
+ WEEKEND_EXCEPTIONS = {
495
+ "TH-FR": {
496
+ "XXXX-WD": "FREQ=WEEKLY;BYDAY=MO,TU,WE,SA,SU",
497
+ "XXXX-WE": "FREQ=WEEKLY;BYDAY=TH,FR",
498
+ },
499
+ "FR-SA": {
500
+ "XXXX-WD": "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,SU",
501
+ "XXXX-WE": "FREQ=WEEKLY;BYDAY=FR,SA",
502
+ },
503
+ }
504
+
505
+ # Countries grouped by their weekend type
506
+ COUNTRY_GROUPS = {
507
+ "TH-FR": ["IR"],
508
+ "FR-SA": [
509
+ "AF",
510
+ "BD",
511
+ "BH",
512
+ "DZ",
513
+ "EG",
514
+ "IL",
515
+ "IQ",
516
+ "JO",
517
+ "KW",
518
+ "LY",
519
+ "MV",
520
+ "MY",
521
+ "OM",
522
+ "PS",
523
+ "QA",
524
+ "SA",
525
+ "SD",
526
+ "SY",
527
+ "YE",
528
+ ],
529
+ }
530
+
531
+ NOTIFICATION_ALARM = "Alarm"
532
+ NOTIFICATION_MUSIC_ALARM = "MusicAlarm"
533
+ NOTIFICATION_REMINDER = "Reminder"
534
+ NOTIFICATION_TIMER = "Timer"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioamazondevices
3
- Version: 6.4.5
3
+ Version: 6.5.0
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
@@ -0,0 +1,12 @@
1
+ aioamazondevices/__init__.py,sha256=T63aexWxTzW2X40gC-0WS_XqR-gWU7pV8ggK1C-xOPM,276
2
+ aioamazondevices/api.py,sha256=5RY7f5mGCpIg3QUxD2ZhjhZ2UwR1IRuuT170aEp1X6U,58561
3
+ aioamazondevices/const.py,sha256=DLjCgIEVBBezEAqlKHNA2KN9JjR-oxr3UCKG1WXdJqQ,13515
4
+ aioamazondevices/exceptions.py,sha256=gRYrxNAJnrV6uRuMx5e76VMvtNKyceXd09q84pDBBrI,638
5
+ aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ aioamazondevices/query.py,sha256=SKn-fXFUnXnCvmKd6IvAGdkFL7sBzhYBEAZ0aZ2ez9E,1800
7
+ aioamazondevices/sounds.py,sha256=CXMDk-KoKVFxBdVAw3MeOClqgpzcVDxvQhFOJp7qX-Y,1896
8
+ aioamazondevices/utils.py,sha256=RzuKRhnq_8ymCoJMoQJ2vBYyuew06RSWpqQWmqdNczE,2019
9
+ aioamazondevices-6.5.0.dist-info/METADATA,sha256=ehwDX3IxhNr57zmjOw4i4EiBP01bBblB64kfZG2iBwk,7679
10
+ aioamazondevices-6.5.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
11
+ aioamazondevices-6.5.0.dist-info/licenses/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
12
+ aioamazondevices-6.5.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- aioamazondevices/__init__.py,sha256=OFf1p25xoL4BCdFQmpphXf541iLmm0Py8LC_rJYPL98,276
2
- aioamazondevices/api.py,sha256=uTWsZagMQDZ4I-v8lpk9xCTebxUTMDXMU6JE4NANhPc,50367
3
- aioamazondevices/const.py,sha256=BZTyUku94uQa50R1ZeXo1h585xgUNT_Pb7KAifjawWc,12266
4
- aioamazondevices/exceptions.py,sha256=gRYrxNAJnrV6uRuMx5e76VMvtNKyceXd09q84pDBBrI,638
5
- aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- aioamazondevices/query.py,sha256=SKn-fXFUnXnCvmKd6IvAGdkFL7sBzhYBEAZ0aZ2ez9E,1800
7
- aioamazondevices/sounds.py,sha256=CXMDk-KoKVFxBdVAw3MeOClqgpzcVDxvQhFOJp7qX-Y,1896
8
- aioamazondevices/utils.py,sha256=RzuKRhnq_8ymCoJMoQJ2vBYyuew06RSWpqQWmqdNczE,2019
9
- aioamazondevices-6.4.5.dist-info/METADATA,sha256=k0QHRnM6juLj756-Okw0vq_8FVolrQR7SCF2FF7UdU8,7648
10
- aioamazondevices-6.4.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
11
- aioamazondevices-6.4.5.dist-info/licenses/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
12
- aioamazondevices-6.4.5.dist-info/RECORD,,