diff --git a/README.rst b/README.rst
index 68457496..3963a8bc 100644
--- a/README.rst
+++ b/README.rst
@@ -8,8 +8,8 @@ Python Ring Door Bell
.. image:: https://travis-ci.org/tchellomello/python-ring-doorbell.svg?branch=master
:target: https://travis-ci.org/tchellomello/python-ring-doorbell
-.. image:: https://coveralls.io/repos/github/tchellomello/python-ring-doorbell/badge.svg
- :target: https://coveralls.io/github/tchellomello/python-ring-doorbell
+.. image:: https://coveralls.io/repos/github/tchellomello/python-ring-doorbell/badge.svg?branch=master
+ :target: https://coveralls.io/github/tchellomello/python-ring-doorbell?branch=master
.. image:: https://img.shields.io/pypi/pyversions/ring-doorbell.svg
:target: https://pypi.python.org/pypi/ring-doorbell
@@ -20,6 +20,8 @@ that exposes the Ring.com devices as Python objects.
*Currently Ring.com does not provide an official API. The results of this project are merely from reverse engineering.*
+Documentation: `http://python-ring-doorbell.readthedocs.io/ `_
+
Installation
------------
@@ -30,7 +32,7 @@ Installation
$ pip install ring_doorbell
# Installing latest development
- $ pip3 install \
+ $ pip install \
git+https://github.com/tchellomello/python-ring-doorbell@dev
@@ -63,11 +65,15 @@ Listing devices linked to your account
myring.doorbells
[]
-Playing with the attributes
----------------------------
+ # All stickup cams
+ myring.stickup_cams
+ []
+
+Playing with the attributes and functions
+-----------------------------------------
.. code-block:: python
- for dev in list(myring.chimes + myring.doorbells):
+ for dev in list(myring.stickup_cams + myring.chimes + myring.doorbells):
# refresh data
dev.update()
@@ -78,6 +84,8 @@ Playing with the attributes
print('ID: %s' % dev.id)
print('Name: %s' % dev.name)
print('Timezone: %s' % dev.timezone)
+ print('Wifi Name: %s' % dev.wifi_name)
+ print('Wifi RSSI: %s' % dev.wifi_signal_strength)
# setting dev volume
print('Volume: %s' % dev.volume)
@@ -86,7 +94,8 @@ Playing with the attributes
# play dev test shound
if dev.family == 'chimes'
- dev.test_sound
+ dev.test_sound(kind = 'ding')
+ dev.test_sound(kind = 'motion')
Showing door bell events
diff --git a/docs/source/credits.rst b/docs/source/credits.rst
deleted file mode 100644
index 1191aedc..00000000
--- a/docs/source/credits.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644
index bf8fce7c..00000000
--- a/docs/source/how_to.rst
+++ /dev/null
@@ -1,91 +0,0 @@
-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
index f160cfee..70c533f5 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -10,15 +10,162 @@ that exposes the Ring.com devices as Python objects.
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
+Installation
+------------
+
+.. code-block:: bash
+
+ # Installing from PyPi
+ $ pip install ring_doorbell
+
+ # Installing latest development
+ $ pip install \
+ git+https://github.com/tchellomello/python-ring-doorbell@dev
+
+
+Initializing your Ring object
+-----------------------------
+
+.. code-block:: python
+
+ from ring_doorbell import Ring
+ myring = Ring('foo@bar', 'secret')
+
+ myring.is_connected
+ True
+
+Listing devices linked to your account
+--------------------------------------
+
+.. code-block:: python
+
+ # All devices
+ myring.devices
+ {'chimes': [],
+ 'doorbells': []}
+
+ # All chimes
+ myring.chimes
+ []
+
+ # All door bells
+ myring.doorbells
+ []
+
+ # All stickup cams
+ myring.stickup_cams
+ []
+
+
+Playing with the attributes
+---------------------------
+.. code-block:: python
+
+ for dev in list(myring.stickup_cams + 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)
+ print('Wifi Name: %s' % dev.wifi_name)
+ print('Wifi RSSI: %s' % dev.wifi_signal_strength)
+
+ # 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')
+
+
+Downloading the last video triggered 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)
+
+
+Displaying the last video capture URL
+-------------------------------------
+.. code-block:: python
+
+ print(doorbell.recording_url(doorbell.last_recording_id))
+ 'https://ring-transcoded-videos.s3.amazonaws.com/99999999.mp4?X-Amz-Expires=3600&X-Amz-Date=20170313T232537Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=TOKEN_SECRET/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=secret'
+
+
+Developing
+==========
+
+.. autoclass:: ring_doorbell.Ring
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: ring_doorbell.generic.RingGeneric
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: ring_doorbell.chime.RingChime
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: ring_doorbell.doorbot.RingDoorBell
+ :members:
+ :undoc-members:
+ :show-inheritance:
+ :inherited-members:
+
+.. autoclass:: ring_doorbell.stickup_cam.RingStickUpCam
+ :members:
+ :undoc-members:
+ :show-inheritance:
+ :inherited-members:
+
+
+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.
+
Indices and tables
==================
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
deleted file mode 100644
index 161c57fb..00000000
--- a/docs/source/installation.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index 221fbd38..00000000
--- a/docs/source/source_code.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-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:
diff --git a/requirements_tests.txt b/requirements_tests.txt
index f871e407..634c6f84 100644
--- a/requirements_tests.txt
+++ b/requirements_tests.txt
@@ -4,4 +4,5 @@ mock
pylint
pytest
pytest-cov
+requests_mock
tox
diff --git a/ring_doorbell/__init__.py b/ring_doorbell/__init__.py
index 4d875044..fe37317a 100644
--- a/ring_doorbell/__init__.py
+++ b/ring_doorbell/__init__.py
@@ -1,31 +1,24 @@
# coding: utf-8
# vim:sw=4:ts=4:et:
"""Python Ring Doorbell wrapper."""
-from datetime import datetime
-
try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
-import os
import logging
import requests
-import pytz
-from ring_doorbell.utils import (
- _locator, _exists_cache, _save_cache, _read_cache)
+from ring_doorbell.utils import _exists_cache, _save_cache, _read_cache
+
from ring_doorbell.const import (
- API_VERSION, API_URI, CACHE_ATTRS, CACHE_FILE, 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_VOL_OUTBOUND,
- NOT_FOUND, URL_DOORBELL_HISTORY, URL_RECORDING,
- POST_DATA, PERSIST_TOKEN_ENDPOINT, PERSIST_TOKEN_DATA,
- RETRY_TOKEN, TESTSOUND_CHIME_ENDPOINT)
+ API_VERSION, API_URI, CACHE_ATTRS, CACHE_FILE,
+ DEVICES_ENDPOINT, HEADERS, NEW_SESSION_ENDPOINT, MSG_GENERIC_FAIL,
+ POST_DATA, PERSIST_TOKEN_ENDPOINT, PERSIST_TOKEN_DATA, RETRY_TOKEN)
+
+from ring_doorbell.doorbot import RingDoorBell
+from ring_doorbell.chime import RingChime
+from ring_doorbell.stickup_cam import RingStickUpCam
_LOGGER = logging.getLogger(__name__)
@@ -207,6 +200,7 @@ def devices(self):
"""Return all devices."""
devs = {}
devs['chimes'] = self.chimes
+ devs['stickup_cams'] = self.stickup_cams
devs['doorbells'] = self.doorbells
return devs
@@ -215,12 +209,17 @@ def __devices(self, device_type):
lst = []
url = API_URI + DEVICES_ENDPOINT
try:
- if device_type == 'chime':
+ if device_type == 'stickup_cams':
+ req = self.query(url).get('stickup_cams')
+ for member in list((obj['description'] for obj in req)):
+ lst.append(RingStickUpCam(self, member))
+
+ if device_type == 'chimes':
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':
+ if device_type == 'doorbells':
req = self.query(url).get('doorbots')
for member in list((obj['description'] for obj in req)):
lst.append(RingDoorBell(self, member))
@@ -237,418 +236,14 @@ def __devices(self, device_type):
@property
def chimes(self):
"""Return a list of RingDoorChime objects."""
- return self.__devices('chime')
-
- @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._ring = None
- self.debug = None
- self.family = None
- self.name = None
-
- # alerts notifications
- self.alert_expires_at = None
-
- def __repr__(self):
- """Return __repr__."""
- return "<{0}: {1}>".format(self.__class__.__name__, self.name)
-
- def update(self):
- """Refresh attributes."""
- self._get_attrs()
- self._update_alert()
-
- @property
- def alert(self):
- """Return alert attribute."""
- return self._ring.cache['alerts']
-
- @alert.setter
- def alert(self, value):
- """Set attribute to alert."""
- self._ring.cache['alerts'] = value
- _save_cache(self._ring.cache, self._ring.cache_file)
- return True
-
- def _update_alert(self):
- """Verify if alert received is still valid."""
- # alert is no longer valid
- if self.alert and self.alert_expires_at:
- if datetime.now() >= self.alert_expires_at:
- self.alert = None
- self.alert_expires_at = None
- _save_cache(self._ring.cache, self._ring.cache_file)
-
- def _get_attrs(self):
- """Return attributes."""
- url = API_URI + DEVICES_ENDPOINT
- try:
- if self.family == 'doorbots' and self.shared:
- lst = self._ring.query(url).get('authorized_doorbots')
- else:
- lst = self._ring.query(url).get(self.family)
- index = _locator(lst, 'description', self.name)
- if index == NOT_FOUND:
- return None
- except AttributeError:
- return None
-
- self._attrs = lst[index]
- return True
-
- @property
- def account_id(self):
- """Return account ID."""
- return self._attrs.get('id')
-
- @property
- def address(self):
- """Return address."""
- return self._attrs.get('address')
-
- @property
- def firmware(self):
- """Return firmware."""
- return self._attrs.get('firmware_version')
-
- # pylint: disable=invalid-name
- @property
- def id(self):
- """Return ID."""
- return self._attrs.get('device_id')
-
- @property
- def latitude(self):
- """Return latitude attr."""
- return self._attrs.get('latitude')
-
- @property
- def longitude(self):
- """Return longitude attr."""
- return self._attrs.get('longitude')
-
- @property
- def kind(self):
- """Return kind attr."""
- return self._attrs.get('kind')
-
- @property
- def timezone(self):
- """Return timezone."""
- return self._attrs.get('time_zone')
-
-
-class RingChime(RingGeneric):
- """Implementation for Ring Chime."""
-
- 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_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()
- return True
-
- @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)
-
- @property
- def test_sound(self):
- """Play chime to test sound."""
- url = API_URI + TESTSOUND_CHIME_ENDPOINT.format(self.account_id)
- self._ring.query(url, method='POST')
- return True
-
-
-class RingDoorBell(RingGeneric):
- """Implementation for Ring Doorbell."""
-
- def __init__(self, ring, name, shared=False):
- """Initilize Ring doorbell object."""
- super(RingDoorBell, self).__init__()
- self._attrs = None
- self._ring = ring
- self.shared = shared
- 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
-
- def check_alerts(self):
- """Return JSON when motion or ring is detected."""
- url = API_URI + DINGS_ENDPOINT
- self.update()
-
- try:
- resp = self._ring.query(url)[0]
- except (IndexError, TypeError):
- 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:
- _save_cache(self._ring.cache, self._ring.cache_file)
- return True
- return None
-
- @property
- def existing_doorbell_type(self):
- """
- Return existing doorbell type.
-
- 0: Mechanical
- 1: Digital
- 2: Not Present
- """
- 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):
- """
- 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}
- 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.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.existing_doorbell_type:
-
- if not isinstance(value, bool):
- _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED)
- return None
-
- 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.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 self.existing_doorbell_type:
-
- 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, kind=None):
- """Return history with datetime objects."""
- # allow modify the items to return
- params = {'limit': str(limit)}
-
- 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
- if timezone:
- mytz = pytz.timezone(timezone)
-
- for entry in response:
- dt_at = datetime.strptime(entry['created_at'],
- '%Y-%m-%dT%H:%M:%S.000Z')
- utc_dt = datetime(dt_at.year, dt_at.month, dt_at.day, dt_at.hour,
- dt_at.minute, dt_at.second, tzinfo=utc)
- if timezone:
- tz_dt = utc_dt.astimezone(mytz)
- 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
- def last_recording_id(self):
- """Return the last recording ID."""
- try:
- return self.history(limit=1)[0]['id']
- except (IndexError, TypeError):
- return None
-
- @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
- try:
- return self._ring.query(url)[0]
- except (IndexError, TypeError):
- pass
- return None
-
- 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:
- req = self._ring.query(url, raw=True)
- if req.status_code == 200:
-
- 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)
- raise
-
- def recording_url(self, recording_id):
- """Return HTTPS recording URL."""
- url = API_URI + URL_RECORDING.format(recording_id)
- req = self._ring.query(url, raw=True)
- if req.status_code == 200:
- 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
+ return self.__devices('chimes')
@property
- def subscribed_motion(self):
- """Return if is subscribed_motion."""
- result = self._attrs.get('subscribed_motions')
- if result is None:
- return False
- return True
+ def stickup_cams(self):
+ """Return a list of RingStickUpCam objects."""
+ return self.__devices('stickup_cams')
@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_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()
- return True
+ def doorbells(self):
+ """Return a list of RingDoorBell objects."""
+ return self.__devices('doorbells')
diff --git a/ring_doorbell/chime.py b/ring_doorbell/chime.py
new file mode 100644
index 00000000..9fa38b23
--- /dev/null
+++ b/ring_doorbell/chime.py
@@ -0,0 +1,61 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Python Ring Chime wrapper."""
+import logging
+
+from ring_doorbell.generic import RingGeneric
+from ring_doorbell.const import (
+ API_URI, CHIMES_ENDPOINT, CHIME_VOL_MIN, CHIME_VOL_MAX,
+ LINKED_CHIMES_ENDPOINT, MSG_VOL_OUTBOUND, TESTSOUND_CHIME_ENDPOINT,
+ CHIME_TEST_SOUND_KINDS, KIND_DING)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RingChime(RingGeneric):
+ """Implementation for Ring Chime."""
+
+ @property
+ def family(self):
+ """Return Ring device family type."""
+ return 'chimes'
+
+ @property
+ def battery_life(self):
+ """Return battery life."""
+ return self._health_attrs.get('battery_percentage')
+
+ @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_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()
+ return True
+
+ @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 test_sound(self, kind=KIND_DING):
+ """Play chime to test sound."""
+ if kind not in CHIME_TEST_SOUND_KINDS:
+ return False
+ url = API_URI + TESTSOUND_CHIME_ENDPOINT.format(self.account_id)
+ self._ring.query(url, method='POST', extra_params={"kind": kind})
+ return True
diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py
index 084ccfe1..f820539b 100644
--- a/ring_doorbell/const.py
+++ b/ring_doorbell/const.py
@@ -33,13 +33,21 @@
DOORBELLS_ENDPOINT = '/clients_api/doorbots/{0}'
PERSIST_TOKEN_ENDPOINT = '/clients_api/device'
+HEALTH_DOORBELL_ENDPOINT = DOORBELLS_ENDPOINT + '/health'
+HEALTH_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/health'
LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/linked_doorbots'
LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + '/vod'
NEW_SESSION_ENDPOINT = '/clients_api/session'
+RINGTONES_ENDPOINT = '/ringtones'
TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + '/play_sound'
URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history'
URL_RECORDING = '/clients_api/dings/{0}/recording'
+# chime test sound kinds
+KIND_DING = 'ding'
+KIND_MOTION = 'motion'
+CHIME_TEST_SOUND_KINDS = (KIND_DING, KIND_MOTION)
+
# default values
CHIME_VOL_MIN = 0
CHIME_VOL_MAX = 10
diff --git a/ring_doorbell/doorbot.py b/ring_doorbell/doorbot.py
new file mode 100644
index 00000000..56755774
--- /dev/null
+++ b/ring_doorbell/doorbot.py
@@ -0,0 +1,314 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Python Ring Doorbell wrapper."""
+import logging
+from datetime import datetime
+import os
+import pytz
+
+
+from ring_doorbell.generic import RingGeneric
+
+from ring_doorbell.utils import _save_cache
+from ring_doorbell.const import (
+ API_URI, DOORBELLS_ENDPOINT, DOORBELL_VOL_MIN, DOORBELL_VOL_MAX,
+ DOORBELL_EXISTING_TYPE, DINGS_ENDPOINT, FILE_EXISTS,
+ LIVE_STREAMING_ENDPOINT, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE,
+ MSG_VOL_OUTBOUND, URL_DOORBELL_HISTORY, URL_RECORDING)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RingDoorBell(RingGeneric):
+ """Implementation for Ring Doorbell."""
+
+ @property
+ def family(self):
+ """Return Ring device family type."""
+ return 'doorbots'
+
+ @property
+ def battery_life(self):
+ """Return battery life."""
+ value = int(self._attrs.get('battery_life'))
+ if value > 100:
+ value = 100
+ return value
+
+ def check_alerts(self):
+ """Return JSON when motion or ring is detected."""
+ url = API_URI + DINGS_ENDPOINT
+ self.update()
+
+ try:
+ resp = self._ring.query(url)[0]
+ except (IndexError, TypeError):
+ 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:
+ _save_cache(self._ring.cache, self._ring.cache_file)
+ return True
+ return None
+
+ @property
+ def existing_doorbell_type(self):
+ """
+ Return existing doorbell type.
+
+ 0: Mechanical
+ 1: Digital
+ 2: Not Present
+ """
+ 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):
+ """
+ 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}
+ 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.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.existing_doorbell_type:
+
+ if not isinstance(value, bool):
+ _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED)
+ return None
+
+ 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.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 self.existing_doorbell_type:
+
+ 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, kind=None,
+ enforce_limit=False, retry=8):
+ """
+ Return history with datetime objects.
+
+ :param limit: specify number of objects to be returned
+ :param timezone: determine which timezone to convert data objects
+ :param kind: filter by kind (ding, motion, on_demand)
+ :param enforce_limit: when True, this will enforce the limit and kind
+ :param retry: determine the max number of attempts to archive the limit
+ """
+ queries = 0
+ original_limit = limit
+
+ # set cap for max queries
+ if retry > 10:
+ retry = 10
+
+ while True:
+ params = {'limit': str(limit)}
+
+ url = API_URI + URL_DOORBELL_HISTORY.format(self.account_id)
+ response = self._ring.query(url, extra_params=params)
+
+ # cherrypick only the selected kind events
+ if kind:
+ response = list(filter(
+ lambda array: array['kind'] == kind, response))
+
+ # convert for specific timezone
+ utc = pytz.utc
+ if timezone:
+ mytz = pytz.timezone(timezone)
+
+ for entry in response:
+ dt_at = datetime.strptime(entry['created_at'],
+ '%Y-%m-%dT%H:%M:%S.000Z')
+ utc_dt = datetime(dt_at.year, dt_at.month, dt_at.day,
+ dt_at.hour, dt_at.minute, dt_at.second,
+ tzinfo=utc)
+ if timezone:
+ tz_dt = utc_dt.astimezone(mytz)
+ entry['created_at'] = tz_dt
+ else:
+ entry['created_at'] = utc_dt
+
+ if enforce_limit:
+ # return because already matched the number
+ # of events by kind
+ if len(response) >= original_limit:
+ return response[:original_limit]
+
+ # ensure the loop will exit after max queries
+ queries += 1
+ if queries == retry:
+ _LOGGER.warning("Could not find total of %s of kind %s",
+ original_limit, kind)
+ break
+
+ # ensure the kind objects returned to match limit
+ limit = limit * 2
+
+ else:
+ break
+
+ return response
+
+ @property
+ def last_recording_id(self):
+ """Return the last recording ID."""
+ try:
+ return self.history(limit=1)[0]['id']
+ except (IndexError, TypeError):
+ return None
+
+ @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
+ try:
+ return self._ring.query(url)[0]
+ except (IndexError, TypeError):
+ pass
+ return None
+
+ 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:
+ req = self._ring.query(url, raw=True)
+ if req.status_code == 200:
+
+ 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)
+ raise
+
+ def recording_url(self, recording_id):
+ """Return HTTPS recording URL."""
+ url = API_URI + URL_RECORDING.format(recording_id)
+ req = self._ring.query(url, raw=True)
+ if req.status_code == 200:
+ 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."""
+ 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_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()
+ return True
+
+ @property
+ def connection_status(self):
+ """Return connection status."""
+ return self._attrs.get('alerts').get('connection')
diff --git a/ring_doorbell/generic.py b/ring_doorbell/generic.py
new file mode 100644
index 00000000..edcfb5c6
--- /dev/null
+++ b/ring_doorbell/generic.py
@@ -0,0 +1,148 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Python Ring RingGeneric wrapper."""
+import logging
+from datetime import datetime
+
+from ring_doorbell.utils import _locator, _save_cache
+from ring_doorbell.const import (
+ API_URI, DEVICES_ENDPOINT, NOT_FOUND,
+ HEALTH_CHIMES_ENDPOINT, HEALTH_DOORBELL_ENDPOINT)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RingGeneric(object):
+ """Generic Implementation for Ring Chime/Doorbell."""
+
+ def __init__(self, ring, name, shared=False):
+ """Initialize Ring Generic."""
+ self._ring = ring
+ self.debug = self._ring.debug
+ self.name = name
+ self.shared = shared
+ self._attrs = None
+ self._health_attrs = None
+
+ # alerts notifications
+ self.alert_expires_at = None
+
+ # force update
+ self.update()
+
+ def __repr__(self):
+ """Return __repr__."""
+ return "<{0}: {1}>".format(self.__class__.__name__, self.name)
+
+ @property
+ def family(self):
+ """Return Ring device family type."""
+ return None
+
+ def update(self):
+ """Refresh attributes."""
+ self._get_attrs()
+ self._get_health_attrs()
+ self._update_alert()
+
+ @property
+ def alert(self):
+ """Return alert attribute."""
+ return self._ring.cache['alerts']
+
+ @alert.setter
+ def alert(self, value):
+ """Set attribute to alert."""
+ self._ring.cache['alerts'] = value
+ _save_cache(self._ring.cache, self._ring.cache_file)
+ return True
+
+ def _update_alert(self):
+ """Verify if alert received is still valid."""
+ # alert is no longer valid
+ if self.alert and self.alert_expires_at:
+ if datetime.now() >= self.alert_expires_at:
+ self.alert = None
+ self.alert_expires_at = None
+ _save_cache(self._ring.cache, self._ring.cache_file)
+
+ def _get_attrs(self):
+ """Return attributes."""
+ url = API_URI + DEVICES_ENDPOINT
+ try:
+ if self.family == 'doorbots' and self.shared:
+ lst = self._ring.query(url).get('authorized_doorbots')
+ else:
+ lst = self._ring.query(url).get(self.family)
+ index = _locator(lst, 'description', self.name)
+ if index == NOT_FOUND:
+ return None
+ except AttributeError:
+ return None
+
+ self._attrs = lst[index]
+ return True
+
+ def _get_health_attrs(self):
+ """Return health attributes."""
+ if self.family == 'doorbots' or self.family == 'stickup_cams':
+ url = API_URI + HEALTH_DOORBELL_ENDPOINT.format(self.account_id)
+ elif self.family == 'chimes':
+ url = API_URI + HEALTH_CHIMES_ENDPOINT.format(self.account_id)
+ self._health_attrs = self._ring.query(url).get('device_health')
+
+ @property
+ def account_id(self):
+ """Return account ID."""
+ return self._attrs.get('id')
+
+ @property
+ def address(self):
+ """Return address."""
+ return self._attrs.get('address')
+
+ @property
+ def firmware(self):
+ """Return firmware."""
+ return self._attrs.get('firmware_version')
+
+ # pylint: disable=invalid-name
+ @property
+ def id(self):
+ """Return ID."""
+ return self._attrs.get('device_id')
+
+ @property
+ def latitude(self):
+ """Return latitude attr."""
+ return self._attrs.get('latitude')
+
+ @property
+ def longitude(self):
+ """Return longitude attr."""
+ return self._attrs.get('longitude')
+
+ @property
+ def kind(self):
+ """Return kind attr."""
+ return self._attrs.get('kind')
+
+ @property
+ def timezone(self):
+ """Return timezone."""
+ return self._attrs.get('time_zone')
+
+ @property
+ def wifi_name(self):
+ """Return wifi ESSID name."""
+ return self._health_attrs.get('wifi_name')
+
+ @property
+ def wifi_signal_strength(self):
+ """Return wifi RSSI."""
+ return self._health_attrs.get('latest_signal_strength')
+
+ @property
+ def wifi_signal_category(self):
+ """Return wifi signal category."""
+ return self._health_attrs.get('latest_signal_category')
diff --git a/ring_doorbell/stickup_cam.py b/ring_doorbell/stickup_cam.py
new file mode 100644
index 00000000..7ad45c5b
--- /dev/null
+++ b/ring_doorbell/stickup_cam.py
@@ -0,0 +1,17 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Python Ring Doorbell wrapper."""
+import logging
+
+from ring_doorbell import RingDoorBell
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RingStickUpCam(RingDoorBell):
+ """Implementation for RingStickUpCam."""
+
+ @property
+ def family(self):
+ """Return Ring device family type."""
+ return 'stickup_cams'
diff --git a/setup.cfg b/setup.cfg
index bec0b5e4..43829428 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[metadata]
-description-file = README.md
+description-file = README.rst
[tool:pytest]
testpaths = tests
diff --git a/setup.py b/setup.py
index 4708b7cb..54e42144 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
setup(
name='ring_doorbell',
packages=['ring_doorbell'],
- version='0.1.4',
+ version='0.1.5',
description='A Python library to communicate with Ring' +
' Door Bell (https://ring.com/)',
author='Marcelo Moreira de Mello',
diff --git a/tests/fixtures/ring_chime_health_attrs.json b/tests/fixtures/ring_chime_health_attrs.json
new file mode 100644
index 00000000..a9e5a845
--- /dev/null
+++ b/tests/fixtures/ring_chime_health_attrs.json
@@ -0,0 +1,19 @@
+{
+ "device_health": {
+ "average_signal_category": "good",
+ "average_signal_strength": -39,
+ "battery_percentage": 100,
+ "battery_percentage_category": null,
+ "battery_voltage": null,
+ "battery_voltage_category": null,
+ "firmware": "1.2.3",
+ "firmware_out_of_date": false,
+ "id": 999999,
+ "latest_signal_category": "good",
+ "latest_signal_strength": -39,
+ "updated_at": "2017-09-30T07:05:03Z",
+ "wifi_is_ring_network": false,
+ "wifi_name": "ring_mock_wifi"
+ }
+}
+
diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json
new file mode 100644
index 00000000..f7f723e3
--- /dev/null
+++ b/tests/fixtures/ring_devices.json
@@ -0,0 +1,128 @@
+{
+ "authorized_doorbots": [
+ {
+ "address": "123 Second St",
+ "alerts": {"connection": "online"},
+ "battery_life": 51,
+ "description": "Back Door",
+ "device_id": "aacdef124",
+ "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": 987653,
+ "kind": "lpd_v1",
+ "latitude": 12.000000,
+ "longitude": -70.12345,
+ "motion_snooze": null,
+ "owned": true,
+ "owner": {
+ "email": "foo@bar.org",
+ "first_name": "Foo",
+ "id": 999999,
+ "last_name": "Bar"},
+ "settings": {
+ "chime_settings": {
+ "duration": 3,
+ "enable": true,
+ "type": 1},
+ "doorbell_volume": 5,
+ "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"}],
+ "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": "Assistant"},
+ "settings": {
+ "ding_audio_id": null,
+ "ding_audio_user_id": null,
+ "motion_audio_id": null,
+ "motion_audio_user_id": null,
+ "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": null,
+ "owned": true,
+ "owner": {
+ "email": "foo@bar.org",
+ "first_name": "Home",
+ "id": 999999,
+ "last_name": "Assistant"},
+ "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": [
+ "null",
+ "low",
+ "medium",
+ "high"]},
+ "subscribed": true,
+ "subscribed_motions": true,
+ "time_zone": "America/New_York"}]
+}
diff --git a/tests/fixtures/ring_ding_active.json b/tests/fixtures/ring_ding_active.json
new file mode 100644
index 00000000..7c9e0b07
--- /dev/null
+++ b/tests/fixtures/ring_ding_active.json
@@ -0,0 +1,26 @@
+[{
+ "audio_jitter_buffer_ms": 0,
+ "device_kind": "lpd_v1",
+ "doorbot_description": "Front Door",
+ "doorbot_id": 987652,
+ "expires_in": 180,
+ "id": 123456789,
+ "id_str": "123456789",
+ "kind": "ding",
+ "motion": false,
+ "now": 1490949469.5498993,
+ "optimization_level": 1,
+ "protocol": "sip",
+ "sip_ding_id": "123456789",
+ "sip_endpoints": null,
+ "sip_from": "sip:abc123@ring.com",
+ "sip_server_ip": "192.168.0.1",
+ "sip_server_port": "15063",
+ "sip_server_tls": "false",
+ "sip_session_id": "28qdvjh-2043",
+ "sip_to": "sip:28qdvjh-2043@192.168.0.1:15063;transport=tcp",
+ "sip_token": "adecc24a428ed704b2d80adb621b5775755915529639e",
+ "snapshot_url": "",
+ "state": "ringing",
+ "video_jitter_buffer_ms": 0
+}]
diff --git a/tests/fixtures/ring_doorboot_health_attrs.json b/tests/fixtures/ring_doorboot_health_attrs.json
new file mode 100644
index 00000000..11f00886
--- /dev/null
+++ b/tests/fixtures/ring_doorboot_health_attrs.json
@@ -0,0 +1,19 @@
+{
+ "device_health": {
+ "average_signal_category": "good",
+ "average_signal_strength": -39,
+ "battery_percentage": 100,
+ "battery_percentage_category": null,
+ "battery_voltage": null,
+ "battery_voltage_category": null,
+ "firmware": "1.9.2",
+ "firmware_out_of_date": false,
+ "id": 987652,
+ "latest_signal_category": "good",
+ "latest_signal_strength": -58,
+ "updated_at": "2017-09-30T07:05:03Z",
+ "wifi_is_ring_network": false,
+ "wifi_name": "ring_mock_wifi"
+ }
+}
+
diff --git a/tests/fixtures/ring_doorboot_health_attrs_id987653.json b/tests/fixtures/ring_doorboot_health_attrs_id987653.json
new file mode 100644
index 00000000..063efabd
--- /dev/null
+++ b/tests/fixtures/ring_doorboot_health_attrs_id987653.json
@@ -0,0 +1,19 @@
+{
+ "device_health": {
+ "average_signal_category": "good",
+ "average_signal_strength": -39,
+ "battery_percentage": 100,
+ "battery_percentage_category": null,
+ "battery_voltage": null,
+ "battery_voltage_category": null,
+ "firmware": "1.9.2",
+ "firmware_out_of_date": false,
+ "id": 987653,
+ "latest_signal_category": "good",
+ "latest_signal_strength": -58,
+ "updated_at": "2017-09-30T07:05:03Z",
+ "wifi_is_ring_network": false,
+ "wifi_name": "ring_mock_wifi"
+ }
+}
+
diff --git a/tests/fixtures/ring_doorbots.json b/tests/fixtures/ring_doorbots.json
new file mode 100644
index 00000000..7ec2d4fd
--- /dev/null
+++ b/tests/fixtures/ring_doorbots.json
@@ -0,0 +1,10 @@
+[{
+ "answered": false,
+ "created_at": "2017-03-05T15:03:40.000Z",
+ "events": [],
+ "favorite": false,
+ "id": 987654321,
+ "kind": "motion",
+ "recording": {"status": "ready"},
+ "snapshot_url": ""
+}]
diff --git a/tests/fixtures/ring_session.json b/tests/fixtures/ring_session.json
new file mode 100644
index 00000000..21ae51c6
--- /dev/null
+++ b/tests/fixtures/ring_session.json
@@ -0,0 +1,36 @@
+{
+ "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": "Home",
+ "id": 999999,
+ "last_name": "Assistant"}
+}
diff --git a/tests/helpers.py b/tests/helpers.py
new file mode 100644
index 00000000..5c0a762c
--- /dev/null
+++ b/tests/helpers.py
@@ -0,0 +1,9 @@
+"""Helper methods for Ring DoorBell tests."""
+import os
+
+
+def load_fixture(filename):
+ """Load a fixture."""
+ path = os.path.join(os.path.dirname(__file__), 'fixtures', filename)
+ with open(path) as fdp:
+ return fdp.read()
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 00000000..dcb4979f
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,39 @@
+# -*- coding:utf-8 -*-
+"""Define basic data for unittests."""
+import os
+import unittest
+import requests_mock
+from tests.helpers import load_fixture
+
+USERNAME = 'foo'
+PASSWORD = 'bar'
+CACHE = os.path.join(os.path.dirname(__file__), 'cache.db')
+
+
+class RingUnitTestBase(unittest.TestCase):
+ """Top level Ring Doorbell test class."""
+
+ @requests_mock.Mocker()
+ def setUp(self, mock):
+ """Setup unit test and load mock."""
+ from ring_doorbell import Ring
+ mock.get('https://api.ring.com/clients_api/ring_devices',
+ text=load_fixture('ring_devices.json'))
+ mock.post('https://api.ring.com/clients_api/session',
+ text=load_fixture('ring_session.json'))
+ mock.put('https://api.ring.com/clients_api/device',
+ text=load_fixture('ring_devices.json'))
+
+ self.ring = Ring(USERNAME, PASSWORD, cache_file=CACHE)
+ self.ring_persistent = \
+ Ring(USERNAME, PASSWORD, cache_file=CACHE, persist_token=True)
+
+ def cleanup(self):
+ """Cleanup any data created from the tests."""
+ self.ring = None
+ if os.path.isfile(CACHE):
+ os.remove(CACHE)
+
+ def tearDown(self):
+ """Stop everything started."""
+ self.cleanup()
diff --git a/tests/test_ring.py b/tests/test_ring.py
index f3edbb2c..3203e832 100644
--- a/tests/test_ring.py
+++ b/tests/test_ring.py
@@ -1,271 +1,45 @@
+# -*- coding: utf-8 -*-
"""The tests for the Ring platform."""
from datetime import datetime
+from tests.test_base import RingUnitTestBase
+from tests.helpers import load_fixture
+import requests_mock
-import os
-import time
-import unittest
-try:
- import mock
-except ImportError:
- from unittest import mock
-USERNAME = 'foo'
-PASSWORD = 'bar'
-CACHE = 'tests/cache.db'
+class TestRing(RingUnitTestBase):
+ """Unit test for core Ring."""
-
-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": [
- {
- "address": "123 Second St",
- "alerts": {"connection": "online"},
- "battery_life": 51,
- "description": "Back Door",
- "device_id": "aacdef124",
- "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": 987653,
- "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": 1},
- "doorbell_volume": 5,
- "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"}],
- "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)
- elif str(args[0])\
- .startswith("https://api.ring.com/clients_api/dings/active"):
- return MockResponse([{
- "audio_jitter_buffer_ms": 0,
- "device_kind": "lpd_v1",
- "doorbot_description": "Front Door",
- "doorbot_id": 12345,
- "expires_in": 180,
- "id": 123456789,
- "id_str": "123456789",
- "kind": "ding",
- "motion": False,
- "now": time.time(),
- "optimization_level": 1,
- "protocol": "sip",
- "sip_ding_id": "123456789",
- "sip_endpoints": None,
- "sip_from": "sip:abc123@ring.com",
- "sip_server_ip": "192.168.0.1",
- "sip_server_port": "15063",
- "sip_server_tls": "false",
- "sip_session_id": "28qdvjh-2043",
- "sip_to": "sip:28qdvjh-2043@192.168.0.1:15063;transport=tcp",
- "sip_token": "adecc24a428ed704b2d80adb621b5775755915529639e",
- "snapshot_url": "",
- "state": "ringing",
- "video_jitter_buffer_ms": 0
- }], 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):
+ @requests_mock.Mocker()
+ def test_basic_attributes(self, mock):
"""Test the Ring class and methods."""
- from ring_doorbell import Ring
-
- myring = Ring(USERNAME, PASSWORD, cache_file=CACHE)
- self.assertTrue(myring.is_connected)
- self.assertIsInstance(myring.cache, dict)
- self.assertFalse(myring.debug)
- self.assertEqual(1, len(myring.chimes))
- self.assertEqual(2, len(myring.doorbells))
- self.assertFalse(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):
+ mock.get('https://api.ring.com/clients_api/ring_devices',
+ text=load_fixture('ring_devices.json'))
+ mock.get('https://api.ring.com/clients_api/chimes/999999/health',
+ text=load_fixture('ring_chime_health_attrs.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987652/health',
+ text=load_fixture('ring_doorboot_health_attrs.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987653/health',
+ text=load_fixture('ring_doorboot_health_attrs_id987653.json'))
+
+ data = self.ring
+ self.assertTrue(data.is_connected)
+ self.assertIsInstance(data.cache, dict)
+ self.assertFalse(data.debug)
+ self.assertEqual(1, len(data.chimes))
+ self.assertEqual(2, len(data.doorbells))
+ self.assertFalse(data._persist_token)
+ self.assertEquals('http://localhost/', data._push_token_notify_url)
+
+ @requests_mock.Mocker()
+ def test_chime_attributes(self, mock):
"""Test the Ring Chime class and methods."""
- from ring_doorbell import Ring
+ mock.get('https://api.ring.com/clients_api/ring_devices',
+ text=load_fixture('ring_devices.json'))
+ mock.get('https://api.ring.com/clients_api/chimes/999999/health',
+ text=load_fixture('ring_chime_health_attrs.json'))
- myring = Ring(USERNAME, PASSWORD, cache_file=CACHE)
- dev = myring.chimes[0]
+ data = self.ring
+ dev = data.chimes[0]
self.assertEqual('123 Main St', dev.address)
self.assertNotEqual(99999, dev.account_id)
@@ -274,28 +48,23 @@ def test_chime_attributes(self, get_mock, post_mock):
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."""
-
- def cleanup(self):
- """Cleanup any data created from the tests."""
- if os.path.isfile(CACHE):
- os.remove(CACHE)
-
- def tearDown(self):
- """Stop everything started."""
- self.cleanup()
-
- @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, cache_file=CACHE, persist_token=True)
- for dev in myring.doorbells:
+ self.assertEqual('ring_mock_wifi', dev.wifi_name)
+ self.assertEqual('good', dev.wifi_signal_category)
+ self.assertNotEqual(100, dev.wifi_signal_strength)
+
+ @requests_mock.Mocker()
+ def test_doorbell_attributes(self, mock):
+ mock.get('https://api.ring.com/clients_api/ring_devices',
+ text=load_fixture('ring_devices.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987652/history',
+ text=load_fixture('ring_doorbots.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987652/health',
+ text=load_fixture('ring_doorboot_health_attrs.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987653/health',
+ text=load_fixture('ring_doorboot_health_attrs_id987653.json'))
+
+ data = self.ring_persistent
+ for dev in data.doorbells:
if not dev.shared:
self.assertEqual('Front Door', dev.name)
self.assertEqual(987652, dev.account_id)
@@ -304,22 +73,35 @@ def test_doorbell_attributes(self, get_mock, post_mock):
self.assertEqual(-70.12345, dev.longitude)
self.assertEqual('America/New_York', dev.timezone)
self.assertEqual(1, dev.volume)
+ self.assertEqual('online', dev.connection_status)
self.assertIsInstance(dev.history(limit=1, kind='motion'),
list)
self.assertEqual(0, len(dev.history(limit=1, kind='ding')))
+ self.assertEqual(0, len(dev.history(limit=1,
+ kind='ding',
+ enforce_limit=True,
+ retry=50)))
self.assertEqual('Mechanical', dev.existing_doorbell_type)
- self.assertTrue(myring._persist_token)
-
- @mock.patch('requests.Session.get', side_effect=mocked_requests_get)
- @mock.patch('requests.Session.post', side_effect=mocked_requests_get)
- def test_shared_doorbell_attributes(self, get_mock, post_mock):
- """Test the Ring Shared DoorBell class and methods."""
- from ring_doorbell import Ring
-
- myring = Ring(USERNAME, PASSWORD, cache_file=CACHE, persist_token=True)
- for dev in myring.doorbells:
+ self.assertTrue(data._persist_token)
+ self.assertEqual('ring_mock_wifi', dev.wifi_name)
+ self.assertEqual('good', dev.wifi_signal_category)
+ self.assertEqual(-58, dev.wifi_signal_strength)
+
+ @requests_mock.Mocker()
+ def test_shared_doorbell_attributes(self, mock):
+ mock.get('https://api.ring.com/clients_api/ring_devices',
+ text=load_fixture('ring_devices.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987652/history',
+ text=load_fixture('ring_doorbots.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987652/health',
+ text=load_fixture('ring_doorboot_health_attrs.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987653/health',
+ text=load_fixture('ring_doorboot_health_attrs_id987653.json'))
+
+ data = self.ring_persistent
+ for dev in data.doorbells:
if dev.shared:
self.assertEqual(987653, dev.account_id)
self.assertEqual(51, dev.battery_life)
@@ -330,29 +112,19 @@ def test_shared_doorbell_attributes(self, get_mock, post_mock):
self.assertEqual(5, dev.volume)
self.assertEqual('Digital', dev.existing_doorbell_type)
- os.remove(CACHE)
-
-
-class TestRingDoorBellAlerts(unittest.TestCase):
- """Test the Ring DoorBell alerts."""
-
- def cleanup(self):
- """Cleanup any data created from the tests."""
- if os.path.isfile(CACHE):
- os.remove(CACHE)
-
- def tearDown(self):
- """Stop everything started."""
- self.cleanup()
-
- @mock.patch('requests.Session.get', side_effect=mocked_requests_get)
- @mock.patch('requests.Session.post', side_effect=mocked_requests_get)
- def test_doorbell_alerts(self, get_mock, post_mock):
- """Test the Ring DoorBell alerts."""
- from ring_doorbell import Ring
-
- myring = Ring(USERNAME, PASSWORD, cache_file=CACHE, persist_token=True)
- for dev in myring.doorbells:
+ @requests_mock.Mocker()
+ def test_doorbell_alerts(self, mock):
+ mock.get('https://api.ring.com/clients_api/ring_devices',
+ text=load_fixture('ring_devices.json'))
+ mock.get('https://api.ring.com/clients_api/dings/active',
+ text=load_fixture('ring_ding_active.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987652/health',
+ text=load_fixture('ring_doorboot_health_attrs.json'))
+ mock.get('https://api.ring.com/clients_api/doorbots/987653/health',
+ text=load_fixture('ring_doorboot_health_attrs_id987653.json'))
+
+ data = self.ring_persistent
+ for dev in data.doorbells:
self.assertEqual('America/New_York', dev.timezone)
# call alerts
@@ -360,5 +132,5 @@ def test_doorbell_alerts(self, get_mock, post_mock):
self.assertIsInstance(dev.alert, dict)
self.assertIsInstance(dev.alert_expires_at, datetime)
- self.assertTrue(datetime.now() <= dev.alert_expires_at)
+ self.assertTrue(datetime.now() >= dev.alert_expires_at)
self.assertIsNotNone(dev._ring.cache_file)