Skip to content

Commit

Permalink
Merge pull request #122 from tchellomello/0.2.3
Browse files Browse the repository at this point in the history
0.2.3
  • Loading branch information
tchellomello committed Mar 5, 2019
2 parents 9d0ecf0 + c057144 commit ae1fbd9
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 28 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Expand Up @@ -9,6 +9,10 @@ matrix:
env: TOXENV=py35
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
env: TOXENV=py37
dist: xenial
sudo: true
- python: "3.4.2"
env: TOXENV=lint
install: pip install -U tox coveralls
Expand Down
9 changes: 6 additions & 3 deletions ring_doorbell/__init__.py
Expand Up @@ -24,6 +24,7 @@
_LOGGER = logging.getLogger(__name__)


# pylint: disable=useless-object-inheritance
class Ring(object):
"""A Python Abstraction object to Ring Door Bell."""

Expand Down Expand Up @@ -159,7 +160,8 @@ def query(self,
attempts=RETRY_TOKEN,
method='GET',
raw=False,
extra_params=None):
extra_params=None,
json=None):
"""Query data from Ring API."""
if self.debug:
_LOGGER.debug("Querying %s", url)
Expand Down Expand Up @@ -189,7 +191,8 @@ def query(self,
elif method == 'PUT':
req = self.session.put((url), params=urlencode(params))
elif method == 'POST':
req = self.session.post((url), params=urlencode(params))
req = self.session.post(
(url), params=urlencode(params), json=json)

if self.debug:
_LOGGER.debug("_query %s ret %s", loop, req.status_code)
Expand All @@ -213,7 +216,7 @@ def query(self,
response = req.json()
break

if self.debug:
if self.debug and response is None:
_LOGGER.debug("%s", MSG_GENERIC_FAIL)
return response

Expand Down
20 changes: 15 additions & 5 deletions ring_doorbell/chime.py
Expand Up @@ -7,7 +7,7 @@
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)
CHIME_TEST_SOUND_KINDS, KIND_DING, CHIME_KINDS, CHIME_PRO_KINDS)

_LOGGER = logging.getLogger(__name__)

Expand All @@ -21,9 +21,19 @@ def family(self):
return 'chimes'

@property
def battery_life(self):
"""Return battery life."""
return int(self._health_attrs.get('battery_percentage'))
def model(self):
"""Return Ring device model name."""
if self.kind in CHIME_KINDS:
return 'Chime'
elif self.kind in CHIME_PRO_KINDS:
return 'Chime Pro'
return None

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

@property
def volume(self):
Expand All @@ -33,7 +43,7 @@ def volume(self):
@volume.setter
def volume(self, value):
if not ((isinstance(value, int)) and
(value >= CHIME_VOL_MIN and value <= CHIME_VOL_MAX)):
(CHIME_VOL_MIN <= value <= CHIME_VOL_MAX)):
_LOGGER.error("%s", MSG_VOL_OUTBOUND.format(CHIME_VOL_MIN,
CHIME_VOL_MAX))
return False
Expand Down
18 changes: 18 additions & 0 deletions ring_doorbell/const.py
Expand Up @@ -43,6 +43,8 @@
NEW_SESSION_ENDPOINT = '/clients_api/session'
RINGTONES_ENDPOINT = '/ringtones'
SIREN_ENDPOINT = DOORBELLS_ENDPOINT + '/siren_{1}'
SNAPSHOT_ENDPOINT = "/clients_api/snapshots/image/{0}"
SNAPSHOT_TIMESTAMP_ENDPOINT = "/clients_api/snapshots/timestamps"
TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + '/play_sound'
URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history'
URL_RECORDING = '/clients_api/dings/{0}/recording'
Expand All @@ -67,6 +69,22 @@
SIREN_DURATION_MIN = 0
SIREN_DURATION_MAX = 120

# device model kinds
CHIME_KINDS = ['chime']
CHIME_PRO_KINDS = ['chime_pro']

DOORBELL_KINDS = ['doorbot', 'doorbell', 'doorbell_v3']
DOORBELL_2_KINDS = ['doorbell_v4', 'doorbell_v5']
DOORBELL_PRO_KINDS = ['lpd_v1', 'lpd_v2']
DOORBELL_ELITE_KINDS = ['jbox_v1']

FLOODLIGHT_CAM_KINDS = ['hp_cam_v1']
SPOTLIGHT_CAM_BATTERY_KINDS = ['stickup_cam_v4']
SPOTLIGHT_CAM_WIRED_KINDS = ['hp_cam_v2']
STICKUP_CAM_KINDS = ['stickup_cam', 'stickup_cam_v3']
STICKUP_CAM_BATTERY_KINDS = ['stickup_cam_lunar']
STICKUP_CAM_WIRED_KINDS = ['stickup_cam_elite']

# error strings
MSG_BOOLEAN_REQUIRED = "Boolean value is required."
MSG_EXISTING_TYPE = "Integer value where {0}.".format(DOORBELL_EXISTING_TYPE)
Expand Down
69 changes: 56 additions & 13 deletions ring_doorbell/doorbot.py
Expand Up @@ -4,6 +4,7 @@
import logging
from datetime import datetime
import os
import time
import pytz


Expand All @@ -12,9 +13,12 @@
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)
DOORBELL_EXISTING_TYPE, DINGS_ENDPOINT, DOORBELL_KINDS,
DOORBELL_2_KINDS, DOORBELL_PRO_KINDS, DOORBELL_ELITE_KINDS,
FILE_EXISTS, LIVE_STREAMING_ENDPOINT, MSG_BOOLEAN_REQUIRED,
MSG_EXISTING_TYPE, MSG_VOL_OUTBOUND, SNAPSHOT_ENDPOINT,
SNAPSHOT_TIMESTAMP_ENDPOINT, URL_DOORBELL_HISTORY,
URL_RECORDING)

_LOGGER = logging.getLogger(__name__)

Expand All @@ -27,12 +31,34 @@ def family(self):
"""Return Ring device family type."""
return 'doorbots'

@property
def model(self):
"""Return Ring device model name."""
if self.kind in DOORBELL_KINDS:
return 'Doorbell'
elif self.kind in DOORBELL_2_KINDS:
return 'Doorbell 2'
elif self.kind in DOORBELL_PRO_KINDS:
return 'Doorbell Pro'
elif self.kind in DOORBELL_ELITE_KINDS:
return 'Doorbell Elite'
return None

def has_capability(self, capability):
"""Return if device has specific capability."""
if capability == 'battery':
return self.kind in (DOORBELL_KINDS +
DOORBELL_2_KINDS)
elif capability == 'volume':
return True
return False

@property
def battery_life(self):
"""Return battery life."""
value = 0
if 'battery_life_2' in self._attrs:
# Camera has two battery bays
value = 0
if self._attrs.get('battery_life') is not None:
# Bay 1
value += int(self._attrs.get('battery_life'))
Expand All @@ -41,9 +67,11 @@ def battery_life(self):
value += int(self._attrs.get('battery_life_2'))
return value
# Camera has a single battery bay
value = int(self._attrs.get('battery_life'))
if value and value > 100:
value = 100
# Latest stickup cam can be externally powered
if self._attrs.get('battery_life') is not None:
value = int(self._attrs.get('battery_life'))
if value and value > 100:
value = 100
return value

def check_alerts(self):
Expand Down Expand Up @@ -150,7 +178,7 @@ def existing_doorbell_type_duration(self, value):
if self.existing_doorbell_type:

if not ((isinstance(value, int)) and
(value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)):
(DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX)):
_LOGGER.error("%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN,
DOORBELL_VOL_MAX))
return False
Expand Down Expand Up @@ -259,8 +287,8 @@ def live_streaming_json(self):
def recording_download(self, recording_id, filename=None, override=False):
"""Save a recording in MP4 format to a file or return raw."""
if not self.has_subscription:
_LOGGER.warning("Your Ring account does not have" +
" an active subscription.")
msg = "Your Ring account does not have an active subscription."
_LOGGER.warning(msg)
return False

url = API_URI + URL_RECORDING.format(recording_id)
Expand All @@ -286,8 +314,8 @@ def recording_download(self, recording_id, filename=None, override=False):
def recording_url(self, recording_id):
"""Return HTTPS recording URL."""
if not self.has_subscription:
_LOGGER.warning("Your Ring account does not have" +
" an active subscription.")
msg = "Your Ring account does not have an active subscription."
_LOGGER.warning(msg)
return False

url = API_URI + URL_RECORDING.format(recording_id)
Expand Down Expand Up @@ -325,7 +353,7 @@ def volume(self):
@volume.setter
def volume(self, value):
if not ((isinstance(value, int)) and
(value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)):
(DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX)):
_LOGGER.error("%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN,
DOORBELL_VOL_MAX))
return False
Expand All @@ -342,3 +370,18 @@ def volume(self, value):
def connection_status(self):
"""Return connection status."""
return self._attrs.get('alerts').get('connection')

def get_snapshot(self, retries=3, delay=1):
"""Take a snapshot and download it"""
url = API_URI + SNAPSHOT_TIMESTAMP_ENDPOINT
payload = {"doorbot_ids": [self._attrs.get('id')]}
self._ring.query(url, json=payload)
request_time = time.time()
for _ in range(retries):
time.sleep(delay)
response = self._ring.query(
url, method="POST", json=payload, raw=1).json()
if response["timestamps"][0]["timestamp"] / 1000 > request_time:
return self._ring.query(API_URI + SNAPSHOT_ENDPOINT.format(
self._attrs.get('id')), raw=True).content
return False
10 changes: 10 additions & 0 deletions ring_doorbell/generic.py
Expand Up @@ -12,6 +12,7 @@
_LOGGER = logging.getLogger(__name__)


# pylint: disable=useless-object-inheritance
class RingGeneric(object):
"""Generic Implementation for Ring Chime/Doorbell."""

Expand Down Expand Up @@ -39,6 +40,15 @@ def family(self):
"""Return Ring device family type."""
return None

@property
def model(self):
"""Return Ring device model name."""
return None

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

def update(self):
"""Refresh attributes."""
self._get_attrs()
Expand Down
43 changes: 41 additions & 2 deletions ring_doorbell/stickup_cam.py
Expand Up @@ -6,6 +6,9 @@
from ring_doorbell import RingDoorBell
from ring_doorbell.const import (
API_URI, LIGHTS_ENDPOINT, MSG_ALLOWED_VALUES, MSG_VOL_OUTBOUND,
FLOODLIGHT_CAM_KINDS, SPOTLIGHT_CAM_BATTERY_KINDS,
SPOTLIGHT_CAM_WIRED_KINDS, STICKUP_CAM_KINDS,
STICKUP_CAM_BATTERY_KINDS, STICKUP_CAM_WIRED_KINDS,
SIREN_DURATION_MIN, SIREN_DURATION_MAX, SIREN_ENDPOINT)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -19,6 +22,43 @@ def family(self):
"""Return Ring device family type."""
return 'stickup_cams'

@property
def model(self):
"""Return Ring device model name."""
if self.kind in FLOODLIGHT_CAM_KINDS:
return 'Floodlight Cam'
elif 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:
return 'Spotlight Cam {}'.format(
self._attrs.get('ring_cam_setup_flow', 'wired').title())
elif self.kind in STICKUP_CAM_KINDS:
return 'Stick Up Cam'
elif self.kind in STICKUP_CAM_BATTERY_KINDS:
return 'Stick Up Cam Battery'
elif 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."""
if capability == 'battery':
return self.kind in (SPOTLIGHT_CAM_BATTERY_KINDS +
STICKUP_CAM_KINDS +
STICKUP_CAM_BATTERY_KINDS)
elif capability == 'light':
return self.kind in (FLOODLIGHT_CAM_KINDS +
SPOTLIGHT_CAM_BATTERY_KINDS +
SPOTLIGHT_CAM_WIRED_KINDS)
elif capability == 'siren':
return self.kind in (FLOODLIGHT_CAM_KINDS +
SPOTLIGHT_CAM_BATTERY_KINDS +
SPOTLIGHT_CAM_WIRED_KINDS +
STICKUP_CAM_BATTERY_KINDS +
STICKUP_CAM_WIRED_KINDS)
return False

@property
def lights(self):
"""Return lights status."""
Expand Down Expand Up @@ -48,8 +88,7 @@ def siren(self):
def siren(self, duration):
"""Control the siren."""
if not ((isinstance(duration, int)) and
(duration >= SIREN_DURATION_MIN and
duration <= SIREN_DURATION_MAX)):
(SIREN_DURATION_MIN <= duration <= SIREN_DURATION_MAX)):
_LOGGER.error("%s", MSG_VOL_OUTBOUND.format(SIREN_DURATION_MIN,
SIREN_DURATION_MAX))
return False
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Expand Up @@ -2,6 +2,8 @@
"""Python Ring Door Bell setup script."""
from setuptools import setup

_VERSION = '0.2.3'


def readme():
with open('README.rst') as desc:
Expand All @@ -11,7 +13,7 @@ def readme():
setup(
name='ring_doorbell',
packages=['ring_doorbell'],
version='0.2.2',
version=_VERSION,
description='A Python library to communicate with Ring' +
' Door Bell (https://ring.com/)',
long_description=readme(),
Expand All @@ -37,6 +39,7 @@ def readme():
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Home Automation',
'Topic :: Software Development :: Libraries :: Python Modules'
],
Expand Down

0 comments on commit ae1fbd9

Please sign in to comment.