From fa4a1a4e8c111d63ef5595e95811d2c624a97e81 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 26 Jan 2017 22:19:58 -0500 Subject: [PATCH 01/21] Fixed refresh function --- blinkpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blinkpy.py b/blinkpy.py index d77b5fb..587a47c 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,7 +17,7 @@ import requests import getpass import json -__version__ = '0.3.0' +__version__ = '0.3.1' BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' @@ -287,7 +287,7 @@ class Blink(object): for name, camera in self._CAMERAS.items(): for element in response: try: - if element['id'] == camera.id: + if element['device_id'] == camera.id: camera.update(element) except KeyError: pass From 09f2e29f277b5e5eeb9b1b8712510dde84517b53 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 26 Jan 2017 23:28:11 -0500 Subject: [PATCH 02/21] String cast for refresh --- blinkpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blinkpy.py b/blinkpy.py index 587a47c..473e16f 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,7 +17,7 @@ import requests import getpass import json -__version__ = '0.3.1' +__version__ = '0.3.2' BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' @@ -287,7 +287,7 @@ class Blink(object): for name, camera in self._CAMERAS.items(): for element in response: try: - if element['device_id'] == camera.id: + if str(element['device_id']) == camera.id: camera.update(element) except KeyError: pass From 32a27f2d984a8e09dab116c217cbd473ce29946c Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 26 Jan 2017 23:43:32 -0500 Subject: [PATCH 03/21] Added image refresh function, increased test coverage --- blinkpy.py | 17 ++++++- tests/test_blink_requests.py | 69 +++++++++++++++++----------- tests/test_const.py | 89 ++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 tests/test_const.py diff --git a/blinkpy.py b/blinkpy.py index 473e16f..3b09577 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,7 +17,7 @@ import requests import getpass import json -__version__ = '0.3.2' +__version__ = '0.4.0' BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' @@ -186,8 +186,21 @@ class BlinkCamera(object): self._BATTERY = values['battery'] self._NOTIFICATIONS = values['notifications'] + def image_refresh(self): + url = BASE_URL + '/homescreen' + response = _request(url, headers=self._HEADER, type='get')['devices'] + for element in response: + try: + if str(element['device_id']) == self._ID: + self._THUMB = BASE_URL + element['thumbnail'] + '.jpg' + return self._THUMB + except KeyError: + pass + return None + def image_to_file(self, path): - response = _request(self._THUMB, headers=self._HEADER, stream=True, json=False) + thumb = self.image_refresh() + response = _request(thumb, headers=self._HEADER, stream=True, json=False) if response.status_code == 200: with open(path, 'wb') as f: for chunk in response: diff --git a/tests/test_blink_requests.py b/tests/test_blink_requests.py index 806e0b6..28776bf 100644 --- a/tests/test_blink_requests.py +++ b/tests/test_blink_requests.py @@ -4,6 +4,7 @@ import unittest from unittest import mock from blinkpy import LOGIN_URL from blinkpy import BASE_URL +import test_const as const def mocked_requests_post(*args, **kwargs): class MockPostResponse: @@ -14,7 +15,7 @@ def mocked_requests_post(*args, **kwargs): return self.json_data if args[0] == LOGIN_URL: - return MockPostResponse({"region":{"test": "Notacountry"}, "authtoken":{"authtoken":"abcd1234"}}, 200) + return MockPostResponse({"region":{const.REGION_ID: const.REGION}, "authtoken":{"authtoken":const.TOKEN}}, 200) elif args[0].split("/")[-1] == 'arm': return MockPostResponse({"armed":True}, 200) elif args[0].split("/")[-1] == 'disarm': @@ -33,20 +34,9 @@ def mocked_requests_get(*args, **kwargs): return self.json_data if args[0] == BASE_URL + '/networks': - return MockGetResponse({'networks':[{"id":7777,"account_id":3333},{"nothing":"nothing"}]}, 200) + return MockGetResponse({'networks':[{"id":const.NETWORK_ID,"account_id":const.ACCOUNT_ID},{"nothing":"nothing"}]}, 200) else: - return MockGetResponse({'devices':[{'device_type':'camera','name':'test','device_id':123,'armed':False,'thumbnail':'/some/url','temp':65,'battery':3,'notifications':1}, - {'device_type':'camera','name':'test2','device_id':321,'armed':True,'thumbnail':'/some/new/url','temp':56,'battery':5,'notifications':0}, - {'device_type':'None'} - ], - 'events':[{'camera_id':123, 'type':'motion', 'video_url':'/some/dumb/location.mp4', 'created_at':'2017-01-01'}, - {'camera_id':321, 'type':'None'} - ], - 'syncmodule':{'name':'SyncName', 'status':'online'}, - 'network':{'name':'Sync','armed':True, 'notifications':4} - }, - 200 - ) + return MockGetResponse(const.response, 200) return MockGetResponse({'message':'ERROR','code':404}, 404) @@ -58,12 +48,12 @@ class TestBlinkRequests(unittest.TestCase): blink = blinkpy.Blink(username='user',password='password') blink.setup_system() - self.assertEqual(blink.network_id, '7777') - self.assertEqual(blink.account_id, '3333') - self.assertEqual(blink.region, 'Notacountry') - self.assertEqual(blink.region_id, 'test') - self.assertEqual(blink.online, True) - self.assertEqual(blink.arm, True) + self.assertEqual(blink.network_id, str(const.NETWORK_ID)) + self.assertEqual(blink.account_id, str(const.ACCOUNT_ID)) + self.assertEqual(blink.region, const.REGION) + self.assertEqual(blink.region_id, const.REGION_ID) + self.assertEqual(blink.online, const.ISONLINE) + self.assertEqual(blink.arm, const.ARMED) @mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post) @mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get) @@ -72,14 +62,39 @@ class TestBlinkRequests(unittest.TestCase): blink.setup_system() blink.last_motion() for name, camera in blink.cameras.items(): - if camera.id == '123': - self.assertEqual(name, 'test') - self.assertEqual(camera.armed, False) - self.assertEqual(camera.motion['video'], BASE_URL+'/some/dumb/location.mp4') - elif camera.id == '321': - self.assertEqual(name, 'test2') - self.assertEqual(camera.armed, True) + if camera.id == str(const.DEVICE_ID): + self.assertEqual(name, const.CAMERA_NAME) + self.assertEqual(camera.armed, const.ARMED) + self.assertEqual(camera.motion['video'], BASE_URL + const.THUMB + '.mp4') + elif camera.id == str(const.DEVICE_ID2): + self.assertEqual(name, const.CAMERA_NAME2) + self.assertEqual(camera.armed, const.ARMED2) self.assertEqual(len(camera.motion.keys()), 0) else: assert False is True + + @mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post) + @mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get) + def test_blink_refresh(self, mock_get, mock_post): + blink = blinkpy.Blink(username='user',password='password') + blink.setup_system() + const.response['devices'][0]['thumbnail'] = const.THUMB + const.THUMB2 + blink.refresh() + for name, camera in blink.cameras.items(): + if camera.id == str(const.DEVICE_ID): + self.assertEqual(camera.thumbnail, BASE_URL + const.THUMB + const.THUMB2 + '.jpg') + elif camera.id == str(const.DEVICE_ID2): + pass + else: + assert False is True + + const.response['devices'][0]['thumbnail'] = 'new' + blink.cameras[const.CAMERA_NAME].image_refresh() + for name, camera in blink.cameras.items(): + if camera.id == str(const.DEVICE_ID): + self.assertEqual(camera.thumbnail, BASE_URL + 'new' + '.jpg') + elif camera.id == str(const.DEVICE_ID2): + pass + else: + assert False is True \ No newline at end of file diff --git a/tests/test_const.py b/tests/test_const.py new file mode 100644 index 0000000..f380274 --- /dev/null +++ b/tests/test_const.py @@ -0,0 +1,89 @@ +ISONLINE = True +ARMED = True +ARMED2 = False +REGION_ID = 'test' +REGION = 'Oceaniaeurasia' +TOKEN = 'abcd1234$$@@' +NETWORK_ID = 1337 +DEVICE_ID = 9876 +DEVICE_ID2 = 6789 +ACCOUNT_ID = 1234 +CAMERA_NAME = 'My Camera' +CAMERA_NAME2 = 'Number 2' +BATTERY = 3 +BATTERY2 = 1 +TEMP = 70 +TEMP2 = 66 +THUMB = '/url/camera/7777/clip' +THUMB2 = '/url/camera/8888/clip' +NOTIFS = 1 +NOTIFS2 = 1 + +if ISONLINE: + ONLINE = 'online' +else: + ONLINE = 'offline' + + +response = {'account': {'notifications': 1}, + 'devices': [{'device_type': 'camera', + 'notifications': NOTIFS, + 'battery': BATTERY, + 'active': 'disabled', + 'errors': 0, + 'error_msg': '', + 'enabled': False, + 'temp': TEMP, + 'updated_at': '2017-01-27T03:14:24+00:00', + 'lfr_strength': 3, + 'armed': ARMED, + 'device_id': DEVICE_ID, + 'wifi_strength': 5, + 'warning': 0, + 'thumbnail': THUMB, + 'name': CAMERA_NAME, + 'status': 'done'}, + {'device_type': 'camera', + 'notifications': NOTIFS2, + 'battery': BATTERY2, + 'active': 'disabled', + 'errors': 0, + 'error_msg': '', + 'enabled': False, + 'temp': TEMP2, + 'updated_at': '2017-01-27T03:14:24+00:00', + 'lfr_strength': 3, + 'armed': ARMED2, + 'device_id': DEVICE_ID2, + 'wifi_strength': 5, + 'warning': 0, + 'thumbnail': THUMB2, + 'name': CAMERA_NAME2, + 'status': 'done'}, + {'updated_at': '2017-01-26T19:32:10+00:00', + 'device_type': 'sync_module', + 'notifications': 0, + 'device_id': 0000, + 'status': + 'online', + 'last_hb': + '2017-01-27T03:14:49+00:00', + 'errors': 0, + 'error_msg': '', + 'warning': 0}], + 'network': {'armed': ARMED, + 'wifi_strength': 5, + 'warning': 0, + 'error_msg': '', + 'name': 'Blink', + 'notifications': NOTIFS, + 'status': 'ok'}, + 'events': [{'camera_id':DEVICE_ID, + 'type':'motion', + 'video_url':THUMB + '.mp4', + 'created_at':'2017-01-01'}, + {'camera_id':DEVICE_ID2, + 'type':'None'}], + 'syncmodule':{'name':'SyncName', + 'status':ONLINE}} + \ No newline at end of file From 529db79ea455d7491f67f2196d60f1b0ea53c03c Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 27 Jan 2017 00:03:35 -0500 Subject: [PATCH 04/21] Added a thumb dict function --- blinkpy.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/blinkpy.py b/blinkpy.py index 3b09577..e0d2228 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,7 +17,7 @@ import requests import getpass import json -__version__ = '0.4.0' +__version__ = '0.4.1' BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' @@ -228,6 +228,15 @@ class Blink(object): def cameras(self): return self._CAMERAS + @property + def camera_thumbs(self): + self.refresh() + data = {} + for name, camera in self._CAMERAS.items(): + data[name] = camera.thumbnail + + return data + @property def id_table(self): return self._IDLOOKUP From 0023be40c13fa8abcb875854b16cca8dfb83ba86 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 28 Jan 2017 15:17:10 -0500 Subject: [PATCH 05/21] Updated test_const with realistic response and fixed key error in blinkpy --- blinkpy.py | 4 +-- tests/test_const.py | 83 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/blinkpy.py b/blinkpy.py index e0d2228..20e599d 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,7 +17,7 @@ import requests import getpass import json -__version__ = '0.4.1' +__version__ = '0.4.2' BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' @@ -262,7 +262,7 @@ class Blink(object): """Gets all events on server""" url = BASE_URL + '/events/network/' + self._NETWORKID headers = self._AUTH_HEADER - self._EVENTS = _request(url, headers=headers, type='get')['events'] + self._EVENTS = _request(url, headers=headers, type='get')['event'] return self._EVENTS @property diff --git a/tests/test_const.py b/tests/test_const.py index f380274..f0dd330 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -18,6 +18,7 @@ THUMB = '/url/camera/7777/clip' THUMB2 = '/url/camera/8888/clip' NOTIFS = 1 NOTIFS2 = 1 +SYNC_ID = 0000 if ISONLINE: ONLINE = 'online' @@ -63,7 +64,7 @@ response = {'account': {'notifications': 1}, {'updated_at': '2017-01-26T19:32:10+00:00', 'device_type': 'sync_module', 'notifications': 0, - 'device_id': 0000, + 'device_id': SYNC_ID, 'status': 'online', 'last_hb': @@ -78,12 +79,80 @@ response = {'account': {'notifications': 1}, 'name': 'Blink', 'notifications': NOTIFS, 'status': 'ok'}, - 'events': [{'camera_id':DEVICE_ID, - 'type':'motion', - 'video_url':THUMB + '.mp4', - 'created_at':'2017-01-01'}, - {'camera_id':DEVICE_ID2, - 'type':'None'}], + + 'event': [{'camera_name': CAMERA_NAME, + 'updated_at': '2017-01-28T19:51:52+00:00', + 'sync_module_id': None, + 'camera': DEVICE_ID, + 'type': 'motion', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T19:51:52+00:00', + 'camera_id': DEVICE_ID, + 'id': 123456789, + 'siren_id': None, + 'account_id': ACCOUNT_ID, + 'notified': True, + 'siren': None, + 'syncmodule': None, + 'video_url': THUMB+'.mp4', + 'command_id': None, + 'network_id': None, + 'account': ACCOUNT_ID, + 'video_id': 100000001}, + {'updated_at': '2017-01-28T19:51:29+00:00', + 'sync_module_id': SYNC_ID, + 'camera': None, + 'type': 'armed', + 'duration': None, + 'status': None, + 'created_at': + '2017-01-28T19:51:29+00:00', + 'camera_id': None, + 'id': 123456789, + 'siren_id': None, + 'account_id': ACCOUNT_ID, + 'notified': False, + 'siren': None, + 'syncmodule': SYNC_ID, + 'command_id': None, + 'network_id': None, + 'account': ACCOUNT_ID}, + {'updated_at': '2017-01-28T19:00:12+00:00', + 'sync_module_id': SYNC_ID, + 'camera': None, + 'type': 'disarmed', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T19:00:12+00:00', + 'camera_id': None, + 'id': 123456789, + 'siren_id': None, + 'account_id': 2463, + 'notified': False, + 'siren': None, + 'syncmodule': SYNC_ID, + 'command_id': None, + 'network_id': None, + 'account': ACCOUNT_ID}, + {'camera_name': CAMERA_NAME, + 'updated_at': '2017-01-28T18:59:55+00:00', + 'sync_module_id': None, + 'camera': DEVICE_ID, + 'type': 'motion', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T18:59:55+00:00', + 'camera_id': DEVICE_ID, + 'id': 123456789, + 'siren_id': None, + 'account_id': ACCOUNT_ID, + 'notified': True, + 'siren': None, + 'syncmodule': None, + 'command_id': None, + 'network_id': None, + 'account': ACCOUNT_ID}], 'syncmodule':{'name':'SyncName', 'status':ONLINE}} \ No newline at end of file From d387a4015494cbcf92bf0f741aba25fffe44a4dc Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 5 Mar 2017 21:56:52 -0500 Subject: [PATCH 06/21] Changed to wheel and incremented minor version --- blinkpy.py | 2 -- setup.py | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/blinkpy.py b/blinkpy.py index 20e599d..178d2df 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,8 +17,6 @@ import requests import getpass import json -__version__ = '0.4.2' - BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' BASE_URL = 'https://prod.' + BLINK_URL diff --git a/setup.py b/setup.py index f62073e..c99a7df 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from blinkpy import __version__ from setuptools import setup setup( name = 'blinkpy', - version = __version__, + version = '0.4.', description = 'A Blink camera Python library', long_description='A library that communicates with Blink cameras', author = 'Kevin Fronczak', @@ -13,7 +12,7 @@ setup( license='MIT', url = 'https://github.com/fronzbot/blinkpy', py_modules=['blinkpy'], - install_requires=['requests'], + install_requires=['requests>=2,<3'], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', From 11b683a156dfbb918dd74b53650efdcc5e2ab969 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 6 Mar 2017 23:20:46 -0500 Subject: [PATCH 07/21] Fixed issue with host name in header, added test to catch this --- .gitignore | 1 + blinkpy.py | 2 +- setup.py | 2 +- tests/test_blink_requests.py | 1 + tests/test_const.py | 5 ++++- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 561086d..2155ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist/* MANIFEST README .sh +build/* diff --git a/blinkpy.py b/blinkpy.py index 178d2df..4c33422 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -376,7 +376,7 @@ class Blink(object): response = _request(LOGIN_URL, headers=headers, data=data, type='post') self._TOKEN = response['authtoken']['authtoken'] (self._REGION_ID, self._REGION), = response['region'].items() - self._HOST = self._REGION + '.' + BLINK_URL + self._HOST = self._REGION_ID + '.' + BLINK_URL self._AUTH_HEADER = {'Host': self._HOST, 'TOKEN_AUTH': self._TOKEN } diff --git a/setup.py b/setup.py index c99a7df..e05ff80 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup( name = 'blinkpy', - version = '0.4.', + version = '0.4.4', description = 'A Blink camera Python library', long_description='A library that communicates with Blink cameras', author = 'Kevin Fronczak', diff --git a/tests/test_blink_requests.py b/tests/test_blink_requests.py index 28776bf..f88c103 100644 --- a/tests/test_blink_requests.py +++ b/tests/test_blink_requests.py @@ -66,6 +66,7 @@ class TestBlinkRequests(unittest.TestCase): self.assertEqual(name, const.CAMERA_NAME) self.assertEqual(camera.armed, const.ARMED) self.assertEqual(camera.motion['video'], BASE_URL + const.THUMB + '.mp4') + self.assertEqual(camera.header, const.auth_header) elif camera.id == str(const.DEVICE_ID2): self.assertEqual(name, const.CAMERA_NAME2) self.assertEqual(camera.armed, const.ARMED2) diff --git a/tests/test_const.py b/tests/test_const.py index f0dd330..415b86b 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -24,7 +24,10 @@ if ISONLINE: ONLINE = 'online' else: ONLINE = 'offline' - + +auth_header = {'Host': REGION_ID+'.immedia-semi.com', + 'TOKEN_AUTH': TOKEN + } response = {'account': {'notifications': 1}, 'devices': [{'device_type': 'camera', From 0a2e087eaa1aee42d7cd91b8bdaca8aabb8ae8f6 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 8 Mar 2017 20:57:00 -0500 Subject: [PATCH 08/21] Working on updating with constants. This commit is broken --- blinkpy.py | 8 ++++---- constants.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 constants.py diff --git a/blinkpy.py b/blinkpy.py index 4c33422..627d5c4 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -185,7 +185,7 @@ class BlinkCamera(object): self._NOTIFICATIONS = values['notifications'] def image_refresh(self): - url = BASE_URL + '/homescreen' + url = HOME_URL response = _request(url, headers=self._HEADER, type='get')['devices'] for element in response: try: @@ -258,7 +258,7 @@ class Blink(object): @property def events(self): """Gets all events on server""" - url = BASE_URL + '/events/network/' + self._NETWORKID + url = EVENT_URL + self._NETWORKID headers = self._AUTH_HEADER self._EVENTS = _request(url, headers=headers, type='get')['event'] return self._EVENTS @@ -266,10 +266,10 @@ class Blink(object): @property def online(self): """Returns True or False depending on if sync module is online/offline""" - url = BASE_URL + 'network/' + self._NETWORKID + '/syncmodules' + url = NETWORK_URL + self._NETWORKID + '/syncmodules' headers = self._AUTH_HEADER online_dict = {'online': True, 'offline': False} - return online_dict[_request(url, headers=headers, type='get')['syncmodule']['status']] + return online_dict[_request(url, headers=headers, type='get')[KEY_SYNCMODULE]['status']] def last_motion(self): """Finds last motion of each camera""" diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..bd79471 --- /dev/null +++ b/constants.py @@ -0,0 +1,20 @@ +''' +constants.py +Generates constants for use in blinkpy +''' + +''' +URLS +''' +BLINK_URL = 'immedia-semi.com' +LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' +BASE_URL = 'https://prod.' + BLINK_URL +DEFAULT_URL = 'prod.' + BLINK_URL +HOME_URL = BASE_URL + '/homescreen' +EVENT_URL = BASE_URL + '/events/network/' +NETWORK_URL = BASE_URL + '/network/' + +''' +DICT KEYS +''' + From 7fa135317364eda2cdf29da729b41132553badc1 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 8 Mar 2017 21:26:28 -0500 Subject: [PATCH 09/21] Working on updating constants --- blinkpy.py | 14 ++++---------- constants.py | 4 ++-- errors.py | 4 ++++ 3 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 errors.py diff --git a/blinkpy.py b/blinkpy.py index f73aa21..f5b33b7 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -5,9 +5,7 @@ blinkpy by Kevin Fronczak - A Blink camera Python library https://github.com/fronzbot/blinkpy Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol - Published under the MIT license - See LICENSE file for more details. - "Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information. I am in no way affiliated with Blink, nor Immedia Inc. ''' @@ -185,11 +183,7 @@ class BlinkCamera(object): self._NOTIFICATIONS = values['notifications'] def image_refresh(self): -<<<<<<< HEAD - url = HOME_URL -======= url = BASE_URL + '/homescreen' ->>>>>>> 8206a49dc76913b9f175bb473e17758fd2e26956 response = _request(url, headers=self._HEADER, type='get')['devices'] for element in response: try: @@ -262,7 +256,7 @@ class Blink(object): @property def events(self): """Gets all events on server""" - url = EVENT_URL + self._NETWORKID + url = BASE_URL + '/events/network/' + self._NETWORKID headers = self._AUTH_HEADER self._EVENTS = _request(url, headers=headers, type='get')['event'] return self._EVENTS @@ -270,10 +264,10 @@ class Blink(object): @property def online(self): """Returns True or False depending on if sync module is online/offline""" - url = NETWORK_URL + self._NETWORKID + '/syncmodules' + url = BASE_URL + 'network/' + self._NETWORKID + '/syncmodules' headers = self._AUTH_HEADER online_dict = {'online': True, 'offline': False} - return online_dict[_request(url, headers=headers, type='get')[KEY_SYNCMODULE]['status']] + return online_dict[_request(url, headers=headers, type='get')['syncmodule']['status']] def last_motion(self): """Finds last motion of each camera""" @@ -395,4 +389,4 @@ class Blink(object): response = _request(url, headers=headers, type='get') self._NETWORKID = str(response['networks'][0]['id']) - self._ACCOUNTID = str(response['networks'][0]['account_id']) + self._ACCOUNTID = str(response['networks'][0]['account_id']) \ No newline at end of file diff --git a/constants.py b/constants.py index bd79471..64e9daa 100644 --- a/constants.py +++ b/constants.py @@ -15,6 +15,6 @@ EVENT_URL = BASE_URL + '/events/network/' NETWORK_URL = BASE_URL + '/network/' ''' -DICT KEYS +Dictionaries ''' - +ONLINE = {'online': True, 'offline': False} \ No newline at end of file diff --git a/errors.py b/errors.py new file mode 100644 index 0000000..6d3fd36 --- /dev/null +++ b/errors.py @@ -0,0 +1,4 @@ +USERNAME = (0, "Username must be a string") +PASSWORD = (1, "Password must be a string") +AUTHENTICATE = (2, "Cannot authenticate since either password or username has not been set") + \ No newline at end of file From 7b07146afa24c91e9476c77e8a3ab87c30d38d49 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 8 Mar 2017 23:11:56 -0500 Subject: [PATCH 10/21] Constants and better style testing --- blinkpy.py | 355 +++++++++++++++++++++++++----------------- constants.py | 34 ++++ errors.py | 2 + pylintrc | 38 +++++ setup.py | 38 +++-- tests/test_servers.py | 8 + tox.ini | 18 ++- 7 files changed, 322 insertions(+), 171 deletions(-) create mode 100644 pylintrc create mode 100644 tests/test_servers.py diff --git a/blinkpy.py b/blinkpy.py index f5b33b7..7f58a4c 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -4,203 +4,242 @@ ''' blinkpy by Kevin Fronczak - A Blink camera Python library https://github.com/fronzbot/blinkpy -Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol +Original protocol hacking by MattTW : +https://github.com/MattTW/BlinkMonitorProtocol Published under the MIT license - See LICENSE file for more details. -"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information. +"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark +owned by Immedia Inc., see www.blinkforhome.com for more information. I am in no way affiliated with Blink, nor Immedia Inc. ''' -import logging -import requests -import getpass import json - -BLINK_URL = 'immedia-semi.com' -LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' -BASE_URL = 'https://prod.' + BLINK_URL -DEFAULT_URL = 'prod.' + BLINK_URL - -logger = logging.getLogger('blinkpy') +import getpass +import requests +import errors as ERROR +from constants import (BLINK_URL, LOGIN_URL, + BASE_URL, DEFAULT_URL, + HOME_URL, EVENT_URL, + NETWORK_URL, NETWORKS_URL, + ONLINE) -def _request(url, data=None, headers=None, type='get', stream=False, json=True): +def _request(url, data=None, headers=None, reqtype='get', + stream=False, json_resp=True): """Wrapper function for request""" - if type is 'post': - response = requests.post(url, headers=headers, data=data).json() - elif type is 'get' and json: - response = requests.get(url, headers=headers, stream=stream).json() - elif type is 'get' and not json: - response = requests.get(url, headers=headers, stream=stream) + if reqtype is 'post': + response = requests.post(url, headers=headers, + data=data).json() + elif reqtype is 'get' and json_resp: + response = requests.get(url, headers=headers, + stream=stream).json() + elif reqtype is 'get' and not json_resp: + response = requests.get(url, headers=headers, + stream=stream) else: - raise ValueError("Cannot perform requests of type " + type) + raise BlinkException(ERROR.REQUEST) if json and 'message' in response.keys(): - raise BlinkAuthenticationException(response['code'], response['message']) + raise BlinkAuthenticationException( + (response['code'], response['message'])) return response +# pylint: disable=super-init-not-called class BlinkException(Exception): - def __init__(self, id, message): - self.id = id - self.message = message + """ + Class to throw general blink exception. + """ + def __init__(self, errcode): + self.errid = errcode[0] + self.message = errcode[1] class BlinkAuthenticationException(BlinkException): + """ + Class to throw authentication exception. + """ pass class BlinkCamera(object): - """Class to initialize individual camera""" + """Class to initialize individual camera.""" def __init__(self, config): - self._ID = str(config['device_id']) - self._NAME = config['name'] - self._STATUS = config['armed'] - self._THUMB = BASE_URL + config['thumbnail'] + '.jpg' - self._CLIP = BASE_URL + config['thumbnail'] + '.mp4' - self._TEMPERATURE = config['temp'] - self._BATTERY = config['battery'] - self._NOTIFICATIONS = config['notifications'] - self._MOTION = {} - self._HEADER = None - self._IMAGE_LINK = None - self._ARM_LINK = None - self._REGION_ID = config['region_id'] + self._id = str(config['device_id']) + self._name = config['name'] + self._status = config['armed'] + self._thumb = BASE_URL + config['thumbnail'] + '.jpg' + self._clip = BASE_URL + config['thumbnail'] + '.mp4' + self._temperature = config['temp'] + self._battery = config['battery'] + self._notifications = config['notifications'] + self._motion = {} + self._header = None + self._image_link = None + self._arm_link = None + self._region_id = config['region_id'] @property + # pylint: disable=invalid-name def id(self): - return self._ID + """Returns camera id""" + return self._id @property def name(self): - return self._NAME + """Returns camera name""" + return self._name @name.setter def name(self, value): - self._NAME = value + """Sets camera name""" + self._name = value @property def region_id(self): - return self._REGION_ID + """Returns region id""" + return self._region_id @property def armed(self): - return self._STATUS + """Returns camera arm status""" + return self._status @property def clip(self): - return self._CLIP + """Returns current clip""" + return self._clip @clip.setter def clip(self, value): - self._CLIP = value + """Sets current clip""" + self._clip = value @property def thumbnail(self): - # RUN THUMB ACQ HERE - return self._THUMB + """Returns current thumbnail""" + return self._thumb @thumbnail.setter def thumbnail(self, value): - self._THUMB = value + """Sets current thumbnail""" + self._thumb = value @property def temperature(self): - return self._TEMPERATURE + """Returns camera temperature""" + return self._temperature @temperature.setter def temperature(self, value): - self._TEMPERATURE = value + """Sets camera temperature""" + self._temperature = value @property def battery(self): - return self._BATTERY + """Returns battery level""" + return self._battery @battery.setter def battery(self, value): - self._BATTERY = value + """Sets battery level""" + self._battery = value @property def notifications(self): - return self._NOTIFICATIONS + """Returns number of notifications""" + return self._notifications @notifications.setter def notifications(self, value): - self._NOTIFICATIONS = value + """Sets number of notifications""" + self._notifications = value @property def image_link(self): - return self._IMAGE_LINK + """Returns image link""" + return self._image_link @image_link.setter def image_link(self, value): - self._IMAGE_LINK = value + """Sets image link""" + self._image_link = value @property def arm_link(self): - return self._ARM_LINK + """Returns link to arm camera""" + return self._arm_link @arm_link.setter def arm_link(self, value): - self._ARM_LINK = value + """Sets link to arm camera""" + self._arm_link = value @property def header(self): - return self._HEADER + """Returns request header""" + return self._header @header.setter def header(self, value): - self._HEADER = value + """Sets request header""" + self._header = value @property def motion(self): - return self._MOTION + """Returns last motion event detail""" + return self._motion @motion.setter def motion(self, value): """Sets link to last motion and timestamp""" - self._MOTION = value + self._motion = value def snap_picture(self): """Takes a picture with camera to create a new thumbnail""" - _request(self._IMAGE_LINK, headers=self._HEADER, type='post') + _request(self._image_link, headers=self._header, reqtype='post') def set_motion_detect(self, enable): """Sets motion detection""" - url = self._ARM_LINK + url = self._arm_link if enable: - _request(url + 'enable', headers=self._HEADER, type='post') + _request(url + 'enable', headers=self._header, reqtype='post') else: - _request(url + 'disable', headers=self._HEADER, type='post') + _request(url + 'disable', headers=self._header, reqtype='post') def update(self, values): - self._NAME = values['name'] - self._STATUS = values['armed'] - self._THUMB = BASE_URL + values['thumbnail'] + '.jpg' - self._CLIP = BASE_URL + values['thumbnail'] + '.mp4' - self._TEMPERATURE = values['temp'] - self._BATTERY = values['battery'] - self._NOTIFICATIONS = values['notifications'] + """Updates camera information""" + self._name = values['name'] + self._status = values['armed'] + self._thumb = BASE_URL + values['thumbnail'] + '.jpg' + self._clip = BASE_URL + values['thumbnail'] + '.mp4' + self._temperature = values['temp'] + self._battery = values['battery'] + self._notifications = values['notifications'] def image_refresh(self): - url = BASE_URL + '/homescreen' - response = _request(url, headers=self._HEADER, type='get')['devices'] + """Refreshs current thumbnail""" + url = HOME_URL + response = _request(url, headers=self._header, + reqtype='get')['devices'] for element in response: try: - if str(element['device_id']) == self._ID: - self._THUMB = BASE_URL + element['thumbnail'] + '.jpg' - return self._THUMB + if str(element['device_id']) == self._id: + self._thumb = BASE_URL + element['thumbnail'] + '.jpg' + return self._thumb except KeyError: pass return None def image_to_file(self, path): + """Writes image to file""" thumb = self.image_refresh() - response = _request(thumb, headers=self._HEADER, stream=True, json=False) + response = _request(thumb, headers=self._header, + stream=True, json_resp=False) if response.status_code == 200: - with open(path, 'wb') as f: + with open(path, 'wb') as imgfile: for chunk in response: - f.write(chunk) + imgfile.write(chunk) class Blink(object): @@ -209,65 +248,76 @@ class Blink(object): """Constructor for class""" self._username = username self._password = password - self._TOKEN = None - self._AUTH_HEADER = None - self._NETWORKID = None - self._ACCOUNTID = None - self._REGION = None - self._REGION_ID = None - self._HOST = None - self._EVENTS = [] - self._CAMERAS = {} - self._IDLOOKUP = {} + self._token = None + self._auth_header = None + self._network_id = None + self._account_id = None + self._region = None + self._region_id = None + self._host = None + self._events = [] + self._cameras = {} + self._idlookup = {} @property def cameras(self): - return self._CAMERAS + """Returns camera/id pairs""" + return self._cameras @property def camera_thumbs(self): + """Returns camera thumbnails""" self.refresh() data = {} - for name, camera in self._CAMERAS.items(): + for name, camera in self._cameras.items(): data[name] = camera.thumbnail return data @property def id_table(self): - return self._IDLOOKUP + """Returns id/camera pairs""" + return self._idlookup @property def network_id(self): - return self._NETWORKID + """Returns network id""" + return self._network_id @property def account_id(self): - return self._ACCOUNTID + """Returns account id""" + return self._account_id @property def region(self): - return self._REGION + """Returns current region""" + return self._region @property def region_id(self): - return self._REGION_ID + """Returns region id""" + return self._region_id @property def events(self): """Gets all events on server""" - url = BASE_URL + '/events/network/' + self._NETWORKID - headers = self._AUTH_HEADER - self._EVENTS = _request(url, headers=headers, type='get')['event'] - return self._EVENTS + url = EVENT_URL + self._network_id + headers = self._auth_header + self._events = _request(url, headers=headers, + reqtype='get')['event'] + return self._events @property def online(self): - """Returns True or False depending on if sync module is online/offline""" - url = BASE_URL + 'network/' + self._NETWORKID + '/syncmodules' - headers = self._AUTH_HEADER - online_dict = {'online': True, 'offline': False} - return online_dict[_request(url, headers=headers, type='get')['syncmodule']['status']] + """ + Returns True or False depending on if + sync module is online/offline + """ + url = NETWORK_URL + self._network_id + '/syncmodules' + headers = self._auth_header + return ONLINE[_request(url, headers=headers, + reqtype='get')['syncmodule']['status']] def last_motion(self): """Finds last motion of each camera""" @@ -275,11 +325,13 @@ class Blink(object): for element in recent: try: camera_id = str(element['camera_id']) - camera_name = self._IDLOOKUP[camera_id] - camera = self._CAMERAS[camera_name] + camera_name = self._idlookup[camera_id] + camera = self._cameras[camera_name] if element['type'] == 'motion': url = BASE_URL + element['video_url'] - camera.motion = {'video': url, 'image': url[:-3] + 'jpg', 'time': element['created_at']} + camera.motion = {'video': url, + 'image': url[:-3] + 'jpg', + 'time': element['created_at']} except KeyError: pass @@ -290,19 +342,23 @@ class Blink(object): @arm.setter def arm(self, value): - """Arms or disarms system. Arms/disarms all if camera not named""" + """ + Arms or disarms system. + Arms/disarms all if camera not named. + """ if value: value_to_append = 'arm' else: value_to_append = 'disarm' - url = BASE_URL + '/network/' + self._NETWORKID + '/' + value_to_append - _request(url, headers=self._AUTH_HEADER, type='post') + url = NETWORK_URL + self._network_id + '/' + value_to_append + _request(url, headers=self._auth_header, reqtype='post') def refresh(self): """Gets all blink cameras and pulls their most recent status""" response = self.get_summary()['devices'] - for name, camera in self._CAMERAS.items(): + for name in self._cameras: + camera = self._cameras[name] for element in response: try: if str(element['device_id']) == camera.id: @@ -313,12 +369,12 @@ class Blink(object): def get_summary(self): """Gets a full summary of device information""" url = BASE_URL + '/homescreen' - headers = self._AUTH_HEADER + headers = self._auth_header - if self._AUTH_HEADER is None: - raise BlinkException(0, "Authentication header incorrect. Are you sure you logged in and received your token?") + if self._auth_header is None: + raise BlinkException(ERROR.AUTH_TOKEN) - return _request(url, headers=headers, type='get') + return _request(url, headers=headers, reqtype='get') def get_cameras(self): """Finds and creates cameras""" @@ -327,24 +383,32 @@ class Blink(object): if 'device_type' in element.keys(): if element['device_type'] == 'camera': # Add region to config - element['region_id'] = self._REGION_ID + element['region_id'] = self._region_id device = BlinkCamera(element) - self._CAMERAS[device.name] = device - self._IDLOOKUP[device.id] = device.name + self._cameras[device.name] = device + self._idlookup[device.id] = device.name def set_links(self): - """Sets access links and required headers for each camera in system""" - for name, camera in self._CAMERAS.items(): - image_url = BASE_URL + '/network/' + self._NETWORKID + '/camera/' + camera.id + '/thumbnail' - arm_url = BASE_URL + '/network/' + self._NETWORKID + '/camera/' + camera.id + '/' + """ + Sets access links and required headers + for each camera in system + """ + for name in self._cameras: + camera = self._cameras[name] + network_id_url = NETWORK_URL + self._network_id + image_url = network_id_url + '/camera/' + camera.id + '/thumbnail' + arm_url = network_id_url + '/camera/' + camera.id + '/' camera.image_link = image_url camera.arm_link = arm_url - camera.header = self._AUTH_HEADER + camera.header = self._auth_header def setup_system(self): - """Method logs in and sets auth token and network ids for future requests""" + """ + Method logs in and sets auth token and + network ids for future requests. + """ if self._username is None or self._password is None: - raise BlinkAuthenticationException(3, "Cannot authenticate since either password or username has not been set") + raise BlinkAuthenticationException(ERROR.AUTHENTICATE) self.get_auth_token() self.get_ids() @@ -359,34 +423,33 @@ class Blink(object): def get_auth_token(self): """Retrieves the authentication token from Blink""" if not isinstance(self._username, str): - raise BlinkAuthenticationException(0, "Username must be a string") + raise BlinkAuthenticationException(ERROR.USERNAME) if not isinstance(self._password, str): - raise BlinkAuthenticationException(0, "Password must be a string") + raise BlinkAuthenticationException(ERROR.PASSWORD) headers = {'Host': DEFAULT_URL, - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json'} data = json.dumps({ "email": self._username, "password": self._password, "client_specifier": "iPhone 9.2 | 2.2 | 222" }) - response = _request(LOGIN_URL, headers=headers, data=data, type='post') - self._TOKEN = response['authtoken']['authtoken'] - (self._REGION_ID, self._REGION), = response['region'].items() - self._HOST = self._REGION_ID + '.' + BLINK_URL - self._AUTH_HEADER = {'Host': self._HOST, - 'TOKEN_AUTH': self._TOKEN - } + response = _request(LOGIN_URL, headers=headers, + data=data, reqtype='post') + self._token = response['authtoken']['authtoken'] + (self._region_id, self._region), = response['region'].items() + self._host = self._region_id + '.' + BLINK_URL + self._auth_header = {'Host': self._host, + 'TOKEN_AUTH': self._token} def get_ids(self): """Sets the network ID and Account ID""" - url = BASE_URL + '/networks' - headers = self._AUTH_HEADER + url = NETWORKS_URL + headers = self._auth_header - if self._AUTH_HEADER is None: - raise BlinkException(0, "Authentication header incorrect. Are you sure you logged in and received your token?") + if self._auth_header is None: + raise BlinkException(ERROR.AUTH_TOKEN) - response = _request(url, headers=headers, type='get') - self._NETWORKID = str(response['networks'][0]['id']) - self._ACCOUNTID = str(response['networks'][0]['account_id']) \ No newline at end of file + response = _request(url, headers=headers, reqtype='get') + self._network_id = str(response['networks'][0]['id']) + self._account_id = str(response['networks'][0]['account_id']) diff --git a/constants.py b/constants.py index 64e9daa..f09e4c8 100644 --- a/constants.py +++ b/constants.py @@ -2,6 +2,39 @@ constants.py Generates constants for use in blinkpy ''' +MAJOR_VERSION = 0 +MINOR_VERSION = 4 +PATCH_VERSION = 4 +__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) + +REQUIRED_PYTHON_VER = (3, 4, 2) + +PROJECT_NAME = 'blinkpy' +PROJECT_PACKAGE_NAME = 'blinkpy' +PROJECT_LICENSE = 'MIT' +PROJECT_AUTHOR = 'Kevin Fronczak' +PROJECT_COPYRIGHT = ' 2017, {}'.format(PROJECT_AUTHOR) +PROJECT_URL = 'https://github.com/fronzbot/blinkpy' +PROJECT_EMAIL = 'kfronczak@gmail.com' +PROJECT_DESCRIPTION = ('A Blink camera Python library ' + 'running on Python 3.') +PROJECT_LONG_DESCRIPTION = ('blinkpy is an open-source ' + 'unofficial API for the Blink Camera ' + 'system with the intention for easy ' + 'integration into various home ' + 'automation platforms.') +PROJECT_CLASSIFIERS = [ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.4', + 'Topic :: Home Automation' +] + +PROJECT_GITHUB_USERNAME = 'home-assistant' +PROJECT_GITHUB_REPOSITORY = 'home-assistant' + +PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) ''' URLS @@ -13,6 +46,7 @@ DEFAULT_URL = 'prod.' + BLINK_URL HOME_URL = BASE_URL + '/homescreen' EVENT_URL = BASE_URL + '/events/network/' NETWORK_URL = BASE_URL + '/network/' +NETWORKS_URL = BASE_URL + '/networks' ''' Dictionaries diff --git a/errors.py b/errors.py index 6d3fd36..725c683 100644 --- a/errors.py +++ b/errors.py @@ -1,4 +1,6 @@ USERNAME = (0, "Username must be a string") PASSWORD = (1, "Password must be a string") AUTHENTICATE = (2, "Cannot authenticate since either password or username has not been set") +AUTH_TOKEN = (3, "Authentication header incorrect. Are you sure you logged in and received your token?") +REQUEST = (4, "Cannot perform request (get/post type incorrect)") \ No newline at end of file diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..1ec5ded --- /dev/null +++ b/pylintrc @@ -0,0 +1,38 @@ +[MASTER] +reports=no + +# Reasons disabled: +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# abstract-class-not-used - is flaky, should not show up but does +# unused-argument - generic callbacks and setup methods create a lot of warnings +# global-statement - used for the on-demand requirement installation +# redefined-variable-type - this is Python, we're duck typing! +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing + +disable= + locally-disabled, + duplicate-code, + cyclic-import, + abstract-class-little-used, + abstract-class-not-used, + unused-argument, + global-statement, + redefined-variable-type, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-locals, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + too-many-lines, + too-few-public-methods, + abstract-method + +[EXCEPTIONS] +overgeneral-exceptions=Exception \ No newline at end of file diff --git a/setup.py b/setup.py index e05ff80..71b8d98 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,24 @@ # -*- coding: utf-8 -*- from setuptools import setup +from constants import (__version__, PROJECT_PACKAGE_NAME, + PROJECT_LICENSE, PROJECT_URL, + PROJECT_EMAIL, PROJECT_DESCRIPTION, + PROJECT_CLASSIFIERS, PROJECT_AUTHOR, + PROJECT_LONG_DESCRIPTION) setup( - name = 'blinkpy', - version = '0.4.4', - description = 'A Blink camera Python library', - long_description='A library that communicates with Blink cameras', - author = 'Kevin Fronczak', - author_email = "kfronczak@gmail.com", - license='MIT', - url = 'https://github.com/fronzbot/blinkpy', - py_modules=['blinkpy'], - install_requires=['requests>=2,<3'], - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Environment :: Plugins', - 'Environment :: Web Environment' - ] + name = PROJECT_PACKAGE_NAME, + version = __version__, + description = PROJECT_DESCRIPTION, + long_description = PROJECT_LONG_DESCRIPTION, + author = PROJECT_AUTHOR, + author_email = PROJECT_EMAIL, + license = PROJECT_LICENSE, + url = PROJECT_URL, + platforms = 'any', + py_modules = ['blinkpy'], + install_requires = ['requests>=2,<3'], + test_suite = 'tests', + classifiers = PROJECT_CLASSIFIERS ) \ No newline at end of file diff --git a/tests/test_servers.py b/tests/test_servers.py new file mode 100644 index 0000000..08e6631 --- /dev/null +++ b/tests/test_servers.py @@ -0,0 +1,8 @@ +import requests +import unittest +from constants import BASE_URL + +class TestRemoteServerResponse(unittest.TestCase): + def test_request_response(self): + response = requests.get(BASE_URL) + self.assertEqual(response.ok, True) \ No newline at end of file diff --git a/tox.ini b/tox.ini index 660f208..1441238 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py34, pep8 +envlist = py34, py35, py36, lint +skip_missing_interpreters = True [testenv] setenv = @@ -10,14 +11,21 @@ commands = deps = -r{toxinidir}/requirements.txt -[testenv:pep8] +[testenv:lint] deps = -r{toxinidir}/requirements.txt flake8 + pydocstyle + pylint basepython = python3 +ignore_errors = True commands = + ;flake8 blinkpy.py tests flake8 blinkpy.py + ;pylint blinkpy.py tests + pylint blinkpy.py + ;pydocstyle blinkpy.py tests -[flake8] -ignore = E501 -exclude = .venv,.git,.tox,dist,doc,*lib/python,*egg,build \ No newline at end of file +;[flake8] +;ignore = E501 +;exclude = .venv,.git,.tox,dist,doc,*lib/python,*egg,build \ No newline at end of file From 14c7322cde23ab0e659f1ee87361eb9babfab7b3 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 8 Mar 2017 23:19:06 -0500 Subject: [PATCH 11/21] Removed leftover merge junk --- tests/test_const.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test_const.py b/tests/test_const.py index 1389a05..b03f96c 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -18,11 +18,7 @@ THUMB = '/url/camera/7777/clip' THUMB2 = '/url/camera/8888/clip' NOTIFS = 1 NOTIFS2 = 1 -<<<<<<< HEAD -SYNC_ID = 0000 -======= SYNC_ID = 1000 ->>>>>>> 8206a49dc76913b9f175bb473e17758fd2e26956 if ISONLINE: ONLINE = 'online' @@ -85,12 +81,7 @@ response = {'account': {'notifications': 1}, 'error_msg': '', 'name': 'Blink', 'notifications': NOTIFS, -<<<<<<< HEAD - 'status': 'ok'}, - -======= 'status': 'ok'}, ->>>>>>> 8206a49dc76913b9f175bb473e17758fd2e26956 'event': [{'camera_name': CAMERA_NAME, 'updated_at': '2017-01-28T19:51:52+00:00', 'sync_module_id': None, From dad1aec874b256989390eeb3b44546bc3c37024c Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Thu, 9 Mar 2017 22:50:54 -0500 Subject: [PATCH 12/21] Initial checkin for test_blink_system.py --- blinkpy.py | 3 ++- tests/test_blink_system.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/test_blink_system.py diff --git a/blinkpy.py b/blinkpy.py index 7f58a4c..67aa2ca 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -422,11 +422,12 @@ class Blink(object): def get_auth_token(self): """Retrieves the authentication token from Blink""" + if not isinstance(self._username, str): raise BlinkAuthenticationException(ERROR.USERNAME) if not isinstance(self._password, str): raise BlinkAuthenticationException(ERROR.PASSWORD) - + headers = {'Host': DEFAULT_URL, 'Content-Type': 'application/json'} data = json.dumps({ diff --git a/tests/test_blink_system.py b/tests/test_blink_system.py new file mode 100644 index 0000000..9027beb --- /dev/null +++ b/tests/test_blink_system.py @@ -0,0 +1,41 @@ +import requests +import unittest +import blinkpy +from unittest import mock +import constants as const + +USERNAME = 'foobar' +PASSWORD = 'deadbeef' + +class TestBlinkSystem(unittest.TestCase): + """Test the Blink class in blinkpy.""" + def test_initialization(self): + """Verify we can initialize blink.""" + blink = blinkpy.Blink(username=USERNAME, password=PASSWORD) + self.assertEqual(blink._username, USERNAME) + self.assertEqual(blink._password, PASSWORD) + + def test_no_credentials(self): + """Check that we throw an exception when no username/password.""" + blink = blinkpy.Blink() + with self.assertRaises(blinkpy.BlinkAuthenticationException): + blink.get_auth_token() + with self.assertRaises(blinkpy.BlinkAuthenticationException): + blink.setup_system() + + def test_no_auth_header(self): + """Check that we throw an excpetion when no auth header given.""" + blink = blinkpy.Blink(username=USERNAME, password=PASSWORD) + with self.assertRaises(blinkpy.BlinkException): + blink.get_ids() + + @mock.patch('blinkpy.getpass.getpass') + def test_manual_login(self, getpwd): + """Check that we can manually use the login() function.""" + blink = blinkpy.Blink() + getpwd.return_value = PASSWORD + with mock.patch('builtins.input', return_value=USERNAME): + blink.login() + self.assertEqual(blink._username, USERNAME) + self.assertEqual(blink._password, PASSWORD) + \ No newline at end of file From afa7b2259fe0a5fac90611aa29c37c3ccf7d3028 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 10 Mar 2017 14:33:51 -0500 Subject: [PATCH 13/21] Fixed travis to run lint instead of pep8 and moved test reqs to own file --- .travis.yml | 2 +- blinkpy.py | 3 +-- requirements_test.txt | 3 +++ tests/test_blink_system.py | 4 +++- tox.ini | 5 ++--- 5 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 requirements_test.txt diff --git a/.travis.yml b/.travis.yml index e98d961..3b343ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ matrix: - python: "3.4.2" env: TOXENV=py34 - python: "3.4.2" - env: TOXENV=pep8 + env: TOXENV=lint - python: "3.5" env: TOXENV=py35 - python: "3.6" diff --git a/blinkpy.py b/blinkpy.py index 67aa2ca..7f58a4c 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -422,12 +422,11 @@ class Blink(object): def get_auth_token(self): """Retrieves the authentication token from Blink""" - if not isinstance(self._username, str): raise BlinkAuthenticationException(ERROR.USERNAME) if not isinstance(self._password, str): raise BlinkAuthenticationException(ERROR.PASSWORD) - + headers = {'Host': DEFAULT_URL, 'Content-Type': 'application/json'} data = json.dumps({ diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..27b840b --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,3 @@ +flake8==3.3 +pylint==1.6.5 +pydocstyle==1.1.1 \ No newline at end of file diff --git a/tests/test_blink_system.py b/tests/test_blink_system.py index 9027beb..2125685 100644 --- a/tests/test_blink_system.py +++ b/tests/test_blink_system.py @@ -7,7 +7,7 @@ import constants as const USERNAME = 'foobar' PASSWORD = 'deadbeef' -class TestBlinkSystem(unittest.TestCase): +class TestBlinkSetup(unittest.TestCase): """Test the Blink class in blinkpy.""" def test_initialization(self): """Verify we can initialize blink.""" @@ -38,4 +38,6 @@ class TestBlinkSystem(unittest.TestCase): blink.login() self.assertEqual(blink._username, USERNAME) self.assertEqual(blink._password, PASSWORD) + + # NEXT NEED ACTUAL REQUEST TESTS \ No newline at end of file diff --git a/tox.ini b/tox.ini index 1441238..e07099e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,13 +10,12 @@ commands = py.test -v --timeout=30 --duration=10 --cov=blinkpy --cov-report html {posargs} deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements_test.txt [testenv:lint] deps = -r{toxinidir}/requirements.txt - flake8 - pydocstyle - pylint + -r{toxinidir}/requirements_test.txt basepython = python3 ignore_errors = True commands = From 94dab01e6b0fb73a45867c139e252b848ad34952 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 11 Mar 2017 12:33:11 -0500 Subject: [PATCH 14/21] Blink tests complete, camera module next --- blinkpy.py | 65 ++++++++------ constants.py | 4 - tests/mock_responses.py | 152 +++++++++++++++++++++++++++++++++ tests/test_blink_cameras.py | 1 + tests/test_blink_requests.py | 101 ---------------------- tests/test_blink_system.py | 95 +++++++++++++++++---- tests/test_classes.py | 75 ---------------- tests/test_const.py | 160 ----------------------------------- tests/test_import.py | 6 -- tests/test_servers.py | 8 -- 10 files changed, 272 insertions(+), 395 deletions(-) create mode 100644 tests/mock_responses.py create mode 100644 tests/test_blink_cameras.py delete mode 100644 tests/test_blink_requests.py delete mode 100644 tests/test_classes.py delete mode 100644 tests/test_const.py delete mode 100644 tests/test_import.py delete mode 100644 tests/test_servers.py diff --git a/blinkpy.py b/blinkpy.py index 7f58a4c..de2bc81 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -17,10 +17,7 @@ import getpass import requests import errors as ERROR from constants import (BLINK_URL, LOGIN_URL, - BASE_URL, DEFAULT_URL, - HOME_URL, EVENT_URL, - NETWORK_URL, NETWORKS_URL, - ONLINE) + DEFAULT_URL, ONLINE) def _request(url, data=None, headers=None, reqtype='get', @@ -38,7 +35,7 @@ def _request(url, data=None, headers=None, reqtype='get', else: raise BlinkException(ERROR.REQUEST) - if json and 'message' in response.keys(): + if json_resp and 'code' in response: raise BlinkAuthenticationException( (response['code'], response['message'])) @@ -62,14 +59,26 @@ class BlinkAuthenticationException(BlinkException): pass +class BlinkURLHandler(object): + """Class that handles Blink URLS""" + def __init__(self, region_id): + """Initialize the urls.""" + self.base_url = 'https://' + region_id + '.' + BLINK_URL + self.home_url = self.base_url + '/homescreen' + self.event_url = self.base_url + '/events/network/' + self.network_url = self.base_url + '/network/' + self.networks_url = self.base_url + '/networks' + + class BlinkCamera(object): """Class to initialize individual camera.""" - def __init__(self, config): + def __init__(self, config, urls): + self.urls = urls self._id = str(config['device_id']) self._name = config['name'] self._status = config['armed'] - self._thumb = BASE_URL + config['thumbnail'] + '.jpg' - self._clip = BASE_URL + config['thumbnail'] + '.mp4' + self._thumb = self.urls.base_url + config['thumbnail'] + '.jpg' + self._clip = self.urls.base_url + config['thumbnail'] + '.mp4' self._temperature = config['temp'] self._battery = config['battery'] self._notifications = config['notifications'] @@ -211,21 +220,22 @@ class BlinkCamera(object): """Updates camera information""" self._name = values['name'] self._status = values['armed'] - self._thumb = BASE_URL + values['thumbnail'] + '.jpg' - self._clip = BASE_URL + values['thumbnail'] + '.mp4' + self._thumb = self.urls.base_url + values['thumbnail'] + '.jpg' + self._clip = self.urls.base_url + values['thumbnail'] + '.mp4' self._temperature = values['temp'] self._battery = values['battery'] self._notifications = values['notifications'] def image_refresh(self): """Refreshs current thumbnail""" - url = HOME_URL + url = self.urls.home_url response = _request(url, headers=self._header, reqtype='get')['devices'] for element in response: try: if str(element['device_id']) == self._id: - self._thumb = BASE_URL + element['thumbnail'] + '.jpg' + self._thumb = (self.urls.base_url + + element['thumbnail'] + '.jpg') return self._thumb except KeyError: pass @@ -258,6 +268,7 @@ class Blink(object): self._events = [] self._cameras = {} self._idlookup = {} + self.urls = None @property def cameras(self): @@ -302,7 +313,7 @@ class Blink(object): @property def events(self): """Gets all events on server""" - url = EVENT_URL + self._network_id + url = self.urls.event_url + self._network_id headers = self._auth_header self._events = _request(url, headers=headers, reqtype='get')['event'] @@ -314,7 +325,7 @@ class Blink(object): Returns True or False depending on if sync module is online/offline """ - url = NETWORK_URL + self._network_id + '/syncmodules' + url = self.urls.network_url + self._network_id + '/syncmodules' headers = self._auth_header return ONLINE[_request(url, headers=headers, reqtype='get')['syncmodule']['status']] @@ -328,7 +339,7 @@ class Blink(object): camera_name = self._idlookup[camera_id] camera = self._cameras[camera_name] if element['type'] == 'motion': - url = BASE_URL + element['video_url'] + url = self.urls.base_url + element['video_url'] camera.motion = {'video': url, 'image': url[:-3] + 'jpg', 'time': element['created_at']} @@ -350,7 +361,7 @@ class Blink(object): value_to_append = 'arm' else: value_to_append = 'disarm' - url = NETWORK_URL + self._network_id + '/' + value_to_append + url = self.urls.network_url + self._network_id + '/' + value_to_append _request(url, headers=self._auth_header, reqtype='post') def refresh(self): @@ -368,7 +379,7 @@ class Blink(object): def get_summary(self): """Gets a full summary of device information""" - url = BASE_URL + '/homescreen' + url = self.urls.home_url headers = self._auth_header if self._auth_header is None: @@ -380,13 +391,13 @@ class Blink(object): """Finds and creates cameras""" response = self.get_summary()['devices'] for element in response: - if 'device_type' in element.keys(): - if element['device_type'] == 'camera': - # Add region to config - element['region_id'] = self._region_id - device = BlinkCamera(element) - self._cameras[device.name] = device - self._idlookup[device.id] = device.name + if ('device_type' in element and + element['device_type'] == 'camera'): + # Add region to config + element['region_id'] = self._region_id + device = BlinkCamera(element, self.urls) + self._cameras[device.name] = device + self._idlookup[device.id] = device.name def set_links(self): """ @@ -395,7 +406,7 @@ class Blink(object): """ for name in self._cameras: camera = self._cameras[name] - network_id_url = NETWORK_URL + self._network_id + network_id_url = self.urls.network_url + self._network_id image_url = network_id_url + '/camera/' + camera.id + '/thumbnail' arm_url = network_id_url + '/camera/' + camera.id + '/' camera.image_link = image_url @@ -442,9 +453,11 @@ class Blink(object): self._auth_header = {'Host': self._host, 'TOKEN_AUTH': self._token} + self.urls = BlinkURLHandler(self._region_id) + def get_ids(self): """Sets the network ID and Account ID""" - url = NETWORKS_URL + url = self.urls.networks_url headers = self._auth_header if self._auth_header is None: diff --git a/constants.py b/constants.py index f09e4c8..e3b2c7a 100644 --- a/constants.py +++ b/constants.py @@ -43,10 +43,6 @@ BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' BASE_URL = 'https://prod.' + BLINK_URL DEFAULT_URL = 'prod.' + BLINK_URL -HOME_URL = BASE_URL + '/homescreen' -EVENT_URL = BASE_URL + '/events/network/' -NETWORK_URL = BASE_URL + '/network/' -NETWORKS_URL = BASE_URL + '/networks' ''' Dictionaries diff --git a/tests/mock_responses.py b/tests/mock_responses.py new file mode 100644 index 0000000..5306206 --- /dev/null +++ b/tests/mock_responses.py @@ -0,0 +1,152 @@ +import constants as const + +"""Fake device attributes.""" +NETWORKS_RESPONSE = {} +NETWORKS_RESPONSE['summary'] = {'onboarded': True, 'name': 'Nilfgaard'} +NETWORKS_RESPONSE['networks'] = [{'network_key': None, + 'name': NETWORKS_RESPONSE['summary']['name'], + 'account_id': 1989, + 'id': 9898, + 'encryption_key': None, + 'armed': True, + 'ping_interval': 60, + 'video_destination': 'server', + 'arm_string': 'Armed', + 'feature_plan_id': None}] + +FIRST_CAMERA = {'device_type': 'camera', + 'notifications': 1, + 'battery': 2, + 'active': 'enabled', + 'enabled': True, + 'temp': 70, + 'updated_at': '2017-01-01T01:23:45+00:00', + 'lfr_strength': 3, + 'armed': True, + 'device_id': 2112, + 'wifi_strength': 5, + 'thumbnail': '/this/is/url1/thumb', + 'name': 'First Camera', + 'status': 'done'} + +SECOND_CAMERA = {'device_type': 'camera', + 'notifications': 0, + 'battery': 3, + 'active': 'disabled', + 'enabled': False, + 'temp': 82, + 'updated_at': '2017-12-21T01:21:12+00:00', + 'lfr_strength': 1, + 'armed': False, + 'device_id': 1221, + 'wifi_strength': 1, + 'thumbnail': '/this/is/url2/thumb', + 'name': 'Second Camera', + 'status': 'done'} + +SYNC_MODULE = {'updated_at': '1970-01-01T01:00:00+00:00', + 'device_type': 'sync_module', + 'notifications': 2, + 'device_id': 7990, + 'status': 'online'} + +FIRST_EVENT = {'camera_name': FIRST_CAMERA['name'], + 'updated_at': '2017-10-10T03:37:37+00:00', + 'sync_module_id': None, + 'camera': FIRST_CAMERA['device_id'], + 'type': 'motion', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T19:51:52+00:00', + 'camera_id': FIRST_CAMERA['device_id'], + 'id': 666777888, + 'siren_id': None, + 'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'], + 'notified': True, + 'siren': None, + 'syncmodule': None, + 'video_url': FIRST_CAMERA['thumbnail'] + '.mp4', + 'command_id': None, + 'network_id': None, + 'account': NETWORKS_RESPONSE['networks'][0]['account_id'], + 'video_id': 123000321} + +SECOND_EVENT = {'updated_at': '2017-01-28T19:51:29+00:00', + 'sync_module_id': SYNC_MODULE['device_id'], + 'camera': None, + 'type': 'armed', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T19:51:29+00:00', + 'camera_id': None, + 'id': 333777555, + 'siren_id': None, + 'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'], + 'notified': False, + 'siren': None, + 'syncmodule': SYNC_MODULE['device_id'], + 'command_id': None, + 'network_id': None, + 'account': NETWORKS_RESPONSE['networks'][0]['account_id']} + + +"""Fake response content.""" +LOGIN_RESPONSE = {} +LOGIN_RESPONSE['region'] = {'ciri': 'Cintra'} +LOGIN_RESPONSE['networks'] = {NETWORKS_RESPONSE['networks'][0]['id'] : NETWORKS_RESPONSE['summary']} +LOGIN_RESPONSE['authtoken'] = {'authtoken': 'foobar7117', 'message': 'auth'} + +RESPONSE = {} +RESPONSE['account'] = {'notifications': SYNC_MODULE['notifications']} +RESPONSE['devices'] = [FIRST_CAMERA, SECOND_CAMERA, SYNC_MODULE] +RESPONSE['network'] = {'armed': NETWORKS_RESPONSE['networks'][0]['armed'], + 'wifi_strength': 4, + 'warning': 0, + 'name': NETWORKS_RESPONSE['summary']['name'], + 'notifications': SYNC_MODULE['notifications']} +RESPONSE['event'] = [FIRST_EVENT, SECOND_EVENT] +RESPONSE['syncmodule'] = {'name': 'Vengerberg', 'status': 'online'} + + +def mocked_requests_post(*args, **kwargs): + """Mock post request.""" + class MockPostResponse: + def __init__(self,json_data,status_code): + self.json_data = json_data + self.status_code = status_code + def json(self): + return self.json_data + + url_tail = args[0].split("/")[-1] + if args[0] == const.LOGIN_URL: + return MockPostResponse(LOGIN_RESPONSE, 200) + elif url_tail == 'arm' or url_tail == 'disarm': + global NETWORKS_RESPONSE + global RESPONSE + NETWORKS_RESPONSE['networks'][0]['armed'] = url_tail == 'arm' + RESPONSE['network']['armed'] = NETWORKS_RESPONSE['networks'][0]['armed'] + return MockPostResponse({}, 200) + + return MockPostResponse({'message':'ERROR','code':404}, 404) + +def mocked_requests_get(*args, **kwargs): + """Mock get request.""" + class MockGetResponse: + def __init__(self,json_data,status_code): + self.json_data = json_data + self.status_code = status_code + def json(self): + return self.json_data + + (region_id, region), = LOGIN_RESPONSE['region'].items() + set_region_id = args[0].split('/')[2].split('.')[0] + NETURL = 'https://' + set_region_id + '.' + const.BLINK_URL + '/networks' + if args[0] == NETURL: + return MockGetResponse(NETWORKS_RESPONSE, 200) + elif set_region_id != region_id: + raise ConnectionError('Received url ' + args[0]) + else: + return MockGetResponse(RESPONSE, 200) + + return MockGetResponse({'message':'ERROR','code':404}, 404) + diff --git a/tests/test_blink_cameras.py b/tests/test_blink_cameras.py new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/tests/test_blink_cameras.py @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/tests/test_blink_requests.py b/tests/test_blink_requests.py deleted file mode 100644 index f88c103..0000000 --- a/tests/test_blink_requests.py +++ /dev/null @@ -1,101 +0,0 @@ -import blinkpy -import requests -import unittest -from unittest import mock -from blinkpy import LOGIN_URL -from blinkpy import BASE_URL -import test_const as const - -def mocked_requests_post(*args, **kwargs): - class MockPostResponse: - def __init__(self,json_data,status_code): - self.json_data = json_data - self.status_code = status_code - def json(self): - return self.json_data - - if args[0] == LOGIN_URL: - return MockPostResponse({"region":{const.REGION_ID: const.REGION}, "authtoken":{"authtoken":const.TOKEN}}, 200) - elif args[0].split("/")[-1] == 'arm': - return MockPostResponse({"armed":True}, 200) - elif args[0].split("/")[-1] == 'disarm': - return MockPostResponse({"armed":False}, 200) - else: - return MockPostResponse({}, 200) - - return MockPostResponse({'message':'ERROR','code':404}, 404) - -def mocked_requests_get(*args, **kwargs): - class MockGetResponse: - def __init__(self,json_data,status_code): - self.json_data = json_data - self.status_code = status_code - def json(self): - return self.json_data - - if args[0] == BASE_URL + '/networks': - return MockGetResponse({'networks':[{"id":const.NETWORK_ID,"account_id":const.ACCOUNT_ID},{"nothing":"nothing"}]}, 200) - else: - return MockGetResponse(const.response, 200) - - - return MockGetResponse({'message':'ERROR','code':404}, 404) - -class TestBlinkRequests(unittest.TestCase): - @mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post) - @mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get) - def test_blink_setup(self, mock_get, mock_post): - blink = blinkpy.Blink(username='user',password='password') - blink.setup_system() - - self.assertEqual(blink.network_id, str(const.NETWORK_ID)) - self.assertEqual(blink.account_id, str(const.ACCOUNT_ID)) - self.assertEqual(blink.region, const.REGION) - self.assertEqual(blink.region_id, const.REGION_ID) - self.assertEqual(blink.online, const.ISONLINE) - self.assertEqual(blink.arm, const.ARMED) - - @mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post) - @mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get) - def test_blink_camera_setup_and_motion(self, mock_get, mock_post): - blink = blinkpy.Blink(username='user',password='password') - blink.setup_system() - blink.last_motion() - for name, camera in blink.cameras.items(): - if camera.id == str(const.DEVICE_ID): - self.assertEqual(name, const.CAMERA_NAME) - self.assertEqual(camera.armed, const.ARMED) - self.assertEqual(camera.motion['video'], BASE_URL + const.THUMB + '.mp4') - self.assertEqual(camera.header, const.auth_header) - elif camera.id == str(const.DEVICE_ID2): - self.assertEqual(name, const.CAMERA_NAME2) - self.assertEqual(camera.armed, const.ARMED2) - self.assertEqual(len(camera.motion.keys()), 0) - else: - assert False is True - - @mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post) - @mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get) - def test_blink_refresh(self, mock_get, mock_post): - blink = blinkpy.Blink(username='user',password='password') - blink.setup_system() - const.response['devices'][0]['thumbnail'] = const.THUMB + const.THUMB2 - blink.refresh() - for name, camera in blink.cameras.items(): - if camera.id == str(const.DEVICE_ID): - self.assertEqual(camera.thumbnail, BASE_URL + const.THUMB + const.THUMB2 + '.jpg') - elif camera.id == str(const.DEVICE_ID2): - pass - else: - assert False is True - - const.response['devices'][0]['thumbnail'] = 'new' - blink.cameras[const.CAMERA_NAME].image_refresh() - for name, camera in blink.cameras.items(): - if camera.id == str(const.DEVICE_ID): - self.assertEqual(camera.thumbnail, BASE_URL + 'new' + '.jpg') - elif camera.id == str(const.DEVICE_ID2): - pass - else: - assert False is True - \ No newline at end of file diff --git a/tests/test_blink_system.py b/tests/test_blink_system.py index 2125685..f56b8e3 100644 --- a/tests/test_blink_system.py +++ b/tests/test_blink_system.py @@ -1,7 +1,8 @@ import requests import unittest -import blinkpy from unittest import mock +import blinkpy +import tests.mock_responses as mresp import constants as const USERNAME = 'foobar' @@ -9,35 +10,99 @@ PASSWORD = 'deadbeef' class TestBlinkSetup(unittest.TestCase): """Test the Blink class in blinkpy.""" + def setUp(self): + """Set up Blink module.""" + self.blink_no_cred = blinkpy.Blink() + self.blink = blinkpy.Blink(username=USERNAME, + password=PASSWORD) + + def tearDown(self): + """Clean up after test.""" + self.blink = None + self.blink_no_cred = None + def test_initialization(self): """Verify we can initialize blink.""" - blink = blinkpy.Blink(username=USERNAME, password=PASSWORD) - self.assertEqual(blink._username, USERNAME) - self.assertEqual(blink._password, PASSWORD) + self.assertEqual(self.blink._username, USERNAME) + self.assertEqual(self.blink._password, PASSWORD) def test_no_credentials(self): """Check that we throw an exception when no username/password.""" - blink = blinkpy.Blink() with self.assertRaises(blinkpy.BlinkAuthenticationException): - blink.get_auth_token() + self.blink_no_cred.get_auth_token() with self.assertRaises(blinkpy.BlinkAuthenticationException): - blink.setup_system() + self.blink_no_cred.setup_system() def test_no_auth_header(self): """Check that we throw an excpetion when no auth header given.""" - blink = blinkpy.Blink(username=USERNAME, password=PASSWORD) + (region_id, region), = mresp.LOGIN_RESPONSE['region'].items() + self.blink.urls = blinkpy.BlinkURLHandler(region_id) with self.assertRaises(blinkpy.BlinkException): - blink.get_ids() + self.blink.get_ids() @mock.patch('blinkpy.getpass.getpass') def test_manual_login(self, getpwd): """Check that we can manually use the login() function.""" - blink = blinkpy.Blink() getpwd.return_value = PASSWORD with mock.patch('builtins.input', return_value=USERNAME): - blink.login() - self.assertEqual(blink._username, USERNAME) - self.assertEqual(blink._password, PASSWORD) + self.blink_no_cred.login() + self.assertEqual(self.blink_no_cred._username, USERNAME) + self.assertEqual(self.blink_no_cred._password, PASSWORD) - # NEXT NEED ACTUAL REQUEST TESTS - \ No newline at end of file + @mock.patch('blinkpy.requests.post', side_effect=mresp.mocked_requests_post) + @mock.patch('blinkpy.requests.get', side_effect=mresp.mocked_requests_get) + def test_full_setup(self, mock_get, mock_post): + """Check that we can set Blink up properly.""" + self.blink.setup_system() + + # Get all test values + authtoken = mresp.LOGIN_RESPONSE['authtoken']['authtoken'] + (region_id, region), = mresp.LOGIN_RESPONSE['region'].items() + host = region_id + '.' + const.BLINK_URL + network_id = mresp.NETWORKS_RESPONSE['networks'][0]['id'] + account_id = mresp.NETWORKS_RESPONSE['networks'][0]['account_id'] + TestURLs = blinkpy.BlinkURLHandler(region_id) + TestCameras = list() + TestCameraID = dict() + for element in mresp.RESPONSE['devices']: + if 'device_type' in element and element['device_type'] == 'camera': + TestCameras.append(element['name']) + TestCameraID[str(element['device_id'])] = element['name'] + + # Check that all links have been set properly + self.assertEqual(self.blink.region_id, region_id) + self.assertEqual(self.blink.urls.base_url, TestURLs.base_url) + self.assertEqual(self.blink.urls.home_url, TestURLs.home_url) + self.assertEqual(self.blink.urls.event_url, TestURLs.event_url) + self.assertEqual(self.blink.urls.network_url, TestURLs.network_url) + self.assertEqual(self.blink.urls.networks_url, TestURLs.networks_url) + + # Check that all properties have been set after startup + self.assertEqual(self.blink._token, authtoken) + self.assertEqual(self.blink._host, host) + self.assertEqual(self.blink.network_id, str(network_id)) + self.assertEqual(self.blink.account_id, str(account_id)) + self.assertEqual(self.blink.region, region) + + # Verify we have initialized all the cameras + self.assertEqual(self.blink.id_table, TestCameraID) + for camera in TestCameras: + self.assertTrue(camera in self.blink.cameras) + + @mock.patch('blinkpy.requests.post', side_effect=mresp.mocked_requests_post) + @mock.patch('blinkpy.requests.get', side_effect=mresp.mocked_requests_get) + def test_arm_disarm_system(self, mock_get, mock_post): + """Check that we can arm/disarm the system""" + self.blink.setup_system() + self.blink.arm = False + self.assertIs(self.blink.arm, False) + self.blink.arm = True + self.assertIs(self.blink.arm, True) + + @mock.patch('blinkpy.requests.post', side_effect=mresp.mocked_requests_post) + @mock.patch('blinkpy.requests.get', side_effect=mresp.mocked_requests_get) + def test_check_online_status(self, mock_get, mock_post): + """Check that we can get our online status.""" + self.blink.setup_system() + expected_status = const.ONLINE[mresp.RESPONSE['syncmodule']['status']] + self.assertIs(self.blink.online, expected_status) diff --git a/tests/test_classes.py b/tests/test_classes.py deleted file mode 100644 index 6a3b50d..0000000 --- a/tests/test_classes.py +++ /dev/null @@ -1,75 +0,0 @@ -import testtools -import unittest -import blinkpy -from blinkpy import BASE_URL - -class TestBlink(unittest.TestCase): - def test_blink_instance(self): - blink = blinkpy.Blink() - - def test_blink_camera_attributes(self): - config = {'name':'name', 'device_id': 1248, 'armed':True, 'thumbnail':'/test/url/image', 'temp':70, 'battery':3, 'notifications':0, 'region_id':'rid'} - - camera = blinkpy.BlinkCamera(config) - - # Check that values are properly stored and can be recalled - self.assertEqual(camera.name, config['name']) - self.assertEqual(camera.id, str(config['device_id'])) - self.assertEqual(camera.armed, config['armed']) - self.assertEqual(camera.clip, BASE_URL + config['thumbnail'] + '.mp4') - self.assertEqual(camera.thumbnail, BASE_URL + config['thumbnail'] + '.jpg') - self.assertEqual(camera.temperature, config['temp']) - self.assertEqual(camera.battery, config['battery']) - self.assertEqual(camera.notifications, config['notifications']) - self.assertEqual(camera.region_id, config['region_id']) - - # Check that values can be changed with individual methods - test_header = {'Header':'test','Test':'1234'} - test_img = 'http://image-link.com' - test_arm = 'http://arm-link.com' - test_motion = {'video':'url','image':'url','time':'timestamp'} - - camera.name = config['name']+'_new' - camera.clip = BASE_URL + config['thumbnail'] + '.mp4_new' - camera.thumbnail = BASE_URL + config['thumbnail'] + '.jpg_new' - camera.temperature = config['temp'] + 10 - camera.battery = config['battery'] + 1 - camera.notifications = config['notifications'] + 1 - camera.image_link = test_img - camera.arm_link = test_arm - camera.header = test_header - camera.motion = test_motion - - # Check that values are properly stored and can be recalled - self.assertEqual(camera.name , config['name']+'_new') - self.assertEqual(camera.clip , BASE_URL + config['thumbnail'] + '.mp4_new') - self.assertEqual(camera.thumbnail, BASE_URL + config['thumbnail'] + '.jpg_new') - self.assertEqual(camera.temperature, config['temp'] + 10) - self.assertEqual(camera.battery, config['battery'] + 1) - self.assertEqual(camera.notifications, config['notifications'] + 1) - self.assertEqual(camera.image_link, test_img) - self.assertEqual(camera.arm_link, test_arm) - self.assertEqual(camera.header, test_header) - self.assertEqual(camera.motion, test_motion) - - # Verify bulk update function - test_name = camera.name +' last' - test_status = True - test_url = '-this-is-a-test/for_realz' - test_temp = camera.temperature + 7 - test_battery = camera.battery - 1 - test_notif = camera.notifications - 1 - values = {'name':test_name, 'armed':test_status, 'thumbnail':test_url, 'temp':test_temp, 'battery':test_battery, 'notifications':test_notif} - - camera.update(values) - # Check that values are properly stored and can be recalled - jpg_url = BASE_URL + test_url +'.jpg' - mp4_url = BASE_URL + test_url +'.mp4' - self.assertEqual(camera.name, test_name) - self.assertEqual(camera.armed, test_status) - self.assertEqual(camera.clip, mp4_url) - self.assertEqual(camera.thumbnail, jpg_url) - self.assertEqual(camera.temperature, test_temp) - self.assertEqual(camera.battery, test_battery) - self.assertEqual(camera.notifications, test_notif) - \ No newline at end of file diff --git a/tests/test_const.py b/tests/test_const.py deleted file mode 100644 index b03f96c..0000000 --- a/tests/test_const.py +++ /dev/null @@ -1,160 +0,0 @@ -ISONLINE = True -ARMED = True -ARMED2 = False -REGION_ID = 'test' -REGION = 'Oceaniaeurasia' -TOKEN = 'abcd1234$$@@' -NETWORK_ID = 1337 -DEVICE_ID = 9876 -DEVICE_ID2 = 6789 -ACCOUNT_ID = 1234 -CAMERA_NAME = 'My Camera' -CAMERA_NAME2 = 'Number 2' -BATTERY = 3 -BATTERY2 = 1 -TEMP = 70 -TEMP2 = 66 -THUMB = '/url/camera/7777/clip' -THUMB2 = '/url/camera/8888/clip' -NOTIFS = 1 -NOTIFS2 = 1 -SYNC_ID = 1000 - -if ISONLINE: - ONLINE = 'online' -else: - ONLINE = 'offline' - -auth_header = {'Host': REGION_ID+'.immedia-semi.com', - 'TOKEN_AUTH': TOKEN - } - -response = {'account': {'notifications': 1}, - 'devices': [{'device_type': 'camera', - 'notifications': NOTIFS, - 'battery': BATTERY, - 'active': 'disabled', - 'errors': 0, - 'error_msg': '', - 'enabled': False, - 'temp': TEMP, - 'updated_at': '2017-01-27T03:14:24+00:00', - 'lfr_strength': 3, - 'armed': ARMED, - 'device_id': DEVICE_ID, - 'wifi_strength': 5, - 'warning': 0, - 'thumbnail': THUMB, - 'name': CAMERA_NAME, - 'status': 'done'}, - {'device_type': 'camera', - 'notifications': NOTIFS2, - 'battery': BATTERY2, - 'active': 'disabled', - 'errors': 0, - 'error_msg': '', - 'enabled': False, - 'temp': TEMP2, - 'updated_at': '2017-01-27T03:14:24+00:00', - 'lfr_strength': 3, - 'armed': ARMED2, - 'device_id': DEVICE_ID2, - 'wifi_strength': 5, - 'warning': 0, - 'thumbnail': THUMB2, - 'name': CAMERA_NAME2, - 'status': 'done'}, - {'updated_at': '2017-01-26T19:32:10+00:00', - 'device_type': 'sync_module', - 'notifications': 0, - 'device_id': SYNC_ID, - 'status': - 'online', - 'last_hb': - '2017-01-27T03:14:49+00:00', - 'errors': 0, - 'error_msg': '', - 'warning': 0}], - 'network': {'armed': ARMED, - 'wifi_strength': 5, - 'warning': 0, - 'error_msg': '', - 'name': 'Blink', - 'notifications': NOTIFS, - 'status': 'ok'}, - 'event': [{'camera_name': CAMERA_NAME, - 'updated_at': '2017-01-28T19:51:52+00:00', - 'sync_module_id': None, - 'camera': DEVICE_ID, - 'type': 'motion', - 'duration': None, - 'status': None, - 'created_at': '2017-01-28T19:51:52+00:00', - 'camera_id': DEVICE_ID, - 'id': 123456789, - 'siren_id': None, - 'account_id': ACCOUNT_ID, - 'notified': True, - 'siren': None, - 'syncmodule': None, - 'video_url': THUMB+'.mp4', - 'command_id': None, - 'network_id': None, - 'account': ACCOUNT_ID, - 'video_id': 100000001}, - {'updated_at': '2017-01-28T19:51:29+00:00', - 'sync_module_id': SYNC_ID, - 'camera': None, - 'type': 'armed', - 'duration': None, - 'status': None, - 'created_at': - '2017-01-28T19:51:29+00:00', - 'camera_id': None, - 'id': 123456789, - 'siren_id': None, - 'account_id': ACCOUNT_ID, - 'notified': False, - 'siren': None, - 'syncmodule': SYNC_ID, - 'command_id': None, - 'network_id': None, - 'account': ACCOUNT_ID}, - {'updated_at': '2017-01-28T19:00:12+00:00', - 'sync_module_id': SYNC_ID, - 'camera': None, - 'type': 'disarmed', - 'duration': None, - 'status': None, - 'created_at': '2017-01-28T19:00:12+00:00', - 'camera_id': None, - 'id': 123456789, - 'siren_id': None, - 'account_id': 2463, - 'notified': False, - 'siren': None, - 'syncmodule': SYNC_ID, - 'command_id': None, - 'network_id': None, - 'account': ACCOUNT_ID}, - {'camera_name': CAMERA_NAME, - 'updated_at': '2017-01-28T18:59:55+00:00', - 'sync_module_id': None, - 'camera': DEVICE_ID, - 'type': 'motion', - 'duration': None, - 'status': None, - 'created_at': '2017-01-28T18:59:55+00:00', - 'camera_id': DEVICE_ID, - 'id': 123456789, - 'siren_id': None, - 'account_id': ACCOUNT_ID, - 'notified': True, - 'siren': None, - 'syncmodule': None, - 'command_id': None, - 'network_id': None, - 'account': ACCOUNT_ID}], - 'syncmodule':{'name':'SyncName', - 'status':ONLINE}} - \ No newline at end of file diff --git a/tests/test_import.py b/tests/test_import.py deleted file mode 100644 index 41efe52..0000000 --- a/tests/test_import.py +++ /dev/null @@ -1,6 +0,0 @@ -import testtools -import blinkpy - -class TestImport(testtools.TestCase): - def test_import(self): - pass \ No newline at end of file diff --git a/tests/test_servers.py b/tests/test_servers.py deleted file mode 100644 index 08e6631..0000000 --- a/tests/test_servers.py +++ /dev/null @@ -1,8 +0,0 @@ -import requests -import unittest -from constants import BASE_URL - -class TestRemoteServerResponse(unittest.TestCase): - def test_request_response(self): - response = requests.get(BASE_URL) - self.assertEqual(response.ok, True) \ No newline at end of file From d54a6cbb71038c40a6e420f7b538f2dccd2feaa7 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 11 Mar 2017 21:53:58 -0500 Subject: [PATCH 15/21] Started adding more camera tests, reorganized package --- MANIFEST.in | 4 + README.rst | 160 +++++++++++++-------------- __init__.py | 6 +- blinkpy.py | 143 ++++++++++++------------ examples/arm_system_camera.py | 22 ---- examples/list_all_cameras.py | 18 --- helpers/__init__.py | 3 + constants.py => helpers/constants.py | 0 errors.py => helpers/errors.py | 0 setup.py | 11 +- tests/__init__.py | 1 + tests/mock_responses.py | 145 +++++++++++++----------- tests/test_blink_cameras.py | 81 +++++++++++++- tests/test_blink_system.py | 69 ++++++++---- tox.ini | 16 +-- 15 files changed, 376 insertions(+), 303 deletions(-) create mode 100644 MANIFEST.in delete mode 100644 examples/arm_system_camera.py delete mode 100644 examples/list_all_cameras.py create mode 100644 helpers/__init__.py rename constants.py => helpers/constants.py (100%) rename errors.py => helpers/errors.py (100%) create mode 100644 tests/__init__.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..831efda --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.rst +include LICENSE +include tests/*.py +include helpers/*.py diff --git a/README.rst b/README.rst index 6a88d1c..5dffd5a 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ -**blinkpy** |Build Status| |Coverage Status| +blinkpy |Build Status| |Coverage Status| ============ A Python library for the Blink Camera system -**Disclaimers** -=============== +Disclaimer: +~~~~~~~~~~~~~~~ Published under the MIT license - See LICENSE file for more details. "Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information. @@ -11,113 +11,105 @@ I am in no way affiliated with Blink, nor Immedia Inc. Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol -**Installation** +API calls faster than 60 seconds is not recommended as it can overwhelm Blink's servers. Please use this module responsibly. + +Installation ================ ``pip3 install blinkpy`` -**Purpose** +Purpose =========== This library was built with the intention of allowing easy communication with Blink camera systems, specifically so I can add a module into homeassistant https://home-assistant.io -**Usage** +Usage ========= In terms of usage, you just need to instantiate the module with a username and password -:: - import blinkpy - blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD') + +.. code:: python + + import blinkpy + blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD') If you leave out either of those parameters, you need to call the login function which will prompt for your username and password -:: - blink.login() + +.. code:: python + + import blinkpy + blink = blinkpy.Blink() + blink.login() Once the login information is entered, you can run the `setup_system()` function which will attempt to authenticate with Blink servers using your username and password, obtain network ids, and create a list of cameras. The cameras are of a BlinkCamera class, of which the following parameters can be used (the code below creates a Blink object and iterates through each camera found) -:: - blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD') - blink.setup_system() - for camera in blink.cameras: - print(camera.name) # Name of the camera - print(camera.id) # Integer id of the camera (assigned by Blink) - print(camera.armed) # Whether the device is armed/disarmed (ie. detecting motion) - print(camera.clip) # Link to last motion clip captured - print(camera.thumbnail) # Link to current camera thumbnail - print(camera.temperature) # Current camera temperature (not super accurate, but might be useful for someone) - print(camera.battery) # Current battery level... I think the value ranges from 0-3, but not quite sure yet. - print(camera.notifications) # Number of unread notifications (ie. motion alerts that haven't been viewed) - print(camera.motion) # Dictionary containing values for keys ['video', 'image', 'time'] - # which correspond to last motion recorded, thumbnail of last motion, and timestamp of last motion +.. code:: python + + import blinkpy + + blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD') + blink.setup_system() + + for camera in blink.cameras: + print(camera.name) # Name of the camera + print(camera.id) # Integer id of the camera (assigned by Blink) + print(camera.armed) # Whether the device is armed/disarmed (ie. detecting motion) + print(camera.clip) # Link to last motion clip captured + print(camera.thumbnail) # Link to current camera thumbnail + print(camera.temperature) # Current camera temperature (not super accurate, but might be useful for someone) + print(camera.battery) # Current battery level... I think the value ranges from 0-3, but not quite sure yet. + print(camera.notifications) # Number of unread notifications (ie. motion alerts that haven't been viewed) + print(camera.motion) # Dictionary containing values for keys ['video', 'image', 'time'] + # which correspond to last motion recorded, thumbnail of last motion, and timestamp of last motion -**class Blink** ---------------- -The following properties/methods are availiable to the Blink class +Class Descriptions +=================== +.. code:: python -**Blink.cameras** -Returns a list of BlinkCamera objects found by the system + class Blink() -**Blink.network_id** -Returns the current network id +* ``Blink.cameras`` Returns a dictionary of ``BlinkCamera`` objects where the key corresponds to the camera name and the value is the actual BlinkCamera object. +* ``Blink.network_id`` Returns the current network id. +* ``Blink.account_id`` Returns the account id. +* ``Blink.events`` Returns a list of events recorded by blink. This information will contain links to any motion caught by an armed camera.. +* ``Blink.online`` Returns online status of sync module (True = online, False = offline). +* ``Blink.last_motion()`` Finds last motion information for each camera and stores it in the ``BlinkCamera.motion`` field. +* ``Blink.arm`` Set to True to arm, False to disarm. Can be used to see the status of the system as well. +* ``Blink.refresh()`` Forces a refresh of all camera information. +* ``Blink.get_summary()`` Returns json formatted summary of the system. +* ``Blink.get_cameras()`` Finds all cameras in the system and creates ``BlinkCamera`` objects to represent them. +* ``Blink.set_links()`` Gives each BlinkCamera object the links needed to find recent images and videos. +* ``Blink.login()`` Prompts user for login information. +* ``Blink.get_auth_token()`` Uses login information to retrieve authorization token from Blink for further communication. +* ``Blink.get_ids()`` Retrieves the network_id and account_id from Blink in order to access video and image pages on their server. +* ``Blink.setup_system()`` A wrapper script that calls: +.. code:: python -**Blink.account_id** -Returns the account id + Blink.get_auth_token() + Blink.get_ids() + Blink.get_camers() + Blink.set_links() -**Blink.events** -Returns a list of events recorded by blink. This information will contain links to any motion caught by an armed camera. +.. code:: python -**Blink.online** -Returns online status of sync module (True = online, False = offline) + class BlinkCamera(config, urls) + +The ``BlinkCamera`` class expects to receive: +* A dictionary ``config`` that contains the camera name, device id, armed status, thumbnail url, camera temperature, camery battery level, number of notifications, and region id +* A ``BlinkURLHandler`` object that contains all the links necessary for communication. -**Blink.last_motion()** -Finds last motion information for each camera and stores it in the BlinkCamera.motion field +Ultimately, this class is just a wrapper for each individual camera in order to make communication with individual cameras less clunky. The following properties/methods are availiable (in addition to the ones mentioned earlier): +* ``BlinkCamera.snap_picture()`` Takes an image with the camera and saves it as the new thumbnail. The ``Blink.refresh()`` method should be called after this if you want to store the new thumbnail link. +* ``BlinkCamera.set_motion_detect(enable=True/False)`` Sending True to this function will enable motion detection for the camera. Setting to False will disable motion detection. +* ``BlinkCamera.image_to_file(path)`` This will write the current thumbnail to the location indicated in 'path' +* ``BlinkCamera.image_refresh()`` Refreshes the current thumbnail. -**Blink.arm** -Set to True to arm, False to disarm. Can be used to see the status of the system as well +.. code:: python -**Blink.refresh()** -Forces a refresh of all camera information - -**Blink.get_summary()** -Returns json formatted summary of the system - -**Blink.get_cameras()** -Finds all cameras in the system and creates them - -**Blink.set_links()** -Gives each BlinkCamera object the links needed to find recent images and videos - -**Blink.login()** -Prompts user for login information - -**Blink.get_auth_token()** -Uses login information to retrieve authorization token from Blink for further communication - -**Blink.get_ids()** -Retrieves the network_id and account_id from Blink in order to access video and image pages on their server - -**Blink.setup_system()** -A wrapper script that calls: -:: - self.get_auth_token() - self.get_ids() - self.get_camers() - self.set_links() - - -**class BlinkCamera** ---------------------- -This class is just a wrapper for each individual camera in order to make communication with individual cameras less clunky. The following properties/methods are availiable (in addition to the ones mentioned earlier) - -**BlinkCamera.snap_picture()** -Takes an image with the camera and saves it as the new thumbnail. The Blink.refresh() method should be called after this if you want to store the new thumbnail link - -**BlinkCamera.set_motion_detect(enable=True/False)** -Sending True to this function will enable motion detection for the camera. Setting to False will disable motion detection - -**BlinkCamera.image_to_file(path)** -This will write the current thumbnail to the location indicated in 'path' + class BlinkURLHandler(region_id) + +The ``BlinkURLHandler`` class expects to be initialized with the region id found in the ``Blink.get_auth_token()`` function. The class will then create the necessary links required for various communication. .. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=master :target: https://travis-ci.org/fronzbot/blinkpy diff --git a/__init__.py b/__init__.py index 550c8cd..ae24b9e 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1,5 @@ -from blinkpy import Blink +"""Init file for blinkpy.""" +from blinkpy.blinkpy import Blink +from blinkpy.blinkpy import BlinkAuthenticationException +from blinkpy.blinkpy import BlinkException +from blinkpy.blinkpy import BlinkURLHandler \ No newline at end of file diff --git a/blinkpy.py b/blinkpy.py index de2bc81..1a773b5 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -1,8 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +""" +blinkpy by Kevin Fronczak - A Blink camera Python library. -''' -blinkpy by Kevin Fronczak - A Blink camera Python library https://github.com/fronzbot/blinkpy Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol @@ -10,19 +10,19 @@ Published under the MIT license - See LICENSE file for more details. "Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information. I am in no way affiliated with Blink, nor Immedia Inc. -''' +""" import json import getpass import requests -import errors as ERROR -from constants import (BLINK_URL, LOGIN_URL, - DEFAULT_URL, ONLINE) +import helpers.errors as ERROR +from helpers.constants import (BLINK_URL, LOGIN_URL, + DEFAULT_URL, ONLINE) def _request(url, data=None, headers=None, reqtype='get', stream=False, json_resp=True): - """Wrapper function for request""" + """Wrapper function for request.""" if reqtype is 'post': response = requests.post(url, headers=headers, data=data).json() @@ -44,23 +44,23 @@ def _request(url, data=None, headers=None, reqtype='get', # pylint: disable=super-init-not-called class BlinkException(Exception): - """ - Class to throw general blink exception. - """ + """Class to throw general blink exception.""" + def __init__(self, errcode): + """Initialize BlinkException.""" self.errid = errcode[0] self.message = errcode[1] class BlinkAuthenticationException(BlinkException): - """ - Class to throw authentication exception. - """ + """Class to throw authentication exception.""" + pass class BlinkURLHandler(object): - """Class that handles Blink URLS""" + """Class that handles Blink URLS.""" + def __init__(self, region_id): """Initialize the urls.""" self.base_url = 'https://' + region_id + '.' + BLINK_URL @@ -72,7 +72,9 @@ class BlinkURLHandler(object): class BlinkCamera(object): """Class to initialize individual camera.""" + def __init__(self, config, urls): + """Initiailize BlinkCamera.""" self.urls = urls self._id = str(config['device_id']) self._name = config['name'] @@ -91,125 +93,125 @@ class BlinkCamera(object): @property # pylint: disable=invalid-name def id(self): - """Returns camera id""" + """Return camera id.""" return self._id @property def name(self): - """Returns camera name""" + """Return camera name.""" return self._name @name.setter def name(self, value): - """Sets camera name""" + """Set camera name.""" self._name = value @property def region_id(self): - """Returns region id""" + """Return region id.""" return self._region_id @property def armed(self): - """Returns camera arm status""" + """Return camera arm status.""" return self._status @property def clip(self): - """Returns current clip""" + """Return current clip.""" return self._clip @clip.setter def clip(self, value): - """Sets current clip""" + """Set current clip.""" self._clip = value @property def thumbnail(self): - """Returns current thumbnail""" + """Return current thumbnail.""" return self._thumb @thumbnail.setter def thumbnail(self, value): - """Sets current thumbnail""" + """Set current thumbnail.""" self._thumb = value @property def temperature(self): - """Returns camera temperature""" + """Return camera temperature.""" return self._temperature @temperature.setter def temperature(self, value): - """Sets camera temperature""" + """Set camera temperature.""" self._temperature = value @property def battery(self): - """Returns battery level""" + """Return battery level.""" return self._battery @battery.setter def battery(self, value): - """Sets battery level""" + """Set battery level.""" self._battery = value @property def notifications(self): - """Returns number of notifications""" + """Return number of notifications.""" return self._notifications @notifications.setter def notifications(self, value): - """Sets number of notifications""" + """Set number of notifications.""" self._notifications = value @property def image_link(self): - """Returns image link""" + """Return image link.""" return self._image_link @image_link.setter def image_link(self, value): - """Sets image link""" + """Set image link.""" self._image_link = value @property def arm_link(self): - """Returns link to arm camera""" + """Return link to arm camera.""" return self._arm_link @arm_link.setter def arm_link(self, value): - """Sets link to arm camera""" + """Set link to arm camera.""" self._arm_link = value @property def header(self): - """Returns request header""" + """Return request header.""" return self._header @header.setter def header(self, value): - """Sets request header""" + """Set request header.""" self._header = value @property def motion(self): - """Returns last motion event detail""" + """Return last motion event detail.""" return self._motion @motion.setter def motion(self, value): - """Sets link to last motion and timestamp""" + """Set link to last motion and timestamp.""" self._motion = value def snap_picture(self): - """Takes a picture with camera to create a new thumbnail""" + """Take a picture with camera to create a new thumbnail.""" _request(self._image_link, headers=self._header, reqtype='post') def set_motion_detect(self, enable): - """Sets motion detection""" + """Set motion detection.""" url = self._arm_link if enable: _request(url + 'enable', headers=self._header, reqtype='post') @@ -217,7 +219,7 @@ class BlinkCamera(object): _request(url + 'disable', headers=self._header, reqtype='post') def update(self, values): - """Updates camera information""" + """Update camera information.""" self._name = values['name'] self._status = values['armed'] self._thumb = self.urls.base_url + values['thumbnail'] + '.jpg' @@ -227,7 +229,7 @@ class BlinkCamera(object): self._notifications = values['notifications'] def image_refresh(self): - """Refreshs current thumbnail""" + """Refresh current thumbnail.""" url = self.urls.home_url response = _request(url, headers=self._header, reqtype='get')['devices'] @@ -242,7 +244,7 @@ class BlinkCamera(object): return None def image_to_file(self, path): - """Writes image to file""" + """Write image to file.""" thumb = self.image_refresh() response = _request(thumb, headers=self._header, stream=True, json_resp=False) @@ -253,9 +255,10 @@ class BlinkCamera(object): class Blink(object): - """Class to initialize communication and sync module""" + """Class to initialize communication and sync module.""" + def __init__(self, username=None, password=None): - """Constructor for class""" + """Initialize Blink system.""" self._username = username self._password = password self._token = None @@ -272,12 +275,12 @@ class Blink(object): @property def cameras(self): - """Returns camera/id pairs""" + """Return camera/id pairs.""" return self._cameras @property def camera_thumbs(self): - """Returns camera thumbnails""" + """Return camera thumbnails.""" self.refresh() data = {} for name, camera in self._cameras.items(): @@ -287,32 +290,32 @@ class Blink(object): @property def id_table(self): - """Returns id/camera pairs""" + """Return id/camera pairs.""" return self._idlookup @property def network_id(self): - """Returns network id""" + """Return network id.""" return self._network_id @property def account_id(self): - """Returns account id""" + """Return account id.""" return self._account_id @property def region(self): - """Returns current region""" + """Return current region.""" return self._region @property def region_id(self): - """Returns region id""" + """Return region id.""" return self._region_id @property def events(self): - """Gets all events on server""" + """Get all events on server.""" url = self.urls.event_url + self._network_id headers = self._auth_header self._events = _request(url, headers=headers, @@ -321,17 +324,14 @@ class Blink(object): @property def online(self): - """ - Returns True or False depending on if - sync module is online/offline - """ + """Return boolean system online status.""" url = self.urls.network_url + self._network_id + '/syncmodules' headers = self._auth_header return ONLINE[_request(url, headers=headers, reqtype='get')['syncmodule']['status']] def last_motion(self): - """Finds last motion of each camera""" + """Find last motion of each camera.""" recent = self.events for element in recent: try: @@ -348,15 +348,12 @@ class Blink(object): @property def arm(self): - """Returns status of sync module: armed/disarmed""" + """Return status of sync module: armed/disarmed.""" return self.get_summary()['network']['armed'] @arm.setter def arm(self, value): - """ - Arms or disarms system. - Arms/disarms all if camera not named. - """ + """Arm or disarm system.""" if value: value_to_append = 'arm' else: @@ -365,7 +362,7 @@ class Blink(object): _request(url, headers=self._auth_header, reqtype='post') def refresh(self): - """Gets all blink cameras and pulls their most recent status""" + """Get all blink cameras and pulls their most recent status.""" response = self.get_summary()['devices'] for name in self._cameras: @@ -378,7 +375,7 @@ class Blink(object): pass def get_summary(self): - """Gets a full summary of device information""" + """Get a full summary of device information.""" url = self.urls.home_url headers = self._auth_header @@ -388,7 +385,7 @@ class Blink(object): return _request(url, headers=headers, reqtype='get') def get_cameras(self): - """Finds and creates cameras""" + """Find and creates cameras.""" response = self.get_summary()['devices'] for element in response: if ('device_type' in element and @@ -400,10 +397,7 @@ class Blink(object): self._idlookup[device.id] = device.name def set_links(self): - """ - Sets access links and required headers - for each camera in system - """ + """Set access links and required headers for each camera in system.""" for name in self._cameras: camera = self._cameras[name] network_id_url = self.urls.network_url + self._network_id @@ -415,8 +409,9 @@ class Blink(object): def setup_system(self): """ - Method logs in and sets auth token and - network ids for future requests. + Wrapper for various setup functions. + + Method logs in and sets auth token, urls, and ids for future requests. """ if self._username is None or self._password is None: raise BlinkAuthenticationException(ERROR.AUTHENTICATE) @@ -427,12 +422,12 @@ class Blink(object): self.set_links() def login(self): - """Prompts user for username and password""" + """Prompt user for username and password.""" self._username = input("Username:") self._password = getpass.getpass("Password:") def get_auth_token(self): - """Retrieves the authentication token from Blink""" + """Retrieve the authentication token from Blink.""" if not isinstance(self._username, str): raise BlinkAuthenticationException(ERROR.USERNAME) if not isinstance(self._password, str): @@ -456,7 +451,7 @@ class Blink(object): self.urls = BlinkURLHandler(self._region_id) def get_ids(self): - """Sets the network ID and Account ID""" + """Set the network ID and Account ID.""" url = self.urls.networks_url headers = self._auth_header diff --git a/examples/arm_system_camera.py b/examples/arm_system_camera.py deleted file mode 100644 index 7d0c240..0000000 --- a/examples/arm_system_camera.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/python -import blinkpy -import time - -blink = blinkpy.Blink() -blink.login() -blink.setup_system() - -for camera in blink.cameras: - print('Arming ' + camera.name) - camera.set_motion_detect(True) - time.sleep(5) - blink.refresh() - -print('Arming Blink') -blink.arm = True -time.sleep(5) -print('Blink armed? ' + str(blink.arm)) -print('Disarming Blink') -time.sleep(5) -blink.arm = False -print('Blink armed? ' + str(blink.arm)) diff --git a/examples/list_all_cameras.py b/examples/list_all_cameras.py deleted file mode 100644 index c5bf179..0000000 --- a/examples/list_all_cameras.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python -import blinkpy - -blink = blinkpy.Blink() -blink.login() -blink.setup_system() - -for camera in blink.cameras: - print(camera.name) # Name of the camera - print(camera.id) # Integer id of the camera (assigned by Blink) - print(camera.armed) # Whether the device is armed/disarmed (ie. detecting motion) - print(camera.clip) # Link to last motion clip captured - print(camera.thumbnail) # Link to current camera thumbnail - print(camera.temperature) # Current camera temperature (not super accurate, but might be useful for someone) - print(camera.battery) # Current battery level... I think the value ranges from 0-3, but not quite sure yet. - print(camera.notifications) # Number of unread notifications (ie. motion alerts that haven't been viewed) - print(camera.motion) # Dictionary containing values for keys ['video', 'image', 'time'] - # which correspond to last motion recorded, thumbnail of last motion, and timestamp of last motion \ No newline at end of file diff --git a/helpers/__init__.py b/helpers/__init__.py new file mode 100644 index 0000000..0a53750 --- /dev/null +++ b/helpers/__init__.py @@ -0,0 +1,3 @@ +"""Init file for blinkpy helper functions.""" +from helpers import constants +from helpers import errors \ No newline at end of file diff --git a/constants.py b/helpers/constants.py similarity index 100% rename from constants.py rename to helpers/constants.py diff --git a/errors.py b/helpers/errors.py similarity index 100% rename from errors.py rename to helpers/errors.py diff --git a/setup.py b/setup.py index 71b8d98..5f5cd7c 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- - from setuptools import setup -from constants import (__version__, PROJECT_PACKAGE_NAME, - PROJECT_LICENSE, PROJECT_URL, - PROJECT_EMAIL, PROJECT_DESCRIPTION, - PROJECT_CLASSIFIERS, PROJECT_AUTHOR, - PROJECT_LONG_DESCRIPTION) +from helpers.constants import (__version__, PROJECT_PACKAGE_NAME, + PROJECT_LICENSE, PROJECT_URL, + PROJECT_EMAIL, PROJECT_DESCRIPTION, + PROJECT_CLASSIFIERS, PROJECT_AUTHOR, + PROJECT_LONG_DESCRIPTION) setup( name = PROJECT_PACKAGE_NAME, diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..0c5f348 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Init file for tests directory.""" diff --git a/tests/mock_responses.py b/tests/mock_responses.py index 5306206..5a21cf6 100644 --- a/tests/mock_responses.py +++ b/tests/mock_responses.py @@ -1,18 +1,26 @@ -import constants as const +""" +Mock responses that mimic actual responses from Blink servers. + +This file should be updated any time the Blink server responses +change so we can make sure blinkpy can still communicate. +""" + +import helpers.constants as const -"""Fake device attributes.""" NETWORKS_RESPONSE = {} NETWORKS_RESPONSE['summary'] = {'onboarded': True, 'name': 'Nilfgaard'} -NETWORKS_RESPONSE['networks'] = [{'network_key': None, - 'name': NETWORKS_RESPONSE['summary']['name'], - 'account_id': 1989, - 'id': 9898, - 'encryption_key': None, - 'armed': True, - 'ping_interval': 60, - 'video_destination': 'server', - 'arm_string': 'Armed', - 'feature_plan_id': None}] +NETWORKS_RESPONSE['networks'] = [{ + 'network_key': None, + 'name': NETWORKS_RESPONSE['summary']['name'], + 'account_id': 1989, + 'id': 9898, + 'encryption_key': None, + 'armed': True, + 'ping_interval': 60, + 'video_destination': 'server', + 'arm_string': 'Armed', + 'feature_plan_id': None +}] FIRST_CAMERA = {'device_type': 'camera', 'notifications': 1, @@ -50,50 +58,51 @@ SYNC_MODULE = {'updated_at': '1970-01-01T01:00:00+00:00', 'device_id': 7990, 'status': 'online'} -FIRST_EVENT = {'camera_name': FIRST_CAMERA['name'], - 'updated_at': '2017-10-10T03:37:37+00:00', - 'sync_module_id': None, - 'camera': FIRST_CAMERA['device_id'], - 'type': 'motion', - 'duration': None, - 'status': None, - 'created_at': '2017-01-28T19:51:52+00:00', - 'camera_id': FIRST_CAMERA['device_id'], - 'id': 666777888, - 'siren_id': None, - 'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'], - 'notified': True, - 'siren': None, - 'syncmodule': None, - 'video_url': FIRST_CAMERA['thumbnail'] + '.mp4', - 'command_id': None, - 'network_id': None, - 'account': NETWORKS_RESPONSE['networks'][0]['account_id'], +FIRST_EVENT = {'camera_name': FIRST_CAMERA['name'], + 'updated_at': '2017-10-10T03:37:37+00:00', + 'sync_module_id': None, + 'camera': FIRST_CAMERA['device_id'], + 'type': 'motion', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T19:51:52+00:00', + 'camera_id': FIRST_CAMERA['device_id'], + 'id': 666777888, + 'siren_id': None, + 'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'], + 'notified': True, + 'siren': None, + 'syncmodule': None, + 'video_url': FIRST_CAMERA['thumbnail'] + '.mp4', + 'command_id': None, + 'network_id': None, + 'account': NETWORKS_RESPONSE['networks'][0]['account_id'], 'video_id': 123000321} -SECOND_EVENT = {'updated_at': '2017-01-28T19:51:29+00:00', - 'sync_module_id': SYNC_MODULE['device_id'], - 'camera': None, - 'type': 'armed', - 'duration': None, - 'status': None, - 'created_at': '2017-01-28T19:51:29+00:00', - 'camera_id': None, - 'id': 333777555, - 'siren_id': None, - 'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'], - 'notified': False, - 'siren': None, - 'syncmodule': SYNC_MODULE['device_id'], - 'command_id': None, - 'network_id': None, - 'account': NETWORKS_RESPONSE['networks'][0]['account_id']} - +SECOND_EVENT = {'updated_at': '2017-01-28T19:51:29+00:00', + 'sync_module_id': SYNC_MODULE['device_id'], + 'camera': None, + 'type': 'armed', + 'duration': None, + 'status': None, + 'created_at': '2017-01-28T19:51:29+00:00', + 'camera_id': None, + 'id': 333777555, + 'siren_id': None, + 'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'], + 'notified': False, + 'siren': None, + 'syncmodule': SYNC_MODULE['device_id'], + 'command_id': None, + 'network_id': None, + 'account': NETWORKS_RESPONSE['networks'][0]['account_id']} """Fake response content.""" LOGIN_RESPONSE = {} LOGIN_RESPONSE['region'] = {'ciri': 'Cintra'} -LOGIN_RESPONSE['networks'] = {NETWORKS_RESPONSE['networks'][0]['id'] : NETWORKS_RESPONSE['summary']} +LOGIN_RESPONSE['networks'] = { + NETWORKS_RESPONSE['networks'][0]['id']: NETWORKS_RESPONSE['summary'] +} LOGIN_RESPONSE['authtoken'] = {'authtoken': 'foobar7117', 'message': 'auth'} RESPONSE = {} @@ -111,42 +120,54 @@ RESPONSE['syncmodule'] = {'name': 'Vengerberg', 'status': 'online'} def mocked_requests_post(*args, **kwargs): """Mock post request.""" class MockPostResponse: - def __init__(self,json_data,status_code): + """Class for mock post response.""" + + def __init__(self, json_data, status_code): + """Initialze mock post response.""" self.json_data = json_data self.status_code = status_code + def json(self): + """Return json data from post request.""" return self.json_data - + url_tail = args[0].split("/")[-1] if args[0] == const.LOGIN_URL: return MockPostResponse(LOGIN_RESPONSE, 200) elif url_tail == 'arm' or url_tail == 'disarm': + # pylint: disable=global-variable-not-assigned global NETWORKS_RESPONSE + # pylint: disable=global-variable-not-assigned global RESPONSE NETWORKS_RESPONSE['networks'][0]['armed'] = url_tail == 'arm' - RESPONSE['network']['armed'] = NETWORKS_RESPONSE['networks'][0]['armed'] + RESPONSE['network']['armed'] = url_tail == 'arm' return MockPostResponse({}, 200) - return MockPostResponse({'message':'ERROR','code':404}, 404) - + return MockPostResponse({'message': 'ERROR', 'code': 404}, 404) + + def mocked_requests_get(*args, **kwargs): """Mock get request.""" class MockGetResponse: - def __init__(self,json_data,status_code): + """Class for mock get response.""" + + def __init__(self, json_data, status_code): + """Initialze mock get response.""" self.json_data = json_data self.status_code = status_code - def json(self): - return self.json_data + def json(self): + """Return json data from post request.""" + return self.json_data + # pylint: disable=unused-variable (region_id, region), = LOGIN_RESPONSE['region'].items() set_region_id = args[0].split('/')[2].split('.')[0] - NETURL = 'https://' + set_region_id + '.' + const.BLINK_URL + '/networks' - if args[0] == NETURL: + neturl = 'https://' + set_region_id + '.' + const.BLINK_URL + '/networks' + if args[0] == neturl: return MockGetResponse(NETWORKS_RESPONSE, 200) elif set_region_id != region_id: raise ConnectionError('Received url ' + args[0]) else: return MockGetResponse(RESPONSE, 200) - return MockGetResponse({'message':'ERROR','code':404}, 404) - + return MockGetResponse({'message': 'ERROR', 'code': 404}, 404) diff --git a/tests/test_blink_cameras.py b/tests/test_blink_cameras.py index f87f5c1..02fd920 100644 --- a/tests/test_blink_cameras.py +++ b/tests/test_blink_cameras.py @@ -1 +1,80 @@ -# TODO \ No newline at end of file +""" +Tests the camera initialization and attributes of +individual BlinkCamera instantiations. +""" + +import unittest +from unittest import mock +import blinkpy +import tests.mock_responses as mresp + +USERNAME = 'foobar' +PASSWORD = 'deadbeef' + + +class TestBlinkCameraSetup(unittest.TestCase): + """Test the Blink class in blinkpy.""" + + def setUp(self): + """Set up Blink module.""" + self.blink = blinkpy.Blink(username=USERNAME, + password=PASSWORD) + + def tearDown(self): + """Clean up after test.""" + self.blink = None + + @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_values_from_setup(self, mock_get, mock_post): + """Tests all property values after camera setup.""" + self.blink.setup_system() + + # Get expected test values + test_network_id = str(mresp.NETWORKS_RESPONSE['networks'][0]['id']) + # pylint: disable=unused-variable + (region_id, region), = mresp.LOGIN_RESPONSE['region'].items() + # pylint: disable=protected-access + expected_header = self.blink._auth_header + test_urls = blinkpy.BlinkURLHandler(region_id) + + test_cameras = dict() + for element in mresp.RESPONSE['devices']: + if ('device_type' in element and + element['device_type'] == 'camera'): + test_cameras[element['name']] = { + 'device_id': str(element['device_id']), + 'armed': element['armed'], + 'thumbnail': (test_urls.base_url + + element['thumbnail'] + '.jpg'), + 'temperature': element['temp'], + 'battery': element['battery'], + 'notifications': element['notifications'] + } + test_net_id_url = test_urls.network_url + test_network_id + for name in self.blink.cameras: + camera = self.blink.cameras[name] + self.assertEqual(name, camera.name) + if name in test_cameras: + self.assertEqual(camera.id, + test_cameras[name]['device_id']) + self.assertEqual(camera.armed, + test_cameras[name]['armed']) + self.assertEqual(camera.thumbnail, + test_cameras[name]['thumbnail']) + self.assertEqual(camera.temperature, + test_cameras[name]['temperature']) + self.assertEqual(camera.battery, + test_cameras[name]['battery']) + self.assertEqual(camera.notifications, + test_cameras[name]['notifications']) + else: + self.fail("Camera wasn't initialized: " + name) + + expected_arm_link = test_net_id_url + '/camera/' + camera.id + '/' + expected_image_link = expected_arm_link + 'thumbnail' + self.assertEqual(camera.image_link, expected_image_link) + self.assertEqual(camera.arm_link, expected_arm_link) + self.assertEqual(camera.header, expected_header) diff --git a/tests/test_blink_system.py b/tests/test_blink_system.py index f56b8e3..360abd1 100644 --- a/tests/test_blink_system.py +++ b/tests/test_blink_system.py @@ -1,15 +1,22 @@ -import requests +""" +Tests the system initialization and attributes of +the main Blink system. Tests if we properly catch +any communication related errors at startup. +""" + import unittest from unittest import mock import blinkpy import tests.mock_responses as mresp -import constants as const +import helpers.constants as const USERNAME = 'foobar' PASSWORD = 'deadbeef' + class TestBlinkSetup(unittest.TestCase): """Test the Blink class in blinkpy.""" + def setUp(self): """Set up Blink module.""" self.blink_no_cred = blinkpy.Blink() @@ -23,7 +30,9 @@ class TestBlinkSetup(unittest.TestCase): def test_initialization(self): """Verify we can initialize blink.""" + # pylint: disable=protected-access self.assertEqual(self.blink._username, USERNAME) + # pylint: disable=protected-access self.assertEqual(self.blink._password, PASSWORD) def test_no_credentials(self): @@ -35,62 +44,72 @@ class TestBlinkSetup(unittest.TestCase): def test_no_auth_header(self): """Check that we throw an excpetion when no auth header given.""" + # pylint: disable=unused-variable (region_id, region), = mresp.LOGIN_RESPONSE['region'].items() self.blink.urls = blinkpy.BlinkURLHandler(region_id) with self.assertRaises(blinkpy.BlinkException): self.blink.get_ids() - - @mock.patch('blinkpy.getpass.getpass') + + @mock.patch('blinkpy.blinkpy.getpass.getpass') def test_manual_login(self, getpwd): """Check that we can manually use the login() function.""" getpwd.return_value = PASSWORD with mock.patch('builtins.input', return_value=USERNAME): self.blink_no_cred.login() + # pylint: disable=protected-access self.assertEqual(self.blink_no_cred._username, USERNAME) + # pylint: disable=protected-access self.assertEqual(self.blink_no_cred._password, PASSWORD) - @mock.patch('blinkpy.requests.post', side_effect=mresp.mocked_requests_post) - @mock.patch('blinkpy.requests.get', side_effect=mresp.mocked_requests_get) + @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_full_setup(self, mock_get, mock_post): """Check that we can set Blink up properly.""" self.blink.setup_system() - + # Get all test values authtoken = mresp.LOGIN_RESPONSE['authtoken']['authtoken'] (region_id, region), = mresp.LOGIN_RESPONSE['region'].items() host = region_id + '.' + const.BLINK_URL network_id = mresp.NETWORKS_RESPONSE['networks'][0]['id'] account_id = mresp.NETWORKS_RESPONSE['networks'][0]['account_id'] - TestURLs = blinkpy.BlinkURLHandler(region_id) - TestCameras = list() - TestCameraID = dict() + test_urls = blinkpy.BlinkURLHandler(region_id) + test_cameras = list() + test_camera_id = dict() for element in mresp.RESPONSE['devices']: - if 'device_type' in element and element['device_type'] == 'camera': - TestCameras.append(element['name']) - TestCameraID[str(element['device_id'])] = element['name'] + if ('device_type' in element and + element['device_type'] == 'camera'): + test_cameras.append(element['name']) + test_camera_id[str(element['device_id'])] = element['name'] # Check that all links have been set properly self.assertEqual(self.blink.region_id, region_id) - self.assertEqual(self.blink.urls.base_url, TestURLs.base_url) - self.assertEqual(self.blink.urls.home_url, TestURLs.home_url) - self.assertEqual(self.blink.urls.event_url, TestURLs.event_url) - self.assertEqual(self.blink.urls.network_url, TestURLs.network_url) - self.assertEqual(self.blink.urls.networks_url, TestURLs.networks_url) + self.assertEqual(self.blink.urls.base_url, test_urls.base_url) + self.assertEqual(self.blink.urls.home_url, test_urls.home_url) + self.assertEqual(self.blink.urls.event_url, test_urls.event_url) + self.assertEqual(self.blink.urls.network_url, test_urls.network_url) + self.assertEqual(self.blink.urls.networks_url, test_urls.networks_url) # Check that all properties have been set after startup + # pylint: disable=protected-access self.assertEqual(self.blink._token, authtoken) + # pylint: disable=protected-access self.assertEqual(self.blink._host, host) self.assertEqual(self.blink.network_id, str(network_id)) self.assertEqual(self.blink.account_id, str(account_id)) self.assertEqual(self.blink.region, region) # Verify we have initialized all the cameras - self.assertEqual(self.blink.id_table, TestCameraID) - for camera in TestCameras: + self.assertEqual(self.blink.id_table, test_camera_id) + for camera in test_cameras: self.assertTrue(camera in self.blink.cameras) - @mock.patch('blinkpy.requests.post', side_effect=mresp.mocked_requests_post) - @mock.patch('blinkpy.requests.get', side_effect=mresp.mocked_requests_get) + @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_arm_disarm_system(self, mock_get, mock_post): """Check that we can arm/disarm the system""" self.blink.setup_system() @@ -99,8 +118,10 @@ class TestBlinkSetup(unittest.TestCase): self.blink.arm = True self.assertIs(self.blink.arm, True) - @mock.patch('blinkpy.requests.post', side_effect=mresp.mocked_requests_post) - @mock.patch('blinkpy.requests.get', side_effect=mresp.mocked_requests_get) + @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_check_online_status(self, mock_get, mock_post): """Check that we can get our online status.""" self.blink.setup_system() diff --git a/tox.ini b/tox.ini index e07099e..5b91d33 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,10 @@ skip_missing_interpreters = True [testenv] setenv = - LAND=en_US.UTF-8 + LANG=en_US.UTF-8 PYTHONPATH = {toxinidir} commands = - py.test -v --timeout=30 --duration=10 --cov=blinkpy --cov-report html {posargs} + py.test -v --timeout=30 --duration=10 --cov=blinkpy --cov-report term {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt @@ -19,12 +19,6 @@ deps = basepython = python3 ignore_errors = True commands = - ;flake8 blinkpy.py tests - flake8 blinkpy.py - ;pylint blinkpy.py tests - pylint blinkpy.py - ;pydocstyle blinkpy.py tests - -;[flake8] -;ignore = E501 -;exclude = .venv,.git,.tox,dist,doc,*lib/python,*egg,build \ No newline at end of file + pylint --rcfile={toxinidir}/pylintrc --load-plugins=pylint.extensions.mccabe blinkpy.py tests + flake8 blinkpy.py tests + pydocstyle blinkpy.py tests From 326906f073fac10c23942a38ea7572fe2f01c3e7 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 11 Mar 2017 23:21:26 -0500 Subject: [PATCH 16/21] Getting there... --- .coveragerc | 5 +++++ .github/PULL_REQUEST_TEMPLATE.md | 9 +++++++++ .gitignore | 2 -- README.rst | 2 ++ helpers/constants.py | 4 ++++ pylintrc | 19 +------------------ tests/test_blink_cameras.py | 31 +++++++++++++++++++++++++++++++ tests/test_blink_functions.py | 1 + tests/test_blink_hass.py | 1 + 9 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 tests/test_blink_functions.py create mode 100644 tests/test_blink_hass.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..b050fc2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = + helpers/* + tests/* + setup.py \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bfd9f43 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Description: + + +**Related issue (if applicable):** fixes # + +## Checklist: +- [ ] Local tests with `tox` run successfully **PR cannot be meged unless tests pass** +- [ ] If user-facing functionality changed, README.rst updated +- [ ] Tests added to verify new code works \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2155ed6..166cd50 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,5 @@ htmlcov/* *.pyc *.egg*/* dist/* -MANIFEST -README .sh build/* diff --git a/README.rst b/README.rst index 5dffd5a..67d2859 100644 --- a/README.rst +++ b/README.rst @@ -96,10 +96,12 @@ Class Descriptions class BlinkCamera(config, urls) The ``BlinkCamera`` class expects to receive: + * A dictionary ``config`` that contains the camera name, device id, armed status, thumbnail url, camera temperature, camery battery level, number of notifications, and region id * A ``BlinkURLHandler`` object that contains all the links necessary for communication. Ultimately, this class is just a wrapper for each individual camera in order to make communication with individual cameras less clunky. The following properties/methods are availiable (in addition to the ones mentioned earlier): + * ``BlinkCamera.snap_picture()`` Takes an image with the camera and saves it as the new thumbnail. The ``Blink.refresh()`` method should be called after this if you want to store the new thumbnail link. * ``BlinkCamera.set_motion_detect(enable=True/False)`` Sending True to this function will enable motion detection for the camera. Setting to False will disable motion detection. * ``BlinkCamera.image_to_file(path)`` This will write the current thumbnail to the location indicated in 'path' diff --git a/helpers/constants.py b/helpers/constants.py index e3b2c7a..08c7861 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -2,6 +2,8 @@ constants.py Generates constants for use in blinkpy ''' +import os + MAJOR_VERSION = 0 MINOR_VERSION = 4 PATCH_VERSION = 4 @@ -23,6 +25,8 @@ PROJECT_LONG_DESCRIPTION = ('blinkpy is an open-source ' 'system with the intention for easy ' 'integration into various home ' 'automation platforms.') +if os.path.exists('README.rst'): + PROJECT_LONG_DESCRIPTION = open('README.rst').read() PROJECT_CLASSIFIERS = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', diff --git a/pylintrc b/pylintrc index 1ec5ded..c458c3e 100644 --- a/pylintrc +++ b/pylintrc @@ -3,26 +3,13 @@ reports=no # Reasons disabled: # locally-disabled - it spams too much -# duplicate-code - unavoidable -# cyclic-import - doesn't test if both import on load -# abstract-class-little-used - prevents from setting right foundation -# abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings -# global-statement - used for the on-demand requirement installation -# redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* -# abstract-method - with intro of async there are always methods missing disable= locally-disabled, - duplicate-code, - cyclic-import, - abstract-class-little-used, - abstract-class-not-used, unused-argument, - global-statement, - redefined-variable-type, too-many-arguments, too-many-branches, too-many-instance-attributes, @@ -31,8 +18,4 @@ disable= too-many-return-statements, too-many-statements, too-many-lines, - too-few-public-methods, - abstract-method - -[EXCEPTIONS] -overgeneral-exceptions=Exception \ No newline at end of file + too-few-public-methods, \ No newline at end of file diff --git a/tests/test_blink_cameras.py b/tests/test_blink_cameras.py index 02fd920..0f80693 100644 --- a/tests/test_blink_cameras.py +++ b/tests/test_blink_cameras.py @@ -24,6 +24,37 @@ class TestBlinkCameraSetup(unittest.TestCase): """Clean up after test.""" self.blink = None + @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): + """Tests all property set/recall.""" + test_value = 'foobar' + self.blink.setup_system() + for name in self.blink.cameras: + camera = self.blink.cameras[name] + camera.name = test_value + camera.clip = test_value + '.mp4' + camera.thumbnail = test_value + '.jpg' + camera.temperature = 10 + camera.battery = 0 + camera.notifications = 100 + camera.image_link = test_value + '/image.jpg' + camera.arm_link = test_value + '/arm' + camera.header = {'foo': 'bar'} + camera.motion = {'bar': 'foo'} + self.assertEqual(camera.clip, test_value + '.mp4') + self.assertEqual(camera.name, test_value) + self.assertEqual(camera.thumbnail, test_value + '.jpg') + self.assertEqual(camera.temperature, 10) + self.assertEqual(camera.battery, 0) + self.assertEqual(camera.notifications, 100) + self.assertEqual(camera.image_link, test_value + '/image.jpg') + self.assertEqual(camera.arm_link, test_value + '/arm') + self.assertEqual(camera.header, {'foo': 'bar'}) + self.assertEqual(camera.motion, {'bar': 'foo'}) + @mock.patch('blinkpy.blinkpy.requests.post', side_effect=mresp.mocked_requests_post) @mock.patch('blinkpy.blinkpy.requests.get', diff --git a/tests/test_blink_functions.py b/tests/test_blink_functions.py new file mode 100644 index 0000000..f98cfbd --- /dev/null +++ b/tests/test_blink_functions.py @@ -0,0 +1 @@ +# Test blink camera functions diff --git a/tests/test_blink_hass.py b/tests/test_blink_hass.py new file mode 100644 index 0000000..79ae84e --- /dev/null +++ b/tests/test_blink_hass.py @@ -0,0 +1 @@ +# Test functionality used for blink component in home assistant From af0cdb006a2187864704adfbe6a8506dd95ce3d6 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 12 Mar 2017 18:21:28 -0400 Subject: [PATCH 17/21] Completed test suite. Fixed some minor issues with file writing and regions --- API.md | 287 ++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 83 ++++++++++ LICENSE => LICENSE.md | 0 MANIFEST.in | 3 +- blinkpy.py | 6 +- pylintrc | 2 + tests/mock_responses.py | 110 ++++++++++++- tests/test_blink_cameras.py | 16 +- tests/test_blink_functions.py | 160 ++++++++++++++++++- tests/test_blink_hass.py | 1 - tests/test_blink_system.py | 29 +++- tox.ini | 4 +- 12 files changed, 665 insertions(+), 36 deletions(-) create mode 100644 API.md create mode 100644 CONTRIBUTING.md rename LICENSE => LICENSE.md (100%) delete mode 100644 tests/test_blink_hass.py diff --git a/API.md b/API.md new file mode 100644 index 0000000..90549bc --- /dev/null +++ b/API.md @@ -0,0 +1,287 @@ +# BlinkMonitorProtocol +Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring and Alert System. + +Copied from https://github.com/MattTW/BlinkMonitorProtocol + +I am not affiliated with the company in any way - this documentation is strictly **"AS-IS"**. My goal was to uncover enough to arm and disarm the system programatically so that I can issue those commands in sync with my home alarm system arm/disarm. Just some raw notes at this point but should be enough for creating programmatic APIs. Lots more to be discovered and documented - feel free to contribute! + +The Client API is a straightforward REST API using JSON and HTTPS. + +## This Document +The purpose here is to describe what is going on behind the scenes, and what commands we know are available to communicate with Blink servers. This is NOT a description of the blinkpy module, but a description of the commands blinkpy relies on to effectively communicate. + +##Login + +Client login to the Blink Servers. + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "Content-Type: application/json" --data-binary '{ +> "password" : "*your blink password*", +> "client_specifier" : "iPhone 9.2 | 2.2 | 222", +> "email" : "*your blink login/email*" +>}' --compressed https://prod.immedia-semi.com/login + +**Response:** +>{"authtoken":{"authtoken":"*an auth token*","message":"auth"}} + +**Notes:** +The authtoken value is passed in a header in future calls. + +##Networks + +Obtain information about the Blink networks defined for the logged in user. + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/networks + +**Response:** +JSON response containing information including Network ID and Account ID. + +**Notes:** +Network ID is needed to issue arm/disarm calls + + +##Sync Modules + +Obtain information about the Blink Sync Modules on the given network. + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules + +**Response:** +JSON response containing information about the known state of the Sync module, most notably if it is online + +**Notes:** +Probably not strictly needed but checking result can verify that the sync module is online and will respond to requests to arm/disarm, etc. + + +##Arm + +Arm the given network (start recording/reporting motion events) + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/arm + +**Response:** +JSON response containing information about the arm command request, including the command/request ID + +**Notes:** +When this call returns, it does not mean the arm request is complete, the client must gather the request ID from the response and poll for the status of the command. + +##Disarm + +Disarm the given network (stop recording/reporting motion events) + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm + +**Response:** +JSON response containing information about the disarm command request, including the command/request ID + +**Notes:** +When this call returns, it does not mean the disarm request is complete, the client must gather the request ID from the response and poll for the status of the command. + + +##Command Status + +Get status info on the given command + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/command/*command_id* + +**Response:** +JSON response containing state information of the given command, most notably whether it has completed and was successful. + +**Notes:** +After an arm/disarm command, the client appears to poll this URL every second or so until the response indicates the command is complete. + +**Known Commands:** +lv_relay, arm, disarm, thumbnail, clip + +##Home Screen + +Return information displayed on the home screen of the mobile client + +**Request:** +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/homescreen + +**Response:** +JSON response containing information that the mobile client displays on the home page, including: status, armed state, links to thumbnails for each camera, etc. + +**Notes:** +Not necessary to as part of issuing arm/disarm commands, but contains good summary info. + +##Events, thumbnails & video captures + +**Request** +Get events for a given network (sync module) -- Need network ID from home + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/events/network/*network__id* + +**Response** +A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip + + +**Request** +Get a video clip from the events list + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed **video url from events list.mp4** > video.mp4 + +**Response** +The mp4 video + +**Request** +Get a thumbnail from the events list + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed **video url from events list.jpg** > video_thumb.jpg + +**Response** +The jpg bytes. + +**Notes** +Note that you replace the 'mp4' with a 'jpg' to get the thumbnail + +**Request** +Captures a new thumbnail for a camera + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail + +**Response** +Command information. + +**Request** +Captures a new video for a camera + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip + +**Response** +Command information. + +##Video Information + +**Request** +Get the total number of videos in the system + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/count + +**Response** +JSON response containing the total video count. + +**Request** +Gets a paginated set of video information + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/page/0 + +**Response** +JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length + +**Request** +Gets information for a specific video by ID + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/video/*video_id* + +**Response** +JSON response containing video information + +**Request** +Gets a list of unwatched videos + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/unwatched + +**Response** +JSON response containing unwatched video information + +**Request** +Deletes a video + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*/delete + +**Response** +Unknown - not tested + +**Request** +Deletes all videos + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/videos/deleteall + +**Response** +Unknown - not tested + +##Cameras + +**Request** +Gets a list of cameras + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/cameras + +**Response** +JSON response containing camera information + +**Request** +Gets information for one camera + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id* + +**Response** +JSON response containing camera information + +**Request** +Gets camera sensor information + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals + +**Response** +JSON response containing camera sensor information, such as wifi strength, temperature, and battery level + +**Request** +Enables motion detection for one camera + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable + +**Response** +JSON response containing camera information + +**Request** +Disables motion detection for one camera + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable + +**Response** +JSON response containing camera information + +*Note*: enabling or disabling motion detection is independent of arming or disarming the system. No motion detection or video recording will take place unless the system is armed. + + +##Miscellaneous + +**Request** +Gets information about devices that have connected to the blink service + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/account/clients + +**Response** +JSON response containing client information, including: type, name, connection time, user ID + +**Request** +Gets information about supported regions + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/regions + +**Response** +JSON response containing region information + +**Request** +Gets information about system health + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/health + +**Response** +"all ports tested are open" + +**Request** +Gets information about programs + +>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v1/networks/*network_id*/programs + +**Response** +Unknown. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..af0aa2e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ +# Contributing to blinkpy + +Everyone is welcome to contribute to blinkpy! The process to get started is described below. + +## Fork the Repository + +You can do this right in gituhb: just click the 'fork' button at the top right. + +## Setup Local Repository + +```shell +$ git clone https://github.com//blinkpy.git +$ cd blinkpy +$ git remote add upstream https://github.com/fronzbot/blinkpy.git +``` + +## Create a Local Branch + +First, you need to make sure you're on the 'dev' branch: +``git checkout dev`` +Next, you will want to create a new branch to hold your changes: +``git checkout -b `` + +## Make changes + +Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section) + +## Commit Your Changes + +To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub: + +```shell +$ git add . +$ git commit -m "Put your commit text here. Please be concise, but descriptive." +$ git push origin HEAD +``` + +## Testing + +It is important to test the code to make sure your changes don't break anything major and that they pass PEP8 style conventions. +FIrst, you need to locally install ``tox`` + +```shell +$ pip3 install tox +``` + +You can then run all of the tests with the following command: + +```shell +$ tox +``` + +### Tips + +If you only want to see if you can pass the local tests, you can run ``tox -e py34``. If you just want to check for style violations, you can run ``tox -e lint``. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters. + +If you need to change anything in ``requirements.txt`` for any reason, you'll want to regenerate the virtual envrionments used by ``tox`` by running with the ``-r`` flag: ``tox -r`` + +Please do not locally disable any linter warnings within the ``blinkpy.py`` module itself (it's ok to do this in any of the ``test_*.py`` files) + +# Catching Up With Reality + +If your code is taking a while to develop, you may be behind the ``dev`` branch, in which case you need to catch up before creating your pull-request. To do this you can run ``git rebase`` as follows (running this on your local branch): + +```shell +$ git fetch upstream/dev +$ git rebase upstrea/dev +``` + +If rebase detects conflicts, repeat the following process until all changes have been resolved: + +1. ``git status`` shows you the filw with a conflict. You will need to edit that file and resolve the lines between ``<<<< | >>>>`. +2. Add the modified file: ``git add `` or ``git add .``. +3. Continue rebase: ``git rebase --continue``. +4. Repeat until all conflicts resolved. + +# Creating a Pull Request + +Please follow these steps to create a pull request against the ``dev`` branch: [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/) + +# Monitor Build Status + +Once you create your PR, you can monitor the status of your build [here](https://travis-ci.org/fronzbot/blinkpy), Your code will be tested to ensure it passes and won't cause any problems after merging. \ No newline at end of file diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/MANIFEST.in b/MANIFEST.in index 831efda..3eb1ac3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.rst -include LICENSE +include LICENSE.md +include API.md include tests/*.py include helpers/*.py diff --git a/blinkpy.py b/blinkpy.py index 1a773b5..296f4d3 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -14,6 +14,7 @@ I am in no way affiliated with Blink, nor Immedia Inc. import json import getpass +from shutil import copyfileobj import requests import helpers.errors as ERROR from helpers.constants import (BLINK_URL, LOGIN_URL, @@ -247,11 +248,10 @@ class BlinkCamera(object): """Write image to file.""" thumb = self.image_refresh() response = _request(thumb, headers=self._header, - stream=True, json_resp=False) + reqtype='get', stream=True, json_resp=False) if response.status_code == 200: with open(path, 'wb') as imgfile: - for chunk in response: - imgfile.write(chunk) + copyfileobj(response.raw, imgfile) class Blink(object): diff --git a/pylintrc b/pylintrc index c458c3e..4c1c361 100644 --- a/pylintrc +++ b/pylintrc @@ -3,6 +3,7 @@ reports=no # Reasons disabled: # locally-disabled - it spams too much +# duplicate-code - it's annoying # 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-* @@ -10,6 +11,7 @@ reports=no disable= locally-disabled, unused-argument, + duplicate-code, too-many-arguments, too-many-branches, too-many-instance-attributes, diff --git a/tests/mock_responses.py b/tests/mock_responses.py index 5a21cf6..c57e72e 100644 --- a/tests/mock_responses.py +++ b/tests/mock_responses.py @@ -22,6 +22,8 @@ NETWORKS_RESPONSE['networks'] = [{ 'feature_plan_id': None }] +NEW_THUMBNAIL = '/NEW/THUMBNAIL/YAY' + FIRST_CAMERA = {'device_type': 'camera', 'notifications': 1, 'battery': 2, @@ -116,6 +118,16 @@ RESPONSE['network'] = {'armed': NETWORKS_RESPONSE['networks'][0]['armed'], RESPONSE['event'] = [FIRST_EVENT, SECOND_EVENT] RESPONSE['syncmodule'] = {'name': 'Vengerberg', 'status': 'online'} +MOCK_BYTES = '\x00\x10JFIF\x00\x01' + +IMAGE_TO_WRITE_URL = list() +IMAGE_TO_WRITE_URL.append('https://ciri.' + const.BLINK_URL + + FIRST_CAMERA['thumbnail'] + '.jpg') +IMAGE_TO_WRITE_URL.append('https://ciri.' + const.BLINK_URL + + SECOND_CAMERA['thumbnail'] + '.jpg') + +FAKE_FILES = list() + def mocked_requests_post(*args, **kwargs): """Mock post request.""" @@ -131,17 +143,46 @@ def mocked_requests_post(*args, **kwargs): """Return json data from post request.""" return self.json_data - url_tail = args[0].split("/")[-1] + # pylint: disable=global-variable-not-assigned + global RESPONSE + # pylint: disable=global-variable-not-assigned + global NETWORKS_RESPONSE + + if args[0] is not None: + url_tail = args[0].split("/")[-1] + else: + return MockPostResponse({'message': 'ERROR', 'code': 404}, 404) + if args[0] == const.LOGIN_URL: + # Request to login return MockPostResponse(LOGIN_RESPONSE, 200) elif url_tail == 'arm' or url_tail == 'disarm': - # pylint: disable=global-variable-not-assigned - global NETWORKS_RESPONSE - # pylint: disable=global-variable-not-assigned - global RESPONSE + # Request to arm/disarm system NETWORKS_RESPONSE['networks'][0]['armed'] = url_tail == 'arm' RESPONSE['network']['armed'] = url_tail == 'arm' return MockPostResponse({}, 200) + elif url_tail == 'enable' or url_tail == 'disable': + # Request to enable/disable motion detection per camera + received_id = args[0].split("/")[-2] + all_devices = list() + for element in RESPONSE['devices']: + all_devices.append(element) + current_id = element['device_id'] + if str(current_id) == received_id: + element['armed'] = url_tail == 'enable' + element['enabled'] = url_tail == 'enable' + RESPONSE['devices'] = all_devices + return MockPostResponse({}, 200) + elif url_tail == 'thumbnail': + # Requesting a new image + received_id = args[0].split("/")[-2] + all_devices = list() + for element in RESPONSE['devices']: + all_devices.append(element) + if str(element['device_id']) == received_id: + element['thumbnail'] = NEW_THUMBNAIL + RESPONSE['devices'] = all_devices + return MockPostResponse({}, 200) return MockPostResponse({'message': 'ERROR', 'code': 404}, 404) @@ -151,14 +192,21 @@ def mocked_requests_get(*args, **kwargs): class MockGetResponse: """Class for mock get response.""" - def __init__(self, json_data, status_code): - """Initialze mock get response.""" + def __init__(self, json_data, status_code, raw_data=None): + """Initialize mock get response.""" self.json_data = json_data self.status_code = status_code + self.raw_data = raw_data def json(self): - """Return json data from post request.""" + """Return json data from get request.""" return self.json_data + + @property + def raw(self): + """Return raw data from get request.""" + return self.raw_data + # pylint: disable=unused-variable (region_id, region), = LOGIN_RESPONSE['region'].items() set_region_id = args[0].split('/')[2].split('.')[0] @@ -167,7 +215,53 @@ def mocked_requests_get(*args, **kwargs): return MockGetResponse(NETWORKS_RESPONSE, 200) elif set_region_id != region_id: raise ConnectionError('Received url ' + args[0]) + elif args[0] in IMAGE_TO_WRITE_URL: + return MockGetResponse({}, 200, raw_data=MOCK_BYTES) else: return MockGetResponse(RESPONSE, 200) return MockGetResponse({'message': 'ERROR', 'code': 404}, 404) + + +def mocked_copyfileobj(*args, **kwargs): + """Mock shutil.copyfileobj.""" + class MockCopyFileObj: + """Class for mock copy file.""" + + def __init__(self, src, dst): + """Initialize copyfile mock.""" + self.src = src + self.dst = dst + # pylint: disable=global-variable-not-assigned + global FAKE_FILES + mockobj = MockCopyFileObj(args[0], args[1]) + FAKE_FILES.append(mockobj.src) + return + + +def get_test_cameras(base_url): + """Helper function to return cameras named in this file.""" + test_cameras = dict() + for element in RESPONSE['devices']: + if ('device_type' in element and + element['device_type'] == 'camera'): + test_cameras[element['name']] = { + 'device_id': str(element['device_id']), + 'armed': element['armed'], + 'thumbnail': (base_url + + element['thumbnail'] + '.jpg'), + 'temperature': element['temp'], + 'battery': element['battery'], + 'notifications': element['notifications'] + } + return test_cameras + + +def get_test_id_table(): + """Helper function to return mock id table.""" + test_id_table = dict() + for element in RESPONSE['devices']: + if ('device_type' in element and + element['device_type'] == 'camera'): + test_id_table[str(element['device_id'])] = element['name'] + return test_id_table diff --git a/tests/test_blink_cameras.py b/tests/test_blink_cameras.py index 0f80693..dd5c62b 100644 --- a/tests/test_blink_cameras.py +++ b/tests/test_blink_cameras.py @@ -31,6 +31,7 @@ class TestBlinkCameraSetup(unittest.TestCase): def test_camera_properties(self, mock_get, mock_post): """Tests all property set/recall.""" test_value = 'foobar' + test_region_id = list(mresp.LOGIN_RESPONSE['region'].keys())[0] self.blink.setup_system() for name in self.blink.cameras: camera = self.blink.cameras[name] @@ -54,6 +55,7 @@ class TestBlinkCameraSetup(unittest.TestCase): self.assertEqual(camera.arm_link, test_value + '/arm') self.assertEqual(camera.header, {'foo': 'bar'}) self.assertEqual(camera.motion, {'bar': 'foo'}) + self.assertEqual(camera.region_id, test_region_id) @mock.patch('blinkpy.blinkpy.requests.post', side_effect=mresp.mocked_requests_post) @@ -71,19 +73,7 @@ class TestBlinkCameraSetup(unittest.TestCase): expected_header = self.blink._auth_header test_urls = blinkpy.BlinkURLHandler(region_id) - test_cameras = dict() - for element in mresp.RESPONSE['devices']: - if ('device_type' in element and - element['device_type'] == 'camera'): - test_cameras[element['name']] = { - 'device_id': str(element['device_id']), - 'armed': element['armed'], - 'thumbnail': (test_urls.base_url + - element['thumbnail'] + '.jpg'), - 'temperature': element['temp'], - 'battery': element['battery'], - 'notifications': element['notifications'] - } + test_cameras = mresp.get_test_cameras(test_urls.base_url) test_net_id_url = test_urls.network_url + test_network_id for name in self.blink.cameras: camera = self.blink.cameras[name] diff --git a/tests/test_blink_functions.py b/tests/test_blink_functions.py index f98cfbd..ad3b6f9 100644 --- a/tests/test_blink_functions.py +++ b/tests/test_blink_functions.py @@ -1 +1,159 @@ -# Test blink camera functions +"""Tests camera and system functions.""" + +import unittest +from unittest import mock +import blinkpy +import tests.mock_responses as mresp + +USERNAME = 'foobar' +PASSWORD = 'deadbeef' + + +class TestBlinkFunctions(unittest.TestCase): + """Test Blink and BlinkCamera functions in blinkpy.""" + + def setUp(self): + """Set up Blink module.""" + self.blink = blinkpy.Blink(username=USERNAME, + password=PASSWORD) + (self.region_id, self.region), = mresp.LOGIN_RESPONSE['region'].items() + self.test_urls = blinkpy.BlinkURLHandler(self.region_id) + + def tearDown(self): + """Clean up after test.""" + self.blink = None + self.region = None + self.region_id = None + self.test_urls = None + + @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_set_motion_detect(self, mock_get, mock_post): + """Checks if we can set motion detection.""" + self.blink.setup_system() + self.test_urls = blinkpy.BlinkURLHandler(self.region_id) + test_cameras = mresp.get_test_cameras(self.test_urls.base_url) + for camera_name in test_cameras: + self.blink.cameras[camera_name].set_motion_detect(True) + self.blink.refresh() + self.assertEqual(self.blink.cameras[camera_name].armed, True) + self.blink.cameras[camera_name].set_motion_detect(False) + self.blink.refresh() + self.assertEqual(self.blink.cameras[camera_name].armed, False) + + @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_last_motion(self, mock_get, mock_post): + """Checks that we can get the last motion info.""" + self.test_urls = blinkpy.BlinkURLHandler(self.region_id) + test_events = mresp.RESPONSE['event'] + test_video = dict() + test_image = dict() + test_time = dict() + for event in test_events: + if event['type'] == 'motion': + url = self.test_urls.base_url + event['video_url'] + test_video[event['camera_name']] = url + test_image[event['camera_name']] = url[:-3] + 'jpg' + test_time[event['camera_name']] = event['created_at'] + + self.blink.setup_system() + for name in self.blink.cameras: + camera = self.blink.cameras[name] + self.blink.last_motion() + if name in test_video: + self.assertEqual(camera.motion['video'], test_video[name]) + else: + self.assertEqual(camera.motion, {}) + if name in test_image: + self.assertEqual(camera.motion['image'], test_image[name]) + else: + self.assertEqual(camera.motion, {}) + if name in test_video: + self.assertEqual(camera.motion['time'], test_time[name]) + else: + self.assertEqual(camera.motion, {}) + + @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_take_new_picture(self, mock_get, mock_post): + """Checks if we can take a new picture and retrieve the thumbnail.""" + self.blink.setup_system() + test_cameras = mresp.get_test_cameras(self.test_urls.base_url) + test_thumbnail = self.test_urls.base_url + mresp.NEW_THUMBNAIL + '.jpg' + # Snap picture for each camera and check new thumb + for camera_name in test_cameras: + camera = self.blink.cameras[camera_name] + camera.snap_picture() + camera.image_refresh() + self.assertEqual(camera.thumbnail, test_thumbnail) + + # Manually set thumbnail, and then globally refresh and check + for camera_name in test_cameras: + camera = self.blink.cameras[camera_name] + camera.thumbnail = 'Testing' + self.assertEqual(camera.thumbnail, 'Testing') + + self.blink.refresh() + for camera_name in test_cameras: + camera = self.blink.cameras[camera_name] + self.assertEqual(camera.thumbnail, test_thumbnail) + + def test_camera_update(self): + """Checks that the update function is doing the right thing.""" + self.test_urls = blinkpy.BlinkURLHandler('test') + test_config = mresp.FIRST_CAMERA + test_camera = blinkpy.blinkpy.BlinkCamera(test_config, self.test_urls) + test_update = mresp.SECOND_CAMERA + test_camera.update(test_update) + test_image_url = self.test_urls.base_url + test_update['thumbnail'] + test_thumbnail = test_image_url + '.jpg' + test_clip = test_image_url + '.mp4' + self.assertEqual(test_camera.name, test_update['name']) + self.assertEqual(test_camera.armed, test_update['armed']) + self.assertEqual(test_camera.thumbnail, test_thumbnail) + self.assertEqual(test_camera.clip, test_clip) + self.assertEqual(test_camera.temperature, test_update['temp']) + self.assertEqual(test_camera.battery, test_update['battery']) + self.assertEqual(test_camera.notifications, + test_update['notifications']) + + @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_thumbs(self, mock_get, mock_post): + """Checks to see if we can retrieve camera thumbs.""" + self.test_urls = blinkpy.BlinkURLHandler(self.region_id) + test_cameras = mresp.get_test_cameras(self.test_urls.base_url) + self.blink.setup_system() + for name in self.blink.cameras: + thumb = self.blink.camera_thumbs[name] + self.assertEqual(test_cameras[name]['thumbnail'], thumb) + + @mock.patch('blinkpy.blinkpy.copyfileobj', + side_effect=mresp.mocked_copyfileobj) + @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_image_to_file(self, mock_get, mock_post, mock_copyfileobj): + """Checks that we can write an image to file.""" + self.blink.setup_system() + cameras = self.blink.cameras + filename = '/tmp/test.jpg' + test_files = list() + for camera_name in cameras: + camera = cameras[camera_name] + test_files.append(mresp.MOCK_BYTES) + mock_fh = mock.mock_open() + with mock.patch('builtins.open', mock_fh, create=True): + camera.image_to_file(camera_name + filename) + mock_fh.assert_called_once_with(camera_name + filename, 'wb') + self.assertEqual(test_files, mresp.FAKE_FILES) diff --git a/tests/test_blink_hass.py b/tests/test_blink_hass.py deleted file mode 100644 index 79ae84e..0000000 --- a/tests/test_blink_hass.py +++ /dev/null @@ -1 +0,0 @@ -# Test functionality used for blink component in home assistant diff --git a/tests/test_blink_system.py b/tests/test_blink_system.py index 360abd1..8e9ab31 100644 --- a/tests/test_blink_system.py +++ b/tests/test_blink_system.py @@ -41,6 +41,10 @@ class TestBlinkSetup(unittest.TestCase): self.blink_no_cred.get_auth_token() with self.assertRaises(blinkpy.BlinkAuthenticationException): self.blink_no_cred.setup_system() + # pylint: disable=protected-access + self.blink_no_cred._username = USERNAME + with self.assertRaises(blinkpy.BlinkAuthenticationException): + self.blink_no_cred.get_auth_token() def test_no_auth_header(self): """Check that we throw an excpetion when no auth header given.""" @@ -49,6 +53,8 @@ class TestBlinkSetup(unittest.TestCase): self.blink.urls = blinkpy.BlinkURLHandler(region_id) with self.assertRaises(blinkpy.BlinkException): self.blink.get_ids() + with self.assertRaises(blinkpy.BlinkException): + self.blink.get_summary() @mock.patch('blinkpy.blinkpy.getpass.getpass') def test_manual_login(self, getpwd): @@ -61,6 +67,20 @@ class TestBlinkSetup(unittest.TestCase): # pylint: disable=protected-access self.assertEqual(self.blink_no_cred._password, PASSWORD) + @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_bad_request(self, mock_get, mock_post): + """Check that we raise an Exception with a bad request.""" + with self.assertRaises(blinkpy.BlinkException): + # pylint: disable=protected-access + blinkpy.blinkpy._request(None, reqtype='bad') + + with self.assertRaises(blinkpy.BlinkAuthenticationException): + # pylint: disable=protected-access + blinkpy.blinkpy._request(None, reqtype='post') + @mock.patch('blinkpy.blinkpy.requests.post', side_effect=mresp.mocked_requests_post) @mock.patch('blinkpy.blinkpy.requests.get', @@ -76,13 +96,8 @@ class TestBlinkSetup(unittest.TestCase): network_id = mresp.NETWORKS_RESPONSE['networks'][0]['id'] account_id = mresp.NETWORKS_RESPONSE['networks'][0]['account_id'] test_urls = blinkpy.BlinkURLHandler(region_id) - test_cameras = list() - test_camera_id = dict() - for element in mresp.RESPONSE['devices']: - if ('device_type' in element and - element['device_type'] == 'camera'): - test_cameras.append(element['name']) - test_camera_id[str(element['device_id'])] = element['name'] + test_cameras = mresp.get_test_cameras(test_urls.base_url) + test_camera_id = mresp.get_test_id_table() # Check that all links have been set properly self.assertEqual(self.blink.region_id, region_id) diff --git a/tox.ini b/tox.ini index 5b91d33..9a9d595 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ setenv = LANG=en_US.UTF-8 PYTHONPATH = {toxinidir} commands = - py.test -v --timeout=30 --duration=10 --cov=blinkpy --cov-report term {posargs} + py.test -v --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt @@ -19,6 +19,6 @@ deps = basepython = python3 ignore_errors = True commands = - pylint --rcfile={toxinidir}/pylintrc --load-plugins=pylint.extensions.mccabe blinkpy.py tests + pylint --rcfile={toxinidir}/pylintrc blinkpy.py tests flake8 blinkpy.py tests pydocstyle blinkpy.py tests From ab7305b7b26890c0a30b9be6320f2e60a8650168 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 12 Mar 2017 18:26:23 -0400 Subject: [PATCH 18/21] Fixed typo --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af0aa2e..db29c3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,8 +63,8 @@ Please do not locally disable any linter warnings within the ``blinkpy.py`` modu If your code is taking a while to develop, you may be behind the ``dev`` branch, in which case you need to catch up before creating your pull-request. To do this you can run ``git rebase`` as follows (running this on your local branch): ```shell -$ git fetch upstream/dev -$ git rebase upstrea/dev +$ git fetch upstream dev +$ git rebase upstream/dev ``` If rebase detects conflicts, repeat the following process until all changes have been resolved: From e31a5979bbf9e5f69eee665f779da41a0f80938e Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 12 Mar 2017 18:45:52 -0400 Subject: [PATCH 19/21] Fixed --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db29c3b..81eab1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,10 +16,10 @@ $ git remote add upstream https://github.com/fronzbot/blinkpy.git ## Create a Local Branch -First, you need to make sure you're on the 'dev' branch: -``git checkout dev`` -Next, you will want to create a new branch to hold your changes: +First, you will want to create a new branch to hold your changes: ``git checkout -b `` +Next, you need to make sure you pull from the 'dev' branch: +``git pull origin dev`` ## Make changes From c529f474fe1c5fe9d2fc038765f6f4f39cda5b82 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 12 Mar 2017 19:45:33 -0400 Subject: [PATCH 20/21] Added support for rest.piri subdomain --- blinkpy.py | 21 +++++++++++++++++---- helpers/constants.py | 1 + tests/mock_responses.py | 9 ++++++++- tests/test_blink_system.py | 19 +++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/blinkpy.py b/blinkpy.py index 296f4d3..6391c56 100644 --- a/blinkpy.py +++ b/blinkpy.py @@ -18,15 +18,19 @@ from shutil import copyfileobj import requests import helpers.errors as ERROR from helpers.constants import (BLINK_URL, LOGIN_URL, + LOGIN_BACKUP_URL, DEFAULT_URL, ONLINE) def _request(url, data=None, headers=None, reqtype='get', stream=False, json_resp=True): """Wrapper function for request.""" - if reqtype is 'post': + if reqtype is 'post' and json_resp: response = requests.post(url, headers=headers, data=data).json() + elif reqtype is 'post' and not json_resp: + response = requests.post(url, headers=headers, + data=data) elif reqtype is 'get' and json_resp: response = requests.get(url, headers=headers, stream=stream).json() @@ -441,10 +445,19 @@ class Blink(object): "client_specifier": "iPhone 9.2 | 2.2 | 222" }) response = _request(LOGIN_URL, headers=headers, - data=data, reqtype='post') - self._token = response['authtoken']['authtoken'] - (self._region_id, self._region), = response['region'].items() + data=data, json_resp=False, reqtype='post') + if response.status_code is 200: + response = response.json() + (self._region_id, self._region), = response['region'].items() + else: + response = _request(LOGIN_BACKUP_URL, headers=headers, + data=data, reqtype='post') + self._region_id = 'rest.piri' + self._region = "UNKNOWN" + self._host = self._region_id + '.' + BLINK_URL + self._token = response['authtoken']['authtoken'] + self._auth_header = {'Host': self._host, 'TOKEN_AUTH': self._token} diff --git a/helpers/constants.py b/helpers/constants.py index 08c7861..bf56aff 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -45,6 +45,7 @@ URLS ''' BLINK_URL = 'immedia-semi.com' LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' +LOGIN_BACKUP_URL = 'https://rest.piri/' + BLINK_URL + '/login' BASE_URL = 'https://prod.' + BLINK_URL DEFAULT_URL = 'prod.' + BLINK_URL diff --git a/tests/mock_responses.py b/tests/mock_responses.py index c57e72e..3161b43 100644 --- a/tests/mock_responses.py +++ b/tests/mock_responses.py @@ -156,6 +156,8 @@ def mocked_requests_post(*args, **kwargs): if args[0] == const.LOGIN_URL: # Request to login return MockPostResponse(LOGIN_RESPONSE, 200) + elif args[0] == const.LOGIN_BACKUP_URL: + return MockPostResponse(LOGIN_RESPONSE, 200) elif url_tail == 'arm' or url_tail == 'disarm': # Request to arm/disarm system NETWORKS_RESPONSE['networks'][0]['armed'] = url_tail == 'arm' @@ -210,11 +212,16 @@ def mocked_requests_get(*args, **kwargs): # pylint: disable=unused-variable (region_id, region), = LOGIN_RESPONSE['region'].items() set_region_id = args[0].split('/')[2].split('.')[0] + if set_region_id == 'rest': + set_region_id = (set_region_id + '.' + + args[0].split('/')[2].split('.')[1]) + region_id = 'rest.piri' neturl = 'https://' + set_region_id + '.' + const.BLINK_URL + '/networks' if args[0] == neturl: return MockGetResponse(NETWORKS_RESPONSE, 200) elif set_region_id != region_id: - raise ConnectionError('Received url ' + args[0]) + raise ConnectionError('Received region id ' + region_id + + ' Expected ' + set_region_id) elif args[0] in IMAGE_TO_WRITE_URL: return MockGetResponse({}, 200, raw_data=MOCK_BYTES) else: diff --git a/tests/test_blink_system.py b/tests/test_blink_system.py index 8e9ab31..37b2be3 100644 --- a/tests/test_blink_system.py +++ b/tests/test_blink_system.py @@ -142,3 +142,22 @@ class TestBlinkSetup(unittest.TestCase): self.blink.setup_system() expected_status = const.ONLINE[mresp.RESPONSE['syncmodule']['status']] self.assertIs(self.blink.online, expected_status) + + @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_setup_backup_subdomain(self, mock_get, mock_post): + """Check that we can use the 'rest.piri' subdomain.""" + test_urls = blinkpy.BlinkURLHandler('rest.piri') + with mock.patch('helpers.constants.LOGIN_URL', + return_value=const.LOGIN_URL + 'NO'): + self.blink.setup_system() + self.assertEqual(self.blink.region_id, 'rest.piri') + # pylint: disable=protected-access + self.assertEqual(self.blink._host, 'rest.piri.' + const.BLINK_URL) + self.assertEqual(self.blink.urls.base_url, test_urls.base_url) + self.assertEqual(self.blink.urls.home_url, test_urls.home_url) + self.assertEqual(self.blink.urls.event_url, test_urls.event_url) + self.assertEqual(self.blink.urls.network_url, test_urls.network_url) + self.assertEqual(self.blink.urls.networks_url, test_urls.networks_url) From 9099909983444e78060575b303bd10da5cc1e3b2 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sun, 12 Mar 2017 19:51:28 -0400 Subject: [PATCH 21/21] Bumping release to 0.5.0 --- helpers/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/constants.py b/helpers/constants.py index bf56aff..6ef8d94 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -5,8 +5,8 @@ Generates constants for use in blinkpy import os MAJOR_VERSION = 0 -MINOR_VERSION = 4 -PATCH_VERSION = 4 +MINOR_VERSION = 5 +PATCH_VERSION = 0 __version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)