1""":rfc:`5545` VALARM component."""
2
3from __future__ import annotations
4
5from datetime import date, datetime, timedelta
6from typing import TYPE_CHECKING, NamedTuple, Optional, Union
7
8from icalendar.attr import (
9 attendees_property,
10 create_single_property,
11 description_property,
12 property_del_duration,
13 property_get_duration,
14 property_set_duration,
15 single_int_property,
16 single_string_property,
17 single_utc_property,
18 summary_property,
19 uid_property,
20)
21from icalendar.cal.component import Component
22
23if TYPE_CHECKING:
24 import uuid
25
26 from icalendar.prop import vCalAddress
27
28
29class Alarm(Component):
30 """
31 A "VALARM" calendar component is a grouping of component
32 properties that defines an alarm or reminder for an event or a
33 to-do. For example, it may be used to define a reminder for a
34 pending event or an overdue to-do.
35 """
36
37 name = "VALARM"
38 # some properties MAY/MUST/MUST NOT appear depending on ACTION value
39 required = (
40 "ACTION",
41 "TRIGGER",
42 )
43 singletons = (
44 "ATTACH",
45 "ACTION",
46 "DESCRIPTION",
47 "SUMMARY",
48 "TRIGGER",
49 "DURATION",
50 "REPEAT",
51 "UID",
52 "PROXIMITY",
53 "ACKNOWLEDGED",
54 )
55 inclusive = (
56 (
57 "DURATION",
58 "REPEAT",
59 ),
60 (
61 "SUMMARY",
62 "ATTENDEE",
63 ),
64 )
65 multiple = ("ATTENDEE", "ATTACH", "RELATED-TO")
66
67 REPEAT = single_int_property(
68 "REPEAT",
69 0,
70 """The REPEAT property of an alarm component.
71
72 The alarm can be defined such that it triggers repeatedly. A
73 definition of an alarm with a repeating trigger MUST include both
74 the "DURATION" and "REPEAT" properties. The "DURATION" property
75 specifies the delay period, after which the alarm will repeat.
76 The "REPEAT" property specifies the number of additional
77 repetitions that the alarm will be triggered. This repetition
78 count is in addition to the initial triggering of the alarm.
79 """,
80 )
81
82 DURATION = property(
83 property_get_duration,
84 property_set_duration,
85 property_del_duration,
86 """The DURATION property of an alarm component.
87
88 The alarm can be defined such that it triggers repeatedly. A
89 definition of an alarm with a repeating trigger MUST include both
90 the "DURATION" and "REPEAT" properties. The "DURATION" property
91 specifies the delay period, after which the alarm will repeat.
92 """,
93 )
94
95 ACKNOWLEDGED = single_utc_property(
96 "ACKNOWLEDGED",
97 """This is defined in RFC 9074:
98
99 Purpose: This property specifies the UTC date and time at which the
100 corresponding alarm was last sent or acknowledged.
101
102 This property is used to specify when an alarm was last sent or acknowledged.
103 This allows clients to determine when a pending alarm has been acknowledged
104 by a calendar user so that any alerts can be dismissed across multiple devices.
105 It also allows clients to track repeating alarms or alarms on recurring events or
106 to-dos to ensure that the right number of missed alarms can be tracked.
107
108 Clients SHOULD set this property to the current date-time value in UTC
109 when a calendar user acknowledges a pending alarm. Certain kinds of alarms,
110 such as email-based alerts, might not provide feedback as to when the calendar user
111 sees them. For those kinds of alarms, the client SHOULD set this property
112 when the alarm is triggered and the action is successfully carried out.
113
114 When an alarm is triggered on a client, clients can check to see if an "ACKNOWLEDGED"
115 property is present. If it is, and the value of that property is greater than or
116 equal to the computed trigger time for the alarm, then the client SHOULD NOT trigger
117 the alarm. Similarly, if an alarm has been triggered and
118 an "alert" has been presented to a calendar user, clients can monitor
119 the iCalendar data to determine whether an "ACKNOWLEDGED" property is added or
120 changed in the alarm component. If the value of any "ACKNOWLEDGED" property
121 in the alarm changes and is greater than or equal to the trigger time of the alarm,
122 then clients SHOULD dismiss or cancel any "alert" presented to the calendar user.
123 """, # noqa: E501
124 )
125
126 TRIGGER = create_single_property(
127 "TRIGGER",
128 "dt",
129 (datetime, timedelta),
130 Optional[Union[timedelta, datetime]],
131 """Purpose: This property specifies when an alarm will trigger.
132
133 Value Type: The default value type is DURATION. The value type can
134 be set to a DATE-TIME value type, in which case the value MUST
135 specify a UTC-formatted DATE-TIME value.
136
137 Either a positive or negative duration may be specified for the
138 "TRIGGER" property. An alarm with a positive duration is
139 triggered after the associated start or end of the event or to-do.
140 An alarm with a negative duration is triggered before the
141 associated start or end of the event or to-do.""",
142 )
143
144 @property
145 def TRIGGER_RELATED(self) -> str: # noqa: N802
146 """The RELATED parameter of the TRIGGER property.
147
148 Values are either "START" (default) or "END".
149
150 A value of START will set the alarm to trigger off the
151 start of the associated event or to-do. A value of END will set
152 the alarm to trigger off the end of the associated event or to-do.
153
154 In this example, we create an alarm that triggers two hours after the
155 end of its parent component:
156
157 >>> from icalendar import Alarm
158 >>> from datetime import timedelta
159 >>> alarm = Alarm()
160 >>> alarm.TRIGGER = timedelta(hours=2)
161 >>> alarm.TRIGGER_RELATED = "END"
162 """
163 trigger = self.get("TRIGGER")
164 if trigger is None:
165 return "START"
166 return trigger.params.get("RELATED", "START")
167
168 @TRIGGER_RELATED.setter
169 def TRIGGER_RELATED(self, value: str): # noqa: N802
170 """Set "START" or "END"."""
171 trigger = self.get("TRIGGER")
172 if trigger is None:
173 raise ValueError(
174 "You must set a TRIGGER before setting the RELATED parameter."
175 )
176 trigger.params["RELATED"] = value
177
178 class Triggers(NamedTuple):
179 """The computed times of alarm triggers.
180
181 start - triggers relative to the start of the Event or Todo (timedelta)
182
183 end - triggers relative to the end of the Event or Todo (timedelta)
184
185 absolute - triggers at a datetime in UTC
186 """
187
188 start: tuple[timedelta]
189 end: tuple[timedelta]
190 absolute: tuple[datetime]
191
192 @property
193 def triggers(self):
194 """The computed triggers of an Alarm.
195
196 This takes the TRIGGER, DURATION and REPEAT properties into account.
197
198 Here, we create an alarm that triggers 3 times before the start of the
199 parent component:
200
201 >>> from icalendar import Alarm
202 >>> from datetime import timedelta
203 >>> alarm = Alarm()
204 >>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours before START
205 >>> alarm.DURATION = timedelta(hours=1) # after 1 hour trigger again
206 >>> alarm.REPEAT = 2 # trigger 2 more times
207 >>> alarm.triggers.start == (timedelta(hours=-4), timedelta(hours=-3), timedelta(hours=-2))
208 True
209 >>> alarm.triggers.end
210 ()
211 >>> alarm.triggers.absolute
212 ()
213 """ # noqa: E501
214 start = []
215 end = []
216 absolute = []
217 trigger = self.TRIGGER
218 if trigger is not None:
219 if isinstance(trigger, date):
220 absolute.append(trigger)
221 add = absolute
222 elif self.TRIGGER_RELATED == "START":
223 start.append(trigger)
224 add = start
225 else:
226 end.append(trigger)
227 add = end
228 duration = self.DURATION
229 if duration is not None:
230 for _ in range(self.REPEAT):
231 add.append(add[-1] + duration)
232 return self.Triggers(
233 start=tuple(start), end=tuple(end), absolute=tuple(absolute)
234 )
235
236 uid = single_string_property(
237 "UID",
238 uid_property.__doc__,
239 "X-ALARMUID",
240 )
241 summary = summary_property
242 description = description_property
243 attendees = attendees_property
244
245 @classmethod
246 def new(
247 cls,
248 /,
249 attendees: Optional[list[vCalAddress]] = None,
250 description: Optional[str] = None,
251 summary: Optional[str] = None,
252 uid: Optional[str | uuid.UUID] = None,
253 ):
254 """Create a new alarm with all required properties.
255
256 This creates a new Alarm in accordance with :rfc:`5545`.
257
258 Arguments:
259 attendees: The :attr:`attendees` of the alarm.
260 description: The :attr:`description` of the alarm.
261 summary: The :attr:`summary` of the alarm.
262 uid: The :attr:`uid` of the alarm.
263
264 Returns:
265 :class:`Alarm`
266
267 Raises:
268 InvalidCalendar: If the content is not valid according to :rfc:`5545`.
269
270 .. warning:: As time progresses, we will be stricter with the validation.
271 """
272 alarm = super().new()
273 alarm.summary = summary
274 alarm.description = description
275 alarm.uid = uid
276 alarm.attendees = attendees
277 return alarm
278
279
280__all__ = ["Alarm"]