Skip to content

Commit

Permalink
Merge pull request #153 from tchellomello/0.2.6
Browse files Browse the repository at this point in the history
0.2.6
  • Loading branch information
tchellomello committed Dec 27, 2019
2 parents 909d048 + 7c16372 commit 6da218c
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 50 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ __pycache__/
# C extensions
*.so

# Visual Studio
.vs

# Distribution / packaging
.Python
env/
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pytz
requests
requests_oauthlib
oauthlib
43 changes: 29 additions & 14 deletions ring_doorbell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
except ImportError:
from urllib import urlencode

from typing import Callable
import logging
import time
import requests
Expand All @@ -15,12 +16,13 @@
from ring_doorbell.const import (
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,
OAUTH_ENDPOINT, OAUTH_DATA)
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
from ring_doorbell.auth import Auth


_LOGGER = logging.getLogger(__name__)

Expand All @@ -29,7 +31,9 @@
class Ring(object):
"""A Python Abstraction object to Ring Door Bell."""

def __init__(self, username, password, debug=False, persist_token=False,
def __init__(self, username, password,
auth_callback: Callable[[], str] = None,
debug=False, persist_token=False,
push_token_notify_url="http://localhost/", reuse_session=True,
cache_file=CACHE_FILE):
"""Initialize the Ring object."""
Expand All @@ -44,14 +48,17 @@ def __init__(self, username, password, debug=False, persist_token=False,
self.password = password
self.session = requests.Session()

self.auth_callback = auth_callback
self.auth = None

self.cache = CACHE_ATTRS
self.cache['account'] = self.username
self.cache_file = cache_file
self._reuse_session = reuse_session

# tries to re-use old session
if self._reuse_session:
self.cache['token'] = self.token
# self.cache['token'] = self.token
self._process_cached_session()
else:
self._authenticate()
Expand All @@ -75,6 +82,9 @@ def _process_cached_session(self):
self.params = {'api_version': API_VERSION,
'auth_token': self.token}

if 'auth' in self.cache:
self.auth = self.cache['auth']

# test if token from cache_file is still valid and functional
# if not, it should continue to get a new auth token
url = API_URI + DEVICES_ENDPOINT
Expand All @@ -90,16 +100,20 @@ def _process_cached_session(self):
def _get_oauth_token(self):
"""Return Oauth Bearer token."""
# this token should be cached / saved for later
oauth_data = OAUTH_DATA.copy()
oauth_data['username'] = self.username
oauth_data['password'] = self.password
response = self.session.post(OAUTH_ENDPOINT,
data=oauth_data,
headers=HEADERS)
oauth_token = None
if response.status_code == 200:
oauth_token = response.json().get('access_token')
return oauth_token
oauth = Auth(self.auth)

if not self.auth:
self.auth = oauth.fetch_token(
self.username,
self.password,
self.auth_callback)
else:
self.auth = oauth.refresh_tokens()

if self.debug:
_LOGGER.debug("response from get oauth token %s", str(self.auth))

return self.auth['access_token']

def _authenticate(self, attempts=RETRY_TOKEN, session=None, wait=1.0):
"""Authenticate user against Ring API."""
Expand Down Expand Up @@ -154,6 +168,7 @@ def _authenticate(self, attempts=RETRY_TOKEN, session=None, wait=1.0):
if self._reuse_session:
self.cache['account'] = self.username
self.cache['token'] = self.token
self.cache['auth'] = self.auth
_save_cache(self.cache, self.cache_file)

return True
Expand Down
73 changes: 73 additions & 0 deletions ring_doorbell/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# coding: utf-8
# vim:sw=4:ts=4:et:
"""Python Ring Auth Class."""
from typing import Optional, Union, Callable, Dict
from requests import Response
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import (
LegacyApplicationClient, TokenExpiredError,
MissingTokenError)
from ring_doorbell.const import OAuth


class Auth:
"""A Python Auth class for Ring"""
def __init__(self,
token: Optional[Dict[str, str]] = None,
token_updater: Optional[Callable[[str], None]] = None):
self.token_updater = token_updater
self._oauth = OAuth2Session(
client=LegacyApplicationClient(client_id=OAuth.CLIENT_ID),
token=token,
token_updater=token_updater)

def fetch_token(self, username: str, password: str,
auth_callback: Callable[[], str] = None):
"""Initial token fetch with username/password & 2FA"""
try:
return self.__fetch_token(username, password)

except MissingTokenError:
if not auth_callback:
raise

return self.__fetch_token(username, password, auth_callback())

def __fetch_token(self, username: str, password: str,
auth_code: str = None):
"""Private fetch token method"""
if auth_code:
headers = {}
headers['2fa-support'] = 'true'
headers['2fa-code'] = auth_code

return self._oauth.fetch_token(
OAuth.ENDPOINT,
username=username,
password=password,
scope=OAuth.SCOPE,
headers=headers)

return self._oauth.fetch_token(
OAuth.ENDPOINT,
username=username,
password=password,
scope=OAuth.SCOPE)

def refresh_tokens(self) -> Dict[str, Union[str, int]]:
"""Refreshes the auth tokens"""
token = self._oauth.refresh_token(OAuth.ENDPOINT)

if self.token_updater is not None:
self.token_updater(token)

return token

def request(self, method: str, resource: str, **kwargs) -> Response:
"""Does an http request, if token is expired, then it will refresh"""
try:
return self._oauth.request(method, resource, **kwargs)

except TokenExpiredError:
self._oauth.token = self.refresh_tokens()
return self._oauth.request(method, resource, **kwargs)
3 changes: 1 addition & 2 deletions ring_doorbell/chime.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ def family(self):
@property
def model(self):
"""Return Ring device model name."""
# ignore R1705: Unnecessary "elif" after "return" (no-else-return)
if self.kind in CHIME_KINDS:
return 'Chime'
elif self.kind in CHIME_PRO_KINDS:
if self.kind in CHIME_PRO_KINDS:
return 'Chime Pro'
return None

Expand Down
23 changes: 11 additions & 12 deletions ring_doorbell/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@
HEADERS = {
'Content-Type': 'application/x-www-form-urlencoded; charset: UTF-8',
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9.0; SM-G850F Build'
'"/LRX22G)',
'/LRX22G)',
'Accept-Encoding': 'gzip, deflate'
}


class OAuth:
"""OAuth class constants"""
ENDPOINT = 'https://oauth.ring.com/oauth/token'
CLIENT_ID = 'ring_official_android'
SCOPE = 'client'


# number of attempts to refresh token
RETRY_TOKEN = 3

# default suffix for session cache file
CACHE_ATTRS = {'account': None, 'alerts': None, 'token': None}
CACHE_ATTRS = {'account': None, 'alerts': None, 'token': None,
'auth': None}

try:
CACHE_FILE = os.path.join(os.getenv("HOME"),
Expand All @@ -27,7 +36,6 @@
NOT_FOUND = -1

# API endpoints
OAUTH_ENDPOINT = 'https://oauth.ring.com/oauth/token'
API_VERSION = '9'
API_URI = 'https://api.ring.com'
CHIMES_ENDPOINT = '/clients_api/chimes/{0}'
Expand Down Expand Up @@ -96,15 +104,6 @@
MSG_VOL_OUTBOUND = 'Must be within the {0}-{1}.'
MSG_ALLOWED_VALUES = 'Only the following values are allowed: {0}.'

# structure acquired from reverse engineering to create auth token
OAUTH_DATA = {
"client_id": "ring_official_android",
"grant_type": "password",
"scope": "client",
"username": None,
"password": None,
}

POST_DATA = {
'api_version': API_VERSION,
'device[hardware_id]': str(uuid()),
Expand Down
14 changes: 6 additions & 8 deletions ring_doorbell/doorbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,27 @@ def family(self):
@property
def model(self):
"""Return Ring device model name."""
# ignore R1705: Unnecessary "elif" after "return" (no-else-return)
if self.kind in DOORBELL_KINDS:
return 'Doorbell'
elif self.kind in DOORBELL_2_KINDS:
if self.kind in DOORBELL_2_KINDS:
return 'Doorbell 2'
elif self.kind in DOORBELL_PRO_KINDS:
if self.kind in DOORBELL_PRO_KINDS:
return 'Doorbell Pro'
elif self.kind in DOORBELL_ELITE_KINDS:
if self.kind in DOORBELL_ELITE_KINDS:
return 'Doorbell Elite'
elif self.kind in PEEPHOLE_CAM_KINDS:
if self.kind in PEEPHOLE_CAM_KINDS:
return 'Peephole Cam'
return None

def has_capability(self, capability):
"""Return if device has specific capability."""
# ignore R1705: Unnecessary "elif" after "return" (no-else-return)
if capability == 'battery':
return self.kind in (DOORBELL_KINDS +
DOORBELL_2_KINDS +
PEEPHOLE_CAM_KINDS)
elif capability == 'knock':
if capability == 'knock':
return self.kind in PEEPHOLE_CAM_KINDS
elif capability == 'volume':
if capability == 'volume':
return True
return False

Expand Down
3 changes: 2 additions & 1 deletion ring_doorbell/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self, ring, name, shared=False):
self.shared = shared
self._attrs = None
self._health_attrs = None
self.capability = False

# alerts notifications
self.alert_expires_at = None
Expand All @@ -47,7 +48,7 @@ def model(self):

def has_capability(self, capability):
"""Return if device has specific capability."""
return False
return self.capability

def update(self):
"""Refresh attributes."""
Expand Down
18 changes: 8 additions & 10 deletions ring_doorbell/stickup_cam.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,35 @@ def family(self):
@property
def model(self):
"""Return Ring device model name."""
# ignore R1705: Unnecessary "elif" after "return" (no-else-return)
if self.kind in FLOODLIGHT_CAM_KINDS:
return 'Floodlight Cam'
elif self.kind in INDOOR_CAM_KINDS:
if self.kind in INDOOR_CAM_KINDS:
return 'Indoor Cam'
elif self.kind in SPOTLIGHT_CAM_BATTERY_KINDS:
if self.kind in SPOTLIGHT_CAM_BATTERY_KINDS:
return 'Spotlight Cam {}'.format(
self._attrs.get('ring_cam_setup_flow', 'battery').title())
elif self.kind in SPOTLIGHT_CAM_WIRED_KINDS:
if self.kind in SPOTLIGHT_CAM_WIRED_KINDS:
return 'Spotlight Cam {}'.format(
self._attrs.get('ring_cam_setup_flow', 'wired').title())
elif self.kind in STICKUP_CAM_KINDS:
if self.kind in STICKUP_CAM_KINDS:
return 'Stick Up Cam'
elif self.kind in STICKUP_CAM_BATTERY_KINDS:
if self.kind in STICKUP_CAM_BATTERY_KINDS:
return 'Stick Up Cam Battery'
elif self.kind in STICKUP_CAM_WIRED_KINDS:
if self.kind in STICKUP_CAM_WIRED_KINDS:
return 'Stick Up Cam Wired'
return None

def has_capability(self, capability):
"""Return if device has specific capability."""
# ignore R1705: Unnecessary "elif" after "return" (no-else-return)
if capability == 'battery':
return self.kind in (SPOTLIGHT_CAM_BATTERY_KINDS +
STICKUP_CAM_KINDS +
STICKUP_CAM_BATTERY_KINDS)
elif capability == 'light':
if capability == 'light':
return self.kind in (FLOODLIGHT_CAM_KINDS +
SPOTLIGHT_CAM_BATTERY_KINDS +
SPOTLIGHT_CAM_WIRED_KINDS)
elif capability == 'siren':
if capability == 'siren':
return self.kind in (FLOODLIGHT_CAM_KINDS +
INDOOR_CAM_KINDS +
SPOTLIGHT_CAM_BATTERY_KINDS +
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""Python Ring Door Bell setup script."""
from setuptools import setup

_VERSION = '0.2.5'
_VERSION = '0.2.6'


def readme():
Expand Down
16 changes: 16 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ignore this, I am a dumb dumb and learning python, was testing with this.
from ring_doorbell import Ring
# from ring_doorbell.auth import Auth


def callback():
auth_code = input('2FA code:')
return auth_code


username = input('Username:')
password = input('Password:')
# auth = Auth()
# token = auth.fetch_token(username, password, callback)
ring = Ring(username, password, callback)
print(ring.devices)
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ skip_missing_interpreters = True

[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/ring_doorbell
PYTHONPATH = {toxinidir}/ring_doorbell
whitelist_externals = /usr/bin/env
install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages}
install_command = pip install {opts} {packages}
commands =
py.test --basetemp={envtmpdir} --cov --cov-report term-missing
deps =
Expand Down

0 comments on commit 6da218c

Please sign in to comment.