├── .github ├── actions │ ├── build-and-test │ │ └── action.yaml │ └── setup-python │ │ └── action.yaml └── workflows │ ├── merge.yaml │ ├── pr.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── MAINTAINERS.md ├── MANIFEST.in ├── README.rst ├── alarmdecoder ├── __init__.py ├── decoder.py ├── devices │ ├── __init__.py │ ├── base_device.py │ ├── serial_device.py │ ├── socket_device.py │ └── usb_device.py ├── event │ ├── __init__.py │ └── event.py ├── messages │ ├── __init__.py │ ├── aui_message.py │ ├── base_message.py │ ├── expander_message.py │ ├── lrr │ │ ├── __init__.py │ │ ├── events.py │ │ ├── message.py │ │ └── system.py │ ├── panel_message.py │ └── rf_message.py ├── panels.py ├── states.py ├── util.py └── zonetracking.py ├── bin ├── ad2-firmwareupload └── ad2-sslterm ├── docs ├── Makefile ├── alarmdecoder.event.rst ├── alarmdecoder.rst ├── build │ ├── doctrees │ │ ├── alarmdecoder.doctree │ │ ├── alarmdecoder.event.doctree │ │ ├── environment.pickle │ │ ├── index.doctree │ │ └── modules.doctree │ └── html │ │ ├── .buildinfo │ │ ├── _modules │ │ ├── alarmdecoder │ │ │ ├── decoder.html │ │ │ ├── devices.html │ │ │ ├── event │ │ │ │ └── event.html │ │ │ ├── messages.html │ │ │ ├── util.html │ │ │ └── zonetracking.html │ │ └── index.html │ │ ├── _sources │ │ ├── alarmdecoder.event.txt │ │ ├── alarmdecoder.txt │ │ ├── index.txt │ │ └── modules.txt │ │ ├── _static │ │ ├── ajax-loader.gif │ │ ├── basic.css │ │ ├── comment-bright.png │ │ ├── comment-close.png │ │ ├── comment.png │ │ ├── default.css │ │ ├── doctools.js │ │ ├── down-pressed.png │ │ ├── down.png │ │ ├── file.png │ │ ├── jquery.js │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── searchtools.js │ │ ├── sidebar.js │ │ ├── underscore.js │ │ ├── up-pressed.png │ │ ├── up.png │ │ └── websupport.js │ │ ├── alarmdecoder.event.html │ │ ├── alarmdecoder.html │ │ ├── genindex.html │ │ ├── index.html │ │ ├── modules.html │ │ ├── objects.inv │ │ ├── py-modindex.html │ │ ├── search.html │ │ └── searchindex.js ├── conf.py ├── index.rst ├── make.bat └── modules.rst ├── examples ├── alarm_email.py ├── lrr_example.py ├── rf_device.py ├── serialport.py ├── socket_example.py ├── ssl_socket.py ├── usb_detection.py ├── usb_device.py └── virtual_zone_expander.py ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── test_ad2.py ├── test_devices.py ├── test_messages.py ├── test_util.py └── test_zonetracking.py /.github/actions/build-and-test/action.yaml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | description: | 3 | Set up Python and run the build and test steps. 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | - name: Set up Python 9 | uses: ./.github/actions/setup-python 10 | # TODO: move dependencies to a separate file (e.g. a requirements.txt file) 11 | - name: Install dependencies 12 | shell: bash 13 | run: | 14 | python -m pip install pytest mock build 15 | - name: Run build 16 | shell: bash 17 | run: python -m build 18 | - name: Show dist files 19 | shell: bash 20 | run: | 21 | echo "Dist files:" 22 | ls -lh dist/ 23 | - name: Run pytest 24 | shell: bash 25 | run: | 26 | python -m pip install -e . 27 | pytest 28 | -------------------------------------------------------------------------------- /.github/actions/setup-python/action.yaml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | description: | 3 | This action lets the Python version for CI be specified in a single place. 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | - name: Set up Python 9 | uses: actions/setup-python@v5 10 | with: 11 | python-version: "3.10" 12 | cache: pip 13 | -------------------------------------------------------------------------------- /.github/workflows/merge.yaml: -------------------------------------------------------------------------------- 1 | # This workflow builds and tests code that is pushed to the `master` branch. 2 | 3 | name: merge 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | merge: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-tags: true 18 | fetch-depth: 0 19 | - name: Build and test 20 | uses: ./.github/actions/build-and-test 21 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | # This workflow builds and tests code in pull requests. 2 | 3 | name: pr 4 | 5 | on: pull_request 6 | 7 | jobs: 8 | pr: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-tags: true 15 | fetch-depth: 0 16 | - name: Build and test 17 | uses: ./.github/actions/build-and-test 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # This workflow initiates a release of the project. 2 | 3 | name: release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: Release version (e.g. `1.13.12`) 10 | type: string 11 | required: true 12 | 13 | jobs: 14 | release: 15 | permissions: 16 | # `contents: write` is required to create tags and create releases 17 | contents: write 18 | runs-on: ubuntu-latest 19 | env: 20 | RELEASE_VERSION: ${{ inputs.version }} 21 | steps: 22 | - name: Checkout repo 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-tags: true 26 | fetch-depth: 0 27 | - name: Create local lightweight tag 28 | run: git tag "${RELEASE_VERSION}" 29 | - name: Build and test 30 | uses: ./.github/actions/build-and-test 31 | - name: Push tag 32 | run: git push origin "${RELEASE_VERSION}" 33 | - name: Create release from tag 34 | env: 35 | GH_TOKEN: ${{ github.token }} 36 | run: | 37 | gh api \ 38 | --method POST \ 39 | "/repos/${GITHUB_REPOSITORY}/releases" \ 40 | -f "tag_name=${RELEASE_VERSION}" \ 41 | -f "name=${RELEASE_VERSION}" \ 42 | -F "draft=false" \ 43 | -F "prerelease=false" \ 44 | -F "generate_release_notes=true" 45 | - name: Publish package distributions to PyPI 46 | # TODO: setup attestations and trusted publishing. 47 | uses: pypa/gh-action-pypi-publish@release/v1 48 | with: 49 | # attestations require trusted publishing which isn't setup yet 50 | attestations: false 51 | password: ${{ secrets.PYPI_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | alarmdecoder.sublime-project 2 | alarmdecoder.sublime-workspace 3 | build/ 4 | dist 5 | tmp 6 | *.pyc 7 | *.pyo 8 | *.egg-info 9 | bin/ad2-test 10 | *~ 11 | .vscode 12 | venv 13 | .venv 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Nu Tech Software Solutions, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | This document is intended for maintainers of the `nutechsoftware/alarmdecoder` repository. 4 | 5 | It summarizes information about the automated processes involved with the repository. 6 | 7 | ## GitHub Actions Automation 8 | 9 | This section describes how GitHub Actions is used to automate test and release processes for the `nutechsoftware/alarmdecoder` repository. GitHub Actions is free for public repositories. More information about GitHub Actions can be found on their official documentation site here: https://docs.github.com/en/actions. 10 | 11 | ### Reusable Actions 12 | 13 | The GitHub Actions workflows described below make use of [composite actions](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action) to help consolidate common workflow steps. 14 | 15 | These actions are found in the [.github/actions](./.github/actions) directory. Each action has a `description` field at the top of the file that describes its purpose. 16 | 17 | ### Workflows 18 | 19 | The GitHub Actions workflows can be found in the [.github/workflows](./.github/workflows) directory. Each workflow has a comment at the top of the file that describes its purpose. 20 | 21 | The sections below further delineate between automated and manual workflows that are in use. More information on triggering workflows (both automatically and manually) can be found here: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow. 22 | 23 | #### Automated Workflows 24 | 25 | Some workflows are configured to run automatically based on certain GitHub events. Examples of these workflows are listed below: 26 | 27 | - `pr.yaml` - runs in response to pull requests being opened 28 | - `merge.yaml` - runs anytime a change is pushed to the `master` branch (i.e. when a PR is merged) 29 | 30 | #### Manual Workflows 31 | 32 | Some workflows are configured to run based on a manual invocation from a maintainer. Examples of these workflows are listed below: 33 | 34 | - `release.yaml` - runs a workflow to build, test, and release the `alarmdecoder` Python packages to PyPI 35 | 36 | More information on manually triggering GitHub Actions workflows can be found here: https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow. 37 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-include docs/build *.* 4 | recursive-include examples *.py 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. _AlarmDecoder: http://www.alarmdecoder.com 2 | .. _ser2sock: http://github.com/nutechsoftware/ser2sock 3 | .. _pyftdi: https://github.com/eblot/pyftdi 4 | .. _pyusb: http://sourceforge.net/apps/trac/pyusb 5 | .. _pyserial: http://pyserial.sourceforge.net 6 | .. _pyopenssl: https://launchpad.net/pyopenssl 7 | .. _readthedocs: http://alarmdecoder.readthedocs.org 8 | .. _examples: http://github.com/nutechsoftware/alarmdecoder/tree/master/examples 9 | 10 | ============ 11 | AlarmDecoder 12 | ============ 13 | 14 | .. image:: https://img.shields.io/pypi/v/alarmdecoder 15 | :target: https://pypi.org/project/alarmdecoder/ 16 | .. image:: https://img.shields.io/github/actions/workflow/status/nutechsoftware/alarmdecoder/merge.yaml?label=tests 17 | :target: https://github.com/nutechsoftware/alarmdecoder/actions/workflows/merge.yaml 18 | 19 | ------- 20 | Summary 21 | ------- 22 | 23 | This Python library aims to provide a consistent interface for the 24 | `AlarmDecoder`_ product line. (AD2USB, AD2SERIAL and AD2PI). 25 | This also includes devices that have been exposed via `ser2sock`_, which 26 | supports encryption via SSL/TLS. 27 | 28 | ------------ 29 | Installation 30 | ------------ 31 | 32 | AlarmDecoder can be installed through ``pip``:: 33 | 34 | pip install alarmdecoder 35 | 36 | or from source:: 37 | 38 | python setup.py install 39 | 40 | * Note: ``python-setuptools`` is required for installation. 41 | 42 | ------------ 43 | Requirements 44 | ------------ 45 | 46 | Required: 47 | 48 | * An `AlarmDecoder`_ device 49 | * Python 2.7 50 | * `pyserial`_ >= 2.7 51 | 52 | Optional: 53 | 54 | * `pyftdi`_ >= 0.9.0 55 | * `pyusb`_ >= 1.0.0b1 56 | * `pyopenssl`_ 57 | 58 | ------------- 59 | Documentation 60 | ------------- 61 | 62 | API documentation can be found at `readthedocs`_. 63 | 64 | -------- 65 | Examples 66 | -------- 67 | 68 | A basic example is included below. Please see the `examples`_ directory for 69 | more.:: 70 | 71 | import time 72 | from alarmdecoder import AlarmDecoder 73 | from alarmdecoder.devices import SerialDevice 74 | 75 | def main(): 76 | """ 77 | Example application that prints messages from the panel to the terminal. 78 | """ 79 | try: 80 | # Retrieve the first USB device 81 | device = AlarmDecoder(SerialDevice(interface='/dev/ttyUSB0')) 82 | 83 | # Set up an event handler and open the device 84 | device.on_message += handle_message 85 | with device.open(baudrate=115200): 86 | while True: 87 | time.sleep(1) 88 | 89 | except Exception as ex: 90 | print ('Exception:', ex) 91 | 92 | def handle_message(sender, message): 93 | """ 94 | Handles message events from the AlarmDecoder. 95 | """ 96 | print sender, message.raw 97 | 98 | if __name__ == '__main__': 99 | main() 100 | -------------------------------------------------------------------------------- /alarmdecoder/__init__.py: -------------------------------------------------------------------------------- 1 | from alarmdecoder.decoder import AlarmDecoder 2 | import alarmdecoder.decoder 3 | import alarmdecoder.devices 4 | import alarmdecoder.util 5 | import alarmdecoder.messages 6 | import alarmdecoder.zonetracking 7 | 8 | __all__ = ['AlarmDecoder', 'decoder', 'devices', 'util', 'messages', 'zonetracking'] -------------------------------------------------------------------------------- /alarmdecoder/devices/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_device import Device 2 | from .serial_device import SerialDevice 3 | from .socket_device import SocketDevice 4 | from .usb_device import USBDevice 5 | 6 | __all__ = ['Device', 'SerialDevice', 'SocketDevice', 'USBDevice'] -------------------------------------------------------------------------------- /alarmdecoder/devices/base_device.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the base device type for the `AlarmDecoder`_ (AD2) family. 3 | 4 | .. _AlarmDecoder: http://www.alarmdecoder.com 5 | 6 | .. moduleauthor:: Scott Petersen 7 | """ 8 | 9 | import threading 10 | 11 | from ..util import CommError, TimeoutError, InvalidMessageError 12 | from ..event import event 13 | 14 | try: 15 | from OpenSSL import SSL, crypto 16 | 17 | have_openssl = True 18 | 19 | except ImportError: 20 | class SSL: 21 | class Error(BaseException): 22 | pass 23 | 24 | class WantReadError(BaseException): 25 | pass 26 | 27 | class SysCallError(BaseException): 28 | pass 29 | 30 | have_openssl = False 31 | 32 | 33 | class Device(object): 34 | """ 35 | Base class for all `AlarmDecoder`_ (AD2) device types. 36 | """ 37 | 38 | # Generic device events 39 | on_open = event.Event("This event is called when the device has been opened.\n\n**Callback definition:** *def callback(device)*") 40 | on_close = event.Event("This event is called when the device has been closed.\n\n**Callback definition:** def callback(device)*") 41 | 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)*") 42 | on_write = event.Event("This event is called when data has been written to the device.\n\n**Callback definition:** def callback(device, data)*") 43 | 44 | def __init__(self): 45 | """ 46 | Constructor 47 | """ 48 | self._id = '' 49 | self._buffer = b'' 50 | self._device = None 51 | self._running = False 52 | self._read_thread = None 53 | 54 | def __enter__(self): 55 | """ 56 | Support for context manager __enter__. 57 | """ 58 | return self 59 | 60 | def __exit__(self, exc_type, exc_value, traceback): 61 | """ 62 | Support for context manager __exit__. 63 | """ 64 | self.close() 65 | 66 | return False 67 | 68 | @property 69 | def id(self): 70 | """ 71 | Retrieve the device ID. 72 | 73 | :returns: identification string for the device 74 | """ 75 | return self._id 76 | 77 | @id.setter 78 | def id(self, value): 79 | """ 80 | Sets the device ID. 81 | 82 | :param value: device identification string 83 | :type value: string 84 | """ 85 | self._id = value 86 | 87 | def is_reader_alive(self): 88 | """ 89 | Indicates whether or not the reader thread is alive. 90 | 91 | :returns: whether or not the reader thread is alive 92 | """ 93 | return self._read_thread.is_alive() 94 | 95 | def stop_reader(self): 96 | """ 97 | Stops the reader thread. 98 | """ 99 | self._read_thread.stop() 100 | 101 | def close(self): 102 | """ 103 | Closes the device. 104 | """ 105 | try: 106 | self._running = False 107 | self._read_thread.stop() 108 | self._device.close() 109 | 110 | except Exception: 111 | pass 112 | 113 | self.on_close() 114 | 115 | class ReadThread(threading.Thread): 116 | """ 117 | Reader thread which processes messages from the device. 118 | """ 119 | 120 | READ_TIMEOUT = 10 121 | """Timeout for the reader thread.""" 122 | 123 | def __init__(self, device): 124 | """ 125 | Constructor 126 | 127 | :param device: device used by the reader thread 128 | :type device: :py:class:`~alarmdecoder.devices.Device` 129 | """ 130 | threading.Thread.__init__(self) 131 | self._device = device 132 | self._running = False 133 | 134 | def stop(self): 135 | """ 136 | Stops the running thread. 137 | """ 138 | self._running = False 139 | 140 | def run(self): 141 | """ 142 | The actual read process. 143 | """ 144 | self._running = True 145 | 146 | while self._running: 147 | try: 148 | self._device.read_line(timeout=self.READ_TIMEOUT) 149 | 150 | except TimeoutError: 151 | pass 152 | 153 | except InvalidMessageError: 154 | pass 155 | 156 | except SSL.WantReadError: 157 | pass 158 | 159 | except CommError as err: 160 | self._device.close() 161 | 162 | except Exception as err: 163 | self._device.close() 164 | self._running = False 165 | raise -------------------------------------------------------------------------------- /alarmdecoder/devices/serial_device.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the :py:class:`SerialDevice` interface for the `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_. 3 | 4 | .. _AD2USB: http://www.alarmdecoder.com 5 | .. _AD2SERIAL: http://www.alarmdecoder.com 6 | .. _AD2PI: http://www.alarmdecoder.com 7 | 8 | .. moduleauthor:: Scott Petersen 9 | """ 10 | 11 | import threading 12 | import serial 13 | import serial.tools.list_ports 14 | import select 15 | import sys 16 | from .base_device import Device 17 | from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack, filter_ad2prot_byte 18 | 19 | 20 | class SerialDevice(Device): 21 | """ 22 | `AD2USB`_, `AD2SERIAL`_ or `AD2PI`_ device utilizing the PySerial interface. 23 | """ 24 | 25 | # Constants 26 | BAUDRATE = 19200 27 | """Default baudrate for Serial devices.""" 28 | 29 | @staticmethod 30 | def find_all(pattern=None): 31 | """ 32 | Returns all serial ports present. 33 | 34 | :param pattern: pattern to search for when retrieving serial ports 35 | :type pattern: string 36 | 37 | :returns: list of devices 38 | :raises: :py:class:`~alarmdecoder.util.CommError` 39 | """ 40 | devices = [] 41 | 42 | try: 43 | if pattern: 44 | devices = serial.tools.list_ports.grep(pattern) 45 | else: 46 | devices = serial.tools.list_ports.comports() 47 | 48 | except serial.SerialException as err: 49 | raise CommError('Error enumerating serial devices: {0}'.format(str(err)), err) 50 | 51 | return devices 52 | 53 | @property 54 | def interface(self): 55 | """ 56 | Retrieves the interface used to connect to the device. 57 | 58 | :returns: interface used to connect to the device 59 | """ 60 | return self._port 61 | 62 | @interface.setter 63 | def interface(self, value): 64 | """ 65 | Sets the interface used to connect to the device. 66 | 67 | :param value: name of the serial device 68 | :type value: string 69 | """ 70 | self._port = value 71 | 72 | def __init__(self, interface=None): 73 | """ 74 | Constructor 75 | 76 | :param interface: device to open 77 | :type interface: string 78 | """ 79 | Device.__init__(self) 80 | 81 | self._port = interface 82 | self._id = interface 83 | # Timeout = non-blocking to match pyftdi. 84 | self._device = serial.Serial(timeout=0, writeTimeout=0) 85 | 86 | def open(self, baudrate=BAUDRATE, no_reader_thread=False): 87 | """ 88 | Opens the device. 89 | 90 | :param baudrate: baudrate to use with the device 91 | :type baudrate: int 92 | :param no_reader_thread: whether or not to automatically start the 93 | reader thread. 94 | :type no_reader_thread: bool 95 | 96 | :raises: :py:class:`~alarmdecoder.util.NoDeviceError` 97 | """ 98 | # Set up the defaults 99 | if baudrate is None: 100 | baudrate = SerialDevice.BAUDRATE 101 | 102 | if self._port is None: 103 | raise NoDeviceError('No device interface specified.') 104 | 105 | self._read_thread = Device.ReadThread(self) 106 | 107 | # Open the device and start up the reader thread. 108 | try: 109 | self._device.port = self._port 110 | self._device.open() 111 | # NOTE: Setting the baudrate before opening the 112 | # port caused issues with Moschip 7840/7820 113 | # USB Serial Driver converter. (mos7840) 114 | # 115 | # Moving it to this point seems to resolve 116 | # all issues with it. 117 | self._device.baudrate = baudrate 118 | 119 | except (serial.SerialException, ValueError, OSError) as err: 120 | raise NoDeviceError('Error opening device on {0}.'.format(self._port), err) 121 | 122 | else: 123 | self._running = True 124 | self.on_open() 125 | 126 | if not no_reader_thread: 127 | self._read_thread.start() 128 | 129 | return self 130 | 131 | def close(self): 132 | """ 133 | Closes the device. 134 | """ 135 | try: 136 | Device.close(self) 137 | 138 | except Exception: 139 | pass 140 | 141 | def fileno(self): 142 | """ 143 | Returns the file number associated with the device 144 | 145 | :returns: int 146 | """ 147 | return self._device.fileno() 148 | 149 | def write(self, data): 150 | """ 151 | Writes data to the device. 152 | 153 | :param data: data to write 154 | :type data: string 155 | 156 | :raises: py:class:`~alarmdecoder.util.CommError` 157 | """ 158 | try: 159 | # Hack to support unicode under Python 2.x 160 | if isinstance(data, str) or (sys.version_info < (3,) and isinstance(data, unicode)): 161 | data = data.encode('utf-8') 162 | 163 | self._device.write(data) 164 | 165 | except serial.SerialTimeoutException: 166 | pass 167 | 168 | except serial.SerialException as err: 169 | raise CommError('Error writing to device.', err) 170 | 171 | else: 172 | self.on_write(data=data) 173 | 174 | def read(self): 175 | """ 176 | Reads a single character from the device. 177 | 178 | :returns: character read from the device 179 | :raises: :py:class:`~alarmdecoder.util.CommError` 180 | """ 181 | data = b'' 182 | 183 | try: 184 | read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) 185 | 186 | if len(read_ready) != 0: 187 | data = filter_ad2prot_byte(self._device.read(1)) 188 | 189 | except serial.SerialException as err: 190 | raise CommError('Error reading from device: {0}'.format(str(err)), err) 191 | 192 | return data.decode('utf-8') 193 | 194 | def read_line(self, timeout=0.0, purge_buffer=False): 195 | """ 196 | Reads a line from the device. 197 | 198 | :param timeout: read timeout 199 | :type timeout: float 200 | :param purge_buffer: Indicates whether to purge the buffer prior to 201 | reading. 202 | :type purge_buffer: bool 203 | 204 | :returns: line that was read 205 | :raises: :py:class:`~alarmdecoder.util.CommError`, :py:class:`~alarmdecoder.util.TimeoutError` 206 | """ 207 | 208 | def timeout_event(): 209 | """Handles read timeout event""" 210 | timeout_event.reading = False 211 | timeout_event.reading = True 212 | 213 | if purge_buffer: 214 | self._buffer = b'' 215 | 216 | got_line, ret = False, None 217 | 218 | timer = threading.Timer(timeout, timeout_event) 219 | if timeout > 0: 220 | timer.start() 221 | 222 | try: 223 | while timeout_event.reading: 224 | read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) 225 | 226 | if len(read_ready) == 0: 227 | continue 228 | 229 | buf = filter_ad2prot_byte(self._device.read(1)) 230 | 231 | if buf != b'': 232 | self._buffer += buf 233 | 234 | if buf == b"\n": 235 | self._buffer = self._buffer.rstrip(b"\r\n") 236 | 237 | if len(self._buffer) > 0: 238 | got_line = True 239 | break 240 | except (OSError, serial.SerialException) as err: 241 | raise CommError('Error reading from device: {0}'.format(str(err)), err) 242 | 243 | else: 244 | if got_line: 245 | ret, self._buffer = self._buffer, b'' 246 | 247 | self.on_read(data=ret) 248 | 249 | else: 250 | raise TimeoutError('Timeout while waiting for line terminator.') 251 | 252 | finally: 253 | timer.cancel() 254 | 255 | return ret.decode('utf-8') 256 | 257 | def purge(self): 258 | """ 259 | Purges read/write buffers. 260 | """ 261 | self._device.flushInput() 262 | self._device.flushOutput() 263 | -------------------------------------------------------------------------------- /alarmdecoder/event/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Event'] 2 | -------------------------------------------------------------------------------- /alarmdecoder/event/event.py: -------------------------------------------------------------------------------- 1 | # event.py (improved) 2 | # 3 | 4 | # Based on pyevent originally found at http://www.emptypage.jp/notes/pyevent.en.html 5 | # 6 | # License: https://creativecommons.org/licenses/by/2.1/jp/deed.en 7 | # 8 | # Changes: 9 | # * Added type check in fire() 10 | # * Removed earg from fire() and added support for args/kwargs. 11 | 12 | 13 | class Event(object): 14 | 15 | def __init__(self, doc=None): 16 | self.__doc__ = doc 17 | 18 | def __get__(self, obj, objtype=None): 19 | if obj is None: 20 | return self 21 | return EventHandler(self, obj) 22 | 23 | def __set__(self, obj, value): 24 | pass 25 | 26 | 27 | class EventHandler(object): 28 | 29 | def __init__(self, event, obj): 30 | 31 | self.event = event 32 | self.obj = obj 33 | 34 | def __iter__(self): 35 | return iter(self._getfunctionlist()) 36 | 37 | def _getfunctionlist(self): 38 | 39 | """(internal use) """ 40 | 41 | try: 42 | eventhandler = self.obj.__eventhandler__ 43 | except AttributeError: 44 | eventhandler = self.obj.__eventhandler__ = {} 45 | return eventhandler.setdefault(self.event, []) 46 | 47 | def add(self, func): 48 | 49 | """Add new event handler function. 50 | 51 | Event handler function must be defined like func(sender, earg). 52 | You can add handler also by using '+=' operator. 53 | """ 54 | 55 | self._getfunctionlist().append(func) 56 | return self 57 | 58 | def remove(self, func): 59 | 60 | """Remove existing event handler function. 61 | 62 | You can remove handler also by using '-=' operator. 63 | """ 64 | 65 | self._getfunctionlist().remove(func) 66 | return self 67 | 68 | def clear(self): 69 | del self._getfunctionlist()[:] 70 | return self 71 | 72 | def fire(self, *args, **kwargs): 73 | 74 | """Fire event and call all handler functions 75 | 76 | You can call EventHandler object itself like e(*args, **kwargs) instead of 77 | e.fire(*args, **kwargs). 78 | """ 79 | 80 | for func in self._getfunctionlist(): 81 | if type(func) == EventHandler: 82 | func.fire(*args, **kwargs) 83 | else: 84 | func(self.obj, *args, **kwargs) 85 | 86 | __iadd__ = add 87 | __isub__ = remove 88 | __call__ = fire 89 | -------------------------------------------------------------------------------- /alarmdecoder/messages/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_message import BaseMessage 2 | from .panel_message import Message 3 | from .expander_message import ExpanderMessage 4 | from .lrr import LRRMessage 5 | from .rf_message import RFMessage 6 | from .aui_message import AUIMessage 7 | 8 | 9 | __all__ = ['BaseMessage', 'Message', 'ExpanderMessage', 'LRRMessage', 'RFMessage', 'AUIMessage'] 10 | -------------------------------------------------------------------------------- /alarmdecoder/messages/aui_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message representations received from the panel through the `AlarmDecoder`_ (AD2) 3 | devices. 4 | 5 | :py:class:`AUIMessage`: Message received destined for an AUI keypad. 6 | 7 | .. _AlarmDecoder: http://www.alarmdecoder.com 8 | 9 | .. moduleauthor:: Scott Petersen 10 | """ 11 | 12 | from . import BaseMessage 13 | from ..util import InvalidMessageError 14 | 15 | class AUIMessage(BaseMessage): 16 | """ 17 | Represents a message destined for an AUI keypad. 18 | """ 19 | 20 | value = None 21 | """Raw value of the AUI message""" 22 | 23 | def __init__(self, data=None): 24 | """ 25 | Constructor 26 | 27 | :param data: message data to parse 28 | :type data: string 29 | """ 30 | BaseMessage.__init__(self, data) 31 | 32 | if data is not None: 33 | self._parse_message(data) 34 | 35 | def _parse_message(self, data): 36 | header, value = data.split(':') 37 | 38 | self.value = value 39 | 40 | def dict(self, **kwargs): 41 | """ 42 | Dictionary representation. 43 | """ 44 | return dict( 45 | value = self.value, 46 | **kwargs 47 | ) -------------------------------------------------------------------------------- /alarmdecoder/messages/base_message.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | try: 4 | from reprlib import repr 5 | except ImportError: 6 | from repr import repr 7 | 8 | class BaseMessage(object): 9 | """ 10 | Base class for messages. 11 | """ 12 | 13 | raw = None 14 | """The raw message text""" 15 | 16 | timestamp = None 17 | """The timestamp of the message""" 18 | 19 | def __init__(self, data=None): 20 | """ 21 | Constructor 22 | """ 23 | self.timestamp = datetime.datetime.now() 24 | self.raw = data 25 | 26 | def __str__(self): 27 | """ 28 | String conversion operator. 29 | """ 30 | return self.raw 31 | 32 | def dict(self, **kwargs): 33 | """ 34 | Dictionary representation. 35 | """ 36 | return dict( 37 | time=self.timestamp, 38 | mesg=self.raw, 39 | **kwargs 40 | ) 41 | 42 | def __repr__(self): 43 | """ 44 | String representation. 45 | """ 46 | return repr(self.dict()) 47 | -------------------------------------------------------------------------------- /alarmdecoder/messages/expander_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message representations received from the panel through the `AlarmDecoder`_ (AD2) 3 | devices. 4 | 5 | :py:class:`ExpanderMessage`: Messages received from Relay or Zone expander modules. 6 | 7 | .. _AlarmDecoder: http://www.alarmdecoder.com 8 | 9 | .. moduleauthor:: Scott Petersen 10 | """ 11 | 12 | from . import BaseMessage 13 | from ..util import InvalidMessageError 14 | 15 | class ExpanderMessage(BaseMessage): 16 | """ 17 | Represents a message from a zone or relay expansion module. 18 | """ 19 | 20 | ZONE = 0 21 | """Flag indicating that the expander message relates to a Zone Expander.""" 22 | RELAY = 1 23 | """Flag indicating that the expander message relates to a Relay Expander.""" 24 | 25 | type = None 26 | """Expander message type: ExpanderMessage.ZONE or ExpanderMessage.RELAY""" 27 | address = -1 28 | """Address of expander""" 29 | channel = -1 30 | """Channel on the expander""" 31 | value = -1 32 | """Value associated with the message""" 33 | 34 | def __init__(self, data=None): 35 | """ 36 | Constructor 37 | 38 | :param data: message data to parse 39 | :type data: string 40 | """ 41 | BaseMessage.__init__(self, data) 42 | 43 | if data is not None: 44 | self._parse_message(data) 45 | 46 | def _parse_message(self, data): 47 | """ 48 | Parse the raw message from the device. 49 | 50 | :param data: message data 51 | :type data: string 52 | 53 | :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` 54 | """ 55 | try: 56 | header, values = data.split(':') 57 | address, channel, value = values.split(',') 58 | 59 | self.address = int(address) 60 | self.channel = int(channel) 61 | self.value = int(value) 62 | 63 | except ValueError: 64 | raise InvalidMessageError('Received invalid message: {0}'.format(data)) 65 | 66 | if header == '!EXP': 67 | self.type = ExpanderMessage.ZONE 68 | elif header == '!REL': 69 | self.type = ExpanderMessage.RELAY 70 | else: 71 | raise InvalidMessageError('Unknown expander message header: {0}'.format(data)) 72 | 73 | def dict(self, **kwargs): 74 | """ 75 | Dictionary representation. 76 | """ 77 | return dict( 78 | time = self.timestamp, 79 | address = self.address, 80 | channel = self.channel, 81 | value = self.value, 82 | **kwargs 83 | ) 84 | -------------------------------------------------------------------------------- /alarmdecoder/messages/lrr/__init__.py: -------------------------------------------------------------------------------- 1 | from .message import LRRMessage 2 | from .system import LRRSystem 3 | from .events import get_event_description, get_event_source, get_event_data_type, LRR_EVENT_TYPE, LRR_EVENT_STATUS, LRR_CID_EVENT, LRR_DSC_EVENT, LRR_ADEMCO_EVENT, \ 4 | LRR_ALARMDECODER_EVENT, LRR_UNKNOWN_EVENT, LRR_CID_MAP, LRR_DSC_MAP, LRR_ADEMCO_MAP, \ 5 | LRR_ALARMDECODER_MAP, LRR_UNKNOWN_MAP, LRR_DATA_TYPE 6 | 7 | __all__ = ['get_event_description', 'get_event_source', 'get_event_data_type', 'LRRMessage', 'LRR_EVENT_TYPE', 'LRR_EVENT_STATUS', 'LRR_CID_EVENT', 'LRR_DSC_EVENT', 8 | 'LRR_ADEMCO_EVENT', 'LRR_ALARMDECODER_EVENT', 'LRR_UNKNOWN_EVENT', 'LRR_CID_MAP', 9 | 'LRR_DSC_MAP', 'LRR_ADEMCO_MAP', 'LRR_ALARMDECODER_MAP', 'LRR_UNKNOWN_MAP', 'LRR_DATA_TYPE'] 10 | -------------------------------------------------------------------------------- /alarmdecoder/messages/lrr/message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message representations received from the panel through the `AlarmDecoder`_ (AD2) 3 | devices. 4 | 5 | :py:class:`LRRMessage`: Message received from a long-range radio module. 6 | 7 | .. _AlarmDecoder: http://www.alarmdecoder.com 8 | 9 | .. moduleauthor:: Scott Petersen 10 | """ 11 | 12 | from .. import BaseMessage 13 | from ...util import InvalidMessageError 14 | 15 | from .events import LRR_EVENT_TYPE, get_event_description, get_event_data_type, get_event_source 16 | 17 | 18 | class LRRMessage(BaseMessage): 19 | """ 20 | Represent a message from a Long Range Radio or emulated Long Range Radio. 21 | """ 22 | event_data_type = None 23 | """Data Type for specific LRR message. User or Zone""" 24 | event_data = None 25 | """Data associated with the LRR message. Usually user ID or zone.""" 26 | partition = -1 27 | """The partition that this message applies to.""" 28 | event_type = None 29 | """The type of the event that occurred.""" 30 | version = 0 31 | """LRR message version""" 32 | 33 | report_code = 0xFF 34 | """The report code used to override the last two digits of the event type.""" 35 | event_prefix = '' 36 | """Extracted prefix for the event_type.""" 37 | event_source = LRR_EVENT_TYPE.UNKNOWN 38 | """Extracted event type source.""" 39 | event_status = 0 40 | """Event status flag that represents triggered or restored events.""" 41 | event_code = 0 42 | """Event code for the LRR message.""" 43 | event_description = '' 44 | """Human-readable description of LRR event.""" 45 | 46 | def __init__(self, data=None, skip_report_override=False): 47 | """ 48 | Constructor 49 | 50 | :param data: message data to parse 51 | :type data: string 52 | """ 53 | BaseMessage.__init__(self, data) 54 | 55 | self.skip_report_override = skip_report_override 56 | 57 | if data is not None: 58 | self._parse_message(data) 59 | 60 | def _parse_message(self, data): 61 | """ 62 | Parses the raw message from the device. 63 | 64 | :param data: message data to parse 65 | :type data: string 66 | 67 | :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` 68 | """ 69 | try: 70 | _, values = data.split(':') 71 | values = values.split(',') 72 | 73 | # Handle older-format events 74 | if len(values) <= 3: 75 | self.event_data, self.partition, self.event_type = values 76 | self.version = 1 77 | 78 | # Newer-format events 79 | else: 80 | self.event_data, self.partition, self.event_type, self.report_code = values 81 | self.version = 2 82 | 83 | event_type_data = self.event_type.split('_') 84 | self.event_prefix = event_type_data[0] # Ex: CID 85 | self.event_source = get_event_source(self.event_prefix) # Ex: LRR_EVENT_TYPE.CID 86 | self.event_status = int(event_type_data[1][0]) # Ex: 1 or 3 87 | self.event_code = int(event_type_data[1][1:], 16) # Ex: 0x100 = Medical 88 | 89 | # replace last 2 digits of event_code with report_code, if applicable. 90 | if not self.skip_report_override and self.report_code not in ['00', 'ff']: 91 | self.event_code = int(event_type_data[1][1] + self.report_code, 16) 92 | self.event_description = get_event_description(self.event_source, self.event_code) 93 | self.event_data_type = get_event_data_type(self.event_source, self.event_code) 94 | 95 | except ValueError: 96 | raise InvalidMessageError('Received invalid message: {0}'.format(data)) 97 | 98 | 99 | def dict(self, **kwargs): 100 | """ 101 | Dictionary representation 102 | """ 103 | return dict( 104 | time = self.timestamp, 105 | event_data = self.event_data, 106 | event_data_type = self.event_data_type, 107 | event_type = self.event_type, 108 | partition = self.partition, 109 | report_code = self.report_code, 110 | event_prefix = self.event_prefix, 111 | event_source = self.event_source, 112 | event_status = self.event_status, 113 | event_code = hex(self.event_code), 114 | event_description = self.event_description, 115 | **kwargs 116 | ) 117 | -------------------------------------------------------------------------------- /alarmdecoder/messages/lrr/system.py: -------------------------------------------------------------------------------- 1 | """ 2 | Primary system for handling LRR events. 3 | 4 | .. moduleauthor:: Scott Petersen 5 | """ 6 | 7 | from .events import LRR_EVENT_TYPE, LRR_EVENT_STATUS, LRR_CID_EVENT, LRR_DATA_TYPE 8 | from .events import LRR_FIRE_EVENTS, LRR_POWER_EVENTS, LRR_BYPASS_EVENTS, LRR_BATTERY_EVENTS, \ 9 | LRR_PANIC_EVENTS, LRR_ARM_EVENTS, LRR_STAY_EVENTS, LRR_ALARM_EVENTS 10 | 11 | 12 | class LRRSystem(object): 13 | """ 14 | Handles LRR events and triggers higher-level events in the AlarmDecoder object. 15 | """ 16 | 17 | def __init__(self, alarmdecoder_object): 18 | """ 19 | Constructor 20 | 21 | :param alarmdecoder_object: Main AlarmDecoder object 22 | :type alarmdecoder_object: :py:class:`~alarmdecoder.AlarmDecoder` 23 | """ 24 | self._alarmdecoder = alarmdecoder_object 25 | 26 | def update(self, message): 27 | """ 28 | Updates the states in the primary AlarmDecoder object based on 29 | the LRR message provided. 30 | 31 | :param message: LRR message object 32 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 33 | """ 34 | # Firmware version < 2.2a.8.6 35 | if message.version == 1: 36 | if message.event_type == 'ALARM_PANIC': 37 | self._alarmdecoder._update_panic_status(True) 38 | 39 | elif message.event_type == 'CANCEL': 40 | self._alarmdecoder._update_panic_status(False) 41 | 42 | # Firmware version >= 2.2a.8.6 43 | elif message.version == 2: 44 | source = message.event_source 45 | if source == LRR_EVENT_TYPE.CID: 46 | self._handle_cid_message(message) 47 | elif source == LRR_EVENT_TYPE.DSC: 48 | self._handle_dsc_message(message) 49 | elif source == LRR_EVENT_TYPE.ADEMCO: 50 | self._handle_ademco_message(message) 51 | elif source == LRR_EVENT_TYPE.ALARMDECODER: 52 | self._handle_alarmdecoder_message(message) 53 | elif source == LRR_EVENT_TYPE.UNKNOWN: 54 | self._handle_unknown_message(message) 55 | else: 56 | pass 57 | 58 | def _handle_cid_message(self, message): 59 | """ 60 | Handles ContactID LRR events. 61 | 62 | :param message: LRR message object 63 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 64 | """ 65 | status = self._get_event_status(message) 66 | if status is None: 67 | return 68 | 69 | if message.event_code in LRR_FIRE_EVENTS: 70 | if message.event_code == LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: 71 | status = False 72 | 73 | self._alarmdecoder._update_fire_status(status=status) 74 | 75 | if message.event_code in LRR_ALARM_EVENTS: 76 | kwargs = {} 77 | field_name = 'zone' 78 | if not status: 79 | field_name = 'user' 80 | 81 | kwargs[field_name] = int(message.event_data) 82 | self._alarmdecoder._update_alarm_status(status=status, **kwargs) 83 | 84 | if message.event_code in LRR_POWER_EVENTS: 85 | self._alarmdecoder._update_power_status(status=status) 86 | 87 | if message.event_code in LRR_BYPASS_EVENTS: 88 | self._alarmdecoder._update_zone_bypass_status(status=status, zone=int(message.event_data)) 89 | 90 | if message.event_code in LRR_BATTERY_EVENTS: 91 | self._alarmdecoder._update_battery_status(status=status) 92 | 93 | if message.event_code in LRR_PANIC_EVENTS: 94 | if message.event_code == LRR_CID_EVENT.OPENCLOSE_CANCEL_BY_USER: 95 | status = False 96 | 97 | self._alarmdecoder._update_panic_status(status=status) 98 | 99 | if message.event_code in LRR_ARM_EVENTS: 100 | # NOTE: status on OPENCLOSE messages is backwards. 101 | status_stay = (message.event_status == LRR_EVENT_STATUS.RESTORE \ 102 | and message.event_code in LRR_STAY_EVENTS) 103 | 104 | if status_stay: 105 | status = False 106 | else: 107 | status = not status 108 | 109 | self._alarmdecoder._update_armed_status(status=status, status_stay=status_stay) 110 | 111 | def _handle_dsc_message(self, message): 112 | """ 113 | Handles DSC LRR events. 114 | 115 | :param message: LRR message object 116 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 117 | """ 118 | pass 119 | 120 | def _handle_ademco_message(self, message): 121 | """ 122 | Handles ADEMCO LRR events. 123 | 124 | :param message: LRR message object 125 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 126 | """ 127 | pass 128 | 129 | def _handle_alarmdecoder_message(self, message): 130 | """ 131 | Handles AlarmDecoder LRR events. 132 | 133 | :param message: LRR message object 134 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 135 | """ 136 | pass 137 | 138 | def _handle_unknown_message(self, message): 139 | """ 140 | Handles UNKNOWN LRR events. 141 | 142 | :param message: LRR message object 143 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 144 | """ 145 | # TODO: Log this somewhere useful. 146 | pass 147 | 148 | def _get_event_status(self, message): 149 | """ 150 | Retrieves the boolean status of an LRR message. 151 | 152 | :param message: LRR message object 153 | :type message: :py:class:`~alarmdecoder.messages.LRRMessage` 154 | 155 | :returns: Boolean indicating whether the event was triggered or restored. 156 | """ 157 | status = None 158 | 159 | if message.event_status == LRR_EVENT_STATUS.TRIGGER: 160 | status = True 161 | elif message.event_status == LRR_EVENT_STATUS.RESTORE: 162 | status = False 163 | 164 | return status 165 | -------------------------------------------------------------------------------- /alarmdecoder/messages/panel_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message representations received from the panel through the `AlarmDecoder`_ (AD2) 3 | devices. 4 | 5 | :py:class:`Message`: The standard and most common message received from a panel. 6 | 7 | .. _AlarmDecoder: http://www.alarmdecoder.com 8 | 9 | .. moduleauthor:: Scott Petersen 10 | """ 11 | 12 | import re 13 | 14 | from . import BaseMessage 15 | from ..util import InvalidMessageError 16 | from ..panels import PANEL_TYPES, ADEMCO, DSC 17 | 18 | class Message(BaseMessage): 19 | """ 20 | Represents a message from the alarm panel. 21 | """ 22 | 23 | ready = False 24 | """Indicates whether or not the panel is in a ready state.""" 25 | armed_away = False 26 | """Indicates whether or not the panel is armed away.""" 27 | armed_home = False 28 | """Indicates whether or not the panel is armed home.""" 29 | backlight_on = False 30 | """Indicates whether or not the keypad backlight is on.""" 31 | programming_mode = False 32 | """Indicates whether or not we're in programming mode.""" 33 | beeps = -1 34 | """Number of beeps associated with a message.""" 35 | zone_bypassed = False 36 | """Indicates whether or not a zone is bypassed.""" 37 | ac_power = False 38 | """Indicates whether or not the panel is on AC power.""" 39 | chime_on = False 40 | """Indicates whether or not the chime is enabled.""" 41 | alarm_event_occurred = False 42 | """Indicates whether or not an alarm event has occurred.""" 43 | alarm_sounding = False 44 | """Indicates whether or not an alarm is sounding.""" 45 | battery_low = False 46 | """Indicates whether or not there is a low battery.""" 47 | entry_delay_off = False 48 | """Indicates whether or not the entry delay is enabled.""" 49 | fire_alarm = False 50 | """Indicates whether or not a fire alarm is sounding.""" 51 | check_zone = False 52 | """Indicates whether or not there are zones that require attention.""" 53 | perimeter_only = False 54 | """Indicates whether or not the perimeter is armed.""" 55 | system_fault = -1 56 | """Indicates if any panel specific system fault exists.""" 57 | panel_type = ADEMCO 58 | """Indicates which panel type was the source of this message.""" 59 | numeric_code = None 60 | """The numeric code associated with the message.""" 61 | text = None 62 | """The human-readable text to be displayed on the panel LCD.""" 63 | cursor_location = -1 64 | """Current cursor location on the keypad.""" 65 | mask = 0xFFFFFFFF 66 | """Address mask this message is intended for.""" 67 | bitfield = None 68 | """The bitfield associated with this message.""" 69 | panel_data = None 70 | """The panel data field associated with this message.""" 71 | 72 | 73 | _regex = re.compile(r'^(!KPM:){0,1}(\[[a-fA-F0-9\-]+\]),([a-fA-F0-9]+),(\[[a-fA-F0-9]+\]),(".+")$') 74 | 75 | def __init__(self, data=None): 76 | """ 77 | Constructor 78 | 79 | :param data: message data to parse 80 | :type data: string 81 | """ 82 | BaseMessage.__init__(self, data) 83 | 84 | if data is not None: 85 | self._parse_message(data) 86 | 87 | def _parse_message(self, data): 88 | """ 89 | Parse the message from the device. 90 | 91 | :param data: message data 92 | :type data: string 93 | 94 | :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` 95 | """ 96 | match = self._regex.match(str(data)) 97 | 98 | if match is None: 99 | raise InvalidMessageError('Received invalid message: {0}'.format(data)) 100 | 101 | header, self.bitfield, self.numeric_code, self.panel_data, alpha = match.group(1, 2, 3, 4, 5) 102 | 103 | is_bit_set = lambda bit: not self.bitfield[bit] == "0" 104 | 105 | self.ready = is_bit_set(1) 106 | self.armed_away = is_bit_set(2) 107 | self.armed_home = is_bit_set(3) 108 | self.backlight_on = is_bit_set(4) 109 | self.programming_mode = is_bit_set(5) 110 | self.beeps = int(self.bitfield[6], 16) 111 | self.zone_bypassed = is_bit_set(7) 112 | self.ac_power = is_bit_set(8) 113 | self.chime_on = is_bit_set(9) 114 | self.alarm_event_occurred = is_bit_set(10) 115 | self.alarm_sounding = is_bit_set(11) 116 | self.battery_low = is_bit_set(12) 117 | self.entry_delay_off = is_bit_set(13) 118 | self.fire_alarm = is_bit_set(14) 119 | self.check_zone = is_bit_set(15) 120 | self.perimeter_only = is_bit_set(16) 121 | if self.bitfield[17] != '-': 122 | self.system_fault = int(self.bitfield[17], 16) 123 | if self.bitfield[18] in list(PANEL_TYPES): 124 | self.panel_type = PANEL_TYPES[self.bitfield[18]] 125 | # pos 20-21 - Unused. 126 | self.text = alpha.strip('"') 127 | self.mask = int(self.panel_data[3:3+8], 16) 128 | 129 | if self.panel_type in (ADEMCO, DSC): 130 | if int(self.panel_data[19:21], 16) & 0x01 > 0: 131 | # Current cursor location on the alpha display. 132 | self.cursor_location = int(self.panel_data[21:23], 16) 133 | 134 | def parse_numeric_code(self, force_hex=False): 135 | """ 136 | Parses and returns the numeric code as an integer. 137 | 138 | The numeric code can be either base 10 or base 16, depending on 139 | where the message came from. 140 | 141 | :param force_hex: force the numeric code to be processed as base 16. 142 | :type force_hex: boolean 143 | 144 | :raises: ValueError 145 | """ 146 | code = None 147 | got_error = False 148 | 149 | if not force_hex: 150 | try: 151 | code = int(self.numeric_code) 152 | except ValueError: 153 | got_error = True 154 | 155 | if force_hex or got_error: 156 | try: 157 | code = int(self.numeric_code, 16) 158 | except ValueError: 159 | raise 160 | 161 | return code 162 | 163 | def dict(self, **kwargs): 164 | """ 165 | Dictionary representation. 166 | """ 167 | return dict( 168 | time = self.timestamp, 169 | bitfield = self.bitfield, 170 | numeric_code = self.numeric_code, 171 | panel_data = self.panel_data, 172 | mask = self.mask, 173 | ready = self.ready, 174 | armed_away = self.armed_away, 175 | armed_home = self.armed_home, 176 | backlight_on = self.backlight_on, 177 | programming_mode = self.programming_mode, 178 | beeps = self.beeps, 179 | zone_bypassed = self.zone_bypassed, 180 | ac_power = self.ac_power, 181 | chime_on = self.chime_on, 182 | alarm_event_occurred = self.alarm_event_occurred, 183 | alarm_sounding = self.alarm_sounding, 184 | battery_low = self.battery_low, 185 | entry_delay_off = self.entry_delay_off, 186 | fire_alarm = self.fire_alarm, 187 | check_zone = self.check_zone, 188 | perimeter_only = self.perimeter_only, 189 | text = self.text, 190 | cursor_location = self.cursor_location, 191 | **kwargs 192 | ) 193 | -------------------------------------------------------------------------------- /alarmdecoder/messages/rf_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message representations received from the panel through the `AlarmDecoder`_ (AD2) 3 | devices. 4 | 5 | :py:class:`RFMessage`: Message received from an RF receiver module. 6 | 7 | .. _AlarmDecoder: http://www.alarmdecoder.com 8 | 9 | .. moduleauthor:: Scott Petersen 10 | """ 11 | 12 | from . import BaseMessage 13 | from ..util import InvalidMessageError 14 | 15 | class RFMessage(BaseMessage): 16 | """ 17 | Represents a message from an RF receiver. 18 | """ 19 | 20 | serial_number = None 21 | """Serial number of the RF device.""" 22 | value = -1 23 | """Value associated with this message.""" 24 | battery = False 25 | """Low battery indication""" 26 | supervision = False 27 | """Supervision required indication""" 28 | loop = [False for _ in list(range(4))] 29 | """Loop indicators""" 30 | 31 | def __init__(self, data=None): 32 | """ 33 | Constructor 34 | 35 | :param data: message data to parse 36 | :type data: string 37 | """ 38 | BaseMessage.__init__(self, data) 39 | 40 | if data is not None: 41 | self._parse_message(data) 42 | 43 | def _parse_message(self, data): 44 | """ 45 | Parses the raw message from the device. 46 | 47 | :param data: message data 48 | :type data: string 49 | 50 | :raises: :py:class:`~alarmdecoder.util.InvalidMessageError` 51 | """ 52 | try: 53 | _, values = data.split(':') 54 | self.serial_number, self.value = values.split(',') 55 | self.value = int(self.value, 16) 56 | 57 | is_bit_set = lambda b: self.value & (1 << (b - 1)) > 0 58 | 59 | # Bit 1 = unknown 60 | self.battery = is_bit_set(2) 61 | self.supervision = is_bit_set(3) 62 | # Bit 4 = unknown 63 | self.loop[2] = is_bit_set(5) 64 | self.loop[1] = is_bit_set(6) 65 | self.loop[3] = is_bit_set(7) 66 | self.loop[0] = is_bit_set(8) 67 | 68 | except ValueError: 69 | raise InvalidMessageError('Received invalid message: {0}'.format(data)) 70 | 71 | def dict(self, **kwargs): 72 | """ 73 | Dictionary representation. 74 | """ 75 | return dict( 76 | time = self.timestamp, 77 | serial_number = self.serial_number, 78 | value = self.value, 79 | battery = self.battery, 80 | supervision = self.supervision, 81 | **kwargs 82 | ) 83 | -------------------------------------------------------------------------------- /alarmdecoder/panels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Representations of Panels and their templates. 3 | 4 | .. moduleauthor:: Scott Petersen 5 | """ 6 | 7 | ADEMCO = 0 8 | DSC = 1 9 | 10 | PANEL_TYPES = { 11 | 'A': ADEMCO, 12 | 'D': DSC, 13 | } 14 | 15 | VISTA20 = 0 16 | 17 | TEMPLATES = { 18 | VISTA20: { 19 | 'name': 'Vista 20', 20 | # number of expanders, starting_address, number of channels 21 | 'expanders': (5, 7, 7) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /alarmdecoder/states.py: -------------------------------------------------------------------------------- 1 | class FireState: 2 | """ 3 | Fire alarm status 4 | """ 5 | NONE = 0 6 | ALARM = 1 7 | ACKNOWLEDGED = 2 8 | -------------------------------------------------------------------------------- /alarmdecoder/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides utility classes for the `AlarmDecoder`_ (AD2) devices. 3 | 4 | .. _AlarmDecoder: http://www.alarmdecoder.com 5 | 6 | .. moduleauthor:: Scott Petersen 7 | """ 8 | 9 | import time 10 | import threading 11 | import select 12 | import sys 13 | import alarmdecoder 14 | 15 | from io import open 16 | from collections import deque 17 | 18 | 19 | class NoDeviceError(Exception): 20 | """ 21 | No devices found. 22 | """ 23 | pass 24 | 25 | 26 | class CommError(Exception): 27 | """ 28 | There was an error communicating with the device. 29 | """ 30 | pass 31 | 32 | 33 | class TimeoutError(Exception): 34 | """ 35 | There was a timeout while trying to communicate with the device. 36 | """ 37 | pass 38 | 39 | 40 | class InvalidMessageError(Exception): 41 | """ 42 | The format of the panel message was invalid. 43 | """ 44 | pass 45 | 46 | 47 | class UploadError(Exception): 48 | """ 49 | Generic firmware upload error. 50 | """ 51 | pass 52 | 53 | 54 | class UploadChecksumError(UploadError): 55 | """ 56 | The firmware upload failed due to a checksum error. 57 | """ 58 | pass 59 | 60 | 61 | def bytes_available(device): 62 | """ 63 | Determines the number of bytes available for reading from an 64 | AlarmDecoder device 65 | 66 | :param device: the AlarmDecoder device 67 | :type device: :py:class:`~alarmdecoder.devices.Device` 68 | 69 | :returns: int 70 | """ 71 | bytes_avail = 0 72 | 73 | if isinstance(device, alarmdecoder.devices.SerialDevice): 74 | if hasattr(device._device, "in_waiting"): 75 | bytes_avail = device._device.in_waiting 76 | else: 77 | bytes_avail = device._device.inWaiting() 78 | elif isinstance(device, alarmdecoder.devices.SocketDevice): 79 | bytes_avail = 4096 80 | 81 | return bytes_avail 82 | 83 | def bytes_hack(buf): 84 | """ 85 | Hacky workaround for old installs of the library on systems without python-future that were 86 | keeping the 2to3 update from working after auto-update. 87 | """ 88 | ub = None 89 | if sys.version_info > (3,): 90 | ub = buf 91 | else: 92 | ub = bytes(buf) 93 | 94 | return ub 95 | 96 | def filter_ad2prot_byte(buf): 97 | """ 98 | Return the byte sent in back if valid visible terminal characters or line terminators. 99 | """ 100 | if sys.version_info > (3,): 101 | c = buf[0] 102 | else: 103 | c = ord(buf) 104 | 105 | if (c == 10 or c == 13): 106 | return buf 107 | if (c > 31 and c < 127): 108 | return buf 109 | else: 110 | return b'' 111 | 112 | def read_firmware_file(file_path): 113 | """ 114 | Reads a firmware file into a dequeue for processing. 115 | 116 | :param file_path: Path to the firmware file 117 | :type file_path: string 118 | 119 | :returns: deque 120 | """ 121 | data_queue = deque() 122 | 123 | with open(file_path) as firmware_handle: 124 | for line in firmware_handle: 125 | line = line.rstrip() 126 | if line != '' and line[0] == ':': 127 | data_queue.append(line + "\r") 128 | 129 | return data_queue 130 | 131 | class Firmware(object): 132 | """ 133 | Represents firmware for the `AlarmDecoder`_ devices. 134 | """ 135 | 136 | # Constants 137 | STAGE_START = 0 138 | STAGE_WAITING = 1 139 | STAGE_BOOT = 2 140 | STAGE_WAITING_ON_LOADER = 2.5 141 | STAGE_LOAD = 3 142 | STAGE_UPLOADING = 4 143 | STAGE_DONE = 5 144 | STAGE_ERROR = 98 145 | STAGE_DEBUG = 99 146 | 147 | @staticmethod 148 | def read(device): 149 | """ 150 | Reads data from the specified device. 151 | 152 | :param device: the AlarmDecoder device 153 | :type device: :py:class:`~alarmdecoder.devices.Device` 154 | 155 | :returns: string 156 | """ 157 | response = None 158 | bytes_avail = bytes_available(device) 159 | 160 | if isinstance(device, alarmdecoder.devices.SerialDevice): 161 | response = device._device.read(bytes_avail) 162 | elif isinstance(device, alarmdecoder.devices.SocketDevice): 163 | response = device._device.recv(bytes_avail) 164 | 165 | return response 166 | 167 | @staticmethod 168 | def upload(device, file_path, progress_callback=None, debug=False): 169 | """ 170 | Uploads firmware to an `AlarmDecoder`_ device. 171 | 172 | :param file_path: firmware file path 173 | :type file_path: string 174 | :param progress_callback: callback function used to report progress 175 | :type progress_callback: function 176 | 177 | :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError` 178 | """ 179 | 180 | def progress_stage(stage, **kwargs): 181 | """Callback to update progress for the specified stage.""" 182 | if progress_callback is not None: 183 | progress_callback(stage, **kwargs) 184 | 185 | return stage 186 | 187 | if device is None: 188 | raise NoDeviceError('No device specified for firmware upload.') 189 | 190 | fds = [device._device.fileno()] 191 | 192 | # Read firmware file into memory 193 | try: 194 | write_queue = read_firmware_file(file_path) 195 | except IOError as err: 196 | stage = progress_stage(Firmware.STAGE_ERROR, error=str(err)) 197 | return 198 | 199 | data_read = '' 200 | got_response = False 201 | running = True 202 | stage = progress_stage(Firmware.STAGE_START) 203 | 204 | if device.is_reader_alive(): 205 | # Close the reader thread and wait for it to die, otherwise 206 | # it interferes with our reading. 207 | device.stop_reader() 208 | while device._read_thread.is_alive(): 209 | stage = progress_stage(Firmware.STAGE_WAITING) 210 | time.sleep(0.5) 211 | 212 | time.sleep(3) 213 | 214 | try: 215 | while running: 216 | rr, wr, _ = select.select(fds, fds, [], 0.5) 217 | 218 | if len(rr) != 0: 219 | response = Firmware.read(device) 220 | 221 | for c in response: 222 | # HACK: Python 3 / PySerial hack. 223 | if isinstance(c, int): 224 | c = chr(c) 225 | 226 | if c == '\xff' or c == '\r': # HACK: odd case for our mystery \xff byte. 227 | # Boot started, start looking for the !boot message 228 | if data_read.startswith("!sn"): 229 | stage = progress_stage(Firmware.STAGE_BOOT) 230 | # Entered bootloader upload mode, start uploading 231 | elif data_read.startswith("!load"): 232 | got_response = True 233 | stage = progress_stage(Firmware.STAGE_UPLOADING) 234 | # Checksum error 235 | elif data_read == '!ce': 236 | running = False 237 | raise UploadChecksumError("Checksum error in {0}".format(file_path)) 238 | # Bad data 239 | elif data_read == '!no': 240 | running = False 241 | raise UploadError("Incorrect data sent to bootloader.") 242 | # Firmware upload complete 243 | elif data_read == '!ok': 244 | running = False 245 | stage = progress_stage(Firmware.STAGE_DONE) 246 | # All other responses are valid during upload. 247 | else: 248 | got_response = True 249 | if stage == Firmware.STAGE_UPLOADING: 250 | progress_stage(stage) 251 | 252 | data_read = '' 253 | elif c == '\n': 254 | pass 255 | else: 256 | data_read += c 257 | 258 | if len(wr) != 0: 259 | # Reboot device 260 | if stage in [Firmware.STAGE_START, Firmware.STAGE_WAITING]: 261 | device.write('=') 262 | stage = progress_stage(Firmware.STAGE_WAITING_ON_LOADER) 263 | 264 | # Enter bootloader 265 | elif stage == Firmware.STAGE_BOOT: 266 | device.write('=') 267 | stage = progress_stage(Firmware.STAGE_LOAD) 268 | 269 | # Upload firmware 270 | elif stage == Firmware.STAGE_UPLOADING: 271 | if len(write_queue) > 0 and got_response == True: 272 | got_response = False 273 | device.write(write_queue.popleft()) 274 | 275 | except UploadError as err: 276 | stage = progress_stage(Firmware.STAGE_ERROR, error=str(err)) 277 | else: 278 | stage = progress_stage(Firmware.STAGE_DONE) 279 | -------------------------------------------------------------------------------- /bin/ad2-firmwareupload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys, time 5 | import traceback 6 | import alarmdecoder 7 | 8 | def handle_firmware(stage, **kwargs): 9 | if stage == alarmdecoder.util.Firmware.STAGE_START: 10 | handle_firmware.wait_tick = 0 11 | handle_firmware.upload_tick = 0 12 | elif stage == alarmdecoder.util.Firmware.STAGE_WAITING: 13 | if handle_firmware.wait_tick == 0: 14 | sys.stdout.write('Waiting for device.') 15 | handle_firmware.wait_tick += 1 16 | 17 | sys.stdout.write('.') 18 | sys.stdout.flush() 19 | elif stage == alarmdecoder.util.Firmware.STAGE_BOOT: 20 | if handle_firmware.wait_tick > 0: print("") 21 | print("Rebooting device..") 22 | elif stage == alarmdecoder.util.Firmware.STAGE_LOAD: 23 | print('Waiting for boot loader..') 24 | elif stage == alarmdecoder.util.Firmware.STAGE_UPLOADING: 25 | if handle_firmware.upload_tick == 0: 26 | sys.stdout.write('Uploading firmware.') 27 | 28 | handle_firmware.upload_tick += 1 29 | 30 | if handle_firmware.upload_tick % 30 == 0: 31 | sys.stdout.write('.') 32 | sys.stdout.flush() 33 | elif stage == alarmdecoder.util.Firmware.STAGE_DONE: 34 | print("\r\nDone!") 35 | elif stage == alarmdecoder.util.Firmware.STAGE_ERROR: 36 | print("\r\nError: {0}".format(kwargs.get("error", ""))) 37 | elif stage == alarmdecoder.util.Firmware.STAGE_DEBUG: 38 | print("\r\nDBG: {0}".format(kwargs.get("data", ""))) 39 | 40 | def main(): 41 | device = '/dev/ttyUSB0' 42 | firmware = None 43 | baudrate = 115200 44 | 45 | if len(sys.argv) < 3: 46 | print("Syntax: {0} [device path or hostname:port] [baudrate=115200]".format(sys.argv[0])) 47 | sys.exit(1) 48 | 49 | firmware = sys.argv[1] 50 | device = sys.argv[2] 51 | 52 | if len(sys.argv) > 3: 53 | baudrate = sys.argv[3] 54 | 55 | debug = os.environ.get("ALARMDECODER_DEBUG") is not None 56 | print("Flashing device: {0} - {2} baud\r\nFirmware: {1}".format(device, firmware, baudrate)) 57 | 58 | dev = None 59 | try: 60 | if ':' in device: 61 | hostname, port = device.split(':') 62 | dev = alarmdecoder.devices.SocketDevice(interface=(hostname, int(port))) 63 | else: 64 | dev = alarmdecoder.devices.SerialDevice(interface=device) 65 | 66 | dev.open(baudrate=baudrate, no_reader_thread=True) 67 | 68 | time.sleep(3) 69 | alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware, debug=debug) 70 | 71 | except alarmdecoder.util.NoDeviceError as ex: 72 | print("Error: Could not find device: {0}".format(ex)) 73 | except alarmdecoder.util.UploadError as ex: 74 | print("Error: Error uploading firmware: {0}".format(ex)) 75 | except Exception as ex: 76 | print("Error: {0}: {1}".format(ex, traceback.format_exc())) 77 | finally: 78 | if dev is not None: 79 | dev.close() 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /bin/ad2-sslterm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import alarmdecoder 4 | import sys, select 5 | import termios, tty 6 | import time 7 | 8 | def main(): 9 | if len(sys.argv) != 5: 10 | print "Syntax: {0} [host:port] [ca cert] [client cert] [client key]\r".format(sys.argv[0]) 11 | return 1 12 | 13 | host, port = sys.argv[1].split(':') 14 | ca_cert = sys.argv[2] 15 | client_cert = sys.argv[3] 16 | client_key = sys.argv[4] 17 | 18 | running = True 19 | 20 | old_term_settings = termios.tcgetattr(sys.stdin.fileno()) 21 | tty.setraw(sys.stdin.fileno()) 22 | 23 | try: 24 | print "Opening connection to {0}:{1}\r".format(host, port) 25 | 26 | dev = alarmdecoder.devices.SocketDevice(interface=(host, int(port))) 27 | dev.ssl = True 28 | dev.ssl_certificate = client_cert 29 | dev.ssl_key = client_key 30 | dev.ssl_ca = ca_cert 31 | 32 | dev.open(no_reader_thread=True) 33 | dev.write("\r") # HACK: Prime the pump. This likely has to do with the SSL handshake 34 | # not being completed when we get down to the select. 35 | 36 | while running: 37 | ifh, ofh, efh = select.select([sys.stdin, dev._device], [], [], 0) 38 | 39 | for h in ifh: 40 | if h == sys.stdin: 41 | data = h.read(1) 42 | 43 | # Break out if we get a CTRL-C 44 | if data == "\x03": 45 | print "Exiting..\r" 46 | running = False 47 | break 48 | 49 | else: 50 | dev.write(data) 51 | 52 | else: 53 | data = h.read(100) 54 | 55 | sys.stdout.write(data) 56 | sys.stdout.flush() 57 | 58 | dev.close() 59 | print "Connection closed.\r" 60 | 61 | finally: 62 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_term_settings) 63 | 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/alarmdecoder.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/alarmdecoder.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/alarmdecoder" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/alarmdecoder" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/alarmdecoder.event.rst: -------------------------------------------------------------------------------- 1 | event Package 2 | ============= 3 | 4 | :mod:`event` Package 5 | -------------------- 6 | 7 | .. automodule:: alarmdecoder.event 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`event` Module 13 | ------------------- 14 | 15 | .. automodule:: alarmdecoder.event.event 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /docs/alarmdecoder.rst: -------------------------------------------------------------------------------- 1 | alarmdecoder Package 2 | ==================== 3 | 4 | :mod:`decoder` Module 5 | -------------------------- 6 | 7 | .. automodule:: alarmdecoder.decoder 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`devices` Module 13 | --------------------- 14 | 15 | .. automodule:: alarmdecoder.devices 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`messages` Module 21 | ---------------------- 22 | 23 | .. automodule:: alarmdecoder.messages 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`zonetracking` Module 29 | -------------------------- 30 | 31 | .. automodule:: alarmdecoder.zonetracking 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`util` Module 37 | ------------------ 38 | 39 | .. automodule:: alarmdecoder.util 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`panels` Module 45 | -------------------- 46 | 47 | .. automodule:: alarmdecoder.panels 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | .. 53 | Subpackages 54 | ----------- 55 | 56 | .. toctree:: 57 | 58 | alarmdecoder.event 59 | 60 | -------------------------------------------------------------------------------- /docs/build/doctrees/alarmdecoder.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/doctrees/alarmdecoder.doctree -------------------------------------------------------------------------------- /docs/build/doctrees/alarmdecoder.event.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/doctrees/alarmdecoder.event.doctree -------------------------------------------------------------------------------- /docs/build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/build/doctrees/modules.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/doctrees/modules.doctree -------------------------------------------------------------------------------- /docs/build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 8fd355ab18ea10d4d654aef01edd4d38 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/build/html/_modules/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Overview: module code — alarmdecoder documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 41 | 42 |
43 |
44 |
45 |
46 | 47 |

All modules for which code is available

48 | 55 | 56 |
57 |
58 |
59 |
60 |
61 | 73 | 74 |
75 |
76 |
77 |
78 | 90 | 94 | 95 | -------------------------------------------------------------------------------- /docs/build/html/_sources/alarmdecoder.event.txt: -------------------------------------------------------------------------------- 1 | event Package 2 | ============= 3 | 4 | :mod:`event` Package 5 | -------------------- 6 | 7 | .. automodule:: alarmdecoder.event 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`event` Module 13 | ------------------- 14 | 15 | .. automodule:: alarmdecoder.event.event 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /docs/build/html/_sources/alarmdecoder.txt: -------------------------------------------------------------------------------- 1 | alarmdecoder Package 2 | ==================== 3 | 4 | :mod:`decoder` Module 5 | -------------------------- 6 | 7 | .. automodule:: alarmdecoder.decoder 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`devices` Module 13 | --------------------- 14 | 15 | .. automodule:: alarmdecoder.devices 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`messages` Module 21 | ---------------------- 22 | 23 | .. automodule:: alarmdecoder.messages 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`zonetracking` Module 29 | -------------------------- 30 | 31 | .. automodule:: alarmdecoder.zonetracking 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`util` Module 37 | ------------------ 38 | 39 | .. automodule:: alarmdecoder.util 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`panels` Module 45 | -------------------- 46 | 47 | .. automodule:: alarmdecoder.panels 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | .. 53 | Subpackages 54 | ----------- 55 | 56 | .. toctree:: 57 | 58 | alarmdecoder.event 59 | 60 | -------------------------------------------------------------------------------- /docs/build/html/_sources/index.txt: -------------------------------------------------------------------------------- 1 | .. alarmdecoder documentation master file, created by 2 | sphinx-quickstart on Sat Jun 8 14:38:46 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Alarm Decoder's documentation! 7 | ========================================= 8 | 9 | .. _Alarm Decoder: http://www.alarmdecoder.com 10 | .. _examples: http://github.com/nutechsoftware/alarmdecoder/tree/master/examples 11 | 12 | This is the API documentation for the `Alarm Decoder`_ Python library. It provides support for interacting with the `Alarm Decoder`_ (AD2) family of security alarm devices, including the AD2USB, AD2SERIAL and AD2PI. 13 | 14 | The source code, requirements and examples for this project may be found `here `_. 15 | 16 | Please see the `examples`_ directory for more samples, but a basic one is included below:: 17 | 18 | import time 19 | from alarmdecoder import AlarmDecoder 20 | from alarmdecoder.devices import USBDevice 21 | 22 | def main(): 23 | """ 24 | Example application that prints messages from the panel to the terminal. 25 | """ 26 | try: 27 | # Retrieve the first USB device 28 | device = AlarmDecoder(USBDevice.find()) 29 | 30 | # Set up an event handler and open the device 31 | device.on_message += handle_message 32 | with device.open(): 33 | while True: 34 | time.sleep(1) 35 | 36 | except Exception, ex: 37 | print 'Exception:', ex 38 | 39 | def handle_message(sender, message): 40 | """ 41 | Handles message events from the AlarmDecoder. 42 | """ 43 | print sender, message.raw 44 | 45 | if __name__ == '__main__': 46 | main() 47 | 48 | Table of Contents: 49 | 50 | .. toctree:: 51 | :maxdepth: 4 52 | 53 | alarmdecoder 54 | 55 | 56 | Indices and tables 57 | ================== 58 | 59 | * :ref:`genindex` 60 | * :ref:`modindex` 61 | * :ref:`search` 62 | 63 | -------------------------------------------------------------------------------- /docs/build/html/_sources/modules.txt: -------------------------------------------------------------------------------- 1 | alarmdecoder 2 | ============ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | alarmdecoder 8 | -------------------------------------------------------------------------------- /docs/build/html/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docs/build/html/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * basic.css 3 | * ~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- basic theme. 6 | * 7 | * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /* -- main layout ----------------------------------------------------------- */ 13 | 14 | div.clearer { 15 | clear: both; 16 | } 17 | 18 | /* -- relbar ---------------------------------------------------------------- */ 19 | 20 | div.related { 21 | width: 100%; 22 | font-size: 90%; 23 | } 24 | 25 | div.related h3 { 26 | display: none; 27 | } 28 | 29 | div.related ul { 30 | margin: 0; 31 | padding: 0 0 0 10px; 32 | list-style: none; 33 | } 34 | 35 | div.related li { 36 | display: inline; 37 | } 38 | 39 | div.related li.right { 40 | float: right; 41 | margin-right: 5px; 42 | } 43 | 44 | /* -- sidebar --------------------------------------------------------------- */ 45 | 46 | div.sphinxsidebarwrapper { 47 | padding: 10px 5px 0 10px; 48 | } 49 | 50 | div.sphinxsidebar { 51 | float: left; 52 | width: 230px; 53 | margin-left: -100%; 54 | font-size: 90%; 55 | } 56 | 57 | div.sphinxsidebar ul { 58 | list-style: none; 59 | } 60 | 61 | div.sphinxsidebar ul ul, 62 | div.sphinxsidebar ul.want-points { 63 | margin-left: 20px; 64 | list-style: square; 65 | } 66 | 67 | div.sphinxsidebar ul ul { 68 | margin-top: 0; 69 | margin-bottom: 0; 70 | } 71 | 72 | div.sphinxsidebar form { 73 | margin-top: 10px; 74 | } 75 | 76 | div.sphinxsidebar input { 77 | border: 1px solid #98dbcc; 78 | font-family: sans-serif; 79 | font-size: 1em; 80 | } 81 | 82 | div.sphinxsidebar #searchbox input[type="text"] { 83 | width: 170px; 84 | } 85 | 86 | div.sphinxsidebar #searchbox input[type="submit"] { 87 | width: 30px; 88 | } 89 | 90 | img { 91 | border: 0; 92 | max-width: 100%; 93 | } 94 | 95 | /* -- search page ----------------------------------------------------------- */ 96 | 97 | ul.search { 98 | margin: 10px 0 0 20px; 99 | padding: 0; 100 | } 101 | 102 | ul.search li { 103 | padding: 5px 0 5px 20px; 104 | background-image: url(file.png); 105 | background-repeat: no-repeat; 106 | background-position: 0 7px; 107 | } 108 | 109 | ul.search li a { 110 | font-weight: bold; 111 | } 112 | 113 | ul.search li div.context { 114 | color: #888; 115 | margin: 2px 0 0 30px; 116 | text-align: left; 117 | } 118 | 119 | ul.keywordmatches li.goodmatch a { 120 | font-weight: bold; 121 | } 122 | 123 | /* -- index page ------------------------------------------------------------ */ 124 | 125 | table.contentstable { 126 | width: 90%; 127 | } 128 | 129 | table.contentstable p.biglink { 130 | line-height: 150%; 131 | } 132 | 133 | a.biglink { 134 | font-size: 1.3em; 135 | } 136 | 137 | span.linkdescr { 138 | font-style: italic; 139 | padding-top: 5px; 140 | font-size: 90%; 141 | } 142 | 143 | /* -- general index --------------------------------------------------------- */ 144 | 145 | table.indextable { 146 | width: 100%; 147 | } 148 | 149 | table.indextable td { 150 | text-align: left; 151 | vertical-align: top; 152 | } 153 | 154 | table.indextable dl, table.indextable dd { 155 | margin-top: 0; 156 | margin-bottom: 0; 157 | } 158 | 159 | table.indextable tr.pcap { 160 | height: 10px; 161 | } 162 | 163 | table.indextable tr.cap { 164 | margin-top: 10px; 165 | background-color: #f2f2f2; 166 | } 167 | 168 | img.toggler { 169 | margin-right: 3px; 170 | margin-top: 3px; 171 | cursor: pointer; 172 | } 173 | 174 | div.modindex-jumpbox { 175 | border-top: 1px solid #ddd; 176 | border-bottom: 1px solid #ddd; 177 | margin: 1em 0 1em 0; 178 | padding: 0.4em; 179 | } 180 | 181 | div.genindex-jumpbox { 182 | border-top: 1px solid #ddd; 183 | border-bottom: 1px solid #ddd; 184 | margin: 1em 0 1em 0; 185 | padding: 0.4em; 186 | } 187 | 188 | /* -- general body styles --------------------------------------------------- */ 189 | 190 | a.headerlink { 191 | visibility: hidden; 192 | } 193 | 194 | h1:hover > a.headerlink, 195 | h2:hover > a.headerlink, 196 | h3:hover > a.headerlink, 197 | h4:hover > a.headerlink, 198 | h5:hover > a.headerlink, 199 | h6:hover > a.headerlink, 200 | dt:hover > a.headerlink { 201 | visibility: visible; 202 | } 203 | 204 | div.body p.caption { 205 | text-align: inherit; 206 | } 207 | 208 | div.body td { 209 | text-align: left; 210 | } 211 | 212 | .field-list ul { 213 | padding-left: 1em; 214 | } 215 | 216 | .first { 217 | margin-top: 0 !important; 218 | } 219 | 220 | p.rubric { 221 | margin-top: 30px; 222 | font-weight: bold; 223 | } 224 | 225 | img.align-left, .figure.align-left, object.align-left { 226 | clear: left; 227 | float: left; 228 | margin-right: 1em; 229 | } 230 | 231 | img.align-right, .figure.align-right, object.align-right { 232 | clear: right; 233 | float: right; 234 | margin-left: 1em; 235 | } 236 | 237 | img.align-center, .figure.align-center, object.align-center { 238 | display: block; 239 | margin-left: auto; 240 | margin-right: auto; 241 | } 242 | 243 | .align-left { 244 | text-align: left; 245 | } 246 | 247 | .align-center { 248 | text-align: center; 249 | } 250 | 251 | .align-right { 252 | text-align: right; 253 | } 254 | 255 | /* -- sidebars -------------------------------------------------------------- */ 256 | 257 | div.sidebar { 258 | margin: 0 0 0.5em 1em; 259 | border: 1px solid #ddb; 260 | padding: 7px 7px 0 7px; 261 | background-color: #ffe; 262 | width: 40%; 263 | float: right; 264 | } 265 | 266 | p.sidebar-title { 267 | font-weight: bold; 268 | } 269 | 270 | /* -- topics ---------------------------------------------------------------- */ 271 | 272 | div.topic { 273 | border: 1px solid #ccc; 274 | padding: 7px 7px 0 7px; 275 | margin: 10px 0 10px 0; 276 | } 277 | 278 | p.topic-title { 279 | font-size: 1.1em; 280 | font-weight: bold; 281 | margin-top: 10px; 282 | } 283 | 284 | /* -- admonitions ----------------------------------------------------------- */ 285 | 286 | div.admonition { 287 | margin-top: 10px; 288 | margin-bottom: 10px; 289 | padding: 7px; 290 | } 291 | 292 | div.admonition dt { 293 | font-weight: bold; 294 | } 295 | 296 | div.admonition dl { 297 | margin-bottom: 0; 298 | } 299 | 300 | p.admonition-title { 301 | margin: 0px 10px 5px 0px; 302 | font-weight: bold; 303 | } 304 | 305 | div.body p.centered { 306 | text-align: center; 307 | margin-top: 25px; 308 | } 309 | 310 | /* -- tables ---------------------------------------------------------------- */ 311 | 312 | table.docutils { 313 | border: 0; 314 | border-collapse: collapse; 315 | } 316 | 317 | table.docutils td, table.docutils th { 318 | padding: 1px 8px 1px 5px; 319 | border-top: 0; 320 | border-left: 0; 321 | border-right: 0; 322 | border-bottom: 1px solid #aaa; 323 | } 324 | 325 | table.field-list td, table.field-list th { 326 | border: 0 !important; 327 | } 328 | 329 | table.footnote td, table.footnote th { 330 | border: 0 !important; 331 | } 332 | 333 | th { 334 | text-align: left; 335 | padding-right: 5px; 336 | } 337 | 338 | table.citation { 339 | border-left: solid 1px gray; 340 | margin-left: 1px; 341 | } 342 | 343 | table.citation td { 344 | border-bottom: none; 345 | } 346 | 347 | /* -- other body styles ----------------------------------------------------- */ 348 | 349 | ol.arabic { 350 | list-style: decimal; 351 | } 352 | 353 | ol.loweralpha { 354 | list-style: lower-alpha; 355 | } 356 | 357 | ol.upperalpha { 358 | list-style: upper-alpha; 359 | } 360 | 361 | ol.lowerroman { 362 | list-style: lower-roman; 363 | } 364 | 365 | ol.upperroman { 366 | list-style: upper-roman; 367 | } 368 | 369 | dl { 370 | margin-bottom: 15px; 371 | } 372 | 373 | dd p { 374 | margin-top: 0px; 375 | } 376 | 377 | dd ul, dd table { 378 | margin-bottom: 10px; 379 | } 380 | 381 | dd { 382 | margin-top: 3px; 383 | margin-bottom: 10px; 384 | margin-left: 30px; 385 | } 386 | 387 | dt:target, .highlighted { 388 | background-color: #fbe54e; 389 | } 390 | 391 | dl.glossary dt { 392 | font-weight: bold; 393 | font-size: 1.1em; 394 | } 395 | 396 | .field-list ul { 397 | margin: 0; 398 | padding-left: 1em; 399 | } 400 | 401 | .field-list p { 402 | margin: 0; 403 | } 404 | 405 | .optional { 406 | font-size: 1.3em; 407 | } 408 | 409 | .versionmodified { 410 | font-style: italic; 411 | } 412 | 413 | .system-message { 414 | background-color: #fda; 415 | padding: 5px; 416 | border: 3px solid red; 417 | } 418 | 419 | .footnote:target { 420 | background-color: #ffa; 421 | } 422 | 423 | .line-block { 424 | display: block; 425 | margin-top: 1em; 426 | margin-bottom: 1em; 427 | } 428 | 429 | .line-block .line-block { 430 | margin-top: 0; 431 | margin-bottom: 0; 432 | margin-left: 1.5em; 433 | } 434 | 435 | .guilabel, .menuselection { 436 | font-family: sans-serif; 437 | } 438 | 439 | .accelerator { 440 | text-decoration: underline; 441 | } 442 | 443 | .classifier { 444 | font-style: oblique; 445 | } 446 | 447 | abbr, acronym { 448 | border-bottom: dotted 1px; 449 | cursor: help; 450 | } 451 | 452 | /* -- code displays --------------------------------------------------------- */ 453 | 454 | pre { 455 | overflow: auto; 456 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 457 | } 458 | 459 | td.linenos pre { 460 | padding: 5px 0px; 461 | border: 0; 462 | background-color: transparent; 463 | color: #aaa; 464 | } 465 | 466 | table.highlighttable { 467 | margin-left: 0.5em; 468 | } 469 | 470 | table.highlighttable td { 471 | padding: 0 0.5em 0 0.5em; 472 | } 473 | 474 | tt.descname { 475 | background-color: transparent; 476 | font-weight: bold; 477 | font-size: 1.2em; 478 | } 479 | 480 | tt.descclassname { 481 | background-color: transparent; 482 | } 483 | 484 | tt.xref, a tt { 485 | background-color: transparent; 486 | font-weight: bold; 487 | } 488 | 489 | h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { 490 | background-color: transparent; 491 | } 492 | 493 | .viewcode-link { 494 | float: right; 495 | } 496 | 497 | .viewcode-back { 498 | float: right; 499 | font-family: sans-serif; 500 | } 501 | 502 | div.viewcode-block:target { 503 | margin: -1px -10px; 504 | padding: 0 10px; 505 | } 506 | 507 | /* -- math display ---------------------------------------------------------- */ 508 | 509 | img.math { 510 | vertical-align: middle; 511 | } 512 | 513 | div.body div.math p { 514 | text-align: center; 515 | } 516 | 517 | span.eqno { 518 | float: right; 519 | } 520 | 521 | /* -- printout stylesheet --------------------------------------------------- */ 522 | 523 | @media print { 524 | div.document, 525 | div.documentwrapper, 526 | div.bodywrapper { 527 | margin: 0 !important; 528 | width: 100%; 529 | } 530 | 531 | div.sphinxsidebar, 532 | div.related, 533 | div.footer, 534 | #top-link { 535 | display: none; 536 | } 537 | } -------------------------------------------------------------------------------- /docs/build/html/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/comment-bright.png -------------------------------------------------------------------------------- /docs/build/html/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/comment-close.png -------------------------------------------------------------------------------- /docs/build/html/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/comment.png -------------------------------------------------------------------------------- /docs/build/html/_static/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * default.css_t 3 | * ~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- default theme. 6 | * 7 | * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: sans-serif; 18 | font-size: 100%; 19 | background-color: #11303d; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background-color: #1c4e63; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | div.body { 39 | background-color: #ffffff; 40 | color: #000000; 41 | padding: 0 20px 30px 20px; 42 | } 43 | 44 | div.footer { 45 | color: #ffffff; 46 | width: 100%; 47 | padding: 9px 0 9px 0; 48 | text-align: center; 49 | font-size: 75%; 50 | } 51 | 52 | div.footer a { 53 | color: #ffffff; 54 | text-decoration: underline; 55 | } 56 | 57 | div.related { 58 | background-color: #133f52; 59 | line-height: 30px; 60 | color: #ffffff; 61 | } 62 | 63 | div.related a { 64 | color: #ffffff; 65 | } 66 | 67 | div.sphinxsidebar { 68 | } 69 | 70 | div.sphinxsidebar h3 { 71 | font-family: 'Trebuchet MS', sans-serif; 72 | color: #ffffff; 73 | font-size: 1.4em; 74 | font-weight: normal; 75 | margin: 0; 76 | padding: 0; 77 | } 78 | 79 | div.sphinxsidebar h3 a { 80 | color: #ffffff; 81 | } 82 | 83 | div.sphinxsidebar h4 { 84 | font-family: 'Trebuchet MS', sans-serif; 85 | color: #ffffff; 86 | font-size: 1.3em; 87 | font-weight: normal; 88 | margin: 5px 0 0 0; 89 | padding: 0; 90 | } 91 | 92 | div.sphinxsidebar p { 93 | color: #ffffff; 94 | } 95 | 96 | div.sphinxsidebar p.topless { 97 | margin: 5px 10px 10px 10px; 98 | } 99 | 100 | div.sphinxsidebar ul { 101 | margin: 10px; 102 | padding: 0; 103 | color: #ffffff; 104 | } 105 | 106 | div.sphinxsidebar a { 107 | color: #98dbcc; 108 | } 109 | 110 | div.sphinxsidebar input { 111 | border: 1px solid #98dbcc; 112 | font-family: sans-serif; 113 | font-size: 1em; 114 | } 115 | 116 | 117 | 118 | /* -- hyperlink styles ------------------------------------------------------ */ 119 | 120 | a { 121 | color: #355f7c; 122 | text-decoration: none; 123 | } 124 | 125 | a:visited { 126 | color: #355f7c; 127 | text-decoration: none; 128 | } 129 | 130 | a:hover { 131 | text-decoration: underline; 132 | } 133 | 134 | 135 | 136 | /* -- body styles ----------------------------------------------------------- */ 137 | 138 | div.body h1, 139 | div.body h2, 140 | div.body h3, 141 | div.body h4, 142 | div.body h5, 143 | div.body h6 { 144 | font-family: 'Trebuchet MS', sans-serif; 145 | background-color: #f2f2f2; 146 | font-weight: normal; 147 | color: #20435c; 148 | border-bottom: 1px solid #ccc; 149 | margin: 20px -20px 10px -20px; 150 | padding: 3px 0 3px 10px; 151 | } 152 | 153 | div.body h1 { margin-top: 0; font-size: 200%; } 154 | div.body h2 { font-size: 160%; } 155 | div.body h3 { font-size: 140%; } 156 | div.body h4 { font-size: 120%; } 157 | div.body h5 { font-size: 110%; } 158 | div.body h6 { font-size: 100%; } 159 | 160 | a.headerlink { 161 | color: #c60f0f; 162 | font-size: 0.8em; 163 | padding: 0 4px 0 4px; 164 | text-decoration: none; 165 | } 166 | 167 | a.headerlink:hover { 168 | background-color: #c60f0f; 169 | color: white; 170 | } 171 | 172 | div.body p, div.body dd, div.body li { 173 | text-align: justify; 174 | line-height: 130%; 175 | } 176 | 177 | div.admonition p.admonition-title + p { 178 | display: inline; 179 | } 180 | 181 | div.admonition p { 182 | margin-bottom: 5px; 183 | } 184 | 185 | div.admonition pre { 186 | margin-bottom: 5px; 187 | } 188 | 189 | div.admonition ul, div.admonition ol { 190 | margin-bottom: 5px; 191 | } 192 | 193 | div.note { 194 | background-color: #eee; 195 | border: 1px solid #ccc; 196 | } 197 | 198 | div.seealso { 199 | background-color: #ffc; 200 | border: 1px solid #ff6; 201 | } 202 | 203 | div.topic { 204 | background-color: #eee; 205 | } 206 | 207 | div.warning { 208 | background-color: #ffe4e4; 209 | border: 1px solid #f66; 210 | } 211 | 212 | p.admonition-title { 213 | display: inline; 214 | } 215 | 216 | p.admonition-title:after { 217 | content: ":"; 218 | } 219 | 220 | pre { 221 | padding: 5px; 222 | background-color: #eeffcc; 223 | color: #333333; 224 | line-height: 120%; 225 | border: 1px solid #ac9; 226 | border-left: none; 227 | border-right: none; 228 | } 229 | 230 | tt { 231 | background-color: #ecf0f3; 232 | padding: 0 1px 0 1px; 233 | font-size: 0.95em; 234 | } 235 | 236 | th { 237 | background-color: #ede; 238 | } 239 | 240 | .warning tt { 241 | background: #efc2c2; 242 | } 243 | 244 | .note tt { 245 | background: #d6d6d6; 246 | } 247 | 248 | .viewcode-back { 249 | font-family: sans-serif; 250 | } 251 | 252 | div.viewcode-block:target { 253 | background-color: #f4debf; 254 | border-top: 1px solid #ac9; 255 | border-bottom: 1px solid #ac9; 256 | } -------------------------------------------------------------------------------- /docs/build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s == 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node) { 70 | if (node.nodeType == 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { 74 | var span = document.createElement("span"); 75 | span.className = className; 76 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 77 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 78 | document.createTextNode(val.substr(pos + text.length)), 79 | node.nextSibling)); 80 | node.nodeValue = val.substr(0, pos); 81 | } 82 | } 83 | else if (!jQuery(node).is("button, select, textarea")) { 84 | jQuery.each(node.childNodes, function() { 85 | highlight(this); 86 | }); 87 | } 88 | } 89 | return this.each(function() { 90 | highlight(this); 91 | }); 92 | }; 93 | 94 | /** 95 | * Small JavaScript module for the documentation. 96 | */ 97 | var Documentation = { 98 | 99 | init : function() { 100 | this.fixFirefoxAnchorBug(); 101 | this.highlightSearchWords(); 102 | this.initIndexTable(); 103 | }, 104 | 105 | /** 106 | * i18n support 107 | */ 108 | TRANSLATIONS : {}, 109 | PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, 110 | LOCALE : 'unknown', 111 | 112 | // gettext and ngettext don't access this so that the functions 113 | // can safely bound to a different name (_ = Documentation.gettext) 114 | gettext : function(string) { 115 | var translated = Documentation.TRANSLATIONS[string]; 116 | if (typeof translated == 'undefined') 117 | return string; 118 | return (typeof translated == 'string') ? translated : translated[0]; 119 | }, 120 | 121 | ngettext : function(singular, plural, n) { 122 | var translated = Documentation.TRANSLATIONS[singular]; 123 | if (typeof translated == 'undefined') 124 | return (n == 1) ? singular : plural; 125 | return translated[Documentation.PLURALEXPR(n)]; 126 | }, 127 | 128 | addTranslations : function(catalog) { 129 | for (var key in catalog.messages) 130 | this.TRANSLATIONS[key] = catalog.messages[key]; 131 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 132 | this.LOCALE = catalog.locale; 133 | }, 134 | 135 | /** 136 | * add context elements like header anchor links 137 | */ 138 | addContextElements : function() { 139 | $('div[id] > :header:first').each(function() { 140 | $('\u00B6'). 141 | attr('href', '#' + this.id). 142 | attr('title', _('Permalink to this headline')). 143 | appendTo(this); 144 | }); 145 | $('dt[id]').each(function() { 146 | $('\u00B6'). 147 | attr('href', '#' + this.id). 148 | attr('title', _('Permalink to this definition')). 149 | appendTo(this); 150 | }); 151 | }, 152 | 153 | /** 154 | * workaround a firefox stupidity 155 | */ 156 | fixFirefoxAnchorBug : function() { 157 | if (document.location.hash && $.browser.mozilla) 158 | window.setTimeout(function() { 159 | document.location.href += ''; 160 | }, 10); 161 | }, 162 | 163 | /** 164 | * highlight the search words provided in the url in the text 165 | */ 166 | highlightSearchWords : function() { 167 | var params = $.getQueryParameters(); 168 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 169 | if (terms.length) { 170 | var body = $('div.body'); 171 | if (!body.length) { 172 | body = $('body'); 173 | } 174 | window.setTimeout(function() { 175 | $.each(terms, function() { 176 | body.highlightText(this.toLowerCase(), 'highlighted'); 177 | }); 178 | }, 10); 179 | $('') 181 | .appendTo($('#searchbox')); 182 | } 183 | }, 184 | 185 | /** 186 | * init the domain index toggle buttons 187 | */ 188 | initIndexTable : function() { 189 | var togglers = $('img.toggler').click(function() { 190 | var src = $(this).attr('src'); 191 | var idnum = $(this).attr('id').substr(7); 192 | $('tr.cg-' + idnum).toggle(); 193 | if (src.substr(-9) == 'minus.png') 194 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 195 | else 196 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 197 | }).css('display', ''); 198 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 199 | togglers.click(); 200 | } 201 | }, 202 | 203 | /** 204 | * helper function to hide the search marks again 205 | */ 206 | hideSearchWords : function() { 207 | $('#searchbox .highlight-link').fadeOut(300); 208 | $('span.highlighted').removeClass('highlighted'); 209 | }, 210 | 211 | /** 212 | * make the url absolute 213 | */ 214 | makeURL : function(relativeURL) { 215 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 216 | }, 217 | 218 | /** 219 | * get the current relative url 220 | */ 221 | getCurrentURL : function() { 222 | var path = document.location.pathname; 223 | var parts = path.split(/\//); 224 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 225 | if (this == '..') 226 | parts.pop(); 227 | }); 228 | var url = parts.join('/'); 229 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 230 | } 231 | }; 232 | 233 | // quick alias for translations 234 | _ = Documentation.gettext; 235 | 236 | $(document).ready(function() { 237 | Documentation.init(); 238 | }); 239 | -------------------------------------------------------------------------------- /docs/build/html/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/down-pressed.png -------------------------------------------------------------------------------- /docs/build/html/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/down.png -------------------------------------------------------------------------------- /docs/build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/file.png -------------------------------------------------------------------------------- /docs/build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 8 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 9 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 10 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 11 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 14 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 15 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 16 | .highlight .go { color: #333333 } /* Generic.Output */ 17 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 18 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 19 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 20 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 21 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 22 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 23 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 24 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 25 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #902000 } /* Keyword.Type */ 27 | .highlight .m { color: #208050 } /* Literal.Number */ 28 | .highlight .s { color: #4070a0 } /* Literal.String */ 29 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 30 | .highlight .nb { color: #007020 } /* Name.Builtin */ 31 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #60add5 } /* Name.Constant */ 33 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 34 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 35 | .highlight .ne { color: #007020 } /* Name.Exception */ 36 | .highlight .nf { color: #06287e } /* Name.Function */ 37 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 38 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 39 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 40 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 41 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 46 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 47 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 48 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 49 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 50 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 51 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 52 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 53 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 54 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 55 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 56 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 57 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 58 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 59 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 60 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 61 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 62 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/build/html/_static/sidebar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * sidebar.js 3 | * ~~~~~~~~~~ 4 | * 5 | * This script makes the Sphinx sidebar collapsible. 6 | * 7 | * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds 8 | * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton 9 | * used to collapse and expand the sidebar. 10 | * 11 | * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden 12 | * and the width of the sidebar and the margin-left of the document 13 | * are decreased. When the sidebar is expanded the opposite happens. 14 | * This script saves a per-browser/per-session cookie used to 15 | * remember the position of the sidebar among the pages. 16 | * Once the browser is closed the cookie is deleted and the position 17 | * reset to the default (expanded). 18 | * 19 | * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. 20 | * :license: BSD, see LICENSE for details. 21 | * 22 | */ 23 | 24 | $(function() { 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | // global elements used by the functions. 34 | // the 'sidebarbutton' element is defined as global after its 35 | // creation, in the add_sidebar_button function 36 | var bodywrapper = $('.bodywrapper'); 37 | var sidebar = $('.sphinxsidebar'); 38 | var sidebarwrapper = $('.sphinxsidebarwrapper'); 39 | 40 | // for some reason, the document has no sidebar; do not run into errors 41 | if (!sidebar.length) return; 42 | 43 | // original margin-left of the bodywrapper and width of the sidebar 44 | // with the sidebar expanded 45 | var bw_margin_expanded = bodywrapper.css('margin-left'); 46 | var ssb_width_expanded = sidebar.width(); 47 | 48 | // margin-left of the bodywrapper and width of the sidebar 49 | // with the sidebar collapsed 50 | var bw_margin_collapsed = '.8em'; 51 | var ssb_width_collapsed = '.8em'; 52 | 53 | // colors used by the current theme 54 | var dark_color = $('.related').css('background-color'); 55 | var light_color = $('.document').css('background-color'); 56 | 57 | function sidebar_is_collapsed() { 58 | return sidebarwrapper.is(':not(:visible)'); 59 | } 60 | 61 | function toggle_sidebar() { 62 | if (sidebar_is_collapsed()) 63 | expand_sidebar(); 64 | else 65 | collapse_sidebar(); 66 | } 67 | 68 | function collapse_sidebar() { 69 | sidebarwrapper.hide(); 70 | sidebar.css('width', ssb_width_collapsed); 71 | bodywrapper.css('margin-left', bw_margin_collapsed); 72 | sidebarbutton.css({ 73 | 'margin-left': '0', 74 | 'height': bodywrapper.height() 75 | }); 76 | sidebarbutton.find('span').text('»'); 77 | sidebarbutton.attr('title', _('Expand sidebar')); 78 | document.cookie = 'sidebar=collapsed'; 79 | } 80 | 81 | function expand_sidebar() { 82 | bodywrapper.css('margin-left', bw_margin_expanded); 83 | sidebar.css('width', ssb_width_expanded); 84 | sidebarwrapper.show(); 85 | sidebarbutton.css({ 86 | 'margin-left': ssb_width_expanded-12, 87 | 'height': bodywrapper.height() 88 | }); 89 | sidebarbutton.find('span').text('«'); 90 | sidebarbutton.attr('title', _('Collapse sidebar')); 91 | document.cookie = 'sidebar=expanded'; 92 | } 93 | 94 | function add_sidebar_button() { 95 | sidebarwrapper.css({ 96 | 'float': 'left', 97 | 'margin-right': '0', 98 | 'width': ssb_width_expanded - 28 99 | }); 100 | // create the button 101 | sidebar.append( 102 | '
«
' 103 | ); 104 | var sidebarbutton = $('#sidebarbutton'); 105 | light_color = sidebarbutton.css('background-color'); 106 | // find the height of the viewport to center the '<<' in the page 107 | var viewport_height; 108 | if (window.innerHeight) 109 | viewport_height = window.innerHeight; 110 | else 111 | viewport_height = $(window).height(); 112 | sidebarbutton.find('span').css({ 113 | 'display': 'block', 114 | 'margin-top': (viewport_height - sidebar.position().top - 20) / 2 115 | }); 116 | 117 | sidebarbutton.click(toggle_sidebar); 118 | sidebarbutton.attr('title', _('Collapse sidebar')); 119 | sidebarbutton.css({ 120 | 'color': '#FFFFFF', 121 | 'border-left': '1px solid ' + dark_color, 122 | 'font-size': '1.2em', 123 | 'cursor': 'pointer', 124 | 'height': bodywrapper.height(), 125 | 'padding-top': '1px', 126 | 'margin-left': ssb_width_expanded - 12 127 | }); 128 | 129 | sidebarbutton.hover( 130 | function () { 131 | $(this).css('background-color', dark_color); 132 | }, 133 | function () { 134 | $(this).css('background-color', light_color); 135 | } 136 | ); 137 | } 138 | 139 | function set_position_from_cookie() { 140 | if (!document.cookie) 141 | return; 142 | var items = document.cookie.split(';'); 143 | for(var k=0; k 3 | 4 | 5 | 6 | 7 | 8 | 9 | event Package — alarmdecoder documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 |

event Package

49 |
50 |

event Package

51 |
52 |
53 |

event Module

54 |
55 |
56 | class alarmdecoder.event.event.Event(doc=None)[source]
57 |

Bases: object

58 |
59 | 60 |
61 |
62 | class alarmdecoder.event.event.EventHandler(event, obj)[source]
63 |

Bases: object

64 |
65 |
66 | add(func)[source]
67 |

Add new event handler function.

68 |

Event handler function must be defined like func(sender, earg). 69 | You can add handler also by using ‘+=’ operator.

70 |
71 | 72 |
73 |
74 | remove(func)[source]
75 |

Remove existing event handler function.

76 |

You can remove handler also by using ‘-=’ operator.

77 |
78 | 79 |
80 |
81 | fire(*args, **kwargs)[source]
82 |

Fire event and call all handler functions

83 |

You can call EventHandler object itself like e(*args, **kwargs) instead of 84 | e.fire(*args, **kwargs).

85 |
86 | 87 |
88 | 89 |
90 |
91 | 92 | 93 |
94 |
95 |
96 |
97 |
98 |

Table Of Contents

99 | 106 | 107 |

This Page

108 | 112 | 124 | 125 |
126 |
127 |
128 |
129 | 141 | 145 | 146 | -------------------------------------------------------------------------------- /docs/build/html/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Welcome to Alarm Decoder’s documentation! — alarmdecoder documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 |

Welcome to Alarm Decoder’s documentation!

53 |

This is the API documentation for the Alarm Decoder Python library. It provides support for interacting with the Alarm Decoder (AD2) family of security alarm devices, including the AD2USB, AD2SERIAL and AD2PI.

54 |

The source code, requirements and examples for this project may be found here.

55 |

Please see the examples directory for more samples, but a basic one is included below:

56 |
import time
 57 | from alarmdecoder import AlarmDecoder
 58 | from alarmdecoder.devices import USBDevice
 59 | 
 60 | def main():
 61 |     """
 62 |     Example application that prints messages from the panel to the terminal.
 63 |     """
 64 |     try:
 65 |         # Retrieve the first USB device
 66 |         device = AlarmDecoder(USBDevice.find())
 67 | 
 68 |         # Set up an event handler and open the device
 69 |         device.on_message += handle_message
 70 |         with device.open():
 71 |             while True:
 72 |                 time.sleep(1)
 73 | 
 74 |     except Exception, ex:
 75 |         print 'Exception:', ex
 76 | 
 77 | def handle_message(sender, message):
 78 |     """
 79 |     Handles message events from the AlarmDecoder.
 80 |     """
 81 |     print sender, message.raw
 82 | 
 83 | if __name__ == '__main__':
 84 |     main()
 85 | 
86 |
87 |

Table of Contents:

88 | 101 |
102 |
103 |

Indices and tables

104 | 109 |
110 | 111 | 112 |
113 |
114 |
115 |
116 |
117 |

Table Of Contents

118 | 122 | 123 |

Next topic

124 |

alarmdecoder Package

126 |

This Page

127 | 131 | 143 | 144 |
145 |
146 |
147 |
148 | 163 | 167 | 168 | -------------------------------------------------------------------------------- /docs/build/html/modules.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | alarmdecoder — alarmdecoder documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 |

alarmdecoder

49 | 62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |

This Page

71 | 75 | 87 | 88 |
89 |
90 |
91 |
92 | 104 | 108 | 109 | -------------------------------------------------------------------------------- /docs/build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/docs/build/html/objects.inv -------------------------------------------------------------------------------- /docs/build/html/py-modindex.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Python Module Index — alarmdecoder documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 44 | 45 |
46 |
47 |
48 |
49 | 50 | 51 |

Python Module Index

52 | 53 |
54 | a 55 |
56 | 57 | 58 | 59 | 61 | 62 | 64 | 67 | 68 | 69 | 72 | 73 | 74 | 77 | 78 | 79 | 82 | 83 | 84 | 87 | 88 | 89 | 92 | 93 | 94 | 97 | 98 | 99 | 102 | 103 | 104 | 107 |
 
60 | a
65 | alarmdecoder 66 |
    70 | alarmdecoder.decoder 71 |
    75 | alarmdecoder.devices 76 |
    80 | alarmdecoder.event 81 |
    85 | alarmdecoder.event.event 86 |
    90 | alarmdecoder.messages 91 |
    95 | alarmdecoder.panels 96 |
    100 | alarmdecoder.util 101 |
    105 | alarmdecoder.zonetracking 106 |
108 | 109 | 110 |
111 |
112 |
113 |
114 |
115 | 127 | 128 |
129 |
130 |
131 |
132 | 144 | 148 | 149 | -------------------------------------------------------------------------------- /docs/build/html/search.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Search — alarmdecoder documentation 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 49 | 50 |
51 |
52 |
53 |
54 | 55 |

Search

56 |
57 | 58 |

59 | Please activate JavaScript to enable the search 60 | functionality. 61 |

62 |
63 |

64 | From here you can search these documents. Enter your search 65 | words into the box below and click "search". Note that the search 66 | function will automatically search for all of the words. Pages 67 | containing fewer words won't appear in the result list. 68 |

69 |
70 | 71 | 72 | 73 |
74 | 75 |
76 | 77 |
78 | 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 100 | 104 | 105 | -------------------------------------------------------------------------------- /docs/build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({envversion:42,terms:{represent:2,all:[0,2],code:[3,2],sleep:3,on_boot:2,stage_don:2,backlight:2,zone:2,readabl:2,send:2,program:2,x03:2,x02:2,x01:2,sent:2,x04:2,sourc:[0,2,3],string:2,clear_zon:2,fals:2,on_messag:[3,2],perimeter_onli:2,lrr:2,on_alarm_restor:2,level:2,list:2,upload:2,dsc:2,"try":[3,2],emul:2,expandermessag:2,pleas:3,second:2,port:2,supervis:2,ad2seri:[3,2],current:2,version:2,"new":0,method:2,ser2sock:2,perimet:2,timeouterror:2,gener:2,usbdevic:[3,2],entry_delay_off:2,here:3,on_config_receiv:2,address:2,path:2,valu:2,fire_alarm:2,search:[3,2],sender:[0,3],checksum:2,prior:2,def:[3,2],invalidmessageerror:2,via:2,vid:2,appli:2,filenam:2,api:3,famili:[3,2],key_pan:2,from:[3,2],usb:[3,2],commun:2,is_reader_al:2,handler:[0,3],call:[0,2],type:2,more:3,relat:2,stage_boot:2,pkei:2,flag:2,templat:2,relai:2,actual:2,cach:2,serialdevic:2,must:0,none:[0,2],retriev:[3,2],key_f2:2,on_restor:2,restor:2,dev:2,itself:0,can:0,aliv:2,backlight_on:2,process:2,indic:[],high:2,cursor_loc:2,serial:2,occur:2,delai:2,progress_callback:2,secur:3,anoth:2,simulate_wire_problem:2,write:2,uploadchecksumerror:2,purg:2,low:2,instead:0,panic:2,panel_typ:2,updat:2,product:2,recogn:2,x509:2,ftdi:2,befor:2,attent:2,mai:3,data:2,classmethod:2,ssl_ca:2,issu:2,callback:2,"switch":2,ttimeout:2,socketdevic:2,disarm:2,jpath:2,through:2,paramet:2,bypass:2,on_read:2,main:[3,2],"return":2,python:3,timestamp:2,on_bypass:2,detach:2,name:2,revert:2,version_flag:2,authent:2,stage_wait:2,mode:2,timeout:2,debug:2,found:[3,2],nodeviceerror:2,"static":2,connect:2,our:2,read_lin:2,event:[],ad2pi:[3,2],reboot:2,content:3,reader:2,print:3,factori:2,written:2,standard:2,on_clos:2,base:[0,2],dictionari:2,"byte":2,armed_hom:2,on_detach:2,key_f4:2,product_id:2,thread:2,key_f3:2,emulate_relai:2,openssl:2,readthread:2,get_config:2,on_rfx_messag:2,find_al:2,ad2usb:[3,2],first:[3,2],oper:0,rang:2,number:2,done:2,on_writ:2,configbit:2,open:[3,2],on_power_chang:2,differ:2,unknown:2,interact:3,system:2,wrapper:2,attach:2,start_detect:2,on_open:2,termin:3,battery_low:2,specifi:2,rfmessag:2,on_fir:2,provid:[3,2],remov:[0,2],charact:2,project:3,save_config:2,bitfield:2,raw:[3,2],dedupl:2,expir:2,"__main__":3,programming_mod:2,also:[0,2],exampl:3,which:2,event_data:2,channel:2,thi:[3,2],index:3,buffer:2,object:[0,2],most:2,detect:2,basemessag:2,"class":[0,2],armed_awai:2,doc:0,clear:2,request:2,emulate_lrr:2,doe:2,on_low_batteri:2,error:2,text:2,default_product_id:2,ssl_kei:2,radio:2,find:[3,2],locat:2,configur:2,solut:2,fault_zon:2,should:2,key_f1:2,dict:2,get_vers:2,serial_numb:2,stop:2,ssl:2,progress:2,report:2,requir:[3,2],fileno:2,enabl:2,earg:0,whether:2,common:2,partit:2,contain:2,alarm_event_occur:2,certif:2,set:[3,2],keypad:2,ac_pow:2,on_alarm:2,see:3,arg:0,fail:2,close:2,arm:2,stop_read:2,pyseri:2,statu:2,wire:2,pattern:2,keypress:2,state:2,between:2,"import":3,awai:2,kei:2,numer:2,baudrat:2,alarmdecoder_object:2,last:2,fault:2,internal_address_mask:2,batteri:2,identif:2,detectthread:2,due:2,been:2,beep:2,trigger:2,basic:3,no_reader_thread:2,fire:[0,2],commerror:2,chime_on:2,convert:2,func:0,present:2,sound:2,check_zon:2,on_fault:2,cursor:2,defin:0,"while":[3,2],match:2,version_numb:2,loop:2,readi:2,kwarg:[0,2],ftdi_vendor_id:2,vendor:2,alarm_sound:2,panel_data:2,author:2,receiv:2,belong:2,handl:[3,2],status:2,finish:2,expans:2,rais:2,user:2,expand:2,lower:2,entri:2,client:2,zone_bypass:2,usual:2,boot:2,human:2,stage_error:2,expos:2,field:2,"_on_open":2,except:[3,2],on_attach:2,add:0,board:2,get_config_str:2,uploaderror:2,stage_upload:2,applic:3,on_zone_fault:2,around:2,format:2,read:2,numeric_cod:2,lcd:2,bit:2,associ:2,ad2:[3,2],like:0,deprec:2,singl:2,page:3,default_vendor_id:2,on_pan:2,intern:2,sampl:3,system_fault:2,fire_timeout:2,home:2,librari:3,definit:2,pyftdi:2,localhost:2,run:2,power:2,event_typ:2,stage_load:2,ssl_certif:2,"__name__":3,describ:2,expander_to_zon:2,simul:2,stage_start:2,address_mask:2,"float":2,automat:2,chime:2,crypto:2,support:[3,2],on_relay_chang:2,"long":2,start:2,interfac:2,includ:3,on_expander_messag:2,stop_detect:2,"function":[0,2],tupl:2,eventhandl:0,line:2,"true":3,emulate_zon:2,"default":2,displai:2,purge_buff:2,below:3,stage_debug:2,alarm:[],"int":2,descript:2,x05:2,pid:2,repres:2,on_zone_restor:2,exist:[0,2],ademco:2,read_timeout:2,ftdi_product_id:2,check:2,battery_timeout:2,handle_messag:3,when:2,invalid:2,on_disarm:2,bool:2,you:0,intend:2,firmwar:2,track:2,on_arm:2,on_sending_receiv:2,directori:3,mask:2,lrrmessag:2,on_lrr_messag:2,obj:0,time:3},objtypes:{"0":"py:module","1":"py:attribute","2":"py:class","3":"py:method","4":"py:exception","5":"py:classmethod","6":"py:staticmethod"},objnames:{"0":["py","module","Python module"],"1":["py","attribute","Python attribute"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","exception","Python exception"],"5":["py","classmethod","Python class method"],"6":["py","staticmethod","Python static method"]},filenames:["alarmdecoder.event","modules","alarmdecoder","index"],titles:["event Package","alarmdecoder","alarmdecoder Package","Welcome to Alarm Decoder’s documentation!"],objects:{"alarmdecoder.messages.LRRMessage":{partition:[2,1,1,""],dict:[2,3,1,""],event_data:[2,1,1,""],event_type:[2,1,1,""]},"alarmdecoder.messages.BaseMessage":{raw:[2,1,1,""],dict:[2,3,1,""],timestamp:[2,1,1,""]},"alarmdecoder.messages.ExpanderMessage":{ZONE:[2,1,1,""],RELAY:[2,1,1,""],value:[2,1,1,""],dict:[2,3,1,""],address:[2,1,1,""],type:[2,1,1,""],channel:[2,1,1,""]},"alarmdecoder.event.event":{EventHandler:[0,2,1,""],Event:[0,2,1,""]},"alarmdecoder.zonetracking.Zone":{status:[2,1,1,""],STATUS:[2,1,1,""],name:[2,1,1,""],zone:[2,1,1,""],timestamp:[2,1,1,""],CLEAR:[2,1,1,""],expander:[2,1,1,""],FAULT:[2,1,1,""],CHECK:[2,1,1,""]},"alarmdecoder.devices.SerialDevice":{write:[2,3,1,""],BAUDRATE:[2,1,1,""],fileno:[2,3,1,""],read:[2,3,1,""],read_line:[2,3,1,""],purge:[2,3,1,""],find_all:[2,6,1,""],"interface":[2,1,1,""],close:[2,3,1,""],open:[2,3,1,""]},"alarmdecoder.zonetracking":{Zonetracker:[2,2,1,""],Zone:[2,2,1,""]},"alarmdecoder.zonetracking.Zonetracker":{faulted:[2,1,1,""],on_restore:[2,1,1,""],update:[2,3,1,""],zones:[2,1,1,""],on_fault:[2,1,1,""],EXPIRE:[2,1,1,""],expander_to_zone:[2,3,1,""]},"alarmdecoder.devices.Device.ReadThread":{READ_TIMEOUT:[2,1,1,""],stop:[2,3,1,""],run:[2,3,1,""]},"alarmdecoder.event":{event:[0,0,0,"-"]},"alarmdecoder.messages":{Message:[2,2,1,""],LRRMessage:[2,2,1,""],RFMessage:[2,2,1,""],ExpanderMessage:[2,2,1,""],BaseMessage:[2,2,1,""]},"alarmdecoder.devices":{Device:[2,2,1,""],SocketDevice:[2,2,1,""],USBDevice:[2,2,1,""],SerialDevice:[2,2,1,""]},"alarmdecoder.devices.USBDevice.DetectThread":{stop:[2,3,1,""],run:[2,3,1,""],on_attached:[2,1,1,""],on_detached:[2,1,1,""]},alarmdecoder:{zonetracking:[2,0,0,"-"],messages:[2,0,0,"-"],devices:[2,0,0,"-"],util:[2,0,0,"-"],decoder:[2,0,0,"-"],panels:[2,0,0,"-"],event:[0,0,0,"-"]},"alarmdecoder.decoder.AlarmDecoder":{configbits:[2,1,1,""],on_rfx_message:[2,1,1,""],fault_zone:[2,3,1,""],on_expander_message:[2,1,1,""],on_open:[2,1,1,""],save_config:[2,3,1,""],serial_number:[2,1,1,""],on_alarm:[2,1,1,""],on_arm:[2,1,1,""],internal_address_mask:[2,1,1,""],on_sending_received:[2,1,1,""],KEY_PANIC:[2,1,1,""],fire_timeout:[2,1,1,""],close:[2,3,1,""],open:[2,3,1,""],id:[2,1,1,""],on_power_changed:[2,1,1,""],BATTERY_TIMEOUT:[2,1,1,""],KEY_F1:[2,1,1,""],KEY_F2:[2,1,1,""],KEY_F3:[2,1,1,""],on_message:[2,1,1,""],get_version:[2,3,1,""],reboot:[2,3,1,""],send:[2,3,1,""],version_flags:[2,1,1,""],on_zone_restore:[2,1,1,""],on_disarm:[2,1,1,""],on_fire:[2,1,1,""],on_write:[2,1,1,""],on_read:[2,1,1,""],on_lrr_message:[2,1,1,""],KEY_F4:[2,1,1,""],clear_zone:[2,3,1,""],on_zone_fault:[2,1,1,""],on_config_received:[2,1,1,""],on_alarm_restored:[2,1,1,""],get_config_string:[2,3,1,""],emulate_relay:[2,1,1,""],on_close:[2,1,1,""],on_bypass:[2,1,1,""],address:[2,1,1,""],battery_timeout:[2,1,1,""],on_panic:[2,1,1,""],on_relay_changed:[2,1,1,""],version_number:[2,1,1,""],on_low_battery:[2,1,1,""],emulate_lrr:[2,1,1,""],deduplicate:[2,1,1,""],emulate_zone:[2,1,1,""],get_config:[2,3,1,""],mode:[2,1,1,""],address_mask:[2,1,1,""],FIRE_TIMEOUT:[2,1,1,""],on_boot:[2,1,1,""]},"alarmdecoder.devices.SocketDevice":{ssl_certificate:[2,1,1,""],ssl_key:[2,1,1,""],ssl:[2,1,1,""],fileno:[2,3,1,""],read:[2,3,1,""],ssl_ca:[2,1,1,""],read_line:[2,3,1,""],purge:[2,3,1,""],write:[2,3,1,""],"interface":[2,1,1,""],close:[2,3,1,""],open:[2,3,1,""]},"alarmdecoder.devices.USBDevice":{stop_detection:[2,5,1,""],start_detection:[2,5,1,""],close:[2,3,1,""],open:[2,3,1,""],find:[2,5,1,""],DEFAULT_VENDOR_ID:[2,1,1,""],write:[2,3,1,""],PRODUCT_IDS:[2,1,1,""],serial_number:[2,1,1,""],BAUDRATE:[2,1,1,""],description:[2,1,1,""],read:[2,3,1,""],DEFAULT_PRODUCT_ID:[2,1,1,""],read_line:[2,3,1,""],find_all:[2,5,1,""],FTDI_VENDOR_ID:[2,1,1,""],"interface":[2,1,1,""],fileno:[2,3,1,""],DetectThread:[2,2,1,""],devices:[2,5,1,""],purge:[2,3,1,""],FTDI_PRODUCT_ID:[2,1,1,""]},"alarmdecoder.messages.Message":{backlight_on:[2,1,1,""],alarm_event_occurred:[2,1,1,""],programming_mode:[2,1,1,""],text:[2,1,1,""],bitfield:[2,1,1,""],armed_home:[2,1,1,""],alarm_sounding:[2,1,1,""],ready:[2,1,1,""],zone_bypassed:[2,1,1,""],panel_data:[2,1,1,""],check_zone:[2,1,1,""],numeric_code:[2,1,1,""],dict:[2,3,1,""],battery_low:[2,1,1,""],chime_on:[2,1,1,""],entry_delay_off:[2,1,1,""],perimeter_only:[2,1,1,""],fire_alarm:[2,1,1,""],ac_power:[2,1,1,""],beeps:[2,1,1,""],mask:[2,1,1,""],system_fault:[2,1,1,""],armed_away:[2,1,1,""],panel_type:[2,1,1,""],cursor_location:[2,1,1,""]},"alarmdecoder.devices.Device":{stop_reader:[2,3,1,""],on_open:[2,1,1,""],on_write:[2,1,1,""],ReadThread:[2,2,1,""],on_close:[2,1,1,""],on_read:[2,1,1,""],close:[2,3,1,""],is_reader_alive:[2,3,1,""],id:[2,1,1,""]},"alarmdecoder.messages.RFMessage":{battery:[2,1,1,""],value:[2,1,1,""],dict:[2,3,1,""],supervision:[2,1,1,""],serial_number:[2,1,1,""],loop:[2,1,1,""]},"alarmdecoder.decoder":{AlarmDecoder:[2,2,1,""]},"alarmdecoder.event.event.EventHandler":{fire:[0,3,1,""],add:[0,3,1,""],remove:[0,3,1,""]},"alarmdecoder.util.Firmware":{STAGE_ERROR:[2,1,1,""],STAGE_LOAD:[2,1,1,""],upload:[2,6,1,""],STAGE_BOOT:[2,1,1,""],STAGE_START:[2,1,1,""],STAGE_UPLOADING:[2,1,1,""],STAGE_DEBUG:[2,1,1,""],STAGE_WAITING:[2,1,1,""],STAGE_DONE:[2,1,1,""]},"alarmdecoder.util":{Firmware:[2,2,1,""],TimeoutError:[2,4,1,""],NoDeviceError:[2,4,1,""],CommError:[2,4,1,""],UploadChecksumError:[2,4,1,""],UploadError:[2,4,1,""],InvalidMessageError:[2,4,1,""]}},titleterms:{alarmdecod:[2,1],welcom:3,alarm:3,devic:2,messag:2,event:0,util:2,packag:[0,2],decod:[3,2],zonetrack:2,indic:3,tabl:3,document:3,modul:[0,2],panel:2}}) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # alarmdecoder documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jun 8 14:38:46 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'alarmdecoder' 44 | copyright = u'2013, Nu Tech Software Solutions, Inc.' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | # If true, keep warnings as "system message" paragraphs in the built documents. 90 | #keep_warnings = False 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'alarmdecoderdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'alarmdecoder.tex', u'AlarmDecoder Documentation', 190 | u'Nu Tech Software Solutions, Inc.', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'alarmdecoder', u'AlarmDecoder Documentation', 220 | [u'Nu Tech Software Solutions, Inc.'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'alarmdecoder', u'AlarmDecoder Documentation', 234 | u'Nu Tech Software Solutions, Inc.', 'alarmdecoder', 'Python library for the Alarm Decoder (AD2) device family.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | 247 | # If true, do not generate a @detailmenu in the "Top" node's menu. 248 | #texinfo_no_detailmenu = False 249 | 250 | 251 | # -- Options for Epub output --------------------------------------------------- 252 | 253 | # Bibliographic Dublin Core info. 254 | epub_title = u'alarmdecoder' 255 | epub_author = u'Nu Tech Software Solutions, Inc.' 256 | epub_publisher = u'Nu Tech Software Solutions, Inc.' 257 | epub_copyright = u'2013, Nu Tech Software Solutions, Inc.' 258 | 259 | # The language of the text. It defaults to the language option 260 | # or en if the language is not set. 261 | #epub_language = '' 262 | 263 | # The scheme of the identifier. Typical schemes are ISBN or URL. 264 | #epub_scheme = '' 265 | 266 | # The unique identifier of the text. This can be a ISBN number 267 | # or the project homepage. 268 | #epub_identifier = '' 269 | 270 | # A unique identification for the text. 271 | #epub_uid = '' 272 | 273 | # A tuple containing the cover image and cover page html template filenames. 274 | #epub_cover = () 275 | 276 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 277 | #epub_guide = () 278 | 279 | # HTML files that should be inserted before the pages created by sphinx. 280 | # The format is a list of tuples containing the path and title. 281 | #epub_pre_files = [] 282 | 283 | # HTML files shat should be inserted after the pages created by sphinx. 284 | # The format is a list of tuples containing the path and title. 285 | #epub_post_files = [] 286 | 287 | # A list of files that should not be packed into the epub file. 288 | #epub_exclude_files = [] 289 | 290 | # The depth of the table of contents in toc.ncx. 291 | #epub_tocdepth = 3 292 | 293 | # Allow duplicate toc entries. 294 | #epub_tocdup = True 295 | 296 | # Fix unsupported image types using the PIL. 297 | #epub_fix_images = False 298 | 299 | # Scale large images. 300 | #epub_max_image_width = 0 301 | 302 | # If 'no', URL addresses will not be shown. 303 | #epub_show_urls = 'inline' 304 | 305 | # If false, no index is generated. 306 | #epub_use_index = True 307 | 308 | autodoc_member_order = 'bysource' 309 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. alarmdecoder documentation master file, created by 2 | sphinx-quickstart on Sat Jun 8 14:38:46 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Alarm Decoder's documentation! 7 | ========================================= 8 | 9 | .. _Alarm Decoder: http://www.alarmdecoder.com 10 | .. _examples: http://github.com/nutechsoftware/alarmdecoder/tree/master/examples 11 | 12 | This is the API documentation for the `Alarm Decoder`_ Python library. It provides support for interacting with the `Alarm Decoder`_ (AD2) family of security alarm devices, including the AD2USB, AD2SERIAL and AD2PI. 13 | 14 | The source code, requirements and examples for this project may be found `here `_. 15 | 16 | Please see the `examples`_ directory for more samples, but a basic one is included below:: 17 | 18 | import time 19 | from alarmdecoder import AlarmDecoder 20 | from alarmdecoder.devices import USBDevice 21 | 22 | def main(): 23 | """ 24 | Example application that prints messages from the panel to the terminal. 25 | """ 26 | try: 27 | # Retrieve the first USB device 28 | device = AlarmDecoder(USBDevice.find()) 29 | 30 | # Set up an event handler and open the device 31 | device.on_message += handle_message 32 | with device.open(): 33 | while True: 34 | time.sleep(1) 35 | 36 | except Exception, ex: 37 | print 'Exception:', ex 38 | 39 | def handle_message(sender, message): 40 | """ 41 | Handles message events from the AlarmDecoder. 42 | """ 43 | print sender, message.raw 44 | 45 | if __name__ == '__main__': 46 | main() 47 | 48 | Table of Contents: 49 | 50 | .. toctree:: 51 | :maxdepth: 4 52 | 53 | alarmdecoder 54 | 55 | 56 | Indices and tables 57 | ================== 58 | 59 | * :ref:`genindex` 60 | * :ref:`modindex` 61 | * :ref:`search` 62 | 63 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\alarmdecoder.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\alarmdecoder.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | alarmdecoder 2 | ============ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | alarmdecoder 8 | -------------------------------------------------------------------------------- /examples/alarm_email.py: -------------------------------------------------------------------------------- 1 | import time 2 | import smtplib 3 | from email.mime.text import MIMEText 4 | from alarmdecoder import AlarmDecoder 5 | from alarmdecoder.devices import SerialDevice 6 | 7 | # Configuration values 8 | SUBJECT = "AlarmDecoder - ALARM" 9 | FROM_ADDRESS = "root@localhost" 10 | TO_ADDRESS = "root@localhost" # NOTE: Sending an SMS is as easy as looking 11 | # up the email address format for your provider. 12 | SMTP_SERVER = "localhost" 13 | SMTP_USERNAME = None 14 | SMTP_PASSWORD = None 15 | 16 | SERIAL_DEVICE = '/dev/ttyUSB0' 17 | BAUDRATE = 115200 18 | 19 | def main(): 20 | """ 21 | Example application that sends an email when an alarm event is 22 | detected. 23 | """ 24 | try: 25 | # Retrieve the first USB device 26 | device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) 27 | 28 | # Set up an event handler and open the device 29 | device.on_alarm += handle_alarm 30 | with device.open(baudrate=BAUDRATE): 31 | while True: 32 | time.sleep(1) 33 | 34 | except Exception as ex: 35 | print('Exception:', ex) 36 | 37 | def handle_alarm(sender, **kwargs): 38 | """ 39 | Handles alarm events from the AlarmDecoder. 40 | """ 41 | zone = kwargs.pop('zone', None) 42 | text = "Alarm: Zone {0}".format(zone) 43 | 44 | # Build the email message 45 | msg = MIMEText(text) 46 | msg['Subject'] = SUBJECT 47 | msg['From'] = FROM_ADDRESS 48 | msg['To'] = TO_ADDRESS 49 | 50 | s = smtplib.SMTP(SMTP_SERVER) 51 | 52 | # Authenticate if needed 53 | if SMTP_USERNAME is not None: 54 | s.login(SMTP_USERNAME, SMTP_PASSWORD) 55 | 56 | # Send the email 57 | s.sendmail(FROM_ADDRESS, TO_ADDRESS, msg.as_string()) 58 | s.quit() 59 | 60 | print('sent alarm email:', text) 61 | 62 | if __name__ == '__main__': 63 | main() 64 | -------------------------------------------------------------------------------- /examples/lrr_example.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import SerialDevice 4 | 5 | SERIAL_DEVICE = '/dev/ttyUSB0' 6 | BAUDRATE = 115200 7 | 8 | def main(): 9 | """ 10 | Example application that prints messages from the panel to the terminal. 11 | """ 12 | try: 13 | # Retrieve the first USB device 14 | device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) 15 | 16 | # Set up an event handler and open the device 17 | device.on_lrr_message += handle_lrr_message 18 | with device.open(baudrate=BAUDRATE): 19 | while True: 20 | time.sleep(1) 21 | 22 | except Exception as ex: 23 | print('Exception:', ex) 24 | 25 | def handle_lrr_message(sender, message): 26 | """ 27 | Handles message events from the AlarmDecoder. 28 | """ 29 | print(sender, message.partition, message.event_type, message.event_data) 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /examples/rf_device.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import SerialDevice 4 | 5 | RF_DEVICE_SERIAL_NUMBER = '0252254' 6 | 7 | SERIAL_DEVICE = '/dev/ttyUSB0' 8 | BAUDRATE = 115200 9 | 10 | def main(): 11 | """ 12 | Example application that watches for an event from a specific RF device. 13 | 14 | This feature allows you to watch for events from RF devices if you have 15 | an RF receiver. This is useful in the case of internal sensors, which 16 | don't emit a FAULT if the sensor is tripped and the panel is armed STAY. 17 | It also will monitor sensors that aren't configured. 18 | 19 | NOTE: You must have an RF receiver installed and enabled in your panel 20 | for RFX messages to be seen. 21 | """ 22 | try: 23 | # Retrieve the first USB device 24 | device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) 25 | 26 | # Set up an event handler and open the device 27 | device.on_rfx_message += handle_rfx 28 | with device.open(baudrate=BAUDRATE): 29 | while True: 30 | time.sleep(1) 31 | 32 | except Exception as ex: 33 | print('Exception:', ex) 34 | 35 | def handle_rfx(sender, message): 36 | """ 37 | Handles RF message events from the AlarmDecoder. 38 | """ 39 | # Check for our target serial number and loop 40 | if message.serial_number == RF_DEVICE_SERIAL_NUMBER and message.loop[0] == True: 41 | print(message.serial_number, 'triggered loop #1') 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /examples/serialport.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import SerialDevice 4 | 5 | # Configuration values 6 | SERIAL_DEVICE = '/dev/ttyUSB0' 7 | BAUDRATE = 115200 8 | 9 | def main(): 10 | """ 11 | Example application that opens a serial device and prints messages to the terminal. 12 | """ 13 | try: 14 | # Retrieve the specified serial device. 15 | device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) 16 | 17 | # Set up an event handler and open the device 18 | device.on_message += handle_message 19 | 20 | # Override the default SerialDevice baudrate since we're using a USB device 21 | # over serial in this example. 22 | with device.open(baudrate=BAUDRATE): 23 | while True: 24 | time.sleep(1) 25 | 26 | except Exception as ex: 27 | print('Exception:', ex) 28 | 29 | def handle_message(sender, message): 30 | """ 31 | Handles message events from the AlarmDecoder. 32 | """ 33 | print(sender, message.raw) 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /examples/socket_example.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import SocketDevice 4 | 5 | # Configuration values 6 | HOSTNAME = 'localhost' 7 | PORT = 10000 8 | 9 | def main(): 10 | """ 11 | Example application that opens a device that has been exposed to the network 12 | with ser2sock or similar serial-to-IP software. 13 | """ 14 | try: 15 | # Retrieve an AD2 device that has been exposed with ser2sock on localhost:10000. 16 | device = AlarmDecoder(SocketDevice(interface=(HOSTNAME, PORT))) 17 | 18 | # Set up an event handler and open the device 19 | device.on_message += handle_message 20 | with device.open(): 21 | while True: 22 | time.sleep(1) 23 | 24 | except Exception as ex: 25 | print('Exception:', ex) 26 | 27 | def handle_message(sender, message): 28 | """ 29 | Handles message events from the AlarmDecoder. 30 | """ 31 | print(sender, message.raw) 32 | 33 | if __name__ == '__main__': 34 | main() 35 | -------------------------------------------------------------------------------- /examples/ssl_socket.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import SocketDevice 4 | 5 | # Configuration values 6 | HOSTNAME = 'localhost' 7 | PORT = 10000 8 | SSL_KEY = 'cert.key' 9 | SSL_CERT = 'cert.pem' 10 | SSL_CA = 'ca.pem' 11 | 12 | def main(): 13 | """ 14 | Example application that opens a device that has been exposed to the network 15 | with ser2sock and SSL encryption and authentication. 16 | """ 17 | try: 18 | # Retrieve an AD2 device that has been exposed with ser2sock on localhost:10000. 19 | ssl_device = SocketDevice(interface=('localhost', 10000)) 20 | 21 | # Enable SSL and set the certificates to be used. 22 | # 23 | # The key/cert attributes can either be a filesystem path or an X509/PKey 24 | # object from pyopenssl. 25 | ssl_device.ssl = True 26 | ssl_device.ssl_ca = SSL_CA # CA certificate 27 | ssl_device.ssl_key = SSL_KEY # Client private key 28 | ssl_device.ssl_certificate = SSL_CERT # Client certificate 29 | 30 | device = AlarmDecoder(ssl_device) 31 | 32 | # Set up an event handler and open the device 33 | device.on_message += handle_message 34 | with device.open(): 35 | while True: 36 | time.sleep(1) 37 | 38 | except Exception as ex: 39 | print('Exception:', ex) 40 | 41 | def handle_message(sender, message): 42 | """ 43 | Handles message events from the AlarmDecoder. 44 | """ 45 | print(sender, message.raw) 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /examples/usb_detection.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import USBDevice 4 | 5 | __devices = {} 6 | 7 | def main(): 8 | """ 9 | Example application that shows how to handle attach/detach events generated 10 | by the USB devices. 11 | 12 | In this case we open the device and listen for messages when it is attached. 13 | And when it is detached we remove it from our list of monitored devices. 14 | """ 15 | try: 16 | # Start up the detection thread such that handle_attached and handle_detached will 17 | # be called when devices are attached and detached, respectively. 18 | USBDevice.start_detection(on_attached=handle_attached, on_detached=handle_detached) 19 | 20 | # Wait for events. 21 | while True: 22 | time.sleep(1) 23 | 24 | except Exception as ex: 25 | print('Exception:', ex) 26 | 27 | finally: 28 | # Close all devices and stop detection. 29 | for sn, device in __devices.items(): 30 | device.close() 31 | 32 | USBDevice.stop_detection() 33 | 34 | def create_device(device_args): 35 | """ 36 | Creates an AlarmDecoder from the specified USB device arguments. 37 | 38 | :param device_args: Tuple containing information on the USB device to open. 39 | :type device_args: Tuple (vid, pid, serialnumber, interface_count, description) 40 | """ 41 | device = AlarmDecoder(USBDevice.find(device_args)) 42 | device.on_message += handle_message 43 | device.open() 44 | 45 | return device 46 | 47 | def handle_message(sender, message): 48 | """ 49 | Handles message events from the AlarmDecoder. 50 | """ 51 | print(sender, message.raw) 52 | 53 | def handle_attached(sender, device): 54 | """ 55 | Handles attached events from USBDevice.start_detection(). 56 | """ 57 | # Create the device from the specified device arguments. 58 | dev = create_device(device) 59 | __devices[dev.id] = dev 60 | 61 | print('attached', dev.id) 62 | 63 | def handle_detached(sender, device): 64 | """ 65 | Handles detached events from USBDevice.start_detection(). 66 | """ 67 | vendor, product, sernum, ifcount, description = device 68 | 69 | # Close and remove the device from our list. 70 | if sernum in list(__devices.keys()): 71 | __devices[sernum].close() 72 | 73 | del __devices[sernum] 74 | 75 | print('detached', sernum) 76 | 77 | if __name__ == '__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /examples/usb_device.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import USBDevice 4 | 5 | def main(): 6 | """ 7 | Example application that prints messages from the panel to the terminal. 8 | """ 9 | try: 10 | # Retrieve the first USB device 11 | device = AlarmDecoder(USBDevice.find()) 12 | 13 | # Set up an event handler and open the device 14 | device.on_message += handle_message 15 | with device.open(): 16 | while True: 17 | time.sleep(1) 18 | 19 | except Exception as ex: 20 | print('Exception:', ex) 21 | 22 | def handle_message(sender, message): 23 | """ 24 | Handles message events from the AlarmDecoder. 25 | """ 26 | print(sender, message.raw) 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /examples/virtual_zone_expander.py: -------------------------------------------------------------------------------- 1 | import time 2 | from alarmdecoder import AlarmDecoder 3 | from alarmdecoder.devices import SerialDevice 4 | 5 | # Configuration values 6 | TARGET_ZONE = 41 7 | WAIT_TIME = 10 8 | 9 | SERIAL_DEVICE = '/dev/ttyUSB0' 10 | BAUDRATE = 115200 11 | 12 | def main(): 13 | """ 14 | Example application that periodically faults a virtual zone and then 15 | restores it. 16 | 17 | This is an advanced feature that allows you to emulate a virtual zone. When 18 | the AlarmDecoder is configured to emulate a zone expander we can fault and 19 | restore those zones programmatically at will. These events can also be seen by 20 | others, such as home automation platforms which allows you to connect other 21 | devices or services and monitor them as you would any physical zone. 22 | 23 | For example, you could connect a ZigBee device and receiver and fault or 24 | restore it's zone(s) based on the data received. 25 | 26 | In order for this to happen you need to perform a couple configuration steps: 27 | 28 | 1. Enable zone expander emulation on your AlarmDecoder device by hitting '!' 29 | in a terminal and going through the prompts. 30 | 2. Enable the zone expander in your panel programming. 31 | """ 32 | try: 33 | # Retrieve the first USB device 34 | device = AlarmDecoder(SerialDevice(interface=SERIAL_DEVICE)) 35 | 36 | # Set up an event handlers and open the device 37 | device.on_zone_fault += handle_zone_fault 38 | device.on_zone_restore += handle_zone_restore 39 | 40 | with device.open(baudrate=BAUDRATE): 41 | last_update = time.time() 42 | while True: 43 | if time.time() - last_update > WAIT_TIME: 44 | last_update = time.time() 45 | 46 | device.fault_zone(TARGET_ZONE) 47 | 48 | time.sleep(1) 49 | 50 | except Exception as ex: 51 | print('Exception:', ex) 52 | 53 | def handle_zone_fault(sender, zone): 54 | """ 55 | Handles zone fault messages. 56 | """ 57 | print('zone faulted', zone) 58 | 59 | # Restore the zone 60 | sender.clear_zone(zone) 61 | 62 | def handle_zone_restore(sender, zone): 63 | """ 64 | Handles zone restore messages. 65 | """ 66 | print('zone cleared', zone) 67 | 68 | if __name__ == '__main__': 69 | main() 70 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future>=0.14.3 2 | pyserial>=2.7 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script""" 2 | 3 | import sys 4 | from setuptools import setup 5 | 6 | def readme(): 7 | """Returns the contents of README.rst""" 8 | 9 | with open('README.rst') as readme_file: 10 | return readme_file.read() 11 | 12 | extra_requirements = [] 13 | if sys.version_info < (3,): 14 | extra_requirements.append('future>=0.14.3') 15 | 16 | setup(name='alarmdecoder', 17 | use_scm_version=True, 18 | setup_requires=["setuptools_scm"], 19 | description='Python interface for the AlarmDecoder (AD2) family ' 20 | 'of alarm devices which includes the AD2USB, AD2SERIAL and AD2PI.', 21 | long_description=readme(), 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3.4', 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | 'Topic :: Communications', 29 | 'Topic :: Home Automation', 30 | 'Topic :: Security', 31 | ], 32 | keywords='alarmdecoder alarm decoder ad2 ad2usb ad2serial ad2pi security ' 33 | 'ademco dsc nutech', 34 | url='http://github.com/nutechsoftware/alarmdecoder', 35 | author='Nu Tech Software Solutions, Inc.', 36 | author_email='general@support.nutech.com', 37 | license='MIT', 38 | packages=['alarmdecoder', 'alarmdecoder.devices', 'alarmdecoder.event', 'alarmdecoder.messages', 'alarmdecoder.messages.lrr'], 39 | install_requires=[ 40 | 'pyserial>=2.7', 41 | ] + extra_requirements, 42 | test_suite='nose.collector', 43 | tests_require=['nose', 'mock'], 44 | scripts=['bin/ad2-sslterm', 'bin/ad2-firmwareupload'], 45 | include_package_data=True, 46 | zip_safe=False, 47 | options={ 48 | 'bdist_wheel': {'universal': 1} 49 | }) 50 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/test/__init__.py -------------------------------------------------------------------------------- /test/test_messages.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from alarmdecoder.messages import Message, ExpanderMessage, RFMessage, LRRMessage 4 | from alarmdecoder.messages.lrr import LRR_EVENT_TYPE, LRR_CID_EVENT, LRR_EVENT_STATUS 5 | from alarmdecoder.util import InvalidMessageError 6 | from alarmdecoder.panels import ADEMCO 7 | 8 | 9 | class TestMessages(TestCase): 10 | def setUp(self): 11 | pass 12 | 13 | def tearDown(self): 14 | pass 15 | 16 | ### Tests 17 | def test_message_parse(self): 18 | msg = Message('[00000000000000000A--],001,[f707000600e5800c0c020000],"FAULT 1 "') 19 | 20 | self.assertFalse(msg.ready) 21 | self.assertFalse(msg.armed_away) 22 | self.assertFalse(msg.armed_home) 23 | self.assertFalse(msg.backlight_on) 24 | self.assertFalse(msg.programming_mode) 25 | self.assertEqual(msg.beeps, 0) 26 | self.assertFalse(msg.zone_bypassed) 27 | self.assertFalse(msg.ac_power) 28 | self.assertFalse(msg.chime_on) 29 | self.assertFalse(msg.alarm_event_occurred) 30 | self.assertFalse(msg.alarm_sounding) 31 | self.assertFalse(msg.battery_low) 32 | self.assertFalse(msg.entry_delay_off) 33 | self.assertFalse(msg.fire_alarm) 34 | self.assertFalse(msg.check_zone) 35 | self.assertFalse(msg.perimeter_only) 36 | self.assertEqual(msg.system_fault, 0) 37 | self.assertFalse(msg.panel_type, ADEMCO) 38 | self.assertEqual(msg.numeric_code, '001') 39 | self.assertEqual(msg.mask, int('07000600', 16)) 40 | self.assertEqual(msg.cursor_location, -1) 41 | self.assertEqual(msg.text, 'FAULT 1 ') 42 | 43 | def test_message_parse_fail(self): 44 | with self.assertRaises(InvalidMessageError): 45 | msg = Message('') 46 | 47 | def test_expander_message_parse(self): 48 | msg = ExpanderMessage('!EXP:07,01,01') 49 | 50 | self.assertEqual(msg.address, 7) 51 | self.assertEqual(msg.channel, 1) 52 | self.assertEqual(msg.value, 1) 53 | 54 | def test_expander_message_parse_fail(self): 55 | with self.assertRaises(InvalidMessageError): 56 | msg = ExpanderMessage('') 57 | 58 | def test_rf_message_parse(self): 59 | msg = RFMessage('!RFX:0180036,80') 60 | 61 | self.assertEqual(msg.serial_number, '0180036') 62 | self.assertEqual(msg.value, int('80', 16)) 63 | 64 | def test_rf_message_parse_fail(self): 65 | with self.assertRaises(InvalidMessageError): 66 | msg = RFMessage('') 67 | 68 | def test_lrr_message_parse_v1(self): 69 | msg = LRRMessage('!LRR:012,1,ARM_STAY') 70 | 71 | self.assertEqual(msg.event_data, '012') 72 | self.assertEqual(msg.partition, '1') 73 | self.assertEqual(msg.event_type, 'ARM_STAY') 74 | 75 | def test_lrr_message_parse_v2(self): 76 | msg = LRRMessage('!LRR:001,1,CID_3401,ff') 77 | self.assertIsInstance(msg, LRRMessage) 78 | self.assertEquals(msg.event_data, '001') 79 | self.assertEquals(msg.partition, '1') 80 | self.assertEquals(msg.event_prefix, 'CID') 81 | self.assertEquals(msg.event_source, LRR_EVENT_TYPE.CID) 82 | self.assertEquals(msg.event_status, LRR_EVENT_STATUS.RESTORE) 83 | self.assertEquals(msg.event_code, LRR_CID_EVENT.OPENCLOSE_BY_USER) 84 | self.assertEquals(msg.report_code, 'ff') 85 | 86 | def test_lrr_event_code_override(self): 87 | msg = LRRMessage('!LRR:001,1,CID_3400,01') 88 | self.assertEquals(msg.event_code, LRR_CID_EVENT.OPENCLOSE_BY_USER) # 400 -> 401 89 | 90 | def test_lrr_message_parse_fail(self): 91 | with self.assertRaises(InvalidMessageError): 92 | msg = LRRMessage('') 93 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutechsoftware/alarmdecoder/8537c9cde6d16ded096f1201fe3352d379505c03/test/test_util.py -------------------------------------------------------------------------------- /test/test_zonetracking.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mock import Mock, MagicMock 3 | 4 | from alarmdecoder import AlarmDecoder 5 | from alarmdecoder.panels import ADEMCO 6 | from alarmdecoder.messages import Message, ExpanderMessage 7 | from alarmdecoder.zonetracking import Zonetracker, Zone 8 | 9 | 10 | class TestZonetracking(TestCase): 11 | def setUp(self): 12 | self._alarmdecoder = Mock(spec=AlarmDecoder) 13 | 14 | self._alarmdecoder.mode = ADEMCO 15 | self._zonetracker = Zonetracker(self._alarmdecoder) 16 | 17 | self._zonetracker.on_fault += self.fault_event 18 | self._zonetracker.on_restore += self.restore_event 19 | 20 | self._faulted = False 21 | self._restored = False 22 | 23 | def tearDown(self): 24 | pass 25 | 26 | ### Library events 27 | def fault_event(self, sender, *args, **kwargs): 28 | self._faulted = True 29 | 30 | def restore_event(self, sender, *args, **kwargs): 31 | self._restored = True 32 | 33 | ### Util 34 | def _build_expander_message(self, msg): 35 | msg = ExpanderMessage(msg) 36 | zone = self._zonetracker.expander_to_zone(msg.address, msg.channel) 37 | 38 | return zone, msg 39 | 40 | ### Tests 41 | def test_zone_fault(self): 42 | zone, msg = self._build_expander_message('!EXP:07,01,01') 43 | self._zonetracker.update(msg) 44 | 45 | self.assertEqual(self._zonetracker._zones[zone].status, Zone.FAULT) 46 | self.assertTrue(self._faulted) 47 | 48 | def test_zone_restore(self): 49 | zone, msg = self._build_expander_message('!EXP:07,01,01') 50 | self._zonetracker.update(msg) 51 | 52 | zone, msg = self._build_expander_message('!EXP:07,01,00') 53 | self._zonetracker.update(msg) 54 | 55 | self.assertEqual(self._zonetracker._zones[zone].status, Zone.CLEAR) 56 | self.assertTrue(self._restored) 57 | 58 | def test_message_ready(self): 59 | msg = Message('[00000000000000100A--],001,[f707000600e5800c0c020000]," "') 60 | self._zonetracker.update(msg) 61 | 62 | self.assertEqual(len(self._zonetracker._zones_faulted), 1) 63 | 64 | msg = Message('[10000000000000000A--],000,[f707000600e5800c0c020000]," "') 65 | self._zonetracker.update(msg) 66 | 67 | self.assertEqual(len(self._zonetracker._zones_faulted), 0) 68 | 69 | def test_message_fault_text(self): 70 | msg = Message('[00000000000000000A--],001,[f707000600e5800c0c020000],"FAULT 1 "') 71 | self._zonetracker.update(msg) 72 | 73 | self.assertEqual(len(self._zonetracker._zones_faulted), 1) 74 | 75 | def test_ECP_failure(self): 76 | msg = Message('[00000000000000100A--],0bf,[f707000600e5800c0c020000],"CHECK 1 "') 77 | self._zonetracker.update(msg) 78 | 79 | self.assertEqual(self._zonetracker._zones[1].status, Zone.CHECK) 80 | 81 | def test_zone_restore_skip(self): 82 | panel_messages = [ 83 | '[00000000000000000A--],001,[f707000600e5800c0c020000],"FAULT 1 "', 84 | '[00000000000000000A--],002,[f707000600e5800c0c020000],"FAULT 2 "', 85 | '[00000000000000000A--],001,[f707000600e5800c0c020000],"FAULT 1 "', 86 | '[00000000000000000A--],001,[f707000600e5800c0c020000],"FAULT 1 "' 87 | ] 88 | 89 | for m in panel_messages: 90 | msg = Message(m) 91 | 92 | self._zonetracker.update(msg) 93 | 94 | self.assertIn(1, self._zonetracker._zones_faulted) 95 | self.assertNotIn(2, self._zonetracker._zones_faulted) 96 | 97 | def test_zone_out_of_order_fault(self): 98 | panel_messages = [ 99 | '[00000000000000100A--],001,[f707000600e5800c0c020000],"FAULT 1 "', 100 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 101 | '[00000000000000100A--],003,[f707000600e5800c0c020000],"FAULT 3 "', 102 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 103 | ] 104 | 105 | for m in panel_messages: 106 | msg = Message(m) 107 | 108 | self._zonetracker.update(msg) 109 | 110 | self.assertIn(1, self._zonetracker._zones_faulted) 111 | self.assertIn(3, self._zonetracker._zones_faulted) 112 | self.assertIn(4, self._zonetracker._zones_faulted) 113 | 114 | def test_zone_multi_zone_skip_restore(self): 115 | panel_messages = [ 116 | '[00000000000000100A--],001,[f707000600e5800c0c020000],"FAULT 1 "', 117 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 118 | '[00000000000000100A--],002,[f707000600e5800c0c020000],"FAULT 2 "', 119 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 120 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 121 | ] 122 | 123 | for m in panel_messages: 124 | msg = Message(m) 125 | 126 | self._zonetracker.update(msg) 127 | 128 | self.assertNotIn(1, self._zonetracker._zones_faulted) 129 | self.assertNotIn(2, self._zonetracker._zones_faulted) 130 | self.assertIn(4, self._zonetracker._zones_faulted) 131 | 132 | def test_zone_timeout_restore(self): 133 | panel_messages = [ 134 | '[00000000000000100A--],001,[f707000600e5800c0c020000],"FAULT 1 "', 135 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 136 | '[00000000000000100A--],002,[f707000600e5800c0c020000],"FAULT 2 "', 137 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 138 | '[00000000000000100A--],004,[f707000600e5800c0c020000],"FAULT 4 "', 139 | ] 140 | 141 | for m in panel_messages: 142 | msg = Message(m) 143 | 144 | self._zonetracker.update(msg) 145 | 146 | self.assertIn(4, self._zonetracker._zones_faulted) 147 | self._zonetracker._zones[4].timestamp -= 35 # forcefully expire the zone 148 | 149 | # generic message to force an update. 150 | msg = Message('[00000000000000000A--],000,[f707000600e5800c0c020000]," "') 151 | self._zonetracker.update(msg) 152 | 153 | self.assertNotIn(4, self._zonetracker._zones_faulted) 154 | --------------------------------------------------------------------------------