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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ In [12]: mydoorbell.
mydoorbell.account_id mydoorbell.kind
mydoorbell.address mydoorbell.last_recording_id
mydoorbell.battery_life mydoorbell.latitude
mydoorbell.check_activity mydoorbell.live_streaming_json
mydoorbell.check_alerts mydoorbell.live_streaming_json
mydoorbell.debug mydoorbell.longitude
mydoorbell.existing_doorbell_type mydoorbell.name
mydoorbell.existing_doorbell_type_duration mydoorbell.recording_download
Expand Down
111 changes: 86 additions & 25 deletions ring_doorbell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import requests
import pytz

from ring_doorbell.utils import _locator
from ring_doorbell.utils import _locator, _save_cache, _read_cache
from ring_doorbell.const import (
API_VERSION, API_URI, CHIMES_ENDPOINT, CHIME_VOL_MIN, CHIME_VOL_MAX,
DEVICES_ENDPOINT, DOORBELLS_ENDPOINT, DOORBELL_VOL_MIN, DOORBELL_VOL_MAX,
Expand All @@ -22,21 +22,25 @@
NEW_SESSION_ENDPOINT, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE,
MSG_GENERIC_FAIL, MSG_VOL_OUTBOUND,
NOT_FOUND, URL_DOORBELL_HISTORY, URL_RECORDING,
POST_DATA, RETRY_TOKEN, TESTSOUND_CHIME_ENDPOINT)
POST_DATA, PERSIST_TOKEN_ENDPOINT, PERSIST_TOKEN_DATA,
RETRY_TOKEN, TESTSOUND_CHIME_ENDPOINT)

_LOGGER = logging.getLogger(__name__)


class Ring(object):
"""A Python Abstraction object to Ring Door Bell."""

def __init__(self, username, password, debug=False):
def __init__(self, username, password, debug=False, persist_token=False,
push_token_notify_url="http://localhost/"):
"""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
self._push_token_notify_url = push_token_notify_url

self.debug = debug
self.username = username
Expand Down Expand Up @@ -67,6 +71,14 @@ def _authenticate(self, attempts=RETRY_TOKEN):
self.token = data.get('authentication_token')
self.params = {'api_version': API_VERSION,
'auth_token': self.token}

if self._persist_token and self._push_token_notify_url:
url = API_URI + PERSIST_TOKEN_ENDPOINT
PERSIST_TOKEN_DATA['auth_token'] = self.token
PERSIST_TOKEN_DATA['device[push_notification_token]'] = \
self._push_token_notify_url
req = self.session.put((url), headers=HEADERS,
data=PERSIST_TOKEN_DATA)
return True

self.is_connected = False
Expand Down Expand Up @@ -127,9 +139,11 @@ def query(self,
else:
if method == 'GET':
response = req.json()
return response
_LOGGER.error("%s", MSG_GENERIC_FAIL)
return None
break

if self.debug:
_LOGGER.debug("%s", MSG_GENERIC_FAIL)
return response

@property
def has_subscription(self):
Expand Down Expand Up @@ -187,13 +201,40 @@ def __init__(self):
self.family = None
self.name = None

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

def __repr__(self):
"""Return __repr__."""
return "<{0}: {1}>".format(self.__class__.__name__, self.name)

def update(self):
"""Refresh attributes."""
self._get_attrs()
self._update_alert()

def _update_alert(self):
"""Verify if alert received is still 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)

def _get_attrs(self):
"""Return chime attributes."""
Expand Down Expand Up @@ -285,22 +326,6 @@ def volume(self, value):
self.update()
return True

@property
def subscribed(self):
"""Return if chime is online."""
result = self._attrs.get('firmware_version')
if result is None:
return False
return True

@property
def subscribed_motions(self):
"""Return if chime is subscribed_motions."""
result = self._attrs.get('subscribed_motions')
if result is None:
return False
return True

@property
def linked_tree(self):
"""Return doorbell data linked to chime."""
Expand Down Expand Up @@ -336,11 +361,31 @@ def battery_life(self):
value = 100
return value

@property
def check_activity(self):
def check_alerts(self, cache=None):
"""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:
self._alert_cache = cache

url = API_URI + DINGS_ENDPOINT
return self._ring.query(url)
self.update()

try:
resp = self._ring.query(url)[0]
except IndexError:
return None

if resp:
timestamp = resp.get('now') + resp.get('expires_in')
self.alert = resp
self.alert_expires_at = datetime.fromtimestamp(timestamp)

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

@property
def existing_doorbell_type(self):
Expand Down Expand Up @@ -509,6 +554,22 @@ def recording_url(self, recording_id):
return req.url
return False

@property
def subscribed(self):
"""Return if is online."""
result = self._attrs.get('subscribed')
if result is None:
return False
return True

@property
def subscribed_motion(self):
"""Return if is subscribed_motion."""
result = self._attrs.get('subscribed_motions')
if result is None:
return False
return True

@property
def volume(self):
"""Return volume."""
Expand Down
49 changes: 31 additions & 18 deletions ring_doorbell/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# coding: utf-8
# vim:sw=4:ts=4:et:
"""Constants."""
from uuid import uuid4 as uuid

HEADERS = {'Content-Type': 'application/x-www-form-urlencoded; charset: UTF-8',
'User-Agent': 'Dalvik/1.6.0 (Linux; Android 4.4.4; Build/KTU84Q)',
'Accept-Encoding': 'gzip, deflate'}
Expand All @@ -18,6 +20,7 @@
DEVICES_ENDPOINT = '/clients_api/ring_devices'
DINGS_ENDPOINT = '/clients_api/dings/active'
DOORBELLS_ENDPOINT = '/clients_api/doorbots/{0}'
PERSIST_TOKEN_ENDPOINT = '/clients_api/device'

LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/linked_doorbots'
LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + '/vod'
Expand All @@ -26,23 +29,6 @@
URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history'
URL_RECORDING = '/clients_api/dings/{0}/recording'

# structure acquired from reverse engineering to create auth token
POST_DATA = {
'api_version': API_VERSION,
'device[os]': 'android',
'device[hardware_id]': '180940d0-aaaa-bbbb-8c64-6ea91491982c',
'device[app_brand]': 'ring',
'device[metadata][device_model]': 'KVM',
'device[metadata][resolution]': '600x800',
'device[metadata][app_version]': '1.7.29',
'device[metadata][app_instalation_date]': '',
'device[metadata][os_version]': '4.4.4',
'device[metadata][manufacturer]': 'innotek GmbH',
'device[metadata][is_tablet]': 'true',
'device[metadata][linphone_initialized]': 'true',
'device[metadata][language]': 'en'}


# default values
CHIME_VOL_MIN = 0
CHIME_VOL_MAX = 10
Expand All @@ -55,10 +41,37 @@
1: 'Digital',
2: 'Not Present'}


# error strings
MSG_BOOLEAN_REQUIRED = "Boolean value is required."
MSG_EXISTING_TYPE = "Integer value where {0}.".format(DOORBELL_EXISTING_TYPE)
MSG_GENERIC_FAIL = 'Sorry.. Something went wrong...'
FILE_EXISTS = 'The file {0} already exists.'
MSG_VOL_OUTBOUND = 'Must be within the {0}-{1}.'

# structure acquired from reverse engineering to create auth token
POST_DATA = {
'api_version': API_VERSION,
'device[hardware_id]': str(uuid()),
'device[os]': 'android',
'device[app_brand]': 'ring',
'device[metadata][device_model]': 'KVM',
'device[metadata][device_name]': 'Python',
'device[metadata][resolution]': '600x800',
'device[metadata][app_version]': '1.3.806',
'device[metadata][app_instalation_date]': '',
'device[metadata][manufacturer]': 'Qemu',
'device[metadata][device_type]': 'desktop',
'device[metadata][architecture]': 'desktop',
'device[metadata][language]': 'en'}

PERSIST_TOKEN_DATA = {
'api_version': API_VERSION,
'device[metadata][device_model]': 'KVM',
'device[metadata][device_name]': 'Python',
'device[metadata][resolution]': '600x800',
'device[metadata][app_version]': '1.3.806',
'device[metadata][app_instalation_date]': '',
'device[metadata][manufacturer]': 'Qemu',
'device[metadata][device_type]': 'desktop',
'device[metadata][architecture]': 'x86',
'device[metadata][language]': 'en'}
26 changes: 26 additions & 0 deletions ring_doorbell/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# coding: utf-8
# vim:sw=4:ts=4:et:
"""Python Ring Doorbell utils."""
import os
from ring_doorbell.const import NOT_FOUND

try:
import cPickle as pickle
except ImportError:
import pickle


def _locator(lst, key, value):
"""Return the position of a match item in list."""
Expand All @@ -11,3 +17,23 @@ def _locator(lst, key, value):
if d[key] == value)
except StopIteration:
return NOT_FOUND


def _save_cache(data, filename):
"""Dump data into a pickle file."""
try:
with open(filename, 'wb') as pickle_db:
pickle.dump(data, pickle_db)
return True
except:
raise


def _read_cache(filename):
"""Read data from a pickle file."""
try:
if os.path.isfile(filename):
return pickle.load(open(filename, 'rb'))
except:
raise
return None
2 changes: 1 addition & 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.0',
version='0.1.1',
description='A Python library to communicate with Ring' +
' Door Bell (https://ring.com/)',
author='Marcelo Moreira de Mello',
Expand Down