"""
Contains different types of devices belonging to the AD2USB family.
"""

import usb.core
import usb.util
import time
import threading
import serial
import serial.tools.list_ports
import socket
from pyftdi.pyftdi.ftdi import *
from pyftdi.pyftdi.usbtools import *
from . import util
from .event import event

class Device(object):
    """
    Generic parent device to all AD2USB 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')

    def __init__(self):
        self._id = ''
        self._buffer = ''
        self._interface = None
        self._device = None
        self._running = False
        self._read_thread = Device.ReadThread(self)    # NOTE: not sure this is going to work..

    def __del__(self):
        pass

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, value):
        self._id = value

    def is_reader_alive(self):
        """
        Indicates 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()

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

        READ_TIMEOUT = 10

        def __init__(self, device):
            """
            Constructor
            """
            threading.Thread.__init__(self)
            self._device = device
            self._running = False

        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 util.TimeoutError, err:
                    pass

                time.sleep(0.01)

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

    # Constants
    FTDI_VENDOR_ID = 0x0403
    FTDI_PRODUCT_ID = 0x6001
    BAUDRATE = 115200

    @staticmethod
    def find_all():
        """
        Returns all FTDI devices matching our vendor and product IDs.
        """
        devices = []

        try:
            devices = Ftdi.find_all([(USBDevice.FTDI_VENDOR_ID, USBDevice.FTDI_PRODUCT_ID)], nocache=True)
        except (usb.core.USBError, FtdiError), err:
            raise util.CommError('Error enumerating AD2USB devices: {0}'.format(str(err)))

        return devices

    def __init__(self, vid=FTDI_VENDOR_ID, pid=FTDI_PRODUCT_ID, serial=None, description=None, interface=0):
        """
        Constructor
        """
        Device.__init__(self)

        self._device = Ftdi()
        self._interface = interface
        self._vendor_id = vid
        self._product_id = pid
        self._serial_number = serial
        self._description = description

    def open(self, baudrate=BAUDRATE, interface=None, index=0, no_reader_thread=False):
        """
        Opens the device.
        """
        # Set up defaults
        if baudrate is None:
            baudrate = USBDevice.BAUDRATE

        if self._interface is None and interface is None:
            self._interface = 0

        if interface is not None:
            self._interface = interface

        if index is None:
            index = 0

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

            self._device.set_baudrate(baudrate)

            self._id = 'USB {0}:{1}'.format(self._device.usb_dev.bus, self._device.usb_dev.address)
            self.on_close()

            raise util.NoDeviceError('Error opening AD2USB device: {0}'.format(str(err)))
        else:
            if not no_reader_thread:
                self._read_thread.start()

            self.on_open((self._serial_number, self._description))

    def close(self):
        """
        Closes the device.
        """
        try:
            self._read_thread.stop()

            self._device.close()

            # HACK: Probably should fork pyftdi and make this call in .close().
            self._device.usb_dev.attach_kernel_driver(self._interface)
        except (FtdiError, usb.core.USBError):
            pass

        self.on_close()

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

            self.on_write(data)
        except FtdiError, err:
            raise util.CommError('Error writing to AD2USB device.')

    def read(self):
        """
        Reads a single character from the device.
        """
        return self._device.read_data(1)

    def read_line(self, timeout=0.0):
        """
        Reads a line from the device.
        """
        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:
            while timeout_event.reading:
                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]

                time.sleep(0.001)

            timer.cancel()

            raise util.CommError('Error reading from AD2USB device: {0}'.format(str(err)))
        else:
            if got_line:
                ret = self._buffer
                self._buffer = ''

                self.on_read(ret)

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

        return ret

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

    BAUDRATE = 19200

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

        try:
            if pattern:
                devices = serial.tools.list_ports.grep(pattern)
            else:
                devices = serial.tools.list_ports.comports()
        except Exception, err:
            raise util.CommError('Error enumerating AD2SERIAL devices: {0}'.format(str(err)))

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

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

    def open(self, baudrate=BAUDRATE, interface=None, index=None, no_reader_thread=False):
        """
        """
        # Set up the defaults
            baudrate = SerialDevice.BAUDRATE

            raise util.NoDeviceError('No AD2SERIAL device interface specified.')

        self._device.port = self._interface

        # 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 util.NoDeviceError('Error opening AD2SERIAL device on port {0}.'.format(interface))
        else:
            self.on_open(('N/A', "AD2SERIAL"))

        """
        """
        try:

            pass

        """
        """
        try:
            self._device.write(data)
        except serial.SerialTimeoutException, err:
            pass
        except serial.SerialException, err:
            raise util.CommError('Error writing to serial device.')
        else:

        """
        """
        return self._device.read(1)

        """
        """

        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:

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

        if timer:
            else:

        return ret

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

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

        self._host, self._port = interface

    def open(self, baudrate=None, interface=None, index=0, no_reader_thread=False):
        """
        """
            self._host, self._port = interface

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

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

        except socket.error, err:

            raise util.NoDeviceError('Error opening AD2SOCKET device at {0}:{1}'.format(self._host, self._port))
        else:

            self.on_open(('N/A', "AD2SOCKET"))

        """
        """

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

        """
        """
        data_sent = self._device.send(data)

        if data_sent == 0:
            raise util.CommError('Error while sending data.')
        else:

        return data_sent

        """
        """
        try:
            data = self._device.recv(1)
            raise util.CommError('Error while reading from device: {0}'.format(str(err)))

        return data

        """
        """

        ret = None

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

                                    break
                        else:

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

        if timer:
            else:

        return ret

"""
Provides zone tracking functionality for the AD2USB device family.
"""

from . import messages

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):
        """

        :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."""

        """
        """
        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
        """
        zone = -1

        if isinstance(message, messages.ExpanderMessage):
            if message.type == messages.ExpanderMessage.ZONE:
                zone = self._expander_to_zone(int(message.address), int(message.channel))

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

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

        else:
            # Panel is ready, restore all zones.
            if message.ready:
                for idx, z in enumerate(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)

                # 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 = found_new = at_end = False

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

                if z == self._last_zone_fault:
                    found_last = 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_new:

                if z == zone:
                    found_new = 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_new:
            it = iter(self._zones_faulted)

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

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

            except StopIteration:
                pass

        # Actually remove the zones and trigger the restores.
        for idx, z in enumerate(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)

    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)

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

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

        return False

    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 the full AD2USB class and factory.
"""

import re
from . import devices

class Overseer(object):
    """
    Factory for creation of AD2USB devices as well as provide4s attach/detach events."
    """

    # Factory events
    on_attached = event.Event('Called when an AD2USB device has been detected.')
    on_detached = event.Event('Called when an AD2USB device has been removed.')

    __devices = []

    @classmethod
    def find_all(cls):
        """
        Returns all AD2USB devices located on the system.
        """
        cls.__devices = devices.USBDevice.find_all()

        return cls.__devices

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

    def create(cls, device=None):
        """
        Factory method that returns the requested AD2USB device, or the first device.
        """
        cls.find_all()

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

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

        vendor, product, sernum, ifcount, description = device
        device = devices.USBDevice(serial=sernum, description=description)

        return AD2USB(device)

    def __init__(self, attached_event=None, detached_event=None):
        """
        """
        self._detect_thread = Overseer.DetectThread(self)

        if attached_event:
            self.on_attached += attached_event

        if detached_event:
            self.on_detached += detached_event

        Overseer.find_all()

        self.start()

        """
        Clean up and shut down.
        """
        self.stop()

    def start(self):
        """
        Starts the detection thread, if not already running.
        """
        if not self._detect_thread.is_alive():
            self._detect_thread.start()

    def stop(self):
        """
        Stops the detection thread.
        """
        self._detect_thread.stop()

    def get_device(self, device=None):
        """
        """
        return Overseer.create(device)

    class DetectThread(threading.Thread):
        """
        Thread that handles detection of added/removed devices.
        """
        def __init__(self, overseer):
            """
            """

            self._overseer = overseer

            """
            Stops the thread.
            """

            """
            The actual detection process.
            """

            last_devices = set()

                try:
                    Overseer.find_all()

                    current_devices = set(Overseer.devices())
                    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._overseer.on_attached(d)

                    for d in removed_devices:
                        self._overseer.on_detached(d)

                except util.CommError, err:
                    pass

                time.sleep(0.25)

class AD2USB(object):
    """
    High-level wrapper around AD2USB/AD2SERIAL devices.
    """

    # High-level Events
    on_status_changed = event.Event('Called when the panel status changes.')
    on_power_changed = event.Event('Called when panel power switches between AC and DC.')
    on_alarm = event.Event('Called when the alarm is triggered.')
    on_bypass = event.Event('Called when a zone is bypassed.')
    on_boot = event.Event('Called when the device finishes bootings.')
    on_config_received = event.Event('Called when the device receives its configuration.')

    # Mid-level Events
    on_message = event.Event('Called when a message has been received from the device.')

    # Low-level 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.')

    F1 = unichr(1) + unichr(1) + unichr(1)
    F2 = unichr(2) + unichr(2) + unichr(2)
    F3 = unichr(3) + unichr(3) + unichr(3)
    F4 = unichr(4) + unichr(4) + unichr(4)

    def __init__(self, device):
        """
        """
        self._device = device
        self._power_status = None
        self._alarm_status = None
        self._bypass_status = None

        self._settings = {}

        self._address_mask = 0xFF80     # TEMP

    def open(self, baudrate=None, interface=None, index=None, no_reader_thread=False):
        """
        """
        self._wire_events()
        self._device.open(baudrate=baudrate, interface=interface, index=index, no_reader_thread=no_reader_thread)

        """
        """
        self._device.close()

    def get_config(self):
        """
        Retrieves the configuration from the device.
        """
        self._device.write("C\r")

    def set_config(self, settings):
        """
        Sets configuration entries on the device.
        """
        pass

    def reboot(self):
        """
        Reboots the device.
        """
        self._device.write('=')

    @property
        return self._device.id

    def _wire_events(self):
        """
        Wires up the internal device events.
        """
        self._device.on_open += self._on_open
        self._device.on_close += self._on_close
        self._device.on_read += self._on_read
        self._device.on_write += self._on_write

    def _handle_message(self, data):
        """
        Parses messages from the panel.
        """
        if data is None:
            return None

        msg = None

        if data[0] != '!':
            msg = Message(data)

            if self._address_mask & msg.mask > 0:
                self._update_internal_states(msg)

        else:   # specialty messages
            header = data[0:4]

            if header == '!EXP' or header == '!REL':
                msg = ExpanderMessage(data)
            elif header == '!RFX':
                msg = RFMessage(data)
            elif header == '!LRR':
                msg = LRRMessage(data)
            elif data.startswith('!Ready'):
                self.on_boot()
            elif data.startswith('!CONFIG'):
                self._handle_config(data)

        return msg

    def _handle_config(self, data):
        _, config_string = data.split('>')
        for setting in config_string.split('&'):
            k, v = setting.split('=')

            self._settings[k] = v

        self.on_config_received(self._settings)

    def _update_internal_states(self, message):
        if message.ac_power != self._power_status:
            self._power_status, old_status = message.ac_power, self._power_status

            if old_status is not None:
                self.on_power_changed(self._power_status)

        if message.alarm_sounding != self._alarm_status:
            self._alarm_status, old_status = message.alarm_sounding, self._alarm_status

                self.on_alarm(self._alarm_status)

        if message.zone_bypassed != self._bypass_status:
            self._bypass_status, old_status = message.zone_bypassed, self._bypass_status

                self.on_bypass(self._bypass_status)

    def _on_open(self, sender, args):
        """
        Internal handler for opening the device.
        """
        self.on_open(args)

    def _on_close(self, sender, args):
        """
        Internal handler for closing the device.
        """
        self.on_close(args)

    def _on_read(self, sender, args):
        """
        Internal handler for reading from the device.
        """
        self.on_read(args)

        msg = self._handle_message(args)
        if msg:
            self.on_message(msg)

    def _on_write(self, sender, args):
        """
        Internal handler for writing to the device.
        """
        self.on_write(args)

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

    def __init__(self, data=None):
        """
        """
        self.ready = False
        self.armed_away = False
        self.armed_home = False
        self.backlight_on = False
        self.programming_mode = False
        self.beeps = -1
        self.zone_bypassed = False
        self.ac_power = False
        self.chime_on = False
        self.alarm_event_occurred = False
        self.alarm_sounding = False
        self.numeric_code = ""
        self.text = ""
        self.cursor_location = -1
        self.data = ""
        self.mask = ""
        self.bitfield = ""
        self.panel_data = ""

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

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

    def _parse_message(self, data):
        """
        Parse the message from the device.
        """
        m = self._regex.match(data)

        if m is None:
            raise util.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)

        self.data = data
        self.ready = not self.bitfield[1:2] == "0"
        self.armed_away = not self.bitfield[2:3] == "0"
        self.armed_home = not self.bitfield[3:4] == "0"
        self.backlight_on = not self.bitfield[4:5] == "0"
        self.programming_mode = not self.bitfield[5:6] == "0"
        self.beeps = int(self.bitfield[6:7], 16)
        self.zone_bypassed = not self.bitfield[7:8] == "0"
        self.ac_power = not self.bitfield[8:9] == "0"
        self.chime_on = not self.bitfield[9:10] == "0"
        self.alarm_event_occurred = not self.bitfield[10:11] == "0"
        self.alarm_sounding = not self.bitfield[11:12] == "0"
        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.

        """
        """
        return 'msg > {0:0<9} [{1}{2}{3}] -- ({4}) {5}'.format(hex(self.mask), 1 if self.ready else 0, 1 if self.armed_away else 0, 1 if self.armed_home else 0, self.numeric_code, self.text)

class ExpanderMessage(object):
    """
    Represents a message from a zone or relay expansion module.
    """
    ZONE = 0
    RELAY = 1

        """
        """
        self.type = None
        self.address = None
        self.channel = None
        self.value = None
        self.raw = None

        """
        """
        expander_type = 'UNKWN'
        if self.type == ExpanderMessage.ZONE:
            expander_type = 'ZONE'
        elif self.type  == ExpanderMessage.RELAY:
            expander_type = 'RELAY'

        return 'exp > [{0: <5}] {1}/{2} -- {3}'.format(expander_type, self.address, self.channel, self.value)

        """
        Parse the raw message from the device.
        """
        header, values = data.split(':')
        address, channel, value = values.split(',')

        self.raw = data
        self.address = address
        self.channel = channel
        self.value = value

        if header == '!EXP':
            self.type = ExpanderMessage.ZONE
        elif header == '!REL':
            self.type = ExpanderMessage.RELAY

class RFMessage(object):
    """
    Represents a message from an RF receiver.
    """
        """
        """
        self.serial_number = None

        """
        """
        return 'rf > {0}: {1}'.format(self.serial_number, self.value)

        """
        Parses the raw message from the device.
        """

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

class LRRMessage(object):
    """
    Represent a message from a Long Range Radio.
    """
        """
        """
        self._event_data = None
        self._partition = None
        self._event_type = None

        """
        """
        return 'lrr > {0} @ {1} -- {2}'.format()

        """
        """

        self._event_data, self._partition, self._event_type = values.split(',')

"""
Message representations received from the panel through the AD2USB.
"""

import re

    """
    """

        """
        """
        self.battery_low = False
        self.entry_delay_off = False
        self.fire_alarm = False
        self.check_zone = False
        self.perimeter_only = False

        """
        """

        self.battery_low = not self.bitfield[12:13] == "0"
        self.entry_delay_off = not self.bitfield[13:14] == "0"
        self.fire_alarm = not self.bitfield[14:15] == "0"
        self.check_zone = not self.bitfield[15:16] == "0"
        self.perimeter_only = not self.bitfield[16:17] == "0"
        # bits 17-20 unused.

        """
        """

    """
    """

    ZONE = 0
    RELAY = 1

        """
        """

        """
        """

        """
        """

    """
    """

        """
        """

        """
        """

        """
        """

    """
    """

        """
        """

        """
        """

        """
        """

"""
Provides utility classes for the AD2USB devices.
"""

import ad2usb
import traceback

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 AD2USB/AD2SERIAL devices.
    """

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

    def upload(dev, filename, progress_callback=None):
        """
        Uploads firmware to an AD2USB/AD2SERIAL device.
        """

        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.05)

        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

                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)

        # Reboot the device and wait for the boot loader.
        stage_callback(Firmware.STAGE_BOOT)
        dev.write("=")
        read_until('!boot', 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)

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, earg=None):

        """Fire event and call all handler functions

        You can call EventHandler object itself like e(earg) instead of
        e.fire(earg).
        """

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

    __iadd__ = add
    __isub__ = remove
    __call__ = fire