From c2899803ff34b5c67edb82f42f8b2752eed5827f Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 25 Feb 2017 05:14:40 -0500 Subject: [PATCH 1/4] Moved subscribed and subscribed_motion attributes to doorbell devices only --- ring_doorbell/__init__.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 98363464..62df6c16 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -285,22 +285,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.""" @@ -509,6 +493,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.""" From 3fbc00e22f2203081bbc6361b1383f9ff614f0bf Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sun, 5 Mar 2017 00:36:31 -0500 Subject: [PATCH 2/4] Allows option to make token persistent and register push_notify_urls --- ring_doorbell/__init__.py | 20 ++++++++++++---- ring_doorbell/const.py | 49 +++++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 62df6c16..869139dd 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -22,7 +22,8 @@ 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__) @@ -30,13 +31,16 @@ 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=""): """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 @@ -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: + 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 @@ -127,9 +139,9 @@ def query(self, else: if method == 'GET': response = req.json() - return response + break _LOGGER.error("%s", MSG_GENERIC_FAIL) - return None + return response @property def has_subscription(self): diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index 82317278..777d4e3b 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -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'} @@ -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' @@ -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 @@ -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'} From b8e06dce294550d3dbf5fca4074b3c5135d20450 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Thu, 9 Mar 2017 15:40:51 -0500 Subject: [PATCH 3/4] Introduced check_alerts() method (#17) * added initial control to alert notifications * Removed JSON from tests * Make lint happy * Update README * Only publish PUSH url if we have one * Display error message only if in debug * override default URL * Make check alerts a callable function * Removed extra space * Move inside the try/except for safeness * Introduced mecanism to save current alert state to a pickle file to allow concurrent objects to share alert notifications * Moved self._alert_cache position --- README.md | 2 +- ring_doorbell/__init__.py | 63 ++++++++++++++++++++++++++++++++++----- ring_doorbell/utils.py | 26 ++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 70316e1b..8647c18b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 869139dd..a23b6d26 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -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, @@ -32,7 +32,7 @@ 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=""): + push_token_notify_url="http://localhost/"): """Initialize the Ring object.""" self.features = None self.is_connected = None @@ -72,7 +72,7 @@ def _authenticate(self, attempts=RETRY_TOKEN): self.params = {'api_version': API_VERSION, 'auth_token': self.token} - if self._persist_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]'] = \ @@ -140,7 +140,9 @@ def query(self, if method == 'GET': response = req.json() break - _LOGGER.error("%s", MSG_GENERIC_FAIL) + + if self.debug: + _LOGGER.debug("%s", MSG_GENERIC_FAIL) return response @property @@ -199,6 +201,11 @@ 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) @@ -206,6 +213,28 @@ def __repr__(self): 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.""" @@ -332,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): diff --git a/ring_doorbell/utils.py b/ring_doorbell/utils.py index 18bf0495..eaa02ade 100644 --- a/ring_doorbell/utils.py +++ b/ring_doorbell/utils.py @@ -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.""" @@ -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 From 10bf75125e8481960fab8d45897919b1884b288b Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Thu, 9 Mar 2017 15:52:44 -0500 Subject: [PATCH 4/4] version bump 0.1.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2540eda4..877de375 100644 --- a/setup.py +++ b/setup.py @@ -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',