Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 87 additions & 46 deletions ring_doorbell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
import pytz

from ring_doorbell.utils import (
_locator, _clean_cache, _save_cache, _read_cache)
_locator, _exists_cache, _save_cache, _read_cache)
from ring_doorbell.const import (
API_VERSION, API_URI, CHIMES_ENDPOINT, CHIME_VOL_MIN, CHIME_VOL_MAX,
API_VERSION, API_URI, CACHE_ATTRS, CACHE_FILE, CHIMES_ENDPOINT,
CHIME_VOL_MIN, CHIME_VOL_MAX,
DEVICES_ENDPOINT, DOORBELLS_ENDPOINT, DOORBELL_VOL_MIN, DOORBELL_VOL_MAX,
DOORBELL_EXISTING_TYPE, DINGS_ENDPOINT, FILE_EXISTS,
HEADERS, LINKED_CHIMES_ENDPOINT, LIVE_STREAMING_ENDPOINT,
Expand All @@ -33,11 +34,10 @@ class Ring(object):
"""A Python Abstraction object to Ring Door Bell."""

def __init__(self, username, password, debug=False, persist_token=False,
push_token_notify_url="http://localhost/"):
push_token_notify_url="http://localhost/", reuse_session=True,
cache_file=CACHE_FILE):
"""Initialize the Ring object."""
self.features = None
self.is_connected = None
self._id = None
self.token = None
self.params = None
self._persist_token = persist_token
Expand All @@ -49,27 +49,76 @@ def __init__(self, username, password, debug=False, persist_token=False,
self.session = requests.Session()
self.session.auth = (self.username, self.password)

self._authenticate()
self.cache = CACHE_ATTRS
self.cache['account'] = self.username
self.cache_file = cache_file
self._reuse_session = reuse_session

def _authenticate(self, attempts=RETRY_TOKEN):
# tries to re-use old session
if self._reuse_session:
self.cache['token'] = self.token
self._process_cached_session()
else:
self._authenticate()

def _process_cached_session(self):
"""Process cache_file to reuse token instead."""
if _exists_cache(self.cache_file):
self.cache = _read_cache(self.cache_file)

# if self.cache['token'] is None, the cache file was corrupted.
# of if self.cache['account'] does not match with self.username
# In both cases, a new auth token is required.
if (self.cache['token'] is None) or \
(self.cache['account'] is None) or \
(self.cache['account'] != self.username):
self._authenticate()
else:
# we need to set the self.token and self.params
# to make use of the self.query() method
self.token = self.cache['token']
self.params = {'api_version': API_VERSION,
'auth_token': self.token}

# test if token from cache_file is still valid and functional
# if not, it should continue to get a new auth token
url = API_URI + DEVICES_ENDPOINT
req = self.query(url, raw=True)
if req.status_code == 200:
self._authenticate(session=req)
else:
self._authenticate()
else:
# first time executing, so we have to create a cache file
self._authenticate()

def _authenticate(self, attempts=RETRY_TOKEN, session=None):
"""Authenticate user against Ring API."""
url = API_URI + NEW_SESSION_ENDPOINT

loop = 0
while loop <= attempts:
loop += 1
try:
req = self.session.post((url), data=POST_DATA, headers=HEADERS)
if session is None:
req = self.session.post((url),
data=POST_DATA,
headers=HEADERS)
else:
req = session
except:
raise

# if token is expired, refresh credentials and try again
if req.status_code == 201:
data = req.json().get('profile')
self.features = data.get('features')
self._id = data.get('id')
if req.status_code == 200 or req.status_code == 201:

# the only way to get a JSON with token is via POST,
# so we need a special conditional for 201 code
if req.status_code == 201:
data = req.json().get('profile')
self.token = data.get('authentication_token')

self.is_connected = True
self.token = data.get('authentication_token')
self.params = {'api_version': API_VERSION,
'auth_token': self.token}

Expand All @@ -80,6 +129,13 @@ def _authenticate(self, attempts=RETRY_TOKEN):
self._push_token_notify_url
req = self.session.put((url), headers=HEADERS,
data=PERSIST_TOKEN_DATA)

# update token if reuse_session is True
if self._reuse_session:
self.cache['account'] = self.username
self.cache['token'] = self.token

_save_cache(self.cache, self.cache_file)
return True

self.is_connected = False
Expand Down Expand Up @@ -146,14 +202,6 @@ def query(self,
_LOGGER.debug("%s", MSG_GENERIC_FAIL)
return response

@property
def has_subscription(self):
"""Return if account has subscription."""
try:
return self.features.get('subscriptions_enabled')
except AttributeError:
return NOT_FOUND

@property
def devices(self):
"""Return all devices."""
Expand Down Expand Up @@ -203,13 +251,12 @@ class RingGeneric(object):
def __init__(self):
"""Initialize Ring Generic."""
self._attrs = None
self._ring = None
self.debug = None
self.family = None
self.name = None

# alerts notifications
self._alert_cache = None
self.alert = None
self.alert_expires_at = None

def __repr__(self):
Expand All @@ -221,26 +268,26 @@ def update(self):
self._get_attrs()
self._update_alert()

@property
def alert(self):
"""Return alert attribute."""
return self._ring.cache['alerts']

@alert.setter
def alert(self, value):
"""Set attribute to alert."""
self._ring.cache['alerts'] = value
_save_cache(self._ring.cache, self._ring.cache_file)
return True

def _update_alert(self):
"""Verify if alert received is still valid."""
# alert is no longer valid
if self.alert and self.alert_expires_at:
if datetime.now() >= self.alert_expires_at:
self.alert = None
self.alert_expires_at = None
elif self._alert_cache:
aux = _read_cache(self._alert_cache)
if ((isinstance(aux, dict)) and
('now' in aux) and
('expires_in' in aux)):
aux_expires_at = datetime.fromtimestamp(
aux.get('now') + aux.get('expires_in'))

# verify if pickle object is still valid
if datetime.now() <= aux_expires_at:
self.alert = aux
self.alert_expires_at = aux_expires_at
else:
_save_cache(None, self._alert_cache)
_save_cache(self._ring.cache, self._ring.cache_file)

def _get_attrs(self):
"""Return attributes."""
Expand Down Expand Up @@ -371,14 +418,8 @@ def battery_life(self):
value = 100
return value

def check_alerts(self, cache=None):
def check_alerts(self):
"""Return JSON when motion or ring is detected."""
# save alerts attributes to an external pickle file
# when multiple resources are checking for alerts
if cache:
_clean_cache(cache)
self._alert_cache = cache

url = API_URI + DINGS_ENDPOINT
self.update()

Expand All @@ -393,8 +434,8 @@ def check_alerts(self, cache=None):
self.alert_expires_at = datetime.fromtimestamp(timestamp)

# save to a pickle data
if self._alert_cache:
_save_cache(self.alert, self._alert_cache)
if self.alert:
_save_cache(self._ring.cache, self._ring.cache_file)
return True
return None

Expand Down
11 changes: 11 additions & 0 deletions ring_doorbell/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# coding: utf-8
# vim:sw=4:ts=4:et:
"""Constants."""
import os
from uuid import uuid4 as uuid

HEADERS = {'Content-Type': 'application/x-www-form-urlencoded; charset: UTF-8',
Expand All @@ -10,6 +11,16 @@
# number of attempts to refresh token
RETRY_TOKEN = 3

# default suffix for session cache file
CACHE_ATTRS = {'account': None, 'alerts': None, 'token': None}

try:
CACHE_FILE = os.path.join(os.getenv("HOME"),
'.ring_doorbell-session.cache')
except (AttributeError, TypeError):
CACHE_FILE = os.path.join('.', '.ring_doorbell-session.cache')


# code when item was not found
NOT_FOUND = -1

Expand Down
29 changes: 23 additions & 6 deletions ring_doorbell/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# vim:sw=4:ts=4:et:
"""Python Ring Doorbell utils."""
import os
from ring_doorbell.const import NOT_FOUND
from ring_doorbell.const import CACHE_ATTRS, NOT_FOUND

try:
import cPickle as pickle
Expand All @@ -23,10 +23,19 @@ def _clean_cache(filename):
"""Remove filename if pickle version mismatch."""
try:
if os.path.isfile(filename):
_read_cache(filename)
except ValueError:
os.remove(filename)
return True
os.remove(filename)
except:
raise

# initialize cache since file was removed
initial_cache_data = CACHE_ATTRS
_save_cache(initial_cache_data, filename)
return initial_cache_data


def _exists_cache(filename):
"""Check if filename exists and if is pickle object."""
return bool(os.path.isfile(filename))


def _save_cache(data, filename):
Expand All @@ -43,6 +52,14 @@ def _read_cache(filename):
"""Read data from a pickle file."""
try:
if os.path.isfile(filename):
return pickle.load(open(filename, 'rb'))
data = pickle.load(open(filename, 'rb'))

# make sure pickle obj has the expected defined keys
# if not reinitialize cache
if data.keys() != CACHE_ATTRS.keys():
raise EOFError
return data
except EOFError:
return _clean_cache(filename)
except:
raise
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
setup(
name='ring_doorbell',
packages=['ring_doorbell'],
version='0.1.2',
version='0.1.3',
description='A Python library to communicate with Ring' +
' Door Bell (https://ring.com/)',
author='Marcelo Moreira de Mello',
Expand All @@ -29,6 +29,7 @@
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Home Automation',
'Topic :: Software Development :: Libraries :: Python Modules'
],
Expand Down
22 changes: 12 additions & 10 deletions tests/test_ring.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

USERNAME = 'foo'
PASSWORD = 'bar'
ALERT_CACHE_DB = 'tests/cache.db'
CACHE = 'tests/cache.db'


def mocked_requests_get(*args, **kwargs):
Expand Down Expand Up @@ -245,9 +245,9 @@ def test_basic_attributes(self, get_mock, post_mock):
"""Test the Ring class and methods."""
from ring_doorbell import Ring

myring = Ring(USERNAME, PASSWORD)
myring = Ring(USERNAME, PASSWORD, cache_file=CACHE)
self.assertTrue(myring.is_connected)
self.assertIsInstance(myring.features, dict)
self.assertIsInstance(myring.cache, dict)
self.assertFalse(myring.debug)
self.assertEqual(1, len(myring.chimes))
self.assertEqual(2, len(myring.doorbells))
Expand All @@ -264,7 +264,7 @@ def test_chime_attributes(self, get_mock, post_mock):
"""Test the Ring Chime class and methods."""
from ring_doorbell import Ring

myring = Ring(USERNAME, PASSWORD)
myring = Ring(USERNAME, PASSWORD, cache_file=CACHE)
dev = myring.chimes[0]

self.assertEqual('123 Main St', dev.address)
Expand All @@ -285,7 +285,7 @@ def test_doorbell_attributes(self, get_mock, post_mock):
"""Test the Ring DoorBell class and methods."""
from ring_doorbell import Ring

myring = Ring(USERNAME, PASSWORD, persist_token=True)
myring = Ring(USERNAME, PASSWORD, cache_file=CACHE, persist_token=True)
for dev in myring.doorbells:
if not dev.shared:
self.assertEqual('Front Door', dev.name)
Expand All @@ -309,7 +309,7 @@ def test_shared_doorbell_attributes(self, get_mock, post_mock):
"""Test the Ring Shared DoorBell class and methods."""
from ring_doorbell import Ring

myring = Ring(USERNAME, PASSWORD, persist_token=True)
myring = Ring(USERNAME, PASSWORD, cache_file=CACHE, persist_token=True)
for dev in myring.doorbells:
if dev.shared:
self.assertEqual(987653, dev.account_id)
Expand All @@ -321,6 +321,8 @@ def test_shared_doorbell_attributes(self, get_mock, post_mock):
self.assertEqual(5, dev.volume)
self.assertEqual('Digital', dev.existing_doorbell_type)

os.remove(CACHE)


class TestRingDoorBellAlerts(unittest.TestCase):
"""Test the Ring DoorBell alerts."""
Expand All @@ -331,16 +333,16 @@ def test_doorbell_alerts(self, get_mock, post_mock):
"""Test the Ring DoorBell alerts."""
from ring_doorbell import Ring

myring = Ring(USERNAME, PASSWORD, persist_token=True)
myring = Ring(USERNAME, PASSWORD, cache_file=CACHE, persist_token=True)
for dev in myring.doorbells:
self.assertEqual('America/New_York', dev.timezone)

# call alerts
dev.check_alerts(cache=ALERT_CACHE_DB)
dev.check_alerts()

self.assertIsInstance(dev.alert, dict)
self.assertIsInstance(dev.alert_expires_at, datetime)
self.assertTrue(datetime.now() <= dev.alert_expires_at)
self.assertIsNotNone(dev._alert_cache)
self.assertIsNotNone(dev._ring.cache_file)

os.remove(ALERT_CACHE_DB)
os.remove(CACHE)
Loading