From 2fe77a2571d745146e79dd445c6e798ca141874c Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Mon, 17 Jun 2013 16:57:24 -0700 Subject: [PATCH] Moved to its own class. ADded support for timeouts and check zones. --- pyad2usb/ad2usb.py | 123 +++++---------------------- pyad2usb/zonetracking.py | 176 +++++++++++++++++++++++++++++++++++++++ test.py | 4 +- 3 files changed, 201 insertions(+), 102 deletions(-) create mode 100644 pyad2usb/zonetracking.py diff --git a/pyad2usb/ad2usb.py b/pyad2usb/ad2usb.py index f934653..69bff97 100644 --- a/pyad2usb/ad2usb.py +++ b/pyad2usb/ad2usb.py @@ -10,6 +10,7 @@ from collections import OrderedDict from .event import event from . import devices from . import util +from . import zonetracking class Overseer(object): """ @@ -159,8 +160,8 @@ class AD2USB(object): 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_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.') + 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.') # Mid-level Events on_message = event.Event('Called when a message has been received from the device.') @@ -177,20 +178,18 @@ class AD2USB(object): F3 = unichr(3) + unichr(3) + unichr(3) F4 = unichr(4) + unichr(4) + unichr(4) - ZONE_EXPIRE = 30 - def __init__(self, device): """ Constructor """ 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 = None - self._zones_faulted = [] - self._last_zone_fault = 0 self.address = 18 self.configbits = 0xFF00 @@ -285,6 +284,8 @@ class AD2USB(object): 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): """ @@ -381,106 +382,16 @@ class AD2USB(object): if old_status is not None: self.on_fire(self._fire_status) - self._update_zone_status(message) + self._update_zone_tracker(message) - def _update_zone_status(self, message): - """ - Update zone statuses based on the current message. - """ + def _update_zone_tracker(self, message): # Retrieve a list of faults. # NOTE: This only happens on first boot or after exiting programming mode. - if "Hit * for faults" in message.text: + if not message.ready and "Hit * for faults" in message.text: self._device.write('*') return - # Panel is ready, restore all zones. - if message.ready: - for idx, z in enumerate(self._zones_faulted): - self.on_restore(z) - - del self._zones_faulted[:] - self._last_zone_fault = 0 - - # Process fault - elif "FAULT" in message.text: - zone = -1 - - # 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._clear_expired_zones(zone) - else: - self._zones_faulted.append(zone) - self._zones_faulted.sort() - self.on_fault(zone) - - # Save our spot for the next message. - self._last_zone_fault = zone - - def _clear_expired_zones(self, zone): - """ - Clear all expired zones from our status list. - """ - cleared_zones = [] - found_last, found_new, at_end = False, False, 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: - z = it.next() - - if z == zone: - found_new = True - break - else: - cleared_zones += [z] - - except StopIteration: - 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._zones_faulted.remove(z) - self.on_restore(z) + self._zonetracker.update(message) def _on_open(self, sender, args): """ @@ -510,6 +421,18 @@ class AD2USB(object): """ 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) + class Message(object): """ Represents a message from the alarm panel. diff --git a/pyad2usb/zonetracking.py b/pyad2usb/zonetracking.py new file mode 100644 index 0000000..47d3755 --- /dev/null +++ b/pyad2usb/zonetracking.py @@ -0,0 +1,176 @@ +""" +Provides zone tracking functionality for the AD2USB device family. +""" + +import time +from .event import event + +class Zone(object): + """ + Representation of a panel zone. + """ + + CLEAR = 0 + FAULT = 1 + WIRE_FAULT = 2 + + def __init__(self, zone=0, name='', status=CLEAR): + self.zone = zone + self.name = name + self.status = status + self.timestamp = time.time() + + def __str__(self): + return '[{0}] {1} - ts {2}'.format(self.zone, 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 + + def __init__(self): + """ + Constructor + """ + self._zones = {} + self._zones_faulted = [] + self._last_zone_fault = 0 + + def update(self, message): + """ + Update zone statuses based on the current message. + """ + # 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: + zone = -1 + + # 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, Zone.FAULT) + self._clear_zones(zone) + else: + self._add_zone(zone, status=Zone.FAULT) + + # 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. + """ + 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: + z = it.next() + + if z == zone: + found_new = True + break + else: + cleared_zones += [z] + + except StopIteration: + 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): + cleared_zones = [] + + for z in self._zones_faulted: + cleared_zones += [z] + + for z in cleared_zones: + if 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. + """ + if not zone in self._zones: + self._zones[zone] = Zone(zone=zone, name=name, status=status) + + if status != Zone.CLEAR: + self._zones_faulted.append(zone) + self._zones_faulted.sort() + self.on_fault(zone) + + def _update_zone(self, zone, status): + """ + Updates a zones status. + """ + if not zone in self._zones: + raise IndexError('Zone does not exist and cannot be updated: %d', zone) + + self._zones[zone].status = status + self._zones[zone].timestamp = time.time() + + if status == Zone.CLEAR: + self._zones_faulted.remove(zone) + self.on_restore(zone) + + def _zone_expired(self, zone): + if time.time() > self._zones[zone].timestamp + Zonetracker.EXPIRE: + return True + + return False diff --git a/test.py b/test.py index 48c50ad..21a9059 100755 --- a/test.py +++ b/test.py @@ -234,8 +234,8 @@ def test_socket(): a2u.on_config_received += handle_config a2u.on_arm += handle_arm a2u.on_disarm += handle_disarm - a2u.on_fault += handle_fault - a2u.on_restore += handle_restore + a2u.on_zone_fault += handle_fault + a2u.on_zone_restore += handle_restore a2u.open() #a2u.save_config()