"""Command-line interface.""" import argparse import code import contextlib import getpass import importlib import json import logging import os import time import keyring from more_itertools import always_iterable import jaraco.abode from jaraco.context import suppress from jaraco.functools import pass_none from . import Client from .helpers import timeline as TIMELINE from .helpers import urls log = logging.getLogger(__name__) @suppress(ImportError) def enable_color(): import colorlog fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" colorfmt = f"%(log_color)s{fmt}%(reset)s" datefmt = '%Y-%m-%d %H:%M:%S' handler = logging.getLogger().handlers[0] handler.setFormatter( colorlog.ColoredFormatter( colorfmt, datefmt=datefmt, reset=True, log_colors=dict( DEBUG='cyan', INFO='green', WARNING='yellow', ERROR='red', CRITICAL='red', ), ) ) def setup_logging(log_level=logging.INFO): """Set up the logging.""" logging.basicConfig(level=log_level) # Suppress overly verbose logs from libraries that aren't helpful logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) logging.getLogger('aiohttp.access').setLevel(logging.WARNING) enable_color() logging.getLogger().setLevel(log_level) def build_parser(): """ Get parsed arguments. >>> parser = build_parser() """ parser = argparse.ArgumentParser("abode") parser.add_argument( '-u', '--username', help='Username', default=os.environ.get('ABODE_USERNAME') ) parser.add_argument('-p', '--password', help='Password') parser.add_argument('--mfa', help='Multifactor authentication code') parser.add_argument( '--mode', help='Output current alarm mode', default=False, action="store_true", ) parser.add_argument( '--arm', metavar='mode', help='Arm alarm to mode', ) parser.add_argument( '--set', metavar='setting=value', help='Set setting to a value', action='append', ) parser.add_argument( '--devices', help='Output all devices', default=False, action="store_true", ) parser.add_argument( '--device', metavar='device_id', help='Output one device for device_id', action='append', ) parser.add_argument( '--interact', default=False, action="store_true", help="interact with the client after running other operations", ) parser.add_argument( '--json', metavar='device_id', help='Output the json for device_id', action='append', ) parser.add_argument( '--on', metavar='device_id', help='Switch on a given device_id', action='append', ) parser.add_argument( '--off', metavar='device_id', help='Switch off a given device_id', action='append', ) parser.add_argument( '--lock', metavar='device_id', help='Lock a given device_id', action='append', ) parser.add_argument( '--unlock', metavar='device_id', help='Unlock a given device_id', action='append', ) parser.add_argument( '--automations', help='Output all automations', default=False, action="store_true", ) parser.add_argument( '--activate', metavar='automation_id', help='Activate (enable) an automation by automation_id', action='append', ) parser.add_argument( '--deactivate', metavar='automation_id', help='Deactivate (disable) an automation by automation_id', action='append', ) parser.add_argument( '--trigger', metavar='automation_id', help='Trigger (apply) a manual (quick) automation by automation_id', action='append', ) parser.add_argument( '--capture', metavar='device_id', help='Trigger a new image capture for the given device_id', action='append', ) parser.add_argument( '--stream', metavar='device_id=location/details.json', help='Start a new KVS stream for the given device_id and save the details to a file', action='append', ) parser.add_argument( '--image', metavar='device_id=location/image.jpg', help='Save an image from a camera (if available) to the given path', action='append', ) parser.add_argument( '--listen', help='Block and listen for device_id', default=False, action="store_true", ) parser.add_argument( '--debug', help='Enable debug logging', default=False, action="store_true", ) parser.add_argument( '--quiet', help='Output only warnings and errors', default=False, action="store_true", ) return parser def _create_client_instance(args): return Client( username=args.username, password=args.password, get_devices=args.mfa is None, ) @contextlib.contextmanager def _log_errors_and_logout(client): try: yield client except jaraco.abode.Exception as exc: log.error(exc) finally: client.logout() def _device_print(dev, append=''): log.info("%s%s", dev.desc, append) def _device_callback(dev): _device_print(dev, ", At: " + time.strftime("%Y-%m-%d %H:%M:%S")) def _timeline_callback(resp): event_code = int(resp['event_code']) if 5100 <= event_code <= 5199: # Ignore device changes return tmpl = '{event_name} - {event_type} at {date} {time}' log.info(tmpl.format_map(resp)) class Dispatcher: # pragma: no cover def __init__(self, client, args): self.client = client self.args = args def dispatch(self): self.login() self.output_current_mode() self.change_system_mode() self.set_setting() self.switch_on() self.switch_off() self.lock() self.unlock() self.output_json() self.print_all_automations() self.enable_automation() self.disable_automation() self.trigger_automation() self.trigger_image_capture() self.start_kvs_stream() self.save_camera_image() self.print_all_devices() self.print_specific_devices() self.start_device_change_listener() self.interact() def login(self): if not self.args.mfa: return self.client.login(mfa_code=self.args.mfa) # fetch devices from Abode self.client.get_devices() def interact(self): if not self.args.interact: return with contextlib.suppress(ImportError): # Allow Up/Down/History in the console importlib.import_module('readline') banner = 'Interact with the Abode `client`.' code.InteractiveConsole(dict(client=self.client)).interact(banner=banner) def output_current_mode(self): if not self.args.mode: return log.info("Current alarm mode: %s", self.client.get_alarm().mode) def change_system_mode(self): if not self.args.arm: return if self.client.get_alarm().set_mode(self.args.arm): log.info("Alarm mode changed to: %s", self.args.arm) else: log.warning("Failed to change alarm mode to: %s", self.args.arm) def set_setting(self): for setting in always_iterable(self.args.set): key, _, val = setting.partition("=") if self.client.set_setting(key, val): log.info("Setting %s changed to %s", key, val) def _get_device(self, id): device = self.client.get_device(id) if not device: log.warning("Could not find device with id: %s", id) return device def switch_on(self): for device_id in always_iterable(self.args.on): self._switch_on(self._get_device(device_id)) @staticmethod @pass_none def _switch_on(device): if device.switch_on(): log.info("Switched on device with id: %s", device.id) def switch_off(self): for device_id in always_iterable(self.args.off): self._switch_off(self._get_device(device_id)) @staticmethod @pass_none def _switch_off(device): if device.switch_off(): log.info("Switched off device with id: %s", device.id) def lock(self): for device_id in always_iterable(self.args.lock): self._lock(self._get_device(device_id)) @staticmethod @pass_none def _lock(device): if device.lock(): log.info("Locked device with id: %s", device.id) def unlock(self): for device_id in always_iterable(self.args.unlock): self._unlock(self._get_device(device_id)) @staticmethod @pass_none def _unlock(device): if device.unlock(): log.info("Unlocked device with id: %s", device.id) def output_json(self): for device_id in always_iterable(self.args.json): self._output_json(self._get_device(device_id)) @staticmethod @pass_none def _output_json(device): print( json.dumps( device._state, sort_keys=True, indent=4, separators=(',', ': '), ) ) def print_all_automations(self): if not self.args.automations: return for automation in self.client.get_automations(): _device_print(automation) def _get_automation(self, id): automation = self.client.get_automation(id) if not automation: log.warning("Could not find automation with id: %s", id) return automation def enable_automation(self): for automation_id in always_iterable(self.args.activate): self._enable_automation(self._get_automation(automation_id)) @staticmethod @pass_none def _enable_automation(automation): if automation.enable(True): log.info("Activated automation with id: %s", automation.id) def disable_automation(self): for automation_id in always_iterable(self.args.deactivate): self._disable_automation(self._get_automation(automation_id)) @staticmethod @pass_none def _disable_automation(automation): if automation.enable(False): log.info("Deactivated automation with id: %s", automation.id) def trigger_automation(self): for automation_id in always_iterable(self.args.trigger): self._trigger_automation(self._get_automation(automation_id)) @staticmethod @pass_none def _trigger_automation(automation): if automation.trigger(): log.info("Triggered automation with id: %s", automation.id) def trigger_image_capture(self): for device_id in always_iterable(self.args.capture): self._trigger_image_capture(self._get_device(device_id)) @staticmethod @pass_none def _trigger_image_capture(device): if device.capture(): log.info("Image requested from device with id: %s", device.id) else: log.warning("Failed to request image from device with id: %s", device.id) def start_kvs_stream(self): for keyval in always_iterable(self.args.stream): dev, _, loc = keyval.partition("=") self._start_kvs_stream(self._get_device(dev), loc) @staticmethod @pass_none def _start_kvs_stream(device, path): if not device.start_kvs_stream(path): log.warning("Failed to start KVS stream for device with id: %s", device.id) def save_camera_image(self): for keyval in always_iterable(self.args.image): dev, _, loc = keyval.partition("=") self._save_camera_image(self._get_device(dev), loc) @staticmethod @pass_none def _save_camera_image(device, path): try: if device.refresh_image() and device.image_to_file(path): log.info("Saved image to %s for device id: %s", path, device.id) except jaraco.abode.Exception as exc: log.warning("Unable to save image: %s", exc) def print_all_devices(self): if not self.args.devices: return for device in self.client.get_devices(): _device_print(device) def print_specific_devices(self): for device_id in always_iterable(self.args.device): device = self._get_device(device_id) device and self._print_specific_device(device) def _print_specific_device(self, device): _device_print(device) # Register the specific devices if we decide to listen. self.client.events.add_device_callback(device.id, _device_callback) def start_device_change_listener(self): if not self.args.listen: return # If no devices were specified then listen to all devices. if self.args.device is None: log.info("Adding all devices to listener...") for device in self.client.get_devices(): self.client.events.add_device_callback(device.id, _device_callback) self.client.events.add_timeline_callback(TIMELINE.ALL, _timeline_callback) log.info("Listening for device and timeline updates...") self.client.events.start() try: while True: time.sleep(1) except KeyboardInterrupt: self.client.events.stop() log.info("Device update listening stopped.") def _get_password(args): if not args.username: raise SystemExit("Username unknown. Pass a username or set ABODE_USERNAME.") args.password = args.password or _get_or_set_password(args.username) def _get_or_set_password(username): password = keyring.get_password(urls.BASE, username) if not password: password = getpass.getpass(f"Password for {username}: ") keyring.set_password(urls.BASE, username, password) return password def main(): """Execute command line helper.""" args = build_parser().parse_args() _get_password(args) setup_logging(log_level=logging.INFO + 10 * (args.quiet - args.debug)) with _log_errors_and_logout(_create_client_instance(args)) as client: Dispatcher(client, args).dispatch()