text: #!/usr/bin/python
  # -*- coding: utf-8 -*-
  """
  skybellpy by Wil Schrader - An Abode alarm Python library.
  
  https://github.com/MisterWil/skybellpy
  
  Influenced by blinkpy, because I'm a python noob:
  https://github.com/fronzbot/blinkpy/
  
  Published under the MIT license - See LICENSE file for more details.
  
  "Skybell" is a trademark owned by SkyBell Technologies, Inc, see
  www.skybell.com for more information. I am in no way affiliated with Skybell.
  """
  import os.path
  import json
  import logging
  import time
  import requests
  from requests.exceptions import RequestException
  
  from skybellpy.device import SkybellDevice
  from skybellpy.exceptions import (
      SkybellAuthenticationException, SkybellException)
  import skybellpy.helpers.constants as CONST
  import skybellpy.helpers.errors as ERROR
  import skybellpy.utils as UTILS
  
  _LOGGER = logging.getLogger(__name__)
  
  
  class Skybell():
      """Main Skybell class."""
  
      def __init__(self, username=None, password=None,
                   auto_login=False, get_devices=False,
                   cache_path=CONST.CACHE_PATH, disable_cache=False,
                   agent_identifier=CONST.DEFAULT_AGENT_IDENTIFIER,
                   login_sleep=True):
          """Init Abode object."""
          self._username = username
          self._password = password
          self._session = None
          self._cache_path = cache_path
          self._disable_cache = disable_cache
          self._devices = None
          self._session = requests.session()
          self._user_agent = '{} ({})'.format(CONST.USER_AGENT, agent_identifier)
          self._login_sleep = login_sleep
  
          # Create a new cache template
          self._cache = {
              CONST.APP_ID: UTILS.gen_id(),
              CONST.CLIENT_ID: UTILS.gen_id(),
              CONST.TOKEN: UTILS.gen_token(),
              CONST.ACCESS_TOKEN: None,
              CONST.DEVICES: {}
          }
  
          # Load and merge an existing cache
          if not disable_cache:
              self._load_cache()
  
          if (self._username is not None and
                  self._password is not None and
                  auto_login):
              self.login()
  
          if get_devices:
              self.get_devices()
  
      def login(self, username=None, password=None, sleep=False):
          """Execute Skybell login."""
          if username is not None:
              self._username = username
          if password is not None:
              self._password = password
  
          if self._username is None or not isinstance(self._username, str):
              raise SkybellAuthenticationException(ERROR.USERNAME)
  
          if self._password is None or not isinstance(self._password, str):
              raise SkybellAuthenticationException(ERROR.PASSWORD)
  
          self.update_cache(
              {
                  CONST.ACCESS_TOKEN: None
              })
  
          self._session = requests.session()
  
          login_data = {
              'username': self._username,
              'password': self._password,
              'appId': self.cache(CONST.APP_ID),
              CONST.TOKEN: self.cache(CONST.TOKEN)
          }
  
          try:
              response = self.send_request('post', CONST.LOGIN_URL,
                                           json_data=login_data, retry=False)
          except Exception as exc:
              raise SkybellAuthenticationException(ERROR.LOGIN_FAILED, exc)
  
          _LOGGER.debug("Login Response: %s", response.text)
  
          response_object = json.loads(response.text)
  
          self.update_cache({
              CONST.ACCESS_TOKEN: response_object[CONST.ACCESS_TOKEN]})
  
          if self._login_sleep:
              _LOGGER.info("Login successful, waiting 5 seconds...")
              time.sleep(5)
          else:
              _LOGGER.info("Login successful")
  
          return True
  
      def logout(self):
          """Explicit Skybell logout."""
          if self.cache(CONST.ACCESS_TOKEN):
              # No explicit logout call as it doesn't seem to matter
              # if a logout happens without registering the app which
              # we aren't currently doing.
              self._session = requests.session()
              self._devices = None
  
              self.update_cache({CONST.ACCESS_TOKEN: None})
  
          return True
  
      def get_devices(self, refresh=False):
          """Get all devices from Abode."""
          if refresh or self._devices is None:
              if self._devices is None:
                  self._devices = {}
  
              _LOGGER.info("Updating all devices...")
              response = self.send_request("get", CONST.DEVICES_URL)
              response_object = json.loads(response.text)
  
              _LOGGER.debug("Get Devices Response: %s", response.text)
  
              for device_json in response_object:
                  # Attempt to reuse an existing device
                  device = self._devices.get(device_json['id'])
  
                  # No existing device, create a new one
                  if device:
                      device.update(device_json)
                  else:
                      device = SkybellDevice(device_json, self)
                      self._devices[device.device_id] = device
  
          return list(self._devices.values())
  
      def get_device(self, device_id, refresh=False):
          """Get a single device."""
          if self._devices is None:
              self.get_devices()
              refresh = False
  
          device = self._devices.get(device_id)
  
          if device and refresh:
              device.refresh()
  
          return device
  
      def send_request(self, method, url, headers=None,
                       json_data=None, retry=True):
          """Send requests to Skybell."""
          if not self.cache(CONST.ACCESS_TOKEN) and url != CONST.LOGIN_URL:
              self.login()
  
          if not headers:
              headers = {}
  
          if self.cache(CONST.ACCESS_TOKEN):
              headers['Authorization'] = 'Bearer ' + \
                  self.cache(CONST.ACCESS_TOKEN)
  
          headers['user-agent'] = self._user_agent
          headers['content-type'] = 'application/json'
          headers['accepts'] = '*/*'
          headers['x-skybell-app-id'] = self.cache(CONST.APP_ID)
          headers['x-skybell-client-id'] = self.cache(CONST.CLIENT_ID)
  
          _LOGGER.debug("HTTP %s %s Request with headers: %s",
                        method, url, headers)
  
          try:
              response = getattr(self._session, method)(
                  url, headers=headers, json=json_data)
              _LOGGER.debug("%s %s", response, response.text)
  
              if response and response.status_code < 400:
                  return response
          except RequestException as exc:
              _LOGGER.warning("Skybell request exception: %s", exc)
  
          if retry:
              self.login()
  
              return self.send_request(method, url, headers, json_data, False)
  
          raise SkybellException(ERROR.REQUEST, "Retry failed")
  
      def cache(self, key):
          """Get a cached value."""
          return self._cache.get(key)
  
      def update_cache(self, data):
          """Update a cached value."""
          UTILS.update(self._cache, data)
          self._save_cache()
  
      def dev_cache(self, device, key=None):
          """Get a cached value for a device."""
          device_cache = self._cache.get(CONST.DEVICES, {}).get(device.device_id)
  
          if device_cache and key:
              return device_cache.get(key)
  
          return device_cache
  
      def update_dev_cache(self, device, data):
          """Update cached values for a device."""
          self.update_cache(
              {
                  CONST.DEVICES: {
                      device.device_id: data
                  }
              })
  
      def _load_cache(self):
          """Load existing cache and merge for updating if required."""
          if not self._disable_cache:
              if os.path.exists(self._cache_path):
                  _LOGGER.debug("Cache found at: %s", self._cache_path)
                  if os.path.getsize(self._cache_path) > 0:
                      loaded_cache = UTILS.load_cache(self._cache_path)
                      UTILS.update(self._cache, loaded_cache)
                  else:
                      _LOGGER.debug("Cache file is empty.  Removing it.")
                      os.remove(self._cache_path)
  
          self._save_cache()
  
      def _save_cache(self):
          """Trigger a cache save."""
          if not self._disable_cache:
              UTILS.save_cache(self._cache, self._cache_path)
  