| 
							- """
 - 
 - Provides the full AD2USB class and factory.
 - 
 - .. moduleauthor:: Scott Petersen <scott@nutech.com>
 - 
 - """
 - 
 - import time
 - import threading
 - import re
 - import logging
 - from collections import OrderedDict
 - from .event import event
 - from . import devices
 - from . import util
 - from . import messages
 - from . import zonetracking
 - 
 - 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.
 - 
 -         :returns: list of devices found
 -         :raises: util.CommError
 -         """
 -         cls.__devices = devices.USBDevice.find_all()
 - 
 -         return cls.__devices
 - 
 -     @classmethod
 -     def devices(cls):
 -         """
 -         Returns a cached list of AD2USB devices located on the system.
 - 
 -         :returns: cached list of devices found.
 -         """
 -         return cls.__devices
 - 
 -     @classmethod
 -     def create(cls, device=None):
 -         """
 -         Factory method that returns the requested AD2USB device, or the first device.
 - 
 -         :param device: Tuple describing the USB device to open, as returned by find_all().
 -         :type device: tuple
 - 
 -         :returns: AD2USB object utilizing the specified device.
 -         :raises: util.NoDeviceError
 -         """
 -         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):
 -         """
 -         Constructor
 - 
 -         :param attached_event: Event to trigger when a device is attached.
 -         :type attached_event: function
 -         :param detached_event: Event to trigger when a device is detached.
 -         :type detached_event: function
 -         """
 -         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()
 - 
 -     def close(self):
 -         """
 -         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):
 -         """
 -         Factory method that returns the requested AD2USB device, or the first device.
 - 
 -         :param device: Tuple describing the USB device to open, as returned by find_all().
 -         :type device: tuple
 -         """
 -         return Overseer.create(device)
 - 
 -     class DetectThread(threading.Thread):
 -         """
 -         Thread that handles detection of added/removed devices.
 -         """
 -         def __init__(self, overseer):
 -             """
 -             Constructor
 - 
 -             :param overseer: Overseer object to use with the thread.
 -             :type overseer: Overseer
 -             """
 -             threading.Thread.__init__(self)
 - 
 -             self._overseer = overseer
 -             self._running = False
 - 
 -         def stop(self):
 -             """
 -             Stops the thread.
 -             """
 -             self._running = False
 - 
 -         def run(self):
 -             """
 -             The actual detection process.
 -             """
 -             self._running = True
 - 
 -             last_devices = set()
 - 
 -             while self._running:
 -                 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_arm = event.Event('Called when the panel is armed.')
 -     on_disarm = event.Event('Called when the panel is disarmed.')
 -     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_fire = event.Event('Called when a fire is detected.')
 -     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.')
 -     on_zone_fault = event.Event('Called when the device detects a zone fault.')
 -     on_zone_restore = event.Event('Called when the device detects that a fault is restored.')
 -     on_low_battery = event.Event('Called when the device detects a low battery.')
 -     on_panic = event.Event('Called when the device detects a panic.')
 -     on_relay_changed = event.Event('Called when a relay is opened or closed on an expander board.')
 - 
 -     # Mid-level Events
 -     on_message = event.Event('Called when a message has been received from the device.')
 -     on_lrr_message = event.Event('Called when an LRR message is received.')
 -     on_rfx_message = event.Event('Called when an RFX message is received.')
 - 
 -     # 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.')
 - 
 -     # Constants
 -     F1 = unichr(1) + unichr(1) + unichr(1)
 -     """Represents panel function key #1"""
 -     F2 = unichr(2) + unichr(2) + unichr(2)
 -     """Represents panel function key #2"""
 -     F3 = unichr(3) + unichr(3) + unichr(3)
 -     """Represents panel function key #3"""
 -     F4 = unichr(4) + unichr(4) + unichr(4)
 -     """Represents panel function key #4"""
 - 
 -     BATTERY_TIMEOUT = 30
 -     """Timeout before the battery status reverts."""
 -     FIRE_TIMEOUT = 30
 -     """Timeout before the fire status reverts."""
 - 
 -     def __init__(self, device):
 -         """
 -         Constructor
 - 
 -         :param device: The low-level device used for this AD2USB interface.
 -         :type device: devices.Device
 -         """
 -         self._device = device
 -         self._zonetracker = zonetracking.Zonetracker()
 - 
 -         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
 - 
 -     @property
 -     def id(self):
 -         """
 -         The ID of the AD2USB device.
 - 
 -         :returns: The identification string for the device.
 -         """
 -         return self._device.id
 - 
 -     def open(self, baudrate=None, no_reader_thread=False):
 -         """
 -         Opens the device.
 - 
 -         :param baudrate: The baudrate used for the device.
 -         :type baudrate: int
 -         :param interface: The interface used for the device.
 -         :type interface: varies depends on device type.. FIXME
 -         :param index: Interface index.. can probably remove.  FIXME
 -         :type index: int
 -         :param no_reader_thread: Specifies whether or not the automatic reader thread should be started or not
 -         :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.
 -         """
 -         self._device.close()
 -         del self._device
 -         self._device = None
 - 
 -     def send(self, data):
 -         if self._device:
 -             self._device.write(data)
 - 
 -     def get_config(self):
 -         """
 -         Retrieves the configuration from the device.
 -         """
 -         self._device.write("C\r")
 - 
 -     def save_config(self):
 -         """
 -         Sets configuration entries on the device.
 -         """
 -         config_string = ''
 - 
 -         # HACK: Both of these methods are ugly.. but I can't think of an elegant way of doing it.
 - 
 -         #config_string += 'ADDRESS={0}&'.format(self.address)
 -         #config_string += 'CONFIGBITS={0:x}&'.format(self.configbits)
 -         #config_string += 'MASK={0:x}&'.format(self.address_mask)
 -         #config_string += 'EXP={0}&'.format(''.join(['Y' if z else 'N' for z in self.emulate_zone]))
 -         #config_string += 'REL={0}&'.format(''.join(['Y' if r else 'N' for r in self.emulate_relay]))
 -         #config_string += 'LRR={0}&'.format('Y' if self.emulate_lrr else 'N')
 -         #config_string += 'DEDUPLICATE={0}'.format('Y' if self.deduplicate else 'N')
 - 
 -         config_entries = []
 -         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._device.write("C{0}\r".format(config_string))
 - 
 -     def reboot(self):
 -         """
 -         Reboots the device.
 -         """
 -         self._device.write('=')
 - 
 -     def fault_zone(self, zone, simulate_wire_problem=False):
 -         """
 -         Faults a zone if we are emulating a zone expander.
 - 
 -         :param zone: The 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):
 -             zone = self._zonetracker._expander_to_zone(*zone)
 - 
 -         status = 2 if simulate_wire_problem else 1
 - 
 -         self._device.write("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: The zone to clear.
 -         :type zone: int
 -         """
 -         self._device.write("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 messages from the panel.
 - 
 -         :param data: Panel data to parse.
 -         :type data: str
 - 
 -         :returns: An object representing the message.
 -         """
 -         if data is None:
 -             return None
 - 
 -         msg = None
 - 
 -         if data[0] != '!':
 -             msg = messages.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 = messages.ExpanderMessage(data)
 - 
 -                 self._update_internal_states(msg)
 - 
 -             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)
 - 
 -         return msg
 - 
 -     def _handle_rfx(self, data):
 -         msg = messages.RFMessage(data)
 - 
 -         self.on_rfx_message(msg)
 - 
 -         return msg
 - 
 -     def _handle_lrr(self, data):
 -         """
 -         Handle Long Range Radio messages.
 - 
 -         :param data: LRR message to parse.
 -         :type data: str
 - 
 -         :returns: An object representing the LRR message.
 -         """
 -         msg = messages.LRRMessage(data)
 - 
 -         args = (msg._partition, msg._event_type, msg._event_data)
 -         if msg._event_type == 'ALARM_PANIC':
 -             self._panic_status = True
 -             self.on_panic(args + (True,))
 - 
 -         elif msg._event_type == 'CANCEL':
 -             if self._panic_status == True:
 -                 self._panic_status = False
 -                 self.on_panic(args + (False,))
 - 
 -         self.on_lrr_message(args)
 - 
 -         return msg
 - 
 -     def _handle_config(self, data):
 -         """
 -         Handles received configuration data.
 - 
 -         :param data: Configuration string to parse.
 -         :type data: str
 -         """
 -         _, config_string = data.split('>')
 -         for setting in config_string.split('&'):
 -             k, v = setting.split('=')
 - 
 -             if k == 'ADDRESS':
 -                 self.address = int(v)
 -             elif k == 'CONFIGBITS':
 -                 self.configbits = int(v, 16)
 -             elif k == 'MASK':
 -                 self.address_mask = int(v, 16)
 -             elif k == 'EXP':
 -                 for z in range(5):
 -                     self.emulate_zone[z] = True if v[z] == 'Y' else False
 -             elif k == 'REL':
 -                 for r in range(4):
 -                     self.emulate_relay[r] = True if v[r] == 'Y' else False
 -             elif k == 'LRR':
 -                 self.emulate_lrr = True if v == 'Y' else False
 -             elif k == 'DEDUPLICATE':
 -                 self.deduplicate = True if v == 'Y' else False
 - 
 -         self.on_config_received()
 - 
 -     def _update_internal_states(self, message):
 -         """
 -         Updates internal device states.
 - 
 -         :param message: Message to update internal states with.
 -         :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
 -         """
 -         if isinstance(message, messages.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
 - 
 -                 if old_status is not None:
 -                     self.on_alarm(self._alarm_status)
 - 
 -             if message.zone_bypassed != self._bypass_status:
 -                 self._bypass_status, old_status = message.zone_bypassed, self._bypass_status
 - 
 -                 if old_status is not None:
 -                     self.on_bypass(self._bypass_status)
 - 
 -             if (message.armed_away | message.armed_home) != self._armed_status:
 -                 self._armed_status, old_status = message.armed_away | message.armed_home, self._armed_status
 - 
 -                 if old_status is not None:
 -                     if self._armed_status:
 -                         self.on_arm()
 -                     else:
 -                         self.on_disarm()
 - 
 -             if message.battery_low == self._battery_status[0]:
 -                 self._battery_status = (self._battery_status[0], time.time())
 -             else:
 -                 if message.battery_low == True or time.time() > self._battery_status[1] + AD2USB.BATTERY_TIMEOUT:
 -                     self._battery_status = (message.battery_low, time.time())
 -                     self.on_low_battery(self._battery_status)
 - 
 -             if message.fire_alarm == self._fire_status[0]:
 -                 self._fire_status = (self._fire_status[0], time.time())
 -             else:
 -                 if message.fire_alarm == True or time.time() > self._fire_status[1] + AD2USB.FIRE_TIMEOUT:
 -                     self._fire_status = (message.fire_alarm, time.time())
 -                     self.on_fire(self._fire_status)
 - 
 -         elif isinstance(message, messages.ExpanderMessage):
 -             if message.type == messages.ExpanderMessage.RELAY:
 -                 self._relay_status[(message.address, message.channel)] = message.value
 - 
 -                 self.on_relay_changed(message)
 - 
 -         self._update_zone_tracker(message)
 - 
 -     def _update_zone_tracker(self, message):
 -         """
 -         Trigger an update of the zonetracker.
 - 
 -         :param message: The message to update the zonetracker with.
 -         :type message: Message, ExpanderMessage, LRRMessage, or RFMessage
 -         """
 - 
 -         # Retrieve a list of faults.
 -         # NOTE: This only happens on first boot or after exiting programming mode.
 -         if isinstance(message, messages.Message):
 -             if not message.ready and "Hit * for faults" in message.text:
 -                 self._device.write('*')
 -                 return
 - 
 -         self._zonetracker.update(message)
 - 
 -     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)
 - 
 -     def _on_zone_fault(self, sender, args):
 -         """
 -         Internal handler for zone faults.
 -         """
 -         self.on_zone_fault(args)
 - 
 -     def _on_zone_restore(self, sender, args):
 -         """
 -         Internal handler for zone restoration.
 -         """
 -         self.on_zone_restore(args)
 
 
  |