Browse Source

Moved to its own class. ADded support for timeouts and check zones.

pyserial_fix
Scott Petersen 11 years ago
parent
commit
2fe77a2571
3 changed files with 201 additions and 102 deletions
  1. +23
    -100
      pyad2usb/ad2usb.py
  2. +176
    -0
      pyad2usb/zonetracking.py
  3. +2
    -2
      test.py

+ 23
- 100
pyad2usb/ad2usb.py View File

@@ -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.


+ 176
- 0
pyad2usb/zonetracking.py View File

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

+ 2
- 2
test.py View File

@@ -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()


Loading…
Cancel
Save