Browse Source

Merge branch 'dev'

pyserial_fix
Scott Petersen 9 years ago
parent
commit
fea57478e8
14 changed files with 189 additions and 71 deletions
  1. +23
    -3
      alarmdecoder/decoder.py
  2. +38
    -4
      alarmdecoder/devices.py
  3. +1
    -2
      alarmdecoder/messages.py
  4. +36
    -16
      alarmdecoder/zonetracking.py
  5. +8
    -3
      bin/ad2-firmwareupload
  6. +6
    -3
      examples/alarm_email.py
  7. +6
    -3
      examples/lrr_example.py
  8. +6
    -3
      examples/rf_device.py
  9. +0
    -0
      examples/usb_detection.py
  10. +0
    -0
      examples/usb_device.py
  11. +6
    -3
      examples/virtual_zone_expander.py
  12. +0
    -15
      requirements.txt
  13. +0
    -6
      setup.py
  14. +59
    -10
      test/test_ad2.py

+ 23
- 3
alarmdecoder/decoder.py View File

@@ -94,7 +94,7 @@ class AlarmDecoder(object):
:type device: Device
"""
self._device = device
self._zonetracker = Zonetracker()
self._zonetracker = Zonetracker(self)

self._battery_timeout = AlarmDecoder.BATTERY_TIMEOUT
self._fire_timeout = AlarmDecoder.FIRE_TIMEOUT
@@ -106,6 +106,7 @@ class AlarmDecoder(object):
self._battery_status = (False, 0)
self._panic_status = None
self._relay_status = {}
self._internal_address_mask = 0xFFFFFFFF

self.address = 18
self.configbits = 0xFF00
@@ -177,6 +178,25 @@ class AlarmDecoder(object):
"""
self._fire_timeout = value

@property
def internal_address_mask(self):
"""
Retrieves the address mask used for updating internal status.

:returns: address mask
"""
return self._internal_address_mask

@internal_address_mask.setter
def internal_address_mask(self, value):
"""
Sets the address mask used internally for updating status.

:param value: address mask
:type value: int
"""
self._internal_address_mask = value

def open(self, baudrate=None, no_reader_thread=False):
"""
Opens the device.
@@ -344,10 +364,10 @@ class AlarmDecoder(object):
"""
msg = Message(data)

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

self.on_message(message=msg)
self.on_message(message=msg)

return msg



+ 38
- 4
alarmdecoder/devices.py View File

@@ -15,19 +15,35 @@ This module contains different types of devices belonging to the `AlarmDecoder`_
.. moduleauthor:: Scott Petersen <scott@nutech.com>
"""

import usb.core
import usb.util
import time
import threading
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
from .event import event

try:
from pyftdi.pyftdi.ftdi import Ftdi, FtdiError
import usb.core
import usb.util

have_pyftdi = True

except ImportError:
have_pyftdi = False

try:
from OpenSSL import SSL, crypto

have_openssl = True

except ImportError:
from collections import namedtuple
SSL = namedtuple('SSL', ['Error', 'WantReadError', 'SysCallError'])
have_openssl = False


class Device(object):
"""
@@ -198,6 +214,9 @@ class USBDevice(Device):
:returns: list of devices
:raises: :py:class:`~alarmdecoder.util.CommError`
"""
if not have_pyftdi:
raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')

cls.__devices = []

query = cls.PRODUCT_IDS
@@ -234,6 +253,9 @@ class USBDevice(Device):
:returns: :py:class:`USBDevice` object utilizing the specified device
:raises: :py:class:`~alarmdecoder.util.NoDeviceError`
"""
if not have_pyftdi:
raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')

cls.find_all()

if len(cls.__devices) == 0:
@@ -257,6 +279,9 @@ class USBDevice(Device):

:type on_detached: function
"""
if not have_pyftdi:
raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')

cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached)

try:
@@ -271,6 +296,9 @@ class USBDevice(Device):
"""
Stops the device detection thread.
"""
if not have_pyftdi:
raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')

try:
cls.__detect_thread.stop()

@@ -347,6 +375,9 @@ class USBDevice(Device):
index.
:type interface: string or int
"""
if not have_pyftdi:
raise ImportError('The USBDevice class has been disabled due to missing requirement: pyftdi or pyusb.')

Device.__init__(self)

self._device = Ftdi()
@@ -1120,6 +1151,9 @@ class SocketDevice(Device):
:raises: :py:class:`~alarmdecoder.util.CommError`
"""

if not have_openssl:
raise ImportError('SSL sockets have been disabled due to missing requirement: pyopenssl.')

try:
ctx = SSL.Context(SSL.TLSv1_METHOD)



+ 1
- 2
alarmdecoder/messages.py View File

@@ -167,10 +167,9 @@ class Message(BaseMessage):
self.panel_type = PANEL_TYPES[self.bitfield[18]]
# pos 20-21 - Unused.
self.text = alpha.strip('"')
self.mask = int(self.panel_data[3:3+8], 16)

if self.panel_type == ADEMCO:
self.mask = int(self.panel_data[3:3+8], 16)

if int(self.panel_data[19:21], 16) & 0x01 > 0:
# Current cursor location on the alpha display.
self.cursor_location = int(self.panel_data[21:23], 16)


+ 36
- 16
alarmdecoder/zonetracking.py View File

@@ -11,6 +11,7 @@ import time

from .event import event
from .messages import ExpanderMessage
from .panels import ADEMCO, DSC


class Zone(object):
@@ -37,8 +38,10 @@ class Zone(object):
"""Zone status"""
timestamp = None
"""Timestamp of last update"""
expander = False
"""Does this zone exist on an expander?"""

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

@@ -53,6 +56,7 @@ class Zone(object):
self.name = name
self.status = status
self.timestamp = time.time()
self.expander = expander

def __str__(self):
"""
@@ -116,7 +120,7 @@ class Zonetracker(object):
"""
self._zones_faulted = value

def __init__(self):
def __init__(self, alarmdecoder_object):
"""
Constructor
"""
@@ -124,6 +128,8 @@ class Zonetracker(object):
self._zones_faulted = []
self._last_zone_fault = 0

self.alarmdecoder_object = alarmdecoder_object

def update(self, message):
"""
Update zone statuses based on the current message.
@@ -132,9 +138,12 @@ class Zonetracker(object):
:type message: :py:class:`~alarmdecoder.messages.Message` or :py:class:`~alarmdecoder.messages.ExpanderMessage`
"""
if isinstance(message, ExpanderMessage):
zone = -1

if message.type == ExpanderMessage.ZONE:
zone = self.expander_to_zone(message.address, message.channel)
zone = self.expander_to_zone(message.address, message.channel, self.alarmdecoder_object.mode)

if zone != -1:
status = Zone.CLEAR
if message.value == 1:
status = Zone.FAULT
@@ -149,7 +158,7 @@ class Zonetracker(object):
self._update_zone(zone, status=status)

except IndexError:
self._add_zone(zone, status=status)
self._add_zone(zone, status=status, expander=True)

else:
# Panel is ready, restore all zones.
@@ -209,7 +218,7 @@ class Zonetracker(object):

self._clear_expired_zones()

def expander_to_zone(self, address, channel):
def expander_to_zone(self, address, channel, panel_type=ADEMCO):
"""
Convert an address and channel into a zone number.

@@ -221,12 +230,19 @@ class Zonetracker(object):
: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.
zone = -1

idx = address - 7 # Expanders start at address 7.
if panel_type == ADEMCO:
# TODO: This is going to need to be reworked to support the larger
# panels without fixed addressing on the expanders.

return address + channel + (idx * 7) + 1
idx = address - 7 # Expanders start at address 7.
zone = address + channel + (idx * 7) + 1

elif panel_type == DSC:
zone = (address * 8) + channel

return zone

def _clear_zones(self, zone):
"""
@@ -301,7 +317,7 @@ class Zonetracker(object):
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):
def _add_zone(self, zone, name='', status=Zone.CLEAR, expander=False):
"""
Adds a zone to the internal zone list.

@@ -313,10 +329,9 @@ class Zonetracker(object):
:type status: int
"""
if not zone in self._zones:
self._zones[zone] = Zone(zone=zone, name=name, status=status)
self._zones[zone] = Zone(zone=zone, name=name, status=None, expander=expander)

if status != Zone.CLEAR:
self.on_fault(zone=zone)
self._update_zone(zone, status=status)

def _update_zone(self, zone, status=None):
"""
@@ -332,9 +347,11 @@ class Zonetracker(object):
if not zone in self._zones:
raise IndexError('Zone does not exist and cannot be updated: %d', zone)

if status is not None:
self._zones[zone].status = status
old_status = self._zones[zone].status
if status is None:
status = old_status

self._zones[zone].status = status
self._zones[zone].timestamp = time.time()

if status == Zone.CLEAR:
@@ -342,6 +359,9 @@ class Zonetracker(object):
self._zones_faulted.remove(zone)

self.on_restore(zone=zone)
else:
if old_status != status and status is not None:
self.on_fault(zone=zone)

def _zone_expired(self, zone):
"""
@@ -352,4 +372,4 @@ class Zonetracker(object):

:returns: whether or not the zone is expired
"""
return time.time() > self._zones[zone].timestamp + Zonetracker.EXPIRE
return (time.time() > self._zones[zone].timestamp + Zonetracker.EXPIRE) and self._zones[zone].expander is False

+ 8
- 3
bin/ad2-firmwareupload View File

@@ -37,7 +37,7 @@ def main():
baudrate = 115200

if len(sys.argv) < 2:
print "Syntax: {0} <firmware> [interface] [baudrate]".format(sys.argv[0])
print "Syntax: {0} <firmware> [device path or hostname:port] [baudrate]".format(sys.argv[0])
sys.exit(1)

firmware = sys.argv[1]
@@ -49,8 +49,13 @@ def main():

print "Flashing device: {0} - {2} baud\r\nFirmware: {1}".format(device, firmware, baudrate)

dev = alarmdecoder.devices.SerialDevice(interface=device)
dev.open(baudrate=baudrate, no_reader_thread=True)
if ':' in device:
hostname, port = device.split(':')
dev = alarmdecoder.devices.SocketDevice(interface=(hostname, int(port)))
dev.open()
else:
dev = alarmdecoder.devices.SerialDevice(interface=device)
dev.open(baudrate=baudrate, no_reader_thread=True)

time.sleep(3)
alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware)


+ 6
- 3
examples/alarm_email.py View File

@@ -2,7 +2,7 @@ import time
import smtplib
from email.mime.text import MIMEText
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import USBDevice
from alarmdecoder.devices import SerialDevice

# Configuration values
SUBJECT = "AlarmDecoder - ALARM"
@@ -13,6 +13,9 @@ SMTP_SERVER = "localhost"
SMTP_USERNAME = None
SMTP_PASSWORD = None

SERIAL_DEVICE = '/dev/ttyUSB0'
BAUDRATE = 115200

def main():
"""
Example application that sends an email when an alarm event is
@@ -20,11 +23,11 @@ def main():
"""
try:
# Retrieve the first USB device
device = AlarmDecoder(USBDevice.find())
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE))

# Set up an event handler and open the device
device.on_alarm += handle_alarm
with device.open():
with device.open(baudrate=BAUDRATE):
while True:
time.sleep(1)



+ 6
- 3
examples/lrr_example.py View File

@@ -1,6 +1,9 @@
import time
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import USBDevice
from alarmdecoder.devices import SerialDevice

SERIAL_DEVICE = '/dev/ttyUSB0'
BAUDRATE = 115200

def main():
"""
@@ -8,11 +11,11 @@ def main():
"""
try:
# Retrieve the first USB device
device = AlarmDecoder(USBDevice.find())
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE))

# Set up an event handler and open the device
device.on_lrr_message += handle_lrr_message
with device.open():
with device.open(baudrate=BAUDRATE):
while True:
time.sleep(1)



+ 6
- 3
examples/rf_device.py View File

@@ -1,9 +1,12 @@
import time
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import USBDevice
from alarmdecoder.devices import SerialDevice

RF_DEVICE_SERIAL_NUMBER = '0252254'

SERIAL_DEVICE = '/dev/ttyUSB0'
BAUDRATE = 115200

def main():
"""
Example application that watches for an event from a specific RF device.
@@ -18,11 +21,11 @@ def main():
"""
try:
# Retrieve the first USB device
device = AlarmDecoder(USBDevice.find())
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE))

# Set up an event handler and open the device
device.on_rfx_message += handle_rfx
with device.open():
with device.open(baudrate=BAUDRATE):
while True:
time.sleep(1)



examples/detection.py → examples/usb_detection.py View File


examples/basics.py → examples/usb_device.py View File


+ 6
- 3
examples/virtual_zone_expander.py View File

@@ -1,11 +1,14 @@
import time
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import USBDevice
from alarmdecoder.devices import SerialDevice

# Configuration values
TARGET_ZONE = 41
WAIT_TIME = 10

SERIAL_DEVICE = '/dev/ttyUSB0'
BAUDRATE = 115200

def main():
"""
Example application that periodically faults a virtual zone and then
@@ -28,13 +31,13 @@ def main():
"""
try:
# Retrieve the first USB device
device = AlarmDecoder(USBDevice.find())
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE))

# Set up an event handlers and open the device
device.on_zone_fault += handle_zone_fault
device.on_zone_restore += handle_zone_restore

with device.open():
with device.open(baudrate=BAUDRATE):
last_update = time.time()
while True:
if time.time() - last_update > WAIT_TIME:


+ 0
- 15
requirements.txt View File

@@ -1,16 +1 @@
Jinja2==2.7.2
MarkupSafe==0.21
Pygments==1.6
Sphinx==1.2.2
argparse==1.2.1
cffi==0.8.2
cryptography==0.3
distribute==0.7.3
docutils==0.11
pyOpenSSL==0.14
pycparser==2.10
pyftdi==0.9.0
pyserial==2.7
pyusb==1.0.0b1
six==1.6.1
wsgiref==0.1.2

+ 0
- 6
setup.py View File

@@ -30,13 +30,7 @@ setup(name='alarmdecoder',
license='MIT',
packages=['alarmdecoder', 'alarmdecoder.event'],
install_requires=[
'pyopenssl',
'pyusb>=1.0.0b1',
'pyserial>=2.7',
'pyftdi>=0.9.0',
],
dependency_links=[
'https://github.com/eblot/pyftdi/archive/v0.9.0.tar.gz#egg=pyftdi-0.9.0'
],
test_suite='nose.collector',
tests_require=['nose', 'mock'],


+ 59
- 10
test/test_ad2.py View File

@@ -24,6 +24,12 @@ class TestAlarmDecoder(TestCase):
self._message_received = False
self._rfx_message_received = False
self._lrr_message_received = False
self._expander_message_received = False
self._sending_received_status = None
self._alarm_restored = False
self._on_boot_received = False
self._zone_faulted = None
self._zone_restored = None

self._device = Mock(spec=USBDevice)
self._device.on_open = EventHandler(Event(), self._device)
@@ -32,15 +38,11 @@ class TestAlarmDecoder(TestCase):
self._device.on_write = EventHandler(Event(), self._device)

self._decoder = AlarmDecoder(self._device)

self._decoder._zonetracker = Mock(spec=Zonetracker)
self._decoder._zonetracker.on_fault = EventHandler(Event(), self._decoder._zonetracker)
self._decoder._zonetracker.on_restore = EventHandler(Event(), self._decoder._zonetracker)

self._decoder.on_panic += self.on_panic
self._decoder.on_relay_changed += self.on_relay_changed
self._decoder.on_power_changed += self.on_power_changed
self._decoder.on_alarm += self.on_alarm
self._decoder.on_alarm_restored += self.on_alarm_restored
self._decoder.on_bypass += self.on_bypass
self._decoder.on_low_battery += self.on_battery
self._decoder.on_fire += self.on_fire
@@ -50,6 +52,11 @@ class TestAlarmDecoder(TestCase):
self._decoder.on_message += self.on_message
self._decoder.on_rfx_message += self.on_rfx_message
self._decoder.on_lrr_message += self.on_lrr_message
self._decoder.on_expander_message += self.on_expander_message
self._decoder.on_sending_received += self.on_sending_received
self._decoder.on_boot += self.on_boot
self._decoder.on_zone_fault += self.on_zone_fault
self._decoder.on_zone_restore += self.on_zone_restore

self._decoder.address_mask = int('ffffffff', 16)
self._decoder.open()
@@ -67,7 +74,10 @@ class TestAlarmDecoder(TestCase):
self._power_changed = kwargs['status']

def on_alarm(self, sender, *args, **kwargs):
self._alarmed = kwargs['status']
self._alarmed = True

def on_alarm_restored(self, sender, *args, **kwargs):
self._alarm_restored = True

def on_bypass(self, sender, *args, **kwargs):
self._bypassed = kwargs['status']
@@ -96,6 +106,21 @@ class TestAlarmDecoder(TestCase):
def on_lrr_message(self, sender, *args, **kwargs):
self._lrr_message_received = True

def on_expander_message(self, sender, *args, **kwargs):
self._expander_message_received = True

def on_sending_received(self, sender, *args, **kwargs):
self._sending_received_status = kwargs['status']

def on_boot(self, sender, *args, **kwargs):
self._on_boot_received = True

def on_zone_fault(self, sender, *args, **kwargs):
self._zone_faulted = kwargs['zone']

def on_zone_restore(self, sender, *args, **kwargs):
self._zone_restored = kwargs['zone']

def test_open(self):
self._decoder.open()
self._device.open.assert_any_calls()
@@ -141,7 +166,7 @@ class TestAlarmDecoder(TestCase):
self._decoder._on_read(self, data='[0000000000000000----],000,[f707000600e5800c0c020000]," "')
self.assertTrue(self._message_received)

def test_message_kpe(self):
def test_message_kpm(self):
msg = self._decoder._handle_message('!KPM:[0000000000000000----],000,[f707000600e5800c0c020000]," "')
self.assertIsInstance(msg, Message)

@@ -152,6 +177,9 @@ class TestAlarmDecoder(TestCase):
msg = self._decoder._handle_message('!EXP:07,01,01')
self.assertIsInstance(msg, ExpanderMessage)

self._decoder._on_read(self, data='!EXP:07,01,01')
self.assertTrue(self._expander_message_received)

def test_relay_message(self):
self._decoder.open()
msg = self._decoder._handle_message('!REL:12,01,01')
@@ -203,6 +231,7 @@ class TestAlarmDecoder(TestCase):

msg = self._decoder._handle_message('[0000000000000000----],000,[f707000600e5800c0c020000]," "')
self.assertEquals(self._alarmed, False)
self.assertEquals(self._alarm_restored, True)

msg = self._decoder._handle_message('[0000000000100000----],000,[f707000600e5800c0c020000]," "')
self.assertEquals(self._alarmed, True)
@@ -261,6 +290,26 @@ class TestAlarmDecoder(TestCase):

self._decoder._device.write.assert_called_with('*')

def test_zonetracker_update(self):
msg = self._decoder._handle_message('[0000000000000000----],000,[f707000600e5800c0c020000]," "')
self._decoder._zonetracker.update.assert_called_with(msg)
def test_sending_received(self):
self._decoder._on_read(self, data='!Sending.done')
self.assertTrue(self._sending_received_status)

self._decoder._on_read(self, data='!Sending.....done')
self.assertFalse(self._sending_received_status)

def test_boot(self):
self._decoder._on_read(self, data='!Ready')
self.assertTrue(self._on_boot_received)

def test_zone_fault_and_restore(self):
self._decoder._on_read(self, data='[00010001000000000A--],003,[f70000051003000008020000000000],"FAULT 03 "')
self.assertEquals(self._zone_faulted, 3)

self._decoder._on_read(self, data='[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "')
self.assertEquals(self._zone_faulted, 4)

self._decoder._on_read(self, data='[00010001000000000A--],005,[f70000051003000008020000000000],"FAULT 05 "')
self.assertEquals(self._zone_faulted, 5)

self._decoder._on_read(self, data='[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "')
self.assertEquals(self._zone_restored, 3)

Loading…
Cancel
Save