From d54a6cbb71038c40a6e420f7b538f2dccd2feaa7 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 11 Mar 2017 21:53:58 -0500 Subject: [PATCH] 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