Compare commits
26
Commits
v0.13.0
...
v0.14.0.dev1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
683650e2c2 | ||
|
|
50b1a35168 | ||
|
|
b88a5feddf | ||
|
|
93472e38de | ||
|
|
85c14ede8d | ||
|
|
58ce109518 | ||
|
|
d46e7ed96d | ||
|
|
00395b3825 | ||
|
|
f67f0dd6a2 | ||
|
|
d90cd3309f | ||
|
|
e836a6fa58 | ||
|
|
92d89b9fe5 | ||
|
|
c10fb432f7 | ||
|
|
37b729b597 | ||
|
|
749c68bb8a | ||
|
|
dcf0ce6394 | ||
|
|
102190e61b | ||
|
|
a7340c97ca | ||
|
|
43c1162634 | ||
|
|
4db7a33ef3 | ||
|
|
bfdc1e47bd | ||
|
|
5511af6244 | ||
|
|
d629d9fa3a | ||
|
|
3e22d83962 | ||
|
|
57b05daad7 | ||
|
|
419eb51b66 |
@@ -3,6 +3,10 @@ Changelog
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.13.1 (2019-03-01)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Remove throttle decorator from network status request
|
||||
|
||||
0.13.0 (2019-03-01)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
**Breaking change:**
|
||||
|
||||
@@ -2,6 +2,10 @@ blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
|
||||
================================================================================
|
||||
A Python library for the Blink Camera system
|
||||
|
||||
Like the library? Consider buying me a cup of coffee!
|
||||
|
||||
|Donate|
|
||||
|
||||
Disclaimer:
|
||||
~~~~~~~~~~~~~~~
|
||||
Published under the MIT license - See LICENSE file for more details.
|
||||
@@ -106,3 +110,6 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
|
||||
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
|
||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
||||
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
||||
|
||||
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
|
||||
|
||||
+2
-5
@@ -40,7 +40,6 @@ def request_networks(blink):
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_network_status(blink, network):
|
||||
"""
|
||||
Request network information.
|
||||
@@ -52,7 +51,6 @@ def request_network_status(blink, network):
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_syncmodule(blink, network):
|
||||
"""
|
||||
Request sync module info.
|
||||
@@ -168,12 +166,11 @@ def request_videos(blink, time=None, page=0):
|
||||
:param page: Page number to get videos from.
|
||||
"""
|
||||
timestamp = get_time(time)
|
||||
url = "{}/api/v2/videos/changed?since={}&page={}".format(
|
||||
blink.urls.base_url, timestamp, page)
|
||||
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
|
||||
blink.urls.base_url, blink.account_id, timestamp, page)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_cameras(blink, network):
|
||||
"""
|
||||
Request all camera information.
|
||||
|
||||
+34
-22
@@ -21,6 +21,7 @@ from shutil import copyfileobj
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from dateutil.parser import parse
|
||||
from slugify import slugify
|
||||
|
||||
from blinkpy import api
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
@@ -29,14 +30,10 @@ from blinkpy.helpers.util import (
|
||||
create_session, merge_dicts, get_time, BlinkURLHandler,
|
||||
BlinkAuthenticationException, Throttle)
|
||||
from blinkpy.helpers.constants import (
|
||||
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL)
|
||||
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
|
||||
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
|
||||
from blinkpy.helpers.constants import __version__
|
||||
|
||||
REFRESH_RATE = 30
|
||||
|
||||
# Prevents rapid calls to blink.refresh()
|
||||
# with the force_cache flag set to True
|
||||
MIN_THROTTLE_TIME = 2
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,7 +42,8 @@ class Blink():
|
||||
"""Class to initialize communication."""
|
||||
|
||||
def __init__(self, username=None, password=None,
|
||||
refresh_rate=REFRESH_RATE):
|
||||
refresh_rate=DEFAULT_REFRESH,
|
||||
motion_interval=DEFAULT_MOTION_INTERVAL):
|
||||
"""
|
||||
Initialize Blink system.
|
||||
|
||||
@@ -53,6 +51,10 @@ class Blink():
|
||||
:param password: Blink password
|
||||
:param refresh_rate: Refresh rate of blink information.
|
||||
Defaults to 15 (seconds)
|
||||
:param motion_interval: How far back to register motion in minutes.
|
||||
Defaults to last refresh time.
|
||||
Useful for preventing motion_detected property
|
||||
from de-asserting too quickly.
|
||||
"""
|
||||
self._username = username
|
||||
self._password = password
|
||||
@@ -72,6 +74,7 @@ class Blink():
|
||||
self.cameras = CaseInsensitiveDict({})
|
||||
self.video_list = CaseInsensitiveDict({})
|
||||
self._login_url = LOGIN_URL
|
||||
self.motion_interval = DEFAULT_MOTION_INTERVAL
|
||||
self.version = __version__
|
||||
|
||||
@property
|
||||
@@ -247,7 +250,8 @@ class Blink():
|
||||
combined = merge_dicts(combined, self.sync[sync].cameras)
|
||||
return combined
|
||||
|
||||
def download_videos(self, path, since=None, camera='all', stop=10):
|
||||
def download_videos(self, path, since=None,
|
||||
camera='all', stop=10, debug=False):
|
||||
"""
|
||||
Download all videos from server since specified time.
|
||||
|
||||
@@ -258,6 +262,8 @@ class Blink():
|
||||
:param camera: Camera name to retrieve. Defaults to "all".
|
||||
Use a list for multiple cameras.
|
||||
:param stop: Page to stop on (~25 items per page. Default page 10).
|
||||
:param debug: Set to TRUE to prevent downloading of items.
|
||||
Instead of downloading, entries will be printed to log.
|
||||
"""
|
||||
if since is None:
|
||||
since_epochs = self.last_refresh
|
||||
@@ -275,23 +281,23 @@ class Blink():
|
||||
response = api.request_videos(self, time=since_epochs, page=page)
|
||||
_LOGGER.debug("Processing page %s", page)
|
||||
try:
|
||||
result = response['videos']
|
||||
result = response['media']
|
||||
if not result:
|
||||
raise IndexError
|
||||
except (KeyError, IndexError):
|
||||
_LOGGER.info("No videos found on page %s. Exiting.", page)
|
||||
break
|
||||
|
||||
self._parse_downloaded_items(result, camera, path)
|
||||
self._parse_downloaded_items(result, camera, path, debug)
|
||||
|
||||
def _parse_downloaded_items(self, result, camera, path):
|
||||
def _parse_downloaded_items(self, result, camera, path, debug):
|
||||
"""Parse downloaded videos."""
|
||||
for item in result:
|
||||
try:
|
||||
created_at = item['created_at']
|
||||
camera_name = item['camera_name']
|
||||
camera_name = item['device_name']
|
||||
is_deleted = item['deleted']
|
||||
address = item['address']
|
||||
address = item['media']
|
||||
except KeyError:
|
||||
_LOGGER.info("Missing clip information, skipping...")
|
||||
continue
|
||||
@@ -307,16 +313,22 @@ class Blink():
|
||||
continue
|
||||
|
||||
clip_address = "{}{}".format(self.urls.base_url, address)
|
||||
filename = "{}_{}.mp4".format(camera_name, created_at)
|
||||
filename = "{}-{}".format(camera_name, created_at)
|
||||
filename = "{}.mp4".format(slugify(filename))
|
||||
filename = os.path.join(path, filename)
|
||||
|
||||
if os.path.isfile(filename):
|
||||
_LOGGER.info("%s already exists, skipping...", filename)
|
||||
continue
|
||||
if not debug:
|
||||
if os.path.isfile(filename):
|
||||
_LOGGER.info("%s already exists, skipping...", filename)
|
||||
continue
|
||||
|
||||
response = api.http_get(self, url=clip_address,
|
||||
stream=True, json=False)
|
||||
with open(filename, 'wb') as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
response = api.http_get(self, url=clip_address,
|
||||
stream=True, json=False)
|
||||
with open(filename, 'wb') as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
_LOGGER.info("Downloaded video to %s", filename)
|
||||
_LOGGER.info("Downloaded video to %s", filename)
|
||||
else:
|
||||
print(("Camera: {}, Timestamp: {}, "
|
||||
"Address: {}, Filename: {}").format(
|
||||
camera_name, created_at, address, filename))
|
||||
|
||||
+3
-2
@@ -41,6 +41,7 @@ class BlinkCamera():
|
||||
'temperature_c': self.temperature_c,
|
||||
'temperature_calibrated': self.temperature_calibrated,
|
||||
'battery': self.battery,
|
||||
'battery_voltage': self.battery_voltage,
|
||||
'thumbnail': self.thumbnail,
|
||||
'video': self.clip,
|
||||
'motion_enabled': self.motion_enabled,
|
||||
@@ -54,8 +55,8 @@ class BlinkCamera():
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
"""Return battery level as percentage."""
|
||||
return round(self.battery_voltage / 180 * 100)
|
||||
"""Return battery as string."""
|
||||
return self.battery_state
|
||||
|
||||
@property
|
||||
def temperature_c(self):
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 13
|
||||
PATCH_VERSION = 0
|
||||
MINOR_VERSION = 14
|
||||
PATCH_VERSION = '0.dev1'
|
||||
|
||||
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||
|
||||
@@ -60,3 +60,7 @@ ONLINE = {'online': True, 'offline': False}
|
||||
OTHER
|
||||
'''
|
||||
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%Z'
|
||||
|
||||
DEFAULT_MOTION_INTERVAL = 1
|
||||
DEFAULT_REFRESH = 30
|
||||
MIN_THROTTLE_TIME = 2
|
||||
|
||||
@@ -15,7 +15,7 @@ def get_time(time_to_convert=None):
|
||||
"""Create blink-compatible timestamp."""
|
||||
if time_to_convert is None:
|
||||
time_to_convert = time.time()
|
||||
return time.strftime(TIMESTAMP_FORMAT, time.localtime(time_to_convert))
|
||||
return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
|
||||
|
||||
|
||||
def merge_dicts(dict_a, dict_b):
|
||||
|
||||
+18
-8
@@ -33,6 +33,7 @@ class BlinkSyncModule():
|
||||
self.network_info = None
|
||||
self.events = []
|
||||
self.cameras = CaseInsensitiveDict({})
|
||||
self.motion_interval = blink.motion_interval
|
||||
self.motion = {}
|
||||
self.last_record = {}
|
||||
self.camera_list = camera_list
|
||||
@@ -64,7 +65,10 @@ class BlinkSyncModule():
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return status of sync module: armed/disarmed."""
|
||||
return self.network_info['network']['armed']
|
||||
try:
|
||||
return self.network_info['network']['armed']
|
||||
except (KeyError, TypeError):
|
||||
return None
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
@@ -77,8 +81,7 @@ class BlinkSyncModule():
|
||||
def start(self):
|
||||
"""Initialize the system."""
|
||||
response = api.request_syncmodule(self.blink,
|
||||
self.network_id,
|
||||
force=True)
|
||||
self.network_id)
|
||||
try:
|
||||
self.summary = response['syncmodule']
|
||||
self.network_id = self.summary['network_id']
|
||||
@@ -159,23 +162,30 @@ class BlinkSyncModule():
|
||||
|
||||
def check_new_videos(self):
|
||||
"""Check if new videos since last refresh."""
|
||||
try:
|
||||
interval = self.blink.last_refresh - self.motion_interval*60
|
||||
except TypeError:
|
||||
# This is the first start, so refresh hasn't happened yet.
|
||||
# No need to check for motion.
|
||||
return False
|
||||
|
||||
resp = api.request_videos(self.blink,
|
||||
time=self.blink.last_refresh,
|
||||
page=0)
|
||||
time=interval,
|
||||
page=1)
|
||||
|
||||
for camera in self.cameras.keys():
|
||||
self.motion[camera] = False
|
||||
|
||||
try:
|
||||
info = resp['videos']
|
||||
info = resp['media']
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.warning("Could not check for motion. Response: %s", resp)
|
||||
return False
|
||||
|
||||
for entry in info:
|
||||
try:
|
||||
name = entry['camera_name']
|
||||
clip = entry['address']
|
||||
name = entry['device_name']
|
||||
clip = entry['media']
|
||||
timestamp = entry['created_at']
|
||||
self.motion[name] = True
|
||||
self.last_record[name] = {'clip': clip, 'time': timestamp}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
python-dateutil==2.7.5
|
||||
requests>=2.20.0
|
||||
python-slugify==3.0.2
|
||||
testtools==2.3.0
|
||||
|
||||
@@ -121,12 +121,12 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
blinkpy._LOGGER.setLevel(logging.DEBUG)
|
||||
generic_entry = {
|
||||
'created_at': '1970',
|
||||
'camera_name': 'foo',
|
||||
'device_name': 'foo',
|
||||
'deleted': True,
|
||||
'address': '/bar.mp4'
|
||||
'media': '/bar.mp4'
|
||||
}
|
||||
result = [generic_entry]
|
||||
mock_req.return_value = {'videos': result}
|
||||
mock_req.return_value = {'media': result}
|
||||
blink.last_refresh = 0
|
||||
formatted_date = get_time(blink.last_refresh)
|
||||
expected_log = [
|
||||
@@ -147,12 +147,12 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
blinkpy._LOGGER.setLevel(logging.DEBUG)
|
||||
generic_entry = {
|
||||
'created_at': '1970',
|
||||
'camera_name': 'foo',
|
||||
'device_name': 'foo',
|
||||
'deleted': True,
|
||||
'address': '/bar.mp4'
|
||||
'media': '/bar.mp4'
|
||||
}
|
||||
result = [generic_entry]
|
||||
mock_req.return_value = {'videos': result}
|
||||
mock_req.return_value = {'media': result}
|
||||
blink.last_refresh = 0
|
||||
formatted_date = get_time(blink.last_refresh)
|
||||
expected_log = [
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
self.assertEqual(self.camera.network_id, '5678')
|
||||
self.assertEqual(self.camera.serial, '12345678')
|
||||
self.assertEqual(self.camera.motion_enabled, False)
|
||||
self.assertEqual(self.camera.battery, 50)
|
||||
self.assertEqual(self.camera.battery, 'ok')
|
||||
self.assertEqual(self.camera.temperature, 68)
|
||||
self.assertEqual(self.camera.temperature_c, 20)
|
||||
self.assertEqual(self.camera.temperature_calibrated, 71)
|
||||
|
||||
@@ -17,12 +17,14 @@ class TestBlinkSyncModule(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
password=PASSWORD,
|
||||
motion_interval=0)
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = {
|
||||
'Host': 'test.url.tld',
|
||||
'TOKEN_AUTH': 'foobar123'
|
||||
}
|
||||
self.blink.last_refresh = 0
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.blink.sync['test'] = BlinkSyncModule(self.blink,
|
||||
'test',
|
||||
@@ -59,12 +61,18 @@ class TestBlinkSyncModule(unittest.TestCase):
|
||||
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
|
||||
'foobar')
|
||||
|
||||
def test_check_new_videos_startup(self, mock_resp):
|
||||
"""Test that check_new_videos does not block startup."""
|
||||
sync_module = self.blink.sync['test']
|
||||
self.blink.last_refresh = None
|
||||
self.assertFalse(sync_module.check_new_videos())
|
||||
|
||||
def test_check_new_videos(self, mock_resp):
|
||||
"""Test recent video response."""
|
||||
mock_resp.return_value = {
|
||||
'videos': [{
|
||||
'camera_name': 'foo',
|
||||
'address': '/foo/bar.mp4',
|
||||
'media': [{
|
||||
'device_name': 'foo',
|
||||
'media': '/foo/bar.mp4',
|
||||
'created_at': '1970-01-01T00:00:00+0:00'
|
||||
}]
|
||||
}
|
||||
@@ -76,7 +84,7 @@ class TestBlinkSyncModule(unittest.TestCase):
|
||||
{'clip': '/foo/bar.mp4',
|
||||
'time': '1970-01-01T00:00:00+0:00'})
|
||||
self.assertEqual(sync_module.motion, {'foo': True})
|
||||
mock_resp.return_value = {'videos': []}
|
||||
mock_resp.return_value = {'media': []}
|
||||
self.assertTrue(sync_module.check_new_videos())
|
||||
self.assertEqual(sync_module.motion, {'foo': False})
|
||||
self.assertEqual(sync_module.last_record['foo'],
|
||||
|
||||
Reference in New Issue
Block a user