@@ -94,7 +94,7 @@ class AlarmDecoder(object): | |||||
:type device: Device | :type device: Device | ||||
""" | """ | ||||
self._device = device | self._device = device | ||||
self._zonetracker = Zonetracker() | |||||
self._zonetracker = Zonetracker(self) | |||||
self._battery_timeout = AlarmDecoder.BATTERY_TIMEOUT | self._battery_timeout = AlarmDecoder.BATTERY_TIMEOUT | ||||
self._fire_timeout = AlarmDecoder.FIRE_TIMEOUT | self._fire_timeout = AlarmDecoder.FIRE_TIMEOUT | ||||
@@ -106,6 +106,7 @@ class AlarmDecoder(object): | |||||
self._battery_status = (False, 0) | self._battery_status = (False, 0) | ||||
self._panic_status = None | self._panic_status = None | ||||
self._relay_status = {} | self._relay_status = {} | ||||
self._internal_address_mask = 0xFFFFFFFF | |||||
self.address = 18 | self.address = 18 | ||||
self.configbits = 0xFF00 | self.configbits = 0xFF00 | ||||
@@ -177,6 +178,25 @@ class AlarmDecoder(object): | |||||
""" | """ | ||||
self._fire_timeout = value | 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): | def open(self, baudrate=None, no_reader_thread=False): | ||||
""" | """ | ||||
Opens the device. | Opens the device. | ||||
@@ -344,10 +364,10 @@ class AlarmDecoder(object): | |||||
""" | """ | ||||
msg = Message(data) | msg = Message(data) | ||||
if self.address_mask & msg.mask > 0: | |||||
if self._internal_address_mask & msg.mask > 0: | |||||
self._update_internal_states(msg) | self._update_internal_states(msg) | ||||
self.on_message(message=msg) | |||||
self.on_message(message=msg) | |||||
return msg | return msg | ||||
@@ -15,19 +15,35 @@ This module contains different types of devices belonging to the `AlarmDecoder`_ | |||||
.. moduleauthor:: Scott Petersen <scott@nutech.com> | .. moduleauthor:: Scott Petersen <scott@nutech.com> | ||||
""" | """ | ||||
import usb.core | |||||
import usb.util | |||||
import time | import time | ||||
import threading | import threading | ||||
import serial | import serial | ||||
import serial.tools.list_ports | import serial.tools.list_ports | ||||
import socket | import socket | ||||
from OpenSSL import SSL, crypto | |||||
from pyftdi.pyftdi.ftdi import Ftdi, FtdiError | |||||
from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError | from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError | ||||
from .event import event | 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): | class Device(object): | ||||
""" | """ | ||||
@@ -198,6 +214,9 @@ class USBDevice(Device): | |||||
:returns: list of devices | :returns: list of devices | ||||
:raises: :py:class:`~alarmdecoder.util.CommError` | :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 = [] | cls.__devices = [] | ||||
query = cls.PRODUCT_IDS | query = cls.PRODUCT_IDS | ||||
@@ -234,6 +253,9 @@ class USBDevice(Device): | |||||
:returns: :py:class:`USBDevice` object utilizing the specified device | :returns: :py:class:`USBDevice` object utilizing the specified device | ||||
:raises: :py:class:`~alarmdecoder.util.NoDeviceError` | :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() | cls.find_all() | ||||
if len(cls.__devices) == 0: | if len(cls.__devices) == 0: | ||||
@@ -257,6 +279,9 @@ class USBDevice(Device): | |||||
:type on_detached: function | :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) | cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached) | ||||
try: | try: | ||||
@@ -271,6 +296,9 @@ class USBDevice(Device): | |||||
""" | """ | ||||
Stops the device detection thread. | 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: | try: | ||||
cls.__detect_thread.stop() | cls.__detect_thread.stop() | ||||
@@ -347,6 +375,9 @@ class USBDevice(Device): | |||||
index. | index. | ||||
:type interface: string or int | :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) | Device.__init__(self) | ||||
self._device = Ftdi() | self._device = Ftdi() | ||||
@@ -1120,6 +1151,9 @@ class SocketDevice(Device): | |||||
:raises: :py:class:`~alarmdecoder.util.CommError` | :raises: :py:class:`~alarmdecoder.util.CommError` | ||||
""" | """ | ||||
if not have_openssl: | |||||
raise ImportError('SSL sockets have been disabled due to missing requirement: pyopenssl.') | |||||
try: | try: | ||||
ctx = SSL.Context(SSL.TLSv1_METHOD) | ctx = SSL.Context(SSL.TLSv1_METHOD) | ||||
@@ -167,10 +167,9 @@ class Message(BaseMessage): | |||||
self.panel_type = PANEL_TYPES[self.bitfield[18]] | self.panel_type = PANEL_TYPES[self.bitfield[18]] | ||||
# pos 20-21 - Unused. | # pos 20-21 - Unused. | ||||
self.text = alpha.strip('"') | self.text = alpha.strip('"') | ||||
self.mask = int(self.panel_data[3:3+8], 16) | |||||
if self.panel_type == ADEMCO: | 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: | if int(self.panel_data[19:21], 16) & 0x01 > 0: | ||||
# Current cursor location on the alpha display. | # Current cursor location on the alpha display. | ||||
self.cursor_location = int(self.panel_data[21:23], 16) | self.cursor_location = int(self.panel_data[21:23], 16) | ||||
@@ -11,6 +11,7 @@ import time | |||||
from .event import event | from .event import event | ||||
from .messages import ExpanderMessage | from .messages import ExpanderMessage | ||||
from .panels import ADEMCO, DSC | |||||
class Zone(object): | class Zone(object): | ||||
@@ -37,8 +38,10 @@ class Zone(object): | |||||
"""Zone status""" | """Zone status""" | ||||
timestamp = None | timestamp = None | ||||
"""Timestamp of last update""" | """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 | Constructor | ||||
@@ -53,6 +56,7 @@ class Zone(object): | |||||
self.name = name | self.name = name | ||||
self.status = status | self.status = status | ||||
self.timestamp = time.time() | self.timestamp = time.time() | ||||
self.expander = expander | |||||
def __str__(self): | def __str__(self): | ||||
""" | """ | ||||
@@ -116,7 +120,7 @@ class Zonetracker(object): | |||||
""" | """ | ||||
self._zones_faulted = value | self._zones_faulted = value | ||||
def __init__(self): | |||||
def __init__(self, alarmdecoder_object): | |||||
""" | """ | ||||
Constructor | Constructor | ||||
""" | """ | ||||
@@ -124,6 +128,8 @@ class Zonetracker(object): | |||||
self._zones_faulted = [] | self._zones_faulted = [] | ||||
self._last_zone_fault = 0 | self._last_zone_fault = 0 | ||||
self.alarmdecoder_object = alarmdecoder_object | |||||
def update(self, message): | def update(self, message): | ||||
""" | """ | ||||
Update zone statuses based on the current 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` | :type message: :py:class:`~alarmdecoder.messages.Message` or :py:class:`~alarmdecoder.messages.ExpanderMessage` | ||||
""" | """ | ||||
if isinstance(message, ExpanderMessage): | if isinstance(message, ExpanderMessage): | ||||
zone = -1 | |||||
if message.type == ExpanderMessage.ZONE: | 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 | status = Zone.CLEAR | ||||
if message.value == 1: | if message.value == 1: | ||||
status = Zone.FAULT | status = Zone.FAULT | ||||
@@ -149,7 +158,7 @@ class Zonetracker(object): | |||||
self._update_zone(zone, status=status) | self._update_zone(zone, status=status) | ||||
except IndexError: | except IndexError: | ||||
self._add_zone(zone, status=status) | |||||
self._add_zone(zone, status=status, expander=True) | |||||
else: | else: | ||||
# Panel is ready, restore all zones. | # Panel is ready, restore all zones. | ||||
@@ -209,7 +218,7 @@ class Zonetracker(object): | |||||
self._clear_expired_zones() | 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. | 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 | :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): | def _clear_zones(self, zone): | ||||
""" | """ | ||||
@@ -301,7 +317,7 @@ class Zonetracker(object): | |||||
if self._zones[z].status != Zone.CLEAR and self._zone_expired(z): | if self._zones[z].status != Zone.CLEAR and self._zone_expired(z): | ||||
self._update_zone(z, Zone.CLEAR) | 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. | Adds a zone to the internal zone list. | ||||
@@ -313,10 +329,9 @@ class Zonetracker(object): | |||||
:type status: int | :type status: int | ||||
""" | """ | ||||
if not zone in self._zones: | 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): | def _update_zone(self, zone, status=None): | ||||
""" | """ | ||||
@@ -332,9 +347,11 @@ class Zonetracker(object): | |||||
if not zone in self._zones: | if not zone in self._zones: | ||||
raise IndexError('Zone does not exist and cannot be updated: %d', zone) | 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() | self._zones[zone].timestamp = time.time() | ||||
if status == Zone.CLEAR: | if status == Zone.CLEAR: | ||||
@@ -342,6 +359,9 @@ class Zonetracker(object): | |||||
self._zones_faulted.remove(zone) | self._zones_faulted.remove(zone) | ||||
self.on_restore(zone=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): | def _zone_expired(self, zone): | ||||
""" | """ | ||||
@@ -352,4 +372,4 @@ class Zonetracker(object): | |||||
:returns: whether or not the zone is expired | :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 |
@@ -37,7 +37,7 @@ def main(): | |||||
baudrate = 115200 | baudrate = 115200 | ||||
if len(sys.argv) < 2: | 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) | sys.exit(1) | ||||
firmware = sys.argv[1] | firmware = sys.argv[1] | ||||
@@ -49,8 +49,13 @@ def main(): | |||||
print "Flashing device: {0} - {2} baud\r\nFirmware: {1}".format(device, firmware, baudrate) | 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) | time.sleep(3) | ||||
alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware) | alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware) | ||||
@@ -2,7 +2,7 @@ import time | |||||
import smtplib | import smtplib | ||||
from email.mime.text import MIMEText | from email.mime.text import MIMEText | ||||
from alarmdecoder import AlarmDecoder | from alarmdecoder import AlarmDecoder | ||||
from alarmdecoder.devices import USBDevice | |||||
from alarmdecoder.devices import SerialDevice | |||||
# Configuration values | # Configuration values | ||||
SUBJECT = "AlarmDecoder - ALARM" | SUBJECT = "AlarmDecoder - ALARM" | ||||
@@ -13,6 +13,9 @@ SMTP_SERVER = "localhost" | |||||
SMTP_USERNAME = None | SMTP_USERNAME = None | ||||
SMTP_PASSWORD = None | SMTP_PASSWORD = None | ||||
SERIAL_DEVICE = '/dev/ttyUSB0' | |||||
BAUDRATE = 115200 | |||||
def main(): | def main(): | ||||
""" | """ | ||||
Example application that sends an email when an alarm event is | Example application that sends an email when an alarm event is | ||||
@@ -20,11 +23,11 @@ def main(): | |||||
""" | """ | ||||
try: | try: | ||||
# Retrieve the first USB device | # Retrieve the first USB device | ||||
device = AlarmDecoder(USBDevice.find()) | |||||
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) | |||||
# Set up an event handler and open the device | # Set up an event handler and open the device | ||||
device.on_alarm += handle_alarm | device.on_alarm += handle_alarm | ||||
with device.open(): | |||||
with device.open(baudrate=BAUDRATE): | |||||
while True: | while True: | ||||
time.sleep(1) | time.sleep(1) | ||||
@@ -1,6 +1,9 @@ | |||||
import time | import time | ||||
from alarmdecoder import AlarmDecoder | from alarmdecoder import AlarmDecoder | ||||
from alarmdecoder.devices import USBDevice | |||||
from alarmdecoder.devices import SerialDevice | |||||
SERIAL_DEVICE = '/dev/ttyUSB0' | |||||
BAUDRATE = 115200 | |||||
def main(): | def main(): | ||||
""" | """ | ||||
@@ -8,11 +11,11 @@ def main(): | |||||
""" | """ | ||||
try: | try: | ||||
# Retrieve the first USB device | # Retrieve the first USB device | ||||
device = AlarmDecoder(USBDevice.find()) | |||||
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) | |||||
# Set up an event handler and open the device | # Set up an event handler and open the device | ||||
device.on_lrr_message += handle_lrr_message | device.on_lrr_message += handle_lrr_message | ||||
with device.open(): | |||||
with device.open(baudrate=BAUDRATE): | |||||
while True: | while True: | ||||
time.sleep(1) | time.sleep(1) | ||||
@@ -1,9 +1,12 @@ | |||||
import time | import time | ||||
from alarmdecoder import AlarmDecoder | from alarmdecoder import AlarmDecoder | ||||
from alarmdecoder.devices import USBDevice | |||||
from alarmdecoder.devices import SerialDevice | |||||
RF_DEVICE_SERIAL_NUMBER = '0252254' | RF_DEVICE_SERIAL_NUMBER = '0252254' | ||||
SERIAL_DEVICE = '/dev/ttyUSB0' | |||||
BAUDRATE = 115200 | |||||
def main(): | def main(): | ||||
""" | """ | ||||
Example application that watches for an event from a specific RF device. | Example application that watches for an event from a specific RF device. | ||||
@@ -18,11 +21,11 @@ def main(): | |||||
""" | """ | ||||
try: | try: | ||||
# Retrieve the first USB device | # Retrieve the first USB device | ||||
device = AlarmDecoder(USBDevice.find()) | |||||
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) | |||||
# Set up an event handler and open the device | # Set up an event handler and open the device | ||||
device.on_rfx_message += handle_rfx | device.on_rfx_message += handle_rfx | ||||
with device.open(): | |||||
with device.open(baudrate=BAUDRATE): | |||||
while True: | while True: | ||||
time.sleep(1) | time.sleep(1) | ||||
@@ -1,11 +1,14 @@ | |||||
import time | import time | ||||
from alarmdecoder import AlarmDecoder | from alarmdecoder import AlarmDecoder | ||||
from alarmdecoder.devices import USBDevice | |||||
from alarmdecoder.devices import SerialDevice | |||||
# Configuration values | # Configuration values | ||||
TARGET_ZONE = 41 | TARGET_ZONE = 41 | ||||
WAIT_TIME = 10 | WAIT_TIME = 10 | ||||
SERIAL_DEVICE = '/dev/ttyUSB0' | |||||
BAUDRATE = 115200 | |||||
def main(): | def main(): | ||||
""" | """ | ||||
Example application that periodically faults a virtual zone and then | Example application that periodically faults a virtual zone and then | ||||
@@ -28,13 +31,13 @@ def main(): | |||||
""" | """ | ||||
try: | try: | ||||
# Retrieve the first USB device | # Retrieve the first USB device | ||||
device = AlarmDecoder(USBDevice.find()) | |||||
device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) | |||||
# Set up an event handlers and open the device | # Set up an event handlers and open the device | ||||
device.on_zone_fault += handle_zone_fault | device.on_zone_fault += handle_zone_fault | ||||
device.on_zone_restore += handle_zone_restore | device.on_zone_restore += handle_zone_restore | ||||
with device.open(): | |||||
with device.open(baudrate=BAUDRATE): | |||||
last_update = time.time() | last_update = time.time() | ||||
while True: | while True: | ||||
if time.time() - last_update > WAIT_TIME: | if time.time() - last_update > WAIT_TIME: | ||||
@@ -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 | pyserial==2.7 | ||||
pyusb==1.0.0b1 | |||||
six==1.6.1 | |||||
wsgiref==0.1.2 |
@@ -30,13 +30,7 @@ setup(name='alarmdecoder', | |||||
license='MIT', | license='MIT', | ||||
packages=['alarmdecoder', 'alarmdecoder.event'], | packages=['alarmdecoder', 'alarmdecoder.event'], | ||||
install_requires=[ | install_requires=[ | ||||
'pyopenssl', | |||||
'pyusb>=1.0.0b1', | |||||
'pyserial>=2.7', | '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', | test_suite='nose.collector', | ||||
tests_require=['nose', 'mock'], | tests_require=['nose', 'mock'], | ||||
@@ -24,6 +24,12 @@ class TestAlarmDecoder(TestCase): | |||||
self._message_received = False | self._message_received = False | ||||
self._rfx_message_received = False | self._rfx_message_received = False | ||||
self._lrr_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 = Mock(spec=USBDevice) | ||||
self._device.on_open = EventHandler(Event(), self._device) | self._device.on_open = EventHandler(Event(), self._device) | ||||
@@ -32,15 +38,11 @@ class TestAlarmDecoder(TestCase): | |||||
self._device.on_write = EventHandler(Event(), self._device) | self._device.on_write = EventHandler(Event(), self._device) | ||||
self._decoder = AlarmDecoder(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_panic += self.on_panic | ||||
self._decoder.on_relay_changed += self.on_relay_changed | self._decoder.on_relay_changed += self.on_relay_changed | ||||
self._decoder.on_power_changed += self.on_power_changed | self._decoder.on_power_changed += self.on_power_changed | ||||
self._decoder.on_alarm += self.on_alarm | 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_bypass += self.on_bypass | ||||
self._decoder.on_low_battery += self.on_battery | self._decoder.on_low_battery += self.on_battery | ||||
self._decoder.on_fire += self.on_fire | self._decoder.on_fire += self.on_fire | ||||
@@ -50,6 +52,11 @@ class TestAlarmDecoder(TestCase): | |||||
self._decoder.on_message += self.on_message | self._decoder.on_message += self.on_message | ||||
self._decoder.on_rfx_message += self.on_rfx_message | self._decoder.on_rfx_message += self.on_rfx_message | ||||
self._decoder.on_lrr_message += self.on_lrr_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.address_mask = int('ffffffff', 16) | ||||
self._decoder.open() | self._decoder.open() | ||||
@@ -67,7 +74,10 @@ class TestAlarmDecoder(TestCase): | |||||
self._power_changed = kwargs['status'] | self._power_changed = kwargs['status'] | ||||
def on_alarm(self, sender, *args, **kwargs): | 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): | def on_bypass(self, sender, *args, **kwargs): | ||||
self._bypassed = kwargs['status'] | self._bypassed = kwargs['status'] | ||||
@@ -96,6 +106,21 @@ class TestAlarmDecoder(TestCase): | |||||
def on_lrr_message(self, sender, *args, **kwargs): | def on_lrr_message(self, sender, *args, **kwargs): | ||||
self._lrr_message_received = True | 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): | def test_open(self): | ||||
self._decoder.open() | self._decoder.open() | ||||
self._device.open.assert_any_calls() | self._device.open.assert_any_calls() | ||||
@@ -141,7 +166,7 @@ class TestAlarmDecoder(TestCase): | |||||
self._decoder._on_read(self, data='[0000000000000000----],000,[f707000600e5800c0c020000]," "') | self._decoder._on_read(self, data='[0000000000000000----],000,[f707000600e5800c0c020000]," "') | ||||
self.assertTrue(self._message_received) | self.assertTrue(self._message_received) | ||||
def test_message_kpe(self): | |||||
def test_message_kpm(self): | |||||
msg = self._decoder._handle_message('!KPM:[0000000000000000----],000,[f707000600e5800c0c020000]," "') | msg = self._decoder._handle_message('!KPM:[0000000000000000----],000,[f707000600e5800c0c020000]," "') | ||||
self.assertIsInstance(msg, Message) | self.assertIsInstance(msg, Message) | ||||
@@ -152,6 +177,9 @@ class TestAlarmDecoder(TestCase): | |||||
msg = self._decoder._handle_message('!EXP:07,01,01') | msg = self._decoder._handle_message('!EXP:07,01,01') | ||||
self.assertIsInstance(msg, ExpanderMessage) | 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): | def test_relay_message(self): | ||||
self._decoder.open() | self._decoder.open() | ||||
msg = self._decoder._handle_message('!REL:12,01,01') | msg = self._decoder._handle_message('!REL:12,01,01') | ||||
@@ -203,6 +231,7 @@ class TestAlarmDecoder(TestCase): | |||||
msg = self._decoder._handle_message('[0000000000000000----],000,[f707000600e5800c0c020000]," "') | msg = self._decoder._handle_message('[0000000000000000----],000,[f707000600e5800c0c020000]," "') | ||||
self.assertEquals(self._alarmed, False) | self.assertEquals(self._alarmed, False) | ||||
self.assertEquals(self._alarm_restored, True) | |||||
msg = self._decoder._handle_message('[0000000000100000----],000,[f707000600e5800c0c020000]," "') | msg = self._decoder._handle_message('[0000000000100000----],000,[f707000600e5800c0c020000]," "') | ||||
self.assertEquals(self._alarmed, True) | self.assertEquals(self._alarmed, True) | ||||
@@ -261,6 +290,26 @@ class TestAlarmDecoder(TestCase): | |||||
self._decoder._device.write.assert_called_with('*') | 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) |