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