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