Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37fc047174 | ||
|
|
6343965bbc | ||
|
|
411da60bb2 | ||
|
|
72c36d8381 | ||
|
|
b0974c1ab2 | ||
|
|
a76eac085a | ||
|
|
5319714a37 | ||
|
|
5d9105655f | ||
|
|
b98c03b96b | ||
|
|
ec703ae4b0 | ||
|
|
9ce09b4761 | ||
|
|
6d002fc8b0 | ||
|
|
7228340677 | ||
|
|
9335a37feb | ||
|
|
371348533f | ||
|
|
26f6defe60 | ||
|
|
52bc4af1f0 | ||
|
|
8c94305804 | ||
|
|
0eb57698bd | ||
|
|
7b0db80edd | ||
|
|
0e63e97a57 | ||
|
|
e4a21964d2 | ||
|
|
39ad37f23b | ||
|
|
34bc61e1c3 | ||
|
|
e6a7c367b3 | ||
|
|
66afd33b79 | ||
|
|
cf617d720e | ||
|
|
4f2eca08e0 | ||
|
|
2972780c18 | ||
|
|
9c3f3f2f98 | ||
|
|
0d83a7fcb1 | ||
|
|
80f4ca7c4f | ||
|
|
96e7fce304 | ||
|
|
13a358c07c | ||
|
|
389745533c | ||
|
|
65dd547a32 | ||
|
|
91a877da79 | ||
|
|
d357215e0e |
@@ -1,3 +1,4 @@
|
||||
.pytest_cache/*
|
||||
.cache/*
|
||||
.tox/*
|
||||
__pycache__/*
|
||||
|
||||
+10
@@ -3,6 +3,16 @@ Changelog
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.9.0.dev (Development Version)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
0.8.0 (2018-05-21)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`_)
|
||||
- Added motion detection per camera
|
||||
- Added fully accessible camera configuration dict
|
||||
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`_)
|
||||
|
||||
0.7.1 (2018-05-09)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`_)
|
||||
|
||||
+93
-13
@@ -26,6 +26,8 @@ from blinkpy.helpers.constants import (
|
||||
|
||||
_LOGGER = logging.getLogger('blinkpy')
|
||||
|
||||
MAX_CLIPS = 5
|
||||
|
||||
|
||||
def _attempt_reauthorization(blink):
|
||||
"""Attempt to refresh auth token and links."""
|
||||
@@ -79,7 +81,7 @@ class BlinkAuthenticationException(BlinkException):
|
||||
pass
|
||||
|
||||
|
||||
class BlinkURLHandler(object):
|
||||
class BlinkURLHandler():
|
||||
"""Class that handles Blink URLS."""
|
||||
|
||||
def __init__(self, region_id):
|
||||
@@ -93,7 +95,7 @@ class BlinkURLHandler(object):
|
||||
_LOGGER.debug("Setting base url to %s.", self.base_url)
|
||||
|
||||
|
||||
class BlinkCamera(object):
|
||||
class BlinkCamera():
|
||||
"""Class to initialize individual camera."""
|
||||
|
||||
def __init__(self, config, blink):
|
||||
@@ -107,13 +109,19 @@ class BlinkCamera(object):
|
||||
config['thumbnail'])
|
||||
self.clip = "{}{}".format(self.urls.base_url, config['video'])
|
||||
self.temperature = config['temp']
|
||||
self.battery = config['battery']
|
||||
self._battery_string = config['battery']
|
||||
self.notifications = config['notifications']
|
||||
self.motion = {}
|
||||
self.motion = dict()
|
||||
self.header = None
|
||||
self.image_link = None
|
||||
self.arm_link = None
|
||||
self.region_id = config['region_id']
|
||||
self.battery_voltage = -180
|
||||
self.motion_detected = None
|
||||
self.wifi_strength = None
|
||||
self.camera_config = dict()
|
||||
self.motion_enabled = None
|
||||
self.last_record = list()
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
@@ -124,11 +132,16 @@ class BlinkCamera(object):
|
||||
'status': self._status,
|
||||
'armed': self.armed,
|
||||
'temperature': self.temperature,
|
||||
'temperature_c': self.temperature_c,
|
||||
'battery': self.battery,
|
||||
'thumbnail': self.thumbnail,
|
||||
'video': self.clip,
|
||||
'motion_enabled': self.motion_enabled,
|
||||
'notifications': self.notifications,
|
||||
'network_id': self.blink.network_id
|
||||
'motion_detected': self.motion_detected,
|
||||
'wifi_strength': self.wifi_strength,
|
||||
'network_id': self.blink.network_id,
|
||||
'last_record': self.last_record
|
||||
}
|
||||
return attributes
|
||||
|
||||
@@ -142,16 +155,26 @@ class BlinkCamera(object):
|
||||
"""Return camera arm status."""
|
||||
return True if self._status == 'armed' else False
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
"""Return battery level as percentage."""
|
||||
return round(self.battery_voltage / 180 * 100)
|
||||
|
||||
@property
|
||||
def battery_string(self):
|
||||
"""Return string indicating battery status."""
|
||||
status = "Unknown"
|
||||
if self.battery > 1 and self.battery <= 3:
|
||||
if self._battery_string > 1 and self._battery_string <= 3:
|
||||
status = "OK"
|
||||
elif self.battery >= 0:
|
||||
elif self._battery_string >= 0:
|
||||
status = "Low"
|
||||
return status
|
||||
|
||||
@property
|
||||
def temperature_c(self):
|
||||
"""Return temperature in celcius."""
|
||||
return round((self.temperature - 32) / 9.0 * 5.0, 1)
|
||||
|
||||
def snap_picture(self):
|
||||
"""Take a picture with camera to create a new thumbnail."""
|
||||
_request(self.blink, url=self.image_link,
|
||||
@@ -171,14 +194,49 @@ class BlinkCamera(object):
|
||||
"""Update camera information."""
|
||||
self.name = values['name']
|
||||
self._status = values['active']
|
||||
self.thumbnail = "{}{}.jpg".format(
|
||||
self.urls.base_url, values['thumbnail'])
|
||||
self.clip = "{}{}".format(
|
||||
self.urls.base_url, values['video'])
|
||||
self.temperature = values['temp']
|
||||
self.battery = values['battery']
|
||||
self.thumbnail = "{}{}.jpg".format(
|
||||
self.urls.base_url, values['thumbnail'])
|
||||
self._battery_string = values['battery']
|
||||
self.notifications = values['notifications']
|
||||
|
||||
try:
|
||||
cfg = self.blink.camera_config_request(self.id)
|
||||
self.camera_config = cfg
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.warning("Could not get config for %s with id %s",
|
||||
self.name, self.id)
|
||||
_LOGGER.warning("Exception raised: %s", err)
|
||||
|
||||
try:
|
||||
self.battery_voltage = cfg['camera'][0]['battery_voltage']
|
||||
self.motion_enabled = cfg['camera'][0]['motion_alert']
|
||||
self.wifi_strength = cfg['camera'][0]['wifi_strength']
|
||||
self.temperature = cfg['camera'][0]['temperature']
|
||||
except KeyError:
|
||||
_LOGGER.warning("Problem extracting config for camera %s",
|
||||
self.name)
|
||||
|
||||
# Check if the most recent clip is included in the last_record list
|
||||
# and that the last_record list is populated
|
||||
try:
|
||||
records = sorted(self.blink.record_dates[self.name])
|
||||
new_clip = records.pop()
|
||||
if new_clip not in self.last_record and self.last_record:
|
||||
self.motion_detected = True
|
||||
self.last_record.insert(0, new_clip)
|
||||
if len(self.last_record) > MAX_CLIPS:
|
||||
self.last_record.pop()
|
||||
elif not self.last_record:
|
||||
self.last_record.insert(0, new_clip)
|
||||
self.motion_detected = False
|
||||
else:
|
||||
self.motion_detected = False
|
||||
except KeyError:
|
||||
_LOGGER.warning("Could not extract clip info from camera %s",
|
||||
self.name)
|
||||
|
||||
def image_refresh(self):
|
||||
"""Refresh current thumbnail."""
|
||||
url = self.urls.home_url
|
||||
@@ -200,11 +258,15 @@ class BlinkCamera(object):
|
||||
"""Write image to file."""
|
||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||
thumb = self.image_refresh()
|
||||
if not thumb:
|
||||
thumb = self.thumbnail
|
||||
response = _request(self.blink, url=thumb, headers=self.header,
|
||||
reqtype='get', stream=True, json_resp=False)
|
||||
if response.status_code == 200:
|
||||
with open(path, 'wb') as imgfile:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
else:
|
||||
print(response)
|
||||
|
||||
def video_to_file(self, path):
|
||||
"""Write video to file."""
|
||||
@@ -215,7 +277,7 @@ class BlinkCamera(object):
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
|
||||
class Blink(object):
|
||||
class Blink():
|
||||
"""Class to initialize communication and sync module."""
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
@@ -236,6 +298,7 @@ class Blink(object):
|
||||
self._video_count = 0
|
||||
self._all_videos = {}
|
||||
self._summary = None
|
||||
self.record_dates = dict()
|
||||
|
||||
@property
|
||||
def camera_thumbs(self):
|
||||
@@ -315,11 +378,11 @@ class Blink(object):
|
||||
camera.update(element)
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_videos(self, start_page=0, end_page=1):
|
||||
"""Retrieve last recorded videos per camera."""
|
||||
videos = list()
|
||||
all_dates = dict()
|
||||
for page_num in range(start_page, end_page + 1):
|
||||
this_page = self._video_request(page_num)
|
||||
if not this_page:
|
||||
@@ -327,10 +390,17 @@ class Blink(object):
|
||||
videos.append(this_page)
|
||||
|
||||
for page in videos:
|
||||
_LOGGER.debug("Retrieved video page %s", page)
|
||||
for entry in page:
|
||||
camera_name = entry['camera_name']
|
||||
clip_addr = entry['address']
|
||||
thumb_addr = entry['thumbnail']
|
||||
clip_date = clip_addr.split('_')[-6:]
|
||||
clip_date = '_'.join(clip_date)
|
||||
clip_date = clip_date.split('.')[0]
|
||||
if camera_name not in all_dates:
|
||||
all_dates[camera_name] = list()
|
||||
all_dates[camera_name].append(clip_date)
|
||||
try:
|
||||
self._all_videos[camera_name].append(
|
||||
{
|
||||
@@ -345,6 +415,7 @@ class Blink(object):
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
]
|
||||
self.record_dates = all_dates
|
||||
|
||||
def get_cameras(self):
|
||||
"""Find and creates cameras."""
|
||||
@@ -365,6 +436,7 @@ class Blink(object):
|
||||
device = BlinkCamera(element, self)
|
||||
self.cameras[device.name] = device
|
||||
self._idlookup[device.id] = device.name
|
||||
self.refresh()
|
||||
|
||||
def set_links(self):
|
||||
"""Set access links and required headers for each camera in system."""
|
||||
@@ -497,3 +569,11 @@ class Blink(object):
|
||||
self.network_id)
|
||||
headers = self._auth_header
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
|
||||
def camera_config_request(self, camera_id):
|
||||
"""Retrieve more info about Blink config."""
|
||||
url = "{}/network/{}/camera/{}/config".format(self.urls.base_url,
|
||||
self.network_id,
|
||||
str(camera_id))
|
||||
headers = self._auth_header
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 7
|
||||
PATCH_VERSION = 1
|
||||
MINOR_VERSION = 8
|
||||
PATCH_VERSION = 3
|
||||
|
||||
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ reports=no
|
||||
# unused-argument - generic callbacks and setup methods create a lot of warnings
|
||||
# too-many-* - are not enforced for the sake of readability
|
||||
# too-few-* - same as too-many-*
|
||||
# no-else-return - I don't see any reason to enforce this. both forms are readable
|
||||
|
||||
disable=
|
||||
locally-disabled,
|
||||
@@ -20,4 +21,5 @@ disable=
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
too-many-lines,
|
||||
too-few-public-methods,
|
||||
too-few-public-methods,
|
||||
no-else-return,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
flake8==3.3
|
||||
flake8-docstrings==1.1.0
|
||||
pylint==1.8.1
|
||||
pydocstyle==2.0.0
|
||||
pytest==3.3.1
|
||||
flake8==3.5.0
|
||||
flake8-docstrings==1.3.0
|
||||
pylint==2.1.1
|
||||
pydocstyle==2.1.1
|
||||
pytest==3.7.1
|
||||
pytest-cov>=2.3.1
|
||||
pytest-sugar>=0.9.0
|
||||
pytest-timeout>=1.0.0
|
||||
restructuredtext-lint>=1.0.1
|
||||
pygments>=2.2.0
|
||||
pygments>=2.2.0
|
||||
@@ -39,7 +39,7 @@ def mocked_requests_post(*args, **kwargs):
|
||||
response_to_return = {'message': 'Error', 'code': 404}
|
||||
code_to_return = 404
|
||||
|
||||
if url_arg == const.LOGIN_URL or url_arg == const.LOGIN_BACKUP_URL:
|
||||
if url_arg in (const.LOGIN_URL, const.LOGIN_BACKUP_URL):
|
||||
response_to_return = LOGIN_RESPONSE
|
||||
code_to_return = 200
|
||||
elif url_arg is not None:
|
||||
@@ -77,6 +77,7 @@ def mocked_requests_get(*args, **kwargs):
|
||||
|
||||
url_arg = args[0]
|
||||
|
||||
# pylint: disable=R1711
|
||||
if url_arg == 'use_bad_response':
|
||||
return MockGetResponse({'foo': 'bar'}, 200)
|
||||
elif url_arg == 'reauth':
|
||||
|
||||
+33
-10
@@ -15,6 +15,17 @@ import tests.mock_responses as mresp
|
||||
USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
|
||||
CAMERA_CFG = {
|
||||
'camera': [
|
||||
{
|
||||
'battery_voltage': 90,
|
||||
'motion_alert': True,
|
||||
'wifi_strength': -30,
|
||||
'temperature': 68
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class TestBlinkCameraSetup(unittest.TestCase):
|
||||
"""Test the Blink class in blinkpy."""
|
||||
@@ -35,6 +46,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
'notifications': 2,
|
||||
'region_id': 'test'
|
||||
}
|
||||
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.blink.network_id = '0000'
|
||||
|
||||
@@ -42,11 +54,13 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.Blink.camera_config_request',
|
||||
return_value=CAMERA_CFG)
|
||||
@mock.patch('blinkpy.blinkpy.requests.post',
|
||||
side_effect=mresp.mocked_requests_post)
|
||||
@mock.patch('blinkpy.blinkpy.requests.get',
|
||||
side_effect=mresp.mocked_requests_get)
|
||||
def test_camera_properties(self, mock_get, mock_post):
|
||||
def test_camera_properties(self, mock_get, mock_post, mock_cfg):
|
||||
"""Tests all property set/recall."""
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
|
||||
@@ -56,7 +70,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
|
||||
for name in self.blink.cameras:
|
||||
camera = self.blink.cameras[name]
|
||||
|
||||
camera.update(self.camera_config)
|
||||
self.assertEqual(camera.id, '1111')
|
||||
self.assertEqual(camera.name, 'foobar')
|
||||
self.assertEqual(camera.armed, False)
|
||||
@@ -68,11 +82,15 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
camera.clip,
|
||||
"https://rest.test.{}/test/clip/clip.mp4".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(camera.temperature, 70)
|
||||
self.assertEqual(camera.battery, 3)
|
||||
self.assertEqual(camera.temperature, 68)
|
||||
self.assertEqual(camera.temperature_c, 20.0)
|
||||
self.assertEqual(camera.battery, 50)
|
||||
self.assertEqual(camera.battery_string, "OK")
|
||||
self.assertEqual(camera.notifications, 2)
|
||||
self.assertEqual(camera.region_id, 'test')
|
||||
self.assertEqual(camera.motion_enabled, True)
|
||||
self.assertEqual(camera.wifi_strength, -30)
|
||||
|
||||
camera_config = self.camera_config
|
||||
camera_config['active'] = 'armed'
|
||||
camera_config['thumbnail'] = '/test2/image'
|
||||
@@ -80,7 +98,6 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
camera_config['temp'] = 60
|
||||
camera_config['battery'] = 0
|
||||
camera_config['notifications'] = 4
|
||||
|
||||
for name in self.blink.cameras:
|
||||
camera = self.blink.cameras[name]
|
||||
camera.update(camera_config)
|
||||
@@ -93,8 +110,8 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
camera.clip,
|
||||
"https://rest.test.{}/test2/clip.mp4".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(camera.temperature, 60)
|
||||
self.assertEqual(camera.battery, 0)
|
||||
self.assertEqual(camera.temperature, 68)
|
||||
self.assertEqual(camera.battery, 50)
|
||||
self.assertEqual(camera.battery_string, "Low")
|
||||
self.assertEqual(camera.notifications, 4)
|
||||
camera_config['battery'] = -10
|
||||
@@ -107,7 +124,9 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
self.blink.cameras['foobar'] = camera_object
|
||||
self.assertEqual(camera_object, self.blink.cameras['fOoBaR'])
|
||||
|
||||
def test_camera_attributes(self):
|
||||
@mock.patch('blinkpy.blinkpy.Blink.camera_config_request',
|
||||
return_value=CAMERA_CFG)
|
||||
def test_camera_attributes(self, mock_cfg):
|
||||
"""Tests camera attributes."""
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
|
||||
@@ -117,6 +136,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
|
||||
for name in self.blink.cameras:
|
||||
camera = self.blink.cameras[name]
|
||||
camera.update(self.camera_config)
|
||||
camera_attr = camera.attributes
|
||||
self.assertEqual(camera_attr['device_id'], '1111')
|
||||
self.assertEqual(camera_attr['name'], 'foobar')
|
||||
@@ -129,7 +149,10 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
camera_attr['video'],
|
||||
"https://rest.test.{}/test/clip/clip.mp4".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(camera_attr['temperature'], 70)
|
||||
self.assertEqual(camera_attr['battery'], 3)
|
||||
self.assertEqual(camera_attr['temperature'], 68)
|
||||
self.assertEqual(camera_attr['temperature_c'], 20.0)
|
||||
self.assertEqual(camera_attr['battery'], 50)
|
||||
self.assertEqual(camera_attr['notifications'], 2)
|
||||
self.assertEqual(camera_attr['network_id'], '0000')
|
||||
self.assertEqual(camera_attr['motion_enabled'], True)
|
||||
self.assertEqual(camera_attr['wifi_strength'], -30)
|
||||
|
||||
@@ -61,9 +61,17 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
'/test/thumb')
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
def test_get_cameras(self, req):
|
||||
@mock.patch('blinkpy.blinkpy.Blink._video_request')
|
||||
def test_get_cameras(self, vid_req, req):
|
||||
"""Test camera extraction."""
|
||||
req.return_value = {'devices': [self.config]}
|
||||
vid_req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new.mp4',
|
||||
'thumbnail': '/new'
|
||||
}
|
||||
]
|
||||
self.blink.get_cameras()
|
||||
self.assertTrue('foobar' in self.blink.cameras)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ any communication related errors at startup.
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy import blinkpy as blinkpy
|
||||
from blinkpy import blinkpy
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = 'foobar'
|
||||
|
||||
Reference in New Issue
Block a user