instantiate and import autotimer/autopoller only as needed
[enigma2-plugins.git] / fritzcall / src / plugin.py
1 # -*- coding: utf-8 -*-
2 from Screens.Screen import Screen
3 from Screens.MessageBox import MessageBox
4
5 from Components.ActionMap import ActionMap
6 from Components.Label import Label
7 from Components.config import config, ConfigSubsection, ConfigSelection, ConfigIP, ConfigEnableDisable, getConfigListEntry, ConfigText, ConfigInteger
8 from Components.ConfigList import ConfigListScreen
9
10 from Plugins.Plugin import PluginDescriptor
11 from Tools import Notifications
12
13 from twisted.internet import reactor
14 from twisted.internet.protocol import ReconnectingClientFactory
15 from twisted.protocols.basic import LineReceiver
16 from twisted.web.client import getPage
17
18 from os import path as os_path
19 from urllib import urlencode
20 import re
21
22
23 my_global_session = None
24
25 config.plugins.FritzCall = ConfigSubsection()
26 config.plugins.FritzCall.enable = ConfigEnableDisable(default = False)
27 config.plugins.FritzCall.hostname = ConfigIP(default = [192, 168, 178, 1])
28 config.plugins.FritzCall.filter = ConfigEnableDisable(default = False)
29 config.plugins.FritzCall.filtermsn = ConfigText(default = "", fixed_size = False)
30 config.plugins.FritzCall.showOutgoing = ConfigEnableDisable(default = False)
31 config.plugins.FritzCall.timeout = ConfigInteger(default = 15, limits = (0,60))
32 config.plugins.FritzCall.lookup = ConfigEnableDisable(default = False)
33 config.plugins.FritzCall.internal = ConfigEnableDisable(default = False)
34 config.plugins.FritzCall.fritzphonebook = ConfigEnableDisable(default = False)
35 config.plugins.FritzCall.phonebook = ConfigEnableDisable(default = False)
36 config.plugins.FritzCall.addcallers = ConfigEnableDisable(default = False)
37 config.plugins.FritzCall.phonebookLocation = ConfigSelection(choices = [("/media/usb/PhoneBook.txt", _("USB Stick")), ("/media/cf/PhoneBook.txt", _("CF Drive")), ("/media/hdd/PhoneBook.txt", _("Harddisk"))])
38 config.plugins.FritzCall.password = ConfigText(default = "", fixed_size = False)
39 config.plugins.FritzCall.prefix = ConfigText(default = "", fixed_size = False)
40                 
41 class FritzCallPhonebook:
42         def __init__(self):
43                 self.phonebook = {}
44                 self.reload()
45                 
46         def notify(self, text):
47                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_ERROR, timeout=config.plugins.FritzCall.timeout.value)
48
49         def create(self):
50                 try:
51                         f = open(config.plugins.FritzCall.phonebookLocation.value, 'w')
52                         f.write("01234567890#Name, Street, Location (Keep the Spaces!!!)\n");
53                         f.close()
54                         return True
55                 except:
56                         return False
57         
58         def error(self, error):
59                 if self.event == "LOGIN":
60                         text = _("Fritz!Box Login failed! - Error: %s") %error
61                         self.notify(text)
62                 elif self.event == "LOAD":
63                         text = _("Could not load phonebook from Fritz!Box - Error: %s") %error
64                         self.notify(text)
65
66         def loadFritzBoxPhonebook(self):
67                 print "[FritzCallPhonebook] loadFritzBoxPhonebook"
68                 self.event = "LOAD"
69                 
70                 host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
71                 uri = "/cgi-bin/webcm"# % tuple(config.plugins.FritzCall.hostname.value)
72                 parms = urlencode({'getpage':'../html/de/menus/menu2.html', 'var:lang':'de','var:pagename':'fonbuch','var:menu':'fon'})
73                         
74                 url = "http://%s%s?%s" %(host, uri, parms)
75                 
76                 getPage(url).addCallback(self._gotPage).addErrback(self.error)
77                 
78         def parseFritzBoxPhonebook(self, html):         
79                 found = re.match('.*<table id="tList".*?</tr>\n(.*?)</table>', html, re.S)
80                 
81                 if found:                                                                                       
82                         table = found.group(1)                                                  
83                         text = re.sub("<.*?>", "", table)                               
84                         text = text.split('\n')
85                          
86                         for line in text:                       
87                                 if line.strip() != "":
88                                         try:
89                                                 line = line.replace("\"", "")
90                                                 line = line.split(", ")
91                                                 name = line[1]
92                                                 number = line[2]
93                                                 name = name.replace("&szlig;", "?").replace("&auml;", "?").replace("&ouml;", "?").replace("&uuml;", "?").replace("&Auml;", "?").replace("&Ouml;", "?").replace("&Uuml;", "?")
94                                                 print "[FritzCallPhonebook] Adding '''%s''' with '''%s''' from Fritz!Box Phonebook!" %(name, number)
95                                                 self.phonebook[number.strip()] = name.strip()
96                                                 
97                                         except IOError():
98                                                 print "[FritzCallPhonebook] Could not parse Fritz!Box Phonebook entry"
99         
100         def _gotPage(self, html):
101 #               print "[FritzCallPhonebook] _gotPage"
102                 # workaround: exceptions in gotPage-callback were ignored
103                 try:
104                         if self.event == "LOGIN":
105                                 self.verifyLogin(html)
106                         if self.event == "LOAD":
107                                 self.parseFritzBoxPhonebook(html)
108                 except:
109                         import traceback, sys
110                         traceback.print_exc(file=sys.stdout)
111                         #raise e
112         
113         def login(self):
114                 print "[FritzCallPhonebook] Login"
115                 self.event = "LOGIN"
116                         
117                 host = "%d.%d.%d.%d" %tuple(config.plugins.FritzCall.hostname.value)
118                 uri =  "/cgi-bin/webcm"
119                 parms = "login:command/password=%s" %(config.plugins.FritzCall.password.value)          
120                 url = "http://%s%s" %(host, uri)                
121
122                 getPage(url, method="POST", headers = {'Content-Type': "application/x-www-form-urlencoded",'Content-Length': str(len(parms))}, postdata=parms).addCallback(self._gotPage).addErrback(self.error)
123                 
124         def verifyLogin(self, html):
125 #               print "[FritzCallPhonebook] verifyLogin - html: %s" %html
126                 self.event = "LOAD"
127                 found = re.match('.*<p class="errorMessage">FEHLER:&nbsp;Das angegebene Kennwort', html, re.S)
128                 if not found:
129                         self.loadFritzBoxPhonebook()
130                 else:
131                         text = _("Fritz!Box Login failed! - Wrong Password!")
132                         self.notify(text)
133
134         def reload(self):
135 #               print "[FritzCallPhonebook] reload"
136                 self.phonebook.clear()
137                 exists = False
138                 if not os_path.exists(config.plugins.FritzCall.phonebookLocation.value):
139                         if(self.create()):
140                                 exists = True
141                 else:
142                         exists = True
143                                 
144                 if exists:
145                         for line in open(config.plugins.FritzCall.phonebookLocation.value):
146                                 try:
147                                         number, name = line.split("#")
148                                         if not self.phonebook.has_key(number):  
149                                                 self.phonebook[number] = name 
150                                 except IOError():
151                                         print "[FritzCallPhonebook] Could not parse internal Phonebook Entry %s" %line 
152
153                 if config.plugins.FritzCall.fritzphonebook.value:
154                         if config.plugins.FritzCall.password.value != "":
155                                 self.login()
156                         else:
157                                 self.loadFritzBoxPhonebook()
158
159         def search(self, number):
160 #               print "[FritzCallPhonebook] Searching for %s" %number
161                 name = None
162                 if config.plugins.FritzCall.phonebook.value:
163                         if self.phonebook.has_key(number):
164                                 name = self.phonebook[number].replace(", ", "\n")
165                 return name
166
167         def add(self, number, name):
168 #               print "[FritzCallPhonebook] add"
169                 if config.plugins.FritzCall.phonebook.value and config.plugins.FritzCall.addcallers.value:                      
170                         try:
171                                 f = open(config.plugins.FritzCall.phonebookLocation.value, 'a')
172                                 name = name.strip() + "\n"
173                                 string = "%s#%s" %(number, name)                
174                                 self.phonebook[number] = name;  
175                                 f.write(string)                                 
176                                 f.close()
177                                 return True
178         
179                         except IOError():
180                                 return False
181
182 phonebook = FritzCallPhonebook()
183                 
184 class FritzCallSetup(ConfigListScreen, Screen):
185         skin = """
186                 <screen position="100,90" size="550,420" title="FritzCall Setup" >
187                 <widget name="config" position="20,10" size="510,300" scrollbarMode="showOnDemand" />
188                 <widget name="consideration" position="20,320" font="Regular;20" halign="center" size="510,50" />
189                 </screen>"""
190
191         def __init__(self, session, args = None):
192                 
193                 Screen.__init__(self, session)
194                 
195                 self["consideration"] = Label(_("You need to enable the monitoring on your Fritz!Box by dialing #96*5*!"))
196                 self.list = []
197                 
198                 self["setupActions"] = ActionMap(["SetupActions"], 
199                 {
200                         "save": self.save, 
201                         "cancel": self.cancel, 
202                         "ok": self.save, 
203                 }, -2)
204
205                 ConfigListScreen.__init__(self, self.list)
206                 self.createSetup()
207
208                 
209         def keyLeft(self):
210                 ConfigListScreen.keyLeft(self)
211                 self.createSetup()
212
213         def keyRight(self):
214                 ConfigListScreen.keyRight(self)
215                 self.createSetup()
216
217         def createSetup(self):
218                 self.list = [ ]
219                 self.list.append(getConfigListEntry(_("Call monitoring"), config.plugins.FritzCall.enable))
220                 if config.plugins.FritzCall.enable.value:
221                         self.list.append(getConfigListEntry(_("Fritz!Box FON IP address"), config.plugins.FritzCall.hostname))
222                         
223                         self.list.append(getConfigListEntry(_("Show Calls for specific MSN"), config.plugins.FritzCall.filter))
224                         if config.plugins.FritzCall.filter.value:
225                                 self.list.append(getConfigListEntry(_("MSN to show"), config.plugins.FritzCall.filtermsn))
226                                 
227                         self.list.append(getConfigListEntry(_("Show Outgoing Calls"), config.plugins.FritzCall.showOutgoing))
228                         self.list.append(getConfigListEntry(_("Timeout for Call Notifications (seconds)"), config.plugins.FritzCall.timeout))
229                         self.list.append(getConfigListEntry(_("Reverse Lookup Caller ID (DE only)"), config.plugins.FritzCall.lookup))
230                 
231                         self.list.append(getConfigListEntry(_("Read PhoneBook from Fritz!Box"), config.plugins.FritzCall.fritzphonebook))
232                         if config.plugins.FritzCall.fritzphonebook.value:
233                                 self.list.append(getConfigListEntry(_("Password Accessing Fritz!Box"), config.plugins.FritzCall.password))
234                         
235                         self.list.append(getConfigListEntry(_("Use internal PhoneBook"), config.plugins.FritzCall.phonebook))
236                         if config.plugins.FritzCall.phonebook.value:
237                                 self.list.append(getConfigListEntry(_("PhoneBook Location"), config.plugins.FritzCall.phonebookLocation))
238                                 self.list.append(getConfigListEntry(_("Automatically add new Caller to PhoneBook"), config.plugins.FritzCall.addcallers))
239                         
240                         self.list.append(getConfigListEntry(_("Strip Leading 0"), config.plugins.FritzCall.internal))
241                         self.list.append(getConfigListEntry(_("Prefix for Outgoing Calls"), config.plugins.FritzCall.prefix))
242                 
243                 self["config"].list = self.list
244                 self["config"].l.setList(self.list)
245
246         def save(self):
247 #               print "[FritzCallSetup] save"
248                 for x in self["config"].list:
249                         x[1].save()
250                 if fritz_call is not None:
251                         fritz_call.connect()
252
253                         if config.plugins.FritzCall.phonebook.value:
254                                 if not os_path.exists(config.plugins.FritzCall.phonebookLocation.value):
255                                         if not phonebook.create():
256                                                 Notifications.AddNotification(MessageBox, _("Can't create PhoneBook.txt"), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)                                   
257                                 else:
258                                         print "[FritzCallSetup] called phonebook.reload()"
259                                         phonebook.reload()
260                 
261                 self.close()
262
263         def cancel(self):
264 #               print "[FritzCallSetup] cancel"
265                 for x in self["config"].list:
266                         x[1].cancel()
267                 self.close()    
268
269 class FritzProtocol(LineReceiver):
270         def __init__(self):
271 #               print "[FritzProtocol] __init__"
272                 self.resetValues()
273         
274         def resetValues(self):
275 #               print "[FritzProtocol] resetValues"
276                 self.number = '0'
277                 self.caller = None
278                 self.phone = None
279                 self.date = '0'
280         
281         def notify(self, text, timeout=config.plugins.FritzCall.timeout.value):
282                 Notifications.AddNotification(MessageBox, text, type=MessageBox.TYPE_INFO, timeout=timeout)
283         
284         def handleIncoming(self):
285 #               print "[FritzProtocol] handle Incoming!"
286                 
287                 text = _("Incoming Call ")
288                 if self.caller is not None:
289                         text += _("on %s from\n---------------------------------------------\n%s\n%s\n---------------------------------------------\nto: %s") % (self.date, self.number, self.caller, self.phone)
290                 else:
291                         text += _("on %s from\n---------------------------------------------\n%s (UNKNOWN)\n---------------------------------------------\nto: %s") % (self.date, self.number, self.phone)
292                 
293                 self.notify(text)
294                 self.resetValues()
295
296         def handleOutgoing(self):
297 #               print "[FritzProtocol] handle Outgoing!"
298                 text = _("Outgoing Call ")
299                 if(self.caller is not None):    
300                         text += _("on %s to\n---------------------------------------------\n%s\n%s\n---------------------------------------------\nfrom: %s") % (self.date, self.number, self.caller, self.phone)
301                 else:
302                         text += _("on %s to\n---------------------------------------------\n%s (UNKNOWN)\n\n---------------------------------------------\nfrom: %s") % (self.date, self.number, self.phone)#
303
304                 self.notify(text)
305                 self.resetValues()
306
307         def handleEvent(self):
308 #               print "[FritzProtocol] handleEvent!"
309                 if self.event == "RING":
310                         self.handleIncoming()
311                 elif self.event == "CALL":
312                         self.handleOutgoing()
313                 
314         def handleEventOnError(self, error):
315 #               print "[FritzProtocol] handleEventOnError - Error :%s" %error
316                 self.handleEvent()
317                 
318         def _gotPage(self, data):
319 #               print "[FritzProtocol] _gotPage"
320                 try:
321                         self.gotPage(data)
322                 except:
323                         import traceback, sys
324                         traceback.print_exc(file=sys.stdout)
325                         #raise e
326                         self.handleEvent()
327         
328         def gotPage(self, html):
329 #               print "[FritzProtocol] gotPage"
330                 found = re.match('.*<td.*?class="cel-data border.*?>(.*?)</td>', html, re.S)
331                 if found:                                                                       
332                         td = found.group(1)                                     # group(1) is the content of (.*?) in our pattern
333                         td.decode("ISO-8859-1").encode("UTF-8")
334                         text = re.sub("<.*?>", "", td)          # remove tags and their content
335                         text = text.split("\n")
336
337                         #wee need to strip the values as there a lots of whitespaces
338                         name = text[2].strip()
339                         address = text[8].replace("&nbsp;", " ").replace(", ", "\n").strip();
340 #                       print "[FritzProtocol] Reverse lookup succeeded:\nName: %s\n\nAddress: %s" %(name, address)
341                         
342                         self.caller = "%s\n%s" %(name, address)
343                         
344                         #Autoadd to PhoneBook.txt if enabled
345                         if config.plugins.FritzCall.addcallers.value and self.event == "RING":
346                                 phonebook.add(self.number, self.caller.replace("\n", ", "))
347 #               else:
348 #                       print "[FritzProtocol] Reverse lookup without result!"  
349
350                 self.handleEvent()
351                 
352         def reverseLookup(self):
353 #               print "[FritzProtocol] reverse Lookup!"
354                 url = "http://www.dasoertliche.de/?form_name=search_inv&ph=%s" %self.number
355                 getPage(url,method="GET").addCallback(self._gotPage).addErrback(self.handleEventOnError)
356
357         def lineReceived(self, line):
358 #               print "[FritzProtocol] lineReceived"
359 #15.07.06 00:38:54;CALL;1;4;<provider>;<callee>;
360 #15.07.06 00:38:58;DISCONNECT;1;0;
361 #15.07.06 00:39:22;RING;0;<caller>;<outgoing msn>;
362 #15.07.06 00:39:27;DISCONNECT;0;0;
363
364                 a = line.split(';')
365                 (self.date, self.event) = a[0:2]
366                 
367                 #incoming Call
368                 if self.event == "RING":
369                         phone = a[4]
370                         
371                         if not config.plugins.FritzCall.filter.value or config.plugins.FritzCall.filtermsn.value == phone:      
372                                 phonename = phonebook.search(phone)
373                                 if phonename is not None:
374                                         self.phone = "%s (%s)" %(phone, phonename)
375                                 else:
376                                         self.phone = phone
377                                 
378                                 if config.plugins.FritzCall.internal.value and a[3][0]=="0" and len(a[3]) > 3:
379                                         self.number = a[3][1:]
380                                 else:
381                                         self.number = a[3]
382                                 
383                                 self.caller = phonebook.search(self.number)
384                                 if self.caller is None:
385                                         if config.plugins.FritzCall.lookup.value:
386                                                 self.reverseLookup()
387                                         else:
388                                                 self.handleEvent()
389                                 else:
390                                         self.handleEvent()
391                 
392                 #Outgoing Call
393                 elif config.plugins.FritzCall.showOutgoing.value and self.event == "CALL":
394                         self.phone = a[4]
395
396                         if not config.plugins.FritzCall.filter.value or config.plugins.FritzCall.filtermsn.value == self.phone:
397
398                                 if config.plugins.FritzCall.internal.value and a[5][0]=="0" and len(a[3]) > 3:
399                                         self.number = a[5][1:]
400                                 else:
401                                         self.number = a[5]
402                                         
403                                 self.caller = phonebook.search(self.number)
404
405                                 if self.number[0] != '0':
406                                         self.number = config.plugins.FritzCall.prefix.value + self.number
407                                 
408                                 if self.caller is None:
409                                         if config.plugins.FritzCall.lookup.value:
410                                                 self.reverseLookup()
411                                 else:
412                                         self.handleEvent()
413                                 
414                                                                 
415 class FritzClientFactory(ReconnectingClientFactory):
416         initialDelay = 20
417         maxDelay = 500
418         
419         def __init__(self):
420                 self.hangup_ok = False
421
422         def startedConnecting(self, connector):
423                 Notifications.AddNotification(MessageBox, _("Connecting to Fritz!Box..."), type=MessageBox.TYPE_INFO, timeout=2)
424         
425         def buildProtocol(self, addr):
426                 Notifications.AddNotification(MessageBox, _("Connected to Fritz!Box!"), type=MessageBox.TYPE_INFO, timeout=4)
427                 self.resetDelay()
428                 return FritzProtocol()
429         
430         def clientConnectionLost(self, connector, reason):
431                 if not self.hangup_ok:
432                         Notifications.AddNotification(MessageBox, _("Connection to Fritz!Box! lost\n (%s)\nretrying...") % reason.getErrorMessage(), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
433                 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
434         
435         def clientConnectionFailed(self, connector, reason):
436                 Notifications.AddNotification(MessageBox, _("Connecting to Fritz!Box failed\n (%s)\nretrying...") % reason.getErrorMessage(), type=MessageBox.TYPE_INFO, timeout=config.plugins.FritzCall.timeout.value)
437                 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
438
439 class FritzCall:
440         def __init__(self):
441                 self.dialog = None
442                 self.d = None
443                 self.connect()
444         
445         def connect(self):      
446                 self.abort()
447                 if config.plugins.FritzCall.enable.value:
448                         f = FritzClientFactory()
449                         self.d = (f, reactor.connectTCP("%d.%d.%d.%d" % tuple(config.plugins.FritzCall.hostname.value), 1012, f))
450
451         def shutdown(self):
452                 self.abort()
453
454         def abort(self):
455                 if self.d is not None:
456                         self.d[0].hangup_ok = True 
457                         self.d[0].stopTrying()
458                         self.d[1].disconnect()
459                         self.d = None
460
461 def main(session):
462         session.open(FritzCallSetup)
463
464 fritz_call = None
465
466 def autostart(reason, **kwargs):
467         global fritz_call
468         
469         # ouch, this is a hack  
470         if kwargs.has_key("session"):
471                 global my_global_session
472                 my_global_session = kwargs["session"]
473                 return
474         
475         print "[Fritz!Call] - Autostart"
476         if reason == 0:
477                 fritz_call = FritzCall()
478         elif reason == 1:
479                 fritz_call.shutdown()
480                 fritz_call = None
481
482 def Plugins(**kwargs):
483         return [ PluginDescriptor(name="FritzCall", description="Display Fritzbox-Fon calls on screen", where = PluginDescriptor.WHERE_PLUGINMENU, fnc=main), 
484                 PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart) ]