1""" 2Module to access lists of recent phone calls: incoming, outgoing and 3missed ones. 4""" 5# This module is part of the FritzConnection package. 6# https://github.com/kbr/fritzconnection 7# License: MIT (https://opensource.org/licenses/MIT) 8# Author: Klaus Bremer 9 10 11import datetime 12 13from ..core.processor import ( 14 processor, 15 process_node, 16 InstanceAttributeFactory, 17 Storage, 18) 19from ..core.utils import get_xml_root 20from .fritzbase import AbstractLibraryBase 21 22 23__all__ = ['FritzCall', 'Call'] 24 25 26ALL_CALL_TYPES = 0 27RECEIVED_CALL_TYPE = 1 28MISSED_CALL_TYPE = 2 29OUT_CALL_TYPE = 3 30ACTIVE_RECEIVED_CALL_TYPE = 9 31REJECTED_CALL_TYPE = 10 32ACTIVE_OUT_CALL_TYPE = 11 33 34SERVICE = 'X_AVM-DE_OnTel1' 35 36 37def datetime_converter(date_string): 38 if not date_string: 39 return date_string 40 return datetime.datetime.strptime(date_string, '%d.%m.%y %H:%M') 41 42 43def timedelta_converter(duration_string): 44 if not duration_string: 45 return duration_string 46 hours, minutes = [int(part) for part in duration_string.split(':', 1)] 47 return datetime.timedelta(hours=hours, minutes=minutes) 48 49 50class FritzCall(AbstractLibraryBase): 51 """ 52 Can dial phone numbers and gives access to lists of recent phone 53 calls: incoming, outgoing and missed ones. All parameters are 54 optional. If given, they have the following meaning: `fc` is an 55 instance of FritzConnection, `address` the ip of the Fritz!Box, 56 `port` the port to connect to, `user` the username, `password` the 57 password, `timeout` a timeout as floating point number in seconds, 58 `use_tls` a boolean indicating to use TLS (default False). 59 """ 60 def __init__(self, *args, **kwargs): 61 super().__init__(*args, **kwargs) 62 self.calls = None 63 64 def _update_calls(self, num=None, days=None): 65 result = self.fc.call_action(SERVICE, 'GetCallList') 66 url = result['NewCallListURL'] 67 if days: 68 url += f'&days={days}' 69 elif num: 70 url += f'&max={num}' 71 root = get_xml_root(url, session=self.fc.session) 72 self.calls = CallCollection(root) 73 74 def get_calls(self, calltype=ALL_CALL_TYPES, update=True, 75 num=None, days=None): 76 """ 77 Return a list of Call instances of type calltypes. If calltype 78 is 0 all calls are listet. If *update* is True, all calls are 79 reread from the router. *num* maximum number of entries in call 80 list. *days* number of days to look back for calls e.g. 1: calls 81 from today and yesterday, 7: calls from the complete last week. 82 """ 83 if not self.calls: 84 update = True 85 if update: 86 self._update_calls(num, days) 87 if calltype == ALL_CALL_TYPES: 88 return self.calls.calls 89 return [call for call in self.calls if call.type == calltype] 90 91 def get_received_calls(self, update=True, num=None, days=None): 92 """ 93 Return a list of Call instances of received calls. If *update* 94 is True, all calls are reread from the router. *num* maximum 95 number of entries in call list. *days* number of days to look 96 back for calls e.g. 1: calls from today and yesterday, 7: calls 97 from the complete last week. 98 """ 99 return self.get_calls(RECEIVED_CALL_TYPE, update, num, days) 100 101 def get_missed_calls(self, update=True, num=None, days=None): 102 """ 103 Return a list of Call instances of missed calls. If *update* is 104 True, all calls are reread from the router. *num* maximum number 105 of entries in call list. *days* number of days to look back for 106 calls e.g. 1: calls from today and yesterday, 7: calls from the 107 complete last week. 108 """ 109 return self.get_calls(MISSED_CALL_TYPE, update, num, days) 110 111 def get_out_calls(self, update=True, num=None, days=None): 112 """ 113 Return a list of Call instances of outgoing calls. If *update* 114 is True, all calls are reread from the router. *num* maximum 115 number of entries in call list. *days* number of days to look 116 back for calls e.g. 1: calls from today and yesterday, 7: calls 117 from the complete last week. 118 """ 119 return self.get_calls(OUT_CALL_TYPE, update, num, days) 120 121 def dial(self, number): 122 """ 123 Dials the given *number* (number must be a string, as phone 124 numbers are allowed to start with leading zeros). This method 125 has no return value, but will raise an error reported from the 126 Fritz!Box on failure. **Note:** The dial-help of the Fritz!Box 127 must be activated to make this work. 128 """ 129 arg = {'NewX_AVM-DE_PhoneNumber': number} 130 self.fc.call_action('X_VoIP1', 'X_AVM-DE_DialNumber', arguments=arg) 131 132 133class AttributeConverter: 134 """ 135 Data descriptor returning converted attribute values. 136 """ 137 def __init__(self, attribute_name, converter=str): 138 self.attribute_name = attribute_name 139 self.converter = converter 140 141 def __set__(self, obj, value): 142 return NotImplemented 143 144 def __get__(self, obj, objtype): 145 attr = getattr(obj, self.attribute_name) 146 try: 147 attr = self.converter(attr) 148 except (TypeError, ValueError): 149 pass 150 return attr 151 152 153@processor 154class Call: 155 """ 156 Represents a call with the attributes provided by AVM. Instance 157 attributes are *Id*, *Type*, *Called*, *Caller*, *CallerNumber*, 158 *CalledNumber*, *Name*, *Device*, *Port*, *Date*, *Duration* and 159 *Count*. The spelling represents the original xml-node names. 160 Additionally the following attributes can be accessed by lowercase 161 names: *id* returning the Id as integer, *type* returning the Type 162 as integer, *date* returning the Date as datetime-instance, 163 *duration* returning the Duration as timedelta-instance. 164 """ 165 id = AttributeConverter('Id', int) 166 type = AttributeConverter('Type', int) 167 date = AttributeConverter('Date', datetime_converter) 168 duration = AttributeConverter('Duration', timedelta_converter) 169 170 def __init__(self): 171 self.Id = None 172 self.Type = None 173 self.Called = None 174 self.Caller = None 175 self.CallerNumber = None 176 self.CalledNumber = None 177 self.Name = None 178 self.Device = None 179 self.Port = None 180 self.Date = None 181 self.Duration = None 182 self.Count = None 183 184 def __str__(self): 185 number = self.Called if self.type == 3 else self.Caller 186 duration = self.Duration if self.type != 2 else "-" 187 if not number: 188 number = "-" 189 return f'{self.Type:>6} {number:24}{self.Date:>18}{duration:>12}' 190 191 192class CallCollection(Storage): 193 """ 194 Container for a sequence of Call instances. 195 """ 196 Call = InstanceAttributeFactory(Call) 197 198 def __init__(self, root): 199 self.timestamp = None 200 self.calls = list() 201 super().__init__(self.calls) 202 process_node(self, root) 203 204 def __iter__(self): 205 return iter(self.calls) 206