import os
import threading
import json
import ssl
import urllib.request
import urllib.parse
import xml.etree.ElementTree as ET
from xmlrpc.server import SimpleXMLRPCServer
from xmlrpc.server import SimpleXMLRPCRequestHandler
import xmlrpc.client
import socket
#from socketserver import ThreadingMixIn
import logging

from pyhomematic import devicetypes
from pyhomematic.devicetypes.generic import HMChannel

LOG = logging.getLogger(__name__)

# Constants
LOCAL = '0.0.0.0'
LOCALPORT = 0
DEFAULT_JSONPORT = 80
REMOTES = {
    'default': {
        'ip': '127.0.0.1',
        'port': 2001,
        'path': '',
        'username': 'Admin',
        'password': '',
        'jsonport': DEFAULT_JSONPORT,
        'resolvenames': False,
        'connect': True,
    }}
DEVICEFILE = None  # e.g. devices_%s.json
PARAMSETFILE = None # e.g. paramsets_%s.json
INTERFACE_ID = 'pyhomematic'
XML_API_URL = '/config/xmlapi/devicelist.cgi'
JSONRPC_URL = '/api/homematic.cgi'
BACKEND_UNKNOWN = 0
BACKEND_CCU = 1
BACKEND_HOMEGEAR = 2
WORKING = False


# Device-storage
devices = {}
devices_all = {}
devices_raw = {}
devices_raw_dict = {}
paramsets = {}


def make_http_credentials(username=None, password=None):
    """Build auth part for api_url."""
    credentials = ''
    if username is None:
        return credentials
    if username is not None:
        if ':' in username:
            return credentials
        credentials += username
    if credentials and password is not None:
        credentials += ":%s" % password
    return "%s@" % credentials


def build_api_url(host=REMOTES['default']['ip'],
                  port=REMOTES['default']['port'],
                  path=REMOTES['default']['path'],
                  username=None,
                  password=None,
                  ssl=False):
    """Build API URL from components."""
    credentials = make_http_credentials(username, password)
    scheme = 'http'
    if not path:
        path = ''
    if path and not path.startswith('/'):
        path = "/%s" % path
    if ssl:
        scheme += 's'
    return "%s://%s%s:%i%s" % (scheme, credentials, host, port, path)


# Object holding the methods the XML-RPC server should provide.
class RPCFunctions():

    def __init__(self,
                 devicefile=DEVICEFILE,
                 paramsetfile=PARAMSETFILE,
                 proxies={},
                 remotes={},
                 eventcallback=False,
                 systemcallback=False,
                 resolveparamsets=False):
        global devices, devices_all, devices_raw, devices_raw_dict, paramsets
        LOG.debug("RPCFunctions.__init__")
        self.devicefile = None
        if devicefile is not None:
            if "%s" in devicefile:
                self.devicefile = devicefile
            else:
                LOG.warning("RPCFunctions.__init__: Invalid devicefile template")
                self.devicefile = None
        self.paramsetfile = None
        if paramsetfile is not None:
            if "%s" in paramsetfile:
                self.paramsetfile = paramsetfile
            else:
                LOG.warning("RPCFunctions.__init__: Invalid paramsetfile template")
                self.paramsetfile = None
        self.eventcallback = eventcallback
        self.systemcallback = systemcallback
        self.resolveparamsets = resolveparamsets
        self.remotes = remotes
        self._paramsets = paramsets

        # The methods need to know about the proxyies to be able to pass it on
        # to the device-objects
        self._proxies = proxies

        # Devices w/o channels will be accessible using the device-address as
        # the key
        self.devices = devices
        # Devices including channels will be accessible using the
        # device-address + channel as the key
        self.devices_all = devices_all

        # The plain JSON (actually dicts) are stored as well
        self._devices_raw_dict = devices_raw_dict
        self._devices_raw = devices_raw

        for interface_id in proxies:
            LOG.debug("RPCFunctions.__init__: iterating proxy = %s", interface_id)
            remote = interface_id.split('-')[-1]
            self.devices[remote] = {}
            self.devices_all[remote] = {}
            self._devices_raw[remote] = []
            self._devices_raw_dict[remote] = {}
            self._paramsets[remote] = {}

            # If there are stored devices, we load them instead of getting them
            # from the server.
            if self.devicefile is not None:
                devicefilename = self.devicefile % remote
                LOG.debug("RPCFunctions.__init__: devicefile = %s", devicefilename)
                if os.path.isfile(devicefilename):
                    with open(devicefilename, 'r') as fptr:
                        fcontent = fptr.read()
                        if fcontent:
                            self._devices_raw[remote] = json.loads(fcontent)

            # Load stored paramsets if available
            if self.paramsetfile is not None:
                paramsetfilename = self.paramsetfile % remote
                LOG.debug("RPCFunctions.__init__: paramsetfile = %s", paramsetfilename)
                if os.path.isfile(paramsetfilename):
                    with open(paramsetfilename, 'r') as fptr:
                        fcontent = fptr.read()
                        if fcontent:
                            self._paramsets[remote] = json.loads(fcontent)

            # Continue if there are no stored devices
            if not self._devices_raw.get(remote):
                continue
            for device in self._devices_raw[remote]:
                self._devices_raw_dict[remote][device['ADDRESS']] = device
            LOG.debug("RPCFunctions.__init__: devices_raw = %s" %
                      (str(self._devices_raw[remote]), ))

            # Create the "interactive" device-objects from cache and store
            # them in self._devices and self._devices_all
            self.createDeviceObjects(interface_id)

    def createDeviceObjects(self, interface_id):
        """Transform the raw device descriptions into instances of devicetypes.generic.HMDevice or availabe subclass."""
        global WORKING
        WORKING = True
        remote = interface_id.split('-')[-1]
        LOG.debug(
            "RPCFunctions.createDeviceObjects: iterating interface_id = %s", remote)
        # First create parent object
        for dev in self._devices_raw[remote]:
            if not dev['PARENT']:
                if dev['ADDRESS'] not in self.devices_all[remote]:
                    try:
                        if dev['TYPE'] in devicetypes.SUPPORTED:
                            deviceObject = devicetypes.SUPPORTED[dev['TYPE']](
                                dev, self._proxies[interface_id], self.resolveparamsets)
                            LOG.debug("RPCFunctions.createDeviceObjects: created %s  as SUPPORTED device for %s" % (
                                dev['ADDRESS'], dev['TYPE']))
                        else:
                            deviceObject = devicetypes.UNSUPPORTED(
                                dev, self._proxies[interface_id], self.resolveparamsets)
                            LOG.debug("RPCFunctions.createDeviceObjects: created %s  as UNSUPPORTED device for %s" % (
                                dev['ADDRESS'], dev['TYPE']))
                        LOG.debug(
                            "RPCFunctions.createDeviceObjects: adding to self.devices_all")
                        self.devices_all[remote][dev['ADDRESS']] = deviceObject
                        LOG.debug(
                            "RPCFunctions.createDeviceObjects: adding to self.devices")
                        self.devices[remote][dev['ADDRESS']] = deviceObject
                    except Exception as err:
                        LOG.critical(
                            "RPCFunctions.createDeviceObjects: Parent: %s", str(err))
        # Then create all children for parent
        for dev in self._devices_raw[remote]:
            if dev['PARENT']:
                try:
                    if dev['ADDRESS'] not in self.devices_all[remote]:
                        deviceObject = HMChannel(
                            dev, self._proxies[interface_id], self.resolveparamsets)
                        self.devices_all[remote][dev['ADDRESS']] = deviceObject
                        self.devices[remote][dev['PARENT']].CHANNELS[
                            dev['INDEX']] = deviceObject
                except Exception as err:
                    LOG.critical(
                        "RPCFunctions.createDeviceObjects: Child: %s", str(err))
        if self.devices_all[remote] and self.remotes[remote].get('resolvenames', False):
            self.addDeviceNames(remote)
        WORKING = False
        if self.systemcallback:
            self.systemcallback('createDeviceObjects')
        return True

    def error(self, interface_id, errorcode, msg):
        """When some error occurs the CCU / Homegear will send it's error message here"""
        LOG.debug("RPCFunctions.error: interface_id = %s, errorcode = %i, message = %s",
                  interface_id, int(errorcode), str(msg))
        if self.systemcallback:
            self.systemcallback('error', interface_id, errorcode, msg)
        return True

    def saveDevices(self, remote):
        """We save known devices into a json-file so we don't have to work through the whole list of devices the CCU / Homegear presents us"""
        if self.devicefile is not None:
            devicefilename = self.devicefile % remote
            LOG.debug("RPCFunctions.saveDevices: devicefile: %s", devicefilename)
            try:
                with open(devicefilename, 'w') as df:
                    df.write(json.dumps(self._devices_raw[remote]))
                return True
            except Exception as err:
                LOG.warning(
                    "RPCFunctions.saveDevices: Exception saving _devices_raw: %s", str(err))
                return False
        else:
            return True

    def saveParamsets(self, remote):
        """Write known paramsets to disk."""
        if self.paramsetfile is not None:
            paramsetfilename = self.paramsetfile % remote
            LOG.debug("RPCFunctions.saveParamsets: paramsetfile: %s", paramsetfilename)
            try:
                with open(paramsetfilename, 'w') as df:
                    df.write(json.dumps(self._paramsets[remote]))
                return True
            except Exception as err:
                LOG.warning(
                    "RPCFunctions.saveParamsets: Exception saving _paramsets: %s", str(err))
                return False

    def event(self, interface_id, address, value_key, value):
        """If a device emits some sort event, we will handle it here."""
        LOG.debug("RPCFunctions.event: interface_id = %s, address = %s, value_key = %s, value = %s" % (
            interface_id, address, value_key.upper(), str(value)))
        self.devices_all[interface_id.split(
            '-')[-1]][address].event(interface_id, value_key.upper(), value)
        if self.eventcallback:
            self.eventcallback(interface_id=interface_id, address=address,
                               value_key=value_key.upper(), value=value)
        return True

    def listDevices(self, interface_id):
        """The CCU / Homegear asks for devices known to our XML-RPC server. We respond to that request using this method."""
        LOG.debug("RPCFunctions.listDevices: interface_id = %s, _devices_raw = %s" % (
            interface_id, str(self._devices_raw)))
        remote = interface_id.split('-')[-1]
        if remote not in self._devices_raw:
            self._devices_raw[remote] = []
        if self.systemcallback:
            self.systemcallback('listDevices', interface_id)

        # return empty list for HmIP, as currently the maximum lenght is limited to 8192 bytes  (see #318 for details)
        if self.remotes.get(remote, {}).get('port') in [2010, 32010, 42010]:
            return []
        return self._devices_raw[remote]

    def newDevices(self, interface_id, dev_descriptions):
        """The CCU / Homegear informs us about newly added devices. We react on that and add those devices as well."""
        LOG.debug("RPCFunctions.newDevices: interface_id = %s, dev_descriptions = %s" % (
            interface_id, str(dev_descriptions)))
        remote = interface_id.split('-')[-1]
        if remote not in self._devices_raw:
            self._devices_raw[remote] = []
        if remote not in self._devices_raw_dict:
            self._devices_raw_dict[remote] = {}
        if remote not in self._paramsets:
            self._paramsets[remote] = {}
        hmip = self.remotes.get(remote, {}).get('port') in [2010, 32010, 42010]
        for d in dev_descriptions:
            if hmip:
                if d in self._devices_raw[remote]:
                    continue
            self._devices_raw[remote].append(d)
            self._devices_raw_dict[remote][d['ADDRESS']] = d
            self._paramsets[remote][d['ADDRESS']] = {}
        self.saveDevices(remote)
        self.saveParamsets(remote)
        self.createDeviceObjects(interface_id)
        if self.systemcallback:
            self.systemcallback('newDevices', interface_id, dev_descriptions)
        return True

    def deleteDevices(self, interface_id, addresses):
        """The CCU / Homegear informs us about removed devices. We react on that and remove those devices as well."""
        LOG.debug("RPCFunctions.deleteDevices: interface_id = %s, addresses = %s" % (
            interface_id, str(addresses)))
        # TODO: remove known device objects as well
        remote = interface_id.split('-')[-1]
        self._devices_raw[remote] = [device for device in self._devices_raw[
            remote] if not device['ADDRESS'] in addresses]
        self.saveDevices(remote)
        for address in addresses:
            try:
                del self._paramsets[remote][address]
            except KeyError:
                pass
        self.saveParamsets(remote)
        if self.systemcallback:
            self.systemcallback('deleteDevice', interface_id, addresses)
        return True

    def updateDevice(self, interface_id, address, hint):
        LOG.debug("RPCFunctions.updateDevice: interface_id = %s, address = %s, hint = %s" % (
            interface_id, address, str(hint)))
        # TODO: Implement updateDevice
        if self.systemcallback:
            self.systemcallback('updateDevice', interface_id, address, hint)
        return True

    def replaceDevice(self, interface_id, oldDeviceAddress, newDeviceAddress):
        LOG.debug("RPCFunctions.replaceDevice: interface_id = %s, oldDeviceAddress = %s, newDeviceAddress = %s" % (
            interface_id, oldDeviceAddress, newDeviceAddress))
        # TODO: Implement replaceDevice
        if self.systemcallback:
            self.systemcallback('replaceDevice', interface_id,
                                oldDeviceAddress, newDeviceAddress)
        return True

    def readdedDevice(self, interface_id, addresses):
        LOG.debug("RPCFunctions.readdedDevices: interface_id = %s, addresses = %s" % (
            interface_id, str(addresses)))
        # TODO: Implement readdedDevice
        if self.systemcallback:
            self.systemcallback('readdedDevice', interface_id, addresses)
        return True

    def jsonRpcPost(self, host, jsonport, method, params={}):
        LOG.debug("RPCFunctions.jsonRpcPost: Method: %s" % method)
        try:
            payload = json.dumps(
                {"method": method, "params": params, "jsonrpc": "1.1", "id": 0}).encode('utf-8')

            headers = {"Content-Type": 'application/json',
                       "Content-Length": len(payload)}
            if jsonport == 443:
                apiendpoint = "https://%s:%s%s" % (host, jsonport, JSONRPC_URL)
            else:
                apiendpoint = "http://%s:%s%s" % (host, jsonport, JSONRPC_URL)
            LOG.debug("RPCFunctions.jsonRpcPost: API-Endpoint: %s" %
                      apiendpoint)
            req = urllib.request.Request(apiendpoint, payload, headers)
            resp = urllib.request.urlopen(req)
            if resp.status == 200:
                try:
                    return json.loads(resp.read().decode('utf-8'))
                except ValueError as err:
                    # Workaround for bug in CCU
                    return json.loads(resp.read().decode('utf-8').replace("\\", ""))
            else:
                LOG.error("RPCFunctions.jsonRpcPost: Status: %i" % resp.status)
                return {'error': resp.status, 'result': {}}
        except Exception as err:
            LOG.error("RPCFunctions.jsonRpcPost: Exception: %s" % str(err))
            return {'error': str(err), 'result': {}}

    def addDeviceNames(self, remote):
        """ If XML-API (http://www.homematic-inside.de/software/addons/item/xmlapi) is installed on CCU this function will add names to CCU devices """
        LOG.debug("RPCFunctions.addDeviceNames")

        # First try to get names from metadata when nur credentials are set
        if self.remotes[remote]['resolvenames'] == 'metadata':
            for address in self.devices[remote]:
                try:
                    name = self.devices[remote][
                        address]._proxy.getMetadata(address, 'NAME')
                    self.devices[remote][address].NAME = name
                    for address, device in self.devices[remote][address].CHANNELS.items():
                        device.NAME = name
                        self.devices_all[remote][device.ADDRESS].NAME = name
                except Exception as err:
                    LOG.debug(
                        "RPCFunctions.addDeviceNames: Unable to get name for %s from metadata." % str(address))

        # Then try to get names via JSON-RPC
        elif (self.remotes[remote]['resolvenames'] == 'json' and
              self.remotes[remote]['username'] and
              self.remotes[remote]['password']):
            LOG.debug("RPCFunctions.addDeviceNames: Getting names via JSON-RPC")
            try:
                session = False
                params = {"username": self.remotes[remote][
                    'username'], "password": self.remotes[remote]['password']}
                response = self.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Session.login", params)
                if response['error'] is None and response['result']:
                    session = response['result']

                if not session:
                    LOG.warning(
                        "RPCFunctions.addDeviceNames: Unable to open session.")
                    return

                params = {"_session_id_": session}
                response = self.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Interface.listInterfaces", params)
                interface = False
                if response['error'] is None and response['result']:
                    for i in response['result']:
                        if i['port'] in [self.remotes[remote]['port'], self.remotes[remote]['port'] + 30000]:
                            interface = i['name']
                            break
                LOG.debug(
                    "RPCFunctions.addDeviceNames: Got interface: %s" % interface)
                if not interface:
                    params = {"_session_id_": session}
                    response = self.jsonRpcPost(
                        self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Session.logout", params)
                    return

                params = {"_session_id_": session}
                response = self.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Device.listAllDetail", params)

                if response['error'] is None and response['result']:
                    LOG.debug(
                        "RPCFunctions.addDeviceNames: Resolving devicenames")
                    for i in response['result']:
                        try:
                            if i.get('address') in self.devices[remote]:
                                self.devices[remote][
                                    i['address']].NAME = i['name']
                        except Exception as err:
                            LOG.warning(
                                "RPCFunctions.addDeviceNames: Exception: %s" % str(err))

                params = {"_session_id_": session}
                response = self.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Session.logout", params)
            except Exception as err:
                params = {"_session_id_": session}
                response = self.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Session.logout", params)
                LOG.warning(
                    "RPCFunctions.addDeviceNames: Exception: %s" % str(err))

        # Then try to get names from XML-API
        elif self.remotes[remote]['resolvenames'] == 'xml':
            LOG.warning("Resolving names with the XML-API addon will be disabled in a future release. Please switch to json.")
            try:
                response = urllib.request.urlopen(
                    "http://%s%s" % (self.remotes[remote]['ip'], XML_API_URL), timeout=5)
                device_list = response.read().decode("ISO-8859-1")
            except Exception as err:
                LOG.warning(
                    "RPCFunctions.addDeviceNames: Could not access XML-API: %s" % (str(err), ))
                return
            device_list_tree = ET.ElementTree(ET.fromstring(device_list))
            for device in device_list_tree.getroot():
                address = device.attrib['address']
                name = device.attrib['name']
                if address in self.devices[remote]:
                    self.devices[remote][address].NAME = name
                    for address, device in self.devices[remote][address].CHANNELS.items():
                        device.NAME = name
                        self.devices_all[remote][device.ADDRESS].NAME = name


class LockingServerProxy(xmlrpc.client.ServerProxy):
    """
    ServerProxy implementation with lock when request is executing
    """

    def __init__(self, *args, **kwargs):
        """
        Initialize new proxy for server and get local ip
        """
        self._remote = kwargs.pop("remote", None)
        self._skipinit = kwargs.pop("skipinit", False)
        self._callbackip = kwargs.pop("callbackip", None)
        self._callbackport = kwargs.pop("callbackport", None)
        self._ssl = kwargs.pop("ssl", False)
        self._verify_ssl = kwargs.pop("verify_ssl", True)
        self.lock = threading.Lock()
        if self._ssl and not self._verify_ssl and self._verify_ssl is not None:
            kwargs['context'] = ssl._create_unverified_context()
        xmlrpc.client.ServerProxy.__init__(self, encoding="ISO-8859-1", *args, **kwargs)
        urlcomponents = urllib.parse.urlparse(args[0])
        self._remoteip = urlcomponents.hostname
        self._remoteport = urlcomponents.port
        LOG.debug("LockingServerProxy.__init__: Getting local ip")
        tmpsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        tmpsocket.connect((self._remoteip, self._remoteport))
        self._localip = tmpsocket.getsockname()[0]
        tmpsocket.close()
        LOG.debug("LockingServerProxy.__init__: Got local ip %s" %
                  self._localip)

    def __request(self, *args, **kwargs):
        """
        Call method on server side
        """

        with self.lock:
            parent = xmlrpc.client.ServerProxy
            # pylint: disable=E1101
            return parent._ServerProxy__request(self, *args, **kwargs)

    def __getattr__(self, *args, **kwargs):
        """
        Magic method dispatcher
        """
        return xmlrpc.client._Method(self.__request, *args, **kwargs)

# Restrict to particular paths.


class RequestHandler(SimpleXMLRPCRequestHandler):
    """We handle requests to / and /RPC2"""
    rpc_paths = ('/', '/RPC2',)


# pylint: disable=too-many-public-methods
class ServerThread(threading.Thread):
    """XML-RPC server thread to handle messages from CCU / Homegear"""

    def __init__(self,
                 local=LOCAL,
                 localport=LOCALPORT,
                 remotes=REMOTES,
                 devicefile=DEVICEFILE,
                 paramsetfile=PARAMSETFILE,
                 interface_id=INTERFACE_ID,
                 eventcallback=False,
                 systemcallback=False,
                 resolveparamsets=False):
        LOG.debug("ServerThread.__init__")
        threading.Thread.__init__(self)

        # Member
        self._interface_id = interface_id
        self._local = local
        self._localport = int(localport)
        self._devicefile = devicefile
        self._paramsetfile = paramsetfile
        self.remotes = remotes
        self.eventcallback = eventcallback
        self.systemcallback = systemcallback
        self.resolveparamsets = resolveparamsets
        self.proxies = {}
        self.failed_inits = []

        self.createProxies()
        if not self.proxies:
            LOG.warning("No proxies available. Aborting.")
            raise Exception

        self._rpcfunctions = RPCFunctions(devicefile=self._devicefile,
                                          paramsetfile=self._paramsetfile,
                                          proxies=self.proxies,
                                          remotes=self.remotes,
                                          eventcallback=self.eventcallback,
                                          systemcallback=self.systemcallback,
                                          resolveparamsets=self.resolveparamsets)

        # Setup server to handle requests from CCU / Homegear
        LOG.debug("ServerThread.__init__: Setting up server")
        # class SimpleThreadedXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):
        #     pass
        # self.server = SimpleThreadedXMLRPCServer((self._local, self._localport),
        #                                          requestHandler=RequestHandler,
        #                                          logRequests=False)
        self.server = SimpleXMLRPCServer((self._local, self._localport),
                                         requestHandler=RequestHandler,
                                         logRequests=False)
        self._localport = self.server.socket.getsockname()[1]
        self.server.register_introspection_functions()
        self.server.register_multicall_functions()
        LOG.debug("ServerThread.__init__: Registering RPC functions")
        self.server.register_instance(
            self._rpcfunctions, allow_dotted_names=True)

    def run(self):
        LOG.info("Starting server at http://%s:%i" %
                 (self._local, self._localport))
        self.server.serve_forever()

    def createProxies(self):
        """Create proxies to interact with CCU / Homegear"""
        LOG.debug("createProxies: Creating proxies")
        for remote, host in self.remotes.items():
            # Initialize XML-RPC
            try:
                socket.gethostbyname(host['ip'])
            except Exception as err:
                LOG.warning("Skipping proxy: %s" % str(err))
                continue
            if 'path' not in host:
                host['path'] = ''
            LOG.info("Creating proxy %s. Connecting to %s:%i%s" %
                     (remote, host['ip'], host['port'], host['path']))
            host['id'] = "%s-%s" % (self._interface_id, remote)
            try:
                api_url = build_api_url(host=host['ip'],
                                        port=host['port'],
                                        path=host['path'],
                                        username=host.get('username'),
                                        password=host.get('password'),
                                        ssl=host.get('ssl'))
                self.proxies[host['id']] = LockingServerProxy(
                    api_url,
                    remote=remote,
                    callbackip=host.get('callbackip', None),
                    callbackport=host.get('callbackport', None),
                    skipinit=not host.get('connect', True),
                    ssl=host.get('ssl', False),
                    verify_ssl=host.get('verify_ssl', True))
            except Exception as err:
                LOG.warning("Failed connecting to proxy at http://%s:%i%s" %
                            (host['ip'], host['port'], host['path']))
                LOG.debug("__init__: Exception: %s" % str(err))
                raise Exception
            try:
                host['type'] = BACKEND_UNKNOWN
                #if "Homegear" in self.proxies[host['id']].getVersion():
                #    LOG.debug("__init__: Host is Homegear")
                #    host['type'] = BACKEND_HOMEGEAR
                #else:
                #    LOG.debug("__init__: Host is CCU")
                #    host['type'] = BACKEND_CCU
            except Exception as err:
                LOG.warning("__init__: Failed to detect backend type: %s" % str(err))
                host['type'] = BACKEND_UNKNOWN

    def clearProxies(self):
        """Remove existing proxy objects."""
        LOG.debug("clearProxies: Clearing proxies")
        self.proxies.clear()

    def proxyInit(self):
        """
        To receive events the proxy has to tell the CCU / Homegear where to send the events. For that we call the init-method.
        """
        # Call init() with local XML RPC config and interface_id (the name of
        # the receiver) to receive events. XML RPC server has to be running.
        for interface_id, proxy in self.proxies.items():
            if proxy._skipinit:
                LOG.warning("Skipping init for %s", interface_id)
                continue
            if proxy._callbackip and proxy._callbackport:
                callbackip = proxy._callbackip
                callbackport = proxy._callbackport
            else:
                callbackip = proxy._localip
                callbackport = self._localport
            LOG.debug("ServerThread.proxyInit: init('http://%s:%i', '%s')" %
                      (callbackip, callbackport, interface_id))
            try:
                # For HomeMatic IP, init is not working correctly. We fetch the device list and create
                # the device objects before the init is performed.
                if proxy._remoteport in [2010, 32010, 42010]:
                    dev_list = proxy.listDevices(interface_id)
                    self._rpcfunctions.newDevices(interface_id=interface_id, dev_descriptions=dev_list)
                proxy.init("http://%s:%i" %
                           (callbackip, callbackport), interface_id)
                LOG.info("Proxy for %s initialized", interface_id)
            except Exception as err:
                LOG.debug("proxyInit: Exception: %s" % str(err))
                LOG.warning("Failed to initialize proxy for %s", interface_id)
                self.failed_inits.append(interface_id)

    def proxyDeInit(self):
        """De-Init from the proxies."""
        stopped = []
        for interface_id, proxy in self.proxies.items():
            if interface_id in self.failed_inits:
                LOG.warning("ServerThread.proxyDeInit: Not performing de-init for %s", interface_id)
                continue
            if proxy._callbackip and proxy._callbackport:
                callbackip = proxy._callbackip
                callbackport = proxy._callbackport
            else:
                callbackip = proxy._localip
                callbackport = self._localport
            remote = "http://%s:%i" % (callbackip, callbackport)
            LOG.debug("ServerThread.proxyDeInit: init('%s')", remote)
            if not interface_id in stopped:
                try:
                    proxy.init(remote)
                    stopped.append(interface_id)
                    LOG.info("proxyDeInit: Proxy for %s de-initialized: %s", interface_id, remote)
                except Exception as err:
                    LOG.debug("proxyDeInit: Exception: %s", err)
                    LOG.warning("proxyDeInit: Failed to de-initialize proxy")

    def stop(self):
        """To stop the server we de-init from the CCU / Homegear, then shut down our XML-RPC server."""
        self.proxyDeInit()
        self.clearProxies()
        LOG.info("Shutting down server")
        self.server.shutdown()
        LOG.debug("ServerThread.stop: Stopping ServerThread")
        self.server.server_close()
        LOG.info("HomeMatic XML-RPC Server stopped")

    def parseCCUSysVar(self, data):
        """Helper to parse type of system variables of CCU"""
        if data['type'] == 'LOGIC':
            return data['name'], data['value'] == 'true'
        elif data['type'] == 'NUMBER':
            return data['name'], float(data['value'])
        elif data['type'] == 'LIST':
            return data['name'], int(data['value'])
        else:
            return data['name'], data['value']

    def jsonRpcLogin(self, remote):
        """Login to CCU and return session"""
        session = False
        try:
            params = {"username": self.remotes[remote][
                'username'], "password": self.remotes[remote]['password']}
            response = self._rpcfunctions.jsonRpcPost(
                self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Session.login", params)
            if response['error'] is None and response['result']:
                session = response['result']

            if not session:
                LOG.warning(
                    "ServerThread.jsonRpcLogin: Unable to open session.")
        except Exception as err:
            LOG.debug(
                "ServerThread.jsonRpcLogin: Exception while logging in via JSON-RPC: %s" % str(err))
        return session

    def jsonRpcLogout(self, remote, session):
        """Logout of CCU"""
        logout = False
        try:
            params = {"_session_id_": session}
            response = self._rpcfunctions.jsonRpcPost(
                self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "Session.logout", params)
            if response['error'] is None and response['result']:
                logout = response['result']
        except Exception as err:
            LOG.debug(
                "ServerThread.jsonRpcLogout: Exception while logging in via JSON-RPC: %s" % str(err))
        return logout

    def getAllSystemVariables(self, remote):
        """Get all system variables from CCU / Homegear"""
        variables = {}
        if self.remotes[remote]['username'] and self.remotes[remote]['password']:
            LOG.debug(
                "ServerThread.getAllSystemVariables: Getting all System variables via JSON-RPC")
            session = self.jsonRpcLogin(remote)
            if not session:
                return
            try:
                params = {"_session_id_": session}
                response = self._rpcfunctions.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "SysVar.getAll", params)
                if response['error'] is None and response['result']:
                    for var in response['result']:
                        key, value = self.parseCCUSysVar(var)
                        variables[key] = value

                self.jsonRpcLogout(remote, session)
            except Exception as err:
                self.jsonRpcLogout(remote, session)
                LOG.warning(
                    "ServerThread.getAllSystemVariables: Exception: %s" % str(err))
        else:
            try:
                variables = self.proxies[
                    "%s-%s" % (self._interface_id, remote)].getAllSystemVariables()
            except Exception as err:
                LOG.debug(
                    "ServerThread.getAllSystemVariables: Exception: %s" % str(err))
        return variables

    def getSystemVariable(self, remote, name):
        """Get single system variable from CCU / Homegear"""
        var = None
        if self.remotes[remote]['username'] and self.remotes[remote]['password']:
            LOG.debug(
                "ServerThread.getSystemVariable: Getting System variable via JSON-RPC")
            session = self.jsonRpcLogin(remote)
            if not session:
                return
            try:
                params = {"_session_id_": session, "name": name}
                response = self._rpcfunctions.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "SysVar.getValueByName", params)
                if response['error'] is None and response['result']:
                    try:
                        var = float(response['result'])
                    except Exception as err:
                        var = response['result'] == 'true'

                self.jsonRpcLogout(remote, session)
            except Exception as err:
                self.jsonRpcLogout(remote, session)
                LOG.warning(
                    "ServerThread.getSystemVariable: Exception: %s" % str(err))
        else:
            try:
                var = self.proxies[
                    "%s-%s" % (self._interface_id, remote)].getSystemVariable(name)
            except Exception as err:
                LOG.debug(
                    "ServerThread.getSystemVariable: Exception: %s" % str(err))
        return var

    def deleteSystemVariable(self, remote, name):
        """Delete a system variable from CCU / Homegear"""
        if self.remotes[remote]['username'] and self.remotes[remote]['password']:
            LOG.debug(
                "ServerThread.deleteSystemVariable: Getting System variable via JSON-RPC")
            session = self.jsonRpcLogin(remote)
            if not session:
                return
            try:
                params = {"_session_id_": session, "name": name}
                response = self._rpcfunctions.jsonRpcPost(
                    self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "SysVar.deleteSysVarByName", params)
                if response['error'] is None and response['result']:
                    deleted = response['result']
                    LOG.warning(
                        "ServerThread.deleteSystemVariable: Deleted: %s" % str(deleted))

                self.jsonRpcLogout(remote, session)
            except Exception as err:
                self.jsonRpcLogout(remote, session)
                LOG.warning(
                    "ServerThread.deleteSystemVariable: Exception: %s" % str(err))
        else:
            try:
                return self.proxies["%s-%s" % (self._interface_id, remote)].deleteSystemVariable(name)
            except Exception as err:
                LOG.debug(
                    "ServerThread.deleteSystemVariable: Exception: %s" % str(err))

    def setSystemVariable(self, remote, name, value):
        """Set a system variable on CCU / Homegear"""
        if self.remotes[remote]['username'] and self.remotes[remote]['password']:
            LOG.debug(
                "ServerThread.setSystemVariable: Setting System variable via JSON-RPC")
            session = self.jsonRpcLogin(remote)
            if not session:
                return
            try:
                params = {"_session_id_": session,
                          "name": name, "value": value}
                if value is True or value is False:
                    params['value'] = int(value)
                    response = self._rpcfunctions.jsonRpcPost(
                        self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "SysVar.setBool", params)
                else:
                    response = self._rpcfunctions.jsonRpcPost(
                        self.remotes[remote]['ip'], self.remotes[remote].get('jsonport', DEFAULT_JSONPORT), "SysVar.setFloat", params)
                if response['error'] is None and response['result']:
                    res = response['result']
                    LOG.debug(
                        "ServerThread.setSystemVariable: Result while setting variable: %s" % str(res))
                else:
                    if response['error']:
                        LOG.debug("ServerThread.setSystemVariable: Error while setting variable: %s" % str(
                            response['error']))

                self.jsonRpcLogout(remote, session)
            except Exception as err:
                self.jsonRpcLogout(remote, session)
                LOG.warning(
                    "ServerThread.setSystemVariable: Exception: %s" % str(err))
        else:
            try:
                return self.proxies["%s-%s" % (self._interface_id, remote)].setSystemVariable(name, value)
            except Exception as err:
                LOG.debug(
                    "ServerThread.setSystemVariable: Exception: %s" % str(err))

    def getServiceMessages(self, remote):
        """Get service messages from CCU / Homegear"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].getServiceMessages()
        except Exception as err:
            LOG.debug("ServerThread.getServiceMessages: Exception: %s" % str(err))

    def rssiInfo(self, remote):
        """Get RSSI information for all devices from CCU / Homegear"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].rssiInfo()
        except Exception as err:
            LOG.debug("ServerThread.rssiInfo: Exception: %s" % str(err))

    def setInstallMode(self, remote, on=True, t=60, mode=1, address=None):
        """Activate or deactivate installmode on CCU / Homegear"""
        try:
            args = [on]
            if on and t:
                args.append(t)
                if address:
                    args.append(address)
                else:
                    args.append(mode)

            return self.proxies["%s-%s" % (self._interface_id, remote)].setInstallMode(*args)
        except Exception as err:
            LOG.debug("ServerThread.setInstallMode: Exception: %s" % str(err))

    def getInstallMode(self, remote):
        """Get remaining time in seconds install mode is active from CCU / Homegear"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].getInstallMode()
        except Exception as err:
            LOG.debug("ServerThread.getInstallMode: Exception: %s" % str(err))

    def getAllMetadata(self, remote, address):
        """Get all metadata of device"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].getAllMetadata(address)
        except Exception as err:
            LOG.debug("ServerThread.getAllMetadata: Exception: %s" % str(err))

    def getMetadata(self, remote, address, key):
        """Get metadata of device"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].getMetadata(address, key)
        except Exception as err:
            LOG.debug("ServerThread.getMetadata: Exception: %s" % str(err))

    def setMetadata(self, remote, address, key, value):
        """Set metadata of device"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].setMetadata(address, key, value)
        except Exception as err:
            LOG.debug("ServerThread.setMetadata: Exception: %s" % str(err))

    def deleteMetadata(self, remote, address, key):
        """Delete metadata of device"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].deleteMetadata(address, key)
        except Exception as err:
            LOG.debug("ServerThread.deleteMetadata: Exception: %s" % str(err))

    def listBidcosInterfaces(self, remote):
        """Return all available BidCos Interfaces"""
        try:
            return self.proxies["%s-%s" % (self._interface_id, remote)].listBidcosInterfaces()
        except Exception as err:
            LOG.debug(
                "ServerThread.listBidcosInterfaces: Exception: %s" % str(err))

    def ping(self, remote):
        """Send ping to CCU/Homegear to generate PONG event"""
        try:
            self.proxies["%s-%s" % (self._interface_id, remote)].ping("%s-%s" % (self._interface_id, remote))
        except Exception as err:
            LOG.warning("ServerThread.ping: Exception: %s" % str(err))

    def homegearCheckInit(self, remote):
        """Check if proxy is still initialized"""
        rdict = self.remotes.get(remote)
        if not rdict:
            return False
        if rdict.get('type') != BACKEND_HOMEGEAR:
            return False
        try:
            interface_id = "%s-%s" % (self._interface_id, remote)
            return self.proxies[interface_id].clientServerInitialized(interface_id)
        except Exception as err:
            LOG.debug(
                "ServerThread.homegearCheckInit: Exception: %s" % str(err))
            return False

    def putParamset(self, remote, address, paramset, value, rx_mode=None):
        """Set paramsets manually"""
        try:
            proxy = self.proxies["%s-%s" % (self._interface_id, remote)]
            if rx_mode is None:
                return proxy.putParamset(address, paramset, value)
            else:
                return proxy.putParamset(address, paramset, value, rx_mode)
        except Exception as err:
            LOG.debug("ServerThread.putParamset: Exception: %s" % str(err))
