diff --git a/.gitignore b/.gitignore index a911660..921905b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ build/ dist tmp *.pyc +*.pyo *.egg-info bin/ad2-test +*~ diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index 568de9d..7c32179 100644 --- a/alarmdecoder/decoder.py +++ b/alarmdecoder/decoder.py @@ -7,6 +7,7 @@ Provides the main AlarmDecoder class. """ import time +import re from .event import event from .util import InvalidMessageError @@ -39,6 +40,7 @@ class AlarmDecoder(object): on_expander_message = event.Event("This event is called when an :py:class:`~alarmdecoder.messages.ExpanderMessage` is received.\n\n**Callback definition:** *def callback(device, message)*") on_lrr_message = event.Event("This event is called when an :py:class:`~alarmdecoder.messages.LRRMessage` is received.\n\n**Callback definition:** *def callback(device, message)*") on_rfx_message = event.Event("This event is called when an :py:class:`~alarmdecoder.messages.RFMessage` is received.\n\n**Callback definition:** *def callback(device, message)*") + on_sending_received = event.Event("This event is called when a !Sending.done message is received from the AlarmDecoder.\n\n**Callback definition:** *def callback(device, status, message)*") # Low-level Events on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*") @@ -55,6 +57,8 @@ class AlarmDecoder(object): """Represents panel function key #3""" KEY_F4 = unichr(4) + unichr(4) + unichr(4) """Represents panel function key #4""" + KEY_PANIC = unichr(5) + unichr(5) + unichr(5) + """Represents a panic keypress""" BATTERY_TIMEOUT = 30 """Default timeout (in seconds) before the battery status reverts.""" @@ -66,7 +70,7 @@ class AlarmDecoder(object): """The keypad address in use by the device.""" configbits = 0xFF00 """The configuration bits set on the device.""" - address_mask = 0x00000000 + address_mask = 0xFFFFFFFF """The address mask configured on the device.""" emulate_zone = [False for _ in range(5)] """List containing the devices zone emulation status.""" @@ -200,8 +204,9 @@ class AlarmDecoder(object): :param data: data to send :type data: string """ + if self._device: - self._device.write(data) + self._device.write(str(data)) def get_config(self): """ @@ -294,7 +299,11 @@ class AlarmDecoder(object): :returns: :py:class:`~alarmdecoder.messages.Message` """ - if data is None: + + if data is not None: + data = data.lstrip('\0') + + if data is None or data == '': raise InvalidMessageError() msg = None @@ -318,6 +327,9 @@ class AlarmDecoder(object): elif data.startswith('!CONFIG'): self._handle_config(data) + elif data.startswith('!Sending'): + self._handle_sending(data) + return msg def _handle_keypad_message(self, data): @@ -421,6 +433,22 @@ class AlarmDecoder(object): self.on_config_received() + def _handle_sending(self, data): + """ + Handles results of a keypress send. + + :param data: Sending string to parse + :type data: string + """ + + matches = re.match('^!Sending(\.{1,5})done.*', data) + if matches is not None: + good_send = False + if len(matches.group(1)) < 5: + good_send = True + + self.on_sending_received(status=good_send, message=data) + def _update_internal_states(self, message): """ Updates internal device states. diff --git a/alarmdecoder/devices.py b/alarmdecoder/devices.py index 7fce36c..42175e4 100644 --- a/alarmdecoder/devices.py +++ b/alarmdecoder/devices.py @@ -25,7 +25,7 @@ import socket from OpenSSL import SSL, crypto from pyftdi.pyftdi.ftdi import Ftdi, FtdiError -from .util import CommError, TimeoutError, NoDeviceError +from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError from .event import event @@ -149,8 +149,19 @@ class Device(object): except TimeoutError: pass - except Exception: + except InvalidMessageError: + pass + + except SSL.WantReadError: + pass + + except CommError, err: + self._device.close() + + except Exception, err: + self._device.close() self._running = False + raise class USBDevice(Device): @@ -234,7 +245,10 @@ class USBDevice(Device): """ cls.__detect_thread = USBDevice.DetectThread(on_attached, on_detached) - cls.find_all() + try: + cls.find_all() + except CommError: + pass cls.__detect_thread.start() @@ -390,6 +404,9 @@ class USBDevice(Device): except Exception: pass + def fileno(self): + raise NotImplementedError('USB devices do not support fileno()') + def write(self, data): """ Writes data to the device. @@ -648,8 +665,8 @@ class SerialDevice(Device): # all issues with it. self._device.baudrate = baudrate - except (serial.SerialException, ValueError), err: - raise NoDeviceError('Error opening device on port {0}.'.format(self._port), err) + except (serial.SerialException, ValueError, OSError), err: + raise NoDeviceError('Error opening device on {0}.'.format(self._port), err) else: self._running = True @@ -670,6 +687,9 @@ class SerialDevice(Device): except Exception: pass + def fileno(self): + return self._device.fileno() + def write(self, data): """ Writes data to the device. @@ -908,9 +928,15 @@ class SocketDevice(Device): self._init_ssl() self._device.connect((self._host, self._port)) + #self._device.setblocking(1) if self._use_ssl: - self._device.do_handshake() + while True: + try: + self._device.do_handshake() + break + except SSL.WantReadError: + pass self._id = '{0}:{1}'.format(self._host, self._port) @@ -939,11 +965,14 @@ class SocketDevice(Device): # Make sure that it closes immediately. self._device.shutdown(socket.SHUT_RDWR) - Device.close(self) - except Exception: pass + Device.close(self) + + def fileno(self): + return self._device.fileno() + def write(self, data): """ Writes data to the device. @@ -1033,6 +1062,13 @@ class SocketDevice(Device): except socket.error, err: raise CommError('Error reading from device: {0}'.format(str(err)), err) + except SSL.SysCallError, err: + errno, msg = err + raise CommError('SSL error while reading from device: {0} ({1})'.format(msg, errno)) + + except Exception: + raise + else: if got_line: ret, self._buffer = self._buffer, '' diff --git a/alarmdecoder/event/event.py b/alarmdecoder/event/event.py index 6200fa8..0086cbb 100644 --- a/alarmdecoder/event/event.py +++ b/alarmdecoder/event/event.py @@ -31,6 +31,9 @@ class EventHandler(object): self.event = event self.obj = obj + def __iter__(self): + return iter(self._getfunctionlist()) + def _getfunctionlist(self): """(internal use) """ diff --git a/alarmdecoder/messages.py b/alarmdecoder/messages.py index a5d67cf..f44edff 100644 --- a/alarmdecoder/messages.py +++ b/alarmdecoder/messages.py @@ -13,6 +13,7 @@ devices. """ import re +import datetime from .util import InvalidMessageError @@ -25,11 +26,14 @@ class BaseMessage(object): raw = None """The raw message text""" + timestamp = None + """The timestamp of the message""" + def __init__(self): """ Constructor """ - pass + self.timestamp = datetime.datetime.now() def __str__(self): """ @@ -37,6 +41,22 @@ class BaseMessage(object): """ return self.raw + def dict(self, **kwargs): + """ + Dictionary representation. + """ + return dict( + time=self.timestamp, + mesg=self.raw, + **kwargs + ) + + def __repr__(self): + """ + String representation. + """ + return repr(self.dict()) + class Message(BaseMessage): """ @@ -102,12 +122,6 @@ class Message(BaseMessage): if data is not None: self._parse_message(data) - def __str__(self): - """ - String conversion operator. - """ - return self.raw - def _parse_message(self, data): """ Parse the message from the device. @@ -151,6 +165,37 @@ class Message(BaseMessage): # Current cursor location on the alpha display. self.cursor_location = int(self.bitfield[21:23], 16) + def dict(self, **kwargs): + """ + Dictionary representation. + """ + return dict( + time = self.timestamp, + bitfield = self.bitfield, + numeric_code = self.numeric_code, + panel_data = self.panel_data, + mask = self.mask, + ready = self.ready, + armed_away = self.armed_away, + armed_home = self.armed_home, + backlight_on = self.backlight_on, + programming_mode = self.programming_mode, + beeps = self.beeps, + zone_bypassed = self.zone_bypassed, + ac_power = self.ac_power, + chime_on = self.chime_on, + alarm_event_occurred = self.alarm_event_occurred, + alarm_sounding = self.alarm_sounding, + battery_low = self.battery_low, + entry_delay_off = self.entry_delay_off, + fire_alarm = self.fire_alarm, + check_zone = self.check_zone, + perimeter_only = self.perimeter_only, + text = self.text, + cursor_location = self.cursor_location, + **kwargs + ) + class ExpanderMessage(BaseMessage): """ @@ -183,12 +228,6 @@ class ExpanderMessage(BaseMessage): if data is not None: self._parse_message(data) - def __str__(self): - """ - String conversion operator. - """ - return self.raw - def _parse_message(self, data): """ Parse the raw message from the device. @@ -217,6 +256,18 @@ class ExpanderMessage(BaseMessage): else: raise InvalidMessageError('Unknown expander message header: {0}'.format(data)) + def dict(self, **kwargs): + """ + Dictionary representation. + """ + return dict( + time = self.timestamp, + address = self.address, + channel = self.channel, + value = self.value, + **kwargs + ) + class RFMessage(BaseMessage): """ @@ -246,12 +297,6 @@ class RFMessage(BaseMessage): if data is not None: self._parse_message(data) - def __str__(self): - """ - String conversion operator. - """ - return self.raw - def _parse_message(self, data): """ Parses the raw message from the device. @@ -282,6 +327,19 @@ class RFMessage(BaseMessage): except ValueError: raise InvalidMessageError('Received invalid message: {0}'.format(data)) + def dict(self, **kwargs): + """ + Dictionary representation. + """ + return dict( + time = self.timestamp, + serial_number = self.serial_number, + value = self.value, + battery = self.battery, + supervision = self.supervision, + **kwargs + ) + class LRRMessage(BaseMessage): """ @@ -307,12 +365,6 @@ class LRRMessage(BaseMessage): if data is not None: self._parse_message(data) - def __str__(self): - """ - String conversion operator. - """ - return self.raw - def _parse_message(self, data): """ Parses the raw message from the device. @@ -330,3 +382,15 @@ class LRRMessage(BaseMessage): except ValueError: raise InvalidMessageError('Received invalid message: {0}'.format(data)) + + def dict(self, **kwargs): + """ + Dictionary representation. + """ + return dict( + time = self.timestamp, + event_data = self.event_data, + event_type = self.event_type, + partition = self.partition, + **kwargs + )