fix reading max duration from config (has been buggy since rev 1.16 of AutoTimer...
[vuplus_dvbapp-plugin] / autotimer / src / AutoTimerConfiguration.py
1 # for localized messages
2 from . import _
3
4 from AutoTimerComponent import AutoTimerComponent
5 from RecordTimer import AFTEREVENT
6 from Tools.XMLTools import stringToXML
7 from ServiceReference import ServiceReference
8
9 CURRENT_CONFIG_VERSION = "5"
10
11 def getValue(definitions, default):
12         # Initialize Output
13         ret = ""
14
15         # How many definitions are present
16         if isinstance(definitions, list):
17                 Len = len(definitions)
18                 if Len > 0:
19                         childNodes = definitions[Len-1].childNodes
20                 else:
21                         childNodes = []
22         else:
23                 childNodes = definitions.childNodes
24
25         # Iterate through nodes of last one
26         for node in childNodes:
27                 # Append text if we have a text node
28                 if node.nodeType == node.TEXT_NODE:
29                         ret = ret + node.data
30
31         # Return stripped output or (if empty) default
32         return ret.strip() or default
33
34 def parseConfig(configuration, list, version = None, uniqueTimerId = 0, defaultTimer = None):
35         if version != CURRENT_CONFIG_VERSION:
36                 parseConfigOld(configuration, list, uniqueTimerId)
37                 return
38
39         if defaultTimer is not None:
40                 # Read in defaults for a new timer
41                 for defaults in configuration.getElementsByTagName("defaults"):
42                         parseEntry(defaults, defaultTimer, True)
43
44         for timer in configuration.getElementsByTagName("timer"):
45                 uniqueTimerId += 1
46                 baseTimer = AutoTimerComponent(
47                         uniqueTimerId,
48                         '',
49                         '',
50                         True
51                 )
52
53                 if parseEntry(timer, baseTimer):
54                         list.append(baseTimer)
55
56 def parseEntry(element, baseTimer, defaults = False):
57         if not defaults:
58                 # Read out match
59                 baseTimer.match = element.getAttribute("match").encode("UTF-8")
60                 if not baseTimer.match:
61                         print '[AutoTimer] Erroneous config is missing attribute "match", skipping entry'
62                         return False
63
64                 # Read out name
65                 baseTimer.name = element.getAttribute("name").encode("UTF-8")
66                 if not baseTimer.name:
67                         print '[AutoTimer] Timer is missing attribute "name", defaulting to match'
68                         baseTimer.name = baseTimer.match
69
70                 # Read out enabled
71                 enabled = element.getAttribute("enabled") or "yes"
72                 if enabled == "no":
73                         baseTimer.enabled = False
74                 elif enabled == "yes":
75                         baseTimer.enabled = True
76                 else:
77                         print '[AutoTimer] Erroneous config contains invalid value for "enabled":', enabled,', disabling'
78                         baseTimer.enabled = False
79
80         # Read out timespan
81         start = element.getAttribute("from")
82         end = element.getAttribute("to")
83         if start and end:
84                 start = [int(x) for x in start.split(':')]
85                 end = [int(x) for x in end.split(':')]
86                 baseTimer.timespan = (start, end)
87
88         # Read out max length
89         maxduration = element.getAttribute("maxduration") or None
90         if maxduration:
91                 baseTimer.maxduration = int(maxduration)*60
92
93         # Read out recording path
94         baseTimer.destination = element.getAttribute("location").encode("UTF-8") or None
95
96         # Read out offset
97         offset = element.getAttribute("offset") or None
98         if offset:
99                 offset = offset.split(",")
100                 if len(offset) == 1:
101                         before = after = int(offset[0] or 0) * 60
102                 else:
103                         before = int(offset[0] or 0) * 60
104                         after = int(offset[1] or 0) * 60
105                 baseTimer.offset = (before, after)
106
107         # Read out counter
108         baseTimer.matchCount = int(element.getAttribute("counter") or '0')
109         baseTimer.matchFormatString = element.getAttribute("counterFormat")
110         if not defaults:
111                 baseTimer.counterLimit = element.getAttribute("lastActivation")
112                 baseTimer.counterFormat = element.getAttribute("counterFormat")
113                 baseTimer.lastBegin = int(element.getAttribute("lastBegin") or 0)
114
115         # Read out justplay
116         justplay = int(element.getAttribute("justplay") or '0')
117
118         # Read out avoidDuplicateDescription
119         baseTimer.avoidDuplicateDescription = int(element.getAttribute("avoidDuplicateDescription") or 0)
120
121         # Read out allowed services
122         servicelist = baseTimer.services        
123         for service in element.getElementsByTagName("serviceref"):
124                 value = getValue(service, None)
125                 if value:
126                         # strip all after last :
127                         pos = value.rfind(':')
128                         if pos != -1:
129                                 value = value[:pos+1]
130
131                         servicelist.append(value)
132         baseTimer.services = servicelist
133
134         # Read out allowed bouquets
135         bouquets = baseTimer.bouquets
136         for bouquet in element.getElementsByTagName("bouquet"):
137                 value = getValue(bouquet, None)
138                 if value:
139                         bouquets.append(value)
140         baseTimer.bouquets = bouquets
141
142         # Read out afterevent
143         idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
144         afterevent = baseTimer.afterevent
145         for element in element.getElementsByTagName("afterevent"):
146                 value = getValue(element, None)
147
148                 try:
149                         value = idx[value]
150                 except KeyError, ke:
151                         print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
152                         continue
153
154                 start = element.getAttribute("from")
155                 end = element.getAttribute("to")
156                 if start and end:
157                         start = [int(x) for x in start.split(':')]
158                         end = [int(x) for x in end.split(':')]
159                         afterevent.append((value, (start, end)))
160                 else:
161                         afterevent.append((value, None))
162         baseTimer.afterevent = afterevent
163
164         # Read out exclude
165         idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
166         excludes = (baseTimer.getExcludedTitle(), baseTimer.getExcludedShort(), baseTimer.getExcludedDescription(), baseTimer.getExcludedDays()) 
167         for exclude in element.getElementsByTagName("exclude"):
168                 where = exclude.getAttribute("where")
169                 value = getValue(exclude, None)
170                 if not (value and where):
171                         continue
172
173                 try:
174                         excludes[idx[where]].append(value.encode("UTF-8"))
175                 except KeyError, ke:
176                         pass
177         baseTimer.exclude = excludes
178
179         # Read out includes (use same idx)
180         includes = (baseTimer.getIncludedTitle(), baseTimer.getIncludedShort(), baseTimer.getIncludedDescription(), baseTimer.getIncludedDays())
181         for include in element.getElementsByTagName("include"):
182                 where = include.getAttribute("where")
183                 value = getValue(include, None)
184                 if not (value and where):
185                         continue
186
187                 try:
188                         includes[idx[where]].append(value.encode("UTF-8"))
189                 except KeyError, ke:
190                         pass
191         baseTimer.include = includes
192
193         # Read out recording tags (needs my enhanced tag support patch)
194         tags = baseTimer.tags
195         for tag in element.getElementsByTagName("tag"):
196                 value = getValue(tag, None)
197                 if not value:
198                         continue
199
200                 tags.append(value.encode("UTF-8"))
201         baseTimer.tags = tags
202         
203         return True
204
205 def parseConfigOld(configuration, list, uniqueTimerId = 0):
206         print "[AutoTimer] Trying to parse old config"
207
208         # Iterate Timers
209         for timer in configuration.getElementsByTagName("timer"):
210                 # Increment uniqueTimerId
211                 uniqueTimerId += 1
212
213                 # Get name (V2+)
214                 if timer.hasAttribute("name"):
215                         name = timer.getAttribute("name").encode("UTF-8")
216                 # Get name (= match) (V1)
217                 else:
218                         # Read out name
219                         name = getValue(timer.getElementsByTagName("name"), "").encode("UTF-8")
220
221                 if not name:
222                         print '[AutoTimer] Erroneous config is missing attribute "name", skipping entry'
223                         continue
224
225                 # Read out match (V3+)
226                 if timer.hasAttribute("match"):
227                         # Read out match
228                         match = timer.getAttribute("match").encode("UTF-8")
229                         if not match:
230                                 print '[AutoTimer] Erroneous config contains empty attribute "match", skipping entry'
231                                 continue
232                 # V2-
233                 else:
234                         # Setting name to match
235                         name = match
236
237
238                 # See if Timer is ensabled (V2+)
239                 if timer.hasAttribute("enabled"):
240                         enabled = timer.getAttribute("enabled") or "yes"
241                         if enabled == "no":
242                                 enabled = False
243                         elif enabled == "yes":
244                                 enabled = True
245                         else:
246                                 print '[AutoTimer] Erroneous config contains invalid value for "enabled":', enabled,', skipping entry'
247                                 enabled = False
248                 # V1
249                 else:
250                         elements = timer.getElementsByTagName("enabled")
251                         if len(elements):
252                                 if getValue(elements, "yes") == "no":
253                                         enabled = False
254                                 else:
255                                         enabled = True
256                         else:
257                                 enabled = True
258                         
259
260                 # Read out timespan (V4+; Falling back on missing definition should be OK)
261                 if timer.hasAttribute("from") and timer.hasAttribute("to"):
262                         start = timer.getAttribute("from")
263                         end = timer.getAttribute("to")
264                         if start and end:
265                                 start = [int(x) for x in start.split(':')]
266                                 end = [int(x) for x in end.split(':')]
267                                 timetuple = (start, end)
268                         else:
269                                 timetuple = None
270                 # V3-
271                 else:
272                         elements = timer.getElementsByTagName("timespan")
273                         Len = len(elements)
274                         if Len:
275                                 # Read out last definition
276                                 start = elements[Len-1].getAttribute("from")
277                                 end = elements[Len-1].getAttribute("to")
278                                 if start and end:
279                                         start = [int(x) for x in start.split(':')]
280                                         end = [int(x) for x in end.split(':')]
281                                         timetuple = (start, end)
282                                 else:
283                                         print '[AutoTimer] Erroneous config contains invalid definition of "timespan", ignoring definition'
284                                         timetuple = None
285                         else:
286                                 timetuple = None
287
288                 # Read out allowed services (V*)
289                 elements = timer.getElementsByTagName("serviceref")
290                 if len(elements):
291                         servicelist = []
292                         for service in elements:
293                                 value = getValue(service, None)
294                                 if value:
295                                         # strip all after last :
296                                         pos = value.rfind(':')
297                                         if pos != -1:
298                                                 value = value[:pos+1]
299
300                                         servicelist.append(value)
301                 else:
302                         servicelist = None
303
304                 # Read out allowed bouquets (V* though officially supported since V4)
305                 bouquets = []
306                 for bouquet in timer.getElementsByTagName("bouquet"):
307                         value = getValue(bouquet, None)
308                         if value:
309                                 bouquets.append(value)
310
311                 # Read out offset (V4+)
312                 if timer.hasAttribute("offset"):
313                         offset = timer.getAttribute("offset") or None
314                         if offset:
315                                 offset = offset.split(",")
316                                 if len(offset) == 1:
317                                         before = after = int(offset[0] or 0) * 60
318                                 else:
319                                         before = int(offset[0] or 0) * 60
320                                         after = int(offset[1] or 0) * 60
321                                 offset = (before, after)
322                 # V3-
323                 else:
324                         elements = timer.getElementsByTagName("offset")
325                         Len = len(elements)
326                         if Len:
327                                 value = elements[Len-1].getAttribute("both")
328                                 if value == '':
329                                         before = int(elements[Len-1].getAttribute("before") or 0) * 60
330                                         after = int(elements[Len-1].getAttribute("after") or 0) * 60
331                                 else:
332                                         before = after = int(value) * 60
333                                 offset = (before, after)
334                         else:
335                                 offset = None
336
337                 # Read out counter
338                 counter = int(timer.getAttribute("counter") or '0')
339                 counterLeft = int(timer.getAttribute("left") or counter)
340                 counterLimit = timer.getAttribute("lastActivation")
341                 counterFormat = timer.getAttribute("counterFormat")
342                 lastBegin = int(timer.getAttribute("lastBegin") or 0)
343
344                 # Read out justplay
345                 justplay = int(timer.getAttribute("justplay") or '0')
346
347                 # Read out avoidDuplicateDescription
348                 avoidDuplicateDescription = int(timer.getAttribute("avoidDuplicateDescription") or 0)
349
350                 # Read out afterevent (compatible to V* though behaviour for V3- is different as V4+ allows multiple afterevents while the last definication was chosen before)
351                 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
352                 afterevent = []
353                 for element in timer.getElementsByTagName("afterevent"):
354                         value = getValue(element, None)
355
356                         try:
357                                 value = idx[value]
358                         except KeyError, ke:
359                                 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
360                                 continue
361
362                         start = element.getAttribute("from")
363                         end = element.getAttribute("to")
364                         if start and end:
365                                 start = [int(x) for x in start.split(':')]
366                                 end = [int(x) for x in end.split(':')]
367                                 afterevent.append((value, (start, end)))
368                         else:
369                                 afterevent.append((value, None))
370
371                 # Read out exclude (V*)
372                 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
373                 excludes = ([], [], [], []) 
374                 for exclude in timer.getElementsByTagName("exclude"):
375                         where = exclude.getAttribute("where")
376                         value = getValue(exclude, None)
377                         if not (value and where):
378                                 continue
379
380                         try:
381                                 excludes[idx[where]].append(value.encode("UTF-8"))
382                         except KeyError, ke:
383                                 pass
384
385                 # Read out includes (use same idx) (V4+ feature, should not harm V3-)
386                 includes = ([], [], [], []) 
387                 for include in timer.getElementsByTagName("include"):
388                         where = include.getAttribute("where")
389                         value = getValue(include, None)
390                         if not (value and where):
391                                 continue
392
393                         try:
394                                 includes[idx[where]].append(value.encode("UTF-8"))
395                         except KeyError, ke:
396                                 pass
397
398                 # Read out max length (V4+)
399                 if timer.hasAttribute("maxduration"):
400                         maxlen = timer.getAttribute("maxduration") or None
401                         if maxlen:
402                                 maxlen = int(maxlen)*60
403                 # V3-
404                 else:
405                         elements = timer.getElementsByTagName("maxduration")
406                         if len(elements):
407                                 maxlen = getValue(elements, None)
408                                 if maxlen is not None:
409                                         maxlen = int(maxlen)*60
410                         else:
411                                 maxlen = None
412
413                 # Read out recording path
414                 destination = timer.getAttribute("destination").encode("UTF-8") or None
415
416                 # Read out recording tags (needs my enhanced tag support patch)
417                 tags = []
418                 for tag in timer.getElementsByTagName("tag"):
419                         value = getValue(tag, None)
420                         if not value:
421                                 continue
422
423                         tags.append(value.encode("UTF-8"))
424
425                 # Finally append timer
426                 list.append(AutoTimerComponent(
427                                 uniqueTimerId,
428                                 name,
429                                 match,
430                                 enabled,
431                                 timespan = timetuple,
432                                 services = servicelist,
433                                 offset = offset,
434                                 afterevent = afterevent,
435                                 exclude = excludes,
436                                 include = includes,
437                                 maxduration = maxlen,
438                                 destination = destination,
439                                 matchCount = counter,
440                                 matchLeft = counterLeft,
441                                 matchLimit = counterLimit,
442                                 matchFormatString = counterFormat,
443                                 lastBegin = lastBegin,
444                                 justplay = justplay,
445                                 avoidDuplicateDescription = avoidDuplicateDescription,
446                                 bouquets = bouquets,
447                                 tags = tags
448                 ))
449
450 def writeConfig(filename, defaultTimer, timers):
451         # Generate List in RAM
452         list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
453
454         # XXX: we might want to make sure that we don't save empty default here
455         list.extend([' <defaults'])
456
457         # Timespan
458         if defaultTimer.hasTimespan():
459                 list.extend([' from="', defaultTimer.getTimespanBegin(), '" to="', defaultTimer.getTimespanEnd(), '"'])
460
461         # Duration
462         if defaultTimer.hasDuration():
463                 list.extend([' maxduration="', str(defaultTimer.getDuration()), '"'])
464
465         # Destination
466         if defaultTimer.hasDestination():
467                 list.extend([' location="', stringToXML(defaultTimer.destination), '"'])
468
469         # Offset
470         if defaultTimer.hasOffset():
471                 if defaultTimer.isOffsetEqual():
472                         list.extend([' offset="', str(defaultTimer.getOffsetBegin()), '"'])
473                 else:
474                         list.extend([' offset="', str(defaultTimer.getOffsetBegin()), ',', str(defaultTimer.getOffsetEnd()), '"'])
475
476         # Counter
477         if defaultTimer.hasCounter():
478                 list.extend([' counter="', str(defaultTimer.getCounter()), '"'])
479                 if defaultTimer.hasCounterFormatString():
480                         list.extend([' counterFormat="', str(defaultTimer.getCounterFormatString()), '"'])
481
482         # Duplicate Description
483         if defaultTimer.getAvoidDuplicateDescription():
484                 list.append(' avoidDuplicateDescription="1" ')
485
486         # Only display justplay if true
487         if defaultTimer.justplay:
488                 list.extend([' justplay="', str(defaultTimer.getJustplay()), '"'])
489
490         # Close still opened defaults tag
491         list.append('>\n')
492
493         # Services
494         for serviceref in defaultTimer.getServices():
495                 list.extend(['  <serviceref>', serviceref, '</serviceref>'])
496                 ref = ServiceReference(str(serviceref))
497                 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
498
499         # Bouquets
500         for bouquet in defaultTimer.getBouquets():
501                 list.extend(['  <bouquet>', str(bouquet), '</bouquet>'])
502                 ref = ServiceReference(str(bouquet))
503                 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
504
505         # AfterEvent
506         if defaultTimer.hasAfterEvent():
507                 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
508                 for afterevent in defaultTimer.getCompleteAfterEvent():
509                         action, timespan = afterevent
510                         list.append('  <afterevent')
511                         if timespan[0] is not None:
512                                 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
513                         list.extend(['>', idx[action], '</afterevent>\n'])
514
515         # Excludes
516         for title in defaultTimer.getExcludedTitle():
517                 list.extend(['  <exclude where="title">', stringToXML(title), '</exclude>\n'])
518         for short in defaultTimer.getExcludedShort():
519                 list.extend(['  <exclude where="shortdescription">', stringToXML(short), '</exclude>\n'])
520         for desc in defaultTimer.getExcludedDescription():
521                 list.extend(['  <exclude where="description">', stringToXML(desc), '</exclude>\n'])
522         for day in defaultTimer.getExcludedDays():
523                 list.extend(['  <exclude where="dayofweek">', stringToXML(day), '</exclude>\n'])
524
525         # Includes
526         for title in defaultTimer.getIncludedTitle():
527                 list.extend(['  <include where="title">', stringToXML(title), '</include>\n'])
528         for short in defaultTimer.getIncludedShort():
529                 list.extend(['  <include where="shortdescription">', stringToXML(short), '</include>\n'])
530         for desc in defaultTimer.getIncludedDescription():
531                 list.extend(['  <include where="description">', stringToXML(desc), '</include>\n'])
532         for day in defaultTimer.getIncludedDays():
533                 list.extend(['  <include where="dayofweek">', stringToXML(day), '</include>\n'])
534
535         # Tags
536         for tag in defaultTimer.tags:
537                 list.extend(['  <tag>', stringToXML(tag), '</tag>\n'])
538
539         # End of Timer
540         list.append(' </defaults>\n\n')
541
542         # Iterate timers
543         for timer in timers:
544                 # Common attributes (match, enabled)
545                 list.extend([' <timer name="', stringToXML(timer.name), '" match="', stringToXML(timer.match), '" enabled="', timer.getEnabled(), '"'])
546
547                 # Timespan
548                 if timer.hasTimespan():
549                         list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
550
551                 # Duration
552                 if timer.hasDuration():
553                         list.extend([' maxduration="', str(timer.getDuration()), '"'])
554
555                 # Destination
556                 if timer.hasDestination():
557                         list.extend([' location="', stringToXML(timer.destination), '"'])
558
559                 # Offset
560                 if timer.hasOffset():
561                         if timer.isOffsetEqual():
562                                 list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
563                         else:
564                                 list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
565
566                 # Counter
567                 if timer.hasCounter():
568                         list.extend([' lastBegin="', str(timer.getLastBegin()), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
569                         if timer.hasCounterFormatString():
570                                 list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
571                                 list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
572
573                 # Duplicate Description
574                 if timer.getAvoidDuplicateDescription():
575                         list.extend([' avoidDuplicateDescription="', str(timer.getAvoidDuplicateDescription()), '"'])
576
577                 # Only display justplay if true
578                 if timer.justplay:
579                         list.extend([' justplay="', str(timer.getJustplay()), '"'])
580
581                 # Close still opened timer tag
582                 list.append('>\n')
583
584                 # Services
585                 for serviceref in timer.getServices():
586                         list.extend(['  <serviceref>', serviceref, '</serviceref>'])
587                         ref = ServiceReference(str(serviceref))
588                         list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
589
590                 # Bouquets
591                 for bouquet in timer.getBouquets():
592                         list.extend(['  <bouquet>', str(bouquet), '</bouquet>'])
593                         ref = ServiceReference(str(bouquet))
594                         list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
595
596                 # AfterEvent
597                 if timer.hasAfterEvent():
598                         idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
599                         for afterevent in timer.getCompleteAfterEvent():
600                                 action, timespan = afterevent
601                                 list.append('  <afterevent')
602                                 if timespan[0] is not None:
603                                         list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
604                                 list.extend(['>', idx[action], '</afterevent>\n'])
605
606                 # Excludes
607                 for title in timer.getExcludedTitle():
608                         list.extend(['  <exclude where="title">', stringToXML(title), '</exclude>\n'])
609                 for short in timer.getExcludedShort():
610                         list.extend(['  <exclude where="shortdescription">', stringToXML(short), '</exclude>\n'])
611                 for desc in timer.getExcludedDescription():
612                         list.extend(['  <exclude where="description">', stringToXML(desc), '</exclude>\n'])
613                 for day in timer.getExcludedDays():
614                         list.extend(['  <exclude where="dayofweek">', stringToXML(day), '</exclude>\n'])
615
616                 # Includes
617                 for title in timer.getIncludedTitle():
618                         list.extend(['  <include where="title">', stringToXML(title), '</include>\n'])
619                 for short in timer.getIncludedShort():
620                         list.extend(['  <include where="shortdescription">', stringToXML(short), '</include>\n'])
621                 for desc in timer.getIncludedDescription():
622                         list.extend(['  <include where="description">', stringToXML(desc), '</include>\n'])
623                 for day in timer.getIncludedDays():
624                         list.extend(['  <include where="dayofweek">', stringToXML(day), '</include>\n'])
625
626                 # Tags
627                 for tag in timer.tags:
628                         list.extend(['  <tag>', stringToXML(tag), '</tag>\n'])
629
630                 # End of Timer
631                 list.append(' </timer>\n\n')
632
633         # End of Configuration
634         list.append('</autotimer>\n')
635
636         # Save to Flash
637         file = open(filename, 'w')
638         file.writelines(list)
639
640         file.close()
641