aioamazondevices 6.4.6__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.6"
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(
@@ -965,6 +1144,7 @@ class AmazonEchoApi:
965
1144
  async def _get_sensor_data(self) -> None:
966
1145
  devices_sensors = await self._get_sensors_states()
967
1146
  dnd_sensors = await self._get_dnd_status()
1147
+ notifications = await self._get_notifications()
968
1148
  for device in self._final_devices.values():
969
1149
  # Update sensors
970
1150
  sensors = devices_sensors.get(device.serial_number, {})
@@ -976,6 +1156,26 @@ class AmazonEchoApi:
976
1156
  if device_dnd := dnd_sensors.get(device.serial_number):
977
1157
  device.sensors["dnd"] = device_dnd
978
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
+
979
1179
  async def _set_device_endpoints_data(self) -> None:
980
1180
  """Set device endpoint data."""
981
1181
  devices_endpoints = await self._get_devices_endpoint_data()
@@ -1039,6 +1239,7 @@ class AmazonEchoApi:
1039
1239
  entity_id=None,
1040
1240
  endpoint_id=None,
1041
1241
  sensors={},
1242
+ notifications={},
1042
1243
  )
1043
1244
 
1044
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.6
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=G3JnYhlhatrCtpn_93BQbFjsfMMhklpDM4ACRD_F0Jw,276
2
- aioamazondevices/api.py,sha256=VaOGSfUmpy8hRF7vISGoalZbyn2RqRpelDN9LepkPbw,50685
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.6.dist-info/METADATA,sha256=e7_GTeYgN9fJp_P4o2Spft3iE2oY78DDaQtpQlakT4c,7648
10
- aioamazondevices-6.4.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
11
- aioamazondevices-6.4.6.dist-info/licenses/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
12
- aioamazondevices-6.4.6.dist-info/RECORD,,