| @@ -8,3 +8,4 @@ tmp | |||
| *.egg-info | |||
| bin/ad2-test | |||
| *~ | |||
| .vscode | |||
| @@ -17,9 +17,11 @@ except ImportError: | |||
| from .event import event | |||
| from .util import InvalidMessageError | |||
| from .messages import Message, ExpanderMessage, RFMessage, LRRMessage | |||
| from .messages import Message, ExpanderMessage, RFMessage, LRRMessage, AUIMessage | |||
| from .messages.lrr import LRRSystem | |||
| from .zonetracking import Zonetracker | |||
| from .panels import PANEL_TYPES, ADEMCO, DSC | |||
| from .states import FireState | |||
| class AlarmDecoder(object): | |||
| @@ -49,6 +51,7 @@ class AlarmDecoder(object): | |||
| 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)*") | |||
| on_aui_message = event.Event("This event is called when an :py:class`~alarmdecoder.messages.AUIMessage` is received\n\n**Callback definition:** *def callback(device, 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)*") | |||
| @@ -90,6 +93,8 @@ class AlarmDecoder(object): | |||
| """The status of message deduplication as configured on the device.""" | |||
| mode = ADEMCO | |||
| """The panel mode that the AlarmDecoder is in. Currently supports ADEMCO and DSC.""" | |||
| emulate_com = False | |||
| """The status of the devices COM emulation.""" | |||
| #Version Information | |||
| serial_number = 0xFFFFFFFF | |||
| @@ -99,25 +104,32 @@ class AlarmDecoder(object): | |||
| version_flags = "" | |||
| """Device flags enabled""" | |||
| def __init__(self, device): | |||
| def __init__(self, device, ignore_message_states=False): | |||
| """ | |||
| Constructor | |||
| :param device: The low-level device used for this `AlarmDecoder`_ | |||
| interface. | |||
| :type device: Device | |||
| :param ignore_message_states: Ignore regular panel messages when updating internal states | |||
| :type ignore_message_states: bool | |||
| """ | |||
| self._device = device | |||
| self._zonetracker = Zonetracker(self) | |||
| self._lrr_system = LRRSystem(self) | |||
| self._ignore_message_states = ignore_message_states | |||
| self._battery_timeout = AlarmDecoder.BATTERY_TIMEOUT | |||
| self._fire_timeout = AlarmDecoder.FIRE_TIMEOUT | |||
| self._power_status = None | |||
| self._alarm_status = None | |||
| self._bypass_status = None | |||
| self._bypass_status = {} | |||
| self._armed_status = None | |||
| self._armed_stay = False | |||
| self._fire_status = (False, 0) | |||
| self._fire_alarming = False | |||
| self._fire_alarming_changed = 0 | |||
| self._fire_state = FireState.NONE | |||
| self._battery_status = (False, 0) | |||
| self._panic_status = False | |||
| self._relay_status = {} | |||
| @@ -134,6 +146,7 @@ class AlarmDecoder(object): | |||
| self.emulate_lrr = False | |||
| self.deduplicate = False | |||
| self.mode = ADEMCO | |||
| self.emulate_com = False | |||
| self.serial_number = 0xFFFFFFFF | |||
| self.version_number = 'Unknown' | |||
| @@ -276,6 +289,12 @@ class AlarmDecoder(object): | |||
| self.send("C{0}\r".format(self.get_config_string())) | |||
| def get_config_string(self): | |||
| """ | |||
| Build a configuration string that's compatible with the AlarmDecoder configuration | |||
| command from the current values in the object. | |||
| :returns: string | |||
| """ | |||
| config_entries = [] | |||
| # HACK: This is ugly.. but I can't think of an elegant way of doing it. | |||
| @@ -289,6 +308,7 @@ class AlarmDecoder(object): | |||
| config_entries.append(('LRR', 'Y' if self.emulate_lrr else 'N')) | |||
| config_entries.append(('DEDUPLICATE', 'Y' if self.deduplicate else 'N')) | |||
| config_entries.append(('MODE', list(PANEL_TYPES)[list(PANEL_TYPES.values()).index(self.mode)])) | |||
| config_entries.append(('COM', 'Y' if self.emulate_com else 'N')) | |||
| config_string = '&'.join(['='.join(t) for t in config_entries]) | |||
| @@ -382,6 +402,9 @@ class AlarmDecoder(object): | |||
| elif header == '!LRR': | |||
| msg = self._handle_lrr(data) | |||
| elif header == '!AUI': | |||
| msg = self._handle_aui(data) | |||
| elif data.startswith('!Ready'): | |||
| self.on_boot() | |||
| @@ -405,10 +428,14 @@ class AlarmDecoder(object): | |||
| :returns: :py:class:`~alarmdecoder.messages.Message` | |||
| """ | |||
| msg = Message(data) | |||
| if self._internal_address_mask & msg.mask > 0: | |||
| self._update_internal_states(msg) | |||
| if not self._ignore_message_states: | |||
| self._update_internal_states(msg) | |||
| else: | |||
| self._update_fire_status(status=None) | |||
| self.on_message(message=msg) | |||
| @@ -456,16 +483,23 @@ class AlarmDecoder(object): | |||
| """ | |||
| msg = LRRMessage(data) | |||
| if msg.event_type == 'ALARM_PANIC': | |||
| self._panic_status = True | |||
| self.on_panic(status=True) | |||
| self._lrr_system.update(msg) | |||
| self.on_lrr_message(message=msg) | |||
| return msg | |||
| elif msg.event_type == 'CANCEL': | |||
| if self._panic_status is True: | |||
| self._panic_status = False | |||
| self.on_panic(status=False) | |||
| def _handle_aui(self, data): | |||
| """ | |||
| Handle AUI messages. | |||
| self.on_lrr_message(message=msg) | |||
| :param data: RF message to parse | |||
| :type data: string | |||
| :returns: :py:class`~alarmdecoder.messages.AUIMessage` | |||
| """ | |||
| msg = AUIMessage(data) | |||
| self.on_aui_message(message=msg) | |||
| return msg | |||
| @@ -511,6 +545,8 @@ class AlarmDecoder(object): | |||
| self.deduplicate = (val == 'Y') | |||
| elif key == 'MODE': | |||
| self.mode = PANEL_TYPES[val] | |||
| elif key == 'COM': | |||
| self.emulate_com = (val == 'Y') | |||
| self.on_config_received() | |||
| @@ -537,7 +573,7 @@ class AlarmDecoder(object): | |||
| :param message: :py:class:`~alarmdecoder.messages.Message` to update internal states with | |||
| :type message: :py:class:`~alarmdecoder.messages.Message`, :py:class:`~alarmdecoder.messages.ExpanderMessage`, :py:class:`~alarmdecoder.messages.LRRMessage`, or :py:class:`~alarmdecoder.messages.RFMessage` | |||
| """ | |||
| if isinstance(message, Message): | |||
| if isinstance(message, Message) and not self._ignore_message_states: | |||
| self._update_power_status(message) | |||
| self._update_alarm_status(message) | |||
| self._update_zone_bypass_status(message) | |||
| @@ -550,122 +586,237 @@ class AlarmDecoder(object): | |||
| self._update_zone_tracker(message) | |||
| def _update_power_status(self, message): | |||
| def _update_power_status(self, message=None, status=None): | |||
| """ | |||
| Uses the provided message to update the AC power state. | |||
| :param message: message to use to update | |||
| :type message: :py:class:`~alarmdecoder.messages.Message` | |||
| :param status: power status, overrides message bits. | |||
| :type status: bool | |||
| :returns: bool indicating the new status | |||
| """ | |||
| if message.ac_power != self._power_status: | |||
| self._power_status, old_status = message.ac_power, self._power_status | |||
| power_status = status | |||
| if isinstance(message, Message): | |||
| power_status = message.ac_power | |||
| if power_status is None: | |||
| return | |||
| if power_status != self._power_status: | |||
| self._power_status, old_status = power_status, self._power_status | |||
| if old_status is not None: | |||
| self.on_power_changed(status=self._power_status) | |||
| return self._power_status | |||
| def _update_alarm_status(self, message): | |||
| def _update_alarm_status(self, message=None, status=None, zone=None, user=None): | |||
| """ | |||
| Uses the provided message to update the alarm state. | |||
| :param message: message to use to update | |||
| :type message: :py:class:`~alarmdecoder.messages.Message` | |||
| :param status: alarm status, overrides message bits. | |||
| :type status: bool | |||
| :param user: user associated with alarm event | |||
| :type user: string | |||
| :returns: bool indicating the new status | |||
| """ | |||
| if message.alarm_sounding != self._alarm_status: | |||
| self._alarm_status, old_status = message.alarm_sounding, self._alarm_status | |||
| alarm_status = status | |||
| alarm_zone = zone | |||
| if isinstance(message, Message): | |||
| alarm_status = message.alarm_sounding | |||
| alarm_zone = message.parse_numeric_code() | |||
| if old_status is not None: | |||
| if alarm_status != self._alarm_status: | |||
| self._alarm_status, old_status = alarm_status, self._alarm_status | |||
| if old_status is not None or status is not None: | |||
| if self._alarm_status: | |||
| self.on_alarm(zone=message.numeric_code) | |||
| self.on_alarm(zone=alarm_zone) | |||
| else: | |||
| self.on_alarm_restored(zone=message.numeric_code) | |||
| self.on_alarm_restored(zone=alarm_zone, user=user) | |||
| return self._alarm_status | |||
| def _update_zone_bypass_status(self, message): | |||
| def _update_zone_bypass_status(self, message=None, status=None, zone=None): | |||
| """ | |||
| Uses the provided message to update the zone bypass state. | |||
| :param message: message to use to update | |||
| :type message: :py:class:`~alarmdecoder.messages.Message` | |||
| :param status: bypass status, overrides message bits. | |||
| :type status: bool | |||
| :param zone: zone associated with bypass event | |||
| :type zone: int | |||
| :returns: bool indicating the new status | |||
| """ | |||
| bypass_status = status | |||
| if isinstance(message, Message): | |||
| bypass_status = message.zone_bypassed | |||
| if message.zone_bypassed != self._bypass_status: | |||
| self._bypass_status, old_status = message.zone_bypassed, self._bypass_status | |||
| if bypass_status is None: | |||
| return | |||
| if old_status is not None: | |||
| self.on_bypass(status=self._bypass_status) | |||
| old_bypass_status = self._bypass_status.get(zone, None) | |||
| if bypass_status != old_bypass_status: | |||
| if bypass_status == False and zone is None: | |||
| self._bypass_status = {} | |||
| else: | |||
| self._bypass_status[zone] = bypass_status | |||
| if old_bypass_status is not None or message is None or (old_bypass_status is None and bypass_status is True): | |||
| self.on_bypass(status=bypass_status, zone=zone) | |||
| return self._bypass_status | |||
| return bypass_status | |||
| def _update_armed_status(self, message): | |||
| def _update_armed_status(self, message=None, status=None, status_stay=None): | |||
| """ | |||
| Uses the provided message to update the armed state. | |||
| :param message: message to use to update | |||
| :type message: :py:class:`~alarmdecoder.messages.Message` | |||
| :param status: armed status, overrides message bits | |||
| :type status: bool | |||
| :param status_stay: armed stay status, overrides message bits | |||
| :type status_stay: bool | |||
| :returns: bool indicating the new status | |||
| """ | |||
| arm_status = status | |||
| stay_status = status_stay | |||
| self._armed_status, old_status = message.armed_away, self._armed_status | |||
| self._armed_stay, old_stay = message.armed_home, self._armed_stay | |||
| if message.armed_away != old_status or message.armed_home != old_stay: | |||
| if old_status is not None: | |||
| if isinstance(message, Message): | |||
| arm_status = message.armed_away | |||
| stay_status = message.armed_home | |||
| if arm_status is None or stay_status is None: | |||
| return | |||
| self._armed_status, old_status = arm_status, self._armed_status | |||
| self._armed_stay, old_stay = stay_status, self._armed_stay | |||
| if arm_status != old_status or stay_status != old_stay: | |||
| if old_status is not None or message is None: | |||
| if self._armed_status or self._armed_stay: | |||
| self.on_arm(stay=message.armed_home) | |||
| self.on_arm(stay=stay_status) | |||
| else: | |||
| self.on_disarm() | |||
| return self._armed_status or self._armed_stay | |||
| def _update_battery_status(self, message): | |||
| def _update_battery_status(self, message=None, status=None): | |||
| """ | |||
| Uses the provided message to update the battery state. | |||
| :param message: message to use to update | |||
| :type message: :py:class:`~alarmdecoder.messages.Message` | |||
| :param status: battery status, overrides message bits | |||
| :type status: bool | |||
| :returns: boolean indicating the new status | |||
| """ | |||
| battery_status = status | |||
| if isinstance(message, Message): | |||
| battery_status = message.battery_low | |||
| if battery_status is None: | |||
| return | |||
| last_status, last_update = self._battery_status | |||
| if message.battery_low == last_status: | |||
| if battery_status == last_status: | |||
| self._battery_status = (last_status, time.time()) | |||
| else: | |||
| if message.battery_low is True or time.time() > last_update + self._battery_timeout: | |||
| self._battery_status = (message.battery_low, time.time()) | |||
| self.on_low_battery(status=message.battery_low) | |||
| if battery_status is True or time.time() > last_update + self._battery_timeout: | |||
| self._battery_status = (battery_status, time.time()) | |||
| self.on_low_battery(status=battery_status) | |||
| return self._battery_status[0] | |||
| def _update_fire_status(self, message): | |||
| def _update_fire_status(self, message=None, status=None): | |||
| """ | |||
| Uses the provided message to update the fire alarm state. | |||
| :param message: message to use to update | |||
| :type message: :py:class:`~alarmdecoder.messages.Message` | |||
| :param status: fire status, overrides message bits | |||
| :type status: bool | |||
| :returns: boolean indicating the new status | |||
| """ | |||
| is_lrr = status is not None | |||
| fire_status = status | |||
| if isinstance(message, Message): | |||
| fire_status = message.fire_alarm | |||
| last_status, last_update = self._fire_status | |||
| if message.fire_alarm == last_status: | |||
| self._fire_status = (last_status, time.time()) | |||
| else: | |||
| if message.fire_alarm is True or time.time() > last_update + self._fire_timeout: | |||
| self._fire_status = (message.fire_alarm, time.time()) | |||
| self.on_fire(status=message.fire_alarm) | |||
| return self._fire_status[0] | |||
| if self._fire_state == FireState.NONE: | |||
| # Always move to a FIRE state if detected | |||
| if fire_status == True: | |||
| self._fire_state = FireState.ALARM | |||
| self._fire_status = (fire_status, time.time()) | |||
| self.on_fire(status=FireState.ALARM) | |||
| elif self._fire_state == FireState.ALARM: | |||
| # If we've received an LRR CANCEL message, move to ACKNOWLEDGED | |||
| if is_lrr and fire_status == False: | |||
| self._fire_state = FireState.ACKNOWLEDGED | |||
| self._fire_status = (fire_status, time.time()) | |||
| self.on_fire(status=FireState.ACKNOWLEDGED) | |||
| else: | |||
| # Handle bouncing status changes and timeout in order to revert back to NONE. | |||
| if last_status != fire_status or fire_status == True: | |||
| self._fire_status = (fire_status, time.time()) | |||
| if fire_status == False and time.time() > last_update + self._fire_timeout: | |||
| self._fire_state = FireState.NONE | |||
| self.on_fire(status=FireState.NONE) | |||
| elif self._fire_state == FireState.ACKNOWLEDGED: | |||
| # If we've received a second LRR FIRE message after a CANCEL, revert back to FIRE and trigger another event. | |||
| if is_lrr and fire_status == True: | |||
| self._fire_state = FireState.ALARM | |||
| self._fire_status = (fire_status, time.time()) | |||
| self.on_fire(status=FireState.ALARM) | |||
| else: | |||
| # Handle bouncing status changes and timeout in order to revert back to NONE. | |||
| if last_status != fire_status or fire_status == True: | |||
| self._fire_status = (fire_status, time.time()) | |||
| if fire_status != True and time.time() > last_update + self._fire_timeout: | |||
| self._fire_state = FireState.NONE | |||
| self.on_fire(status=FireState.NONE) | |||
| return self._fire_state == FireState.ALARM | |||
| def _update_panic_status(self, status=None): | |||
| """ | |||
| Updates the panic status of the alarm panel. | |||
| :param status: status to use to update | |||
| :type status: boolean | |||
| :returns: boolean indicating the new status | |||
| """ | |||
| if status is None: | |||
| return | |||
| if status != self._panic_status: | |||
| self._panic_status, old_status = status, self._panic_status | |||
| if old_status is not None: | |||
| self.on_panic(status=self._panic_status) | |||
| return self._panic_status | |||
| def _update_expander_status(self, message): | |||
| """ | |||
| @@ -708,7 +859,6 @@ class AlarmDecoder(object): | |||
| Internal handler for opening the device. | |||
| """ | |||
| self.get_config() | |||
| self.get_version() | |||
| self.on_open() | |||
| @@ -0,0 +1,6 @@ | |||
| from .base_device import Device | |||
| from .serial_device import SerialDevice | |||
| from .socket_device import SocketDevice | |||
| from .usb_device import USBDevice | |||
| __all__ = ['Device', 'SerialDevice', 'SocketDevice', 'USBDevice'] | |||
| @@ -0,0 +1,147 @@ | |||
| """ | |||
| This module contains the base device type for the `AlarmDecoder`_ (AD2) family. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| import threading | |||
| from ..util import CommError, TimeoutError, InvalidMessageError | |||
| from ..event import event | |||
| class Device(object): | |||
| """ | |||
| Base class for all `AlarmDecoder`_ (AD2) device types. | |||
| """ | |||
| # Generic device events | |||
| on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*") | |||
| on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*") | |||
| on_read = event.Event("This event is called when a line has been read from the device.\n\n**Callback definition:** def callback(device, data)*") | |||
| on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*") | |||
| def __init__(self): | |||
| """ | |||
| Constructor | |||
| """ | |||
| self._id = '' | |||
| self._buffer = b'' | |||
| self._device = None | |||
| self._running = False | |||
| self._read_thread = None | |||
| def __enter__(self): | |||
| """ | |||
| Support for context manager __enter__. | |||
| """ | |||
| return self | |||
| def __exit__(self, exc_type, exc_value, traceback): | |||
| """ | |||
| Support for context manager __exit__. | |||
| """ | |||
| self.close() | |||
| return False | |||
| @property | |||
| def id(self): | |||
| """ | |||
| Retrieve the device ID. | |||
| :returns: identification string for the device | |||
| """ | |||
| return self._id | |||
| @id.setter | |||
| def id(self, value): | |||
| """ | |||
| Sets the device ID. | |||
| :param value: device identification string | |||
| :type value: string | |||
| """ | |||
| self._id = value | |||
| def is_reader_alive(self): | |||
| """ | |||
| Indicates whether or not the reader thread is alive. | |||
| :returns: whether or not the reader thread is alive | |||
| """ | |||
| return self._read_thread.is_alive() | |||
| def stop_reader(self): | |||
| """ | |||
| Stops the reader thread. | |||
| """ | |||
| self._read_thread.stop() | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| self._running = False | |||
| self._read_thread.stop() | |||
| self._device.close() | |||
| except Exception: | |||
| pass | |||
| self.on_close() | |||
| class ReadThread(threading.Thread): | |||
| """ | |||
| Reader thread which processes messages from the device. | |||
| """ | |||
| READ_TIMEOUT = 10 | |||
| """Timeout for the reader thread.""" | |||
| def __init__(self, device): | |||
| """ | |||
| Constructor | |||
| :param device: device used by the reader thread | |||
| :type device: :py:class:`~alarmdecoder.devices.Device` | |||
| """ | |||
| threading.Thread.__init__(self) | |||
| self._device = device | |||
| self._running = False | |||
| def stop(self): | |||
| """ | |||
| Stops the running thread. | |||
| """ | |||
| self._running = False | |||
| def run(self): | |||
| """ | |||
| The actual read process. | |||
| """ | |||
| self._running = True | |||
| while self._running: | |||
| try: | |||
| self._device.read_line(timeout=self.READ_TIMEOUT) | |||
| except TimeoutError: | |||
| pass | |||
| except InvalidMessageError: | |||
| pass | |||
| except SSL.WantReadError: | |||
| pass | |||
| except CommError as err: | |||
| self._device.close() | |||
| except Exception as err: | |||
| self._device.close() | |||
| self._running = False | |||
| raise | |||
| @@ -0,0 +1,278 @@ | |||
| """ | |||
| This module contains the :py:class:`SerialDevice` interface for the `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_. | |||
| .. _AD2USB: http://www.alarmdecoder.com | |||
| .. _AD2SERIAL: http://www.alarmdecoder.com | |||
| .. _AD2PI: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| import threading | |||
| import serial | |||
| import serial.tools.list_ports | |||
| import select | |||
| import sys | |||
| from .base_device import Device | |||
| from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
| class SerialDevice(Device): | |||
| """ | |||
| `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_ device utilizing the PySerial interface. | |||
| """ | |||
| # Constants | |||
| BAUDRATE = 19200 | |||
| """Default baudrate for Serial devices.""" | |||
| @staticmethod | |||
| def find_all(pattern=None): | |||
| """ | |||
| Returns all serial ports present. | |||
| :param pattern: pattern to search for when retrieving serial ports | |||
| :type pattern: string | |||
| :returns: list of devices | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| devices = [] | |||
| try: | |||
| if pattern: | |||
| devices = serial.tools.list_ports.grep(pattern) | |||
| else: | |||
| devices = serial.tools.list_ports.comports() | |||
| except serial.SerialException as err: | |||
| raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err) | |||
| return devices | |||
| @property | |||
| def interface(self): | |||
| """ | |||
| Retrieves the interface used to connect to the device. | |||
| :returns: interface used to connect to the device | |||
| """ | |||
| return self._port | |||
| @interface.setter | |||
| def interface(self, value): | |||
| """ | |||
| Sets the interface used to connect to the device. | |||
| :param value: name of the serial device | |||
| :type value: string | |||
| """ | |||
| self._port = value | |||
| def __init__(self, interface=None): | |||
| """ | |||
| Constructor | |||
| :param interface: device to open | |||
| :type interface: string | |||
| """ | |||
| Device.__init__(self) | |||
| self._port = interface | |||
| self._id = interface | |||
| # Timeout = non-blocking to match pyftdi. | |||
| self._device = serial.Serial(timeout=0, writeTimeout=0) | |||
| def open(self, baudrate=BAUDRATE, no_reader_thread=False): | |||
| """ | |||
| Opens the device. | |||
| :param baudrate: baudrate to use with the device | |||
| :type baudrate: int | |||
| :param no_reader_thread: whether or not to automatically start the | |||
| reader thread. | |||
| :type no_reader_thread: bool | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError` | |||
| """ | |||
| # Set up the defaults | |||
| if baudrate is None: | |||
| baudrate = SerialDevice.BAUDRATE | |||
| if self._port is None: | |||
| raise NoDeviceError('No device interface specified.') | |||
| self._read_thread = Device.ReadThread(self) | |||
| # Open the device and start up the reader thread. | |||
| try: | |||
| self._device.port = self._port | |||
| self._device.open() | |||
| # NOTE: Setting the baudrate before opening the | |||
| # port caused issues with Moschip 7840/7820 | |||
| # USB Serial Driver converter. (mos7840) | |||
| # | |||
| # Moving it to this point seems to resolve | |||
| # all issues with it. | |||
| self._device.baudrate = baudrate | |||
| except (serial.SerialException, ValueError, OSError) as err: | |||
| raise NoDeviceError('Error opening device on {0}.'.format(self._port), err) | |||
| else: | |||
| self._running = True | |||
| self.on_open() | |||
| if not no_reader_thread: | |||
| self._read_thread.start() | |||
| return self | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| Device.close(self) | |||
| except Exception: | |||
| pass | |||
| def fileno(self): | |||
| """ | |||
| Returns the file number associated with the device | |||
| :returns: int | |||
| """ | |||
| return self._device.fileno() | |||
| def write(self, data): | |||
| """ | |||
| Writes data to the device. | |||
| :param data: data to write | |||
| :type data: string | |||
| :raises: py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| try: | |||
| # Hack to support unicode under Python 2.x | |||
| if isinstance(data, str) or (sys.version_info < (3,) and isinstance(data, unicode)): | |||
| data = data.encode('utf-8') | |||
| self._device.write(data) | |||
| except serial.SerialTimeoutException: | |||
| pass | |||
| except serial.SerialException as err: | |||
| raise CommError('Error writing to device.', err) | |||
| else: | |||
| self.on_write(data=data) | |||
| def read(self): | |||
| """ | |||
| Reads a single character from the device. | |||
| :returns: character read from the device | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| data = '' | |||
| try: | |||
| read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | |||
| if len(read_ready) != 0: | |||
| data = self._device.read(1) | |||
| except serial.SerialException as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| return data.decode('utf-8') | |||
| def read_line(self, timeout=0.0, purge_buffer=False): | |||
| """ | |||
| Reads a line from the device. | |||
| :param timeout: read timeout | |||
| :type timeout: float | |||
| :param purge_buffer: Indicates whether to purge the buffer prior to | |||
| reading. | |||
| :type purge_buffer: bool | |||
| :returns: line that was read | |||
| :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` | |||
| """ | |||
| def timeout_event(): | |||
| """Handles read timeout event""" | |||
| timeout_event.reading = False | |||
| timeout_event.reading = True | |||
| if purge_buffer: | |||
| self._buffer = b'' | |||
| got_line, data = False, '' | |||
| timer = threading.Timer(timeout, timeout_event) | |||
| if timeout > 0: | |||
| timer.start() | |||
| leftovers = b'' | |||
| try: | |||
| while timeout_event.reading and not got_line: | |||
| read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | |||
| if len(read_ready) == 0: | |||
| continue | |||
| bytes_avail = 0 | |||
| if hasattr(self._device, "in_waiting"): | |||
| bytes_avail = self._device.in_waiting | |||
| else: | |||
| bytes_avail = self._device.inWaiting() | |||
| buf = self._device.read(bytes_avail) | |||
| for idx in range(len(buf)): | |||
| c = buf[idx] | |||
| ub = bytes_hack(c) | |||
| if sys.version_info > (3,): | |||
| ub = bytes([ub]) | |||
| # NOTE: AD2SERIAL and AD2PI apparently sends down \xFF on boot. | |||
| if ub != b'' and ub != b"\xff": | |||
| self._buffer += ub | |||
| if ub == b"\n": | |||
| self._buffer = self._buffer.strip(b"\r\n") | |||
| if len(self._buffer) > 0: | |||
| got_line = True | |||
| leftovers = buf[idx:] | |||
| break | |||
| except (OSError, serial.SerialException) as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| else: | |||
| if got_line: | |||
| data, self._buffer = self._buffer, leftovers | |||
| self.on_read(data=data) | |||
| else: | |||
| raise TimeoutError('Timeout while waiting for line terminator.') | |||
| finally: | |||
| timer.cancel() | |||
| return data.decode('utf-8') | |||
| def purge(self): | |||
| """ | |||
| Purges read/write buffers. | |||
| """ | |||
| self._device.flushInput() | |||
| self._device.flushOutput() | |||
| @@ -0,0 +1,399 @@ | |||
| """ | |||
| This module contains :py:class:`SocketDevice` interface for `AlarmDecoder`_ devices | |||
| that are exposed through `ser2sock`_ or another IP to serial solution. Also supports | |||
| SSL if using `ser2sock`_. | |||
| .. _ser2sock: http://github.com/nutechsoftware/ser2sock | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| import threading | |||
| import socket | |||
| import select | |||
| from .base_device import Device | |||
| from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
| try: | |||
| from OpenSSL import SSL, crypto | |||
| have_openssl = True | |||
| except ImportError: | |||
| class SSL: | |||
| class Error(BaseException): | |||
| pass | |||
| class WantReadError(BaseException): | |||
| pass | |||
| class SysCallError(BaseException): | |||
| pass | |||
| have_openssl = False | |||
| class SocketDevice(Device): | |||
| """ | |||
| Device that supports communication with an `AlarmDecoder`_ (AD2) that is | |||
| exposed via `ser2sock`_ or another Serial to IP interface. | |||
| """ | |||
| @property | |||
| def interface(self): | |||
| """ | |||
| Retrieves the interface used to connect to the device. | |||
| :returns: interface used to connect to the device | |||
| """ | |||
| return (self._host, self._port) | |||
| @interface.setter | |||
| def interface(self, value): | |||
| """ | |||
| Sets the interface used to connect to the device. | |||
| :param value: Tuple containing the host and port to use | |||
| :type value: tuple | |||
| """ | |||
| self._host, self._port = value | |||
| @property | |||
| def ssl(self): | |||
| """ | |||
| Retrieves whether or not the device is using SSL. | |||
| :returns: whether or not the device is using SSL | |||
| """ | |||
| return self._use_ssl | |||
| @ssl.setter | |||
| def ssl(self, value): | |||
| """ | |||
| Sets whether or not SSL communication is in use. | |||
| :param value: Whether or not SSL communication is in use | |||
| :type value: bool | |||
| """ | |||
| self._use_ssl = value | |||
| @property | |||
| def ssl_certificate(self): | |||
| """ | |||
| Retrieves the SSL client certificate path used for authentication. | |||
| :returns: path to the certificate path or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| return self._ssl_certificate | |||
| @ssl_certificate.setter | |||
| def ssl_certificate(self, value): | |||
| """ | |||
| Sets the SSL client certificate to use for authentication. | |||
| :param value: path to the SSL certificate or :py:class:`OpenSSL.crypto.X509` | |||
| :type value: string or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| self._ssl_certificate = value | |||
| @property | |||
| def ssl_key(self): | |||
| """ | |||
| Retrieves the SSL client certificate key used for authentication. | |||
| :returns: jpath to the SSL key or :py:class:`OpenSSL.crypto.PKey` | |||
| """ | |||
| return self._ssl_key | |||
| @ssl_key.setter | |||
| def ssl_key(self, value): | |||
| """ | |||
| Sets the SSL client certificate key to use for authentication. | |||
| :param value: path to the SSL key or :py:class:`OpenSSL.crypto.PKey` | |||
| :type value: string or :py:class:`OpenSSL.crypto.PKey` | |||
| """ | |||
| self._ssl_key = value | |||
| @property | |||
| def ssl_ca(self): | |||
| """ | |||
| Retrieves the SSL Certificate Authority certificate used for | |||
| authentication. | |||
| :returns: path to the CA certificate or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| return self._ssl_ca | |||
| @ssl_ca.setter | |||
| def ssl_ca(self, value): | |||
| """ | |||
| Sets the SSL Certificate Authority certificate used for authentication. | |||
| :param value: path to the SSL CA certificate or :py:class:`OpenSSL.crypto.X509` | |||
| :type value: string or :py:class:`OpenSSL.crypto.X509` | |||
| """ | |||
| self._ssl_ca = value | |||
| def __init__(self, interface=("localhost", 10000)): | |||
| """ | |||
| Constructor | |||
| :param interface: Tuple containing the hostname and port of our target | |||
| :type interface: tuple | |||
| """ | |||
| Device.__init__(self) | |||
| self._host, self._port = interface | |||
| self._use_ssl = False | |||
| self._ssl_certificate = None | |||
| self._ssl_key = None | |||
| self._ssl_ca = None | |||
| def open(self, baudrate=None, no_reader_thread=False): | |||
| """ | |||
| Opens the device. | |||
| :param baudrate: baudrate to use | |||
| :type baudrate: int | |||
| :param no_reader_thread: whether or not to automatically open the reader | |||
| thread. | |||
| :type no_reader_thread: bool | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| try: | |||
| self._read_thread = Device.ReadThread(self) | |||
| self._device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| if self._use_ssl: | |||
| self._init_ssl() | |||
| self._device.connect((self._host, self._port)) | |||
| if self._use_ssl: | |||
| while True: | |||
| try: | |||
| self._device.do_handshake() | |||
| break | |||
| except SSL.WantReadError: | |||
| pass | |||
| self._id = '{0}:{1}'.format(self._host, self._port) | |||
| except socket.error as err: | |||
| raise NoDeviceError('Error opening device at {0}:{1}'.format(self._host, self._port), err) | |||
| else: | |||
| self._running = True | |||
| self.on_open() | |||
| if not no_reader_thread: | |||
| self._read_thread.start() | |||
| return self | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| # TODO: Find a way to speed up this shutdown. | |||
| if self.ssl: | |||
| self._device.shutdown() | |||
| else: | |||
| # Make sure that it closes immediately. | |||
| self._device.shutdown(socket.SHUT_RDWR) | |||
| except Exception: | |||
| pass | |||
| Device.close(self) | |||
| def fileno(self): | |||
| """ | |||
| Returns the file number associated with the device | |||
| :returns: int | |||
| """ | |||
| return self._device.fileno() | |||
| def write(self, data): | |||
| """ | |||
| Writes data to the device. | |||
| :param data: data to write | |||
| :type data: string | |||
| :returns: number of bytes sent | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| data_sent = None | |||
| try: | |||
| if isinstance(data, str): | |||
| data = data.encode('utf-8') | |||
| data_sent = self._device.send(data) | |||
| if data_sent == 0: | |||
| raise CommError('Error writing to device.') | |||
| self.on_write(data=data) | |||
| except (SSL.Error, socket.error) as err: | |||
| raise CommError('Error writing to device.', err) | |||
| return data_sent | |||
| def read(self): | |||
| """ | |||
| Reads a single character from the device. | |||
| :returns: character read from the device | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| data = '' | |||
| try: | |||
| read_ready, _, _ = select.select([self._device], [], [], 0.5) | |||
| if len(read_ready) != 0: | |||
| data = self._device.recv(1) | |||
| except socket.error as err: | |||
| raise CommError('Error while reading from device: {0}'.format(str(err)), err) | |||
| return data.decode('utf-8') | |||
| def read_line(self, timeout=0.0, purge_buffer=False): | |||
| """ | |||
| Reads a line from the device. | |||
| :param timeout: read timeout | |||
| :type timeout: float | |||
| :param purge_buffer: Indicates whether to purge the buffer prior to | |||
| reading. | |||
| :type purge_buffer: bool | |||
| :returns: line that was read | |||
| :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` | |||
| """ | |||
| def timeout_event(): | |||
| """Handles read timeout event""" | |||
| timeout_event.reading = False | |||
| timeout_event.reading = True | |||
| if purge_buffer: | |||
| self._buffer = b'' | |||
| got_line, ret = False, None | |||
| timer = threading.Timer(timeout, timeout_event) | |||
| if timeout > 0: | |||
| timer.start() | |||
| try: | |||
| while timeout_event.reading: | |||
| read_ready, _, _ = select.select([self._device], [], [], 0.5) | |||
| if len(read_ready) == 0: | |||
| continue | |||
| buf = self._device.recv(1) | |||
| if buf != b'' and buf != b"\xff": | |||
| ub = bytes_hack(buf) | |||
| self._buffer += ub | |||
| if ub == b"\n": | |||
| self._buffer = self._buffer.rstrip(b"\r\n") | |||
| if len(self._buffer) > 0: | |||
| got_line = True | |||
| break | |||
| except socket.error as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| except SSL.SysCallError as 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, b'' | |||
| self.on_read(data=ret) | |||
| else: | |||
| raise TimeoutError('Timeout while waiting for line terminator.') | |||
| finally: | |||
| timer.cancel() | |||
| return ret.decode('utf-8') | |||
| def purge(self): | |||
| """ | |||
| Purges read/write buffers. | |||
| """ | |||
| try: | |||
| self._device.setblocking(0) | |||
| while(self._device.recv(1)): | |||
| pass | |||
| except socket.error as err: | |||
| pass | |||
| finally: | |||
| self._device.setblocking(1) | |||
| def _init_ssl(self): | |||
| """ | |||
| Initializes our device as an SSL connection. | |||
| :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) | |||
| if isinstance(self.ssl_key, crypto.PKey): | |||
| ctx.use_privatekey(self.ssl_key) | |||
| else: | |||
| ctx.use_privatekey_file(self.ssl_key) | |||
| if isinstance(self.ssl_certificate, crypto.X509): | |||
| ctx.use_certificate(self.ssl_certificate) | |||
| else: | |||
| ctx.use_certificate_file(self.ssl_certificate) | |||
| if isinstance(self.ssl_ca, crypto.X509): | |||
| store = ctx.get_cert_store() | |||
| store.add_cert(self.ssl_ca) | |||
| else: | |||
| ctx.load_verify_locations(self.ssl_ca, None) | |||
| ctx.set_verify(SSL.VERIFY_PEER, self._verify_ssl_callback) | |||
| self._device = SSL.Connection(ctx, self._device) | |||
| except SSL.Error as err: | |||
| raise CommError('Error setting up SSL connection.', err) | |||
| def _verify_ssl_callback(self, connection, x509, errnum, errdepth, ok): | |||
| """ | |||
| SSL verification callback. | |||
| """ | |||
| return ok | |||
| @@ -0,0 +1,490 @@ | |||
| """ | |||
| This module contains the :py:class:`USBDevice` interface for the `AD2USB`_. | |||
| .. _AD2USB: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| import time | |||
| import threading | |||
| from .base_device import Device | |||
| from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||
| from ..event import event | |||
| have_pyftdi = False | |||
| try: | |||
| from pyftdi.pyftdi.ftdi import Ftdi, FtdiError | |||
| import usb.core | |||
| import usb.util | |||
| have_pyftdi = True | |||
| except ImportError: | |||
| try: | |||
| from pyftdi.ftdi import Ftdi, FtdiError | |||
| import usb.core | |||
| import usb.util | |||
| have_pyftdi = True | |||
| except ImportError: | |||
| have_pyftdi = False | |||
| class USBDevice(Device): | |||
| """ | |||
| `AD2USB`_ device utilizing PyFTDI's interface. | |||
| """ | |||
| # Constants | |||
| PRODUCT_IDS = ((0x0403, 0x6001), (0x0403, 0x6015)) | |||
| """List of Vendor and Product IDs used to recognize `AD2USB`_ devices.""" | |||
| DEFAULT_VENDOR_ID = PRODUCT_IDS[0][0] | |||
| """Default Vendor ID used to recognize `AD2USB`_ devices.""" | |||
| DEFAULT_PRODUCT_ID = PRODUCT_IDS[0][1] | |||
| """Default Product ID used to recognize `AD2USB`_ devices.""" | |||
| # Deprecated constants | |||
| FTDI_VENDOR_ID = DEFAULT_VENDOR_ID | |||
| """DEPRECATED: Vendor ID used to recognize `AD2USB`_ devices.""" | |||
| FTDI_PRODUCT_ID = DEFAULT_PRODUCT_ID | |||
| """DEPRECATED: Product ID used to recognize `AD2USB`_ devices.""" | |||
| BAUDRATE = 115200 | |||
| """Default baudrate for `AD2USB`_ devices.""" | |||
| __devices = [] | |||
| __detect_thread = None | |||
| @classmethod | |||
| def find_all(cls, vid=None, pid=None): | |||
| """ | |||
| Returns all FTDI devices matching our vendor and product IDs. | |||
| :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 | |||
| if vid and pid: | |||
| query = [(vid, pid)] | |||
| try: | |||
| cls.__devices = Ftdi.find_all(query, nocache=True) | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise CommError('Error enumerating AD2USB devices: {0}'.format(str(err)), err) | |||
| return cls.__devices | |||
| @classmethod | |||
| def devices(cls): | |||
| """ | |||
| Returns a cached list of `AD2USB`_ devices located on the system. | |||
| :returns: cached list of devices found | |||
| """ | |||
| return cls.__devices | |||
| @classmethod | |||
| def find(cls, device=None): | |||
| """ | |||
| Factory method that returns the requested :py:class:`USBDevice` device, or the | |||
| first device. | |||
| :param device: Tuple describing the USB device to open, as returned | |||
| by find_all(). | |||
| :type device: tuple | |||
| :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: | |||
| raise NoDeviceError('No AD2USB devices present.') | |||
| if device is None: | |||
| device = cls.__devices[0] | |||
| vendor, product, sernum, ifcount, description = device | |||
| return USBDevice(interface=sernum, vid=vendor, pid=product) | |||
| @classmethod | |||
| def start_detection(cls, on_attached=None, on_detached=None): | |||
| """ | |||
| Starts the device detection thread. | |||
| :param on_attached: function to be called when a device is attached **Callback definition:** *def callback(thread, device)* | |||
| :type on_attached: function | |||
| :param on_detached: function to be called when a device is detached **Callback definition:** *def callback(thread, 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: | |||
| cls.find_all() | |||
| except CommError: | |||
| pass | |||
| cls.__detect_thread.start() | |||
| @classmethod | |||
| def stop_detection(cls): | |||
| """ | |||
| 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() | |||
| except Exception: | |||
| pass | |||
| @property | |||
| def interface(self): | |||
| """ | |||
| Retrieves the interface used to connect to the device. | |||
| :returns: the interface used to connect to the device | |||
| """ | |||
| return self._interface | |||
| @interface.setter | |||
| def interface(self, value): | |||
| """ | |||
| Sets the interface used to connect to the device. | |||
| :param value: may specify either the serial number or the device index | |||
| :type value: string or int | |||
| """ | |||
| self._interface = value | |||
| if isinstance(value, int): | |||
| self._device_number = value | |||
| else: | |||
| self._serial_number = value | |||
| @property | |||
| def serial_number(self): | |||
| """ | |||
| Retrieves the serial number of the device. | |||
| :returns: serial number of the device | |||
| """ | |||
| return self._serial_number | |||
| @serial_number.setter | |||
| def serial_number(self, value): | |||
| """ | |||
| Sets the serial number of the device. | |||
| :param value: serial number of the device | |||
| :type value: string | |||
| """ | |||
| self._serial_number = value | |||
| @property | |||
| def description(self): | |||
| """ | |||
| Retrieves the description of the device. | |||
| :returns: description of the device | |||
| """ | |||
| return self._description | |||
| @description.setter | |||
| def description(self, value): | |||
| """ | |||
| Sets the description of the device. | |||
| :param value: description of the device | |||
| :type value: string | |||
| """ | |||
| self._description = value | |||
| def __init__(self, interface=0, vid=None, pid=None): | |||
| """ | |||
| Constructor | |||
| :param interface: May specify either the serial number or the 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() | |||
| self._interface = 0 | |||
| self._device_number = 0 | |||
| self._serial_number = None | |||
| self._vendor_id = USBDevice.DEFAULT_VENDOR_ID | |||
| if vid: | |||
| self._vendor_id = vid | |||
| self._product_id = USBDevice.DEFAULT_PRODUCT_ID | |||
| if pid: | |||
| self._product_id = pid | |||
| self._endpoint = 0 | |||
| self._description = None | |||
| self.interface = interface | |||
| def open(self, baudrate=BAUDRATE, no_reader_thread=False): | |||
| """ | |||
| Opens the device. | |||
| :param baudrate: baudrate to use | |||
| :type baudrate: int | |||
| :param no_reader_thread: whether or not to automatically start the | |||
| reader thread. | |||
| :type no_reader_thread: bool | |||
| :raises: :py:class:`~alarmdecoder.util.NoDeviceError` | |||
| """ | |||
| # Set up defaults | |||
| if baudrate is None: | |||
| baudrate = USBDevice.BAUDRATE | |||
| self._read_thread = Device.ReadThread(self) | |||
| # 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._device.set_baudrate(baudrate) | |||
| if not self._serial_number: | |||
| self._serial_number = self._get_serial_number() | |||
| self._id = self._serial_number | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise NoDeviceError('Error opening device: {0}'.format(str(err)), err) | |||
| except KeyError as err: | |||
| raise NoDeviceError('Unsupported device. ({0:04x}:{1:04x}) You probably need a newer version of pyftdi.'.format(err[0][0], err[0][1])) | |||
| else: | |||
| self._running = True | |||
| self.on_open() | |||
| if not no_reader_thread: | |||
| self._read_thread.start() | |||
| return self | |||
| def close(self): | |||
| """ | |||
| Closes the device. | |||
| """ | |||
| try: | |||
| Device.close(self) | |||
| # HACK: Probably should fork pyftdi and make this call in .close() | |||
| self._device.usb_dev.attach_kernel_driver(self._device_number) | |||
| except Exception: | |||
| pass | |||
| def fileno(self): | |||
| """ | |||
| File number not supported for USB devices. | |||
| :raises: NotImplementedError | |||
| """ | |||
| raise NotImplementedError('USB devices do not support fileno()') | |||
| def write(self, data): | |||
| """ | |||
| Writes data to the device. | |||
| :param data: data to write | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| try: | |||
| self._device.write_data(data) | |||
| self.on_write(data=data) | |||
| except FtdiError as err: | |||
| raise CommError('Error writing to device: {0}'.format(str(err)), err) | |||
| def read(self): | |||
| """ | |||
| Reads a single character from the device. | |||
| :returns: character read from the device | |||
| :raises: :py:class:`~alarmdecoder.util.CommError` | |||
| """ | |||
| ret = None | |||
| try: | |||
| ret = self._device.read_data(1) | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| return ret | |||
| def read_line(self, timeout=0.0, purge_buffer=False): | |||
| """ | |||
| Reads a line from the device. | |||
| :param timeout: read timeout | |||
| :type timeout: float | |||
| :param purge_buffer: Indicates whether to purge the buffer prior to | |||
| reading. | |||
| :type purge_buffer: bool | |||
| :returns: line that was read | |||
| :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` | |||
| """ | |||
| def timeout_event(): | |||
| """Handles read timeout event""" | |||
| timeout_event.reading = False | |||
| timeout_event.reading = True | |||
| if purge_buffer: | |||
| self._buffer = b'' | |||
| got_line, ret = False, None | |||
| timer = threading.Timer(timeout, timeout_event) | |||
| if timeout > 0: | |||
| timer.start() | |||
| try: | |||
| while timeout_event.reading: | |||
| buf = self._device.read_data(1) | |||
| if buf != b'': | |||
| ub = bytes_hack(buf) | |||
| self._buffer += ub | |||
| if ub == b"\n": | |||
| self._buffer = self._buffer.rstrip(b"\r\n") | |||
| if len(self._buffer) > 0: | |||
| got_line = True | |||
| break | |||
| else: | |||
| time.sleep(0.01) | |||
| except (usb.core.USBError, FtdiError) as err: | |||
| raise CommError('Error reading from device: {0}'.format(str(err)), err) | |||
| else: | |||
| if got_line: | |||
| ret, self._buffer = self._buffer, b'' | |||
| self.on_read(data=ret) | |||
| else: | |||
| raise TimeoutError('Timeout while waiting for line terminator.') | |||
| finally: | |||
| timer.cancel() | |||
| return ret | |||
| def purge(self): | |||
| """ | |||
| Purges read/write buffers. | |||
| """ | |||
| self._device.purge_buffers() | |||
| def _get_serial_number(self): | |||
| """ | |||
| Retrieves the FTDI device serial number. | |||
| :returns: string containing the device serial number | |||
| """ | |||
| return usb.util.get_string(self._device.usb_dev, 64, self._device.usb_dev.iSerialNumber) | |||
| class DetectThread(threading.Thread): | |||
| """ | |||
| Thread that handles detection of added/removed devices. | |||
| """ | |||
| on_attached = event.Event("This event is called when an `AD2USB`_ device has been detected.\n\n**Callback definition:** def callback(thread, device*") | |||
| on_detached = event.Event("This event is called when an `AD2USB`_ device has been removed.\n\n**Callback definition:** def callback(thread, device*") | |||
| def __init__(self, on_attached=None, on_detached=None): | |||
| """ | |||
| Constructor | |||
| :param on_attached: Function to call when a device is attached **Callback definition:** *def callback(thread, device)* | |||
| :type on_attached: function | |||
| :param on_detached: Function to call when a device is detached **Callback definition:** *def callback(thread, device)* | |||
| :type on_detached: function | |||
| """ | |||
| threading.Thread.__init__(self) | |||
| if on_attached: | |||
| self.on_attached += on_attached | |||
| if on_detached: | |||
| self.on_detached += on_detached | |||
| self._running = False | |||
| def stop(self): | |||
| """ | |||
| Stops the thread. | |||
| """ | |||
| self._running = False | |||
| def run(self): | |||
| """ | |||
| The actual detection process. | |||
| """ | |||
| self._running = True | |||
| last_devices = set() | |||
| while self._running: | |||
| try: | |||
| current_devices = set(USBDevice.find_all()) | |||
| for dev in current_devices.difference(last_devices): | |||
| self.on_attached(device=dev) | |||
| for dev in last_devices.difference(current_devices): | |||
| self.on_detached(device=dev) | |||
| last_devices = current_devices | |||
| except CommError: | |||
| pass | |||
| time.sleep(0.25) | |||
| @@ -1,410 +0,0 @@ | |||
| """ | |||
| Message representations received from the panel through the `AlarmDecoder`_ (AD2) | |||
| devices. | |||
| * :py:class:`Message`: The standard and most common message received from a panel. | |||
| * :py:class:`ExpanderMessage`: Messages received from Relay or Zone expander modules. | |||
| * :py:class:`RFMessage`: Message received from an RF receiver module. | |||
| * :py:class:`LRRMessage`: Message received from a long-range radio module. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| import re | |||
| import datetime | |||
| try: | |||
| from reprlib import repr | |||
| except ImportError: | |||
| from repr import repr | |||
| from .util import InvalidMessageError | |||
| from .panels import PANEL_TYPES, ADEMCO, DSC | |||
| class BaseMessage(object): | |||
| """ | |||
| Base class for messages. | |||
| """ | |||
| raw = None | |||
| """The raw message text""" | |||
| timestamp = None | |||
| """The timestamp of the message""" | |||
| def __init__(self): | |||
| """ | |||
| Constructor | |||
| """ | |||
| self.timestamp = datetime.datetime.now() | |||
| def __str__(self): | |||
| """ | |||
| String conversion operator. | |||
| """ | |||
| 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): | |||
| """ | |||
| Represents a message from the alarm panel. | |||
| """ | |||
| ready = False | |||
| """Indicates whether or not the panel is in a ready state.""" | |||
| armed_away = False | |||
| """Indicates whether or not the panel is armed away.""" | |||
| armed_home = False | |||
| """Indicates whether or not the panel is armed home.""" | |||
| backlight_on = False | |||
| """Indicates whether or not the keypad backlight is on.""" | |||
| programming_mode = False | |||
| """Indicates whether or not we're in programming mode.""" | |||
| beeps = -1 | |||
| """Number of beeps associated with a message.""" | |||
| zone_bypassed = False | |||
| """Indicates whether or not a zone is bypassed.""" | |||
| ac_power = False | |||
| """Indicates whether or not the panel is on AC power.""" | |||
| chime_on = False | |||
| """Indicates whether or not the chime is enabled.""" | |||
| alarm_event_occurred = False | |||
| """Indicates whether or not an alarm event has occurred.""" | |||
| alarm_sounding = False | |||
| """Indicates whether or not an alarm is sounding.""" | |||
| battery_low = False | |||
| """Indicates whether or not there is a low battery.""" | |||
| entry_delay_off = False | |||
| """Indicates whether or not the entry delay is enabled.""" | |||
| fire_alarm = False | |||
| """Indicates whether or not a fire alarm is sounding.""" | |||
| check_zone = False | |||
| """Indicates whether or not there are zones that require attention.""" | |||
| perimeter_only = False | |||
| """Indicates whether or not the perimeter is armed.""" | |||
| system_fault = False | |||
| """Indicates whether a system fault has occurred.""" | |||
| panel_type = ADEMCO | |||
| """Indicates which panel type was the source of this message.""" | |||
| numeric_code = None | |||
| """The numeric code associated with the message.""" | |||
| text = None | |||
| """The human-readable text to be displayed on the panel LCD.""" | |||
| cursor_location = -1 | |||
| """Current cursor location on the keypad.""" | |||
| mask = 0xFFFFFFFF | |||
| """Address mask this message is intended for.""" | |||
| bitfield = None | |||
| """The bitfield associated with this message.""" | |||
| panel_data = None | |||
| """The panel data field associated with this message.""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self) | |||
| self._regex = re.compile('^(!KPM:){0,1}(\[[a-fA-F0-9\-]+\]),([a-fA-F0-9]+),(\[[a-fA-F0-9]+\]),(".+")$') | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parse the message from the device. | |||
| :param data: message data | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| match = self._regex.match(str(data)) | |||
| if match is None: | |||
| raise InvalidMessageError('Received invalid message: {0}'.format(data)) | |||
| header, self.bitfield, self.numeric_code, self.panel_data, alpha = match.group(1, 2, 3, 4, 5) | |||
| is_bit_set = lambda bit: not self.bitfield[bit] == "0" | |||
| self.raw = data | |||
| self.ready = is_bit_set(1) | |||
| self.armed_away = is_bit_set(2) | |||
| self.armed_home = is_bit_set(3) | |||
| self.backlight_on = is_bit_set(4) | |||
| self.programming_mode = is_bit_set(5) | |||
| self.beeps = int(self.bitfield[6], 16) | |||
| self.zone_bypassed = is_bit_set(7) | |||
| self.ac_power = is_bit_set(8) | |||
| self.chime_on = is_bit_set(9) | |||
| self.alarm_event_occurred = is_bit_set(10) | |||
| self.alarm_sounding = is_bit_set(11) | |||
| self.battery_low = is_bit_set(12) | |||
| self.entry_delay_off = is_bit_set(13) | |||
| self.fire_alarm = is_bit_set(14) | |||
| self.check_zone = is_bit_set(15) | |||
| self.perimeter_only = is_bit_set(16) | |||
| self.system_fault = is_bit_set(17) | |||
| if self.bitfield[18] in list(PANEL_TYPES): | |||
| 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 in (ADEMCO, DSC): | |||
| 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) | |||
| 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): | |||
| """ | |||
| Represents a message from a zone or relay expansion module. | |||
| """ | |||
| ZONE = 0 | |||
| """Flag indicating that the expander message relates to a Zone Expander.""" | |||
| 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 | |||
| """Address of expander""" | |||
| channel = -1 | |||
| """Channel on the expander""" | |||
| value = -1 | |||
| """Value associated with the message""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self) | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parse the raw message from the device. | |||
| :param data: message data | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| try: | |||
| header, values = data.split(':') | |||
| address, channel, value = values.split(',') | |||
| self.raw = data | |||
| self.address = int(address) | |||
| self.channel = int(channel) | |||
| self.value = int(value) | |||
| except ValueError: | |||
| raise InvalidMessageError('Received invalid message: {0}'.format(data)) | |||
| if header == '!EXP': | |||
| self.type = ExpanderMessage.ZONE | |||
| elif header == '!REL': | |||
| self.type = ExpanderMessage.RELAY | |||
| 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): | |||
| """ | |||
| Represents a message from an RF receiver. | |||
| """ | |||
| serial_number = None | |||
| """Serial number of the RF device.""" | |||
| value = -1 | |||
| """Value associated with this message.""" | |||
| battery = False | |||
| """Low battery indication""" | |||
| supervision = False | |||
| """Supervision required indication""" | |||
| loop = [False for _ in list(range(4))] | |||
| """Loop indicators""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self) | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parses the raw message from the device. | |||
| :param data: message data | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| try: | |||
| self.raw = data | |||
| _, values = data.split(':') | |||
| self.serial_number, self.value = values.split(',') | |||
| self.value = int(self.value, 16) | |||
| is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0 | |||
| # Bit 1 = unknown | |||
| self.battery = is_bit_set(2) | |||
| self.supervision = is_bit_set(3) | |||
| # Bit 4 = unknown | |||
| self.loop[2] = is_bit_set(5) | |||
| self.loop[1] = is_bit_set(6) | |||
| self.loop[3] = is_bit_set(7) | |||
| self.loop[0] = is_bit_set(8) | |||
| 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): | |||
| """ | |||
| Represent a message from a Long Range Radio. | |||
| """ | |||
| event_data = None | |||
| """Data associated with the LRR message. Usually user ID or zone.""" | |||
| partition = -1 | |||
| """The partition that this message applies to.""" | |||
| event_type = None | |||
| """The type of the event that occurred.""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self) | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parses the raw message from the device. | |||
| :param data: message data to parse | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| try: | |||
| self.raw = data | |||
| _, values = data.split(':') | |||
| self.event_data, self.partition, self.event_type = values.split(',') | |||
| 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 | |||
| ) | |||
| @@ -0,0 +1,9 @@ | |||
| from .base_message import BaseMessage | |||
| from .panel_message import Message | |||
| from .expander_message import ExpanderMessage | |||
| from .lrr import LRRMessage | |||
| from .rf_message import RFMessage | |||
| from .aui_message import AUIMessage | |||
| __all__ = ['BaseMessage', 'Message', 'ExpanderMessage', 'LRRMessage', 'RFMessage', 'AUIMessage'] | |||
| @@ -0,0 +1,47 @@ | |||
| """ | |||
| Message representations received from the panel through the `AlarmDecoder`_ (AD2) | |||
| devices. | |||
| :py:class:`AUIMessage`: Message received destined for an AUI keypad. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| from . import BaseMessage | |||
| from ..util import InvalidMessageError | |||
| class AUIMessage(BaseMessage): | |||
| """ | |||
| Represents a message destined for an AUI keypad. | |||
| """ | |||
| value = None | |||
| """Raw value of the AUI message""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self, data) | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| header, value = data.split(':') | |||
| self.value = value | |||
| def dict(self, **kwargs): | |||
| """ | |||
| Dictionary representation. | |||
| """ | |||
| return dict( | |||
| value = self.value, | |||
| **kwargs | |||
| ) | |||
| @@ -0,0 +1,46 @@ | |||
| import datetime | |||
| try: | |||
| from repr import repr | |||
| except ImportError: | |||
| from repr import repr | |||
| class BaseMessage(object): | |||
| """ | |||
| Base class for messages. | |||
| """ | |||
| raw = None | |||
| """The raw message text""" | |||
| timestamp = None | |||
| """The timestamp of the message""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| """ | |||
| self.timestamp = datetime.datetime.now() | |||
| self.raw = data | |||
| def __str__(self): | |||
| """ | |||
| String conversion operator. | |||
| """ | |||
| 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()) | |||
| @@ -0,0 +1,83 @@ | |||
| """ | |||
| Message representations received from the panel through the `AlarmDecoder`_ (AD2) | |||
| devices. | |||
| :py:class:`ExpanderMessage`: Messages received from Relay or Zone expander modules. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| from . import BaseMessage | |||
| from ..util import InvalidMessageError | |||
| class ExpanderMessage(BaseMessage): | |||
| """ | |||
| Represents a message from a zone or relay expansion module. | |||
| """ | |||
| ZONE = 0 | |||
| """Flag indicating that the expander message relates to a Zone Expander.""" | |||
| 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 | |||
| """Address of expander""" | |||
| channel = -1 | |||
| """Channel on the expander""" | |||
| value = -1 | |||
| """Value associated with the message""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self, data) | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parse the raw message from the device. | |||
| :param data: message data | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| try: | |||
| header, values = data.split(':') | |||
| address, channel, value = values.split(',') | |||
| self.address = int(address) | |||
| self.channel = int(channel) | |||
| self.value = int(value) | |||
| except ValueError: | |||
| raise InvalidMessageError('Received invalid message: {0}'.format(data)) | |||
| if header == '!EXP': | |||
| self.type = ExpanderMessage.ZONE | |||
| elif header == '!REL': | |||
| self.type = ExpanderMessage.RELAY | |||
| 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 | |||
| ) | |||
| @@ -0,0 +1,9 @@ | |||
| from .message import LRRMessage | |||
| from .system import LRRSystem | |||
| from .events import get_event_description, get_event_source, LRR_EVENT_TYPE, LRR_EVENT_STATUS, LRR_CID_EVENT, LRR_DSC_EVENT, LRR_ADEMCO_EVENT, \ | |||
| LRR_ALARMDECODER_EVENT, LRR_UNKNOWN_EVENT, LRR_CID_MAP, LRR_DSC_MAP, LRR_ADEMCO_MAP, \ | |||
| LRR_ALARMDECODER_MAP, LRR_UNKNOWN_MAP | |||
| __all__ = ['get_event_description', 'get_event_source', 'LRRMessage', 'LRR_EVENT_TYPE', 'LRR_EVENT_STATUS', 'LRR_CID_EVENT', 'LRR_DSC_EVENT', | |||
| 'LRR_ADEMCO_EVENT', 'LRR_ALARMDECODER_EVENT', 'LRR_UNKNOWN_EVENT', 'LRR_CID_MAP', | |||
| 'LRR_DSC_MAP', 'LRR_ADEMCO_MAP', 'LRR_ALARMDECODER_MAP', 'LRR_UNKNOWN_MAP'] | |||
| @@ -0,0 +1,819 @@ | |||
| """ | |||
| Constants and utility functions used for LRR event handling. | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| def get_event_description(event_type, event_code): | |||
| """ | |||
| Retrieves the human-readable description of an LRR event. | |||
| :param event_type: Base LRR event type. Use LRR_EVENT_TYPE.* | |||
| :type event_type: int | |||
| :param event_code: LRR event code | |||
| :type event_code: int | |||
| :returns: string | |||
| """ | |||
| description = 'Unknown' | |||
| lookup_map = LRR_TYPE_MAP.get(event_type, None) | |||
| if lookup_map is not None: | |||
| description = lookup_map.get(event_code, description) | |||
| return description | |||
| def get_event_source(prefix): | |||
| """ | |||
| Retrieves the LRR_EVENT_TYPE corresponding to the prefix provided.abs | |||
| :param prefix: Prefix to convert to event type | |||
| :type prefix: string | |||
| :returns: int | |||
| """ | |||
| source = LRR_EVENT_TYPE.UNKNOWN | |||
| if prefix == 'CID': | |||
| source = LRR_EVENT_TYPE.CID | |||
| elif prefix == 'DSC': | |||
| source = LRR_EVENT_TYPE.DSC | |||
| elif prefix == 'AD2': | |||
| source = LRR_EVENT_TYPE.ALARMDECODER | |||
| elif prefix == 'ADEMCO': | |||
| source = LRR_EVENT_TYPE.ADEMCO | |||
| return source | |||
| class LRR_EVENT_TYPE: | |||
| """ | |||
| Base LRR event types | |||
| """ | |||
| CID = 1 | |||
| DSC = 2 | |||
| ADEMCO = 3 | |||
| ALARMDECODER = 4 | |||
| UNKNOWN = 5 | |||
| class LRR_EVENT_STATUS: | |||
| """ | |||
| LRR event status codes | |||
| """ | |||
| TRIGGER = 1 | |||
| RESTORE = 3 | |||
| class LRR_CID_EVENT: | |||
| """ | |||
| ContactID event codes | |||
| """ | |||
| MEDICAL = 0x100 | |||
| MEDICAL_PENDANT = 0x101 | |||
| MEDICAL_FAIL_TO_REPORT = 0x102 | |||
| # 103-108: ? | |||
| TAMPER_ZONE = 0x109 # NOTE: Where did we find this? | |||
| FIRE = 0x110 | |||
| FIRE_SMOKE = 0x111 | |||
| FIRE_COMBUSTION = 0x112 | |||
| FIRE_WATER_FLOW = 0x113 | |||
| FIRE_HEAT = 0x114 | |||
| FIRE_PULL_STATION = 0x115 | |||
| FIRE_DUCT = 0x116 | |||
| FIRE_FLAME = 0x117 | |||
| FIRE_NEAR_ALARM = 0x118 | |||
| PANIC = 0x120 | |||
| PANIC_DURESS = 0x121 | |||
| PANIC_SILENT = 0x122 | |||
| PANIC_AUDIBLE = 0x123 | |||
| PANIC_DURESS_ACCESS_GRANTED = 0x124 | |||
| PANIC_DURESS_EGRESS_GRANTED = 0x125 | |||
| PANIC_HOLDUP_SUSPICION = 0x126 | |||
| # 127-128: ? | |||
| PANIC_HOLDUP_VERIFIER = 0x129 | |||
| BURGLARY = 0x130 | |||
| BURGLARY_PERIMETER = 0x131 | |||
| BURGLARY_INTERIOR = 0x132 | |||
| BURGLARY_AUX = 0x133 | |||
| BURGLARY_ENTRYEXIT = 0x134 | |||
| BURGLARY_DAYNIGHT = 0x135 | |||
| BURGLARY_OUTDOOR = 0x136 | |||
| BURGLARY_TAMPER = 0x137 | |||
| BURGLARY_NEAR_ALARM = 0x138 | |||
| BURGLARY_INTRUSION_VERIFIER = 0x139 | |||
| ALARM_GENERAL = 0x140 | |||
| ALARM_POLLING_LOOP_OPEN = 0x141 | |||
| ALARM_POLLING_LOOP_SHORT = 0x142 | |||
| ALARM_EXPANSION_MOD_FAILURE = 0x143 | |||
| ALARM_SENSOR_TAMPER = 0x144 | |||
| ALARM_EXPANSION_MOD_TAMPER = 0x145 | |||
| BURGLARY_SILENT = 0x146 | |||
| TROUBLE_SENSOR_SUPERVISION = 0x147 | |||
| # 148-149: ? | |||
| ALARM_AUX = 0x150 | |||
| ALARM_GAS_DETECTED = 0x151 | |||
| ALARM_REFRIDGERATION = 0x152 | |||
| ALARM_LOSS_OF_HEAT = 0x153 | |||
| ALARM_WATER_LEAKAGE = 0x154 | |||
| TROUBLE_FOIL_BREAK = 0x155 | |||
| TROUBLE_DAY_TROUBLE = 0x156 | |||
| ALARM_LOW_BOTTLED_GAS_LEVEL = 0x157 | |||
| ALARM_HIGH_TEMP = 0x158 | |||
| ALARM_LOW_TEMP = 0x159 | |||
| # 160: ? | |||
| ALARM_LOSS_OF_AIR_FLOW = 0x161 | |||
| ALARM_CARBON_MONOXIDE = 0x162 | |||
| TROUBLE_TANK_LEVEL = 0x163 | |||
| # 164-167: ? | |||
| TROUBLE_HIGH_HUMIDITY = 0x168 | |||
| TROUBLE_LOW_HUMIDITY = 0x169 | |||
| # 170-199: ? | |||
| SUPERVISORY_FIRE = 0x200 | |||
| SUPERVISORY_LOW_PRESSURE = 0x201 | |||
| SUPERVISORY_LOW_CO2 = 0x202 | |||
| SUPERVISORY_GATE_VALVE_SENSOR = 0x203 | |||
| SUPERVISORY_LOW_WATER_LEVEL = 0x204 | |||
| SUPERVISORY_PUMP_ACTIVATED = 0x205 | |||
| SUPERVISORY_PUMP_FAILURE = 0x206 | |||
| # 207-299: ? | |||
| TROUBLE_SYSTEM_TROUBLE = 0x300 | |||
| TROUBLE_AC_LOSS = 0x301 | |||
| TROUBLE_LOW_BATTERY = 0x302 | |||
| TROUBLE_RAM_CHECKSUM_BAD = 0x303 | |||
| TROUBLE_ROM_CHECKSUM_BAD = 0x304 | |||
| TROUBLE_RESET = 0x305 | |||
| TROUBLE_PANEL_PROGRAMMING_CHANGED = 0x306 | |||
| TROUBLE_SELF_TEST_FAILURE = 0x307 | |||
| TROUBLE_SHUTDOWN = 0x308 | |||
| TROUBLE_BATTERY_TEST_FAIL = 0x309 | |||
| TROUBLE_GROUND_FAULT = 0x310 | |||
| TROUBLE_BATTERY_MISSING = 0x311 | |||
| TROUBLE_POWER_SUPPLY_OVERCURRENT = 0x312 | |||
| STATUS_ENGINEER_RESET = 0x313 | |||
| TROUBLE_PRIMARY_POWER_SUPPLY_FAILURE = 0x314 | |||
| # 315: ? | |||
| TROUBLE_TAMPER = 0x316 | |||
| # 317-319: ? | |||
| TROUBLE_SOUNDER = 0x320 | |||
| TROUBLE_BELL_1 = 0x321 | |||
| TROUBLE_BELL_2 = 0x322 | |||
| TROUBLE_ALARM_RELAY = 0x323 | |||
| TROUBLE_TROUBLE_RELAY = 0x324 | |||
| TROUBLE_REVERSING_RELAY = 0x325 | |||
| TROUBLE_NOTIFICATION_APPLIANCE_CIRCUIT_3 = 0x326 | |||
| TROUBLE_NOTIFICATION_APPLIANCE_CIRCUIT_4 = 0x327 | |||
| # 328-329: ? | |||
| TROUBLE_SYSTEM_PERIPHERAL = 0x330 | |||
| TROUBLE_POLLING_LOOP_OPEN = 0x331 | |||
| TROUBLE_POLLING_LOOP_SHORT = 0x332 | |||
| TROUBLE_EXPANSION_MODULE_FAILURE = 0x333 | |||
| TROUBLE_REPEATER_FAILURE = 0x334 | |||
| TROUBLE_LOCAL_PRINTER_PAPER_OUT = 0x335 | |||
| TROUBLE_LOCAL_PRINTER_FAILURE = 0x336 | |||
| TROUBLE_EXPANDER_MODULE_DC_LOSS = 0x337 | |||
| TROUBLE_EXPANDER_MODULE_LOW_BATTERY = 0x338 | |||
| TROUBLE_EXPANDER_MODULE_RESET = 0x339 | |||
| # 340: ? | |||
| TROUBLE_EXPANDER_MODULE_TAMPER = 0x341 | |||
| TROUBLE_EXPANDER_MODULE_AC_LOSS = 0x342 | |||
| TROUBLE_EXPANDER_MODULE_SELF_TEST_FAIL = 0x343 | |||
| TROUBLE_RF_RECEIVER_JAM_DETECTED = 0x344 | |||
| TROUBLE_AES_ENCRYPTION = 0x345 | |||
| # 346-349: ? | |||
| TROUBLE_COMMUNICATION = 0x350 | |||
| TROUBLE_TELCO_1_FAULT = 0x351 | |||
| TROUBLE_TELCO_2_FAULT = 0x352 | |||
| TROUBLE_LRR_TRANSMITTER_FAULT = 0x353 | |||
| TROUBLE_FAILURE_TO_COMMUNICATE = 0x354 | |||
| TROUBLE_LOSS_OF_RADIO_SUPERVISION = 0x355 | |||
| TROUBLE_LOSS_OF_CENTRAL_POLLING = 0x356 | |||
| TROUBLE_LRR_TRANSMITTER_VSWR = 0x357 | |||
| TROUBLE_PERIODIC_COMM_TEST = 0x358 | |||
| # 359-369: ? | |||
| TROUBLE_PROTECTION_LOOP = 0x370 | |||
| TROUBLE_PROTECTION_LOOP_OPEN = 0x371 | |||
| TROUBLE_PROTECTION_LOOP_SHORT = 0x372 | |||
| TROUBLE_FIRE = 0x373 | |||
| TROUBLE_EXIT_ERROR = 0x374 | |||
| TROUBLE_PANIC_ZONE_TROUBLE = 0x375 | |||
| TROUBLE_HOLDUP_ZONE_TROUBLE = 0x376 | |||
| TROUBLE_SWINGER_TROUBLE = 0x377 | |||
| TROUBLE_CROSS_ZONE_TROUBLE = 0x378 | |||
| # 379: ? | |||
| TROUBLE_SENSOR_TROUBLE = 0x380 | |||
| TROUBLE_RF_LOSS_OF_SUPERVISION = 0x381 | |||
| TROUBLE_RPM_LOSS_OF_SUPERVISION = 0x382 | |||
| TROUBLE_SENSOR_TAMPER = 0x383 | |||
| TROUBLE_RF_LOW_BATTERY = 0x384 | |||
| TROUBLE_SMOKE_HI_SENS = 0x385 | |||
| TROUBLE_SMOKE_LO_SENS = 0x386 | |||
| TROUBLE_INTRUSION_HI_SENS = 0x387 | |||
| TROUBLE_INTRUSION_LO_SENS = 0x388 | |||
| TROUBLE_SELF_TEST_FAIL = 0x389 | |||
| # 390: ? | |||
| TROUBLE_SENSOR_WATCH_FAIL = 0x391 | |||
| TROUBLE_DRIFT_COMP_ERROR = 0x392 | |||
| TROUBLE_MAINTENANCE_ALERT = 0x393 | |||
| # 394-399: ? | |||
| OPENCLOSE = 0x400 | |||
| OPENCLOSE_BY_USER = 0x401 | |||
| OPENCLOSE_GROUP = 0x402 | |||
| OPENCLOSE_AUTOMATIC = 0x403 | |||
| OPENCLOSE_LATE = 0x404 | |||
| OPENCLOSE_DEFERRED = 0x405 | |||
| OPENCLOSE_CANCEL_BY_USER = 0x406 | |||
| OPENCLOSE_REMOTE_ARMDISARM = 0x407 | |||
| OPENCLOSE_QUICK_ARM = 0x408 | |||
| OPENCLOSE_KEYSWITCH = 0x409 | |||
| # 410: ? | |||
| REMOTE_CALLBACK_REQUESTED = 0x411 | |||
| REMOTE_SUCCESS = 0x412 | |||
| REMOTE_UNSUCCESSFUL = 0x413 | |||
| REMOTE_SYSTEM_SHUTDOWN = 0x414 | |||
| REMOTE_DIALER_SHUTDOWN = 0x415 | |||
| REMOTE_SUCCESSFUL_UPLOAD = 0x416 | |||
| # 417-420: ? | |||
| ACCESS_DENIED = 0x421 | |||
| ACCESS_REPORT_BY_USER = 0x422 | |||
| ACCESS_FORCED_ACCESS = 0x423 | |||
| ACCESS_EGRESS_DENIED = 0x424 | |||
| ACCESS_EGRESS_GRANTED = 0x425 | |||
| ACCESS_DOOR_PROPPED_OPEN = 0x426 | |||
| ACCESS_POINT_DSM_TROUBLE = 0x427 | |||
| ACCESS_POINT_RTE_TROUBLE = 0x428 | |||
| ACCESS_PROGRAM_MODE_ENTRY = 0x429 | |||
| ACCESS_PROGRAM_MODE_EXIT = 0x430 | |||
| ACCESS_THREAT_LEVEL_CHANGE = 0x431 | |||
| ACCESS_RELAY_FAIL = 0x432 | |||
| ACCESS_RTE_SHUNT = 0x433 | |||
| ACCESS_DSM_SHUNT = 0x434 | |||
| ACCESS_SECOND_PERSON = 0x435 | |||
| ACCESS_IRREGULAR_ACCESS = 0x436 | |||
| # 437-440: ? | |||
| OPENCLOSE_ARMED_STAY = 0x441 | |||
| OPENCLOSE_KEYSWITCH_ARMED_STAY = 0x442 | |||
| # 443-449: ? | |||
| OPENCLOSE_EXCEPTION = 0x450 | |||
| OPENCLOSE_EARLY = 0x451 | |||
| OPENCLOSE_LATE = 0x452 | |||
| TROUBLE_FAILED_TO_OPEN = 0x453 | |||
| TROUBLE_FAILED_TO_CLOSE = 0x454 | |||
| TROUBLE_AUTO_ARM_FAILED = 0x455 | |||
| OPENCLOSE_PARTIAL_ARM = 0x456 | |||
| OPENCLOSE_EXIT_ERROR = 0x457 | |||
| OPENCLOSE_USER_ON_PREMISES = 0x458 | |||
| TROUBLE_RECENT_CLOSE = 0x459 | |||
| # 460: ? | |||
| ACCESS_WRONG_CODE_ENTRY = 0x461 | |||
| ACCESS_LEGAL_CODE_ENTRY = 0x462 | |||
| STATUS_REARM_AFTER_ALARM = 0x463 | |||
| STATUS_AUTO_ARM_TIME_EXTENDED = 0x464 | |||
| STATUS_PANIC_ALARM_RESET = 0x465 | |||
| ACCESS_SERVICE_ONOFF_PREMISES = 0x466 | |||
| # 467-469: ? | |||
| OPENCLOSE_PARTIAL_CLOSING = 0x470 # HACK: This is from our DSC firmware implementation, | |||
| # and is named far too closely to 0x480. | |||
| # 471-479: ? | |||
| OPENCLOSE_PARTIAL_CLOSE = 0x480 | |||
| # 481-500: ? | |||
| DISABLE_ACCESS_READER = 0x501 | |||
| # 502-519: ? | |||
| DISABLE_SOUNDER = 0x520 | |||
| DISABLE_BELL_1 = 0x521 | |||
| DISABLE_BELL_2 = 0x522 | |||
| DISABLE_ALARM_RELAY = 0x523 | |||
| DISABLE_TROUBLE_RELAY = 0x524 | |||
| DISABLE_REVERSING_RELAY = 0x525 | |||
| DISABLE_NOTIFICATION_APPLIANCE_CIRCUIT_3 = 0x526 | |||
| DISABLE_NOTIFICATION_APPLIANCE_CIRCUIT_4 = 0x527 | |||
| # 528-530: ? | |||
| SUPERVISORY_MODULE_ADDED = 0x531 | |||
| SUPERVISORY_MODULE_REMOVED = 0x532 | |||
| # 533-550: ? | |||
| DISABLE_DIALER = 0x551 | |||
| DISABLE_RADIO_TRANSMITTER = 0x552 | |||
| DISABLE_REMOTE_UPLOADDOWNLOAD = 0x553 | |||
| # 554-569: ? | |||
| BYPASS_ZONE = 0x570 | |||
| BYPASS_FIRE = 0x571 | |||
| BYPASS_24HOUR_ZONE = 0x572 | |||
| BYPASS_BURGLARY = 0x573 | |||
| BYPASS_GROUP = 0x574 | |||
| BYPASS_SWINGER = 0x575 | |||
| BYPASS_ACCESS_ZONE_SHUNT = 0x576 | |||
| BYPASS_ACCESS_POINT_BYPASS = 0x577 | |||
| BYPASS_ZONE_VAULT = 0x578 | |||
| BYPASS_ZONE_VENT = 0x579 | |||
| # 580-600: ? | |||
| TEST_MANUAL = 0x601 | |||
| TEST_PERIODIC = 0x602 | |||
| TEST_PERIODIC_RF_TRANSMISSION = 0x603 | |||
| TEST_FIRE = 0x604 | |||
| TEST_FIRE_STATUS = 0x605 | |||
| TEST_LISTENIN_TO_FOLLOW = 0x606 | |||
| TEST_WALK = 0x607 | |||
| TEST_SYSTEM_TROUBLE_PRESENT = 0x608 | |||
| TEST_VIDEO_TRANSMITTER_ACTIVE = 0x609 | |||
| # 610: ? | |||
| TEST_POINT_TESTED_OK = 0x611 | |||
| TEST_POINT_NOT_TESTED = 0x612 | |||
| TEST_INTRUSION_ZONE_WALK_TESTED = 0x613 | |||
| TEST_FIRE_ZONE_WALK_TESTED = 0x614 | |||
| TEST_PANIC_ZONE_WALK_TESTED = 0x615 | |||
| TROUBLE_SERVICE_REQUEST = 0x616 | |||
| # 617-620: ? | |||
| TROUBLE_EVENT_LOG_RESET = 0x621 | |||
| TROUBLE_EVENT_LOG_50PERCENT_FULL = 0x622 | |||
| TROUBLE_EVENT_LOG_90PERCENT_FULL = 0x623 | |||
| TROUBLE_EVENT_LOG_OVERFLOW = 0x624 | |||
| TROUBLE_TIMEDATE_RESET = 0x625 | |||
| TROUBLE_TIMEDATE_INACCURATE = 0x626 | |||
| TROUBLE_PROGRAM_MODE_ENTRY = 0x627 | |||
| TROUBLE_PROGRAM_MODE_EXIT = 0x628 | |||
| TROUBLE_32HOUR_EVENT_LOG_MARKER = 0x629 | |||
| SCHEDULE_CHANGE = 0x630 | |||
| SCHEDULE_EXCEPTION_SCHEDULE_CHANGE = 0x631 | |||
| SCHEDULE_ACCESS_SCHEDULE_CHANGE = 0x632 | |||
| # 633-640: ? | |||
| TROUBLE_SENIOR_WATCH_TROUBLE = 0x641 | |||
| STATUS_LATCHKEY_SUPERVISION = 0x642 | |||
| # 643-650: ? | |||
| SPECIAL_ADT_AUTHORIZATION = 0x651 | |||
| RESERVED_652 = 0x652 | |||
| RESERVED_653 = 0x653 | |||
| TROUBLE_SYSTEM_INACTIVITY = 0x654 | |||
| # 750-789: User Assigned | |||
| # 790-795: ? | |||
| TROUBLE_UNABLE_TO_OUTPUT_SIGNAL = 0x796 | |||
| # 797: ? | |||
| TROUBLE_STU_CONTROLLER_DOWN = 0x798 | |||
| # 799-899: ? | |||
| REMOTE_DOWNLOAD_ABORT = 0x900 | |||
| REMOTE_DOWNLOAD_STARTEND = 0x901 | |||
| REMOTE_DOWNLOAD_INTERRUPTED = 0x902 | |||
| REMOTE_CODE_DOWNLOAD_STARTEND = 0x903 | |||
| REMOTE_CODE_DOWNLOAD_FAILED = 0x904 | |||
| # 905-909: ? | |||
| OPENCLOSE_AUTOCLOSE_WITH_BYPASS = 0x910 | |||
| OPENCLOSE_BYPASS_CLOSING = 0x911 | |||
| EVENT_FIRE_ALARM_SILENCED = 0x912 | |||
| EVENT_SUPERVISOR_POINT_STARTEND = 0x913 | |||
| EVENT_HOLDUP_TEST_STARTEND = 0x914 | |||
| EVENT_BURGLARY_TEST_PRINT_STARTEND = 0x915 | |||
| EVENT_SUPERVISORY_TEST_PRINT_STARTEND = 0x916 | |||
| EVENT_BURGLARY_DIAGNOSTICS_STARTEND = 0x917 | |||
| EVENT_FIRE_DIAGNOSTICS_STARTEND = 0x918 | |||
| EVENT_UNTYPED_DIAGNOSTICS = 0x919 | |||
| EVENT_TROUBLE_CLOSING = 0x920 | |||
| EVENT_ACCESS_DENIED_CODE_UNKNOWN = 0x921 | |||
| ALARM_SUPERVISORY_POINT = 0x922 | |||
| EVENT_SUPERVISORY_POINT_BYPASS = 0x923 | |||
| TROUBLE_SUPERVISORY_POINT = 0x924 | |||
| EVENT_HOLDUP_POINT_BYPASS = 0x925 | |||
| EVENT_AC_FAILURE_FOR_4HOURS = 0x926 | |||
| TROUBLE_OUTPUT = 0x927 | |||
| EVENT_USER_CODE_FOR_EVENT = 0x928 | |||
| EVENT_LOG_OFF = 0x929 | |||
| # 930-953: ? | |||
| EVENT_CS_CONNECTION_FAILURE = 0x954 | |||
| # 955-960: ? | |||
| EVENT_RECEIVER_DATABASE_CONNECTION = 0x961 | |||
| EVENT_LICENSE_EXPIRATION = 0x962 | |||
| # 963-998: ? | |||
| OTHER_NO_READ_LOG = 0x999 | |||
| class LRR_DSC_EVENT: | |||
| """ | |||
| DSC event codes | |||
| """ | |||
| ZONE_EXPANDER_SUPERVISORY_ALARM = 0x04c | |||
| ZONE_EXPANDER_SUPERVISORY_RESTORE = 0x04d | |||
| AUX_INPUT_ALARM = 0x051 | |||
| SPECIAL_CLOSING = 0x0bf | |||
| CROSS_ZONE_POLICE_CODE_ALARM = 0x103 | |||
| AUTOMATIC_CLOSING = 0x12b | |||
| ZONE_BYPASS = 0x570 | |||
| REPORT_DSC_USER_LOG_EVENT = 0x800 | |||
| class LRR_ADEMCO_EVENT: | |||
| """ | |||
| ADEMCO event codes | |||
| """ | |||
| pass | |||
| class LRR_ALARMDECODER_EVENT: | |||
| """ | |||
| AlarmDecoder event codes | |||
| """ | |||
| CUSTOM_PROG_MSG = 0x0 | |||
| CUSTOM_PROG_KEY = 0x1 | |||
| class LRR_UNKNOWN_EVENT: | |||
| """ | |||
| Unknown event codes. Realistically there shouldn't ever be anything here. | |||
| """ | |||
| pass | |||
| # Map of ContactID event codes to human-readable text. | |||
| LRR_CID_MAP = { | |||
| LRR_CID_EVENT.MEDICAL: 'Medical Emergency: Non-specific', | |||
| LRR_CID_EVENT.MEDICAL_PENDANT: 'Emergency Assistance Request', | |||
| LRR_CID_EVENT.MEDICAL_FAIL_TO_REPORT: 'Medical: Failed to activate monitoring device', | |||
| LRR_CID_EVENT.TAMPER_ZONE: 'Zone Tamper', | |||
| LRR_CID_EVENT.FIRE: 'Fire: Non-specific', | |||
| LRR_CID_EVENT.FIRE_SMOKE: 'Fire: Smoke Alarm', | |||
| LRR_CID_EVENT.FIRE_COMBUSTION: 'Fire: Combustion', | |||
| LRR_CID_EVENT.FIRE_WATER_FLOW: 'Fire: Water Flow', | |||
| LRR_CID_EVENT.FIRE_HEAT: 'Fire: Heat', | |||
| LRR_CID_EVENT.FIRE_PULL_STATION: 'Fire: Pull Station', | |||
| LRR_CID_EVENT.FIRE_DUCT: 'Fire: Duct', | |||
| LRR_CID_EVENT.FIRE_FLAME: 'Fire: Flame', | |||
| LRR_CID_EVENT.FIRE_NEAR_ALARM: 'Fire: Near Alarm', | |||
| LRR_CID_EVENT.PANIC: 'Panic', | |||
| LRR_CID_EVENT.PANIC_DURESS: 'Panic: Duress', | |||
| LRR_CID_EVENT.PANIC_SILENT: 'Panic: Silent', | |||
| LRR_CID_EVENT.PANIC_AUDIBLE: 'Panic: Audible', | |||
| LRR_CID_EVENT.PANIC_DURESS_ACCESS_GRANTED: 'Fire: Duress', | |||
| LRR_CID_EVENT.PANIC_DURESS_EGRESS_GRANTED: 'Fire: Egress', | |||
| LRR_CID_EVENT.PANIC_HOLDUP_SUSPICION: 'Panic: Hold-up, Suspicious Condition', | |||
| LRR_CID_EVENT.PANIC_HOLDUP_VERIFIER: 'Panic: Hold-up Verified', | |||
| LRR_CID_EVENT.BURGLARY: 'Burglary', | |||
| LRR_CID_EVENT.BURGLARY_PERIMETER: 'Burglary: Perimeter', | |||
| LRR_CID_EVENT.BURGLARY_INTERIOR: 'Burglary: Interior', | |||
| LRR_CID_EVENT.BURGLARY_AUX: 'Burglary: 24 Hour', | |||
| LRR_CID_EVENT.BURGLARY_ENTRYEXIT: 'Burglary: Entry/Exit', | |||
| LRR_CID_EVENT.BURGLARY_DAYNIGHT: 'Burglary: Day/Night', | |||
| LRR_CID_EVENT.BURGLARY_OUTDOOR: 'Burglary: Outdoor', | |||
| LRR_CID_EVENT.BURGLARY_TAMPER: 'Burglary: Tamper', | |||
| LRR_CID_EVENT.BURGLARY_NEAR_ALARM: 'Burglary: Near Alarm', | |||
| LRR_CID_EVENT.BURGLARY_INTRUSION_VERIFIER: 'Burglary: Intrusion Verifier', | |||
| LRR_CID_EVENT.ALARM_GENERAL: 'Alarm: General', | |||
| LRR_CID_EVENT.ALARM_POLLING_LOOP_OPEN: 'Alarm: Polling Loop Open', | |||
| LRR_CID_EVENT.ALARM_POLLING_LOOP_SHORT: 'Alarm: Polling Loop Closed', | |||
| LRR_CID_EVENT.ALARM_EXPANSION_MOD_FAILURE: 'Alarm: Expansion Module Failure', | |||
| LRR_CID_EVENT.ALARM_SENSOR_TAMPER: 'Alarm: Sensor Tamper', | |||
| LRR_CID_EVENT.ALARM_EXPANSION_MOD_TAMPER: 'Alarm: Expansion Module Tamper', | |||
| LRR_CID_EVENT.BURGLARY_SILENT: 'Burglary: Silent', | |||
| LRR_CID_EVENT.TROUBLE_SENSOR_SUPERVISION: 'Trouble: Sensor Supervision Failure', | |||
| LRR_CID_EVENT.ALARM_AUX: 'Alarm: 24 Hour Non-Burglary', | |||
| LRR_CID_EVENT.ALARM_GAS_DETECTED: 'Alarm: Gas Detected', | |||
| LRR_CID_EVENT.ALARM_REFRIDGERATION: 'Alarm: Refridgeration', | |||
| LRR_CID_EVENT.ALARM_LOSS_OF_HEAT: 'Alarm: Loss of Heat', | |||
| LRR_CID_EVENT.ALARM_WATER_LEAKAGE: 'Alarm: Water Leakage', | |||
| LRR_CID_EVENT.TROUBLE_FOIL_BREAK: 'Trouble: Foil Break', | |||
| LRR_CID_EVENT.TROUBLE_DAY_TROUBLE: 'Trouble: Day Trouble', | |||
| LRR_CID_EVENT.ALARM_LOW_BOTTLED_GAS_LEVEL: 'Alarm: Low Bottled Gas Level', | |||
| LRR_CID_EVENT.ALARM_HIGH_TEMP: 'Alarm: High Temperature', | |||
| LRR_CID_EVENT.ALARM_LOW_TEMP: 'Alarm: Low Temperature', | |||
| LRR_CID_EVENT.ALARM_LOSS_OF_AIR_FLOW: 'Alarm: Loss of Air Flow', | |||
| LRR_CID_EVENT.ALARM_CARBON_MONOXIDE: 'Alarm: Carbon Monoxide', | |||
| LRR_CID_EVENT.TROUBLE_TANK_LEVEL: 'Trouble: Tank Level', | |||
| LRR_CID_EVENT.TROUBLE_HIGH_HUMIDITY: 'Trouble: High Humidity', | |||
| LRR_CID_EVENT.TROUBLE_LOW_HUMIDITY: 'Trouble: Low Humidity', | |||
| LRR_CID_EVENT.SUPERVISORY_FIRE: 'Supervisory: Fire', | |||
| LRR_CID_EVENT.SUPERVISORY_LOW_PRESSURE: 'Supervisory: Low Water Pressure', | |||
| LRR_CID_EVENT.SUPERVISORY_LOW_CO2: 'Supervisory: Low CO2', | |||
| LRR_CID_EVENT.SUPERVISORY_GATE_VALVE_SENSOR: 'Supervisory: Gate Valve Sensor', | |||
| LRR_CID_EVENT.SUPERVISORY_LOW_WATER_LEVEL: 'Supervisory: Low Water Level', | |||
| LRR_CID_EVENT.SUPERVISORY_PUMP_ACTIVATED: 'Supervisory: Pump Activated', | |||
| LRR_CID_EVENT.SUPERVISORY_PUMP_FAILURE: 'Supervisory: Pump Failure', | |||
| LRR_CID_EVENT.TROUBLE_SYSTEM_TROUBLE: 'Trouble: System Trouble', | |||
| LRR_CID_EVENT.TROUBLE_AC_LOSS: 'Trouble: AC Loss', | |||
| LRR_CID_EVENT.TROUBLE_LOW_BATTERY: 'Trouble: Low Battery', | |||
| LRR_CID_EVENT.TROUBLE_RAM_CHECKSUM_BAD: 'Trouble: RAM Checksum Bad', | |||
| LRR_CID_EVENT.TROUBLE_ROM_CHECKSUM_BAD: 'Trouble: ROM Checksum Bad', | |||
| LRR_CID_EVENT.TROUBLE_RESET: 'Trouble: System Reset', | |||
| LRR_CID_EVENT.TROUBLE_PANEL_PROGRAMMING_CHANGED: 'Trouble: Panel Programming Changed', | |||
| LRR_CID_EVENT.TROUBLE_SELF_TEST_FAILURE: 'Trouble: Self-Test Failure', | |||
| LRR_CID_EVENT.TROUBLE_SHUTDOWN: 'Trouble: System Shutdown', | |||
| LRR_CID_EVENT.TROUBLE_BATTERY_TEST_FAIL: 'Trouble: Battery Test Failure', | |||
| LRR_CID_EVENT.TROUBLE_GROUND_FAULT: 'Trouble: Ground Fault', | |||
| LRR_CID_EVENT.TROUBLE_BATTERY_MISSING: 'Trouble: Battery Missing', | |||
| LRR_CID_EVENT.TROUBLE_POWER_SUPPLY_OVERCURRENT: 'Trouble: Power Supply Overcurrent', | |||
| LRR_CID_EVENT.STATUS_ENGINEER_RESET: 'Status: Engineer Reset', | |||
| LRR_CID_EVENT.TROUBLE_PRIMARY_POWER_SUPPLY_FAILURE: 'Trouble: Primary Power Supply Failure', | |||
| LRR_CID_EVENT.TROUBLE_TAMPER: 'Trouble: System Tamper', | |||
| LRR_CID_EVENT.TROUBLE_SOUNDER: 'Trouble: Sounder', | |||
| LRR_CID_EVENT.TROUBLE_BELL_1: 'Trouble: Bell 1', | |||
| LRR_CID_EVENT.TROUBLE_BELL_2: 'Trouble: Bell 2', | |||
| LRR_CID_EVENT.TROUBLE_ALARM_RELAY: 'Trouble: Alarm Relay', | |||
| LRR_CID_EVENT.TROUBLE_TROUBLE_RELAY: 'Trouble: Trouble Relay', | |||
| LRR_CID_EVENT.TROUBLE_REVERSING_RELAY: 'Trouble: Reversing Relay', | |||
| LRR_CID_EVENT.TROUBLE_NOTIFICATION_APPLIANCE_CIRCUIT_3: 'Trouble: Notification Appliance Circuit #3', | |||
| LRR_CID_EVENT.TROUBLE_NOTIFICATION_APPLIANCE_CIRCUIT_4: 'Trouble: Notification Appliance Circuit #3', | |||
| LRR_CID_EVENT.TROUBLE_SYSTEM_PERIPHERAL: 'Trouble: System Peripheral', | |||
| LRR_CID_EVENT.TROUBLE_POLLING_LOOP_OPEN: 'Trouble: Pooling Loop Open', | |||
| LRR_CID_EVENT.TROUBLE_POLLING_LOOP_SHORT: 'Trouble: Polling Loop Short', | |||
| LRR_CID_EVENT.TROUBLE_EXPANSION_MODULE_FAILURE: 'Trouble: Expansion Module Failure', | |||
| LRR_CID_EVENT.TROUBLE_REPEATER_FAILURE: 'Trouble: Repeater Failure', | |||
| LRR_CID_EVENT.TROUBLE_LOCAL_PRINTER_PAPER_OUT: 'Trouble: Local Printer Out Of Paper', | |||
| LRR_CID_EVENT.TROUBLE_LOCAL_PRINTER_FAILURE: 'Trouble: Local Printer Failure', | |||
| LRR_CID_EVENT.TROUBLE_EXPANDER_MODULE_DC_LOSS: 'Trouble: Expander Module, DC Power Loss', | |||
| LRR_CID_EVENT.TROUBLE_EXPANDER_MODULE_LOW_BATTERY: 'Trouble: Expander Module, Low Battery', | |||
| LRR_CID_EVENT.TROUBLE_EXPANDER_MODULE_RESET: 'Trouble: Expander Module, Reset', | |||
| LRR_CID_EVENT.TROUBLE_EXPANDER_MODULE_TAMPER: 'Trouble: Expander Module, Tamper', | |||
| LRR_CID_EVENT.TROUBLE_EXPANDER_MODULE_AC_LOSS: 'Trouble: Expander Module, AC Power Loss', | |||
| LRR_CID_EVENT.TROUBLE_EXPANDER_MODULE_SELF_TEST_FAIL: 'Trouble: Expander Module, Self-test Failure', | |||
| LRR_CID_EVENT.TROUBLE_RF_RECEIVER_JAM_DETECTED: 'Trouble: RF Receiver Jam Detected', | |||
| LRR_CID_EVENT.TROUBLE_AES_ENCRYPTION: 'Trouble: AES Encryption', | |||
| LRR_CID_EVENT.TROUBLE_COMMUNICATION: 'Trouble: Communication', | |||
| LRR_CID_EVENT.TROUBLE_TELCO_1_FAULT: 'Trouble: Telco 1', | |||
| LRR_CID_EVENT.TROUBLE_TELCO_2_FAULT: 'Trouble: Telco 2', | |||
| LRR_CID_EVENT.TROUBLE_LRR_TRANSMITTER_FAULT: 'Trouble: Long Range Radio Transmitter Fault', | |||
| LRR_CID_EVENT.TROUBLE_FAILURE_TO_COMMUNICATE: 'Trouble: Failure To Communicate', | |||
| LRR_CID_EVENT.TROUBLE_LOSS_OF_RADIO_SUPERVISION: 'Trouble: Loss of Radio Supervision', | |||
| LRR_CID_EVENT.TROUBLE_LOSS_OF_CENTRAL_POLLING: 'Trouble: Loss of Central Polling', | |||
| LRR_CID_EVENT.TROUBLE_LRR_TRANSMITTER_VSWR: 'Trouble: Long Range Radio Transmitter/Antenna', | |||
| LRR_CID_EVENT.TROUBLE_PERIODIC_COMM_TEST: 'Trouble: Periodic Communication Test', | |||
| LRR_CID_EVENT.TROUBLE_PROTECTION_LOOP: 'Trouble: Protection Loop', | |||
| LRR_CID_EVENT.TROUBLE_PROTECTION_LOOP_OPEN: 'Trouble: Protection Loop Open', | |||
| LRR_CID_EVENT.TROUBLE_PROTECTION_LOOP_SHORT: 'Trouble: Protection Loop Short', | |||
| LRR_CID_EVENT.TROUBLE_FIRE: 'Trouble: Fire', | |||
| LRR_CID_EVENT.TROUBLE_EXIT_ERROR: 'Trouble: Exit Error', | |||
| LRR_CID_EVENT.TROUBLE_PANIC_ZONE_TROUBLE: 'Trouble: Panic', | |||
| LRR_CID_EVENT.TROUBLE_HOLDUP_ZONE_TROUBLE: 'Trouble: Hold-up', | |||
| LRR_CID_EVENT.TROUBLE_SWINGER_TROUBLE: 'Trouble: Swinger', | |||
| LRR_CID_EVENT.TROUBLE_CROSS_ZONE_TROUBLE: 'Trouble: Cross-zone', | |||
| LRR_CID_EVENT.TROUBLE_SENSOR_TROUBLE: 'Trouble: Sensor', | |||
| LRR_CID_EVENT.TROUBLE_RF_LOSS_OF_SUPERVISION: 'Trouble: RF Loss of Supervision', | |||
| LRR_CID_EVENT.TROUBLE_RPM_LOSS_OF_SUPERVISION: 'Trouble: RPM Loss of Supervision', | |||
| LRR_CID_EVENT.TROUBLE_SENSOR_TAMPER: 'Trouble: Sensor Tamper', | |||
| LRR_CID_EVENT.TROUBLE_RF_LOW_BATTERY: 'Trouble: RF Low Battery', | |||
| LRR_CID_EVENT.TROUBLE_SMOKE_HI_SENS: 'Trouble: Smoke Detector, High Sensitivity', | |||
| LRR_CID_EVENT.TROUBLE_SMOKE_LO_SENS: 'Trouble: Smoke Detector, Low Sensitivity', | |||
| LRR_CID_EVENT.TROUBLE_INTRUSION_HI_SENS: 'Trouble: Intrusion Detector, High Sensitivity', | |||
| LRR_CID_EVENT.TROUBLE_INTRUSION_LO_SENS: 'Trouble: Intrusion Detector, Low Sensitivity', | |||
| LRR_CID_EVENT.TROUBLE_SELF_TEST_FAIL: 'Trouble: Self-test Failure', | |||
| LRR_CID_EVENT.TROUBLE_SENSOR_WATCH_FAIL: 'Trouble: Sensor Watch', | |||
| LRR_CID_EVENT.TROUBLE_DRIFT_COMP_ERROR: 'Trouble: Drift Compensation Error', | |||
| LRR_CID_EVENT.TROUBLE_MAINTENANCE_ALERT: 'Trouble: Maintenance Alert', | |||
| LRR_CID_EVENT.OPENCLOSE: 'Open/Close', | |||
| LRR_CID_EVENT.OPENCLOSE_BY_USER: 'Open/Close: By User', | |||
| LRR_CID_EVENT.OPENCLOSE_GROUP: 'Open/Close: Group', | |||
| LRR_CID_EVENT.OPENCLOSE_AUTOMATIC: 'Open/Close: Automatic', | |||
| LRR_CID_EVENT.OPENCLOSE_LATE: 'Open/Close: Late', | |||
| LRR_CID_EVENT.OPENCLOSE_DEFERRED: 'Open/Close: Deferred', | |||
| LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: 'Open/Close: Cancel', | |||
| LRR_CID_EVENT.OPENCLOSE_REMOTE_ARMDISARM: 'Open/Close: Remote', | |||
| LRR_CID_EVENT.OPENCLOSE_QUICK_ARM: 'Open/Close: Quick Arm', | |||
| LRR_CID_EVENT.OPENCLOSE_KEYSWITCH: 'Open/Close: Keyswitch', | |||
| LRR_CID_EVENT.REMOTE_CALLBACK_REQUESTED: 'Remote: Callback Requested', | |||
| LRR_CID_EVENT.REMOTE_SUCCESS: 'Remote: Successful Access', | |||
| LRR_CID_EVENT.REMOTE_UNSUCCESSFUL: 'Remote: Unsuccessful Access', | |||
| LRR_CID_EVENT.REMOTE_SYSTEM_SHUTDOWN: 'Remote: System Shutdown', | |||
| LRR_CID_EVENT.REMOTE_DIALER_SHUTDOWN: 'Remote: Dialer Shutdown', | |||
| LRR_CID_EVENT.REMOTE_SUCCESSFUL_UPLOAD: 'Remote: Successful Upload', | |||
| LRR_CID_EVENT.ACCESS_DENIED: 'Access: Denied', | |||
| LRR_CID_EVENT.ACCESS_REPORT_BY_USER: 'Access: Report By User', | |||
| LRR_CID_EVENT.ACCESS_FORCED_ACCESS: 'Access: Forced Access', | |||
| LRR_CID_EVENT.ACCESS_EGRESS_DENIED: 'Access: Egress Denied', | |||
| LRR_CID_EVENT.ACCESS_EGRESS_GRANTED: 'Access: Egress Granted', | |||
| LRR_CID_EVENT.ACCESS_DOOR_PROPPED_OPEN: 'Access: Door Propped Open', | |||
| LRR_CID_EVENT.ACCESS_POINT_DSM_TROUBLE: 'Access: Door Status Monitor Trouble', | |||
| LRR_CID_EVENT.ACCESS_POINT_RTE_TROUBLE: 'Access: Request To Exit Trouble', | |||
| LRR_CID_EVENT.ACCESS_PROGRAM_MODE_ENTRY: 'Access: Program Mode Entry', | |||
| LRR_CID_EVENT.ACCESS_PROGRAM_MODE_EXIT: 'Access: Program Mode Exit', | |||
| LRR_CID_EVENT.ACCESS_THREAT_LEVEL_CHANGE: 'Access: Threat Level Change', | |||
| LRR_CID_EVENT.ACCESS_RELAY_FAIL: 'Access: Relay Fail', | |||
| LRR_CID_EVENT.ACCESS_RTE_SHUNT: 'Access: Request to Exit Shunt', | |||
| LRR_CID_EVENT.ACCESS_DSM_SHUNT: 'Access: Door Status Monitor Shunt', | |||
| LRR_CID_EVENT.ACCESS_SECOND_PERSON: 'Access: Second Person Access', | |||
| LRR_CID_EVENT.ACCESS_IRREGULAR_ACCESS: 'Access: Irregular Access', | |||
| LRR_CID_EVENT.OPENCLOSE_ARMED_STAY: 'Open/Close: Armed Stay', | |||
| LRR_CID_EVENT.OPENCLOSE_KEYSWITCH_ARMED_STAY: 'Open/Close: Keyswitch, Armed Stay', | |||
| LRR_CID_EVENT.OPENCLOSE_EXCEPTION: 'Open/Close: Armed with Trouble Override', | |||
| LRR_CID_EVENT.OPENCLOSE_EARLY: 'Open/Close: Early', | |||
| LRR_CID_EVENT.OPENCLOSE_LATE: 'Open/Close: Late', | |||
| LRR_CID_EVENT.TROUBLE_FAILED_TO_OPEN: 'Trouble: Failed To Open', | |||
| LRR_CID_EVENT.TROUBLE_FAILED_TO_CLOSE: 'Trouble: Failed To Close', | |||
| LRR_CID_EVENT.TROUBLE_AUTO_ARM_FAILED: 'Trouble: Auto Arm Failed', | |||
| LRR_CID_EVENT.OPENCLOSE_PARTIAL_ARM: 'Open/Close: Partial Arm', | |||
| LRR_CID_EVENT.OPENCLOSE_EXIT_ERROR: 'Open/Close: Exit Error', | |||
| LRR_CID_EVENT.OPENCLOSE_USER_ON_PREMISES: 'Open/Close: User On Premises', | |||
| LRR_CID_EVENT.TROUBLE_RECENT_CLOSE: 'Trouble: Recent Close', | |||
| LRR_CID_EVENT.ACCESS_WRONG_CODE_ENTRY: 'Access: Wrong Code', | |||
| LRR_CID_EVENT.ACCESS_LEGAL_CODE_ENTRY: 'Access: Legal Code', | |||
| LRR_CID_EVENT.STATUS_REARM_AFTER_ALARM: 'Status: Re-arm After Alarm', | |||
| LRR_CID_EVENT.STATUS_AUTO_ARM_TIME_EXTENDED: 'Status: Auto-arm Time Extended', | |||
| LRR_CID_EVENT.STATUS_PANIC_ALARM_RESET: 'Status: Panic Alarm Reset', | |||
| LRR_CID_EVENT.ACCESS_SERVICE_ONOFF_PREMISES: 'Status: Service On/Off Premises', | |||
| LRR_CID_EVENT.OPENCLOSE_PARTIAL_CLOSING: 'Open/Close: Partial Closing', | |||
| LRR_CID_EVENT.OPENCLOSE_PARTIAL_CLOSE: 'Open/Close: Partial Close', | |||
| LRR_CID_EVENT.DISABLE_ACCESS_READER: 'Disable: Access Reader', | |||
| LRR_CID_EVENT.DISABLE_SOUNDER: 'Disable: Sounder', | |||
| LRR_CID_EVENT.DISABLE_BELL_1: 'Disable: Bell 1', | |||
| LRR_CID_EVENT.DISABLE_BELL_2: 'Disable: Bell 2', | |||
| LRR_CID_EVENT.DISABLE_ALARM_RELAY: 'Disable: Alarm Relay', | |||
| LRR_CID_EVENT.DISABLE_TROUBLE_RELAY: 'Disable: Trouble Relay', | |||
| LRR_CID_EVENT.DISABLE_REVERSING_RELAY: 'Disable: Reversing Relay', | |||
| LRR_CID_EVENT.DISABLE_NOTIFICATION_APPLIANCE_CIRCUIT_3: 'Disable: Notification Appliance Circuit #3', | |||
| LRR_CID_EVENT.DISABLE_NOTIFICATION_APPLIANCE_CIRCUIT_4: 'Disable: Notification Appliance Circuit #4', | |||
| LRR_CID_EVENT.SUPERVISORY_MODULE_ADDED: 'Supervisory: Module Added', | |||
| LRR_CID_EVENT.SUPERVISORY_MODULE_REMOVED: 'Supervisory: Module Removed', | |||
| LRR_CID_EVENT.DISABLE_DIALER: 'Disable: Dialer', | |||
| LRR_CID_EVENT.DISABLE_RADIO_TRANSMITTER: 'Disable: Radio Transmitter', | |||
| LRR_CID_EVENT.DISABLE_REMOTE_UPLOADDOWNLOAD: 'Disable: Remote Upload/Download', | |||
| LRR_CID_EVENT.BYPASS_ZONE: 'Bypass: Zone', | |||
| LRR_CID_EVENT.BYPASS_FIRE: 'Bypass: Fire', | |||
| LRR_CID_EVENT.BYPASS_24HOUR_ZONE: 'Bypass: 24 Hour Zone', | |||
| LRR_CID_EVENT.BYPASS_BURGLARY: 'Bypass: Burglary', | |||
| LRR_CID_EVENT.BYPASS_GROUP: 'Bypass: Group', | |||
| LRR_CID_EVENT.BYPASS_SWINGER: 'Bypass: Swinger', | |||
| LRR_CID_EVENT.BYPASS_ACCESS_ZONE_SHUNT: 'Bypass: Access Zone Shunt', | |||
| LRR_CID_EVENT.BYPASS_ACCESS_POINT_BYPASS: 'Bypass: Access Point', | |||
| LRR_CID_EVENT.BYPASS_ZONE_VAULT: 'Bypass: Vault', | |||
| LRR_CID_EVENT.BYPASS_ZONE_VENT: 'Bypass: Vent', | |||
| LRR_CID_EVENT.TEST_MANUAL: 'Test: Manual Trigger', | |||
| LRR_CID_EVENT.TEST_PERIODIC: 'Test: Periodic', | |||
| LRR_CID_EVENT.TEST_PERIODIC_RF_TRANSMISSION: 'Test: Periodic RF Transmission', | |||
| LRR_CID_EVENT.TEST_FIRE: 'Test: Fire', | |||
| LRR_CID_EVENT.TEST_FIRE_STATUS: 'Test: Fire, Status Report To Follow', | |||
| LRR_CID_EVENT.TEST_LISTENIN_TO_FOLLOW: 'Test: Listen-in To Follow', | |||
| LRR_CID_EVENT.TEST_WALK: 'Test: Walk', | |||
| LRR_CID_EVENT.TEST_SYSTEM_TROUBLE_PRESENT: 'Test: Periodic Test, System Trouble Present', | |||
| LRR_CID_EVENT.TEST_VIDEO_TRANSMITTER_ACTIVE: 'Test: Video Transmitter Active', | |||
| LRR_CID_EVENT.TEST_POINT_TESTED_OK: 'Test: Point Tested OK', | |||
| LRR_CID_EVENT.TEST_POINT_NOT_TESTED: 'Test: Point Not Tested', | |||
| LRR_CID_EVENT.TEST_INTRUSION_ZONE_WALK_TESTED: 'Test: Intrusion Zone Walk Tested', | |||
| LRR_CID_EVENT.TEST_FIRE_ZONE_WALK_TESTED: 'Test: Fire Zone Walk Tested', | |||
| LRR_CID_EVENT.TEST_PANIC_ZONE_WALK_TESTED: 'Test: Panic Zone Walk Tested', | |||
| LRR_CID_EVENT.TROUBLE_SERVICE_REQUEST: 'Trouble: Service Request', | |||
| LRR_CID_EVENT.TROUBLE_EVENT_LOG_RESET: 'Trouble: Event Log Reset', | |||
| LRR_CID_EVENT.TROUBLE_EVENT_LOG_50PERCENT_FULL: 'Trouble: Event Log 50% Full', | |||
| LRR_CID_EVENT.TROUBLE_EVENT_LOG_90PERCENT_FULL: 'Trouble: Event Log 90% Full', | |||
| LRR_CID_EVENT.TROUBLE_EVENT_LOG_OVERFLOW: 'Trouble: Event Log Overflow', | |||
| LRR_CID_EVENT.TROUBLE_TIMEDATE_RESET: 'Trouble: Time/Date Reset', | |||
| LRR_CID_EVENT.TROUBLE_TIMEDATE_INACCURATE: 'Trouble: Time/Date Inaccurate', | |||
| LRR_CID_EVENT.TROUBLE_PROGRAM_MODE_ENTRY: 'Trouble: Program Mode Entry', | |||
| LRR_CID_EVENT.TROUBLE_PROGRAM_MODE_EXIT: 'Trouble: Program Mode Exit', | |||
| LRR_CID_EVENT.TROUBLE_32HOUR_EVENT_LOG_MARKER: 'Trouble: 32 Hour Event Log Marker', | |||
| LRR_CID_EVENT.SCHEDULE_CHANGE: 'Schedule: Change', | |||
| LRR_CID_EVENT.SCHEDULE_EXCEPTION_SCHEDULE_CHANGE: 'Schedule: Exception Schedule Change', | |||
| LRR_CID_EVENT.SCHEDULE_ACCESS_SCHEDULE_CHANGE: 'Schedule: Access Schedule Change', | |||
| LRR_CID_EVENT.TROUBLE_SENIOR_WATCH_TROUBLE: 'Schedule: Senior Watch Trouble', | |||
| LRR_CID_EVENT.STATUS_LATCHKEY_SUPERVISION: 'Status: Latch-key Supervision', | |||
| LRR_CID_EVENT.SPECIAL_ADT_AUTHORIZATION: 'Special: ADT Authorization', | |||
| LRR_CID_EVENT.RESERVED_652: 'Reserved: For Ademco Use', | |||
| LRR_CID_EVENT.RESERVED_652: 'Reserved: For Ademco Use', | |||
| LRR_CID_EVENT.TROUBLE_SYSTEM_INACTIVITY: 'Trouble: System Inactivity', | |||
| LRR_CID_EVENT.TROUBLE_UNABLE_TO_OUTPUT_SIGNAL: 'Trouble: Unable To Output Signal (Derived Channel)', | |||
| LRR_CID_EVENT.TROUBLE_STU_CONTROLLER_DOWN: 'Trouble: STU Controller Down (Derived Channel)', | |||
| LRR_CID_EVENT.REMOTE_DOWNLOAD_ABORT: 'Remote: Download Aborted', | |||
| LRR_CID_EVENT.REMOTE_DOWNLOAD_STARTEND: 'Remote: Download Start/End', | |||
| LRR_CID_EVENT.REMOTE_DOWNLOAD_INTERRUPTED: 'Remote: Download Interrupted', | |||
| LRR_CID_EVENT.REMOTE_CODE_DOWNLOAD_STARTEND: 'Remote: Device Flash Start/End', | |||
| LRR_CID_EVENT.REMOTE_CODE_DOWNLOAD_FAILED: 'Remote: Device Flash Failed', | |||
| LRR_CID_EVENT.OPENCLOSE_AUTOCLOSE_WITH_BYPASS: 'Open/Close: Auto-Close With Bypass', | |||
| LRR_CID_EVENT.OPENCLOSE_BYPASS_CLOSING: 'Open/Close: Bypass Closing', | |||
| LRR_CID_EVENT.EVENT_FIRE_ALARM_SILENCED: 'Event: Fire Alarm Silenced', | |||
| LRR_CID_EVENT.EVENT_SUPERVISOR_POINT_STARTEND: 'Event: Supervisory Point Test Start/End', | |||
| LRR_CID_EVENT.EVENT_HOLDUP_TEST_STARTEND: 'Event: Hold-up Test Start/End', | |||
| LRR_CID_EVENT.EVENT_BURGLARY_TEST_PRINT_STARTEND: 'Event: Burglary Test Print Start/End', | |||
| LRR_CID_EVENT.EVENT_SUPERVISORY_TEST_PRINT_STARTEND: 'Event: Supervisory Test Print Start/End', | |||
| LRR_CID_EVENT.EVENT_BURGLARY_DIAGNOSTICS_STARTEND: 'Event: Burglary Diagnostics Start/End', | |||
| LRR_CID_EVENT.EVENT_FIRE_DIAGNOSTICS_STARTEND: 'Event: Fire Diagnostics Start/End', | |||
| LRR_CID_EVENT.EVENT_UNTYPED_DIAGNOSTICS: 'Event: Untyped Diagnostics', | |||
| LRR_CID_EVENT.EVENT_TROUBLE_CLOSING: 'Event: Trouble Closing', | |||
| LRR_CID_EVENT.EVENT_ACCESS_DENIED_CODE_UNKNOWN: 'Event: Access Denied, Code Unknown', | |||
| LRR_CID_EVENT.ALARM_SUPERVISORY_POINT: 'Alarm: Supervisory Point', | |||
| LRR_CID_EVENT.EVENT_SUPERVISORY_POINT_BYPASS: 'Event: Supervisory Point Bypass', | |||
| LRR_CID_EVENT.TROUBLE_SUPERVISORY_POINT: 'Trouble: Supervisory Point', | |||
| LRR_CID_EVENT.EVENT_HOLDUP_POINT_BYPASS: 'Event: Hold-up Point Bypass', | |||
| LRR_CID_EVENT.EVENT_AC_FAILURE_FOR_4HOURS: 'Event: AC Failure For 4 Hours', | |||
| LRR_CID_EVENT.TROUBLE_OUTPUT: 'Trouble: Output Trouble', | |||
| LRR_CID_EVENT.EVENT_USER_CODE_FOR_EVENT: 'Event: User Code For Event', | |||
| LRR_CID_EVENT.EVENT_LOG_OFF: 'Event: Log-off', | |||
| LRR_CID_EVENT.EVENT_CS_CONNECTION_FAILURE: 'Event: Central Station Connection Failure', | |||
| LRR_CID_EVENT.EVENT_RECEIVER_DATABASE_CONNECTION: 'Event: Receiver Database Connection', | |||
| LRR_CID_EVENT.EVENT_LICENSE_EXPIRATION: 'Event: License Expiration', | |||
| LRR_CID_EVENT.OTHER_NO_READ_LOG: 'Other: No Read Log', | |||
| } | |||
| # Map of DSC event codes to human-readable text. | |||
| LRR_DSC_MAP = { | |||
| LRR_DSC_EVENT.ZONE_EXPANDER_SUPERVISORY_ALARM: 'Zone Expander Supervisory Alarm', | |||
| LRR_DSC_EVENT.ZONE_EXPANDER_SUPERVISORY_RESTORE: 'Zone Expander Supervisory Restore', | |||
| LRR_DSC_EVENT.AUX_INPUT_ALARM: 'Auxillary Input Alarm', | |||
| LRR_DSC_EVENT.SPECIAL_CLOSING: 'Special Closing', | |||
| LRR_DSC_EVENT.CROSS_ZONE_POLICE_CODE_ALARM: 'Cross-zone Police Code Alarm', | |||
| LRR_DSC_EVENT.AUTOMATIC_CLOSING: 'Automatic Closing', | |||
| LRR_DSC_EVENT.ZONE_BYPASS: 'Zone Bypass', | |||
| LRR_DSC_EVENT.REPORT_DSC_USER_LOG_EVENT: 'Report DSC User Log Event', | |||
| } | |||
| # Map of ADEMCO event codes to human-readable text. | |||
| LRR_ADEMCO_MAP = { | |||
| } | |||
| LRR_ALARMDECODER_MAP = { | |||
| LRR_ALARMDECODER_EVENT.CUSTOM_PROG_MSG: 'Custom Programming Message', | |||
| LRR_ALARMDECODER_EVENT.CUSTOM_PROG_KEY: 'Custom Programming Key' | |||
| } | |||
| # Map of UNKNOWN event codes to human-readable text. | |||
| LRR_UNKNOWN_MAP = { | |||
| } | |||
| # Map of event type codes to text maps. | |||
| LRR_TYPE_MAP = { | |||
| LRR_EVENT_TYPE.CID: LRR_CID_MAP, | |||
| LRR_EVENT_TYPE.DSC: LRR_DSC_MAP, | |||
| LRR_EVENT_TYPE.ADEMCO: LRR_ADEMCO_MAP, | |||
| LRR_EVENT_TYPE.ALARMDECODER: LRR_ALARMDECODER_MAP, | |||
| LRR_EVENT_TYPE.UNKNOWN: LRR_UNKNOWN_MAP, | |||
| } | |||
| # LRR events that should be considered Fire events. | |||
| LRR_FIRE_EVENTS = [ | |||
| LRR_CID_EVENT.FIRE, | |||
| LRR_CID_EVENT.FIRE_SMOKE, | |||
| LRR_CID_EVENT.FIRE_COMBUSTION, | |||
| LRR_CID_EVENT.FIRE_WATER_FLOW, | |||
| LRR_CID_EVENT.FIRE_HEAT, | |||
| LRR_CID_EVENT.FIRE_PULL_STATION, | |||
| LRR_CID_EVENT.FIRE_DUCT, | |||
| LRR_CID_EVENT.FIRE_FLAME, | |||
| LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER # HACK: Don't really like having this here | |||
| ] | |||
| # LRR events that should be considered Alarm events. | |||
| LRR_ALARM_EVENTS = [ | |||
| LRR_CID_EVENT.BURGLARY, | |||
| LRR_CID_EVENT.BURGLARY_PERIMETER, | |||
| LRR_CID_EVENT.BURGLARY_INTERIOR, | |||
| LRR_CID_EVENT.BURGLARY_AUX, | |||
| LRR_CID_EVENT.BURGLARY_ENTRYEXIT, | |||
| LRR_CID_EVENT.BURGLARY_DAYNIGHT, | |||
| LRR_CID_EVENT.BURGLARY_OUTDOOR, | |||
| LRR_CID_EVENT.ALARM_GENERAL, | |||
| LRR_CID_EVENT.BURGLARY_SILENT, | |||
| LRR_CID_EVENT.ALARM_AUX, | |||
| LRR_CID_EVENT.ALARM_GAS_DETECTED, | |||
| LRR_CID_EVENT.ALARM_REFRIDGERATION, | |||
| LRR_CID_EVENT.ALARM_LOSS_OF_HEAT, | |||
| LRR_CID_EVENT.ALARM_WATER_LEAKAGE, | |||
| LRR_CID_EVENT.ALARM_LOW_BOTTLED_GAS_LEVEL, | |||
| LRR_CID_EVENT.ALARM_HIGH_TEMP, | |||
| LRR_CID_EVENT.ALARM_LOW_TEMP, | |||
| LRR_CID_EVENT.ALARM_LOSS_OF_AIR_FLOW, | |||
| LRR_CID_EVENT.ALARM_CARBON_MONOXIDE, | |||
| LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER # HACK: Don't really like having this here | |||
| ] | |||
| # LRR events that should be considered Power events. | |||
| LRR_POWER_EVENTS = [ | |||
| LRR_CID_EVENT.TROUBLE_AC_LOSS | |||
| ] | |||
| # LRR events that should be considered Bypass events. | |||
| LRR_BYPASS_EVENTS = [ | |||
| LRR_CID_EVENT.BYPASS_ZONE, | |||
| LRR_CID_EVENT.BYPASS_24HOUR_ZONE, | |||
| LRR_CID_EVENT.BYPASS_BURGLARY | |||
| ] | |||
| # LRR events that should be considered Battery events. | |||
| LRR_BATTERY_EVENTS = [ | |||
| LRR_CID_EVENT.TROUBLE_LOW_BATTERY | |||
| ] | |||
| # LRR events that should be considered Panic events. | |||
| LRR_PANIC_EVENTS = [ | |||
| LRR_CID_EVENT.MEDICAL, | |||
| LRR_CID_EVENT.MEDICAL_PENDANT, | |||
| LRR_CID_EVENT.MEDICAL_FAIL_TO_REPORT, | |||
| LRR_CID_EVENT.PANIC, | |||
| LRR_CID_EVENT.PANIC_DURESS, | |||
| LRR_CID_EVENT.PANIC_SILENT, | |||
| LRR_CID_EVENT.PANIC_AUDIBLE, | |||
| LRR_CID_EVENT.PANIC_DURESS_ACCESS_GRANTED, | |||
| LRR_CID_EVENT.PANIC_DURESS_EGRESS_GRANTED, | |||
| LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER # HACK: Don't really like having this here | |||
| ] | |||
| # LRR events that should be considered Arm events. | |||
| LRR_ARM_EVENTS = [ | |||
| LRR_CID_EVENT.OPENCLOSE, | |||
| LRR_CID_EVENT.OPENCLOSE_BY_USER, | |||
| LRR_CID_EVENT.OPENCLOSE_GROUP, | |||
| LRR_CID_EVENT.OPENCLOSE_AUTOMATIC, | |||
| LRR_CID_EVENT.OPENCLOSE_REMOTE_ARMDISARM, | |||
| LRR_CID_EVENT.OPENCLOSE_QUICK_ARM, | |||
| LRR_CID_EVENT.OPENCLOSE_KEYSWITCH, | |||
| LRR_CID_EVENT.OPENCLOSE_ARMED_STAY, # HACK: Not sure if I like having these in here. | |||
| LRR_CID_EVENT.OPENCLOSE_KEYSWITCH_ARMED_STAY | |||
| ] | |||
| # LRR events that should be considered Arm Stay events. | |||
| LRR_STAY_EVENTS = [ | |||
| LRR_CID_EVENT.OPENCLOSE_ARMED_STAY, | |||
| LRR_CID_EVENT.OPENCLOSE_KEYSWITCH_ARMED_STAY | |||
| ] | |||
| @@ -0,0 +1,113 @@ | |||
| """ | |||
| Message representations received from the panel through the `AlarmDecoder`_ (AD2) | |||
| devices. | |||
| :py:class:`LRRMessage`: Message received from a long-range radio module. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| from .. import BaseMessage | |||
| from ...util import InvalidMessageError | |||
| from .events import LRR_EVENT_TYPE, get_event_description, get_event_source | |||
| class LRRMessage(BaseMessage): | |||
| """ | |||
| Represent a message from a Long Range Radio or emulated Long Range Radio. | |||
| """ | |||
| event_data = None | |||
| """Data associated with the LRR message. Usually user ID or zone.""" | |||
| partition = -1 | |||
| """The partition that this message applies to.""" | |||
| event_type = None | |||
| """The type of the event that occurred.""" | |||
| version = 0 | |||
| """LRR message version""" | |||
| report_code = 0xFF | |||
| """The report code used to override the last two digits of the event type.""" | |||
| event_prefix = '' | |||
| """Extracted prefix for the event_type.""" | |||
| event_source = LRR_EVENT_TYPE.UNKNOWN | |||
| """Extracted event type source.""" | |||
| event_status = 0 | |||
| """Event status flag that represents triggered or restored events.""" | |||
| event_code = 0 | |||
| """Event code for the LRR message.""" | |||
| event_description = '' | |||
| """Human-readable description of LRR event.""" | |||
| def __init__(self, data=None, skip_report_override=False): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self, data) | |||
| self.skip_report_override = skip_report_override | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parses the raw message from the device. | |||
| :param data: message data to parse | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| try: | |||
| _, values = data.split(':') | |||
| values = values.split(',') | |||
| # Handle older-format events | |||
| if len(values) <= 3: | |||
| self.event_data, self.partition, self.event_type = values | |||
| self.version = 1 | |||
| # Newer-format events | |||
| else: | |||
| self.event_data, self.partition, self.event_type, self.report_code = values | |||
| self.version = 2 | |||
| event_type_data = self.event_type.split('_') | |||
| self.event_prefix = event_type_data[0] # Ex: CID | |||
| self.event_source = get_event_source(self.event_prefix) # Ex: LRR_EVENT_TYPE.CID | |||
| self.event_status = int(event_type_data[1][0]) # Ex: 1 or 3 | |||
| self.event_code = int(event_type_data[1][1:], 16) # Ex: 0x100 = Medical | |||
| # replace last 2 digits of event_code with report_code, if applicable. | |||
| if not self.skip_report_override and self.report_code not in ['00', 'ff']: | |||
| self.event_code = int(event_type_data[1][1] + self.report_code, 16) | |||
| self.event_description = get_event_description(self.event_source, self.event_code) | |||
| 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, | |||
| report_code = self.report_code, | |||
| event_prefix = self.event_prefix, | |||
| event_source = self.event_source, | |||
| event_status = self.event_status, | |||
| event_code = hex(self.event_code), | |||
| event_description = self.event_description, | |||
| **kwargs | |||
| ) | |||
| @@ -0,0 +1,164 @@ | |||
| """ | |||
| Primary system for handling LRR events. | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| from .events import LRR_EVENT_TYPE, LRR_EVENT_STATUS, LRR_CID_EVENT | |||
| from .events import LRR_FIRE_EVENTS, LRR_POWER_EVENTS, LRR_BYPASS_EVENTS, LRR_BATTERY_EVENTS, \ | |||
| LRR_PANIC_EVENTS, LRR_ARM_EVENTS, LRR_STAY_EVENTS, LRR_ALARM_EVENTS | |||
| class LRRSystem(object): | |||
| """ | |||
| Handles LRR events and triggers higher-level events in the AlarmDecoder object. | |||
| """ | |||
| def __init__(self, alarmdecoder_object): | |||
| """ | |||
| Constructor | |||
| :param alarmdecoder_object: Main AlarmDecoder object | |||
| :type alarmdecoder_object: :py:class:`~alarmdecoder.AlarmDecoder` | |||
| """ | |||
| self._alarmdecoder = alarmdecoder_object | |||
| def update(self, message): | |||
| """ | |||
| Updates the states in the primary AlarmDecoder object based on | |||
| the LRR message provided. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| """ | |||
| # Firmware version < 2.2a.8.6 | |||
| if message.version == 1: | |||
| if message.event_type == 'ALARM_PANIC': | |||
| self._alarmdecoder._update_panic_status(True) | |||
| elif message.event_type == 'CANCEL': | |||
| self._alarmdecoder._update_panic_status(False) | |||
| # Firmware version >= 2.2a.8.6 | |||
| elif message.version == 2: | |||
| source = message.event_source | |||
| if source == LRR_EVENT_TYPE.CID: | |||
| self._handle_cid_message(message) | |||
| elif source == LRR_EVENT_TYPE.DSC: | |||
| self._handle_dsc_message(message) | |||
| elif source == LRR_EVENT_TYPE.ADEMCO: | |||
| self._handle_ademco_message(message) | |||
| elif source == LRR_EVENT_TYPE.ALARMDECODER: | |||
| self._handle_alarmdecoder_message(message) | |||
| elif source == LRR_EVENT_TYPE.UNKNOWN: | |||
| self._handle_unknown_message(message) | |||
| else: | |||
| pass | |||
| def _handle_cid_message(self, message): | |||
| """ | |||
| Handles ContactID LRR events. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| """ | |||
| status = self._get_event_status(message) | |||
| if status is None: | |||
| return | |||
| if message.event_code in LRR_FIRE_EVENTS: | |||
| if message.event_code == LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: | |||
| status = False | |||
| self._alarmdecoder._update_fire_status(status=status) | |||
| if message.event_code in LRR_ALARM_EVENTS: | |||
| kwargs = {} | |||
| field_name = 'zone' | |||
| if not status: | |||
| field_name = 'user' | |||
| kwargs[field_name] = int(message.event_data) | |||
| self._alarmdecoder._update_alarm_status(status=status, **kwargs) | |||
| if message.event_code in LRR_POWER_EVENTS: | |||
| self._alarmdecoder._update_power_status(status=status) | |||
| if message.event_code in LRR_BYPASS_EVENTS: | |||
| self._alarmdecoder._update_zone_bypass_status(status=status, zone=int(message.event_data)) | |||
| if message.event_code in LRR_BATTERY_EVENTS: | |||
| self._alarmdecoder._update_battery_status(status=status) | |||
| if message.event_code in LRR_PANIC_EVENTS: | |||
| if message.event_code == LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: | |||
| status = False | |||
| self._alarmdecoder._update_panic_status(status=status) | |||
| if message.event_code in LRR_ARM_EVENTS: | |||
| # NOTE: status on OPENCLOSE messages is backwards. | |||
| status_stay = (message.event_status == LRR_EVENT_STATUS.RESTORE \ | |||
| and message.event_code in LRR_STAY_EVENTS) | |||
| if status_stay: | |||
| status = False | |||
| else: | |||
| status = not status | |||
| self._alarmdecoder._update_armed_status(status=status, status_stay=status_stay) | |||
| def _handle_dsc_message(self, message): | |||
| """ | |||
| Handles DSC LRR events. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| """ | |||
| pass | |||
| def _handle_ademco_message(self, message): | |||
| """ | |||
| Handles ADEMCO LRR events. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| """ | |||
| pass | |||
| def _handle_alarmdecoder_message(self, message): | |||
| """ | |||
| Handles AlarmDecoder LRR events. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| """ | |||
| pass | |||
| def _handle_unknown_message(self, message): | |||
| """ | |||
| Handles UNKNOWN LRR events. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| """ | |||
| # TODO: Log this somewhere useful. | |||
| pass | |||
| def _get_event_status(self, message): | |||
| """ | |||
| Retrieves the boolean status of an LRR message. | |||
| :param message: LRR message object | |||
| :type message: :py:class:`~alarmdecoder.messages.LRRMessage` | |||
| :returns: Boolean indicating whether the event was triggered or restored. | |||
| """ | |||
| status = None | |||
| if message.event_status == LRR_EVENT_STATUS.TRIGGER: | |||
| status = True | |||
| elif message.event_status == LRR_EVENT_STATUS.RESTORE: | |||
| status = False | |||
| return status | |||
| @@ -0,0 +1,190 @@ | |||
| """ | |||
| Message representations received from the panel through the `AlarmDecoder`_ (AD2) | |||
| devices. | |||
| :py:class:`Message`: The standard and most common message received from a panel. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| import re | |||
| from . import BaseMessage | |||
| from ..util import InvalidMessageError | |||
| from ..panels import PANEL_TYPES, ADEMCO, DSC | |||
| class Message(BaseMessage): | |||
| """ | |||
| Represents a message from the alarm panel. | |||
| """ | |||
| ready = False | |||
| """Indicates whether or not the panel is in a ready state.""" | |||
| armed_away = False | |||
| """Indicates whether or not the panel is armed away.""" | |||
| armed_home = False | |||
| """Indicates whether or not the panel is armed home.""" | |||
| backlight_on = False | |||
| """Indicates whether or not the keypad backlight is on.""" | |||
| programming_mode = False | |||
| """Indicates whether or not we're in programming mode.""" | |||
| beeps = -1 | |||
| """Number of beeps associated with a message.""" | |||
| zone_bypassed = False | |||
| """Indicates whether or not a zone is bypassed.""" | |||
| ac_power = False | |||
| """Indicates whether or not the panel is on AC power.""" | |||
| chime_on = False | |||
| """Indicates whether or not the chime is enabled.""" | |||
| alarm_event_occurred = False | |||
| """Indicates whether or not an alarm event has occurred.""" | |||
| alarm_sounding = False | |||
| """Indicates whether or not an alarm is sounding.""" | |||
| battery_low = False | |||
| """Indicates whether or not there is a low battery.""" | |||
| entry_delay_off = False | |||
| """Indicates whether or not the entry delay is enabled.""" | |||
| fire_alarm = False | |||
| """Indicates whether or not a fire alarm is sounding.""" | |||
| check_zone = False | |||
| """Indicates whether or not there are zones that require attention.""" | |||
| perimeter_only = False | |||
| """Indicates whether or not the perimeter is armed.""" | |||
| system_fault = False | |||
| """Indicates whether a system fault has occurred.""" | |||
| panel_type = ADEMCO | |||
| """Indicates which panel type was the source of this message.""" | |||
| numeric_code = None | |||
| """The numeric code associated with the message.""" | |||
| text = None | |||
| """The human-readable text to be displayed on the panel LCD.""" | |||
| cursor_location = -1 | |||
| """Current cursor location on the keypad.""" | |||
| mask = 0xFFFFFFFF | |||
| """Address mask this message is intended for.""" | |||
| bitfield = None | |||
| """The bitfield associated with this message.""" | |||
| panel_data = None | |||
| """The panel data field associated with this message.""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self, data) | |||
| self._regex = re.compile('^(!KPM:){0,1}(\[[a-fA-F0-9\-]+\]),([a-fA-F0-9]+),(\[[a-fA-F0-9]+\]),(".+")$') | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parse the message from the device. | |||
| :param data: message data | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| match = self._regex.match(str(data)) | |||
| if match is None: | |||
| raise InvalidMessageError('Received invalid message: {0}'.format(data)) | |||
| header, self.bitfield, self.numeric_code, self.panel_data, alpha = match.group(1, 2, 3, 4, 5) | |||
| is_bit_set = lambda bit: not self.bitfield[bit] == "0" | |||
| self.ready = is_bit_set(1) | |||
| self.armed_away = is_bit_set(2) | |||
| self.armed_home = is_bit_set(3) | |||
| self.backlight_on = is_bit_set(4) | |||
| self.programming_mode = is_bit_set(5) | |||
| self.beeps = int(self.bitfield[6], 16) | |||
| self.zone_bypassed = is_bit_set(7) | |||
| self.ac_power = is_bit_set(8) | |||
| self.chime_on = is_bit_set(9) | |||
| self.alarm_event_occurred = is_bit_set(10) | |||
| self.alarm_sounding = is_bit_set(11) | |||
| self.battery_low = is_bit_set(12) | |||
| self.entry_delay_off = is_bit_set(13) | |||
| self.fire_alarm = is_bit_set(14) | |||
| self.check_zone = is_bit_set(15) | |||
| self.perimeter_only = is_bit_set(16) | |||
| self.system_fault = is_bit_set(17) | |||
| if self.bitfield[18] in list(PANEL_TYPES): | |||
| 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 in (ADEMCO, DSC): | |||
| 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) | |||
| def parse_numeric_code(self, force_hex=False): | |||
| """ | |||
| Parses and returns the numeric code as an integer. | |||
| The numeric code can be either base 10 or base 16, depending on | |||
| where the message came from. | |||
| :param force_hex: force the numeric code to be processed as base 16. | |||
| :type force_hex: boolean | |||
| :raises: ValueError | |||
| """ | |||
| code = None | |||
| got_error = False | |||
| if not force_hex: | |||
| try: | |||
| code = int(self.numeric_code) | |||
| except ValueError: | |||
| got_error = True | |||
| if force_hex or got_error: | |||
| try: | |||
| code = int(self.numeric_code, 16) | |||
| except ValueError: | |||
| raise | |||
| return code | |||
| 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 | |||
| ) | |||
| @@ -0,0 +1,82 @@ | |||
| """ | |||
| Message representations received from the panel through the `AlarmDecoder`_ (AD2) | |||
| devices. | |||
| :py:class:`RFMessage`: Message received from an RF receiver module. | |||
| .. _AlarmDecoder: http://www.alarmdecoder.com | |||
| .. moduleauthor:: Scott Petersen <scott@nutech.com> | |||
| """ | |||
| from . import BaseMessage | |||
| from ..util import InvalidMessageError | |||
| class RFMessage(BaseMessage): | |||
| """ | |||
| Represents a message from an RF receiver. | |||
| """ | |||
| serial_number = None | |||
| """Serial number of the RF device.""" | |||
| value = -1 | |||
| """Value associated with this message.""" | |||
| battery = False | |||
| """Low battery indication""" | |||
| supervision = False | |||
| """Supervision required indication""" | |||
| loop = [False for _ in list(range(4))] | |||
| """Loop indicators""" | |||
| def __init__(self, data=None): | |||
| """ | |||
| Constructor | |||
| :param data: message data to parse | |||
| :type data: string | |||
| """ | |||
| BaseMessage.__init__(self, data) | |||
| if data is not None: | |||
| self._parse_message(data) | |||
| def _parse_message(self, data): | |||
| """ | |||
| Parses the raw message from the device. | |||
| :param data: message data | |||
| :type data: string | |||
| :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` | |||
| """ | |||
| try: | |||
| _, values = data.split(':') | |||
| self.serial_number, self.value = values.split(',') | |||
| self.value = int(self.value, 16) | |||
| is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0 | |||
| # Bit 1 = unknown | |||
| self.battery = is_bit_set(2) | |||
| self.supervision = is_bit_set(3) | |||
| # Bit 4 = unknown | |||
| self.loop[2] = is_bit_set(5) | |||
| self.loop[1] = is_bit_set(6) | |||
| self.loop[3] = is_bit_set(7) | |||
| self.loop[0] = is_bit_set(8) | |||
| 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 | |||
| ) | |||
| @@ -0,0 +1,7 @@ | |||
| class FireState: | |||
| """ | |||
| Fire alarm status | |||
| """ | |||
| NONE = 0 | |||
| ALARM = 1 | |||
| ACKNOWLEDGED = 2 | |||
| @@ -9,6 +9,7 @@ Provides utility classes for the `AlarmDecoder`_ (AD2) devices. | |||
| import time | |||
| import threading | |||
| import select | |||
| import sys | |||
| import alarmdecoder | |||
| from io import open | |||
| @@ -58,6 +59,15 @@ class UploadChecksumError(UploadError): | |||
| def bytes_available(device): | |||
| """ | |||
| Determines the number of bytes available for reading from an | |||
| AlarmDecoder device | |||
| :param device: the AlarmDecoder device | |||
| :type device: :py:class:`~alarmdecoder.devices.Device` | |||
| :returns: int | |||
| """ | |||
| bytes_avail = 0 | |||
| if isinstance(device, alarmdecoder.devices.SerialDevice): | |||
| @@ -70,7 +80,28 @@ def bytes_available(device): | |||
| return bytes_avail | |||
| def bytes_hack(buf): | |||
| """ | |||
| Hacky workaround for old installs of the library on systems without python-future that were | |||
| keeping the 2to3 update from working after auto-update. | |||
| """ | |||
| ub = None | |||
| if sys.version_info > (3,): | |||
| ub = buf | |||
| else: | |||
| ub = bytes(buf) | |||
| return ub | |||
| def read_firmware_file(file_path): | |||
| """ | |||
| Reads a firmware file into a dequeue for processing. | |||
| :param file_path: Path to the firmware file | |||
| :type file_path: string | |||
| :returns: deque | |||
| """ | |||
| data_queue = deque() | |||
| with open(file_path) as firmware_handle: | |||
| @@ -99,6 +130,14 @@ class Firmware(object): | |||
| @staticmethod | |||
| def read(device): | |||
| """ | |||
| Reads data from the specified device. | |||
| :param device: the AlarmDecoder device | |||
| :type device: :py:class:`~alarmdecoder.devices.Device` | |||
| :returns: string | |||
| """ | |||
| response = None | |||
| bytes_avail = bytes_available(device) | |||
| @@ -177,14 +177,8 @@ class Zonetracker(object): | |||
| self._last_zone_fault = 0 | |||
| # Process fault | |||
| elif message.check_zone or message.text.startswith("FAULT") or message.text.startswith("ALARM"): | |||
| # 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) | |||
| elif self.alarmdecoder_object.mode != DSC and (message.check_zone or message.text.startswith("FAULT") or message.text.startswith("ALARM")): | |||
| zone = message.parse_numeric_code() | |||
| # NOTE: Odd case for ECP failures. Apparently they report as | |||
| # zone 191 (0xBF) regardless of whether or not the | |||
| @@ -10,6 +10,9 @@ from alarmdecoder.devices import USBDevice | |||
| from alarmdecoder.messages import Message, RFMessage, LRRMessage, ExpanderMessage | |||
| from alarmdecoder.event.event import Event, EventHandler | |||
| from alarmdecoder.zonetracking import Zonetracker | |||
| from alarmdecoder.panels import ADEMCO, DSC | |||
| from alarmdecoder.messages.lrr import LRR_EVENT_TYPE, LRR_EVENT_STATUS | |||
| from alarmdecoder.states import FireState | |||
| class TestAlarmDecoder(TestCase): | |||
| @@ -66,6 +69,7 @@ class TestAlarmDecoder(TestCase): | |||
| def tearDown(self): | |||
| pass | |||
| ### Library events | |||
| def on_panic(self, sender, *args, **kwargs): | |||
| self._panicked = kwargs['status'] | |||
| @@ -123,6 +127,7 @@ class TestAlarmDecoder(TestCase): | |||
| def on_zone_restore(self, sender, *args, **kwargs): | |||
| self._zone_restored = kwargs['zone'] | |||
| ### Tests | |||
| def test_open(self): | |||
| self._decoder.open() | |||
| self._device.open.assert_any_calls() | |||
| @@ -183,108 +188,132 @@ class TestAlarmDecoder(TestCase): | |||
| self.assertTrue(self._expander_message_received) | |||
| def test_relay_message(self): | |||
| self._decoder.open() | |||
| msg = self._decoder._handle_message(b'!REL:12,01,01') | |||
| self.assertIsInstance(msg, ExpanderMessage) | |||
| self.assertEqual(self._relay_changed, True) | |||
| self.assertTrue(self._relay_changed) | |||
| def test_rfx_message(self): | |||
| msg = self._decoder._handle_message(b'!RFX:0180036,80') | |||
| self.assertIsInstance(msg, RFMessage) | |||
| self.assertTrue(self._rfx_message_received) | |||
| def test_panic(self): | |||
| self._decoder.open() | |||
| def test_panic_v1(self): | |||
| # LRR v1 | |||
| msg = self._decoder._handle_message(b'!LRR:012,1,ALARM_PANIC') | |||
| self.assertEquals(self._panicked, True) | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertTrue(self._panicked) | |||
| msg = self._decoder._handle_message(b'!LRR:012,1,CANCEL') | |||
| self.assertEquals(self._panicked, False) | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertFalse(self._panicked) | |||
| def test_config_message(self): | |||
| self._decoder.open() | |||
| def test_panic_v2(self): | |||
| # LRR v2 | |||
| msg = self._decoder._handle_message(b'!LRR:099,1,CID_1123,ff') # Panic | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertTrue(self._panicked) | |||
| msg = self._decoder._handle_message(b'!CONFIG>ADDRESS=18&CONFIGBITS=ff00&LRR=N&EXP=NNNNN&REL=NNNN&MASK=ffffffff&DEDUPLICATE=N') | |||
| msg = self._decoder._handle_message(b'!LRR:001,1,CID_1406,ff') # Cancel | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertFalse(self._panicked) | |||
| def test_config_message(self): | |||
| msg = self._decoder._handle_message(b'!CONFIG>MODE=A&CONFIGBITS=ff04&ADDRESS=18&LRR=N&COM=N&EXP=NNNNN&REL=NNNN&MASK=ffffffff&DEDUPLICATE=N') | |||
| self.assertEquals(self._decoder.mode, ADEMCO) | |||
| self.assertEquals(self._decoder.address, 18) | |||
| self.assertEquals(self._decoder.configbits, int('ff00', 16)) | |||
| self.assertEquals(self._decoder.configbits, int('ff04', 16)) | |||
| self.assertEquals(self._decoder.address_mask, int('ffffffff', 16)) | |||
| self.assertEquals(self._decoder.emulate_zone, [False for x in range(5)]) | |||
| self.assertEquals(self._decoder.emulate_relay, [False for x in range(4)]) | |||
| self.assertEquals(self._decoder.emulate_lrr, False) | |||
| self.assertEquals(self._decoder.deduplicate, False) | |||
| self.assertEqual(self._got_config, True) | |||
| self.assertFalse(self._decoder.emulate_lrr) | |||
| self.assertFalse(self._decoder.emulate_com) | |||
| self.assertFalse(self._decoder.deduplicate) | |||
| self.assertTrue(self._got_config) | |||
| def test_power_changed_event(self): | |||
| msg = self._decoder._handle_message(b'[0000000100000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._power_changed, False) # Not set first time we hit it. | |||
| self.assertFalse(self._power_changed) # Not set first time we hit it. | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._power_changed, False) | |||
| self.assertFalse(self._power_changed) | |||
| msg = self._decoder._handle_message(b'[0000000100000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._power_changed, True) | |||
| self.assertTrue(self._power_changed) | |||
| def test_alarm_event(self): | |||
| msg = self._decoder._handle_message(b'[0000000000100000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._alarmed, False) # Not set first time we hit it. | |||
| self.assertFalse(self._alarmed) # Not set first time we hit it. | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._alarmed, False) | |||
| self.assertEquals(self._alarm_restored, True) | |||
| self.assertFalse(self._alarmed) | |||
| self.assertTrue(self._alarm_restored) | |||
| msg = self._decoder._handle_message(b'[0000000000100000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._alarmed, True) | |||
| self.assertTrue(self._alarmed) | |||
| def test_zone_bypassed_event(self): | |||
| msg = self._decoder._handle_message(b'[0000001000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._bypassed, False) # Not set first time we hit it. | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._bypassed, False) | |||
| self.assertFalse(self._bypassed) | |||
| msg = self._decoder._handle_message(b'[0000001000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._bypassed, True) | |||
| self.assertTrue(self._bypassed) | |||
| def test_armed_away_event(self): | |||
| msg = self._decoder._handle_message(b'[0100000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._armed, False) # Not set first time we hit it. | |||
| self.assertFalse(self._armed) # Not set first time we hit it. | |||
| msg = self._decoder._handle_message(b'[0100000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertFalse(self._armed) | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._armed, False) | |||
| self.assertFalse(self._armed) | |||
| msg = self._decoder._handle_message(b'[0100000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._armed, True) | |||
| self.assertTrue(self._armed) | |||
| self._armed = False | |||
| msg = self._decoder._handle_message(b'[0010000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._armed, False) # Not set first time we hit it. | |||
| self.assertTrue(self._armed) | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._armed, False) | |||
| msg = self._decoder._handle_message(b'[0010000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._armed, True) | |||
| self.assertFalse(self._armed) | |||
| def test_battery_low_event(self): | |||
| msg = self._decoder._handle_message(b'[0000000000010000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._battery, True) | |||
| self.assertTrue(self._battery) | |||
| # force the timeout to expire. | |||
| with patch.object(time, 'time', return_value=self._decoder._battery_status[1] + 35): | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._battery, False) | |||
| self.assertFalse(self._battery) | |||
| def test_fire_alarm_event(self): | |||
| self._fire = FireState.NONE | |||
| msg = self._decoder._handle_message(b'[0000000000000100----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._fire, True) | |||
| self.assertEquals(self._fire, FireState.ALARM) | |||
| # force the timeout to expire. | |||
| with patch.object(time, 'time', return_value=self._decoder._battery_status[1] + 35): | |||
| with patch.object(time, 'time', return_value=self._decoder._fire_status[1] + 35): | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._fire, FireState.NONE) | |||
| def test_fire_lrr(self): | |||
| self._fire = FireState.NONE | |||
| msg = self._decoder._handle_message(b'!LRR:095,1,CID_1110,ff') # Fire: Non-specific | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertEquals(self._fire, FireState.ALARM) | |||
| msg = self._decoder._handle_message(b'!LRR:001,1,CID_1406,ff') # Open/Close: Cancel | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertEquals(self._fire, FireState.ACKNOWLEDGED) | |||
| # force the timeout to expire. | |||
| with patch.object(time, 'time', return_value=self._decoder._fire_status[1] + 35): | |||
| msg = self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000]," "') | |||
| self.assertEquals(self._fire, False) | |||
| self.assertEquals(self._fire, FireState.NONE) | |||
| def test_hit_for_faults(self): | |||
| self._decoder._handle_message(b'[0000000000000000----],000,[f707000600e5800c0c020000],"Hit * for faults "') | |||
| @@ -314,4 +343,3 @@ class TestAlarmDecoder(TestCase): | |||
| self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "') | |||
| self.assertEquals(self._zone_restored, 3) | |||
| @@ -48,12 +48,14 @@ class TestUSBDevice(TestCase): | |||
| def tearDown(self): | |||
| self._device.close() | |||
| ### Library events | |||
| def attached_event(self, sender, *args, **kwargs): | |||
| self._attached = True | |||
| def detached_event(self, sender, *args, **kwargs): | |||
| self._detached = True | |||
| ### Tests | |||
| def test_find_default_param(self): | |||
| with patch.object(Ftdi, 'find_all', return_value=[(0, 0, 'AD2', 1, 'AD2')]): | |||
| device = USBDevice.find() | |||
| @@ -69,8 +71,8 @@ class TestUSBDevice(TestCase): | |||
| self.assertEqual(device.interface, 'AD2-2') | |||
| def test_events(self): | |||
| self.assertEqual(self._attached, False) | |||
| self.assertEqual(self._detached, False) | |||
| self.assertFalse(self._attached) | |||
| self.assertFalse(self._detached) | |||
| # this is ugly, but it works. | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-1', 1, 'AD2'), (0, 0, 'AD2-2', 1, 'AD2')]): | |||
| @@ -81,8 +83,8 @@ class TestUSBDevice(TestCase): | |||
| time.sleep(1) | |||
| USBDevice.stop_detection() | |||
| self.assertEqual(self._attached, True) | |||
| self.assertEqual(self._detached, True) | |||
| self.assertTrue(self._attached) | |||
| self.assertTrue(self._detached) | |||
| def test_find_all(self): | |||
| with patch.object(USBDevice, 'find_all', return_value=[]) as mock: | |||
| @@ -149,6 +151,7 @@ class TestSerialDevice(TestCase): | |||
| def tearDown(self): | |||
| self._device.close() | |||
| ### Tests | |||
| def test_open(self): | |||
| self._device.interface = '/dev/ttyS0' | |||
| @@ -249,6 +252,7 @@ class TestSocketDevice(TestCase): | |||
| def tearDown(self): | |||
| self._device.close() | |||
| ### Tests | |||
| def test_open(self): | |||
| with patch.object(socket.socket, '__init__', return_value=None): | |||
| with patch.object(socket.socket, 'connect', return_value=None) as mock: | |||
| @@ -411,12 +415,14 @@ if have_pyftdi: | |||
| def tearDown(self): | |||
| self._device.close() | |||
| ### Library events | |||
| def attached_event(self, sender, *args, **kwargs): | |||
| self._attached = True | |||
| def detached_event(self, sender, *args, **kwargs): | |||
| self._detached = True | |||
| ### Tests | |||
| def test_find_default_param(self): | |||
| with patch.object(Ftdi, 'find_all', return_value=[(0, 0, 'AD2', 1, 'AD2')]): | |||
| device = USBDevice.find() | |||
| @@ -432,8 +438,8 @@ if have_pyftdi: | |||
| self.assertEquals(device.interface, 'AD2-2') | |||
| def test_events(self): | |||
| self.assertEquals(self._attached, False) | |||
| self.assertEquals(self._detached, False) | |||
| self.assertFalse(self._attached) | |||
| self.assertFalse(self._detached) | |||
| # this is ugly, but it works. | |||
| with patch.object(USBDevice, 'find_all', return_value=[(0, 0, 'AD2-1', 1, 'AD2'), (0, 0, 'AD2-2', 1, 'AD2')]): | |||
| @@ -444,8 +450,8 @@ if have_pyftdi: | |||
| time.sleep(1) | |||
| USBDevice.stop_detection() | |||
| self.assertEquals(self._attached, True) | |||
| self.assertEquals(self._detached, True) | |||
| self.assertTrue(self._attached) | |||
| self.assertTrue(self._detached) | |||
| def test_find_all(self): | |||
| with patch.object(USBDevice, 'find_all', return_value=[]) as mock: | |||
| @@ -1,7 +1,9 @@ | |||
| from unittest import TestCase | |||
| from alarmdecoder.messages import Message, ExpanderMessage, RFMessage, LRRMessage | |||
| from alarmdecoder.messages.lrr import LRR_EVENT_TYPE, LRR_CID_EVENT, LRR_EVENT_STATUS | |||
| from alarmdecoder.util import InvalidMessageError | |||
| from alarmdecoder.panels import ADEMCO | |||
| class TestMessages(TestCase): | |||
| @@ -11,10 +13,32 @@ class TestMessages(TestCase): | |||
| def tearDown(self): | |||
| pass | |||
| ### Tests | |||
| def test_message_parse(self): | |||
| msg = Message('[0000000000000000----],001,[f707000600e5800c0c020000],"FAULT 1 "') | |||
| msg = Message('[00000000000000000A--],001,[f707000600e5800c0c020000],"FAULT 1 "') | |||
| self.assertFalse(msg.ready) | |||
| self.assertFalse(msg.armed_away) | |||
| self.assertFalse(msg.armed_home) | |||
| self.assertFalse(msg.backlight_on) | |||
| self.assertFalse(msg.programming_mode) | |||
| self.assertEqual(msg.beeps, 0) | |||
| self.assertFalse(msg.zone_bypassed) | |||
| self.assertFalse(msg.ac_power) | |||
| self.assertFalse(msg.chime_on) | |||
| self.assertFalse(msg.alarm_event_occurred) | |||
| self.assertFalse(msg.alarm_sounding) | |||
| self.assertFalse(msg.battery_low) | |||
| self.assertFalse(msg.entry_delay_off) | |||
| self.assertFalse(msg.fire_alarm) | |||
| self.assertFalse(msg.check_zone) | |||
| self.assertFalse(msg.perimeter_only) | |||
| self.assertFalse(msg.system_fault) | |||
| self.assertFalse(msg.panel_type, ADEMCO) | |||
| self.assertEqual(msg.numeric_code, '001') | |||
| self.assertEqual(msg.mask, int('07000600', 16)) | |||
| self.assertEqual(msg.cursor_location, -1) | |||
| self.assertEqual(msg.text, 'FAULT 1 ') | |||
| def test_message_parse_fail(self): | |||
| with self.assertRaises(InvalidMessageError): | |||
| @@ -24,6 +48,8 @@ class TestMessages(TestCase): | |||
| msg = ExpanderMessage('!EXP:07,01,01') | |||
| self.assertEqual(msg.address, 7) | |||
| self.assertEqual(msg.channel, 1) | |||
| self.assertEqual(msg.value, 1) | |||
| def test_expander_message_parse_fail(self): | |||
| with self.assertRaises(InvalidMessageError): | |||
| @@ -33,16 +59,34 @@ class TestMessages(TestCase): | |||
| msg = RFMessage('!RFX:0180036,80') | |||
| self.assertEqual(msg.serial_number, '0180036') | |||
| self.assertEqual(msg.value, int('80', 16)) | |||
| def test_rf_message_parse_fail(self): | |||
| with self.assertRaises(InvalidMessageError): | |||
| msg = RFMessage('') | |||
| def test_lrr_message_parse(self): | |||
| def test_lrr_message_parse_v1(self): | |||
| msg = LRRMessage('!LRR:012,1,ARM_STAY') | |||
| self.assertEqual(msg.event_data, '012') | |||
| self.assertEqual(msg.partition, '1') | |||
| self.assertEqual(msg.event_type, 'ARM_STAY') | |||
| def test_lrr_message_parse_v2(self): | |||
| msg = LRRMessage(b'!LRR:001,1,CID_3401,ff') | |||
| self.assertIsInstance(msg, LRRMessage) | |||
| self.assertEquals(msg.event_data, '001') | |||
| self.assertEquals(msg.partition, '1') | |||
| self.assertEquals(msg.event_prefix, 'CID') | |||
| self.assertEquals(msg.event_source, LRR_EVENT_TYPE.CID) | |||
| self.assertEquals(msg.event_status, LRR_EVENT_STATUS.RESTORE) | |||
| self.assertEquals(msg.event_code, LRR_CID_EVENT.OPENCLOSE_BY_USER) | |||
| self.assertEquals(msg.report_code, 'ff') | |||
| def test_lrr_event_code_override(self): | |||
| msg = LRRMessage(b'!LRR:001,1,CID_3400,01') | |||
| self.assertEquals(msg.event_code, LRR_CID_EVENT.OPENCLOSE_BY_USER) # 400 -> 401 | |||
| def test_lrr_message_parse_fail(self): | |||
| with self.assertRaises(InvalidMessageError): | |||
| msg = LRRMessage('') | |||
| @@ -23,18 +23,21 @@ class TestZonetracking(TestCase): | |||
| def tearDown(self): | |||
| pass | |||
| ### Library events | |||
| def fault_event(self, sender, *args, **kwargs): | |||
| self._faulted = True | |||
| def restore_event(self, sender, *args, **kwargs): | |||
| self._restored = True | |||
| ### Util | |||
| def _build_expander_message(self, msg): | |||
| msg = ExpanderMessage(msg) | |||
| zone = self._zonetracker.expander_to_zone(msg.address, msg.channel) | |||
| return zone, msg | |||
| ### Tests | |||
| def test_zone_fault(self): | |||
| zone, msg = self._build_expander_message('!EXP:07,01,01') | |||
| self._zonetracker.update(msg) | |||