update lt language
[enigma2.git] / RecordTimer.py
1 import time
2 #from time import datetime
3 from Tools import Directories, Notifications
4
5 from Components.config import config
6 import timer
7 import xml.dom.minidom
8
9 from enigma import eEPGCache, getBestPlayableServiceReference, \
10         eServiceReference, iRecordableService, quitMainloop
11
12 from Screens.MessageBox import MessageBox
13
14 import NavigationInstance
15
16 import Screens.Standby
17
18 from time import localtime
19
20 from Tools.XMLTools import elementsWithTag, mergeText, stringToXML
21 from ServiceReference import ServiceReference
22
23 # ok, for descriptions etc we have:
24 # service reference  (to get the service name)
25 # name               (title)
26 # description        (description)
27 # event data         (ONLY for time adjustments etc.)
28
29
30 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
31 # begin and end will be corrected
32 def parseEvent(ev, description = True):
33         if description:
34                 name = ev.getEventName()
35                 description = ev.getShortDescription()
36         else:
37                 name = ""
38                 description = ""
39         begin = ev.getBeginTime()
40         end = begin + ev.getDuration()
41         eit = ev.getEventId()
42         begin -= config.recording.margin_before.value * 60
43         end += config.recording.margin_after.value * 60
44         return (begin, end, name, description, eit)
45
46 class AFTEREVENT:
47         NONE = 0
48         STANDBY = 1
49         DEEPSTANDBY = 2
50
51 # please do not translate log messages
52 class RecordTimerEntry(timer.TimerEntry, object):
53 ######### the following static methods and members are only in use when the box is in (soft) standby
54         receiveRecordEvents = False
55
56         @staticmethod
57         def shutdown():
58                 quitMainloop(1)
59
60         @staticmethod
61         def staticGotRecordEvent(recservice, event):
62                 if event == iRecordableService.evEnd:
63                         print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
64                         recordings = NavigationInstance.instance.getRecordings()
65                         if not len(recordings): # no more recordings exist
66                                 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
67                                 if rec_time > 0 and (rec_time - time.time()) < 360:
68                                         print "another recording starts in", rec_time - time.time(), "seconds... do not shutdown yet"
69                                 else:
70                                         print "no starting records in the next 360 seconds... immediate shutdown"
71                                         RecordTimerEntry.shutdown() # immediate shutdown
72                 elif event == iRecordableService.evStart:
73                         print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
74
75         @staticmethod
76         def stopTryQuitMainloop():
77                 print "RecordTimer.stopTryQuitMainloop"
78                 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
79                 RecordTimerEntry.receiveRecordEvents = False
80
81         @staticmethod
82         def TryQuitMainloop():
83                 if not RecordTimerEntry.receiveRecordEvents:
84                         print "RecordTimer.TryQuitMainloop"
85                         NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
86                         RecordTimerEntry.receiveRecordEvents = True
87                         # send fake event.. to check if another recordings are running or
88                         # other timers start in a few seconds
89                         RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
90                         # send normal notification for the case the user leave the standby now..
91                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop)
92 #################################################################
93
94         def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.NONE, checkOldTimers = False, dirname = None):
95                 timer.TimerEntry.__init__(self, int(begin), int(end))
96
97                 if checkOldTimers == True:
98                         if self.begin < time.time() - 1209600:
99                                 self.begin = int(time.time())
100                 
101                 if self.end < self.begin:
102                         self.end = self.begin
103                 
104                 assert isinstance(serviceref, ServiceReference)
105                 
106                 self.service_ref = serviceref
107                 self.eit = eit
108                 self.dontSave = False
109                 self.name = name
110                 self.description = description
111                 self.disabled = disabled
112                 self.timer = None
113                 self.__record_service = None
114                 self.start_prepare = 0
115                 self.justplay = justplay
116                 self.afterEvent = afterEvent
117                 self.dirname = dirname
118                 
119                 self.log_entries = []
120                 self.resetState()
121         
122         def log(self, code, msg):
123                 self.log_entries.append((int(time.time()), code, msg))
124                 print "[TIMER]", msg
125         
126         def resetState(self):
127                 self.state = self.StateWaiting
128                 self.cancelled = False
129                 self.first_try_prepare = True
130                 self.timeChanged()
131         
132         def calculateFilename(self):
133                 service_name = self.service_ref.getServiceName()
134                 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
135                 
136                 print "begin_date: ", begin_date
137                 print "service_name: ", service_name
138                 print "name:", self.name
139                 print "description: ", self.description
140                 
141                 filename = begin_date + " - " + service_name
142                 if self.name:
143                         filename += " - " + self.name
144
145                 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
146                 self.log(0, "Filename calculated as: '%s'" % self.Filename)
147                 #begin_date + " - " + service_name + description)
148
149         def tryPrepare(self):
150                 if self.justplay:
151                         return True
152                 else:
153                         self.calculateFilename()
154                         rec_ref = self.service_ref and self.service_ref.ref
155                         if rec_ref and rec_ref.flags & eServiceReference.isGroup:
156                                 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
157                                 if not rec_ref:
158                                         self.log(1, "'get best playable service for group... record' failed")
159                                         return False
160                                 
161                         self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
162
163                         if not self.record_service:
164                                 self.log(1, "'record service' failed")
165                                 return False
166
167                         if self.repeated:
168                                 epgcache = eEPGCache.getInstance()
169                                 queryTime=self.begin+(self.end-self.begin)/2
170                                 evt = epgcache.lookupEventTime(rec_ref, queryTime)
171                                 if evt:
172                                         self.description = evt.getShortDescription()
173                                         event_id = evt.getEventId()
174                                 else:
175                                         event_id = -1
176                         else:
177                                 event_id = self.eit
178                                 if event_id is None:
179                                         event_id = -1
180
181                         prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
182                         if prep_res:
183                                 self.log(2, "'prepare' failed: error %d" % prep_res)
184                                 NavigationInstance.instance.stopRecordService(self.record_service)
185                                 self.record_service = None
186                                 return False
187
188                         self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
189                         try:
190                                 f = open(self.Filename + ".ts.meta", "w")
191                                 f.write(rec_ref.toString() + "\n")
192                                 f.write(self.name + "\n")
193                                 f.write(self.description + "\n")
194                                 f.write(str(self.begin) + "\n")
195                                 f.close()
196                         except IOError:
197                                 self.log(4, "failed to write meta information")
198                                 NavigationInstance.instance.stopRecordService(self.record_service)
199                                 self.record_service = None
200                                 return False
201                         return True
202
203         def do_backoff(self):
204                 if self.backoff == 0:
205                         self.backoff = 5
206                 else:
207                         self.backoff *= 2
208                         if self.backoff > 100:
209                                 self.backoff = 100
210                 self.log(10, "backoff: retry in %d seconds" % self.backoff)
211
212         def activate(self):
213                 next_state = self.state + 1
214                 self.log(5, "activating state %d" % next_state)
215                 
216                 if next_state == self.StatePrepared:
217                         if self.tryPrepare():
218                                 self.log(6, "prepare ok, waiting for begin")
219                                 # fine. it worked, resources are allocated.
220                                 self.next_activation = self.begin
221                                 self.backoff = 0
222                                 return True
223                         
224                         self.log(7, "prepare failed")
225                         if self.first_try_prepare:
226                                 self.first_try_prepare = False
227                                 if not config.recording.asktozap.value:
228                                         self.log(8, "asking user to zap away")
229                                         Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
230                                 else: # zap without asking
231                                         self.log(9, "zap without asking")
232                                         Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
233                                         self.failureCB(True)
234
235                         self.do_backoff()
236                         # retry
237                         self.start_prepare = time.time() + self.backoff
238                         return False
239                 elif next_state == self.StateRunning:
240                         # if this timer has been cancelled, just go to "end" state.
241                         if self.cancelled:
242                                 return True
243
244                         if self.justplay:
245                                 if Screens.Standby.inStandby:
246                                         self.log(11, "wakeup and zap")
247                                         #set service to zap after standby
248                                         Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
249                                         #wakeup standby
250                                         Screens.Standby.inStandby.Power()
251                                 else:
252                                         self.log(11, "zapping")
253                                         NavigationInstance.instance.playService(self.service_ref.ref)
254                                 return True
255                         else:
256                                 self.log(11, "start recording")
257                                 record_res = self.record_service.start()
258                                 
259                                 if record_res:
260                                         self.log(13, "start record returned %d" % record_res)
261                                         self.do_backoff()
262                                         # retry
263                                         self.begin = time.time() + self.backoff
264                                         return False
265
266                                 return True
267                 elif next_state == self.StateEnded:
268                         self.log(12, "stop recording")
269                         if not self.justplay:
270                                 NavigationInstance.instance.stopRecordService(self.record_service)
271                                 self.record_service = None
272                         if self.afterEvent == AFTEREVENT.STANDBY:
273                                 if not Screens.Standby.inStandby: # not already in standby
274                                         Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
275                         if self.afterEvent == AFTEREVENT.DEEPSTANDBY:
276                                 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
277                                         if Screens.Standby.inStandby: # not in standby
278                                                 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
279                                         else:
280                                                 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
281                         return True
282
283         def sendStandbyNotification(self, answer):
284                 if answer:
285                         Notifications.AddNotification(Screens.Standby.Standby)
286
287         def sendTryQuitMainloopNotification(self, answer):
288                 if answer:
289                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
290
291         def getNextActivation(self):
292                 if self.state == self.StateEnded:
293                         return self.end
294                 
295                 next_state = self.state + 1
296                 
297                 return {self.StatePrepared: self.start_prepare, 
298                                 self.StateRunning: self.begin, 
299                                 self.StateEnded: self.end }[next_state]
300
301         def failureCB(self, answer):
302                 if answer == True:
303                         self.log(13, "ok, zapped away")
304                         #NavigationInstance.instance.stopUserServices()
305                         NavigationInstance.instance.playService(self.service_ref.ref)
306                 else:
307                         self.log(14, "user didn't want to zap away, record will probably fail")
308
309         def timeChanged(self):
310                 old_prepare = self.start_prepare
311                 self.start_prepare = self.begin - self.prepare_time
312                 self.backoff = 0
313                 
314                 if int(old_prepare) != int(self.start_prepare):
315                         self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
316
317         def gotRecordEvent(self, record, event):
318                 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
319                 if self.__record_service.__deref__() != record.__deref__():
320                         return
321                 self.log(16, "record event %d" % event)
322                 if event == iRecordableService.evRecordWriteError:
323                         print "WRITE ERROR on recording, disk full?"
324                         # show notification. the 'id' will make sure that it will be
325                         # displayed only once, even if more timers are failing at the
326                         # same time. (which is very likely in case of disk fullness)
327                         Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
328                         # ok, the recording has been stopped. we need to properly note 
329                         # that in our state, with also keeping the possibility to re-try.
330                         # TODO: this has to be done.
331                 elif event == iRecordableService.evStart:
332                         # maybe this should be configurable?
333                         Notifications.AddPopup(text = _("A record has been started:\n%s") % self.name, type = MessageBox.TYPE_INFO, timeout = 3)
334
335         # we have record_service as property to automatically subscribe to record service events
336         def setRecordService(self, service):
337                 if self.__record_service is not None:
338                         print "[remove callback]"
339                         NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
340
341                 self.__record_service = service
342
343                 if self.__record_service is not None:
344                         print "[add callback]"
345                         NavigationInstance.instance.record_event.append(self.gotRecordEvent)
346
347         record_service = property(lambda self: self.__record_service, setRecordService)
348
349 def createTimer(xml):
350         begin = int(xml.getAttribute("begin"))
351         end = int(xml.getAttribute("end"))
352         serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
353         description = xml.getAttribute("description").encode("utf-8")
354         repeated = xml.getAttribute("repeated").encode("utf-8")
355         disabled = long(xml.getAttribute("disabled") or "0")
356         justplay = long(xml.getAttribute("justplay") or "0")
357         afterevent = str(xml.getAttribute("afterevent") or "nothing")
358         afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
359         if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
360                 eit = long(xml.getAttribute("eit"))
361         else:
362                 eit = None
363         if xml.hasAttribute("location") and xml.getAttribute("location") != "None":
364                 location = str(xml.getAttribute("location")).encode("utf-8")
365         else:
366                 location = None
367
368
369         name = xml.getAttribute("name").encode("utf-8")
370         #filename = xml.getAttribute("filename").encode("utf-8")
371         entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location)
372         entry.repeated = int(repeated)
373         
374         for l in elementsWithTag(xml.childNodes, "log"):
375                 time = int(l.getAttribute("time"))
376                 code = int(l.getAttribute("code"))
377                 msg = mergeText(l.childNodes).strip().encode("utf-8")
378                 entry.log_entries.append((time, code, msg))
379         
380         return entry
381
382 class RecordTimer(timer.Timer):
383         def __init__(self):
384                 timer.Timer.__init__(self)
385                 
386                 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
387                 
388                 try:
389                         self.loadTimer()
390                 except IOError:
391                         print "unable to load timers from file!"
392                         
393         def isRecording(self):
394                 isRunning = False
395                 for timer in self.timer_list:
396                         if timer.isRunning() and not timer.justplay:
397                                 isRunning = True
398                 return isRunning
399         
400         def loadTimer(self):
401                 # TODO: PATH!
402                 try:
403                         doc = xml.dom.minidom.parse(self.Filename)
404                 except xml.parsers.expat.ExpatError:
405                         from Tools.Notifications import AddPopup
406                         from Screens.MessageBox import MessageBox
407
408                         AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
409
410                         print "timers.xml failed to load!"
411                         try:
412                                 import os
413                                 os.rename(self.Filename, self.Filename + "_old")
414                         except IOError:
415                                 print "renaming broken timer failed"
416                         return
417
418                 root = doc.childNodes[0]
419                 for timer in elementsWithTag(root.childNodes, "timer"):
420                         self.record(createTimer(timer))
421
422         def saveTimer(self):
423                 #doc = xml.dom.minidom.Document()
424                 #root_element = doc.createElement('timers')
425                 #doc.appendChild(root_element)
426                 #root_element.appendChild(doc.createTextNode("\n"))
427                 
428                 #for timer in self.timer_list + self.processed_timers:
429                         # some timers (instant records) don't want to be saved.
430                         # skip them
431                         #if timer.dontSave:
432                                 #continue
433                         #t = doc.createTextNode("\t")
434                         #root_element.appendChild(t)
435                         #t = doc.createElement('timer')
436                         #t.setAttribute("begin", str(int(timer.begin)))
437                         #t.setAttribute("end", str(int(timer.end)))
438                         #t.setAttribute("serviceref", str(timer.service_ref))
439                         #t.setAttribute("repeated", str(timer.repeated))                        
440                         #t.setAttribute("name", timer.name)
441                         #t.setAttribute("description", timer.description)
442                         #t.setAttribute("eit", str(timer.eit))
443                         
444                         #for time, code, msg in timer.log_entries:
445                                 #t.appendChild(doc.createTextNode("\t\t"))
446                                 #l = doc.createElement('log')
447                                 #l.setAttribute("time", str(time))
448                                 #l.setAttribute("code", str(code))
449                                 #l.appendChild(doc.createTextNode(msg))
450                                 #t.appendChild(l)
451                                 #t.appendChild(doc.createTextNode("\n"))
452
453                         #root_element.appendChild(t)
454                         #t = doc.createTextNode("\n")
455                         #root_element.appendChild(t)
456
457
458                 #file = open(self.Filename, "w")
459                 #doc.writexml(file)
460                 #file.write("\n")
461                 #file.close()
462
463                 list = []
464
465                 list.append('<?xml version="1.0" ?>\n')
466                 list.append('<timers>\n')
467                 
468                 for timer in self.timer_list + self.processed_timers:
469                         if timer.dontSave:
470                                 continue
471
472                         list.append('<timer')
473                         list.append(' begin="' + str(int(timer.begin)) + '"')
474                         list.append(' end="' + str(int(timer.end)) + '"')
475                         list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
476                         list.append(' repeated="' + str(int(timer.repeated)) + '"')
477                         list.append(' name="' + str(stringToXML(timer.name)) + '"')
478                         list.append(' description="' + str(stringToXML(timer.description)) + '"')
479                         list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
480                         if timer.eit is not None:
481                                 list.append(' eit="' + str(timer.eit) + '"')
482                         if timer.dirname is not None:
483                                 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
484                         list.append(' disabled="' + str(int(timer.disabled)) + '"')
485                         list.append(' justplay="' + str(int(timer.justplay)) + '"')
486                         list.append('>\n')
487                         
488                         if config.recording.debug.value:
489                                 for time, code, msg in timer.log_entries:
490                                         list.append('<log')
491                                         list.append(' code="' + str(code) + '"')
492                                         list.append(' time="' + str(time) + '"')
493                                         list.append('>')
494                                         list.append(str(stringToXML(msg)))
495                                         list.append('</log>\n')
496                         
497                         list.append('</timer>\n')
498
499                 list.append('</timers>\n')
500
501                 file = open(self.Filename, "w")
502                 for x in list:
503                         file.write(x)
504                 file.close()
505
506         def getNextZapTime(self):
507                 now = time.time()
508                 for timer in self.timer_list:
509                         if not timer.justplay or timer.begin < now:
510                                 continue
511                         return timer.begin
512                 return -1
513
514         def getNextRecordingTime(self):
515                 now = time.time()
516                 for timer in self.timer_list:
517                         if timer.justplay or timer.begin < now:
518                                 continue
519                         return timer.begin
520                 return -1
521
522         def record(self, entry):
523                 entry.timeChanged()
524                 print "[Timer] Record " + str(entry)
525                 entry.Timer = self
526                 self.addTimerEntry(entry)
527                 self.saveTimer()
528                 
529         def isInTimer(self, eventid, begin, duration, service):
530                 time_match = 0
531                 chktime = None
532                 chktimecmp = None
533                 chktimecmp_end = None
534                 end = begin + duration
535                 for x in self.timer_list:
536                         check = x.service_ref.ref.toCompareString() == str(service)
537                         if not check:
538                                 sref = x.service_ref.ref
539                                 parent_sid = sref.getUnsignedData(5)
540                                 parent_tsid = sref.getUnsignedData(6)
541                                 if parent_sid and parent_tsid: # check for subservice
542                                         sid = sref.getUnsignedData(1)
543                                         tsid = sref.getUnsignedData(2)
544                                         sref.setUnsignedData(1, parent_sid)
545                                         sref.setUnsignedData(2, parent_tsid)
546                                         sref.setUnsignedData(5, 0)
547                                         sref.setUnsignedData(6, 0)
548                                         check = x.service_ref.ref.toCompareString() == str(service)
549                                         num = 0
550                                         if check:
551                                                 check = False
552                                                 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
553                                                 num = event and event.getNumOfLinkageServices() or 0
554                                         sref.setUnsignedData(1, sid)
555                                         sref.setUnsignedData(2, tsid)
556                                         sref.setUnsignedData(5, parent_sid)
557                                         sref.setUnsignedData(6, parent_tsid)
558                                         for cnt in range(num):
559                                                 subservice = event.getLinkageService(sref, cnt)
560                                                 if sref.toCompareString() == subservice.toCompareString():
561                                                         check = True
562                                                         break
563                         if check:
564                                 #if x.eit is not None and x.repeated == 0:
565                                 #       if x.eit == eventid:
566                                 #               return duration
567                                 if x.repeated != 0:
568                                         if chktime is None:
569                                                 chktime = localtime(begin)
570                                                 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
571                                                 chktimecmp_end = chktimecmp + (duration / 60)
572                                         time = localtime(x.begin)
573                                         for y in range(7):
574                                                 if x.repeated & (2 ** y):
575                                                         timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
576                                                         if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
577                                                                 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
578                                                         elif chktimecmp <= timecmp < chktimecmp_end:
579                                                                 time_match = (chktimecmp_end - timecmp) * 60
580                                 else: #if x.eit is None:
581                                         if begin <= x.begin <= end:
582                                                 diff = end - x.begin
583                                                 if time_match < diff:
584                                                         time_match = diff
585                                         elif x.begin <= begin <= x.end:
586                                                 diff = x.end - begin
587                                                 if time_match < diff:
588                                                         time_match = diff
589                 return time_match
590
591         def removeEntry(self, entry):
592                 print "[Timer] Remove " + str(entry)
593                 
594                 # avoid re-enqueuing
595                 entry.repeated = False
596
597                 # abort timer.
598                 # this sets the end time to current time, so timer will be stopped.
599                 entry.abort()
600                 
601                 if entry.state != entry.StateEnded:
602                         self.timeChanged(entry)
603                 
604                 print "state: ", entry.state
605                 print "in processed: ", entry in self.processed_timers
606                 print "in running: ", entry in self.timer_list
607                 # now the timer should be in the processed_timers list. remove it from there.
608                 self.processed_timers.remove(entry)
609                 self.saveTimer()
610
611         def shutdown(self):
612                 self.saveTimer()