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/8] 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/8] 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/8] 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/8] 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', From 33f7885e04458c31c260921e91043621c35c6d88 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 10 Mar 2017 18:37:25 -0500 Subject: [PATCH 5/8] Allows to filter history by event kind: 'motion', 'on_demand', 'ding' (#20) * Allows to filter history by event kind: 'motion', 'on_demand', 'ding' * Fixed whitespace --- ring_doorbell/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py index a23b6d26..8fd2fe12 100644 --- a/ring_doorbell/__init__.py +++ b/ring_doorbell/__init__.py @@ -485,7 +485,7 @@ def existing_doorbell_type_duration(self, value): return True return None - def history(self, limit=30, timezone=None): + def history(self, limit=30, timezone=None, kind=None): """Return history with datetime objects.""" # allow modify the items to return params = {'limit': str(limit)} @@ -508,6 +508,10 @@ def history(self, limit=30, timezone=None): entry['created_at'] = tz_dt else: entry['created_at'] = utc_dt + + if kind: + return list(filter(lambda array: array['kind'] == kind, response)) + return response @property From 9f53b8bcd0287ad21e3ba709b944e7272eb3a1ed Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sun, 12 Mar 2017 18:20:14 -0400 Subject: [PATCH 6/8] Unittests (#22) * Introduced base skeleton for unittest --- .coveragerc | 5 + .travis.yml | 14 +-- requirements.txt | 5 +- requirements_tests.txt | 6 ++ setup.cfg | 4 + setup.py | 1 + tests/__init__.py | 1 + tests/test_ring.py | 219 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 9 +- 9 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 .coveragerc create mode 100644 requirements_tests.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_ring.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..a5f7fcee --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +omit = + */python?.?/* + */site-packages/nose/* + *__init__* diff --git a/.travis.yml b/.travis.yml index 22d99d81..e21c6b25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,15 @@ language: python matrix: fast_finish: true include: - #- python: "3.4.2" - #env: TOXENV=py34 + - python: "2.7" + env: TOXENV=py27 + - python: "3.5" + env: TOXENV=py35 + - python: "3.6" + env: TOXENV=py36 - python: "3.4.2" env: TOXENV=lint - #- python: "3.5" - # env: TOXENV=py35 - #- python: "3.6" - # env: TOXENV=py36 +install: pip install -U tox coveralls script: tox cache: pip +after_success: coveralls diff --git a/requirements.txt b/requirements.txt index 7fd41edc..03c5fdd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ -flake8 -pylint -requests -tox pytz +requests diff --git a/requirements_tests.txt b/requirements_tests.txt new file mode 100644 index 00000000..e07ebba6 --- /dev/null +++ b/requirements_tests.txt @@ -0,0 +1,6 @@ +coveralls +flake8 +mock +pylint +pytest +tox diff --git a/setup.cfg b/setup.cfg index b88034e4..bec0b5e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [metadata] description-file = README.md + +[tool:pytest] +testpaths = tests +norecursedirs = .git diff --git a/setup.py b/setup.py index 877de375..98d3feb2 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ license='LGPLv3+', include_package_data=True, install_requires=['requests', 'pytz'], + test_suite='tests', keywords=[ 'ring', 'door bell', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..35ce998d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Ring Door Bell components.""" diff --git a/tests/test_ring.py b/tests/test_ring.py new file mode 100644 index 00000000..60381192 --- /dev/null +++ b/tests/test_ring.py @@ -0,0 +1,219 @@ +"""The tests for the Ring platform.""" +import unittest +try: + import mock +except ImportError: + from unittest import mock + +USERNAME = 'foo' +PASSWORD = 'bar' + + +def mocked_requests_get(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + if str(args[0]).startswith('https://api.ring.com/clients_api/session'): + return MockResponse({ + "profile": { + "authentication_token": "12345678910", + "email": "foo@bar.org", + "features": { + "chime_dnd_enabled": False, + "chime_pro_enabled": True, + "delete_all_enabled": True, + "delete_all_settings_enabled": False, + "device_health_alerts_enabled": True, + "floodlight_cam_enabled": True, + "live_view_settings_enabled": True, + "lpd_enabled": True, + "lpd_motion_announcement_enabled": False, + "multiple_calls_enabled": True, + "multiple_delete_enabled": True, + "nw_enabled": True, + "nw_larger_area_enabled": False, + "nw_user_activated": False, + "owner_proactive_snoozing_enabled": True, + "power_cable_enabled": False, + "proactive_snoozing_enabled": False, + "reactive_snoozing_enabled": False, + "remote_logging_format_storing": False, + "remote_logging_level": 1, + "ringplus_enabled": True, + "starred_events_enabled": True, + "stickupcam_setup_enabled": True, + "subscriptions_enabled": True, + "ujet_enabled": False, + "video_search_enabled": False, + "vod_enabled": False}, + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"} + }, 201) + elif str(args[0])\ + .startswith("https://api.ring.com/clients_api/ring_devices"): + return MockResponse({ + "authorized_doorbots": [], + "chimes": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "description": "Downstairs", + "device_id": "abcdef123", + "do_not_disturb": {"seconds_left": 0}, + "features": {"ringtones_enabled": True}, + "firmware_version": "1.2.3", + "id": 999999, + "kind": "chime", + "latitude": 12.000000, + "longitude": -70.12345, + "owned": True, + "owner": { + "email": "foo@bar.org", + "first_name": "Marcelo", + "id": 999999, + "last_name": "Bar"}, + "settings": { + "ding_audio_id": None, + "ding_audio_user_id": None, + "motion_audio_id": None, + "motion_audio_user_id": None, + "volume": 2}, + "time_zone": "America/New_York"}], + "doorbots": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 4081, + "description": "Front Door", + "device_id": "aacdef123", + "external_connection": False, + "features": { + "advanced_motion_enabled": False, + "motion_message_enabled": False, + "motions_enabled": True, + "people_only_enabled": False, + "shadow_correction_enabled": False, + "show_recordings": True}, + "firmware_version": "1.4.26", + "id": 987652, + "kind": "lpd_v1", + "latitude": 12.000000, + "longitude": -70.12345, + "motion_snooze": None, + "owned": True, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "settings": { + "chime_settings": { + "duration": 3, + "enable": True, + "type": 0}, + "doorbell_volume": 1, + "enable_vod": True, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": False, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"]}, + "subscribed": True, + "subscribed_motions": True, + "time_zone": "America/New_York"}] + }, 200) + elif str(args[0]).startswith("https://api.ring.com/clients_api/doorbots"): + return MockResponse([{ + "answered": False, + "created_at": "2017-03-05T15:03:40.000Z", + "events": [], + "favorite": False, + "id": 987654321, + "kind": "motion", + "recording": {"status": "ready"}, + "snapshot_url": "" + }], 200) + + +class TestRing(unittest.TestCase): + """Test the Ring.""" + + @mock.patch('requests.Session.get', side_effect=mocked_requests_get) + @mock.patch('requests.Session.post', side_effect=mocked_requests_get) + def test_basic_attributes(self, get_mock, post_mock): + """Test the Ring class and methods.""" + from ring_doorbell import Ring + + myring = Ring(USERNAME, PASSWORD, persist_token=True) + self.assertTrue(myring.is_connected) + self.assertIsInstance(myring.features, dict) + self.assertFalse(myring.debug) + self.assertEqual(1, len(myring.chimes)) + self.assertNotEqual(2, len(myring.doorbells)) + self.assertTrue(myring._persist_token) + self.assertEquals('http://localhost/', myring._push_token_notify_url) + + +class TestRingChime(unittest.TestCase): + """Test the Ring Chime object.""" + + @mock.patch('requests.Session.get', side_effect=mocked_requests_get) + @mock.patch('requests.Session.post', side_effect=mocked_requests_get) + 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, persist_token=True) + dev = myring.chimes[0] + + self.assertEqual('123 Main St', dev.address) + self.assertNotEqual(99999, dev.account_id) + self.assertEqual('abcdef123', dev.id) + self.assertEqual('chime', dev.kind) + self.assertIsNotNone(dev.latitude) + self.assertEqual('America/New_York', dev.timezone) + self.assertEqual(2, dev.volume) + + +class TestRingDoorBell(unittest.TestCase): + """Test the Ring DoorBell object.""" + + @mock.patch('requests.Session.get', side_effect=mocked_requests_get) + @mock.patch('requests.Session.post', side_effect=mocked_requests_get) + 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) + dev = myring.doorbells[0] + + self.assertEqual(987652, dev.account_id) + self.assertEqual('123 Main St', dev.address) + self.assertEqual('lpd_v1', dev.kind) + self.assertEqual(-70.12345, dev.longitude) + self.assertEqual('America/New_York', dev.timezone) + self.assertEqual(1, dev.volume) + + self.assertIsInstance(dev.history(limit=1, kind='motion'), list) + self.assertEqual(0, len(dev.history(limit=1, kind='ding'))) + + self.assertEqual('Mechanical', dev.existing_doorbell_type) diff --git a/tox.ini b/tox.ini index cff51f13..2993fe27 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,17 @@ [tox] -envlist = py34, py35, py36, lint +envlist = py27, py35, py36, lint skip_missing_interpreters = True [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/ring_doorbell +whitelist_externals = /usr/bin/env +install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} +commands = + py.test --verbose --color=auto --duration=0 +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements_tests.txt [testenv:lint] ignore_errors = True From 0ffea2f15eca7b927f8c1978f136203b428f7399 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Mon, 13 Mar 2017 05:09:46 -0400 Subject: [PATCH 7/8] Added thanks to the http://www.android-x86.org/ community --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8647c18b..73efa54b 100644 --- a/README.md +++ b/README.md @@ -84,4 +84,4 @@ Out[17]: False - A guy named MadBagger at Prism19 for his initial research (http://www.prism19.com/doorbot/second-pass-and-comm-reversing/) - The creators of mitmproxy (https://mitmproxy.org/) great http and https traffic inspector - @mfussenegger for his post on mitmproxy and virtualbox https://zignar.net/2015/12/31/sniffing-vbox-traffic-mitmproxy/ - +- To the project http://www.android-x86.org/ which allowed me to install Android on KVM. From c37e599b847f9e78abfa4bdf94a3838b1cc53ca9 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Mon, 13 Mar 2017 07:16:39 -0400 Subject: [PATCH 8/8] Added basic structure for docs (#24) * Added basic structure for docs * fixed lint --- docs/Makefile | 20 +++++ docs/source/conf.py | 158 +++++++++++++++++++++++++++++++++++ docs/source/credits.rst | 9 ++ docs/source/how_to.rst | 91 ++++++++++++++++++++ docs/source/index.rst | 29 +++++++ docs/source/installation.rst | 13 +++ docs/source/source_code.rst | 36 ++++++++ 7 files changed, 356 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/source/conf.py create mode 100644 docs/source/credits.rst create mode 100644 docs/source/how_to.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/installation.rst create mode 100644 docs/source/source_code.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..6a7218da --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = PythonRingDoorBell +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..b4bad9de --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Python Ring Door Bell documentation build configuration file, created by +# sphinx-quickstart on Mon Mar 13 05:17:01 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../../ring_doorbell/')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Python Ring Door Bell' +copyright = '2017, Marcelo Moreira de Mello' +author = 'Marcelo Moreira de Mello' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1.1' +# The full version, including alpha/beta/rc tags. +release = '0.1.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PythonRingDoorBelldoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'PythonRingDoorBell.tex', + 'Python Ring Door Bell Documentation', + 'Marcelo Moreira de Mello', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pythonringdoorbell', 'Python Ring Door Bell Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PythonRingDoorBell', 'Python Ring Door Bell Documentation', + author, 'PythonRingDoorBell', 'One line description of project.', + 'Miscellaneous'), +] diff --git a/docs/source/credits.rst b/docs/source/credits.rst new file mode 100644 index 00000000..1191aedc --- /dev/null +++ b/docs/source/credits.rst @@ -0,0 +1,9 @@ +Credits && Thanks +----------------- + +* This project was inspired and based on https://github.com/jeroenmoors/php-ring-api. Many thanks @jeroenmoors. +* A guy named MadBagger at Prism19 for his initial research (http://www.prism19.com/doorbot/second-pass-and-comm-reversing/) +* The creators of mitmproxy (https://mitmproxy.org/) great http and https traffic inspector +* @mfussenegger for his post on mitmproxy and virtualbox https://zignar.net/2015/12/31/sniffing-vbox-traffic-mitmproxy/ +* To the project http://www.android-x86.org/ which allowed me to install Android on KVM. + diff --git a/docs/source/how_to.rst b/docs/source/how_to.rst new file mode 100644 index 00000000..bf8fce7c --- /dev/null +++ b/docs/source/how_to.rst @@ -0,0 +1,91 @@ +How To Use It +============= + +Initializing your Ring object +----------------------------- + +.. code-block:: python + + from ring_doorbell import Ring + myring = Ring('foo@bar', 'secret') + + myring.is_connected + True + + myring.has_subscription + True + + Chimes + ------ + +Listing devices linked to your account +------------------------------------------ + +.. code-block:: python + + # All devices + myring.devices + {'chimes': [], + 'doorbells': []} + + # All chimes + myring.chimes + [] + + # All door bells + myring.doorbells + [] + +Getting/setting attributes +-------------------------------- +.. code-block:: python + + for dev in list(myring.chimes + myring.doorbells): + + # refresh data + dev.update() + + print('Account ID: %s' % dev.account_id) + print('Address: %s' % dev.address) + print('Family: %s' % dev.family) + print('ID: %s' % dev.id) + print('Name: %s' % dev.name) + print('Timezone: %s' % dev.timezone) + + # setting dev volume + print('Volume: %s' % dev.volume) + dev.volume = 5 + print('Volume: %s' % dev.volume) + + # play dev test shound + if dev.family == 'chimes' + dev.test_sound + + +Showing door bell events +------------------------ +.. code-block:: python + + for doorbell in myring.doorbells: + + # listing the last 15 events of any kind + for event in doorbell.history(limit=15): + print('ID: %s' % event['id']) + print('Kind: %s' % event['kind']) + print('Answered: %s' % event['answered']) + print('When: %s' % event['created_at']) + print('--' * 50) + + # get a event list only the triggered by motion + events = doorbell.history(kind='motion') + + +Download the last video triggerd by ding +---------------------------------------- +.. code-block:: python + + doorbell = myring.doorbells[0] + doorbell.recording_download( + doorbell.history(limit=100, kind='ding')[0]['id'], + filename='/home/user/last_ding.mp4', + override=True) diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..f160cfee --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,29 @@ +================================================= +Python Ring Door Bell's documentation +================================================= + +Python Ring Door Bell is a library written in Python 2.7/3x +that exposes the Ring.com devices as Python objects. + + +.. note:: + Ring.com does not provide an official API. + The results of this project are merely from reverse engineering. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Installation + How to Use it + Source Code + Credits & Thanks + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` + +.. _Python Ring DoorBell: https://github.com/tchellomello/python-ring-doorbell diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 00000000..161c57fb --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,13 @@ +Installation +------------ + +.. code-block:: bash + + # Installing from PyPi + $ pip install ring_doorbell #python 2.7 + $ pip3 install ring_doorbell #python 3.x + + # Installing latest development + $ pip3 install \ + git+https://github.com/tchellomello/python-ring-doorbell@dev + diff --git a/docs/source/source_code.rst b/docs/source/source_code.rst new file mode 100644 index 00000000..221fbd38 --- /dev/null +++ b/docs/source/source_code.rst @@ -0,0 +1,36 @@ +Source code +----------- + + +class Ring +========== + +.. autoclass:: ring_doorbell.Ring + :members: + :undoc-members: + :show-inheritance: + +class RingGeneric +================= + +.. autoclass:: ring_doorbell.RingGeneric + :members: + :undoc-members: + :show-inheritance: + +class RingChime +=============== + +.. autoclass:: ring_doorbell.RingChime + :members: + :undoc-members: + :show-inheritance: + +class RingDoorBell +================== + +.. autoclass:: ring_doorbell.RingDoorBell + :members: + :undoc-members: + :show-inheritance: + :inherited-members: