@@ -12,6 +12,7 @@ from .util import CommError, NoDeviceError | |||
from .messages import Message, ExpanderMessage, RFMessage, LRRMessage | |||
from .zonetracking import Zonetracker | |||
class AlarmDecoder(object): | |||
""" | |||
High-level wrapper around Alarm Decoder (AD2) devices. | |||
@@ -237,8 +238,8 @@ class AlarmDecoder(object): | |||
raise InvalidMessageError() | |||
msg = None | |||
header = data[0:4] | |||
if header[0] != '!' or header == '!KPE': | |||
msg = Message(data) | |||
@@ -295,7 +296,7 @@ class AlarmDecoder(object): | |||
self.on_panic(status=True) | |||
elif msg.event_type == 'CANCEL': | |||
if self._panic_status == True: | |||
if self._panic_status is True: | |||
self._panic_status = False | |||
self.on_panic(status=False) | |||
@@ -321,11 +322,9 @@ class AlarmDecoder(object): | |||
elif k == 'MASK': | |||
self.address_mask = int(v, 16) | |||
elif k == 'EXP': | |||
for z in range(5): | |||
self.emulate_zone[z] = (v[z] == 'Y') | |||
self.emulate_zone = [v[z] == 'Y' for z in range(5)] | |||
elif k == 'REL': | |||
for r in range(4): | |||
self.emulate_relay[r] = (v[r] == 'Y') | |||
self.emulate_relay = [v[r] == 'Y' for r in range(4)] | |||
elif k == 'LRR': | |||
self.emulate_lrr = (v == 'Y') | |||
elif k == 'DEDUPLICATE': | |||
@@ -371,14 +370,14 @@ class AlarmDecoder(object): | |||
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] + AlarmDecoder.BATTERY_TIMEOUT: | |||
if message.battery_low is True or time.time() > self._battery_status[1] + AlarmDecoder.BATTERY_TIMEOUT: | |||
self._battery_status = (message.battery_low, time.time()) | |||
self.on_low_battery(status=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] + AlarmDecoder.FIRE_TIMEOUT: | |||
if message.fire_alarm is True or time.time() > self._fire_status[1] + AlarmDecoder.FIRE_TIMEOUT: | |||
self._fire_status = (message.fire_alarm, time.time()) | |||
self.on_fire(status=self._fire_status) | |||
@@ -4,10 +4,12 @@ Contains different types of devices belonging to the Alarm Decoder (AD2) family. | |||
.. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
""" | |||
import usb.core, usb.util | |||
import usb.core | |||
import usb.util | |||
import time | |||
import threading | |||
import serial, serial.tools.list_ports | |||
import serial | |||
import serial.tools.list_ports | |||
import socket | |||
from OpenSSL import SSL, crypto | |||
@@ -16,6 +18,7 @@ from pyftdi.pyftdi.usbtools import * | |||
from .util import CommError, TimeoutError, NoDeviceError | |||
from .event import event | |||
class Device(object): | |||
""" | |||
Generic parent device to all Alarm Decoder (AD2) products. | |||
@@ -143,6 +146,7 @@ class Device(object): | |||
time.sleep(0.01) | |||
class USBDevice(Device): | |||
""" | |||
AD2USB device exposed with PyFTDI's interface. | |||
@@ -334,11 +338,11 @@ class USBDevice(Device): | |||
# 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._product_id, | |||
self._endpoint, | |||
self._device_number, | |||
self._serial_number, | |||
self._description) | |||
self._device.set_baudrate(baudrate) | |||
@@ -763,6 +767,7 @@ class SerialDevice(Device): | |||
return ret | |||
class SocketDevice(Device): | |||
""" | |||
Device that supports communication with an Alarm Decoder (AD2) that is | |||
@@ -9,6 +9,7 @@ | |||
# * Added type check in fire() | |||
# * Removed earg from fire() and added support for args/kwargs. | |||
class Event(object): | |||
def __init__(self, doc=None): | |||
@@ -9,6 +9,7 @@ import re | |||
from .util import InvalidMessageError | |||
class BaseMessage(object): | |||
""" | |||
Base class for messages. | |||
@@ -29,6 +30,7 @@ class BaseMessage(object): | |||
""" | |||
return self.raw | |||
class Message(BaseMessage): | |||
""" | |||
Represents a message from the alarm panel. | |||
@@ -141,6 +143,7 @@ class Message(BaseMessage): | |||
if int(self.panel_data[19:21], 16) & 0x01 > 0: | |||
self.cursor_location = int(self.bitfield[21:23], 16) # Alpha character index that the cursor is on. | |||
class ExpanderMessage(BaseMessage): | |||
""" | |||
Represents a message from a zone or relay expansion module. | |||
@@ -151,7 +154,6 @@ class ExpanderMessage(BaseMessage): | |||
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 | |||
@@ -205,6 +207,7 @@ class ExpanderMessage(BaseMessage): | |||
else: | |||
raise InvalidMessageError('Unknown expander message header: {0}'.format(data)) | |||
class RFMessage(BaseMessage): | |||
""" | |||
Represents a message from an RF receiver. | |||
@@ -267,6 +270,7 @@ class RFMessage(BaseMessage): | |||
except ValueError: | |||
raise InvalidMessageError('Received invalid message: {0}'.format(data)) | |||
class LRRMessage(BaseMessage): | |||
""" | |||
Represent a message from a Long Range Radio. | |||
@@ -9,6 +9,7 @@ from ..messages import Message, RFMessage, LRRMessage, ExpanderMessage | |||
from ..event.event import Event, EventHandler | |||
from ..zonetracking import Zonetracker | |||
class TestAlarmDecoder(TestCase): | |||
def setUp(self): | |||
self._panicked = False | |||
@@ -164,6 +164,7 @@ class TestUSBDevice(TestCase): | |||
with self.assertRaises(CommError): | |||
self._device.read_line() | |||
class TestSerialDevice(TestCase): | |||
def setUp(self): | |||
self._device = SerialDevice() | |||
@@ -250,6 +251,7 @@ class TestSerialDevice(TestCase): | |||
with self.assertRaises(CommError): | |||
self._device.read_line() | |||
class TestSocketDevice(TestCase): | |||
def setUp(self): | |||
self._device = SocketDevice() | |||
@@ -3,6 +3,7 @@ from unittest import TestCase | |||
from ..messages import Message, ExpanderMessage, RFMessage, LRRMessage | |||
from ..util import InvalidMessageError | |||
class TestMessages(TestCase): | |||
def setUp(self): | |||
pass | |||
@@ -4,6 +4,7 @@ from mock import Mock, MagicMock | |||
from ..messages import Message, ExpanderMessage | |||
from ..zonetracking import Zonetracker, Zone | |||
class TestZonetracking(TestCase): | |||
def setUp(self): | |||
self._zonetracker = Zonetracker() | |||
@@ -135,7 +136,7 @@ class TestZonetracking(TestCase): | |||
self._zonetracker.update(msg) | |||
self.assertIn(4, self._zonetracker._zones_faulted) | |||
self._zonetracker._zones[4].timestamp -= 35 # forcefully expire the zone | |||
self._zonetracker._zones[4].timestamp -= 35 # forcefully expire the zone | |||
# generic message to force an update. | |||
msg = Message('[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
@@ -7,30 +7,35 @@ Provides utility classes for the Alarm Decoder (AD2) devices. | |||
import time | |||
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 Alarm Decoder devices. | |||
@@ -10,6 +10,7 @@ import time | |||
from .event import event | |||
from .messages import ExpanderMessage | |||
class Zone(object): | |||
""" | |||
Representation of a panel zone. | |||
@@ -22,7 +23,7 @@ class Zone(object): | |||
CHECK = 2 # Wire fault | |||
"""Status indicating that there is a wiring issue with the zone.""" | |||
STATUS = { CLEAR: 'CLEAR', FAULT: 'FAULT', CHECK: 'CHECK' } | |||
STATUS = {CLEAR: 'CLEAR', FAULT: 'FAULT', CHECK: 'CHECK'} | |||
def __init__(self, zone=0, name='', status=CLEAR): | |||
""" | |||
@@ -52,6 +53,7 @@ class Zone(object): | |||
""" | |||
return 'Zone({0}, {1}, ts {2})'.format(self.zone, Zone.STATUS[self.status], self.timestamp) | |||
class Zonetracker(object): | |||
""" | |||
Handles tracking of zone and their statuses. | |||