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