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 98363464..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, @@ -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="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 @@ -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 @@ -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): @@ -187,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) @@ -194,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.""" @@ -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.""" @@ -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): @@ -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.""" 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'} 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 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',