Commits

Michel Albert committed bb86f94 Draft

Using "entry points" as executables.

Moving towards shared CLI arguments.

Comments (0)

Files changed (6)

fritzconnection/fritzconnection.py

-# -*- coding: utf-8 -*-
-
-"""
-fritzconnection.py
-
-This is a tool to communicate with the FritzBox.
-All availabel actions (aka commands) and corresponding parameters are
-read from the xml-configuration files requested from the FritzBox. So
-the available actions may change depending on the FritzBox model and
-firmware.
-The command-line interface allows the api-inspection.
-
-#Runs with python >= 2.7 # TODO: test with 2.7
-"""
-
-_version_ = '0.4.2'
-
-import argparse
-import requests
-from requests.auth import HTTPDigestAuth
-
-from lxml import etree
-
-
-# FritzConnection defaults:
-FRITZ_IP_ADDRESS = '169.254.1.1'
-FRITZ_TCP_PORT = 49000
-FRITZ_IGD_DESC_FILE = 'igddesc.xml'
-FRITZ_TR64_DESC_FILE = 'tr64desc.xml'
-FRITZ_USERNAME = 'dslf-config'
-
-
-# version-access:
-def get_version():
-    return _version_
-
-
-class FritzAction(object):
-    """
-    Class representing an action (aka command).
-    Knows how to execute itself.
-    Access to any password-protected action must require HTTP digest
-    authentication.
-    See: http://www.broadband-forum.org/technical/download/TR-064.pdf
-    """
-    header = {'soapaction': '',
-              'content-type': 'text/xml',
-              'charset': 'utf-8'}
-    envelope = """
-        <?xml version="1.0" encoding="utf-8"?>
-        <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
-                    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">%s
-        </s:Envelope>
-        """
-    body_template = """
-        <s:Body>
-        <u:%(action_name)s xmlns:u="%(service_type)s">%(arguments)s
-        </u:%(action_name)s>
-        </s:Body>
-        """
-    argument_template = """
-        <s:%(name)s>%(value)s</s:%(name)s>"""
-
-    address = FRITZ_IP_ADDRESS
-    port = FRITZ_TCP_PORT
-    method = 'post'
-    user = ''
-    password = ''
-
-    def __init__(self, service_type, control_url):
-        self.service_type = service_type
-        self.control_url = control_url
-        self.name = ''
-        self.arguments = {}
-
-    @property
-    def info(self):
-        return [self.arguments[argument].info for argument in self.arguments]
-
-    def _body_builder(self, kwargs):
-        """
-        Helper method to construct the appropriate SOAP-body to call a
-        FritzBox-Service.
-        """
-        p = {
-            'action_name': self.name,
-            'service_type': self.service_type,
-            'arguments': '',
-        }
-        if kwargs:
-            arguments = [
-                self.argument_template % {'name': k, 'value': v}
-                for k, v in kwargs.items()
-            ]
-            p['arguments'] = ''.join(arguments)
-        body = self.body_template % p
-        return body
-
-    def execute(self, **kwargs):
-        """
-        Calls the FritzBox action and returns a dictionary with the arguments.
-        TODO: send arguments in case of tr64-connection.
-        """
-        headers = self.header.copy()
-        headers['soapaction'] = '%s#%s' % (self.service_type, self.name)
-        data = self.envelope % self._body_builder(kwargs)
-        url = 'http://%s:%s%s' % (self.address, self.port, self.control_url)
-        auth = None
-        if self.password:
-            auth = HTTPDigestAuth(self.user, self.password)
-        response = requests.post(url, data=data, headers=headers, auth=auth)
-        # lxml needs bytes, therefor .content (not .text)
-        result = self.parse_response(response.content)
-        return result
-
-    def parse_response(self, response):
-        """
-        Evaluates the action-call response from a FritzBox.
-        The response is a xml byte-string.
-        Returns a dictionary with the received arguments-value pairs.
-        The values are converted according to the given data_types.
-        TODO: boolean and signed integers data-types from tr64 responses
-        """
-        result = {}
-        root = etree.fromstring(response)
-        for argument in self.arguments.values():
-            try:
-                value = root.find('.//%s' % argument.name).text
-            except AttributeError:
-                # will happen by searching for in-parameters and by
-                # parsing responses with status_code != 200
-                continue
-            if argument.data_type.startswith('ui'):
-                try:
-                    value = int(value)
-                except ValueError:
-                    # should not happen
-                    value = None
-            result[argument.name] = value
-        return result
-
-
-class FritzActionArgument(object):
-    """Attribute class for arguments."""
-    name = ''
-    direction = ''
-    data_type = ''
-
-    @property
-    def info(self):
-        return (self.name, self.direction, self.data_type)
-
-
-class FritzService(object):
-    """Attribute class for service."""
-
-    def __init__(self, service_type, control_url, scpd_url):
-        self.service_type = service_type
-        self.control_url = control_url
-        self.scpd_url = scpd_url
-        self.actions = {}
-
-    @property
-    def name(self):
-        return self.service_type.split(':')[-2]
-
-
-class FritzXmlParser(object):
-    """Base class for parsing fritzbox-xml-files."""
-
-    def __init__(self, address, port, filename=None):
-        """Loads and parses an xml-file from a FritzBox."""
-        if address is None:
-            source = filename
-        else:
-            source = 'http://{0}:{1}/{2}'.format(address, port, filename)
-        tree = etree.parse(source)
-        self.root = tree.getroot()
-        self.namespace = etree.QName(self.root.tag).namespace
-
-    def nodename(self, name):
-        """Extends name with the xmlns-prefix to a valid nodename."""
-        return etree.QName(self.root, name)
-
-
-class FritzDescParser(FritzXmlParser):
-    """Class for parsing desc.xml-files."""
-
-    def get_modelname(self):
-        """Returns the FritzBox model name."""
-        xpath = '%s/%s' % (self.nodename('device'), self.nodename('modelName'))
-        return self.root.find(xpath).text
-
-    def get_services(self):
-        """Returns a list of FritzService-objects."""
-        result = []
-        nodes = self.root.iterfind(
-            './/ns:service', namespaces={'ns': self.namespace})
-        for node in nodes:
-            result.append(FritzService(
-                node.find(self.nodename('serviceType')).text,
-                node.find(self.nodename('controlURL')).text,
-                node.find(self.nodename('SCPDURL')).text))
-        return result
-
-
-class FritzSCDPParser(FritzXmlParser):
-    """Class for parsing SCDP.xml-files"""
-
-    def __init__(self, address, port, service, filename=None):
-        """
-        Reads and parses a SCDP.xml-file from FritzBox.
-        'service' is a tuple of containing:
-        (serviceType, controlURL, SCPDURL)
-        'service' is a FritzService object:
-        """
-        self.state_variables = {}
-        self.service = service
-        if filename is None:
-            # access the FritzBox
-            super(FritzSCDPParser, self).__init__(address, port,
-                                                  service.scpd_url)
-        else:
-            # for testing read the xml-data from a file
-            super(FritzSCDPParser, self).__init__(
-                None, None, filename=filename)
-
-    def _read_state_variables(self):
-        """
-        Reads the stateVariable information from the xml-file.
-        The information we like to extract are name and dataType so we
-        can assign them later on to FritzActionArgument-instances.
-        Returns a dictionary: key:value = name:dataType
-        """
-        nodes = self.root.iterfind(
-            './/ns:stateVariable', namespaces={'ns': self.namespace})
-        for node in nodes:
-            key = node.find(self.nodename('name')).text
-            value = node.find(self.nodename('dataType')).text
-            self.state_variables[key] = value
-
-    def get_actions(self):
-        """Returns a list of FritzAction instances."""
-        self._read_state_variables()
-        actions = []
-        nodes = self.root.iterfind(
-            './/ns:action', namespaces={'ns': self.namespace})
-        for node in nodes:
-            action = FritzAction(self.service.service_type,
-                                 self.service.control_url)
-            action.name = node.find(self.nodename('name')).text
-            action.arguments = self._get_arguments(node)
-            actions.append(action)
-        return actions
-
-    def _get_arguments(self, action_node):
-        """
-        Returns a dictionary of arguments for the given action_node.
-        """
-        arguments = {}
-        argument_nodes = action_node.iterfind(
-            r'./ns:argumentList/ns:argument',
-            namespaces={'ns': self.namespace})
-        for argument_node in argument_nodes:
-            argument = self._get_argument(argument_node)
-            arguments[argument.name] = argument
-        return arguments
-
-    def _get_argument(self, argument_node):
-        """
-        Returns a FritzActionArgument instance for the given argument_node.
-        """
-        argument = FritzActionArgument()
-        argument.name = argument_node.find(self.nodename('name')).text
-        argument.direction = argument_node.find(
-            self.nodename('direction')).text
-        rsv = argument_node.find(self.nodename('relatedStateVariable')).text
-        # TODO: track malformed xml-nodes (i.e. misspelled)
-        argument.data_type = self.state_variables.get(rsv, None)
-        return argument
-
-
-class FritzConnection(object):
-    """
-    FritzBox-Interface for status-information
-    """
-    def __init__(self,
-                 address=FRITZ_IP_ADDRESS,
-                 port=FRITZ_TCP_PORT,
-                 user=FRITZ_USERNAME,
-                 password=''):
-        if password and type(password) is list:
-            password = password[0]
-        if user and type(user) is list:
-            user = user[0]
-        FritzAction.address = address
-        FritzAction.port = port
-        FritzAction.user = user
-        FritzAction.password = password
-        self.address = address
-        self.port = port
-        self.modelname = None
-        self.services = {}
-        self._read_descriptions(password)
-
-    def _read_descriptions(self, password):
-        """
-        Read and evaluate the igddesc.xml file
-        and the tr64desc.xml file if a password is given.
-        """
-        descfiles = [FRITZ_IGD_DESC_FILE]
-        if password:
-            descfiles.append(FRITZ_TR64_DESC_FILE)
-        for descfile in descfiles:
-            parser = FritzDescParser(self.address, self.port, descfile)
-            if not self.modelname:
-                self.modelname = parser.get_modelname()
-            services = parser.get_services()
-            self._read_services(services)
-
-    def _read_services(self, services):
-        """Get actions from services."""
-        for service in services:
-            parser = FritzSCDPParser(self.address, self.port, service)
-            actions = parser.get_actions()
-            service.actions = {action.name: action for action in actions}
-            self.services[service.name] = service
-
-    @property
-    def actionnames(self):
-        """
-        Returns a alphabetical sorted list of tuples with all known
-        service- and action-names.
-        """
-        actions = []
-        for service_name in sorted(self.services.keys()):
-            action_names = self.services[service_name].actions.keys()
-            for action_name in sorted(action_names):
-                actions.append((service_name, action_name))
-        return actions
-
-    def get_action_arguments(self, service_name, action_name):
-        """
-        Returns a list of tuples with all known arguments for the given
-        service- and action-name combination. The tuples contain the
-        argument-name, direction and data_type.
-        """
-        return self.services[service_name].actions[action_name].info
-
-    def call_action(self, service_name, action_name, **kwargs):
-        """Executes the given action. Raise a KeyError on unkown actions."""
-        action = self.services[service_name].actions[action_name]
-        return action.execute(**kwargs)
-
-    def reconnect(self):
-        """
-        Terminate the connection and reconnects with a new ip.
-        Will raise a KeyError if this command is unknown (by any means).
-        """
-        self.call_action('WANIPConnection', 'ForceTermination')
-
-
-# ---------------------------------------------------------
-# Inspection class for cli use:
-# ---------------------------------------------------------
-
-class FritzInspection(object):
-
-    def __init__(self, address, port, username, password):
-        self.fc = FritzConnection(address, port, username, password)
-
-    def get_servicenames(self):
-        return sorted(self.fc.services.keys())
-
-    def get_actionnames(self, servicename):
-        try:
-            service = self.fc.services[servicename]
-        except KeyError:
-            return []
-        return sorted(service.actions.keys())
-
-    def view_header(self):
-        print('\nFritzConnection:')
-        print('{:<20}{}'.format('version:', get_version()))
-        print('{:<20}{}'.format('model:', self.fc.modelname))
-
-    def view_servicenames(self):
-        print('Servicenames:')
-        for name in self.get_servicenames():
-            print('{:20}{}'.format('', name))
-
-    def view_actionnames(self, servicename):
-        print('\n{:<20}{}'.format('Servicename:', servicename))
-        print('Actionnames:')
-        for name in self.get_actionnames(servicename):
-            print('{:20}{}'.format('', name))
-
-    def view_actionarguments(self, servicename, actionname):
-        print('\n{:<20}{}'.format('Servicename:', servicename))
-        print('{:<20}{}'.format('Actionname:', actionname))
-        print('Arguments:')
-        self._view_arguments('{:20}{}', servicename, actionname)
-
-    def view_servicearguments(self, servicename):
-        print('\n{:<20}{}'.format('Servicename:', servicename))
-        actionnames = self.get_actionnames(servicename)
-        for actionname in actionnames:
-            print('{:<20}{}'.format('Actionname:', actionname))
-            self._view_arguments('{:24}{}', servicename, actionname)
-
-    def _view_arguments(self, fs, servicename, actionname):
-        for argument in sorted(
-                self.fc.get_action_arguments(servicename, actionname)):
-            print(fs.format('', argument))
-
-    def view_complete(self):
-        print('FritzBox API:')
-        for servicename in self.get_servicenames():
-            self.view_servicearguments(servicename)
-
-
-# ---------------------------------------------------------
-# cli-section:
-# ---------------------------------------------------------
-
-def get_cli_arguments():
-    parser = argparse.ArgumentParser(description='FritzBox API')
-    parser.add_argument('-i', '--ip-address',
-                        nargs='?', default=FRITZ_IP_ADDRESS,
-                        dest='address',
-                        help='Specify ip-address of the FritzBox to connect '
-                             'to. Default: %s' % FRITZ_IP_ADDRESS)
-    parser.add_argument('--port',
-                        nargs='?', default=FRITZ_TCP_PORT,
-                        help='Port of the FritzBox to connect to. '
-                             'Default: %s' % FRITZ_TCP_PORT)
-    parser.add_argument('-u', '--username',
-                        nargs=1, default='',
-                        help='Fritzbox authentication username')
-    parser.add_argument('-p', '--password',
-                        nargs=1, default='',
-                        help='Fritzbox authentication password')
-    parser.add_argument('-r', '--reconnect',
-                        action='store_true',
-                        help='Reconnect and get a new ip')
-    parser.add_argument('-s', '--services',
-                        action='store_true',
-                        help='List all available services')
-    parser.add_argument('-S', '--serviceactions',
-                        nargs=1,
-                        help='List actions for the given service: <service>')
-    parser.add_argument('-a', '--servicearguments',
-                        nargs=1,
-                        help='List arguments for the actions of a'
-                             'specified service: <service>.')
-    parser.add_argument('-A', '--actionarguments',
-                        nargs=2,
-                        help='List arguments for the given action of a'
-                             'specified service: <service> <action>.')
-    parser.add_argument('-c', '--complete',
-                        action='store_true',
-                        help=('List all services with actionnames and '
-                              'arguments.')
-                        )
-    args = parser.parse_args()
-    return args
-
-
-if __name__ == '__main__':
-    args = get_cli_arguments()
-    fi = FritzInspection(args.address, args.port, args.username, args.password)
-    fi.view_header()
-    if args.services:
-        fi.view_servicenames()
-    elif args.serviceactions:
-        fi.view_actionnames(args.serviceactions[0])
-    elif args.servicearguments:
-        fi.view_servicearguments(args.servicearguments[0])
-    elif args.actionarguments:
-        fi.view_actionarguments(args.actionarguments[0],
-                                args.actionarguments[1])
-    elif args.complete:
-        fi.view_complete()
-    print()  # print an empty line

fritzconnection/fritzhosts.py

 _version_ = '0.1.0'
 
 import argparse
-import fritzconnection
+from . import model
 
 
 SERVICE = 'Hosts'
 
     def __init__(self,
                  fc=None,
-                 address=fritzconnection.FRITZ_IP_ADDRESS,
-                 port=fritzconnection.FRITZ_TCP_PORT,
-                 user=fritzconnection.FRITZ_USERNAME,
+                 address=model.FRITZ_IP_ADDRESS,
+                 port=model.FRITZ_TCP_PORT,
+                 user=model.FRITZ_USERNAME,
                  password=''):
         super(FritzHosts, self).__init__()
         if fc is None:
-            fc = fritzconnection.FritzConnection(address, port, user, password)
+            fc = model.FritzConnection(address, port, user, password)
         self.fc = fc
 
     def action(self, actionname, **kwargs):
 def _get_cli_arguments():
     parser = argparse.ArgumentParser(description='FritzBox Hosts')
     parser.add_argument('-i', '--ip-address',
-                        nargs='?', default=fritzconnection.FRITZ_IP_ADDRESS,
+                        nargs='?', default=model.FRITZ_IP_ADDRESS,
                         dest='address',
                         help='ip-address of the FritzBox to connect to. '
-                             'Default: %s' % fritzconnection.FRITZ_IP_ADDRESS)
+                             'Default: %s' % model.FRITZ_IP_ADDRESS)
     parser.add_argument('--port',
-                        nargs='?', default=fritzconnection.FRITZ_TCP_PORT,
+                        nargs='?', default=model.FRITZ_TCP_PORT,
                         dest='port',
                         help='port of the FritzBox to connect to. '
-                             'Default: %s' % fritzconnection.FRITZ_TCP_PORT)
+                             'Default: %s' % model.FRITZ_TCP_PORT)
     parser.add_argument('-u', '--username',
-                        nargs=1, default=fritzconnection.FRITZ_USERNAME,
+                        nargs=1, default=model.FRITZ_USERNAME,
                         help='Fritzbox authentication username')
     parser.add_argument('-p', '--password',
                         nargs=1, default='',
         _print_nums(fh)
     else:
         _print_hosts(fh)
-
-
-if __name__ == '__main__':
-    _print_status(_get_cli_arguments())

fritzconnection/fritzmonitor.py

     # python 3
     import tkinter as tk
     import tkinter.font as tkfont
-import fritzconnection
+from . import model
 import fritzstatus
 import fritztools
 
 
     def __init__(self,
                  master=None,
-                 address=fritzconnection.FRITZ_IP_ADDRESS,
-                 port=fritzconnection.FRITZ_TCP_PORT):
+                 address=model.FRITZ_IP_ADDRESS,
+                 port=model.FRITZ_TCP_PORT):
         tk.Frame.__init__(self, master)
         self.status = fritzstatus.FritzStatus(address=address, port=port)
         self.max_upstream, self.max_downstream = self.status.max_byte_rate
 def _get_cli_arguments():
     parser = argparse.ArgumentParser(description='FritzBox Monitor')
     parser.add_argument('-i', '--ip-address',
-                        nargs='?', default=fritzconnection.FRITZ_IP_ADDRESS,
+                        nargs='?', default=model.FRITZ_IP_ADDRESS,
                         dest='address',
                         help='ip-address of the FritzBox to connect to. '
-                             'Default: %s' % fritzconnection.FRITZ_IP_ADDRESS)
+                             'Default: %s' % model.FRITZ_IP_ADDRESS)
     parser.add_argument('-p', '--port',
-                        nargs='?', default=fritzconnection.FRITZ_TCP_PORT,
+                        nargs='?', default=model.FRITZ_TCP_PORT,
                         dest='port',
                         help='port of the FritzBox to connect to. '
-                             'Default: %s' % fritzconnection.FRITZ_TCP_PORT)
+                             'Default: %s' % model.FRITZ_TCP_PORT)
     args = parser.parse_args()
     return args
 
-if __name__ == '__main__':
+
+def main():
     arguments = _get_cli_arguments()
     app = FritzMonitor(address=arguments.address, port=arguments.port)
     app.master.title('FritzMonitor')

fritzconnection/fritzstatus.py

 import argparse
 import collections
 import time
-import fritzconnection
+from . import model
 import fritztools
 
 
     """
 
     def __init__(self,
-                 address=fritzconnection.FRITZ_IP_ADDRESS,
-                 port=fritzconnection.FRITZ_TCP_PORT):
+                 address=model.FRITZ_IP_ADDRESS,
+                 port=model.FRITZ_TCP_PORT):
         super(FritzStatus, self).__init__()
-        self.fc = fritzconnection.FritzConnection(address=address, port=port)
+        self.fc = model.FritzConnection(address=address, port=port)
         self.last_bytes_sent = self.bytes_sent
         self.last_bytes_received = self.bytes_received
         self.last_traffic_call = time.time()
 def _get_cli_arguments():
     parser = argparse.ArgumentParser(description='FritzBox Status')
     parser.add_argument('-i', '--ip-address',
-                        nargs='?', default=fritzconnection.FRITZ_IP_ADDRESS,
+                        nargs='?', default=model.FRITZ_IP_ADDRESS,
                         dest='address',
                         help='ip-address of the FritzBox to connect to. '
-                             'Default: %s' % fritzconnection.FRITZ_IP_ADDRESS)
+                             'Default: %s' % model.FRITZ_IP_ADDRESS)
     parser.add_argument('--port',
-                        nargs='?', default=fritzconnection.FRITZ_TCP_PORT,
+                        nargs='?', default=model.FRITZ_TCP_PORT,
                         dest='port',
                         help='port of the FritzBox to connect to. '
-                             'Default: %s' % fritzconnection.FRITZ_TCP_PORT)
+                             'Default: %s' % model.FRITZ_TCP_PORT)
     args = parser.parse_args()
     return args
 
         ('max. bit rate:', fs.str_max_bit_rate)
     ]).items():
         print('{:<20}{}'.format(status, info))
-
-if __name__ == '__main__':
-    _print_status(_get_cli_arguments())

fritzconnection/model.py

+# -*- coding: utf-8 -*-
+
+"""
+fritzconnection.py
+
+This is a tool to communicate with the FritzBox.
+All availabel actions (aka commands) and corresponding parameters are
+read from the xml-configuration files requested from the FritzBox. So
+the available actions may change depending on the FritzBox model and
+firmware.
+The command-line interface allows the api-inspection.
+
+#Runs with python >= 2.7 # TODO: test with 2.7
+"""
+
+_version_ = '0.4.2'
+
+import argparse
+import requests
+from requests.auth import HTTPDigestAuth
+
+from lxml import etree
+
+
+# FritzConnection defaults:
+FRITZ_IP_ADDRESS = '169.254.1.1'
+FRITZ_TCP_PORT = 49000
+FRITZ_IGD_DESC_FILE = 'igddesc.xml'
+FRITZ_TR64_DESC_FILE = 'tr64desc.xml'
+FRITZ_USERNAME = 'dslf-config'
+
+
+# version-access:
+def get_version():
+    return _version_
+
+
+class FritzAction(object):
+    """
+    Class representing an action (aka command).
+    Knows how to execute itself.
+    Access to any password-protected action must require HTTP digest
+    authentication.
+    See: http://www.broadband-forum.org/technical/download/TR-064.pdf
+    """
+    header = {'soapaction': '',
+              'content-type': 'text/xml',
+              'charset': 'utf-8'}
+    envelope = """
+        <?xml version="1.0" encoding="utf-8"?>
+        <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
+                    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">%s
+        </s:Envelope>
+        """
+    body_template = """
+        <s:Body>
+        <u:%(action_name)s xmlns:u="%(service_type)s">%(arguments)s
+        </u:%(action_name)s>
+        </s:Body>
+        """
+    argument_template = """
+        <s:%(name)s>%(value)s</s:%(name)s>"""
+
+    address = FRITZ_IP_ADDRESS
+    port = FRITZ_TCP_PORT
+    method = 'post'
+    user = ''
+    password = ''
+
+    def __init__(self, service_type, control_url):
+        self.service_type = service_type
+        self.control_url = control_url
+        self.name = ''
+        self.arguments = {}
+
+    @property
+    def info(self):
+        return [self.arguments[argument].info for argument in self.arguments]
+
+    def _body_builder(self, kwargs):
+        """
+        Helper method to construct the appropriate SOAP-body to call a
+        FritzBox-Service.
+        """
+        p = {
+            'action_name': self.name,
+            'service_type': self.service_type,
+            'arguments': '',
+        }
+        if kwargs:
+            arguments = [
+                self.argument_template % {'name': k, 'value': v}
+                for k, v in kwargs.items()
+            ]
+            p['arguments'] = ''.join(arguments)
+        body = self.body_template % p
+        return body
+
+    def execute(self, **kwargs):
+        """
+        Calls the FritzBox action and returns a dictionary with the arguments.
+        TODO: send arguments in case of tr64-connection.
+        """
+        headers = self.header.copy()
+        headers['soapaction'] = '%s#%s' % (self.service_type, self.name)
+        data = self.envelope % self._body_builder(kwargs)
+        url = 'http://%s:%s%s' % (self.address, self.port, self.control_url)
+        auth = None
+        if self.password:
+            auth = HTTPDigestAuth(self.user, self.password)
+        response = requests.post(url, data=data, headers=headers, auth=auth)
+        # lxml needs bytes, therefor .content (not .text)
+        result = self.parse_response(response.content)
+        return result
+
+    def parse_response(self, response):
+        """
+        Evaluates the action-call response from a FritzBox.
+        The response is a xml byte-string.
+        Returns a dictionary with the received arguments-value pairs.
+        The values are converted according to the given data_types.
+        TODO: boolean and signed integers data-types from tr64 responses
+        """
+        result = {}
+        root = etree.fromstring(response)
+        for argument in self.arguments.values():
+            try:
+                value = root.find('.//%s' % argument.name).text
+            except AttributeError:
+                # will happen by searching for in-parameters and by
+                # parsing responses with status_code != 200
+                continue
+            if argument.data_type.startswith('ui'):
+                try:
+                    value = int(value)
+                except ValueError:
+                    # should not happen
+                    value = None
+            result[argument.name] = value
+        return result
+
+
+class FritzActionArgument(object):
+    """Attribute class for arguments."""
+    name = ''
+    direction = ''
+    data_type = ''
+
+    @property
+    def info(self):
+        return (self.name, self.direction, self.data_type)
+
+
+class FritzService(object):
+    """Attribute class for service."""
+
+    def __init__(self, service_type, control_url, scpd_url):
+        self.service_type = service_type
+        self.control_url = control_url
+        self.scpd_url = scpd_url
+        self.actions = {}
+
+    @property
+    def name(self):
+        return self.service_type.split(':')[-2]
+
+
+class FritzXmlParser(object):
+    """Base class for parsing fritzbox-xml-files."""
+
+    def __init__(self, address, port, filename=None):
+        """Loads and parses an xml-file from a FritzBox."""
+        if address is None:
+            source = filename
+        else:
+            source = 'http://{0}:{1}/{2}'.format(address, port, filename)
+        tree = etree.parse(source)
+        self.root = tree.getroot()
+        self.namespace = etree.QName(self.root.tag).namespace
+
+    def nodename(self, name):
+        """Extends name with the xmlns-prefix to a valid nodename."""
+        return etree.QName(self.root, name)
+
+
+class FritzDescParser(FritzXmlParser):
+    """Class for parsing desc.xml-files."""
+
+    def get_modelname(self):
+        """Returns the FritzBox model name."""
+        xpath = '%s/%s' % (self.nodename('device'), self.nodename('modelName'))
+        return self.root.find(xpath).text
+
+    def get_services(self):
+        """Returns a list of FritzService-objects."""
+        result = []
+        nodes = self.root.iterfind(
+            './/ns:service', namespaces={'ns': self.namespace})
+        for node in nodes:
+            result.append(FritzService(
+                node.find(self.nodename('serviceType')).text,
+                node.find(self.nodename('controlURL')).text,
+                node.find(self.nodename('SCPDURL')).text))
+        return result
+
+
+class FritzSCDPParser(FritzXmlParser):
+    """Class for parsing SCDP.xml-files"""
+
+    def __init__(self, address, port, service, filename=None):
+        """
+        Reads and parses a SCDP.xml-file from FritzBox.
+        'service' is a tuple of containing:
+        (serviceType, controlURL, SCPDURL)
+        'service' is a FritzService object:
+        """
+        self.state_variables = {}
+        self.service = service
+        if filename is None:
+            # access the FritzBox
+            super(FritzSCDPParser, self).__init__(address, port,
+                                                  service.scpd_url)
+        else:
+            # for testing read the xml-data from a file
+            super(FritzSCDPParser, self).__init__(
+                None, None, filename=filename)
+
+    def _read_state_variables(self):
+        """
+        Reads the stateVariable information from the xml-file.
+        The information we like to extract are name and dataType so we
+        can assign them later on to FritzActionArgument-instances.
+        Returns a dictionary: key:value = name:dataType
+        """
+        nodes = self.root.iterfind(
+            './/ns:stateVariable', namespaces={'ns': self.namespace})
+        for node in nodes:
+            key = node.find(self.nodename('name')).text
+            value = node.find(self.nodename('dataType')).text
+            self.state_variables[key] = value
+
+    def get_actions(self):
+        """Returns a list of FritzAction instances."""
+        self._read_state_variables()
+        actions = []
+        nodes = self.root.iterfind(
+            './/ns:action', namespaces={'ns': self.namespace})
+        for node in nodes:
+            action = FritzAction(self.service.service_type,
+                                 self.service.control_url)
+            action.name = node.find(self.nodename('name')).text
+            action.arguments = self._get_arguments(node)
+            actions.append(action)
+        return actions
+
+    def _get_arguments(self, action_node):
+        """
+        Returns a dictionary of arguments for the given action_node.
+        """
+        arguments = {}
+        argument_nodes = action_node.iterfind(
+            r'./ns:argumentList/ns:argument',
+            namespaces={'ns': self.namespace})
+        for argument_node in argument_nodes:
+            argument = self._get_argument(argument_node)
+            arguments[argument.name] = argument
+        return arguments
+
+    def _get_argument(self, argument_node):
+        """
+        Returns a FritzActionArgument instance for the given argument_node.
+        """
+        argument = FritzActionArgument()
+        argument.name = argument_node.find(self.nodename('name')).text
+        argument.direction = argument_node.find(
+            self.nodename('direction')).text
+        rsv = argument_node.find(self.nodename('relatedStateVariable')).text
+        # TODO: track malformed xml-nodes (i.e. misspelled)
+        argument.data_type = self.state_variables.get(rsv, None)
+        return argument
+
+
+class FritzConnection(object):
+    """
+    FritzBox-Interface for status-information
+    """
+    def __init__(self,
+                 address=FRITZ_IP_ADDRESS,
+                 port=FRITZ_TCP_PORT,
+                 user=FRITZ_USERNAME,
+                 password=''):
+        if password and type(password) is list:
+            password = password[0]
+        if user and type(user) is list:
+            user = user[0]
+        FritzAction.address = address
+        FritzAction.port = port
+        FritzAction.user = user
+        FritzAction.password = password
+        self.address = address
+        self.port = port
+        self.modelname = None
+        self.services = {}
+        self._read_descriptions(password)
+
+    def _read_descriptions(self, password):
+        """
+        Read and evaluate the igddesc.xml file
+        and the tr64desc.xml file if a password is given.
+        """
+        descfiles = [FRITZ_IGD_DESC_FILE]
+        if password:
+            descfiles.append(FRITZ_TR64_DESC_FILE)
+        for descfile in descfiles:
+            parser = FritzDescParser(self.address, self.port, descfile)
+            if not self.modelname:
+                self.modelname = parser.get_modelname()
+            services = parser.get_services()
+            self._read_services(services)
+
+    def _read_services(self, services):
+        """Get actions from services."""
+        for service in services:
+            parser = FritzSCDPParser(self.address, self.port, service)
+            actions = parser.get_actions()
+            service.actions = {action.name: action for action in actions}
+            self.services[service.name] = service
+
+    @property
+    def actionnames(self):
+        """
+        Returns a alphabetical sorted list of tuples with all known
+        service- and action-names.
+        """
+        actions = []
+        for service_name in sorted(self.services.keys()):
+            action_names = self.services[service_name].actions.keys()
+            for action_name in sorted(action_names):
+                actions.append((service_name, action_name))
+        return actions
+
+    def get_action_arguments(self, service_name, action_name):
+        """
+        Returns a list of tuples with all known arguments for the given
+        service- and action-name combination. The tuples contain the
+        argument-name, direction and data_type.
+        """
+        return self.services[service_name].actions[action_name].info
+
+    def call_action(self, service_name, action_name, **kwargs):
+        """Executes the given action. Raise a KeyError on unkown actions."""
+        action = self.services[service_name].actions[action_name]
+        return action.execute(**kwargs)
+
+    def reconnect(self):
+        """
+        Terminate the connection and reconnects with a new ip.
+        Will raise a KeyError if this command is unknown (by any means).
+        """
+        self.call_action('WANIPConnection', 'ForceTermination')
+
+
+# ---------------------------------------------------------
+# Inspection class for cli use:
+# ---------------------------------------------------------
+
+class FritzInspection(object):
+
+    def __init__(self, address, port, username, password):
+        self.fc = FritzConnection(address, port, username, password)
+
+    def get_servicenames(self):
+        return sorted(self.fc.services.keys())
+
+    def get_actionnames(self, servicename):
+        try:
+            service = self.fc.services[servicename]
+        except KeyError:
+            return []
+        return sorted(service.actions.keys())
+
+    def view_header(self):
+        print('\nFritzConnection:')
+        print('{:<20}{}'.format('version:', get_version()))
+        print('{:<20}{}'.format('model:', self.fc.modelname))
+
+    def view_servicenames(self):
+        print('Servicenames:')
+        for name in self.get_servicenames():
+            print('{:20}{}'.format('', name))
+
+    def view_actionnames(self, servicename):
+        print('\n{:<20}{}'.format('Servicename:', servicename))
+        print('Actionnames:')
+        for name in self.get_actionnames(servicename):
+            print('{:20}{}'.format('', name))
+
+    def view_actionarguments(self, servicename, actionname):
+        print('\n{:<20}{}'.format('Servicename:', servicename))
+        print('{:<20}{}'.format('Actionname:', actionname))
+        print('Arguments:')
+        self._view_arguments('{:20}{}', servicename, actionname)
+
+    def view_servicearguments(self, servicename):
+        print('\n{:<20}{}'.format('Servicename:', servicename))
+        actionnames = self.get_actionnames(servicename)
+        for actionname in actionnames:
+            print('{:<20}{}'.format('Actionname:', actionname))
+            self._view_arguments('{:24}{}', servicename, actionname)
+
+    def _view_arguments(self, fs, servicename, actionname):
+        for argument in sorted(
+                self.fc.get_action_arguments(servicename, actionname)):
+            print(fs.format('', argument))
+
+    def view_complete(self):
+        print('FritzBox API:')
+        for servicename in self.get_servicenames():
+            self.view_servicearguments(servicename)
+
+
+# ---------------------------------------------------------
+# cli-section:
+# ---------------------------------------------------------
+
+def get_cli_arguments():
+    parser = argparse.ArgumentParser(description='FritzBox API')
+    parser.add_argument('-i', '--ip-address',
+                        nargs='?', default=FRITZ_IP_ADDRESS,
+                        dest='address',
+                        help='Specify ip-address of the FritzBox to connect '
+                             'to. Default: %s' % FRITZ_IP_ADDRESS)
+    parser.add_argument('--port',
+                        nargs='?', default=FRITZ_TCP_PORT,
+                        help='Port of the FritzBox to connect to. '
+                             'Default: %s' % FRITZ_TCP_PORT)
+    parser.add_argument('-u', '--username',
+                        nargs=1, default='',
+                        help='Fritzbox authentication username')
+    parser.add_argument('-p', '--password',
+                        nargs=1, default='',
+                        help='Fritzbox authentication password')
+    parser.add_argument('-r', '--reconnect',
+                        action='store_true',
+                        help='Reconnect and get a new ip')
+    parser.add_argument('-s', '--services',
+                        action='store_true',
+                        help='List all available services')
+    parser.add_argument('-S', '--serviceactions',
+                        nargs=1,
+                        help='List actions for the given service: <service>')
+    parser.add_argument('-a', '--servicearguments',
+                        nargs=1,
+                        help='List arguments for the actions of a'
+                             'specified service: <service>.')
+    parser.add_argument('-A', '--actionarguments',
+                        nargs=2,
+                        help='List arguments for the given action of a'
+                             'specified service: <service> <action>.')
+    parser.add_argument('-c', '--complete',
+                        action='store_true',
+                        help=('List all services with actionnames and '
+                              'arguments.')
+                        )
+    args = parser.parse_args()
+    return args
+
+
+def main():
+    args = get_cli_arguments()
+    fi = FritzInspection(args.address, args.port, args.username, args.password)
+    fi.view_header()
+    if args.services:
+        fi.view_servicenames()
+    elif args.serviceactions:
+        fi.view_actionnames(args.serviceactions[0])
+    elif args.servicearguments:
+        fi.view_servicearguments(args.servicearguments[0])
+    elif args.actionarguments:
+        fi.view_actionarguments(args.actionarguments[0],
+                                args.actionarguments[1])
+    elif args.complete:
+        fi.view_complete()
         'lxml',
         'requests'
     ],
+    entry_points={
+        'console_scripts': [
+            'fritzmonitor = fritzconnection.cli:monitor',
+            'fritzhosts = fritzconnection.cli:hosts',
+            'fritzstatus = fritzconnection.cli:status',
+            'fritzconnection = fritzconnection.cli:connection',
+        ]
+    },
     packages=find_packages(exclude=["tests.*", "tests"])
 )