From e00aa1e8c89a5b4a26fa0cde181f6a61a3652b03 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 04:17:50 -0500 Subject: [PATCH 1/8] Refactored project to make it more Pythonish and transparent --- ring_doorbell/__init__.py | 518 ++++++++++++++++++++++++-------------- ring_doorbell/const.py | 31 ++- 2 files changed, 357 insertions(+), 192 deletions(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 16722f74..471ccc93 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -13,13 +13,16 @@ import requests import pytz +from ring_doorbell.utils import _locator from ring_doorbell.const import ( - API_VERSION, API_URI, DEVICES_ENDPOINT, DINGS_ENDPOINT, - FILE_EXISTS, GENERIC_FAIL, HEADERS, LINKED_CHIMES_ENDPOINT, - LIVE_STREAMING_ENDPOINT, NEW_SESSION_ENDPOINT, NOT_FOUND, - URL_HISTORY, URL_DOORBELL_HISTORY, URL_RECORDING, + API_VERSION, API_URI, 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, + NEW_SESSION_ENDPOINT, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE, + MSG_GENERIC_FAIL, MSG_VOLUME_OUTBOUNDS, + NOT_FOUND, URL_DOORBELL_HISTORY, URL_RECORDING, POST_DATA, RETRY_TOKEN) -from ring_doorbell.utils import _locator _LOGGER = logging.getLogger(__name__) @@ -33,7 +36,7 @@ def __init__(self, username, password, debug=False): self.is_connected = None self._id = None self.token = None - self._params = None + self.params = None self.debug = debug self.username = username @@ -62,15 +65,19 @@ def _authenticate(self, attempts=RETRY_TOKEN): self._id = data.get('id') self.is_connected = True self.token = data.get('authentication_token') - self._params = {'api_version': API_VERSION, - 'auth_token': self.token} + self.params = {'api_version': API_VERSION, + 'auth_token': self.token} return True self.is_connected = False req.raise_for_status() - def _query(self, url, attempts=RETRY_TOKEN, - raw=False, extra_params=None): + def query(self, + url, + attempts=RETRY_TOKEN, + method='GET', + raw=False, + extra_params=None): """Query data from Ring API.""" if self.debug: _LOGGER.debug("Querying %s", url) @@ -82,18 +89,28 @@ def _query(self, url, attempts=RETRY_TOKEN, response = None loop = 0 while loop <= attempts: + if self.debug: + _LOGGER.debug("running query loop %s", loop) # allow to override params when necessary - # and update self._params globally for the next connection + # and update self.params globally for the next connection if extra_params: - params = self._params + params = self.params params.update(extra_params) else: - params = self._params + params = self.params loop += 1 try: - req = self.session.get((url), params=urlencode(params)) + if method == 'GET': + req = self.session.get((url), params=urlencode(params)) + elif method == 'PUT': + req = self.session.put((url), params=urlencode(params)) + elif method == 'POST': + req = self.session.post((url), params=urlencode(params)) + + if self.debug: + _LOGGER.debug("_query %s ret %s", loop, req.status_code) except: raise @@ -103,18 +120,16 @@ def _query(self, url, attempts=RETRY_TOKEN, self._authenticate() continue - if req.status_code == 200: + if req.status_code == 200 or req.status_code == 204: # if raw, return session object otherwise return JSON if raw: response = req else: - response = req.json() - - break - - if response is None and self.debug: - _LOGGER.debug(GENERIC_FAIL) - return response + if method == 'GET': + response = req.json() + return response + _LOGGER.debug("%s", MSG_GENERIC_FAIL) + return None @property def has_subscription(self): @@ -132,192 +147,290 @@ def devices(self): devs['doorbells'] = self.doorbells return devs - @property - def __devices(self): + def __devices(self, device_type): """Private method to query devices.""" + lst = [] url = API_URI + DEVICES_ENDPOINT - return self._query(url) + try: + if device_type == 'chime': + req = self.query(url).get('chimes') + for member in list((obj['description'] for obj in req)): + lst.append(RingChime(self, member)) + + if device_type == 'doorbell': + req = self.query(url).get('doorbots') + for member in list((obj['description'] for obj in req)): + lst.append(RingDoorBell(self, member)) + + except AttributeError: + pass + return lst @property def chimes(self): - """Return list of chimes by name.""" - try: - req = self.__devices.get('chimes') - return list((obj['description'] for obj in req)) - except AttributeError: - return NOT_FOUND + """Return a list of RingDoorChime objects.""" + return self.__devices('chime') - def chime_id(self, name): - """Return chime ID.""" - try: - return self.chime_attributes(name).get('id') - except AttributeError: - return NOT_FOUND + @property + def doorbells(self): + """Return a list of RingDoorBell objects.""" + return self.__devices('doorbell') + + +class RingGeneric(object): + """Generic Implementation for Ring Chime/Doorbell.""" + + def __init__(self): + """Initialize Ring Generic.""" + self._attrs = None + self.debug = None + self.family = None + self.name = None + + def __repr__(self): + """Return __repr__.""" + return "<{0}: {1}>".format(self.__class__.__name__, self.name) - def chime_attributes(self, name): + def update(self): + """Refresh attributes.""" + self._get_attrs() + + def _get_attrs(self): """Return chime attributes.""" + url = API_URI + DEVICES_ENDPOINT try: - lst = self.__devices.get('chimes') - index = _locator(lst, 'description', name) + lst = self._ring.query(url).get(self.family) + index = _locator(lst, 'description', self.name) if index == NOT_FOUND: - return NOT_FOUND - return lst[index] + return None except AttributeError: - return NOT_FOUND + return None - def chime_get_volume(self, name): - """Return if chime volume.""" - try: - return self.chime_attributes(name).get('settings').get('volume') - except AttributeError: - return NOT_FOUND + self._attrs = lst[index] + return True + + @property + def account_id(self): + """Return account ID.""" + return self._attrs.get('id') - def chime_firmware(self, name): + @property + def address(self): + """Return address.""" + return self._attrs.get('address') + + @property + def firmware(self): """Return firmware.""" - try: - return self.chime_attributes(name).get('firmware_version') - except AttributeError: - return NOT_FOUND + return self._attrs.get('firmware_version') - def is_chime_online(self, name): - """Return if chime is online.""" - try: - result = self.chime_attributes(name).get('subscribed') - if result is None: - return False - except AttributeError: - return NOT_FOUND - return True + # pylint: disable=invalid-name + @property + def id(self): + """Return ID.""" + return self._attrs.get('device_id') - def is_chime_subscribed_motions(self, name): - """Return if chime is subscribed_motions.""" - try: - result = self.chime_attributes(name).get('subscribed_motions') - if result is None: - return False - except AttributeError: - return NOT_FOUND - return True + @property + def latitude(self): + """Return latitude attr.""" + return self._attrs.get('latitude') - def chime_tree(self, name): - """Return doorbell data linked to chime.""" - chime_id = self.chime_id(name) - url = API_URI + LINKED_CHIMES_ENDPOINT.format(chime_id) - return self._query(url) + @property + def longitude(self): + """Return longitude attr.""" + return self._attrs.get('longitude') @property - def doorbells(self): - """Return list of doorbells by name.""" - try: - req = self.__devices.get('doorbots') - return list((obj['description'] for obj in req)) - except AttributeError: - return NOT_FOUND + def kind(self): + """Return kind attr.""" + return self._attrs.get('kind') - def doorbell_attributes(self, name): - """Return doorbell attributes.""" - try: - lst = self.__devices.get('doorbots') - index = _locator(lst, 'description', name) - if index == NOT_FOUND: - return NOT_FOUND - return lst[index] - except AttributeError: - return NOT_FOUND + @property + def timezone(self): + """Return timezone.""" + return self._attrs.get('time_zone') - def doorbell_id(self, name): - """Return doorbell ID.""" - try: - return self.doorbell_attributes(name).get('id') - except AttributeError: - return NOT_FOUND - def doorbell_get_volume(self, name): - """Return volume.""" - try: - return self.doorbell_attributes(name). \ - get('settings').get('doorbell_volume') - except AttributeError: - return NOT_FOUND +class RingChime(RingGeneric): + """Implementation for Ring Chime.""" - def is_doorbell_online(self, name): - """Return state for doorbell is online.""" - try: - result = self.doorbell_attributes(name).get('subscribed') - if result is None: - return False - except AttributeError: - return NOT_FOUND + def __init__(self, ring, name): + """Initilize Ring chime object.""" + super(RingChime, self).__init__() + self._attrs = None + self._ring = ring + self.debug = self._ring.debug + self.family = 'chimes' + self.name = name + self.update() + + @property + def volume(self): + """Return if chime volume.""" + return self._attrs.get('settings').get('volume') + + @volume.setter + def volume(self, value): + if not ((isinstance(value, int)) and + (value >= CHIME_VOL_MIN and value <= CHIME_VOL_MAX)): + _LOGGER.error("%s", MSG_VOLUME_OUTBOUNDS.format(CHIME_VOL_MIN, + CHIME_VOL_MAX)) + return False + + params = { + 'chime[description]': self.name, + 'chime[settings][volume]': str(value)} + + url = API_URI + CHIMES_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() return True - def is_doorbell_subscribed_motions(self, name): - """Return if doorbell is subscribed.""" - try: - result = self.doorbell_attributes(name).get('subscribed_motions') - if result is None: - return False - except AttributeError: - return NOT_FOUND + @property + def subscribed(self): + """Return if chime is online.""" + result = self._attrs.get('firmware_version') + if result is None: + return False return True - def doorbell_battery_life(self, name): - """Return doorbell battery life.""" - try: - value = int(self.doorbell_attributes(name).get('battery_life')) - if value > 100: - value = 100 - return value - except AttributeError: - return NOT_FOUND + @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 - def doorbell_firmware(self, name): - """Return doorbell firmware.""" - try: - return self.doorbell_attributes(name).get('firmware_version') - except AttributeError: - return NOT_FOUND + @property + def linked_tree(self): + """Return doorbell data linked to chime.""" + url = API_URI + LINKED_CHIMES_ENDPOINT.format(self.account_id) + return self._ring.query(url) - def doorbell_timezone(self, name): - """Return doorbell timezone.""" - try: - return self.doorbell_attributes(name).get('time_zone') - except AttributeError: - return NOT_FOUND - def __live_streaming_create_session(self, name): - """Initiate session live streaming URL.""" - door_id = self.doorbell_id(name) - url = API_URI + LIVE_STREAMING_ENDPOINT.format(door_id) - req = self.session.post((url), params=urlencode(self._params)) - if req.status_code == 204: - return True - return False +class RingDoorBell(RingGeneric): + """Implementation for Ring Doorbell.""" - def live_streaming(self, name): - """Return JSON for live streaming.""" - ret = self.__live_streaming_create_session(name) - if ret: - url = API_URI + DINGS_ENDPOINT - return self._query(url) - return False + def __init__(self, ring, name): + """Initilize Ring doorbell object.""" + super(RingDoorBell, self).__init__() + self._attrs = None + self._ring = ring + self.debug = self._ring.debug + self.family = 'doorbots' + self.name = name + self.update() + + @property + def battery_life(self): + """Return battery life.""" + value = int(self._attrs.get('battery_life')) + if value > 100: + value = 100 + return value @property def check_activity(self): """Return JSON when motion or ring is detected.""" url = API_URI + DINGS_ENDPOINT - return self._query(url) + return self._ring.query(url) + + @property + def existing_doorbell_type(self): + """ + Return existing doorbell type. + + 0: Mechanical + 1: Digital + 2: Not Present + """ + return DOORBELL_EXISTING_TYPE[ + self._attrs.get('settings').get('chime_settings').get('type')] + + @existing_doorbell_type.setter + def existing_doorbell_type(self, value): + """ + Return existing doorbell type. + + 0: Mechanical + 1: Digital + 2: Not Present + """ + if value not in DOORBELL_EXISTING_TYPE.keys(): + _LOGGER.error("%s", MSG_EXISTING_TYPE) + return False + params = { + 'doorbot[description]': self.name, + 'doorbot[settings][chime_settings][type]': value} - def history(self, name=None, limit=30, timezone=None): + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True + + @property + def existing_doorbell_type_enabled(self): + """Return if existing doorbell type is enabled.""" + if self._attrs.get('settings').get('chime_settings').get('type') == 2: + return False + return self._attrs.get('settings').get('chime_settings').get('enable') + + @existing_doorbell_type_enabled.setter + def existing_doorbell_type_enabled(self, value): + """Enable/disable the existing doorbell if Digital/Mechanical.""" + if self._attrs.get('settings').\ + get('chime_settings').get('type') == 2: + return False + if not isinstance(value, bool): + _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED) + return False + + params = { + 'doorbot[description]': self.name, + 'doorbot[settings][chime_settings][enable]': value} + + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True + + @property + def existing_doorbell_type_duration(self): + """Return duration for Digital chime.""" + if self._attrs.get('settings').get('chime_settings').get('type') == 1: + return self._attrs.get('settings').\ + get('chime_settings').get('duration') + return None + + @existing_doorbell_type_duration.setter + def existing_doorbell_type_duration(self, value): + """Set duration for Digital chime.""" + if not ((isinstance(value, int)) and + (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)): + _LOGGER.error("%s", MSG_VOLUME_OUTBOUNDS.format(DOORBELL_VOL_MIN, + DOORBELL_VOL_MAX)) + return False + + if self._attrs.get('settings').get('chime_settings').get('type') != 1: + return None + + params = { + 'doorbot[description]': self.name, + 'doorbot[settings][chime_settings][duration]': value} + + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True + + def history(self, limit=30, timezone=None): """Return history with datetime objects.""" # allow modify the items to return params = {'limit': str(limit)} - if name: - url = API_URI + URL_DOORBELL_HISTORY.format(self.doorbell_id(name)) - else: - url = API_URI + URL_HISTORY - - response = self._query(url, extra_params=params) + url = API_URI + URL_DOORBELL_HISTORY.format(self.account_id) + response = self._ring.query(url, extra_params=params) # convert for specific timezone utc = pytz.utc @@ -336,35 +449,68 @@ def history(self, name=None, limit=30, timezone=None): entry['created_at'] = utc_dt return response - def doorbell_recording(self, recording_id): - """Return recording in MP4 format.""" - url = API_URI + URL_RECORDING.format(recording_id) - req = self._query(url, raw=True) - if req.status_code == 200: - return req.content + @property + def last_recording_id(self): + """Return the last recording ID.""" + return self.history(limit=1)[0]['id'] + + @property + def live_streaming_json(self): + """Return JSON for live streaming.""" + url = API_URI + LIVE_STREAMING_ENDPOINT.format(self.account_id) + req = self._ring.query((url), method='POST', raw=True) + if req.status_code == 204: + url = API_URI + DINGS_ENDPOINT + return self._ring.query(url)[0] return None - def doorbell_download_recording(self, recording_id, - filename, override=False): - """Download and save recording in MP4 format to a file.""" + def recording_download(self, recording_id, filename=None, override=False): + """Save a recording in MP4 format to a file or return raw.""" + url = API_URI + URL_RECORDING.format(recording_id) try: - if os.path.isfile(filename) and not override: - _LOGGER.error("%s", FILE_EXISTS.format(filename)) - return False + req = self._ring.query(url, raw=True) + if req.status_code == 200: - with open(filename, 'wb') as recording: - mp4 = self.doorbell_recording(recording_id) - recording.write(mp4) + if filename: + if os.path.isfile(filename) and not override: + _LOGGER.error("%s", FILE_EXISTS.format(filename)) + return False + with open(filename, 'wb') as recording: + recording.write(req.content) + return True + else: + return req.content except IOError as error: _LOGGER.error("%s", error) - return False - return True + raise - def doorbell_recording_url(self, recording_id): + def recording_url(self, recording_id): """Return HTTPS recording URL.""" url = API_URI + URL_RECORDING.format(recording_id) - req = self._query(url, raw=True) + req = self._ring.query(url, raw=True) if req.status_code == 200: return req.url return False + + @property + def volume(self): + """Return volume.""" + return self._attrs.get('settings').get('doorbell_volume') + + @volume.setter + def volume(self, value): + if not ((isinstance(value, int)) and + (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)): + _LOGGER.error("%s", MSG_VOLUME_OUTBOUNDS.format(DOORBELL_VOL_MIN, + DOORBELL_VOL_MAX)) + return False + + params = { + 'doorbot[description]': self.name, + 'doorbot[settings][doorbell_volume]': str(value)} + + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index 658c8cd7..550b6e93 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -14,13 +14,15 @@ # API endpoints API_VERSION = '9' API_URI = 'https://api.ring.com' +CHIMES_ENDPOINT = '/clients_api/chimes/{0}' DEVICES_ENDPOINT = '/clients_api/ring_devices' DINGS_ENDPOINT = '/clients_api/dings/active' -LINKED_CHIMES_ENDPOINT = '/clients_api/chimes/{0}/linked_doorbots' -LIVE_STREAMING_ENDPOINT = '/clients_api/doorbots/{0}/vod' +DOORBELLS_ENDPOINT = '/clients_api/doorbots/{0}' + +LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/linked_doorbots' +LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + '/vod' NEW_SESSION_ENDPOINT = '/clients_api/session' -URL_HISTORY = '/clients_api/doorbots/history' -URL_DOORBELL_HISTORY = '/clients_api/doorbots/{0}/history' +URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history' URL_RECORDING = '/clients_api/dings/{0}/recording' # structure acquired from reverse engineering to create auth token @@ -39,6 +41,23 @@ 'device[metadata][linphone_initialized]': 'true', 'device[metadata][language]': 'en'} + +# default values +CHIME_VOL_MIN = 0 +CHIME_VOL_MAX = 10 + +DOORBELL_VOL_MIN = 0 +DOORBELL_VOL_MAX = 11 + +DOORBELL_EXISTING_TYPE = { + 0: 'Mechanical', + 1: 'Digital', + 2: 'Not Present'} + + # error strings -GENERIC_FAIL = 'Sorry.. Something went wrong...' -FILE_EXISTS = 'The file {0} already exists' +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_VOLUME_OUTBOUNDS = 'Must be within the {0}-{1}.' From 831a488243a24e862b5f5fec7bdbbd78565e96f2 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 04:43:12 -0500 Subject: [PATCH 2/8] Bumped version to 0.1.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9d0bc02..2540eda4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='ring_doorbell', packages=['ring_doorbell'], - version='0.0.4', + version='0.1.0', description='A Python library to communicate with Ring' + ' Door Bell (https://ring.com/)', author='Marcelo Moreira de Mello', From 740e49690c4675740bb0c0aab949f5325988d21b Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 04:46:38 -0500 Subject: [PATCH 3/8] Updated Readme Update Readmine due to refactor --- README.md | 227 +++++++++++++++--------------------------------------- 1 file changed, 64 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 661458a9..abffd3b4 100644 --- a/README.md +++ b/README.md @@ -12,169 +12,70 @@ $ pip3 install git+https://github.com/tchellomello/python-ring-doorbell@dev --up ## Usage ```python -from ring_doorbell import Ring -myring = Ring('user@email.com', 'secret') - -myring.devices -{'chimes': ['Downstairs'], 'doorbells': ['Front Door']} - -myring.chimes -['Downstairs'] - -myring.doorbells -['Front Door'] - -myring.chime_attributes('Downstairs') -{'address': '123 Example St, New York, NY, USA', - 'alerts': {'connection': 'online'}, - 'description': 'Downstairs', - 'device_id': '1234567', - 'do_not_disturb': {'seconds_left': 0}, - 'features': {'ringtones_enabled': True}, - 'firmware_version': '1.1.31', - 'id': 123456, - 'kind': 'chime', - 'latitude': 00.0000000, - 'longitude': -00.000000, - 'owned': True, - 'owner': {'email': 'owner@email.com', - 'first_name': 'Owner Name', - 'id': 123456, - 'last_name': 'Owner Surname'}, - 'settings': {'ding_audio_id': None, - 'ding_audio_user_id': None, - 'motion_audio_id': None, - 'motion_audio_user_id': None, - 'volume': 10}, - 'time_zone': 'America/New_York'} - -# for battery powered it will show a range 0-100 -myring.doorbell_battery_life('Front Door') -'4107 - -myring.doorbell_attributes('Front Door') -{'address': '123 Example St, New York, NY, USA', - 'alerts': {'connection': 'online'}, - 'battery_life': '4107', - 'description': 'Front Door', - 'device_id': '12345678', - 'external_connection': False, - 'features': {'advanced_motion_enabled': False, - 'motions_enabled': True, - 'people_only_enabled': False, - 'shadow_correction_enabled': False, - 'show_recordings': True}, - 'firmware_version': '1.3.91', - - '....SNIP....' : '....SNIP....', - - 'video_settings': {'ae_level': 32, - 'birton': None, - 'brightness': 16, - 'contrast': 80, - 'saturation': 48}}, - 'subscribed': True, - 'subscribed_motions': True, - 'time_zone': 'America/New_York'} - -myring.history(limit=2, timezone='America/Sao_Paulo') -[{'answered': False, - 'created_at': datetime.datetime(2017, 2, 14, 19, 51, 31, tzinfo=), - 'doorbot': {'description': 'Front Door', 'id': 12345}, - 'events': [], - 'favorite': False, - 'id': 1234, - 'kind': 'motion', - 'recording': {'status': 'ready'}, - 'snapshot_url': ''}, - {'answered': False, - 'created_at': datetime.datetime(2017, 2, 14, 18, 26, 6, tzinfo=), - 'doorbot': {'description': 'Front Door', 'id': 12345}, - 'events': [], - 'favorite': False, - 'id': 12345, - 'kind': 'motion', - 'recording': {'status': 'ready'}, - 'snapshot_url': ''}] - -myring.history(name='Front Door', limit=1, timezone='America/Sao_Paulo') -[{'answered': False, - 'created_at': datetime.datetime(2017, 2, 14, 19, 51, 31, tzinfo=), - 'doorbot': {'description': 'Front Door', 'id': 12345}, - 'events': [], - 'favorite': False, - 'id': 1234, - 'kind': 'motion', - 'recording': {'status': 'ready'}, - 'snapshot_url': ''}] - -# download video -myring.doorbell_download_recording(123456, '/home/user/test.mp4') -True - -# show video URL -myring.doorbell_recording_url(123456) -'https://ring-transcoded-videos.s3.amazonaws.com/123456.mp4?X-Amz-Expires=3600&X-Amz-Date=20170210T000928Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=zzzzzzzzzzzzAAA/20170210/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - -# generate live streaming -myring.live_streaming('Front Door') -[{'audio_jitter_buffer_ms': 0, - 'device_kind': 'lpd_v1', - 'doorbot_description': 'Front Door', - 'doorbot_id': 12345, - 'expires_in': 179, - 'id': 123456, - 'id_str': '12345', - 'kind': 'on_demand', - 'motion': False, - 'now': 1486710809.55569, - 'optimization_level': 3, - 'protocol': 'sip', - 'sip_ding_id': '1234563', - 'sip_endpoints': None, - 'sip_from': 'sip:12345@ring.com', - 'sip_server_ip': '1.2.3.4', - 'sip_server_port': '15063', - 'sip_server_tls': 'false', - 'sip_session_id': '1iaaaaaaaaaq', - 'sip_to': 'sip:1iaaaaaaaaaq@1.2.3.4.5:15063;transport=tcp', - 'sip_token': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', - 'snapshot_url': '', - 'state': 'ringing', - 'video_jitter_buffer_ms': 0}] - -# push notifications (motion or ring) - while True: - time.sleep(3) - print(myring.check_activity) -[] -[] -[{'audio_jitter_buffer_ms': 0, - 'device_kind': 'lpd_v1', - 'doorbot_description': 'Front Door', - 'doorbot_id': 12345, - 'expires_in': 179, - 'id': 123456, - 'id_str': '12345', - 'kind': 'on_demand', - 'motion': False, - 'now': 1486710809.55569, - 'optimization_level': 3, - 'protocol': 'sip', - 'sip_ding_id': '1234563', - 'sip_endpoints': None, - 'sip_from': 'sip:12345@ring.com', - 'sip_server_ip': '1.2.3.4', - 'sip_server_port': '15063', - 'sip_server_tls': 'false', - 'sip_session_id': '1iaaaaaaaaaq', - 'sip_to': 'sip:1iaaaaaaaaaq@1.2.3.4.5:15063;transport=tcp', - 'sip_token': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', - 'snapshot_url': '', - 'state': 'ringing', - 'video_jitter_buffer_ms': 0}] -[] -[] +In [1]: from ring_doorbell import Ring +In [2]: myring = Ring('user@email.com', 'password') + +In [3]: myring.devices +Out[3]: +{'chimes': [], + 'doorbells': []} + +In [4]: myring.chimes +Out[4]: [] + +In [5]: myring.doorbells +Out[5]: [] + +In [6]: mychime = myring.chimes[0] + +In [7]: mychime. + mychime.account_id mychime.firmware mychime.linked_tree mychime.subscribed_motions + mychime.address mychime.id mychime.longitude mychime.timezone + mychime.debug mychime.kind mychime.name mychime.update + mychime.family mychime.latitude mychime.subscribed mychime.volume + +In [7]: mychime.volume +Out[7]: 5 + +#updating volume +In [8]: mychime.volume = 200 +Must be within the 0-10. + +In [9]: mychime.volume = 4 + +In [10]: mychime.volume +Out[10]: 4 + +# DoorBells +In [11]: mydoorbell = myring.doorbells[0] + +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.debug mydoorbell.longitude + mydoorbell.existing_doorbell_type mydoorbell.name + mydoorbell.existing_doorbell_type_duration mydoorbell.recording_download + mydoorbell.existing_doorbell_type_enabled mydoorbell.recording_url + mydoorbell.family mydoorbell.timezone + mydoorbell.firmware mydoorbell.update + mydoorbell.history mydoorbell.volume + mydoorbell.id + +In [12]: mydoorbell.last_recording_id +Out[12]: 2222222221 + +In [14]: mydoorbell.existing_doorbell_type +Out[14]: 'Mechanical' + +In [15]: mydoorbell.existing_doorbell_type_enabled +Out[15]: True + +In [16]: mydoorbell.existing_doorbell_type_enabled = False + +In [17]: mydoorbell.existing_doorbell_type_enabled +Out[17]: False ``` ## Credits From 560c995fbf3fac5730356bc63096c09e06fe6c84 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 04:48:22 -0500 Subject: [PATCH 4/8] Removed all trailing whitespaces from Readme --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index abffd3b4..81e80704 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ In [1]: from ring_doorbell import Ring In [2]: myring = Ring('user@email.com', 'password') In [3]: myring.devices -Out[3]: +Out[3]: {'chimes': [], 'doorbells': []} - + In [4]: myring.chimes Out[4]: [] @@ -29,14 +29,14 @@ Out[5]: [] In [6]: mychime = myring.chimes[0] In [7]: mychime. - mychime.account_id mychime.firmware mychime.linked_tree mychime.subscribed_motions - mychime.address mychime.id mychime.longitude mychime.timezone - mychime.debug mychime.kind mychime.name mychime.update - mychime.family mychime.latitude mychime.subscribed mychime.volume + mychime.account_id mychime.firmware mychime.linked_tree mychime.subscribed_motions + mychime.address mychime.id mychime.longitude mychime.timezone + mychime.debug mychime.kind mychime.name mychime.update + mychime.family mychime.latitude mychime.subscribed mychime.volume In [7]: mychime.volume Out[7]: 5 - + #updating volume In [8]: mychime.volume = 200 Must be within the 0-10. @@ -46,23 +46,23 @@ In [9]: mychime.volume = 4 In [10]: mychime.volume Out[10]: 4 -# DoorBells +# DoorBells In [11]: mydoorbell = myring.doorbells[0] 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.debug mydoorbell.longitude - mydoorbell.existing_doorbell_type mydoorbell.name - mydoorbell.existing_doorbell_type_duration mydoorbell.recording_download - mydoorbell.existing_doorbell_type_enabled mydoorbell.recording_url - mydoorbell.family mydoorbell.timezone - mydoorbell.firmware mydoorbell.update - mydoorbell.history mydoorbell.volume - mydoorbell.id - + mydoorbell.account_id mydoorbell.kind + mydoorbell.address mydoorbell.last_recording_id + mydoorbell.battery_life mydoorbell.latitude + mydoorbell.check_activity mydoorbell.live_streaming_json + mydoorbell.debug mydoorbell.longitude + mydoorbell.existing_doorbell_type mydoorbell.name + mydoorbell.existing_doorbell_type_duration mydoorbell.recording_download + mydoorbell.existing_doorbell_type_enabled mydoorbell.recording_url + mydoorbell.family mydoorbell.timezone + mydoorbell.firmware mydoorbell.update + mydoorbell.history mydoorbell.volume + mydoorbell.id + In [12]: mydoorbell.last_recording_id Out[12]: 2222222221 From 1db95d157587d78098f87f75d7ce04c924016863 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 05:02:20 -0500 Subject: [PATCH 5/8] Allow to play test sound on chime --- README.md | 1 + ring_doorbell/__init__.py | 8 +++++++- ring_doorbell/const.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 81e80704..70316e1b 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ In [7]: mychime. mychime.address mychime.id mychime.longitude mychime.timezone mychime.debug mychime.kind mychime.name mychime.update mychime.family mychime.latitude mychime.subscribed mychime.volume + mychime.test_sound In [7]: mychime.volume Out[7]: 5 diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 471ccc93..14b64297 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -22,7 +22,7 @@ NEW_SESSION_ENDPOINT, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE, MSG_GENERIC_FAIL, MSG_VOLUME_OUTBOUNDS, NOT_FOUND, URL_DOORBELL_HISTORY, URL_RECORDING, - POST_DATA, RETRY_TOKEN) + POST_DATA, RETRY_TOKEN, TESTSOUND_CHIME_ENDPOINT) _LOGGER = logging.getLogger(__name__) @@ -308,6 +308,12 @@ def linked_tree(self): url = API_URI + LINKED_CHIMES_ENDPOINT.format(self.account_id) return self._ring.query(url) + @property + def test_sound(self): + """Play chime to test sound.""" + url = API_URI + TESTSOUND_CHIME_ENDPOINT.format(self.account_id) + response = self._ring.query(url, method='POST') + class RingDoorBell(RingGeneric): """Implementation for Ring Doorbell.""" diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index 550b6e93..4c30011f 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -22,6 +22,7 @@ LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/linked_doorbots' LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + '/vod' NEW_SESSION_ENDPOINT = '/clients_api/session' +TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + '/play_sound' URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history' URL_RECORDING = '/clients_api/dings/{0}/recording' From b0b6e0b195fe3c9f09d50fb3012d38d35bf925b3 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 12:55:14 -0500 Subject: [PATCH 6/8] Fixed lint --- ring_doorbell/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 14b64297..8f28b3d9 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -312,7 +312,8 @@ def linked_tree(self): def test_sound(self): """Play chime to test sound.""" url = API_URI + TESTSOUND_CHIME_ENDPOINT.format(self.account_id) - response = self._ring.query(url, method='POST') + self._ring.query(url, method='POST') + return True class RingDoorBell(RingGeneric): From e40456fed800834859cad7476fc07d5137606b55 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 13:45:27 -0500 Subject: [PATCH 7/8] Validates if doorbell has existing_doorbell_type attr --- ring_doorbell/__init__.py | 106 ++++++++++++++++++++------------------ ring_doorbell/const.py | 2 +- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 8f28b3d9..0c426641 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -20,7 +20,7 @@ DOORBELL_EXISTING_TYPE, DINGS_ENDPOINT, FILE_EXISTS, HEADERS, LINKED_CHIMES_ENDPOINT, LIVE_STREAMING_ENDPOINT, NEW_SESSION_ENDPOINT, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE, - MSG_GENERIC_FAIL, MSG_VOLUME_OUTBOUNDS, + MSG_GENERIC_FAIL, MSG_VOL_OUTBOUND, NOT_FOUND, URL_DOORBELL_HISTORY, URL_RECORDING, POST_DATA, RETRY_TOKEN, TESTSOUND_CHIME_ENDPOINT) @@ -273,14 +273,13 @@ def volume(self): def volume(self, value): if not ((isinstance(value, int)) and (value >= CHIME_VOL_MIN and value <= CHIME_VOL_MAX)): - _LOGGER.error("%s", MSG_VOLUME_OUTBOUNDS.format(CHIME_VOL_MIN, - CHIME_VOL_MAX)) + _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(CHIME_VOL_MIN, + CHIME_VOL_MAX)) return False params = { 'chime[description]': self.name, 'chime[settings][volume]': str(value)} - url = API_URI + CHIMES_ENDPOINT.format(self.account_id) self._ring.query(url, extra_params=params, method='PUT') self.update() @@ -352,8 +351,11 @@ def existing_doorbell_type(self): 1: Digital 2: Not Present """ - return DOORBELL_EXISTING_TYPE[ - self._attrs.get('settings').get('chime_settings').get('type')] + try: + return DOORBELL_EXISTING_TYPE[ + self._attrs.get('settings').get('chime_settings').get('type')] + except AttributeError: + return None @existing_doorbell_type.setter def existing_doorbell_type(self, value): @@ -370,66 +372,73 @@ def existing_doorbell_type(self, value): params = { 'doorbot[description]': self.name, 'doorbot[settings][chime_settings][type]': value} - - url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) - self._ring.query(url, extra_params=params, method='PUT') - self.update() - return True + if self.existing_doorbell_type: + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True + return None @property def existing_doorbell_type_enabled(self): """Return if existing doorbell type is enabled.""" - if self._attrs.get('settings').get('chime_settings').get('type') == 2: - return False - return self._attrs.get('settings').get('chime_settings').get('enable') + if self.existing_doorbell_type: + if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: + return None + return \ + self._attrs.get('settings').get('chime_settings').get('enable') + return False @existing_doorbell_type_enabled.setter def existing_doorbell_type_enabled(self, value): """Enable/disable the existing doorbell if Digital/Mechanical.""" - if self._attrs.get('settings').\ - get('chime_settings').get('type') == 2: - return False - if not isinstance(value, bool): - _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED) - return False + if self.existing_doorbell_type: - params = { - 'doorbot[description]': self.name, - 'doorbot[settings][chime_settings][enable]': value} + if not isinstance(value, bool): + _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED) + return None - url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) - self._ring.query(url, extra_params=params, method='PUT') - self.update() - return True + if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: + return None + + params = { + 'doorbot[description]': self.name, + 'doorbot[settings][chime_settings][enable]': value} + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True + return False @property def existing_doorbell_type_duration(self): """Return duration for Digital chime.""" - if self._attrs.get('settings').get('chime_settings').get('type') == 1: - return self._attrs.get('settings').\ - get('chime_settings').get('duration') + if self.existing_doorbell_type: + if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1]: + return self._attrs.get('settings').\ + get('chime_settings').get('duration') return None @existing_doorbell_type_duration.setter def existing_doorbell_type_duration(self, value): """Set duration for Digital chime.""" - if not ((isinstance(value, int)) and - (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)): - _LOGGER.error("%s", MSG_VOLUME_OUTBOUNDS.format(DOORBELL_VOL_MIN, - DOORBELL_VOL_MAX)) - return False - - if self._attrs.get('settings').get('chime_settings').get('type') != 1: - return None + if self.existing_doorbell_type: - params = { - 'doorbot[description]': self.name, - 'doorbot[settings][chime_settings][duration]': value} - - url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) - self._ring.query(url, extra_params=params, method='PUT') - self.update() - return True + if not ((isinstance(value, int)) and + (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)): + _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, + DOORBELL_VOL_MAX)) + return False + + if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1]: + params = { + 'doorbot[description]': self.name, + 'doorbot[settings][chime_settings][duration]': value} + url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True + return None def history(self, limit=30, timezone=None): """Return history with datetime objects.""" @@ -509,14 +518,13 @@ def volume(self): def volume(self, value): if not ((isinstance(value, int)) and (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)): - _LOGGER.error("%s", MSG_VOLUME_OUTBOUNDS.format(DOORBELL_VOL_MIN, - DOORBELL_VOL_MAX)) + _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, + DOORBELL_VOL_MAX)) return False params = { 'doorbot[description]': self.name, 'doorbot[settings][doorbell_volume]': str(value)} - url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id) self._ring.query(url, extra_params=params, method='PUT') self.update() diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index 4c30011f..82317278 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -61,4 +61,4 @@ 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_VOLUME_OUTBOUNDS = 'Must be within the {0}-{1}.' +MSG_VOL_OUTBOUND = 'Must be within the {0}-{1}.' From b84d985f55aafd18ebda1856cef00bea1b551e7d Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 24 Feb 2017 21:38:54 -0500 Subject: [PATCH 8/8] Updated log --- ring_doorbell/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index 0c426641..98363464 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -128,7 +128,7 @@ def query(self, if method == 'GET': response = req.json() return response - _LOGGER.debug("%s", MSG_GENERIC_FAIL) + _LOGGER.error("%s", MSG_GENERIC_FAIL) return None @property