"""
Provides zone tracking functionality for the Alarm Decoder (AD2) device family.

.. moduleauthor:: Scott Petersen <scott@nutech.com>
"""

import re
import time

from .event import event
from .messages import ExpanderMessage

class Zone(object):
    """
    Representation of a panel zone.
    """

    CLEAR = 0
    """Status indicating that the zone is cleared."""
    FAULT = 1
    """Status indicating that the zone is faulted."""
    CHECK = 2   # Wire fault
    """Status indicating that there is a wiring issue with the zone."""

    STATUS = { CLEAR: 'CLEAR', FAULT: 'FAULT', CHECK: 'CHECK' }

    def __init__(self, zone=0, name='', status=CLEAR):
        """
        Constructor

        :param zone: The zone number.
        :type zone: int
        :param name: Human readable zone name.
        :type name: str
        :param status: Initial zone state.
        :type status: int
        """
        self.zone = zone
        self.name = name
        self.status = status
        self.timestamp = time.time()

    def __str__(self):
        """
        String conversion operator.
        """
        return 'Zone {0} {1}'.format(self.zone, self.name)

    def __repr__(self):
        """
        Human readable representation operator.
        """
        return 'Zone({0}, {1}, ts {2})'.format(self.zone, Zone.STATUS[self.status], self.timestamp)

class Zonetracker(object):
    """
    Handles tracking of zone and their statuses.
    """

    on_fault = event.Event('Called when the device detects a zone fault.')
    on_restore = event.Event('Called when the device detects that a fault is restored.')

    EXPIRE = 30
    """Zone expiration timeout."""

    def __init__(self):
        """
        """
        self._zones = {}
        self._zones_faulted = []
        self._last_zone_fault = 0

    def update(self, message):
        """
        Update zone statuses based on the current message.

        :param message: Message to use to update the zone tracking.
        :type message: Message or ExpanderMessage
        """
        if isinstance(message, ExpanderMessage):
            if message.type == ExpanderMessage.ZONE:
                zone = self._expander_to_zone(message.address, message.channel)

                status = Zone.CLEAR
                if message.value == 1:
                    status = Zone.FAULT
                elif message.value == 2:
                    status = Zone.CHECK

                # NOTE: Expander zone faults are handled differently than regular messages.
                # We don't include them in self._zones_faulted because they are not reported
                # by the panel in it's rolling list of faults.
                try:
                    self._update_zone(zone, status=status)

                except IndexError:
                    self._add_zone(zone, status=status)

        else:
            # Panel is ready, restore all zones.
            #
            # NOTE: This will need to be updated to support panels with multiple partitions.
            # In it's current state a ready on partition #1 will end up clearing all zones, even
            # if they exist elsewhere and it shouldn't.
            if message.ready:
                for z in self._zones_faulted:
                    self._update_zone(z, Zone.CLEAR)

                self._last_zone_fault = 0

            # Process fault
            elif "FAULT" in message.text or message.check_zone:
                # Apparently this representation can be both base 10
                # or base 16, depending on where the message came
                # from.
                try:
                    zone = int(message.numeric_code)
                except ValueError:
                    zone = int(message.numeric_code, 16)

                # NOTE: Odd case for ECP failures.  Apparently they report as zone 191 (0xBF) regardless
                # of whether or not the 3-digit mode is enabled... so we have to pull it out of the
                # alpha message.
                if zone == 191:
                    zone_regex = re.compile('^CHECK (\d+).*$')

                    m = zone_regex.match(message.text)
                    if m is None:
                        return

                    zone = m.group(1)

                # Add new zones and clear expired ones.
                if zone in self._zones_faulted:
                    self._update_zone(zone)
                    self._clear_zones(zone)

                else:
                    if message.check_zone:
                        status = Zone.CHECK

                    self._zones_faulted.append(zone)
                    self._zones_faulted.sort()

                # Save our spot for the next message.
                self._last_zone_fault = zone

            self._clear_expired_zones()

    def _clear_zones(self, zone):
        """
        Clear all expired zones from our status list.

        :param zone: current zone being processed.
        """
        cleared_zones = []
        found_last_faulted = found_current = at_end = False

        # First pass: Find our start spot.
        it = iter(self._zones_faulted)
        try:
            while not found_last_faulted:
                z = it.next()

                if z == self._last_zone_fault:
                    found_last_faulted = True
                    break

        except StopIteration:
            at_end = True

        # Continue until we find our end point and add zones in
        # between to our clear list.
        try:
            while not at_end and not found_current:

                if z == zone:
                    found_current = True
                    break
                else:
                    cleared_zones += [z]

            pass

        # Second pass: roll through the list again if we didn't find
        # our end point and remove everything until we do.
        if not found_current:
            it = iter(self._zones_faulted)

            try:
                while not found_current:
                    z = it.next()

                    if z == zone:
                        found_current = True
                        break
                    else:
                        cleared_zones += [z]

            except StopIteration:
                pass

        # Actually remove the zones and trigger the restores.
        for z in cleared_zones:
            self._update_zone(z, Zone.CLEAR)

    def _clear_expired_zones(self):
        """
        Update zone status for all expired zones.
        """
        zones = []

        for z in self._zones.keys():
            zones += [z]

        for z in zones:
            if self._zones[z].status != Zone.CLEAR and self._zone_expired(z):
                self._update_zone(z, Zone.CLEAR)

    def _add_zone(self, zone, name='', status=Zone.CLEAR):
        """
        Adds a zone to the internal zone list.

        :param status: The zone status.
        """
        if not zone in self._zones:
            self._zones[zone] = Zone(zone=zone, name=name, status=status)

        if status != Zone.CLEAR:
            self.on_fault(zone=zone)

    def _update_zone(self, zone, status=None):
        """
        Updates a zones status.

        :raises: IndexError
        """
            raise IndexError('Zone does not exist and cannot be updated: %d', zone)

        if status is not None:
            self._zones[zone].status = status

        self._zones[zone].timestamp = time.time()

        if status == Zone.CLEAR:
            if zone in self._zones_faulted:
                self._zones_faulted.remove(zone)

            self.on_restore(zone=zone)

    def _zone_expired(self, zone):
        """
        Determine if a zone is expired or not.

        :returns: Whether or not the zone is expired.
        """
        return time.time() > self._zones[zone].timestamp + Zonetracker.EXPIRE

    def _expander_to_zone(self, address, channel):
        """
        Convert an address and channel into a zone number.

        :param address: The expander address
        :type address: int
        :param channel: The channel
        :type channel: int

        :returns: The zone number associated with an address and channel.
        """

        # TODO: This is going to need to be reworked to support the larger
        #       panels without fixed addressing on the expanders.

        idx = address - 7   # Expanders start at address 7.

        return address + channel + (idx * 7) + 1

"""
Provides utility classes for the Alarm Decoder (AD2) devices.

"""

import threading

class NoDeviceError(Exception):
    """
    No devices found.
    """
    pass

class CommError(Exception):
    """
    There was an error communicating with the device.
    """
    pass

class TimeoutError(Exception):
    """
    There was a timeout while trying to communicate with the device.
    """
    pass

class InvalidMessageError(Exception):
    """
    The format of the panel message was invalid.
    """
    pass

class Firmware(object):
    """
    Represents firmware for the Alarm Decoder devices.
    """

    # Constants
    STAGE_START = 0
    STAGE_WAITING = 1
    STAGE_BOOT = 2
    STAGE_LOAD = 3
    STAGE_UPLOADING = 4
    STAGE_DONE = 5

    @staticmethod
    def upload(dev, filename, progress_callback=None):
        """
        Uploads firmware to an Alarm Decoder device.

        :param filename: The firmware filename
        :type filename: str
        :param progress_callback: Callback function used to report progress.
        :type progress_callback: function

        :raises: NoDeviceError, TimeoutError
        """

        def do_upload():
            """
            Perform the actual firmware upload to the device.
            """
            with open(filename) as f:
                for line in f:
                    line = line.rstrip()

                    if line[0] == ':':
                        dev.write(line + "\r")
                        res = dev.read_line(timeout=10.0)

                        if progress_callback is not None:
                            progress_callback(Firmware.STAGE_UPLOADING)

                        time.sleep(0.0)

        def read_until(pattern, timeout=0.0):
            """
            Read characters until a specific pattern is found or the timeout is hit.
            """
            def timeout_event():
                timeout_event.reading = False

            timeout_event.reading = True

            timer = None
            if timeout > 0:
                timer = threading.Timer(timeout, timeout_event)
                timer.start()

            buf = ''
            position = 0

            while timeout_event.reading:
                try:
                    char = dev.read()

                    if char is not None and char != '':
                        if char == pattern[position]:
                            position = position + 1
                            if position == len(pattern):
                                break
                        else:
                            position = 0

                except Exception, err:
                    pass

            if timer:
                if timer.is_alive():
                    timer.cancel()
                else:
                    raise TimeoutError('Timeout while waiting for line terminator.')

        def stage_callback(stage):
            if progress_callback is not None:
                progress_callback(stage)

        if dev is None:
            raise NoDeviceError('No device specified for firmware upload.')

        stage_callback(Firmware.STAGE_START)

        if dev.is_reader_alive():
            # Close the reader thread and wait for it to die, otherwise
            # it interferes with our reading.
            dev.stop_reader()
            while dev._read_thread.is_alive():
                stage_callback(Firmware.STAGE_WAITING)
                time.sleep(1)

        time.sleep(2)

        # Reboot the device and wait for the boot loader.
        stage_callback(Firmware.STAGE_BOOT)
        dev.write("=")
        read_until('......', timeout=15.0)

        # Get ourselves into the boot loader and wait for indication
        # that it's ready for the firmware upload.
        stage_callback(Firmware.STAGE_LOAD)
        read_until('!load', timeout=15.0)

        # And finally do the upload.
        do_upload()
        stage_callback(Firmware.STAGE_DONE)

# event.py (improved)
#

# Based on pyevent originally found at http://www.emptypage.jp/notes/pyevent.en.html
#
# License: https://creativecommons.org/licenses/by/2.1/jp/deed.en
#
# Changes:
#   * Added type check in fire()
#   * Removed earg from fire() and added support for args/kwargs.

class Event(object):

    def __init__(self, doc=None):
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return EventHandler(self, obj)

    def __set__(self, obj, value):
        pass

class EventHandler(object):

    def __init__(self, event, obj):

        self.event = event
        self.obj = obj

    def _getfunctionlist(self):

        """(internal use) """

        try:
            eventhandler = self.obj.__eventhandler__
        except AttributeError:
            eventhandler = self.obj.__eventhandler__ = {}
        return eventhandler.setdefault(self.event, [])

    def add(self, func):

        """Add new event handler function.

        Event handler function must be defined like func(sender, earg).
        You can add handler also by using '+=' operator.
        """

        self._getfunctionlist().append(func)
        return self

    def remove(self, func):

        """Remove existing event handler function.

        You can remove handler also by using '-=' operator.
        """

        self._getfunctionlist().remove(func)

    def fire(self, *args, **kwargs):

        """Fire event and call all handler functions

        You can call EventHandler object itself like e(*args, **kwargs) instead of
        e.fire(*args, **kwargs).
        """

        for func in self._getfunctionlist():
            if type(func) == EventHandler:
                func.fire(*args, **kwargs)
            else:
                func(self.obj, *args, **kwargs)

    __iadd__ = add
    __isub__ = remove
    __call__ = fire

"""
Contains different types of devices belonging to the Alarm Decoder (AD2) family.

"""

import usb.core, usb.util
import serial, serial.tools.list_ports
import socket

from OpenSSL import SSL, crypto
from pyftdi.pyftdi.ftdi import *
from pyftdi.pyftdi.usbtools import *
from .util import CommError, TimeoutError, NoDeviceError

class Device(object):
    """
    Generic parent device to all Alarm Decoder (AD2) products.
    """

    # Generic device events
    on_open = event.Event('Called when the device has been opened')
    on_close = event.Event('Called when the device has been closed')
    on_read = event.Event('Called when a line has been read from the device')
    on_write = event.Event('Called when data has been written to the device')

        """
        """
        self._id = ''
        self._buffer = ''
        self._device = None
        self._running = False
        self._read_thread = Device.ReadThread(self)

    def __enter__(self):
        """
        Support for context manager __enter__.
        """

    def __exit__(self, type, value, traceback):
        """
        Support for context manager __exit__.
        """
        self.close()

        return False

    @property
    def id(self):
        """
        Retrieve the device ID.

        :returns: The identification string for the device.
        """
        return self._id

    @id.setter
    def id(self, value):
        """
        Sets the device ID.

        :param value: The device identification.
        :type value: str
        """
        self._id = value

    def is_reader_alive(self):
        """
        Indicates whether or not the reader thread is alive.

        :returns: Whether or not the reader thread is alive.
        """
        return self._read_thread.is_alive()

    def stop_reader(self):
        """
        Stops the reader thread.
        """
        self._read_thread.stop()

    def close(self):
        """
        Closes the device.
        """
        try:
            self._running = False
            self._read_thread.stop()
            self._device.close()

        except:
            pass

        self.on_close()

    class ReadThread(threading.Thread):
        """
        Reader thread which processes messages from the device.
        """

        READ_TIMEOUT = 10
        """Timeout for the reader thread."""

        def __init__(self, device):
            """
            Constructor

            :param device: The device used by the reader thread.
            :type device: devices.Device
            """
            threading.Thread.__init__(self)
            self._device = device

        def stop(self):
            """
            Stops the running thread.
            """

        def run(self):
            """
            The actual read process.
            """
            self._running = True

            while self._running:
                try:
                    self._device.read_line(timeout=self.READ_TIMEOUT)

                except TimeoutError, err:
                    pass

                    self._running = False

                    #raise err

                time.sleep(0.01)

class USBDevice(Device):
    """
    AD2USB device exposed with PyFTDI's interface.
    """

    FTDI_VENDOR_ID = 0x0403
    """Vendor ID used to recognize AD2USB devices."""
    FTDI_PRODUCT_ID = 0x6001
    """Product ID used to recognize AD2USB devices."""
    BAUDRATE = 115200
    """Default baudrate for AD2USB devices."""

    __devices = []

    @classmethod
    def find_all(cls, vid=FTDI_VENDOR_ID, pid=FTDI_PRODUCT_ID):
        """
        Returns all FTDI devices matching our vendor and product IDs.

        :returns: list of devices
        :raises: CommError
        """
        cls.__devices = []

        try:
            cls.__devices = Ftdi.find_all([(vid, pid)], nocache=True)

        except (usb.core.USBError, FtdiError), err:
            raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), err)

        return cls.__devices

    def devices(cls):
        """
        Returns a cached list of AD2USB devices located on the system.

        :returns: cached list of devices found.
        """

    def find(cls, device=None):
        """
        Factory method that returns the requested USBDevice device, or the first device.

        :param device: Tuple describing the USB device to open, as returned by find_all().
        :type device: tuple

        :returns: USBDevice object utilizing the specified device.
        :raises: NoDeviceError
        """
        cls.find_all()

        if len(cls.__devices) == 0:
            raise NoDeviceError('No AD2USB devices present.')

        if device is None:
            device = cls.__devices[0]

        vendor, product, sernum, ifcount, description = device

        return USBDevice(interface=sernum)

    def start_detection(cls, on_attached=None, on_detached=None):
        """
        Starts the device detection thread.

        :param on_attached: function to be called when a device is attached.
        :type on_attached: function
        :param on_detached: function to be called when a device is detached.
        :type on_detached: function
        """
        cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached)

        cls.__detect_thread.start()

    def stop_detection(cls):
        """
        Stops the device detection thread.
        """
        try:
            cls.__detect_thread.stop()

        except:
            pass

    @property
    def interface(self):
        """
        Retrieves the interface used to connect to the device.

        :returns: the interface used to connect to the device.
        """
        return self._interface

    @interface.setter
    def interface(self, value):
        """
        Sets the interface used to connect to the device.

        :param value: May specify either the serial number or the device index.
        :type value: str or int
        """
        self._interface = value
        if isinstance(value, int):
            self._device_number = value
        else:
            self._serial_number = value

    @property
    def serial_number(self):
        """
        Retrieves the serial number of the device.

        :returns: The serial number of the device.
        """

        return self._serial_number

    @serial_number.setter
    def serial_number(self, value):
        """
        Sets the serial number of the device.

        :param value: The serial number of the device.
        :type value: string
        """
        self._serial_number = value

    @property
    def description(self):
        """
        Retrieves the description of the device.

        :returns: The description of the device.
        """
        return self._description

    @description.setter
    def description(self, value):
        """
        Sets the description of the device.

        :param value: The description of the device.
        """
        self._description = value

    def __init__(self, interface=0):
        """

        :param interface: May specify either the serial number or the device index.
        :type interface: str or int
        """
        Device.__init__(self)

        self._device = Ftdi()

        self._device_number = 0
        self._serial_number = None
        self.interface = interface
        self._vendor_id = USBDevice.FTDI_VENDOR_ID
        self._product_id = USBDevice.FTDI_PRODUCT_ID
        self._endpoint = 0
        self._description = None

    def open(self, baudrate=BAUDRATE, no_reader_thread=False):
        """
        Opens the device.

        :param baudrate: The baudrate to use.
        :type baudrate: int
        :param no_reader_thread: Whether or not to automatically start the reader thread.
        :type no_reader_thread: bool

        """
        # Set up defaults
        if baudrate is None:
            baudrate = USBDevice.BAUDRATE

        # Open the device and start up the thread.
        try:
            self._device.open(self._vendor_id,
                             self._product_id,
                             self._endpoint,
                             self._device_number,
                             self._serial_number,
                             self._description)

            self._device.set_baudrate(baudrate)

            if not self._serial_number:
                self._serial_number = self._get_serial_number()

            self._id = self._serial_number

            raise NoDeviceError('Error opening device: {0}'.format(str(err)), err)

        else:
            self.on_open()

            if not no_reader_thread:
                self._read_thread.start()

        """
        """
        try:
            Device.close(self)

            # HACK: Probably should fork pyftdi and make this call in .close().
            self._device.usb_dev.attach_kernel_driver(self._device_number)

        except:
            pass

    def write(self, data):
        """
        Writes data to the device.

        :param data: Data to write
        :type data: str

        """
        try:
            self._device.write_data(data)

            self.on_write(data=data)

        except FtdiError, err:
            raise CommError('Error writing to device: {0}'.format(str(err)), err)

    def read(self):
        """
        Reads a single character from the device.

        :returns: The character read from the device.
        """
        ret = None

        try:
            ret = self._device.read_data(1)

            raise CommError('Error reading from device: {0}'.format(str(err)), err)

        return ret

    def read_line(self, timeout=0.0, purge_buffer=False):
        """
        Reads a line from the device.

        :param timeout: Read timeout
        :type timeout: float
        :param purge_buffer: Indicates whether to purge the buffer prior to reading.
        :type purge_buffer: bool

        :returns: The line that was read.
        :raises: CommError, TimeoutError
        """

        if purge_buffer:
            self._buffer = ''

        def timeout_event():
            timeout_event.reading = False

        timeout_event.reading = True

        got_line = False
        ret = None

        timer = None
        if timeout > 0:
            timer = threading.Timer(timeout, timeout_event)
            timer.start()

        try:
                buf = self._device.read_data(1)

                if buf != '':
                    self._buffer += buf

                    if buf == "\n":
                        if len(self._buffer) > 1:
                            if self._buffer[-2] == "\r":
                                self._buffer = self._buffer[:-2]

                                # ignore if we just got \r\n with nothing else in the buffer.
                                if len(self._buffer) != 0:
                                    got_line = True
                                    break
                        else:
                            self._buffer = self._buffer[:-1]

            if timer:
                timer.cancel()

        else:
            if got_line:
                ret = self._buffer
                self._buffer = ''

                self.on_read(data=ret)

        if timer:
            if timer.is_alive():
            else:
                raise TimeoutError('Timeout while waiting for line terminator.')

        return ret

    def _get_serial_number(self):
        """
        Retrieves the FTDI device serial number.

        :returns: string containing the device serial number.
        """
        return usb.util.get_string(self._device.usb_dev, 64, self._device.usb_dev.iSerialNumber)

    class DetectThread(threading.Thread):
        """
        Thread that handles detection of added/removed devices.
        """
        on_attached = event.Event('Called when an AD2USB device has been detected.')
        on_detached = event.Event('Called when an AD2USB device has been removed.')

        def __init__(self, on_attached=None, on_detached=None):
            """

            :param on_attached: Function to call when a device is attached.
            :type on_attached: function
            :param on_detached: Function to call when a device is detached.
            :type on_detached: function
            """

            if on_attached:
                self.on_attached += on_attached

            if on_detached:
                self.on_detached += on_detached

            """
            Stops the thread.
            """

            """
            The actual detection process.
            """

            last_devices = set()

                try:
                    current_devices = set(USBDevice.find_all())

                    new_devices = [d for d in current_devices if d not in last_devices]
                    removed_devices = [d for d in last_devices if d not in current_devices]
                    last_devices = current_devices

                    for d in new_devices:
                        self.on_attached(device=d)

                    for d in removed_devices:
                        self.on_detached(device=d)

                except CommError, err:
                    pass

                time.sleep(0.25)

class SerialDevice(Device):
    """
    AD2USB or AD2SERIAL device exposed with the pyserial interface.
    """

    BAUDRATE = 19200
    """Default baudrate for Serial devices."""

    def find_all(pattern=None):
        """
        Returns all serial ports present.

        :param pattern: Pattern to search for when retrieving serial ports.
        :type pattern: str

        """
        devices = []

        try:
            if pattern:
                devices = serial.tools.list_ports.grep(pattern)
            else:
                devices = serial.tools.list_ports.comports()

        except SerialException, err:
            raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err)

        return devices

    @property
        """

        """
        return self._port

        """

        :param value: The name of the serial device.
        """
        self._port = value

    def __init__(self, interface=None):
        """

        :param interface: The device to open.
        :type interface: str
        """

        self._port = interface
        self._id = interface
        self._device = serial.Serial(timeout=0, writeTimeout=0)     # Timeout = non-blocking to match pyftdi.

        """

        :param baudrate: The baudrate to use with the device.

        """
        # Set up the defaults
            baudrate = SerialDevice.BAUDRATE

        if self._port is None:
            raise NoDeviceError('No device interface specified.')

        self._device.port = self._port

        # Open the device and start up the reader thread.
        try:
            self._device.open()
            self._device.baudrate = baudrate            # NOTE: Setting the baudrate before opening the
                                                        #       port caused issues with Moschip 7840/7820
                                                        #       USB Serial Driver converter. (mos7840)
                                                        #
                                                        #       Moving it to this point seems to resolve
                                                        #       all issues with it.

        except (serial.SerialException, ValueError), err:
            raise NoDeviceError('Error opening device on port {0}.'.format(self._port), err)

        else:

        """
        """
        try:

        except:
            pass

        """

        :param data: The data to write.

        """
        try:
            self._device.write(data)

        except serial.SerialTimeoutException, err:
            pass

        except serial.SerialException, err:
            raise CommError('Error writing to device.', err)

        else:

        """

        """
        ret = None

        try:
            ret = self._device.read(1)

        return ret

        """

        :param timeout: The read timeout.

        :returns: The line read.
        """

        ret = None

        try:
                buf = self._device.read(1)

                if buf != '' and buf != "\xff":     # AD2SERIAL specifically apparently sends down \xFF on boot.

                                    break
                        else:

        except (OSError, serial.SerialException), err:
            if timer:

        else:

        if timer:
            else:

        return ret

class SocketDevice(Device):
    """
    Device that supports communication with an Alarm Decoder (AD2) that is
    exposed via ser2sock or another Serial to IP interface.
    """

    @property
        """

        """
        return (self._host, self._port)

        """

        :param value: Tuple containing the host and port to use.
        :type value: tuple
        """
        self._host = value[0]
        self._port = value[1]

    @property
    def ssl(self):
        """
        Retrieves whether or not the device is using SSL.

        :returns: Whether or not the device is using SSL.
        """
        return self._use_ssl

    @ssl.setter
    def ssl(self, value):
        """
        Sets whether or not SSL communication is in use.

        :param value: Whether or not SSL communication is in use.
        :type value: bool
        """
        self._use_ssl = value

    @property
    def ssl_certificate(self):
        """
        Retrieves the SSL client certificate path used for authentication.

        :returns: The certificate path
        """
        return self._ssl_certificate

    @ssl_certificate.setter
    def ssl_certificate(self, value):
        """
        Sets the SSL client certificate to use for authentication.

        :param value: The path to the SSL certificate.
        """
        self._ssl_certificate = value

    @property
    def ssl_key(self):
        """
        Retrieves the SSL client certificate key used for authentication.

        :returns: The key path
        """
        return self._ssl_key

    @ssl_key.setter
    def ssl_key(self, value):
        """
        Sets the SSL client certificate key to use for authentication.

        :param value: The path to the SSL key.
        """
        self._ssl_key = value

    @property
    def ssl_ca(self):
        """
        Retrieves the SSL Certificate Authority certificate used for authentication.

        :returns: The CA path
        """
        return self._ssl_ca

    @ssl_ca.setter
    def ssl_ca(self, value):
        """
        Sets the SSL Certificate Authority certificate used for authentication.

        :param value: The path to the SSL CA certificate.
        """
        self._ssl_ca = value

    def __init__(self, interface=("localhost", 10000)):
        """

        :param interface: Tuple containing the hostname and port of our target.
        :type interface: tuple
        """

        self._host, self._port = interface
        self._use_ssl = False
        self._ssl_certificate = None
        self._ssl_key = None
        self._ssl_ca = None

    def open(self, baudrate=None, no_reader_thread=False):
        """

        :param baudrate: The baudrate to use
        :param no_reader_thread: Whether or not to automatically open the reader thread.

        :raises: NoDeviceError, CommError
        """

        try:
            self._device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

            if self._use_ssl:
                self._init_ssl()

            self._device.connect((self._host, self._port))

                self._device.do_handshake()

            self._id = '{0}:{1}'.format(self._host, self._port)

        except socket.error, err:
            raise NoDeviceError('Error opening device at {0}:{1}'.format(self._host, self._port), err)

        else:

        """
        """
        try:
            # TODO: Find a way to speed up this shutdown.
            if self.ssl:
                self._device.shutdown()

            else:
                self._device.shutdown(socket.SHUT_RDWR)     # Make sure that it closes immediately.

        except Exception, ex:
            pass

        """

        :returns: The number of bytes sent.
        """
        data_sent = None

        try:
            data_sent = self._device.send(data)

            if data_sent == 0:
                raise CommError('Error writing to device.')

        except (SSL.Error, socket.error), err:

        return data_sent

        """

        """
        data = None

        try:
            data = self._device.recv(1)

            raise CommError('Error while reading from device: {0}'.format(str(err)), err)

        return data

        """

        :returns: The line read from the device.
        """

        ret = None

        try:
                buf = self._device.recv(1)

                                    break
                        else:

            if timer:

        else:

        if timer:
            else:

        return ret

    def _init_ssl(self):
        try:
            ctx = SSL.Context(SSL.TLSv1_METHOD)

            if isinstance(self.ssl_key, crypto.PKey):
                ctx.use_privatekey(self.ssl_key)
            else:
                ctx.use_privatekey_file(self.ssl_key)

            if isinstance(self.ssl_certificate, crypto.X509):
                ctx.use_certificate(self.ssl_certificate)
            else:
                ctx.use_certificate_file(self.ssl_certificate)

            if isinstance(self.ssl_ca, crypto.X509):
                store = ctx.get_cert_store()
                store.add_cert(self.ssl_ca)
            else:
                ctx.load_verify_locations(self.ssl_ca, None)

            ctx.set_verify(SSL.VERIFY_PEER, self._verify_ssl_callback)

            self._device = SSL.Connection(ctx, self._device)

        except SSL.Error, err:
            raise CommError('Error setting up SSL connection.', err)

    def _verify_ssl_callback(self, connection, x509, errnum, errdepth, ok):
        return ok

"""
Message representations received from the panel through the Alarm Decoder (AD2)
devices.

"""

import re

from .util import InvalidMessageError

class BaseMessage(object):
    """
    Base class for messages.
    """

    raw = None
    """The raw message text"""

        """
        """
        pass

        """
        """
        return self.raw

class Message(BaseMessage):
    """
    Represents a message from the alarm panel.
    """

    ready = False
    """Indicates whether or not the panel is in a ready state"""
    armed_away = False
    """Indicates whether or not the panel is armed away"""
    armed_home = False
    """Indicates whether or not the panel is armed home"""
    backlight_on = False
    """Indicates whether or not the keypad backlight is on"""
    programming_mode = False
    """Indicates whether or not we're in programming mode"""
    beeps = -1
    """Number of beeps associated with a message"""
    zone_bypassed = False
    """Indicates whether or not a zone is bypassed"""
    ac_power = False
    """Indicates whether or not the panel is on AC power"""
    chime_on = False
    """Indicates whether or not the chime is enabled"""
    alarm_event_occurred = False
    """Indicates whether or not an alarm event has occurred"""
    alarm_sounding = False
    """Indicates whether or not an alarm is sounding"""
    battery_low = False
    """Indicates whether or not there is a low battery"""
    entry_delay_off = False
    """Indicates whether or not the entry delay is enabled"""
    fire_alarm = False
    """Indicates whether or not a fire alarm is sounding"""
    check_zone = False
    """Indicates whether or not there are zones that require attention."""
    perimeter_only = False
    """Indicates whether or not the perimeter is armed"""
    numeric_code = None
    """The numeric code associated with the message"""
    text = None
    """The human-readable text to be displayed on the panel LCD"""
    cursor_location = -1
    """Current cursor location on the keypad"""
    mask = None
    """Address mask this message is intended for"""
    bitfield = None
    """The bitfield associated with this message"""
    panel_data = None
    """The panel data field associated with this message"""

    def __init__(self, data=None):
        """

        :param data: Message data to parse.
        """
        BaseMessage.__init__(self)

        self._regex = re.compile('("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*),("(?:[^"]|"")*"|[^,]*)')

        if data is not None:
            self._parse_message(data)

        """
        """

    def _parse_message(self, data):
        """
        Parse the message from the device.

        :param data: The message data.

        :raises: InvalidMessageError
        """
        m = self._regex.match(data)

        if m is None:
            raise InvalidMessageError('Received invalid message: {0}'.format(data))

        self.bitfield, self.numeric_code, self.panel_data, alpha = m.group(1, 2, 3, 4)
        self.mask = int(self.panel_data[3:3+8], 16)

        is_bit_set = lambda bit: not self.bitfield[bit] == "0"

        self.raw = data
        self.ready = is_bit_set(1)
        self.armed_away = is_bit_set(2)
        self.armed_home = is_bit_set(3)
        self.backlight_on = is_bit_set(4)
        self.programming_mode = is_bit_set(5)
        self.beeps = int(self.bitfield[6], 16)
        self.zone_bypassed = is_bit_set(7)
        self.ac_power = is_bit_set(8)
        self.chime_on = is_bit_set(9)
        self.alarm_event_occurred = is_bit_set(10)
        self.alarm_sounding = is_bit_set(11)
        self.battery_low = is_bit_set(12)
        self.entry_delay_off = is_bit_set(13)
        self.fire_alarm = is_bit_set(14)
        self.check_zone = is_bit_set(15)
        self.perimeter_only = is_bit_set(16)
        # bits 17-20 unused.
        self.text = alpha.strip('"')

        if int(self.panel_data[19:21], 16) & 0x01 > 0:
            self.cursor_location = int(self.bitfield[21:23], 16)    # Alpha character index that the cursor is on.

class ExpanderMessage(BaseMessage):
    """
    Represents a message from a zone or relay expansion module.
    """

    ZONE = 0
    """Flag indicating that the expander message relates to a Zone Expander."""
    RELAY = 1
    """Flag indicating that the expander message relates to a Relay Expander."""

    type = None
    """Expander message type: ExpanderMessage.ZONE or ExpanderMessage.RELAY"""
    address = -1
    """Address of expander"""
    channel = -1
    """Channel on the expander"""
    value = -1
    """Value associated with the message"""

        """

        :param data: The message data to parse.
        """

        """
        """

        """
        Parse the raw message from the device.

        :param data: The message data
        """
        try:
            header, values = data.split(':')
            address, channel, value = values.split(',')

            self.raw = data
            self.address = int(address)
            self.channel = int(channel)
            self.value = int(value)

        except ValueError:

        if header == '!EXP':
            self.type = ExpanderMessage.ZONE
        elif header == '!REL':
            self.type = ExpanderMessage.RELAY
        else:
            raise InvalidMessageError('Unknown expander message header: {0}'.format(data))

class RFMessage(BaseMessage):
    """
    Represents a message from an RF receiver.
    """

    serial_number = None
    """Serial number of the RF device"""
    value = -1
    """Value associated with this message"""
    battery = False
    """Battery low indication"""
    supervision = False
    """Supervision required indication"""
    loop = [False for x in range(4)]
    """Loop indicators"""

        """

        :param data: The message data to parse
        """

        """
        """

        """
        Parses the raw message from the device.

        """
        try:

            _, values = data.split(':')
            self.serial_number, self.value = values.split(',')
            self.value = int(self.value, 16)

            is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0

            # Bit 1 = unknown
            self.battery = is_bit_set(2)
            self.supervision = is_bit_set(3)
            # Bit 4 = unknown
            self.loop[2] = is_bit_set(5)
            self.loop[1] = is_bit_set(6)
            self.loop[3] = is_bit_set(7)
            self.loop[0] = is_bit_set(8)

class LRRMessage(BaseMessage):
    """
    Represent a message from a Long Range Radio.
    """

    event_data = None
    """Data associated with the LRR message.  Usually user ID or zone."""
    partition = -1
    """The partition that this message applies to"""
    event_type = None
    """The type of the event that occurred"""

        """

        """

        """
        """

        """

        """
        try:

            self.event_data, self.partition, self.event_type = values.split(',')

on_attached (alarmdecoder.devices.USBDevice.DetectThread attribute)

on_detached (alarmdecoder.devices.USBDevice.DetectThread attribute)