diff --git a/py_vista_turbo_serial/examples/__init__.py b/py_vista_turbo_serial/examples/__init__.py new file mode 100644 index 0000000..0c5ac87 --- /dev/null +++ b/py_vista_turbo_serial/examples/__init__.py @@ -0,0 +1,36 @@ +""" +The latest version of this package is available at: + + +################################################################################## +Copyright 2018 Jason Antman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +################################################################################## +While not legally required, I sincerely request that anyone who finds +bugs please submit them at or +to me via email, and that you send any contributions or improvements +either as a pull request on GitHub, or to me via email. +################################################################################## + +AUTHORS: +Jason Antman +################################################################################## +""" diff --git a/py_vista_turbo_serial/examples/pushover_example.py b/py_vista_turbo_serial/examples/pushover_example.py new file mode 100644 index 0000000..949e461 --- /dev/null +++ b/py_vista_turbo_serial/examples/pushover_example.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +""" +The latest version of this package is available at: + + +################################################################################## +Copyright 2018 Jason Antman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +################################################################################## +While not legally required, I sincerely request that anyone who finds +bugs please submit them at or +to me via email, and that you send any contributions or improvements +either as a pull request on GitHub, or to me via email. +################################################################################## + +AUTHORS: +Jason Antman +################################################################################## +""" + +import sys +import os +import argparse +import logging +from typing import List, Optional +import threading +from time import sleep +from datetime import datetime + +from py_vista_turbo_serial.communicator import Communicator +from py_vista_turbo_serial.messages import ( + MessagePacket, ArmingStatusRequest, ZoneStatusRequest +) + +import requests + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s %(levelname)s] %(message)s" +) +logger: logging.Logger = logging.getLogger() + + +class MessageStore: + + def __init__(self): + self._lock: threading.Lock = threading.Lock() + self._messages: List[str] = [] + + def add(self, msg: str): + logger.debug('Enqueue: %s', msg) + with self._lock: + self._messages.append(msg) + + def get_all(self) -> List[str]: + with self._lock: + items, self._messages = self._messages, [] + return items + + +class PushoverNotifier(threading.Thread): + + def __init__( + self, store: MessageStore, app_token: str, user_key: str, + batch_seconds: int = 10, proxies: Optional[dict] = None + ): + super().__init__() + self.daemon = True + self._store: MessageStore = store + self._app_token: str = app_token + self._user_key: str = user_key + self._batch_seconds: int = batch_seconds + self._proxies: Optional[dict] = proxies + + def _do_notify_pushover(self, title, message, sound=None): + """Build Pushover API request arguments and call _send_pushover""" + d = { + 'data': { + 'token': self._app_token, + 'user': self._user_key, + 'title': title, + 'message': message, + 'retry': 300 # 5 minutes + } + } + if sound is not None: + d['data']['sound'] = sound + logger.info('Sending Pushover notification: %s', d) + for i in range(0, 2): + try: + self._send_pushover(d) + return + except Exception: + logger.critical( + 'send_pushover raised exception', exc_info=True + ) + if self._proxies: + logger.critical( + 'send_pushover failed on all attempts and proxies is empty!' + ) + return + # try sending through proxy + d['proxies'] = self._proxies + for i in range(0, 2): + try: + self._send_pushover(d) + return + except Exception: + logger.critical( + 'send_pushover via proxy raised exception', exc_info=True + ) + + def _send_pushover(self, params): + """ + Send the actual Pushover notification. + + We do this directly with ``requests`` because python-pushover still + doesn't have support for images or some other API options. + """ + url = 'https://api.pushover.net/1/messages.json' + if 'proxies' in params: + logger.debug( + 'Sending Pushover notification with proxies=%s', + params['proxies'] + ) + else: + logger.debug('Sending Pushover notification') + r = requests.post(url, timeout=5, **params) + logger.debug( + 'Pushover POST response HTTP %s: %s', r.status_code, r.text + ) + r.raise_for_status() + if r.json()['status'] != 1: + raise RuntimeError('Error response from Pushover: %s', r.text) + logger.info('Pushover Notification Success: %s', r.text) + + def _handle_messages(self, messages: List[str]): + title: str = f'{len(messages)} Alarm Messages' + if len(messages) == 1: + title = '1 Alarm Message' + self._do_notify_pushover(title, '\n'.join(messages)) + + def run(self): + messages: List[str] + while True: + messages = self._store.get_all() + if messages: + logger.info('Got %d messages to handle', len(messages)) + self._handle_messages(messages) + sleep(self._batch_seconds) + + +class PushoverAlarmNotifier: + + def __init__(self, port: str, batch_seconds: int = 10): + self.store: MessageStore = MessageStore() + self.store.add( + 'PushoverAlarmNotifier initializing at ' + + datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + ) + self.notifier: PushoverNotifier = PushoverNotifier( + store=self.store, + app_token=os.environ['PUSHOVER_APIKEY'], + user_key=os.environ['PUSHOVER_USERKEY'], + batch_seconds=batch_seconds + ) + self.notifier.start() + self.panel: Communicator = Communicator(port=port) + + def run(self): + self.store.add( + 'PushoverAlarmNotifier starting run loop at ' + + datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + ) + self.panel.send_message(ArmingStatusRequest.generate()) + self.panel.send_message(ZoneStatusRequest.generate()) + message: MessagePacket + dt: str + for message in self.panel.communicate(): + dt = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + """ + if isinstance(message, ArmingStatusReport): + raise NotImplementedError() + elif isinstance(message, ZoneStatusReport): + raise NotImplementedError() + else: + self.store.add(str(message) + ' (' + dt + ')') + """ + self.store.add(str(message) + ' (' + dt + ')') + + +def parse_args(argv): + p = argparse.ArgumentParser(description='Alarm state Pushover notifier') + p.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', + default=False, help='verbose output' + ) + p.add_argument( + '-s', '--seconds', dest='seconds', action='store', + type=int, default=10, + help='How many seconds to wait before sending batches of messages ' + 'to Pushover; default: 10' + ) + p.add_argument( + 'PORT', action='store', type=str, default='/dev/ttyUSB0', + help='Serial port to connect to (default: /dev/ttyUSB0)' + ) + args = p.parse_args(argv) + return args + + +def set_log_info(l: logging.Logger): + """set logger level to INFO""" + set_log_level_format( + l, + logging.INFO, + '%(asctime)s %(levelname)s:%(name)s:%(message)s' + ) + + +def set_log_debug(l: logging.Logger): + """set logger level to DEBUG, and debug-level output format""" + set_log_level_format( + l, + logging.DEBUG, + "%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - " + "%(name)s.%(funcName)s() ] %(message)s" + ) + + +def set_log_level_format(lgr: logging.Logger, level: int, fmt: str): + """Set logger level and format.""" + formatter = logging.Formatter(fmt=fmt) + lgr.handlers[0].setFormatter(formatter) + lgr.setLevel(level) + + +def main(): + args = parse_args(sys.argv[1:]) + + # set logging level + if args.verbose: + set_log_debug(logger) + else: + set_log_info(logger) + + PushoverAlarmNotifier( + args.PORT, batch_seconds=args.seconds + ).run() + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 2c09d1e..044220a 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,9 @@ description='Python library and daemon to interface with Honeywell/Ademco Vista Turbo alarm panels via RS232 serial.', long_description=long_description, install_requires=requires, + extras_require={ + 'examples': ['requests'], # dependencies for examples/ + }, keywords="honeywell ademco resideo vista alarm burglar fire serial rs232", classifiers=classifiers )