added a "Starting on" option to set a starting date for repeated timers
[openblackhole/openblackhole-enigma2.git] / timer.py
1 from bisect import insort
2 from time import strftime, time, localtime, gmtime, mktime
3 from calendar import timegm
4 from enigma import eTimer
5 import calendar
6 import datetime
7
8 class TimerEntry:
9         StateWaiting  = 0
10         StatePrepared = 1
11         StateRunning  = 2
12         StateEnded    = 3
13         
14         def __init__(self, begin, end):
15                 self.begin = begin
16                 self.prepare_time = 20
17                 self.end = end
18                 self.state = 0
19                 self.resetRepeated()
20                 #begindate = localtime(self.begin)
21                 #newdate = datetime.datetime(begindate.tm_year, begindate.tm_mon, begindate.tm_mday 0, 0, 0);
22                 self.repeatedbegindate = begin
23                 self.backoff = 0
24                 
25                 self.disabled = False
26                 
27         def resetRepeated(self):
28                 self.repeated = int(0)
29
30         def setRepeated(self, day):
31                 self.repeated |= (2 ** day)
32                 print "Repeated: " + str(self.repeated)
33                 
34         def isRunning(self):
35                 return self.state == self.StateRunning
36                 
37         def addOneDay(self, timedatestruct):
38                 oldHour = timedatestruct.tm_hour
39                 newdate =  (datetime.datetime(timedatestruct.tm_year, timedatestruct.tm_mon, timedatestruct.tm_mday, timedatestruct.tm_hour, timedatestruct.tm_min, timedatestruct.tm_sec) + datetime.timedelta(days=1)).timetuple()
40                 if localtime(mktime(newdate)).tm_hour != oldHour:
41                         return (datetime.datetime(timedatestruct.tm_year, timedatestruct.tm_mon, timedatestruct.tm_mday, timedatestruct.tm_hour, timedatestruct.tm_min, timedatestruct.tm_sec) + datetime.timedelta(days=2)).timetuple()                        
42                 return newdate
43                 
44         # update self.begin and self.end according to the self.repeated-flags
45         def processRepeated(self, findRunningEvent = True):
46                 print "ProcessRepeated"
47                 if (self.repeated != 0):
48                         now = int(time()) + 1
49
50                         #to avoid problems with daylight saving, we need to calculate with localtime, in struct_time representation
51                         localrepeatedbegindate = localtime(self.repeatedbegindate)
52                         localbegin = localtime(self.begin)
53                         localend = localtime(self.end)
54                         localnow = localtime(now)
55
56                         print "localbegin:", strftime("%c", localbegin)
57                         print "localend:", strftime("%c", localend)
58
59                         day = []
60                         flags = self.repeated
61                         for x in range(0, 7):
62                                 if (flags & 1 == 1):
63                                         day.append(0)
64                                         print "Day: " + str(x)
65                                 else:
66                                         day.append(1)
67                                 flags = flags >> 1
68
69                         print strftime("%c", localnow)
70
71                         # if day is NOT in the list of repeated days
72                         # OR if the day IS in the list of the repeated days, check, if event is currently running... then if findRunningEvent is false, go to the next event
73                         while ((day[localbegin.tm_wday] != 0) or (mktime(localrepeatedbegindate) > mktime(localbegin))  or ((day[localbegin.tm_wday] == 0) and ((findRunningEvent and localend < localnow) or ((not findRunningEvent) and localbegin < localnow)))):
74                                 localbegin = self.addOneDay(localbegin)
75                                 localend = self.addOneDay(localend)
76                                 print "localbegin after addOneDay:", strftime("%c", localbegin)
77                                 print "localend after addOneDay:", strftime("%c", localend)
78                                 
79                         #we now have a struct_time representation of begin and end in localtime, but we have to calculate back to (gmt) seconds since epoch
80                         self.begin = int(mktime(localbegin))
81                         self.end = int(mktime(localend))
82                         if self.begin == self.end:
83                                 self.end += 1
84
85                         print "ProcessRepeated result"
86                         print strftime("%c", localtime(self.begin))
87                         print strftime("%c", localtime(self.end))
88
89                         self.timeChanged()
90
91         def __lt__(self, o):
92                 return self.getNextActivation() < o.getNextActivation()
93         
94         # must be overridden
95         def activate(self):
96                 pass
97                 
98         # can be overridden
99         def timeChanged(self):
100                 pass
101
102         # check if a timer entry must be skipped
103         def shouldSkip(self):
104                 return self.end <= time() and self.state == TimerEntry.StateWaiting
105
106         def abort(self):
107                 self.end = time()
108                 
109                 # in case timer has not yet started, but gets aborted (so it's preparing),
110                 # set begin to now.
111                 if self.begin > self.end:
112                         self.begin = self.end
113
114                 self.cancelled = True
115         
116         # must be overridden!
117         def getNextActivation():
118                 pass
119
120         def disable(self):
121                 self.disabled = True
122         
123         def enable(self):
124                 self.disabled = False
125
126 class Timer:
127         # the time between "polls". We do this because
128         # we want to account for time jumps etc.
129         # of course if they occur <100s before starting,
130         # it's not good. thus, you have to repoll when
131         # you change the time.
132         #
133         # this is just in case. We don't want the timer 
134         # hanging. we use this "edge-triggered-polling-scheme"
135         # anyway, so why don't make it a bit more fool-proof?
136         MaxWaitTime = 100
137
138         def __init__(self):
139                 self.timer_list = [ ]
140                 self.processed_timers = [ ]
141                 
142                 self.timer = eTimer()
143                 self.timer.timeout.get().append(self.calcNextActivation)
144                 self.lastActivation = time()
145                 
146                 self.calcNextActivation()
147                 self.on_state_change = [ ]
148
149         def stateChanged(self, entry):
150                 for f in self.on_state_change:
151                         f(entry)
152
153         def cleanup(self):
154                 self.processed_timers = [entry for entry in self.processed_timers if entry.disabled]
155         
156         def addTimerEntry(self, entry, noRecalc=0):
157                 entry.processRepeated()
158
159                 # when the timer has not yet started, and is already passed,
160                 # don't go trough waiting/running/end-states, but sort it
161                 # right into the processedTimers.
162                 if entry.shouldSkip() or entry.state == TimerEntry.StateEnded or (entry.state == TimerEntry.StateWaiting and entry.disabled):
163                         print "already passed, skipping"
164                         print "shouldSkip:", entry.shouldSkip()
165                         print "state == ended", entry.state == TimerEntry.StateEnded
166                         print "waiting && disabled:", (entry.state == TimerEntry.StateWaiting and entry.disabled)
167                         insort(self.processed_timers, entry)
168                         entry.state = TimerEntry.StateEnded
169                 else:
170                         insort(self.timer_list, entry)
171                         if not noRecalc:
172                                 self.calcNextActivation()
173         
174         def setNextActivation(self, when):
175                 delay = int((when - time()) * 1000)
176                 print "[timer.py] next activation: %d (in %d ms)" % (when, delay)
177                 
178                 self.timer.start(delay, 1)
179                 self.next = when
180
181         def calcNextActivation(self):
182                 if self.lastActivation > time():
183                         print "[timer.py] timewarp - re-evaluating all processed timers."
184                         tl = self.processed_timers
185                         self.processed_timers = [ ]
186                         for x in tl:
187                                 # simulate a "waiting" state to give them a chance to re-occure
188                                 x.resetState()
189                                 self.addTimerEntry(x, noRecalc=1)
190                 
191                 self.processActivation()
192                 self.lastActivation = time()
193         
194                 min = int(time()) + self.MaxWaitTime
195                 
196                 # calculate next activation point
197                 if len(self.timer_list):
198                         w = self.timer_list[0].getNextActivation()
199                         if w < min:
200                                 min = w
201                 
202                 self.setNextActivation(min)
203         
204         def timeChanged(self, timer):
205                 print "time changed"
206                 timer.timeChanged()
207                 if timer.state == TimerEntry.StateEnded:
208                         self.processed_timers.remove(timer)
209                 else:
210                         self.timer_list.remove(timer)
211
212                 # give the timer a chance to re-enqueue
213                 if timer.state == TimerEntry.StateEnded:
214                         timer.state = TimerEntry.StateWaiting
215                 self.addTimerEntry(timer)
216         
217         def doActivate(self, w):
218                 self.timer_list.remove(w)
219                 
220                 # when activating a timer which has already passed,
221                 # simply abort the timer. don't run trough all the stages.
222                 if w.shouldSkip():
223                         w.state = TimerEntry.StateEnded
224                 else:
225                         # when active returns true, this means "accepted".
226                         # otherwise, the current state is kept.
227                         # the timer entry itself will fix up the delay then.
228                         if w.activate():
229                                 w.state += 1
230
231                 # did this timer reached the last state?
232                 if w.state < TimerEntry.StateEnded:
233                         # no, sort it into active list
234                         insort(self.timer_list, w)
235                 else:
236                         # yes. Process repeated, and re-add.
237                         if w.repeated:
238                                 w.processRepeated()
239                                 w.state = TimerEntry.StateWaiting
240                                 self.addTimerEntry(w)
241                         else:
242                                 insort(self.processed_timers, w)
243                 
244                 self.stateChanged(w)
245
246         def processActivation(self):
247                 print "It's now ", strftime("%c", localtime(time()))
248                 t = int(time()) + 1
249                 
250                 # we keep on processing the first entry until it goes into the future.
251                 while len(self.timer_list) and self.timer_list[0].getNextActivation() < t:
252                         self.doActivate(self.timer_list[0])