"""
Provides the main AlarmDecoder class.

.. _AlarmDecoder: http://www.alarmdecoder.com

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

import time
import re

from .event import event
from .util import InvalidMessageError
from .messages import Message, ExpanderMessage, RFMessage, LRRMessage
from .zonetracking import Zonetracker

class AlarmDecoder(object):
    """
    High-level wrapper around `AlarmDecoder`_ (AD2) devices.
    """

    # High-level Events
    on_arm = event.Event("This event is called when the panel is armed.\n\n**Callback definition:** *def callback(device)*")
    on_disarm = event.Event("This event is called when the panel is disarmed.\n\n**Callback definition:** *def callback(device)*")
    on_power_changed = event.Event("This event is called when panel power switches between AC and DC.\n\n**Callback definition:** *def callback(device, status)*")
    on_alarm = event.Event("This event is called when the alarm is triggered.\n\n**Callback definition:** *def callback(device, status)*")
    on_fire = event.Event("This event is called when a fire is detected.\n\n**Callback definition:** *def callback(device, status)*")
    on_bypass = event.Event("This event is called when a zone is bypassed.  \n\n\n\n**Callback definition:** *def callback(device, status)*")
    on_boot = event.Event("This event is called when the device finishes booting.\n\n**Callback definition:** *def callback(device)*")
    on_config_received = event.Event("This event is called when the device receives its configuration. \n\n**Callback definition:** *def callback(device)*")
    on_zone_fault = event.Event("This event is called when :py:class:`~alarmdecoder.zonetracking.Zonetracker` detects a zone fault.\n\n**Callback definition:** *def callback(device, zone)*")
    on_zone_restore = event.Event("This event is called when :py:class:`~alarmdecoder.zonetracking.Zonetracker` detects that a fault is restored.\n\n**Callback definition:** *def callback(device, zone)*")
    on_low_battery = event.Event("This event is called when the device detects a low battery.\n\n**Callback definition:** *def callback(device, status)*")
    on_panic = event.Event("This event is called when the device detects a panic.\n\n**Callback definition:** *def callback(device, status)*")
    on_relay_changed = event.Event("This event is called when a relay is opened or closed on an expander board.\n\n**Callback definition:** *def callback(device, message)*")

    # Mid-level Events
    on_message = event.Event("This event is called when standard panel :py:class:`~alarmdecoder.messages.Message` is received.\n\n**Callback definition:** *def callback(device, message)*")
    on_expander_message = event.Event("This event is called when an :py:class:`~alarmdecoder.messages.ExpanderMessage` is received.\n\n**Callback definition:** *def callback(device, message)*")
    on_lrr_message = event.Event("This event is called when an :py:class:`~alarmdecoder.messages.LRRMessage` is received.\n\n**Callback definition:** *def callback(device, message)*")
    on_rfx_message = event.Event("This event is called when an :py:class:`~alarmdecoder.messages.RFMessage` is received.\n\n**Callback definition:** *def callback(device, message)*")
    on_sending_received = event.Event("This event is called when a !Sending.done message is received from the AlarmDecoder.\n\n**Callback definition:** *def callback(device, status, message)*")

    # Low-level Events
    on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*")
    on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** *def callback(device)*")
    on_read = event.Event("This event is called when a line has been read from the device.\n\n**Callback definition:** *def callback(device, data)*")
    on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** *def callback(device, data)*")

    # Constants
    KEY_F1 = unichr(1) + unichr(1) + unichr(1)
    """Represents panel function key #1"""
    KEY_F2 = unichr(2) + unichr(2) + unichr(2)
    """Represents panel function key #2"""
    KEY_F3 = unichr(3) + unichr(3) + unichr(3)
    """Represents panel function key #3"""
    KEY_F4 = unichr(4) + unichr(4) + unichr(4)
    """Represents panel function key #4"""
    KEY_PANIC = unichr(5) + unichr(5) + unichr(5)
    """Represents a panic keypress"""

    BATTERY_TIMEOUT = 30
    """Default timeout (in seconds) before the battery status reverts."""
    FIRE_TIMEOUT = 30
    """Default tTimeout (in seconds) before the fire status reverts."""

    # Attributes
    address = 18
    """The keypad address in use by the device."""
    configbits = 0xFF00
    """The configuration bits set on the device."""
    address_mask = 0xFFFFFFFF
    """The address mask configured on the device."""
    emulate_zone = [False for _ in range(5)]
    """List containing the devices zone emulation status."""
    emulate_relay = [False for _ in range(4)]
    """List containing the devices relay emulation status."""
    emulate_lrr = False
    """The status of the devices LRR emulation."""
    deduplicate = False
    """The status of message deduplication as configured on the device."""

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

        :param device: The low-level device used for this `AlarmDecoder`_
                       interface.
        :type device: Device
        """
        self._device = device
        self._zonetracker = Zonetracker()

        self._battery_timeout = AlarmDecoder.BATTERY_TIMEOUT
        self._fire_timeout = AlarmDecoder.FIRE_TIMEOUT
        self._power_status = None
        self._alarm_status = None
        self._bypass_status = None
        self._armed_status = None
        self._fire_status = (False, 0)
        self._battery_status = (False, 0)
        self._panic_status = None
        self._relay_status = {}

        self.address = 18
        self.configbits = 0xFF00
        self.address_mask = 0x00000000
        self.emulate_zone = [False for x in range(5)]
        self.emulate_relay = [False for x in range(4)]
        self.emulate_lrr = False
        self.deduplicate = False

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

    def __exit__(self, exc_type, exc_value, traceback):
        """
        Support for context manager __exit__.
        """
        self.close()

        return False

    @property
    def id(self):
        """
        The ID of the `AlarmDecoder`_ device.

        :returns: identification string for the device
        """
        return self._device.id

    @property
    def battery_timeout(self):
        """
        Retrieves the timeout for restoring the battery status, in seconds.

        :returns: battery status timeout
        """
        return self._battery_timeout

    @battery_timeout.setter
    def battery_timeout(self, value):
        """
        Sets the timeout for restoring the battery status, in seconds.

        :param value: timeout in seconds
        :type value: int
        """
        self._battery_timeout = value

    @property
    def fire_timeout(self):
        """
        Retrieves the timeout for restoring the fire status, in seconds.

        :returns: fire status timeout
        """
        return self._fire_timeout

    @fire_timeout.setter
    def fire_timeout(self, value):
        """
        Sets the timeout for restoring the fire status, in seconds.

        """
        self._fire_timeout = value

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

        :param baudrate: baudrate used for the device.  Defaults to the lower-level device default.
        :type baudrate: int
        :param no_reader_thread: Specifies whether or not the automatic reader
                                 thread should be started.
        :type no_reader_thread: bool
        """
        self._wire_events()
        self._device.open(baudrate=baudrate, no_reader_thread=no_reader_thread)

    def close(self):
        """
        Closes the device.
        """
        if self._device:
            self._device.close()

        del self._device
        self._device = None

    def send(self, data):
        """
        Sends data to the `AlarmDecoder`_ device.

        :param data: data to send
        :type data: string
        """

            self._device.write(str(data))

    def get_config(self):
        """
        Retrieves the configuration from the device.  Called automatically by :py:meth:`_on_open`.
        """
        self.send("C\r")

    def save_config(self):
        """
        Sets configuration entries on the device.
        """
        config_string = ''
        config_entries = []

        # HACK: This is ugly.. but I can't think of an elegant way of doing it.
        config_entries.append(('ADDRESS',
                               '{0}'.format(self.address)))
        config_entries.append(('CONFIGBITS',
                               '{0:x}'.format(self.configbits)))
        config_entries.append(('MASK',
                               '{0:x}'.format(self.address_mask)))
        config_entries.append(('EXP',
                               ''.join(['Y' if z else 'N' for z in self.emulate_zone])))
        config_entries.append(('REL',
                               ''.join(['Y' if r else 'N' for r in self.emulate_relay])))
        config_entries.append(('LRR',
                               'Y' if self.emulate_lrr else 'N'))
        config_entries.append(('DEDUPLICATE',
                               'Y' if self.deduplicate else 'N'))

        config_string = '&'.join(['='.join(t) for t in config_entries])

        self.send("C{0}\r".format(config_string))

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

    def fault_zone(self, zone, simulate_wire_problem=False):
        """
        Faults a zone if we are emulating a zone expander.

        :param zone: zone to fault
        :type zone: int
        :param simulate_wire_problem: Whether or not to simulate a wire fault
        :type simulate_wire_problem: bool
        """

        # Allow ourselves to also be passed an address/channel combination
        # for zone expanders.
        #
        # Format (expander index, channel)
        if isinstance(zone, tuple):
            expander_idx, channel = zone

            zone = self._zonetracker.expander_to_zone(expander_idx, channel)

        status = 2 if simulate_wire_problem else 1

        self.send("L{0:02}{1}\r".format(zone, status))

    def clear_zone(self, zone):
        """
        Clears a zone if we are emulating a zone expander.

        :param zone: zone to clear
        """
        self.send("L{0:02}0\r".format(zone))

    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
        self._zonetracker.on_fault += self._on_zone_fault
        self._zonetracker.on_restore += self._on_zone_restore

    def _handle_message(self, data):
        """
        Parses keypad messages from the panel.

        :param data: keypad data to parse

        :returns: :py:class:`~alarmdecoder.messages.Message`
        """

        if data is not None:
            data = data.lstrip('\0')

        if data is None or data == '':
            raise InvalidMessageError()

        msg = None
        header = data[0:4]

        if header[0] != '!' or header == '!KPM':
            msg = self._handle_keypad_message(data)

        elif header == '!EXP' or header == '!REL':
            msg = self._handle_expander_message(data)

        elif header == '!RFX':
            msg = self._handle_rfx(data)

        elif header == '!LRR':
            msg = self._handle_lrr(data)

        elif data.startswith('!Ready'):
            self.on_boot()

        elif data.startswith('!CONFIG'):
            self._handle_config(data)

        elif data.startswith('!Sending'):
            self._handle_sending(data)

        return msg

    def _handle_keypad_message(self, data):
        """
        Handle keypad messages.

        :param data: keypad message to parse

        """
        msg = Message(data)

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

        self.on_message(message=msg)

        return msg

    def _handle_expander_message(self, data):
        """
        Handle expander messages.

        :param data: expander message to parse

        :returns: :py:class:`~alarmdecoder.messages.ExpanderMessage`
        """
        msg = ExpanderMessage(data)

        self._update_internal_states(msg)
        self.on_expander_message(message=msg)

        return msg

    def _handle_rfx(self, data):
        """
        Handle RF messages.

        :param data: RF message to parse

        :returns: :py:class:`~alarmdecoder.messages.RFMessage`
        """
        msg = RFMessage(data)

        self.on_rfx_message(message=msg)

        return msg

    def _handle_lrr(self, data):
        """
        Handle Long Range Radio messages.

        :param data: LRR message to parse

        :returns: :py:class:`~alarmdecoder.messages.LRRMessage`
        """
        msg = LRRMessage(data)

        if msg.event_type == 'ALARM_PANIC':
            self._panic_status = True
            self.on_panic(status=True)

        elif msg.event_type == 'CANCEL':
            if self._panic_status is True:
                self._panic_status = False
                self.on_panic(status=False)

        self.on_lrr_message(message=msg)

        return msg

    def _handle_config(self, data):
        """
        Handles received configuration data.

        :param data: Configuration string to parse
        """
        _, config_string = data.split('>')
        for setting in config_string.split('&'):
            key, val = setting.split('=')

            if key == 'ADDRESS':
                self.address = int(val)
            elif key == 'CONFIGBITS':
                self.configbits = int(val, 16)
            elif key == 'MASK':
                self.address_mask = int(val, 16)
            elif key == 'EXP':
                self.emulate_zone = [val[z] == 'Y' for z in range(5)]
            elif key == 'REL':
                self.emulate_relay = [val[r] == 'Y' for r in range(4)]
            elif key == 'LRR':
                self.emulate_lrr = (val == 'Y')
            elif key == 'DEDUPLICATE':
                self.deduplicate = (val == 'Y')

        self.on_config_received()

    def _handle_sending(self, data):
        """
        Handles results of a keypress send.

        :param data: Sending string to parse
        """

        matches = re.match('^!Sending(\.{1,5})done.*', data)
        if matches is not None:
            good_send = False
            if len(matches.group(1)) < 5:
                good_send = True

            self.on_sending_received(status=good_send, message=data)

    def _update_internal_states(self, message):
        """
        Updates internal device states.

        :param message: :py:class:`~alarmdecoder.messages.Message` to update internal states with
        :type message: :py:class:`~alarmdecoder.messages.Message`, :py:class:`~alarmdecoder.messages.ExpanderMessage`, :py:class:`~alarmdecoder.messages.LRRMessage`, or :py:class:`~alarmdecoder.messages.RFMessage`
        """
        if isinstance(message, Message):
            self._update_power_status(message)
            self._update_alarm_status(message)
            self._update_zone_bypass_status(message)
            self._update_armed_status(message)
            self._update_battery_status(message)
            self._update_fire_status(message)

        elif isinstance(message, ExpanderMessage):
            self._update_expander_status(message)

        self._update_zone_tracker(message)

    def _update_power_status(self, message):
        """
        Uses the provided message to update the AC power state.

        :param message: message to use to update
        :type message: :py:class:`~alarmdecoder.messages.Message`

        :returns: bool indicating the new status
        """
        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(status=self._power_status)

        return self._power_status

    def _update_alarm_status(self, message):
        """
        Uses the provided message to update the alarm state.

        """

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

                self.on_alarm(status=self._alarm_status)

        return self._alarm_status

    def _update_zone_bypass_status(self, message):
        """
        Uses the provided message to update the zone bypass state.

        """

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

                self.on_bypass(status=self._bypass_status)

        return self._bypass_status

    def _update_armed_status(self, message):
        """
        Uses the provided message to update the armed state.

        """

        message_status = message.armed_away | message.armed_home
        if message_status != self._armed_status:
            self._armed_status, old_status = message_status, self._armed_status

                if self._armed_status:
                    self.on_arm()
                else:
                    self.on_disarm()

        return self._armed_status

    def _update_battery_status(self, message):
        """
        Uses the provided message to update the battery state.

        :returns: boolean indicating the new status
        """

        last_status, last_update = self._battery_status
        if message.battery_low == last_status:
            self._battery_status = (last_status, time.time())
        else:
            if message.battery_low is True or time.time() > last_update + self._battery_timeout:
                self._battery_status = (message.battery_low, time.time())
                self.on_low_battery(status=message.battery_low)

        return self._battery_status[0]

    def _update_fire_status(self, message):
        """
        Uses the provided message to update the fire alarm state.

        """

        last_status, last_update = self._fire_status
        if message.fire_alarm == last_status:
            self._fire_status = (last_status, time.time())
        else:
            if message.fire_alarm is True or time.time() > last_update + self._fire_timeout:
                self._fire_status = (message.fire_alarm, time.time())
                self.on_fire(status=message.fire_alarm)

        return self._fire_status[0]

    def _update_expander_status(self, message):
        """
        Uses the provided message to update the expander states.

        :type message: :py:class:`~alarmdecoder.messages.ExpanderMessage`

        """

        if message.type == ExpanderMessage.RELAY:
            self._relay_status[(message.address, message.channel)] = message.value

            self.on_relay_changed(message=message)

            return self._relay_status[(message.address, message.channel)]

    def _update_zone_tracker(self, message):
        """
        Trigger an update of the :py:class:`~alarmdecoder.messages.Zonetracker`.

        :param message: message to update the zonetracker with
        """

        # Retrieve a list of faults.
        # NOTE: This only happens on first boot or after exiting programming mode.
            if not message.ready and "Hit * for faults" in message.text:
                self.send('*')
                return

        self._zonetracker.update(message)

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

        self.on_open()

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

    def _on_read(self, sender, *args, **kwargs):
        """
        Internal handler for reading from the device.
        """
        data = kwargs.get('data', None)
        self.on_read(data=data)

        self._handle_message(data)

    def _on_write(self, sender, *args, **kwargs):
        """
        Internal handler for writing to the device.
        """
        self.on_write(data=kwargs.get('data', None))

    def _on_zone_fault(self, sender, *args, **kwargs):
        """
        Internal handler for zone faults.
        """
        self.on_zone_fault(*args, **kwargs)

    def _on_zone_restore(self, sender, *args, **kwargs):
        """
        Internal handler for zone restoration.
        """
        self.on_zone_restore(*args, **kwargs)

# 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 __iter__(self):
        return iter(self._getfunctionlist())

    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)

    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

"""
Provides utility classes for the `AlarmDecoder`_ (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 `AlarmDecoder`_ devices.
    """

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

    # FIXME: Rewrite this monstrosity.
    @staticmethod
    def upload(dev, filename, progress_callback=None):
        """
        Uploads firmware to an `AlarmDecoder`_ device.

        :param filename: firmware filename
        :type filename: string
        :param progress_callback: callback function used to report progress
        :type progress_callback: function

        :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError`
        """

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

                    if line[0] == ':':
                        dev.write(line + "\r")
                        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():
                """Handles the read timeout event."""
                timeout_event.reading = False

            timeout_event.reading = True

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

            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:
                    pass

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

        def stage_callback(stage):
            """Callback to update progress for the specified 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)

"""
Provides zone tracking functionality for the `AlarmDecoder`_ (AD2) device family.

"""

import re

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'}

    zone = 0
    """Zone ID"""
    name = ''
    """Zone name"""
    status = CLEAR
    """Zone status"""
    timestamp = None
    """Timestamp of last update"""

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

        :param zone: zone number
        :param name: Human readable zone name
        :type name: string
        :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 zones and their statuses.
    """

    on_fault = event.Event("This event is called when the device detects a zone fault.\n\n**Callback definition:** *def callback(device, zone)*")
    on_restore = event.Event("This event is called when the device detects that a fault is restored.\n\n**Callback definition:** *def callback(device, zone)*")

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

    @property
    def zones(self):
        """
        Returns the current list of zones being tracked.

        :returns: dictionary of :py:class:`Zone` being tracked
        """
        return self._zones

    @zones.setter
    def zones(self, value):
        """
        Sets the current list of zones being tracked.

        :param value: new list of zones being tracked
        :type value: dictionary of :py:class:`Zone` being tracked
        """
        self._zones = value

    @property
    def faulted(self):
        """
        Retrieves the current list of faulted zones.

        :returns: list of faulted zones
        """
        return self._zones_faulted

    @faulted.setter
    def faulted(self, value):
        """
        Sets the current list of faulted zones.

        :param value: new list of faulted zones
        :type value: list of integers
        """
        self._zones_faulted = value

    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: :py:class:`~alarmdecoder.messages.Message` or :py:class:`~alarmdecoder.messages.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 zone in self._zones_faulted:
                    self._update_zone(zone, 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+).*$')

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

                    zone = match.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 expander_to_zone(self, address, channel):
        """
        Convert an address and channel into a zone number.

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

        :returns: 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

    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 name: human readable zone name
        :param status: 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

"""
Message representations received from the panel through the `AlarmDecoder`_ (AD2)
devices.

* :py:class:`Message`: The standard and most common message received from a panel.
* :py:class:`ExpanderMessage`: Messages received from Relay or Zone expander modules.
* :py:class:`RFMessage`: Message received from an RF receiver module.
* :py:class:`LRRMessage`: Message received from a long-range radio module.

"""

import re
import datetime

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

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

    """The timestamp of the message"""

        """
        """
        self.timestamp = datetime.datetime.now()

        """
        """
        return self.raw

    def dict(self, **kwargs):
        """
        Dictionary representation.
        """
        return dict(
            time=self.timestamp,
            mesg=self.raw,
            **kwargs
        )

        """
        String representation.
        """
        return repr(self.dict())

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('^(!KPM:){0,1}(\[[a-fA-F0-9\-]+\]),([a-fA-F0-9]+),(\[[a-fA-F0-9]+\]),(".+")$')

            self._parse_message(data)

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

        :param data: message data

        :raises: :py:class:`~alarmdecoder.util.InvalidMessageError`
        """
        match = self._regex.match(data)

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

        header, self.bitfield, self.numeric_code, self.panel_data, alpha = match.group(1, 2, 3, 4, 5)
        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:
            # Current cursor location on the alpha display.
            self.cursor_location = int(self.bitfield[21:23], 16)

        """
        """
            time                  = self.timestamp,
            bitfield              = self.bitfield,
            numeric_code          = self.numeric_code,
            panel_data            = self.panel_data,
            mask                  = self.mask,
            ready                 = self.ready,
            armed_away            = self.armed_away,
            armed_home            = self.armed_home,
            backlight_on          = self.backlight_on,
            programming_mode      = self.programming_mode,
            beeps                 = self.beeps,
            zone_bypassed         = self.zone_bypassed,
            ac_power              = self.ac_power,
            chime_on              = self.chime_on,
            alarm_event_occurred  = self.alarm_event_occurred,
            alarm_sounding        = self.alarm_sounding,
            battery_low           = self.battery_low,
            entry_delay_off       = self.entry_delay_off,
            fire_alarm            = self.fire_alarm,
            check_zone            = self.check_zone,
            perimeter_only        = self.perimeter_only,
            text                  = self.text,
            cursor_location       = self.cursor_location,
            **kwargs
        )

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

        """

        """

        """
        Parse the raw message from the device.

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

        """
        """
            address               = self.address,
            channel               = self.channel,
            value                 = self.value,
            **kwargs
        )

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
    """Low battery indication"""
    supervision = False
    """Supervision required indication"""
    loop = [False for _ in range(4)]
    """Loop indicators"""

        """

        """

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

        """
        """
            serial_number         = self.serial_number,
            battery               = self.battery,
            supervision           = self.supervision,
            **kwargs
        )

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(',')

        """
        """
            event_data            = self.event_data,
            event_type            = self.event_type,
            partition             = self.partition,
            **kwargs
        )

"""
This module contains different types of devices belonging to the `AlarmDecoder`_ (AD2) family.

* :py:class:`USBDevice`: Interfaces with the `AD2USB`_ device.
* :py:class:`SerialDevice`: Interfaces with the `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_.
* :py:class:`SocketDevice`: Interfaces with devices exposed through `ser2sock`_ or another IP to Serial solution.
  Also supports SSL if using `ser2sock`_.

.. _ser2sock: http://github.com/nutechsoftware/ser2sock
.. _AD2USB: http://www.alarmdecoder.com
.. _AD2SERIAL: http://www.alarmdecoder.com
.. _AD2PI: http://www.alarmdecoder.com

"""

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

from OpenSSL import SSL, crypto
from pyftdi.pyftdi.ftdi import Ftdi, FtdiError
from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError

class Device(object):
    """
    Base class for all `AlarmDecoder`_ (AD2) device types.
    """

    # Generic device events
    on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*")
    on_read = event.Event("This event is called when a line has been read from the device.\n\n**Callback definition:** def callback(device, data)*")
    on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*")

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

        """
        """

        """
        """

    @property
        """
        Retrieve the device ID.

        """
        return self._id

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

        :param value: device identification string
        :type value: string
        """
        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()

        """
        """
        try:
            self._running = False
            self._read_thread.stop()

        except Exception:
            pass

    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: device used by the reader thread
            :type device: :py:class:`~alarmdecoder.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:
                    pass

                except InvalidMessageError:
                    pass

                except SSL.WantReadError:
                    pass

                except CommError, err:
                    self._device.close()

                except Exception, err:
                    self._running = False
                    raise

class USBDevice(Device):
    """
    `AD2USB`_ device utilizing 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 = []
    __detect_thread = None

    @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: :py:class:`~alarmdecoder.util.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 :py:class:`USBDevice` device, or the
        first device.

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

        :returns: :py:class:`USBDevice` object utilizing the specified device
        :raises: :py:class:`~alarmdecoder.util.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  **Callback definition:** *def callback(thread, device)*
        :type on_attached: function
        :param on_detached: function to be called when a device is detached  **Callback definition:** *def callback(thread, device)*

        :type on_detached: function
        """
        cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached)

        try:
            cls.find_all()
        except CommError:
            pass

        cls.__detect_thread.start()

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

            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: string 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: 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: serial number of the device
        """
        self._serial_number = value

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

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

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

        :param value: 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: string or int
        """
        Device.__init__(self)

        self._device = Ftdi()

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

        self.interface = interface

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

        :param baudrate: baudrate to use
        :param no_reader_thread: whether or not to automatically start the
                                 reader thread.

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

            pass

    def fileno(self):
        raise NotImplementedError('USB devices do not support fileno()')

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

        :param data: data to write

        """
        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: 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: line that was read
        :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError`
        """

        def timeout_event():
            """Handles read timeout event"""
            timeout_event.reading = False
        timeout_event.reading = True

        if purge_buffer:
            self._buffer = ''

        got_line, ret = False, None

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

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

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

                    if buf == "\n":
                        self._buffer = self._buffer.rstrip("\r\n")

                        if len(self._buffer) > 0:
                            got_line = True
                            break
                else:
                    time.sleep(0.01)

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

                self.on_read(data=ret)

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

        finally:
            timer.cancel()

        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("This event is called when an `AD2USB`_ device has been detected.\n\n**Callback definition:** def callback(thread, device*")
        on_detached = event.Event("This event is called when an `AD2USB`_ device has been removed.\n\n**Callback definition:** def callback(thread, device*")

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

            :param on_attached: Function to call when a device is attached  **Callback definition:** *def callback(thread, device)*
            :type on_attached: function
            :param on_detached: Function to call when a device is detached  **Callback definition:** *def callback(thread, device)*
            :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())

                    for dev in current_devices.difference(last_devices):
                        self.on_attached(device=dev)

                    for dev in last_devices.difference(current_devices):
                        self.on_detached(device=dev)

                    last_devices = current_devices

                except CommError:
                    pass

                time.sleep(0.25)

class SerialDevice(Device):
    """
    `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_ device utilizing 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: string

        """
        devices = []

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

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

        return devices

    @property
        """

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

        """

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

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

        :param interface: device to open
        :type interface: string
        """

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

        """

        :param baudrate: baudrate to use with the device

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

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

        # Open the device and start up the reader thread.
        try:
            self._device.port = self._port
            self._device.open()
            # 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.
            self._device.baudrate = baudrate

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

        else:

        """
        """
        try:

            pass

        return self._device.fileno()

        """

        :raises: py:class:`~alarmdecoder.util.CommError`
        """
        try:
            self._device.write(data)

        except serial.SerialTimeoutException:
            pass

            raise CommError('Error writing to device.', err)

        else:

        """

        """
        ret = None

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

        return ret

        """

                             reading.

        """

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

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

                            break
                else:

        except (OSError, serial.SerialException), err:

        else:

            else:

        finally:

        return ret

class SocketDevice(Device):
    """
    Device that supports communication with an `AlarmDecoder`_ (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, self._port = value

    @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: path to the certificate path or :py:class:`OpenSSL.crypto.X509`
        """
        return self._ssl_certificate

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

        :param value: path to the SSL certificate or :py:class:`OpenSSL.crypto.X509`
        :type value: string or :py:class:`OpenSSL.crypto.X509`
        """
        self._ssl_certificate = value

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

        :returns: jpath to the SSL key or :py:class:`OpenSSL.crypto.PKey`
        """
        return self._ssl_key

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

        :param value: path to the SSL key or :py:class:`OpenSSL.crypto.PKey`
        :type value: string or :py:class:`OpenSSL.crypto.PKey`
        """
        self._ssl_key = value

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

        :returns: path to the CA certificate or :py:class:`OpenSSL.crypto.X509`
        """
        return self._ssl_ca

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

        :param value: path to the SSL CA certificate or :py:class:`OpenSSL.crypto.X509`
        """
        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

        """

        :param no_reader_thread: whether or not to automatically open the reader
                                 thread.

        :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.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.setblocking(1)

                while True:
                    try:
                        self._device.do_handshake()
                        break
                    except SSL.WantReadError:
                        pass

            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:
                # Make sure that it closes immediately.
                self._device.shutdown(socket.SHUT_RDWR)

            pass

        Device.close(self)

        """

        :returns: 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

        """

                             reading.

        """

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

                            break
                else:

        except SSL.SysCallError, err:
            errno, msg = err
            raise CommError('SSL error while reading from device: {0} ({1})'.format(msg, errno))

            raise

        else:

            else:

        finally:

        return ret

    def _init_ssl(self):
        """
        Initializes our device as an SSL connection.

        """

        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):
        """
        SSL verification callback.
        """
        return ok

on_config_received (alarmdecoder.decoder.AlarmDecoder attribute)

on_expander_message (alarmdecoder.decoder.AlarmDecoder attribute)

on_sending_received (alarmdecoder.decoder.AlarmDecoder attribute)

on_attached (alarmdecoder.devices.USBDevice.DetectThread attribute)

on_detached (alarmdecoder.devices.USBDevice.DetectThread attribute)

expander_to_zone() (alarmdecoder.zonetracking.Zonetracker method)