From 147b6143b0aa1d41a5cff084b85da1aa89d07953 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 31 Jan 2019 21:21:17 -0500 Subject: [PATCH 01/16] Dev version bump --- blinkpy/helpers/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blinkpy/helpers/constants.py b/blinkpy/helpers/constants.py index b647740f..1b59f974 100644 --- a/blinkpy/helpers/constants.py +++ b/blinkpy/helpers/constants.py @@ -3,8 +3,8 @@ import os MAJOR_VERSION = 0 -MINOR_VERSION = 12 -PATCH_VERSION = 1 +MINOR_VERSION = 13 +PATCH_VERSION = '0.dev0' __version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) From a8b731fe21d7c3a14b4960e54abeba24f349ed34 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 17 Feb 2019 23:19:28 -0500 Subject: [PATCH 02/16] Adds throttle decorator --- blinkpy/helpers/util.py | 31 +++++++++++++ pylintrc | 4 ++ tests/test_util.py | 100 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 tests/test_util.py diff --git a/blinkpy/helpers/util.py b/blinkpy/helpers/util.py index 0023dd39..aabcbdbe 100644 --- a/blinkpy/helpers/util.py +++ b/blinkpy/helpers/util.py @@ -2,6 +2,7 @@ import logging import time +from functools import wraps from requests import Request, Session, exceptions from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT import blinkpy.helpers.errors as ERROR @@ -121,3 +122,33 @@ def __init__(self, region_id): self.networks_url = "{}/networks".format(self.base_url) self.video_url = "{}/api/v2/videos".format(self.base_url) _LOGGER.debug("Setting base url to %s.", self.base_url) + + +class Throttle(): + """Class for throttling api calls.""" + + def __init__(self, seconds=10): + """Initialize throttle class.""" + self.throttle_time = seconds + self.last_call = 0 + + def __call__(self, method): + """Throttle caller method.""" + def throttle_method(): + """Call when method is throttled.""" + return None + + @wraps(method) + def wrapper(*args, **kwargs): + """Wrap that checks for throttling.""" + force = kwargs.pop('force', False) + now = int(time.time()) + last_call_delta = now - self.last_call + if force or last_call_delta > self.throttle_time: + result = method(*args, *kwargs) + self.last_call = now + return result + + return throttle_method() + + return wrapper diff --git a/pylintrc b/pylintrc index 95ef2707..85dfdd6d 100644 --- a/pylintrc +++ b/pylintrc @@ -8,6 +8,8 @@ reports=no # 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 +# no-self-use - stupid and only annoying +# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb disable= locally-disabled, @@ -23,3 +25,5 @@ disable= too-many-lines, too-few-public-methods, no-else-return, + no-self-use, + unexpected-keyword-arg, diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 00000000..f75cf760 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,100 @@ +"""Test various api functions.""" + +import unittest +from unittest import mock +import time +from blinkpy.helpers.util import Throttle + + +class TestUtil(unittest.TestCase): + """Test the helpers/util module.""" + + def setUp(self): + """Initialize the blink module.""" + + def tearDown(self): + """Tear down blink module.""" + + def test_throttle(self): + """Test the throttle decorator.""" + calls = [] + + @Throttle(seconds=5) + def test_throttle(): + calls.append(1) + + now = int(time.time()) + now_plus_four = now + 4 + now_plus_six = now + 6 + + test_throttle() + self.assertEqual(1, len(calls)) + + # Call again, still shouldn't fire + test_throttle() + self.assertEqual(1, len(calls)) + + # Call with force + test_throttle(force=True) + self.assertEqual(2, len(calls)) + + # Call without throttle, shouldn't fire + test_throttle() + self.assertEqual(2, len(calls)) + + # Fake time as 4 seconds from now + with mock.patch('time.time', return_value=now_plus_four): + test_throttle() + self.assertEqual(2, len(calls)) + + # Fake time as 6 seconds from now + with mock.patch('time.time', return_value=now_plus_six): + test_throttle() + self.assertEqual(3, len(calls)) + + def test_throttle_per_instance(self): + """Test that throttle is done once per instance of class.""" + class Tester: + """A tester class for throttling.""" + + def test(self): + """Test the throttle.""" + return True + + tester = Tester() + throttled = Throttle(seconds=1)(tester.test) + self.assertEqual(throttled(), True) + self.assertEqual(throttled(), None) + + def test_throttle_on_two_methods(self): + """Test that throttle works for multiple methods.""" + class Tester: + """A tester class for throttling.""" + + @Throttle(seconds=3) + def test1(self): + """Test function for throttle.""" + return True + + @Throttle(seconds=5) + def test2(self): + """Test function for throttle.""" + return True + + tester = Tester() + now = time.time() + now_plus_4 = now + 4 + now_plus_6 = now + 6 + + self.assertEqual(tester.test1(), True) + self.assertEqual(tester.test2(), True) + self.assertEqual(tester.test1(), None) + self.assertEqual(tester.test2(), None) + + with mock.patch('time.time', return_value=now_plus_4): + self.assertEqual(tester.test1(), True) + self.assertEqual(tester.test2(), None) + + with mock.patch('time.time', return_value=now_plus_6): + self.assertEqual(tester.test1(), None) + self.assertEqual(tester.test2(), True) From 74aad2af1c64922117d57b550922a6f610948767 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Tue, 26 Feb 2019 09:53:49 -0500 Subject: [PATCH 03/16] Added throttles to api calls --- blinkpy/api.py | 17 ++++++++++++++++- blinkpy/camera.py | 8 ++++++-- blinkpy/sync_module.py | 24 +++++++++++++++++------- tests/test_cameras.py | 2 +- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/blinkpy/api.py b/blinkpy/api.py index d8a54455..4b8feaa8 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -3,11 +3,13 @@ import logging from json import dumps import blinkpy.helpers.errors as ERROR -from blinkpy.helpers.util import http_req, get_time, BlinkException +from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle from blinkpy.helpers.constants import DEFAULT_URL _LOGGER = logging.getLogger(__name__) +MIN_THROTTLE_TIME = 4 + def request_login(blink, url, username, password, is_retry=False): """ @@ -38,6 +40,7 @@ def request_networks(blink): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_network_status(blink, network): """ Request network information. @@ -49,6 +52,7 @@ 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. @@ -60,6 +64,7 @@ def request_syncmodule(blink, network): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_system_arm(blink, network): """ Arm system. @@ -71,6 +76,7 @@ def request_system_arm(blink, network): return http_post(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_system_disarm(blink, network): """ Disarm system. @@ -102,6 +108,7 @@ def request_homescreen(blink): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_sync_events(blink, network): """ Request events from sync module. @@ -113,6 +120,7 @@ def request_sync_events(blink, network): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_new_image(blink, network, camera_id): """ Request to capture new thumbnail for camera. @@ -127,6 +135,7 @@ def request_new_image(blink, network, camera_id): return http_post(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_new_video(blink, network, camera_id): """ Request to capture new video clip. @@ -141,6 +150,7 @@ def request_new_video(blink, network, camera_id): return http_post(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_video_count(blink): """Request total video count.""" url = "{}/api/v2/videos/count".format(blink.urls.base_url) @@ -161,6 +171,7 @@ def request_videos(blink, time=None, page=0): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_cameras(blink, network): """ Request all camera information. @@ -172,6 +183,7 @@ def request_cameras(blink, network): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_camera_info(blink, network, camera_id): """ Request camera info for one camera. @@ -186,6 +198,7 @@ def request_camera_info(blink, network, camera_id): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_camera_sensors(blink, network, camera_id): """ Request camera sensor info for one camera. @@ -200,6 +213,7 @@ def request_camera_sensors(blink, network, camera_id): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_motion_detection_enable(blink, network, camera_id): """ Enable motion detection for a camera. @@ -214,6 +228,7 @@ def request_motion_detection_enable(blink, network, camera_id): return http_post(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_motion_detection_disable(blink, network, camera_id): """Disable motion detection for a camera. diff --git a/blinkpy/camera.py b/blinkpy/camera.py index 71e38207..2ffd3c1e 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -92,8 +92,9 @@ def set_motion_detect(self, enable): self.network_id, self.camera_id) - def update(self, config, force_cache=False): + def update(self, config, force_cache=False, **kwargs): """Update camera info.""" + force = kwargs.pop('force', False) self.name = config['name'] self.camera_id = str(config['camera_id']) self.network_id = str(config['network_id']) @@ -107,12 +108,15 @@ def update(self, config, force_cache=False): # Retrieve calibrated temperature from special endpoint resp = api.request_camera_sensors(self.sync.blink, self.network_id, - self.camera_id) + self.camera_id, + force=force) try: self.temperature_calibrated = resp['temp'] except KeyError: self.temperature_calibrated = self.temperature _LOGGER.warning("Could not retrieve calibrated temperature.") + except TypeError: + _LOGGER.debug("API call temporarily throttled.") # Check if thumbnail exists in config, if not try to # get it from the homescreen info in teh sync module diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 08e31e59..2c37287a 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -76,7 +76,9 @@ def arm(self, value): def start(self): """Initialize the system.""" - response = api.request_syncmodule(self.blink, self.network_id) + response = api.request_syncmodule(self.blink, + self.network_id, + force=True) try: self.summary = response['syncmodule'] self.network_id = self.summary['network_id'] @@ -94,7 +96,7 @@ def start(self): response, exc_info=True) - self.events = self.get_events() + self.events = self.get_events(force=True) self.homescreen = api.request_homescreen(self.blink) self.network_info = api.request_network_status(self.blink, self.network_id) @@ -105,13 +107,18 @@ def start(self): name = camera_config['name'] self.cameras[name] = BlinkCamera(self) self.motion[name] = False - self.cameras[name].update(camera_config, force_cache=True) + self.cameras[name].update(camera_config, + force_cache=True, + force=True) return True - def get_events(self): + def get_events(self, **kwargs): """Retrieve events from server.""" - response = api.request_sync_events(self.blink, self.network_id) + force = kwargs.pop('force', False) + response = api.request_sync_events(self.blink, + self.network_id, + force=force) try: return response['event'] except (TypeError, KeyError): @@ -120,9 +127,12 @@ def get_events(self): exc_info=True) return False - def get_camera_info(self): + def get_camera_info(self, **kwargs): """Retrieve camera information.""" - response = api.request_cameras(self.blink, self.network_id) + force = kwargs.pop('force', False) + response = api.request_cameras(self.blink, + self.network_id, + force=force) try: return response['devicestatus'] except (TypeError, KeyError): diff --git a/tests/test_cameras.py b/tests/test_cameras.py index 54443e2e..e501e810 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -159,7 +159,7 @@ def test_no_thumbnails(self, mock_sess): } self.assertEqual(self.camera.temperature_calibrated, None) with self.assertLogs() as logrecord: - self.camera.update(config) + self.camera.update(config, force=True) self.assertEqual(self.camera.thumbnail, None) self.assertEqual(self.camera.last_record, ['1']) self.assertEqual(self.camera.temperature_calibrated, 68) From 602b6160ab1489674bbd5c4c63e083aaf8b58f9d Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 11:35:30 -0500 Subject: [PATCH 04/16] New method to retrieve cameras due to api change --- blinkpy/api.py | 12 ++++++------ blinkpy/blinkpy.py | 28 +++++++++++++++++++++++++++- blinkpy/camera.py | 7 ++----- blinkpy/sync_module.py | 16 +++++++++------- tests/test_blink_functions.py | 4 ++-- tests/test_blinkpy.py | 5 ++++- tests/test_cameras.py | 2 +- tests/test_sync_module.py | 19 ++++--------------- 8 files changed, 55 insertions(+), 38 deletions(-) diff --git a/blinkpy/api.py b/blinkpy/api.py index 4b8feaa8..709382d5 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -102,9 +102,11 @@ def request_command_status(blink, network, command_id): return http_get(blink, url) +@Throttle(seconds=MIN_THROTTLE_TIME) def request_homescreen(blink): """Request homescreen info.""" - url = "{}/homescreen".format(blink.urls.base_url) + url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url, + blink.account_id) return http_get(blink, url) @@ -183,7 +185,6 @@ def request_cameras(blink, network): return http_get(blink, url) -@Throttle(seconds=MIN_THROTTLE_TIME) def request_camera_info(blink, network, camera_id): """ Request camera info for one camera. @@ -192,13 +193,12 @@ def request_camera_info(blink, network, camera_id): :param network: Sync module network id. :param camera_id: Camera ID of camera to request info from. """ - url = "{}/network/{}/camera/{}".format(blink.urls.base_url, - network, - camera_id) + url = "{}/network/{}/camera/{}/config".format(blink.urls.base_url, + network, + camera_id) return http_get(blink, url) -@Throttle(seconds=MIN_THROTTLE_TIME) def request_camera_sensors(blink, network, camera_id): """ Request camera sensor info for one camera. diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 9c0d48af..4190d6ac 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -88,9 +88,16 @@ def start(self): elif not self.get_auth_token(): return + camera_list = self.get_cameras() networks = self.get_ids() for network_name, network_id in networks.items(): - sync_module = BlinkSyncModule(self, network_name, network_id) + if network_id not in camera_list.keys(): + camera_list[network_id] = {} + _LOGGER.warning("No cameras found for %s", network_name) + sync_module = BlinkSyncModule(self, + network_name, + network_id, + camera_list[network_id]) sync_module.start() self.sync[network_name] = sync_module self.cameras = self.merge_cameras() @@ -170,6 +177,25 @@ def get_ids(self): self.network_ids = all_networks return network_dict + def get_cameras(self): + """Retrieve a camera list for each onboarded network.""" + response = api.request_homescreen(self) + try: + all_cameras = response['cameras'] + for camera in all_cameras: + camera_network = camera['network_id'] + camera_name = camera['name'] + camera_id = camera['id'] + camera_info = {'name': camera_name, 'id': camera_id} + if camera_network in all_cameras: + all_cameras[camera_network].append(camera_info) + else: + all_cameras[camera_network] = [camera_info] + return all_cameras + except KeyError: + _LOGGER.error("Initialization failue. Could not retrieve cameras.") + return {} + def refresh(self, force_cache=False): """ Perform a system refresh. diff --git a/blinkpy/camera.py b/blinkpy/camera.py index 2ffd3c1e..f7615df8 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -94,7 +94,7 @@ def set_motion_detect(self, enable): def update(self, config, force_cache=False, **kwargs): """Update camera info.""" - force = kwargs.pop('force', False) + # force = kwargs.pop('force', False) self.name = config['name'] self.camera_id = str(config['camera_id']) self.network_id = str(config['network_id']) @@ -108,15 +108,12 @@ def update(self, config, force_cache=False, **kwargs): # Retrieve calibrated temperature from special endpoint resp = api.request_camera_sensors(self.sync.blink, self.network_id, - self.camera_id, - force=force) + self.camera_id) try: self.temperature_calibrated = resp['temp'] except KeyError: self.temperature_calibrated = self.temperature _LOGGER.warning("Could not retrieve calibrated temperature.") - except TypeError: - _LOGGER.debug("API call temporarily throttled.") # Check if thumbnail exists in config, if not try to # get it from the homescreen info in teh sync module diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 2c37287a..2b85ff86 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -13,7 +13,7 @@ class BlinkSyncModule(): """Class to initialize sync module.""" - def __init__(self, blink, network_name, network_id): + def __init__(self, blink, network_name, network_id, camera_list): """ Initialize Blink sync module. @@ -36,6 +36,7 @@ def __init__(self, blink, network_name, network_id): self.cameras = CaseInsensitiveDict({}) self.motion = {} self.last_record = {} + self.camera_list = camera_list @property def attributes(self): @@ -96,18 +97,20 @@ def start(self): response, exc_info=True) - self.events = self.get_events(force=True) - self.homescreen = api.request_homescreen(self.blink) self.network_info = api.request_network_status(self.blink, self.network_id) self.check_new_videos() - camera_info = self.get_camera_info() - for camera_config in camera_info: + for camera_config in self.camera_list: + if 'name' not in camera_config: + break name = camera_config['name'] self.cameras[name] = BlinkCamera(self) self.motion[name] = False - self.cameras[name].update(camera_config, + camera_info = api.request_camera_info(self.blink, + self.network_id, + camera_config['id']) + self.cameras[name].update(camera_info, force_cache=True, force=True) @@ -143,7 +146,6 @@ def get_camera_info(self, **kwargs): def refresh(self, force_cache=False): """Get all blink cameras and pulls their most recent status.""" - self.events = self.get_events() self.homescreen = api.request_homescreen(self.blink) self.network_info = api.request_network_status(self.blink, self.network_id) diff --git a/tests/test_blink_functions.py b/tests/test_blink_functions.py index 011c0de1..d5d10abc 100644 --- a/tests/test_blink_functions.py +++ b/tests/test_blink_functions.py @@ -79,8 +79,8 @@ def test_merge_cameras(self, mock_sess): """Test merge camera functionality.""" first_dict = {'foo': 'bar', 'test': 123} next_dict = {'foobar': 456, 'bar': 'foo'} - self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1) - self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2) + self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, []) + self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, []) self.blink.sync['foo'].cameras = first_dict self.blink.sync['bar'].cameras = next_dict result = self.blink.merge_cameras() diff --git a/tests/test_blinkpy.py b/tests/test_blinkpy.py index f704da2c..68cce738 100644 --- a/tests/test_blinkpy.py +++ b/tests/test_blinkpy.py @@ -31,7 +31,10 @@ def setUp(self): self.blink_no_cred = Blink() self.blink = Blink(username=USERNAME, password=PASSWORD) - self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234') + self.blink.sync['test'] = BlinkSyncModule(self.blink, + 'test', + '1234', + []) self.blink.urls = BlinkURLHandler('test') self.blink.session = create_session() diff --git a/tests/test_cameras.py b/tests/test_cameras.py index e501e810..2bd0c0ef 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -46,7 +46,7 @@ def setUp(self): self.blink._auth_header = header self.blink.session = create_session() self.blink.urls = BlinkURLHandler('test') - self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234) + self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, []) self.camera = BlinkCamera(self.blink.sync['test']) self.camera.name = 'foobar' self.blink.sync['test'].cameras['foobar'] = self.camera diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index f479f509..7b04deae 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -24,7 +24,10 @@ def setUp(self): 'TOKEN_AUTH': 'foobar123' } self.blink.urls = blinkpy.BlinkURLHandler('test') - self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234') + self.blink.sync['test'] = BlinkSyncModule(self.blink, + 'test', + '1234', + []) self.camera = BlinkCamera(self.blink.sync) self.mock_start = [ {'syncmodule': { @@ -126,20 +129,6 @@ def test_summary_with_only_network_id(self, mock_resp): self.blink.sync['test'].start() self.assertEqual(self.blink.sync['test'].network_id, 8675309) - def test_unexpected_events(self, mock_resp): - """Test unexpected events response.""" - self.mock_start[1] = None - mock_resp.side_effect = self.mock_start - self.blink.sync['test'].start() - self.assertEqual(self.blink.sync['test'].events, False) - - def test_missing_events(self, mock_resp): - """Test missing events key from response.""" - self.mock_start[1] = {} - mock_resp.side_effect = self.mock_start - self.blink.sync['test'].start() - self.assertEqual(self.blink.sync['test'].events, False) - def test_unexpected_camera_info(self, mock_resp): """Test unexpected camera info response.""" self.blink.sync['test'].cameras['foo'] = None From 793797e3a78bbddfb2e729c10cc2217f9643949e Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 13:09:03 -0500 Subject: [PATCH 05/16] Fixed issues with new endpoint --- blinkpy/blinkpy.py | 14 +++++------ blinkpy/camera.py | 2 +- blinkpy/sync_module.py | 50 ++++++++++++++++++++------------------- tests/test_cameras.py | 8 +++---- tests/test_sync_module.py | 5 ++-- 5 files changed, 41 insertions(+), 38 deletions(-) diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 4190d6ac..18dc25fc 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -181,16 +181,16 @@ def get_cameras(self): """Retrieve a camera list for each onboarded network.""" response = api.request_homescreen(self) try: - all_cameras = response['cameras'] - for camera in all_cameras: - camera_network = camera['network_id'] + all_cameras = {} + for camera in response['cameras']: + camera_network = str(camera['network_id']) camera_name = camera['name'] camera_id = camera['id'] camera_info = {'name': camera_name, 'id': camera_id} - if camera_network in all_cameras: - all_cameras[camera_network].append(camera_info) - else: - all_cameras[camera_network] = [camera_info] + if camera_network not in all_cameras: + all_cameras[camera_network] = [] + + all_cameras[camera_network].append(camera_info) return all_cameras except KeyError: _LOGGER.error("Initialization failue. Could not retrieve cameras.") diff --git a/blinkpy/camera.py b/blinkpy/camera.py index f7615df8..7b05b931 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -96,7 +96,7 @@ def update(self, config, force_cache=False, **kwargs): """Update camera info.""" # force = kwargs.pop('force', False) self.name = config['name'] - self.camera_id = str(config['camera_id']) + self.camera_id = str(config['id']) self.network_id = str(config['network_id']) self.serial = config['serial'] self.motion_enabled = config['enabled'] diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index 2b85ff86..01e88fc5 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -30,7 +30,6 @@ def __init__(self, blink, network_name, network_id, camera_list): self.sync_id = None self.host = None self.summary = None - self.homescreen = None self.network_info = None self.events = [] self.cameras = CaseInsensitiveDict({}) @@ -101,18 +100,22 @@ def start(self): self.network_id) self.check_new_videos() - for camera_config in self.camera_list: - if 'name' not in camera_config: - break - name = camera_config['name'] - self.cameras[name] = BlinkCamera(self) - self.motion[name] = False - camera_info = api.request_camera_info(self.blink, - self.network_id, - camera_config['id']) - self.cameras[name].update(camera_info, - force_cache=True, - force=True) + try: + for camera_config in self.camera_list: + if 'name' not in camera_config: + break + name = camera_config['name'] + self.cameras[name] = BlinkCamera(self) + self.motion[name] = False + camera_info = self.get_camera_info(camera_config['id']) + self.cameras[name].update(camera_info, + force_cache=True, + force=True) + except KeyError: + _LOGGER.error("Could not create cameras instances for %s", + self.name, + exc_info=True) + return False return True @@ -130,14 +133,13 @@ def get_events(self, **kwargs): exc_info=True) return False - def get_camera_info(self, **kwargs): + def get_camera_info(self, camera_id): """Retrieve camera information.""" - force = kwargs.pop('force', False) - response = api.request_cameras(self.blink, - self.network_id, - force=force) + response = api.request_camera_info(self.blink, + self.network_id, + camera_id) try: - return response['devicestatus'] + return response['camera'][0] except (TypeError, KeyError): _LOGGER.error("Could not extract camera info: %s", response, @@ -146,14 +148,14 @@ def get_camera_info(self, **kwargs): def refresh(self, force_cache=False): """Get all blink cameras and pulls their most recent status.""" - self.homescreen = api.request_homescreen(self.blink) self.network_info = api.request_network_status(self.blink, self.network_id) - camera_info = self.get_camera_info() self.check_new_videos() - for camera_config in camera_info: - name = camera_config['name'] - self.cameras[name].update(camera_config, force_cache=force_cache) + for camera_name in self.cameras.keys(): + camera_id = self.cameras[camera_name].camera_id + camera_info = self.get_camera_info(camera_id) + self.cameras[camera_name].update(camera_info, + force_cache=force_cache) def check_new_videos(self): """Check if new videos since last refresh.""" diff --git a/tests/test_cameras.py b/tests/test_cameras.py index 2bd0c0ef..866a2a03 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -59,7 +59,7 @@ def test_camera_update(self, mock_sess): """Test that we can properly update camera properties.""" config = { 'name': 'new', - 'camera_id': 1234, + 'id': 1234, 'network_id': 5678, 'serial': '12345678', 'enabled': False, @@ -115,7 +115,7 @@ def test_thumbnail_not_in_info(self, mock_sess): } config = { 'name': 'new', - 'camera_id': 1234, + 'id': 1234, 'network_id': 5678, 'serial': '12345678', 'enabled': False, @@ -144,7 +144,7 @@ def test_no_thumbnails(self, mock_sess): self.camera.last_record = ['1'] config = { 'name': 'new', - 'camera_id': 1234, + 'id': 1234, 'network_id': 5678, 'serial': '12345678', 'enabled': False, @@ -176,7 +176,7 @@ def test_no_video_clips(self, mock_sess): mock_sess.return_value = 'foobar' config = { 'name': 'new', - 'camera_id': 1234, + 'id': 1234, 'network_id': 5678, 'serial': '12345678', 'enabled': False, diff --git a/tests/test_sync_module.py b/tests/test_sync_module.py index 7b04deae..ae7ad8c9 100644 --- a/tests/test_sync_module.py +++ b/tests/test_sync_module.py @@ -55,8 +55,9 @@ def test_get_events(self, mock_resp): def test_get_camera_info(self, mock_resp): """Test get camera info function.""" - mock_resp.return_value = {'devicestatus': True} - self.assertEqual(self.blink.sync['test'].get_camera_info(), True) + mock_resp.return_value = {'camera': ['foobar']} + self.assertEqual(self.blink.sync['test'].get_camera_info('1234'), + 'foobar') def test_check_new_videos(self, mock_resp): """Test recent video response.""" From 532a8d5071f073d30885e43905b0ea2abcab0c1a Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 13:36:19 -0500 Subject: [PATCH 06/16] lint and build python version now 3.7 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d9712276..f2b39992 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,13 @@ sudo: required matrix: fast_finish: true include: - - python: "3.6" + - python: "3.7" env: TOXENV=lint - python: "3.5.3" env: TOXENV=py35 - python: "3.6" env: TOXENV=py36 - - python: "3.6" + - python: "3.7" env: TOXENV=build - python: "3.7" env: TOXENV=py37 From b536bd7eb53d3fba2e49cd7e78bbcb4b8cd764f1 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 13:40:10 -0500 Subject: [PATCH 07/16] Build back to 3.6, forgot dist for lint 3.7 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f2b39992..00cca5c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,12 @@ matrix: include: - python: "3.7" env: TOXENV=lint + dist: xenial - python: "3.5.3" env: TOXENV=py35 - python: "3.6" env: TOXENV=py36 - - python: "3.7" + - python: "3.6" env: TOXENV=build - python: "3.7" env: TOXENV=py37 From 414eabd3ae1c5faa2d18d1cd526bc2f8534e9922 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 13:52:13 -0500 Subject: [PATCH 08/16] Add test for get_camera method. Downgrade travis lint to 3.5.3 --- .travis.yml | 3 +-- tests/test_blinkpy.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00cca5c5..62b4e387 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,8 @@ sudo: required matrix: fast_finish: true include: - - python: "3.7" + - python: "3.5.3" env: TOXENV=lint - dist: xenial - python: "3.5.3" env: TOXENV=py35 - python: "3.6" diff --git a/tests/test_blinkpy.py b/tests/test_blinkpy.py index 68cce738..4c9507fc 100644 --- a/tests/test_blinkpy.py +++ b/tests/test_blinkpy.py @@ -163,3 +163,24 @@ def test_unexpected_login(self, mock_login, mock_sess): """Check that we appropriately handle unexpected login info.""" mock_login.return_value = None self.assertFalse(self.blink.get_auth_token()) + + @mock.patch('blinkpy.api.request_homescreen') + def test_get_cameras(self, mock_home, mock_sess): + """Check retrieval of camera information.""" + mock_home.return_value = { + 'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678}, + {'name': 'bar', 'network_id': 1234, 'id': 5679}, + {'name': 'test', 'network_id': 4321, 'id': 0000}] + } + result = self.blink.get_cameras() + self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678}, + {'name': 'bar', 'id': 5679}], + '4321': [{'name': 'test', 'id': 0000}]}) + + @mock.patch('blinkpy.api.request_homescreen') + def test_get_cameras_failure(self, mock_home, mock_sess): + """Check that on failure we initialize empty info and move on.""" + mock_home.return_value = {} + result = self.blink.get_cameras() + self.assertEqual(result, {}) + From 09c168f6e861750ebc95d385e5b3c308034a28e5 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 13:53:13 -0500 Subject: [PATCH 09/16] Remove blank line at EOF --- tests/test_blinkpy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_blinkpy.py b/tests/test_blinkpy.py index 4c9507fc..627b359a 100644 --- a/tests/test_blinkpy.py +++ b/tests/test_blinkpy.py @@ -183,4 +183,3 @@ def test_get_cameras_failure(self, mock_home, mock_sess): mock_home.return_value = {} result = self.blink.get_cameras() self.assertEqual(result, {}) - From b22764614d5a3d22a30b21b7640e75e319a839a7 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 14:08:22 -0500 Subject: [PATCH 10/16] Upgrade pylint to 2.3.0 --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 154e9f25..3af0828e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ flake8==3.5.0 flake8-docstrings==1.3.0 -pylint==2.1.1 +pylint==2.3.0 pydocstyle==2.1.1 pytest==3.7.1 pytest-cov>=2.3.1 From 2ca6b9edda84b30d8b0c55d9ae5e25a478b18dc7 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 14:25:46 -0500 Subject: [PATCH 11/16] -_- --- blinkpy/helpers/util.py | 2 -- tests/mock_responses.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/blinkpy/helpers/util.py b/blinkpy/helpers/util.py index aabcbdbe..e2e44a43 100644 --- a/blinkpy/helpers/util.py +++ b/blinkpy/helpers/util.py @@ -107,8 +107,6 @@ def __init__(self, errcode): class BlinkAuthenticationException(BlinkException): """Class to throw authentication exception.""" - pass - class BlinkURLHandler(): """Class that handles Blink URLS.""" diff --git a/tests/mock_responses.py b/tests/mock_responses.py index fe00feae..90ae81c4 100644 --- a/tests/mock_responses.py +++ b/tests/mock_responses.py @@ -67,5 +67,3 @@ def mocked_session_send(*args, **kwargs): class MockURLHandler(BlinkURLHandler): """Mocks URL Handler in blinkpy module.""" - - pass From 54bfb99d605dca681b98a0dddd2c11915f4ee3d5 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 15:10:28 -0500 Subject: [PATCH 12/16] Added simple throttle for refresh to prevent too many force calls --- blinkpy/blinkpy.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 18dc25fc..51d71349 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -27,13 +27,17 @@ from blinkpy.helpers import errors as ERROR from blinkpy.helpers.util import ( create_session, merge_dicts, get_time, BlinkURLHandler, - BlinkAuthenticationException) + BlinkAuthenticationException, Throttle) from blinkpy.helpers.constants import ( BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL) 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__) @@ -196,6 +200,7 @@ def get_cameras(self): _LOGGER.error("Initialization failue. Could not retrieve cameras.") return {} + @Throttle(seconds=MIN_THROTTLE_TIME) def refresh(self, force_cache=False): """ Perform a system refresh. @@ -209,6 +214,8 @@ def refresh(self, force_cache=False): if not force_cache: # Prevents rapid clearing of motion detect property self.last_refresh = int(time.time()) + return True + return False def check_if_ok_to_update(self): """Check if it is ok to perform an http request.""" From 95b45f507ab997c31523eba06d7a94387fdf3a6b Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 15:11:23 -0500 Subject: [PATCH 13/16] Change api throttle time to 2 seconds --- blinkpy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blinkpy/api.py b/blinkpy/api.py index 709382d5..6e417385 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -8,7 +8,7 @@ _LOGGER = logging.getLogger(__name__) -MIN_THROTTLE_TIME = 4 +MIN_THROTTLE_TIME = 2 def request_login(blink, url, username, password, is_retry=False): From d8bac39c527e8bf0df30720cbca9c4650c2b1b86 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 16:03:22 -0500 Subject: [PATCH 14/16] Added ability to have multiple backup api endpoints for logging in --- blinkpy/blinkpy.py | 51 ++++++++++++++++++++++------------- blinkpy/helpers/constants.py | 3 ++- tests/test_blink_functions.py | 23 ++++++++++------ 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 18dc25fc..b9d98c0a 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -29,7 +29,7 @@ create_session, merge_dicts, get_time, BlinkURLHandler, BlinkAuthenticationException) from blinkpy.helpers.constants import ( - BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL) + BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL) from blinkpy.helpers.constants import __version__ REFRESH_RATE = 30 @@ -119,7 +119,33 @@ def get_auth_token(self, is_retry=False): if not isinstance(self._password, str): raise BlinkAuthenticationException(ERROR.PASSWORD) - login_url = LOGIN_URL + login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL] + + response = self.login_request(login_urls, is_retry=is_retry) + + if not response: + return False + + self._host = "{}.{}".format(self.region_id, BLINK_URL) + self._token = response['authtoken']['authtoken'] + self.networks = response['networks'] + + self._auth_header = {'Host': self._host, + 'TOKEN_AUTH': self._token} + self.urls = BlinkURLHandler(self.region_id) + + return self._auth_header + + def login_request(self, login_urls, is_retry=False): + """Make a login request.""" + try: + login_url = login_urls.pop(0) + except IndexError: + _LOGGER.error("Could not login to blink servers.") + return False + + _LOGGER.info("Attempting login with %s", login_url) + response = api.request_login(self, login_url, self._username, @@ -127,36 +153,23 @@ def get_auth_token(self, is_retry=False): is_retry=is_retry) try: if response.status_code != 200: - _LOGGER.debug("Received response code %s during login.", - response.status_code) - login_url = LOGIN_BACKUP_URL - response = api.request_login(self, - login_url, - self._username, - self._password, - is_retry=is_retry) + response = self.login_request(login_urls) response = response.json() (self.region_id, self.region), = response['region'].items() + except AttributeError: _LOGGER.error("Login API endpoint failed with response %s", response, exc_info=True) return False + except KeyError: _LOGGER.warning("Could not extract region info.") self.region_id = 'piri' self.region = 'UNKNOWN' - self._host = "{}.{}".format(self.region_id, BLINK_URL) - self._token = response['authtoken']['authtoken'] - self.networks = response['networks'] - - self._auth_header = {'Host': self._host, - 'TOKEN_AUTH': self._token} - self.urls = BlinkURLHandler(self.region_id) self._login_url = login_url - - return self._auth_header + return response def get_ids(self): """Set the network ID and Account ID.""" diff --git a/blinkpy/helpers/constants.py b/blinkpy/helpers/constants.py index 1b59f974..b9f3b40b 100644 --- a/blinkpy/helpers/constants.py +++ b/blinkpy/helpers/constants.py @@ -47,7 +47,8 @@ BLINK_URL = 'immedia-semi.com' DEFAULT_URL = "{}.{}".format('prod', BLINK_URL) BASE_URL = "https://{}".format(DEFAULT_URL) -LOGIN_URL = "{}/login".format(BASE_URL) +LOGIN_URL = "{}/api/v2/login".format(BASE_URL) +OLD_LOGIN_URL = "{}/login".format(BASE_URL) LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest.piri', BLINK_URL) ''' diff --git a/tests/test_blink_functions.py b/tests/test_blink_functions.py index d5d10abc..3607a739 100644 --- a/tests/test_blink_functions.py +++ b/tests/test_blink_functions.py @@ -2,7 +2,6 @@ import unittest from unittest import mock import logging -from requests import Request from blinkpy import blinkpy from blinkpy.sync_module import BlinkSyncModule @@ -56,24 +55,32 @@ def tearDown(self): """Clean up after test.""" self.blink = None - @mock.patch('blinkpy.blinkpy.api.http_req') + @mock.patch('blinkpy.blinkpy.api.request_login') def test_backup_url(self, req, mock_sess): """Test backup login method.""" - fake_req = Request('POST', 'http://wrong.url').prepare() json_resp = { 'authtoken': {'authtoken': 'foobar123'}, 'networks': {'1234': {'name': 'foobar', 'onboarded': True}} } + bad_req = mresp.MockResponse({}, 404) new_req = mresp.MockResponse(json_resp, 200) req.side_effect = [ - mresp.mocked_session_send(fake_req), + bad_req, + bad_req, new_req ] - self.blink.get_auth_token() - self.assertEqual(self.blink.region_id, 'piri') - self.assertEqual(self.blink.region, 'UNKNOWN') + self.blink.login_request(['test1', 'test2', 'test3']) # pylint: disable=protected-access - self.assertEqual(self.blink._token, 'foobar123') + self.assertEqual(self.blink._login_url, 'test3') + + req.side_effect = [ + bad_req, + new_req, + bad_req + ] + self.blink.login_request(['test1', 'test2', 'test3']) + # pylint: disable=protected-access + self.assertEqual(self.blink._login_url, 'test2') def test_merge_cameras(self, mock_sess): """Test merge camera functionality.""" From 13b7debb3374906b174194dc19d15e5e41f23e6e Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 20:54:21 -0500 Subject: [PATCH 15/16] Update CHANGES.rst --- CHANGES.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ba8ba660..1149fd09 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,31 @@ Changelog A list of changes between each release +0.13.0 (2019-03-01) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Breaking change:** +Wifi status reported in dBm again, instead of bars (which is great). Also, the old `get_camera_info` method has changed and requires a `camera_id` parameter. + +- Adds throttle decorator +- Decorate following functions with 4s throttle (call method with `force=True` to override): + - request_network_status + - request_syncmodule + - request_system_arm + - request_system_disarm + - request_sync_events + - request_new_image + - request_new_video + - request_video_count + - request_cameras + - request_camera_info + - request_camera_sensors + - request_motion_detection_enable + - request_motion_detection_disable +- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work. +- Adds throttle decorator to refresh function to prevent too many frequent calls with `force_cache` flag set to `True`. This additional throttle can be overridden with the `force=True` argument passed to the refresh function. +- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation + + 0.12.1 (2019-01-31) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Remove logging improvements since they were incompatible with home-assistant logging From c4b255d3bf6a3e396f4d6d4f0e563bd524e99e32 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Mar 2019 20:54:52 -0500 Subject: [PATCH 16/16] Version bump --- blinkpy/helpers/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blinkpy/helpers/constants.py b/blinkpy/helpers/constants.py index b9f3b40b..4a7db36f 100644 --- a/blinkpy/helpers/constants.py +++ b/blinkpy/helpers/constants.py @@ -4,7 +4,7 @@ MAJOR_VERSION = 0 MINOR_VERSION = 13 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = 0 __version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)