Compare commits

...
47 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub 86aae07367 Merge pull request #242 from fronzbot/add-2FA
Add 2FA Support
2020-05-05 22:51:45 -04:00
Kevin Fronczak 1e86e7eb79 Add unit tests for new methods 2020-05-05 22:45:48 -04:00
Kevin Fronczak 0c7737cd7f Update README 2020-05-05 15:38:45 -04:00
Kevin Fronczak 0c79620baa Updated login method to allow for email verification and 2FA in future 2020-05-05 15:28:38 -04:00
Kevin FronczakandGitHub 9bc20cc82e Merge pull request #241 from fronzbot/refactor-login-logic
Refactor login logic in preperation for 2FA
2020-05-05 13:56:39 -04:00
Kevin Fronczak c51551afc5 Add skeleton for 2FA 2020-05-05 13:56:04 -04:00
Kevin Fronczak f80a93f8e1 Refactor login logic in preperation for 2FA 2020-05-05 12:46:48 -04:00
Kevin FronczakandGitHub 853c596167 Merge pull request #240 from fronzbot/dependabot/pip/pylint-2.5.1
Bump pylint from 2.5.0 to 2.5.1
2020-05-05 08:10:58 -04:00
dependabot-preview[bot]andGitHub 819e383b05 Bump pylint from 2.5.0 to 2.5.1
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.5.0...pylint-2.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-05 08:29:12 +00:00
Kevin FronczakandGitHub 724734847a Merge pull request #239 from fronzbot/codecov-threshold
Fix codecov threshold
2020-05-04 14:44:53 -04:00
Kevin Fronczak cdd01c5186 Fix codecov threshold 2020-05-04 14:38:47 -04:00
Kevin FronczakandGitHub 5a5c34b6a1 Merge pull request #238 from fronzbot/add-black
Add black
2020-05-04 14:37:50 -04:00
Kevin Fronczak 28271efbe6 Remove hound. 2020-05-04 14:30:48 -04:00
Kevin Fronczak 1dbb8d4b21 Add black badge 2020-05-04 14:28:14 -04:00
Kevin Fronczak e1bbbb5c7e Add black formatting style 2020-05-04 14:23:02 -04:00
Kevin FronczakandGitHub addc32e3e6 Merge pull request #237 from fronzbot/deprecate-py35
Remove Python 3.5.3 support
2020-05-04 13:41:11 -04:00
Kevin Fronczak b201c95e8d Remove Python 3.5.3 support 2020-05-04 13:37:32 -04:00
Kevin FronczakandGitHub 19b2541c81 Merge pull request #236 from fronzbot/remove-flake8-filewq
Remove .flake8 file
2020-05-04 13:31:57 -04:00
Kevin Fronczak 6a2650dda0 Remove .flake8 file 2020-05-04 13:28:37 -04:00
Kevin FronczakandGitHub c122c4ecfb Merge pull request #235 from fronzbot/dependabot/pip/pylint-2.5.0
Bump pylint from 2.4.4 to 2.5.0
2020-05-04 13:21:22 -04:00
Kevin Fronczak 43ed6f1600 Fix lint errors 2020-05-04 13:14:52 -04:00
Kevin FronczakandGitHub fae1e1a554 Merge pull request #230 from fronzbot/dependabot/pip/python-dateutil-approx-eq-2.8.1
Update python-dateutil requirement from ~=2.8.0 to ~=2.8.1
2020-05-04 12:39:11 -04:00
Kevin Fronczak 4587268ecb Bump dateutil to 2.8.1 2020-05-04 12:32:40 -04:00
dependabot-preview[bot]andGitHub e0ff5c484a Update python-dateutil requirement from ~=2.8.0 to ~=2.8.1
Updates the requirements on [python-dateutil](https://github.com/dateutil/dateutil) to permit the latest version.
- [Release notes](https://github.com/dateutil/dateutil/releases)
- [Changelog](https://github.com/dateutil/dateutil/blob/master/NEWS)
- [Commits](https://github.com/dateutil/dateutil/compare/2.8.0...2.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 16:28:09 +00:00
Kevin FronczakandGitHub bc4956c64b Merge pull request #234 from fronzbot/dependabot/pip/python-slugify-approx-eq-4.0.0
Update python-slugify requirement from ~=3.0.2 to ~=4.0.0
2020-05-04 12:26:45 -04:00
Kevin Fronczak cc5fbcd3a7 Bump slugify to 4.0.0 2020-05-04 12:19:30 -04:00
dependabot-preview[bot]andGitHub db23acd46c Update python-slugify requirement from ~=3.0.2 to ~=4.0.0
Updates the requirements on [python-slugify](https://github.com/un33k/python-slugify) to permit the latest version.
- [Release notes](https://github.com/un33k/python-slugify/releases)
- [Changelog](https://github.com/un33k/python-slugify/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un33k/python-slugify/compare/3.0.2...4.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 16:17:35 +00:00
Kevin FronczakandGitHub 973374f6a1 Merge pull request #227 from fronzbot/dependabot/pip/testtools-2.4.0
Bump testtools from 2.3.0 to 2.4.0
2020-05-04 12:16:25 -04:00
dependabot-preview[bot]andGitHub 13e2262c2f Bump pylint from 2.4.4 to 2.5.0
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.4.4 to 2.5.0.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.4.4...pylint-2.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 16:13:02 +00:00
Kevin FronczakandGitHub 69775188a6 Merge pull request #228 from fronzbot/dependabot/pip/coverage-5.1
Bump coverage from 4.5.4 to 5.1
2020-05-04 12:12:10 -04:00
Kevin FronczakandGitHub 0554e4e159 Merge pull request #229 from fronzbot/dependabot/pip/pytest-5.4.1
Bump pytest from 5.3.5 to 5.4.1
2020-05-04 12:11:44 -04:00
dependabot-preview[bot]andGitHub f576c97c1d Bump coverage from 4.5.4 to 5.1
Bumps [coverage](https://github.com/nedbat/coveragepy) from 4.5.4 to 5.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-4.5.4...coverage-5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:59:53 +00:00
dependabot-preview[bot]andGitHub e6c842072c Bump pytest from 5.3.5 to 5.4.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.5 to 5.4.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.3.5...5.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:59:16 +00:00
dependabot-preview[bot]andGitHub bf87f3676d Bump testtools from 2.3.0 to 2.4.0
Bumps [testtools](https://github.com/testing-cabal/testtools) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/testing-cabal/testtools/releases)
- [Changelog](https://github.com/testing-cabal/testtools/blob/master/NEWS)
- [Commits](https://github.com/testing-cabal/testtools/compare/2.3.0...2.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:52:19 +00:00
Kevin FronczakandGitHub a87581638c Merge pull request #231 from fronzbot/dependabot/pip/requests-2.23.0
Bump requests from 2.22.0 to 2.23.0
2020-05-04 11:51:09 -04:00
Kevin Fronczak 42ed18fcd5 Bump requests to 2.23.0 2020-05-04 11:45:54 -04:00
Kevin FronczakandGitHub a66c926475 Merge pull request #232 from fronzbot/stale-bot
Create stale.yml
2020-05-04 11:40:53 -04:00
Kevin FronczakandGitHub 6a481aaa74 Update stale.yml 2020-05-04 11:33:22 -04:00
dependabot-preview[bot]andGitHub 72d97f4944 Bump requests from 2.22.0 to 2.23.0
Bumps [requests](https://github.com/psf/requests) from 2.22.0 to 2.23.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.22.0...v2.23.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:30:44 +00:00
Kevin FronczakandGitHub 1694187b39 Merge pull request #233 from fronzbot/pip-downgrade-workflows
Fix setup.py use of internal pip structure
2020-05-04 11:29:30 -04:00
Kevin Fronczak a531a8e8a3 Update setup.py to not rely on internal pip functions 2020-05-04 11:05:46 -04:00
Kevin FronczakandGitHub 44d5bb65ae Exempt stuff added to projects and milestones 2020-05-04 10:21:06 -04:00
Kevin FronczakandGitHub c64f5cfb23 Downgrade pip again 2020-05-04 10:17:13 -04:00
Kevin FronczakandGitHub 19a29bc3a1 Create stale.yml 2020-05-04 10:12:41 -04:00
Kevin FronczakandGitHub 6159987928 Downgrade pip 2020-05-04 09:57:23 -04:00
Kevin FronczakandGitHub 8a8c4ea20b Change to SemVer for dev versions (now rc) 2020-05-04 09:31:41 -04:00
Kevin FronczakandGitHub e43d2f9a88 Dev version bump 2020-04-22 10:37:43 -04:00
30 changed files with 1145 additions and 893 deletions
-2
View File
@@ -1,2 +0,0 @@
[flake8]
ignore = BLK100
+30
View File
@@ -0,0 +1,30 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- Priority
- bug
- Help Wanted
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs within seven days. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue is now being closed due to inactivity.
+2 -2
View File
@@ -10,11 +10,11 @@ jobs:
build:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
max-parallel: 3
matrix:
platform:
- ubuntu-latest
python-version: [3.5, 3.6, 3.7, 3.8]
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -3,7 +3,7 @@
.tox/*
__pycache__/*
htmlcov/*
.coverage
.coverage.*
coverage.xml
*.pyc
*.egg*/*
-11
View File
@@ -1,11 +0,0 @@
python:
enabled: true
ignore_file: setup.py
ignored_directories:
- docs
flake8:
enabled: true
config_file: .flake8
fail_on_violations: true
+10 -3
View File
@@ -1,5 +1,5 @@
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version| |Codestyle|
=============================================================================================
A Python library for the Blink Camera system
Like the library? Consider buying me a cup of coffee!
@@ -53,7 +53,12 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD', refresh_rate=30)
blink.start()
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
At startup, you may ne prompted for a verification key. Just enter this in the command-line prompt. If you just receive a verification email asking to validate access for your device, enter nothing at this prompt. To avoid any command-line interaction, call the ``Blink`` class with the ``no_prompt=True`` flag. Instead, once you receive the verification email, call the following functions:
.. code:: python
blink.login_handler.send_auth_key(blink, VERIFICATION_KEY)
blink.setup_post_verify()
In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
@@ -130,3 +135,5 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
.. |Codestyle| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
+5 -5
View File
@@ -4,9 +4,9 @@ from datetime import datetime, timedelta
from blinkpy import blinkpy
USERNAME = environ.get('USERNAME')
PASSWORD = environ.get('PASSWORD')
TIMEDELTA = timedelta(environ.get('TIMEDELTA', 1))
USERNAME = environ.get("USERNAME")
PASSWORD = environ.get("PASSWORD")
TIMEDELTA = timedelta(environ.get("TIMEDELTA", 1))
def get_date():
@@ -14,7 +14,7 @@ def get_date():
return (datetime.now() - TIMEDELTA).isoformat()
def download_videos(blink, save_dir='/media'):
def download_videos(blink, save_dir="/media"):
"""Make request to download videos."""
blink.download_videos(save_dir, since=get_date())
@@ -26,6 +26,6 @@ def start():
return blink
if __name__ == '__main__':
if __name__ == "__main__":
BLINK = start()
download_videos(BLINK)
+79 -40
View File
@@ -11,7 +11,9 @@ _LOGGER = logging.getLogger(__name__)
MIN_THROTTLE_TIME = 2
def request_login(blink, url, username, password, is_retry=False):
def request_login(
blink, url, username, password, notification_key, uid, is_retry=False
):
"""
Login request.
@@ -19,19 +21,51 @@ def request_login(blink, url, username, password, is_retry=False):
:param url: Login url.
:param username: Blink username.
:param password: Blink password.
:param notification_key: Randomly genereated key.
:param uid: Randomoly genreated unique id key.
:param is_retry: Is this part of a re-authorization attempt?
"""
headers = {
'Host': DEFAULT_URL,
'Content-Type': 'application/json'
headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
data = dumps(
{
"email": username,
"password": password,
"notification_key": notification_key,
"unique_id": uid,
"app_version": "6.0.7 (520300) #afb0be72a",
"client_name": "Computer",
"client_type": "android",
"device_identifier": "Blinkpy",
"device_name": "Blinkpy",
"os_version": "5.1.1",
"reauth": "true",
}
data = dumps({
'email': username,
'password': password,
'client_specifier': 'iPhone 9.2 | 2.2 | 222'
})
return http_req(blink, url=url, headers=headers, data=data,
json_resp=False, reqtype='post', is_retry=is_retry)
)
return http_req(
blink,
url=url,
headers=headers,
data=data,
json_resp=False,
reqtype="post",
is_retry=is_retry,
)
def request_verify(blink, verify_key):
"""Send verification key to blink servers."""
url = "{}/api/v4/account/{}/client/{}/pin/verify".format(
blink.urls.base_url, blink.account_id, blink.client_id
)
data = dumps({"pin": verify_key})
return http_req(
blink,
url=url,
headers=blink.auth_header,
data=data,
json_resp=False,
reqtype="post",
)
def request_networks(blink):
@@ -94,17 +128,16 @@ def request_command_status(blink, network, command_id):
:param network: Sync module network id.
:param command_id: Command id to check.
"""
url = "{}/network/{}/command/{}".format(blink.urls.base_url,
network,
command_id)
url = "{}/network/{}/command/{}".format(blink.urls.base_url, network, command_id)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_homescreen(blink):
"""Request homescreen info."""
url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url,
blink.account_id)
url = "{}/api/v3/accounts/{}/homescreen".format(
blink.urls.base_url, blink.account_id
)
return http_get(blink, url)
@@ -129,9 +162,9 @@ def request_new_image(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new image from.
"""
url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/thumbnail".format(
blink.urls.base_url, network, camera_id
)
return http_post(blink, url)
@@ -144,9 +177,7 @@ def request_new_video(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new video from.
"""
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url, network, camera_id)
return http_post(blink, url)
@@ -167,7 +198,8 @@ def request_videos(blink, time=None, page=0):
"""
timestamp = get_time(time)
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
blink.urls.base_url, blink.account_id, timestamp, page)
blink.urls.base_url, blink.account_id, timestamp, page
)
return http_get(blink, url)
@@ -190,9 +222,9 @@ def request_camera_info(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request info from.
"""
url = "{}/network/{}/camera/{}/config".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/config".format(
blink.urls.base_url, network, camera_id
)
return http_get(blink, url)
@@ -204,9 +236,9 @@ def request_camera_sensors(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request sesnor info from.
"""
url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/signals".format(
blink.urls.base_url, network, camera_id
)
return http_get(blink, url)
@@ -219,9 +251,9 @@ def request_motion_detection_enable(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to enable.
"""
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/enable".format(
blink.urls.base_url, network, camera_id
)
return http_post(blink, url)
@@ -233,9 +265,9 @@ def request_motion_detection_disable(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to disable.
"""
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/disable".format(
blink.urls.base_url, network, camera_id
)
return http_post(blink, url)
@@ -251,9 +283,15 @@ def http_get(blink, url, stream=False, json=True, is_retry=False):
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making GET request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header,
reqtype='get', stream=stream, json_resp=json,
is_retry=is_retry)
return http_req(
blink,
url=url,
headers=blink.auth_header,
reqtype="get",
stream=stream,
json_resp=json,
is_retry=is_retry,
)
def http_post(blink, url, is_retry=False):
@@ -266,5 +304,6 @@ def http_post(blink, url, is_retry=False):
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making POST request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header,
reqtype='post', is_retry=is_retry)
return http_req(
blink, url=url, headers=blink.auth_header, reqtype="post", is_retry=is_retry
)
+103 -130
View File
@@ -15,9 +15,7 @@ blinkpy is in no way affiliated with Blink, nor Immedia Inc.
import os.path
import time
import getpass
import logging
import json
from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict
@@ -26,27 +24,41 @@ from slugify import slugify
from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers import errors as ERROR
from blinkpy.helpers.util import (
create_session, merge_dicts, get_time, BlinkURLHandler,
BlinkAuthenticationException, Throttle)
create_session,
merge_dicts,
get_time,
BlinkURLHandler,
Throttle,
)
from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
BLINK_URL,
DEFAULT_MOTION_INTERVAL,
DEFAULT_REFRESH,
MIN_THROTTLE_TIME,
LOGIN_URLS,
)
from blinkpy.helpers.constants import __version__
from blinkpy.login_handler import LoginHandler
_LOGGER = logging.getLogger(__name__)
class Blink():
class Blink:
"""Class to initialize communication."""
def __init__(self, username=None, password=None,
def __init__(
self,
username=None,
password=None,
cred_file=None,
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False):
legacy_subdomain=False,
no_prompt=False,
persist_key=None,
):
"""
Initialize Blink system.
@@ -65,14 +77,21 @@ class Blink():
:param legacy_subdomain: Set to TRUE to use old 'rest.region'
endpoints (only use if you are having
api issues).
:param no_prompt: Set to TRUE if using an implementation that needs to
suppress command-line output.
:param persist_key: Location of persistant identifier.
"""
self._username = username
self._password = password
self._cred_file = cred_file
self.login_handler = LoginHandler(
username=username,
password=password,
cred_file=cred_file,
persist_key=persist_key,
)
self._token = None
self._auth_header = None
self._host = None
self.account_id = None
self.client_id = None
self.network_ids = []
self.urls = None
self.sync = CaseInsensitiveDict({})
@@ -84,11 +103,15 @@ class Blink():
self.networks = []
self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL
self.login_url = LOGIN_URLS[0]
self.login_urls = []
self.motion_interval = motion_interval
self.version = __version__
self.legacy = legacy_subdomain
self.no_prompt = no_prompt
self.available = False
self.key_required = False
self.login_response = {}
@property
def auth_header(self):
@@ -102,126 +125,78 @@ class Blink():
Method logs in and sets auth token, urls, and ids for future requests.
Essentially this is just a wrapper function for ease of use.
"""
if self._username is None or self._password is None:
if not self.login():
return
elif not self.get_auth_token():
return
if not self.available:
self.get_auth_token()
if self.key_required and not self.no_prompt:
email = self.login_handler.data["username"]
key = input("Enter code sent to {}: ".format(email))
result = self.login_handler.send_auth_key(self, key)
self.key_required = not result
self.setup_post_verify()
elif not self.key_required:
self.setup_post_verify()
def setup_post_verify(self):
"""Initialize blink system after verification."""
camera_list = self.get_cameras()
networks = self.get_ids()
for network_name, network_id in networks.items():
if network_id not in camera_list.keys():
camera_list[network_id] = {}
_LOGGER.warning("No cameras found for %s", network_name)
sync_module = BlinkSyncModule(self,
network_name,
network_id,
camera_list[network_id])
sync_module = BlinkSyncModule(
self, network_name, network_id, camera_list[network_id]
)
sync_module.start()
self.sync[network_name] = sync_module
self.cameras = self.merge_cameras()
def login(self):
"""Prompt user for username and password."""
if self._cred_file is not None and os.path.isfile(self._cred_file):
try:
with open(self._cred_file, 'r') as json_file:
creds = json.load(json_file)
self._username = creds['username']
self._password = creds['password']
except ValueError:
_LOGGER.error("Improperly formated json file %s.",
self._cred_file,
exc_info=True)
return False
except KeyError:
_LOGGER.error("JSON file information incomplete %s.",
exc_info=True)
return False
else:
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token():
_LOGGER.debug("Login successful!")
return True
_LOGGER.warning("Unable to login with %s.", self._username)
return False
"""Perform server login. DEPRECATED."""
_LOGGER.warning(
"Method is deprecated and will be removed in a future version. Please use the LoginHandler.login() method instead."
)
return self.login_handler.login(self)
def get_auth_token(self, is_retry=False):
"""Retrieve 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)
self.login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
response = self.login_request(is_retry=is_retry)
if not response:
self.login_response = self.login_handler.login(self)
if not self.login_response:
return False
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response['authtoken']['authtoken']
self.networks = response['networks']
self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token}
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
self.setup_params(self.login_response)
if self.login_handler.check_key_required(self):
self.key_required = True
return self._auth_header
def login_request(self, is_retry=False):
"""Make a login request."""
def setup_params(self, response):
"""Retrieve blink parameters from login response."""
self.login_url = self.login_handler.login_url
((self.region_id, self.region),) = response["region"].items()
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response["authtoken"]["authtoken"]
self._auth_header = {"Host": self._host, "TOKEN_AUTH": self._token}
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
self.networks = self.get_networks()
self.client_id = response["client"]["id"]
self.account_id = response["account"]["id"]
def get_networks(self):
"""Get network information."""
response = api.request_networks(self)
try:
login_url = self.login_urls.pop(0)
except IndexError:
_LOGGER.error("Could not login to blink servers.")
return False
_LOGGER.info("Attempting login with %s", login_url)
response = api.request_login(self,
login_url,
self._username,
self._password,
is_retry=is_retry)
try:
if response.status_code != 200:
response = self.login_request(is_retry=True)
response = response.json()
(self.region_id, self.region), = response['region'].items()
except AttributeError:
_LOGGER.error("Login API endpoint failed with response %s",
response)
return False
return response["summary"]
except KeyError:
_LOGGER.warning("Could not extract region info.")
self.region_id = 'piri'
self.region = 'UNKNOWN'
self._login_url = login_url
return response
return None
def get_ids(self):
"""Set the network ID and Account ID."""
response = api.request_networks(self)
all_networks = []
network_dict = {}
for network, status in self.networks.items():
if status['onboarded']:
all_networks.append('{}'.format(network))
network_dict[status['name']] = network
# For the first onboarded network we find, grab the account id
for resp in response['networks']:
if str(resp['id']) in all_networks:
self.account_id = resp['account_id']
break
if status["onboarded"]:
all_networks.append("{}".format(network))
network_dict[status["name"]] = network
self.network_ids = all_networks
return network_dict
@@ -231,11 +206,11 @@ class Blink():
response = api.request_homescreen(self)
try:
all_cameras = {}
for camera in response['cameras']:
camera_network = str(camera['network_id'])
camera_name = camera['name']
camera_id = camera['id']
camera_info = {'name': camera_name, 'id': camera_id}
for camera in response["cameras"]:
camera_network = str(camera["network_id"])
camera_name = camera["name"]
camera_id = camera["id"]
camera_info = {"name": camera_name, "id": camera_id}
if camera_network not in all_cameras:
all_cameras[camera_network] = []
@@ -279,8 +254,7 @@ class Blink():
combined = merge_dicts(combined, self.sync[sync].cameras)
return combined
def download_videos(self, path, since=None,
camera='all', stop=10, debug=False):
def download_videos(self, path, since=None, camera="all", stop=10, debug=False):
"""
Download all videos from server since specified time.
@@ -310,7 +284,7 @@ class Blink():
response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page)
try:
result = response['media']
result = response["media"]
if not result:
raise IndexError
except (KeyError, IndexError):
@@ -323,22 +297,20 @@ class Blink():
"""Parse downloaded videos."""
for item in result:
try:
created_at = item['created_at']
camera_name = item['device_name']
is_deleted = item['deleted']
address = item['media']
created_at = item["created_at"]
camera_name = item["device_name"]
is_deleted = item["deleted"]
address = item["media"]
except KeyError:
_LOGGER.info("Missing clip information, skipping...")
continue
if camera_name not in camera and 'all' not in camera:
if camera_name not in camera and "all" not in camera:
_LOGGER.debug("Skipping videos for %s.", camera_name)
continue
if is_deleted:
_LOGGER.debug("%s: %s is marked as deleted.",
camera_name,
address)
_LOGGER.debug("%s: %s is marked as deleted.", camera_name, address)
continue
clip_address = "{}{}".format(self.urls.base_url, address)
@@ -351,13 +323,14 @@ class Blink():
_LOGGER.info("%s already exists, skipping...", filename)
continue
response = api.http_get(self, url=clip_address,
stream=True, json=False)
with open(filename, 'wb') as vidfile:
response = api.http_get(self, url=clip_address, stream=True, json=False)
with open(filename, "wb") as vidfile:
copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename)
else:
print(("Camera: {}, Timestamp: {}, "
"Address: {}, Filename: {}").format(
camera_name, created_at, address, filename))
print(
("Camera: {}, Timestamp: {}, " "Address: {}, Filename: {}").format(
camera_name, created_at, address, filename
)
)
+58 -64
View File
@@ -7,7 +7,7 @@ from blinkpy import api
_LOGGER = logging.getLogger(__name__)
class BlinkCamera():
class BlinkCamera:
"""Class to initialize individual camera."""
def __init__(self, sync):
@@ -34,22 +34,22 @@ class BlinkCamera():
def attributes(self):
"""Return dictionary of all camera attributes."""
attributes = {
'name': self.name,
'camera_id': self.camera_id,
'serial': self.serial,
'temperature': self.temperature,
'temperature_c': self.temperature_c,
'temperature_calibrated': self.temperature_calibrated,
'battery': self.battery,
'battery_voltage': self.battery_voltage,
'thumbnail': self.thumbnail,
'video': self.clip,
'motion_enabled': self.motion_enabled,
'motion_detected': self.motion_detected,
'wifi_strength': self.wifi_strength,
'network_id': self.sync.network_id,
'sync_module': self.sync.name,
'last_record': self.last_record
"name": self.name,
"camera_id": self.camera_id,
"serial": self.serial,
"temperature": self.temperature,
"temperature_c": self.temperature_c,
"temperature_calibrated": self.temperature_calibrated,
"battery": self.battery,
"battery_voltage": self.battery_voltage,
"thumbnail": self.thumbnail,
"video": self.clip,
"motion_enabled": self.motion_enabled,
"motion_detected": self.motion_detected,
"wifi_strength": self.wifi_strength,
"network_id": self.sync.network_id,
"sync_module": self.sync.name,
"last_record": self.last_record,
}
return attributes
@@ -79,39 +79,37 @@ class BlinkCamera():
def snap_picture(self):
"""Take a picture with camera to create a new thumbnail."""
return api.request_new_image(self.sync.blink,
self.network_id,
self.camera_id)
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
def set_motion_detect(self, enable):
"""Set motion detection."""
if enable:
return api.request_motion_detection_enable(self.sync.blink,
self.network_id,
self.camera_id)
return api.request_motion_detection_disable(self.sync.blink,
self.network_id,
self.camera_id)
return api.request_motion_detection_enable(
self.sync.blink, self.network_id, self.camera_id
)
return api.request_motion_detection_disable(
self.sync.blink, self.network_id, self.camera_id
)
def update(self, config, force_cache=False, **kwargs):
"""Update camera info."""
# force = kwargs.pop('force', False)
self.name = config['name']
self.camera_id = str(config['id'])
self.network_id = str(config['network_id'])
self.serial = config['serial']
self.motion_enabled = config['enabled']
self.battery_voltage = config['battery_voltage']
self.battery_state = config['battery_state']
self.temperature = config['temperature']
self.wifi_strength = config['wifi_strength']
self.name = config["name"]
self.camera_id = str(config["id"])
self.network_id = str(config["network_id"])
self.serial = config["serial"]
self.motion_enabled = config["enabled"]
self.battery_voltage = config["battery_voltage"]
self.battery_state = config["battery_state"]
self.temperature = config["temperature"]
self.wifi_strength = config["wifi_strength"]
# Retrieve calibrated temperature from special endpoint
resp = api.request_camera_sensors(self.sync.blink,
self.network_id,
self.camera_id)
resp = api.request_camera_sensors(
self.sync.blink, self.network_id, self.camera_id
)
try:
self.temperature_calibrated = resp['temp']
self.temperature_calibrated = resp["temp"]
except KeyError:
self.temperature_calibrated = self.temperature
_LOGGER.warning("Could not retrieve calibrated temperature.")
@@ -121,16 +119,15 @@ class BlinkCamera():
# otherwise set it to None and log an error
new_thumbnail = None
thumb_addr = None
if config['thumbnail']:
thumb_addr = config['thumbnail']
if config["thumbnail"]:
thumb_addr = config["thumbnail"]
else:
_LOGGER.warning("Could not find thumbnail for camera %s",
self.name,
exc_info=True)
_LOGGER.warning(
"Could not find thumbnail for camera %s", self.name, exc_info=True
)
if thumb_addr is not None:
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url,
thumb_addr)
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, thumb_addr)
try:
self.motion_detected = self.sync.motion[self.name]
@@ -139,10 +136,9 @@ class BlinkCamera():
clip_addr = None
if self.name in self.sync.last_record:
clip_addr = self.sync.last_record[self.name]['clip']
self.last_record = self.sync.last_record[self.name]['time']
self.clip = "{}{}".format(self.sync.urls.base_url,
clip_addr)
clip_addr = self.sync.last_record[self.name]["clip"]
self.last_record = self.sync.last_record[self.name]["time"]
self.clip = "{}{}".format(self.sync.urls.base_url, clip_addr)
# If the thumbnail or clip have changed, update the cache
update_cached_image = False
@@ -155,15 +151,13 @@ class BlinkCamera():
update_cached_video = True
if new_thumbnail is not None and (update_cached_image or force_cache):
self._cached_image = api.http_get(self.sync.blink,
url=self.thumbnail,
stream=True,
json=False)
self._cached_image = api.http_get(
self.sync.blink, url=self.thumbnail, stream=True, json=False
)
if clip_addr is not None and (update_cached_video or force_cache):
self._cached_video = api.http_get(self.sync.blink,
url=self.clip,
stream=True,
json=False)
self._cached_video = api.http_get(
self.sync.blink, url=self.clip, stream=True, json=False
)
def image_to_file(self, path):
"""
@@ -174,11 +168,12 @@ class BlinkCamera():
_LOGGER.debug("Writing image from %s to %s", self.name, path)
response = self._cached_image
if response.status_code == 200:
with open(path, 'wb') as imgfile:
with open(path, "wb") as imgfile:
copyfileobj(response.raw, imgfile)
else:
_LOGGER.error("Cannot write image to file, response %s",
response.status_code)
_LOGGER.error(
"Cannot write image to file, response %s", response.status_code
)
def video_to_file(self, path):
"""Write video to file.
@@ -188,8 +183,7 @@ class BlinkCamera():
_LOGGER.debug("Writing video from %s to %s", self.name, path)
response = self._cached_video
if response is None:
_LOGGER.error("No saved video exist for %s.",
self.name)
_LOGGER.error("No saved video exist for %s.", self.name)
return
with open(path, 'wb') as vidfile:
with open(path, "wb") as vidfile:
copyfileobj(response.raw, vidfile)
+47 -44
View File
@@ -3,65 +3,68 @@
import os
MAJOR_VERSION = 0
MINOR_VERSION = 14
PATCH_VERSION = 3
MINOR_VERSION = 15
PATCH_VERSION = "0-rc.0"
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
__version__ = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
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.')
if os.path.exists('README.rst'):
PROJECT_LONG_DESCRIPTION = open('README.rst').read()
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."
)
if os.path.exists("README.rst"):
PROJECT_LONG_DESCRIPTION = open("README.rst").read()
PROJECT_CLASSIFIERS = [
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Home Automation'
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Home Automation",
]
PROJECT_GITHUB_USERNAME = 'fronzbot'
PROJECT_GITHUB_REPOSITORY = 'blinkpy'
PROJECT_GITHUB_USERNAME = "fronzbot"
PROJECT_GITHUB_REPOSITORY = "blinkpy"
PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME)
'''
"""
URLS
'''
BLINK_URL = 'immedia-semi.com'
DEFAULT_URL = "{}.{}".format('rest-prod', BLINK_URL)
"""
BLINK_URL = "immedia-semi.com"
DEFAULT_URL = "{}.{}".format("rest-prod", BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
OLD_LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest-piri', BLINK_URL)
LOGIN_URLS = [
"{}/api/v4/login".format(BASE_URL),
"{}/api/v3/login".format(BASE_URL),
"{}/api/v2/login".format(BASE_URL),
]
'''
"""
Dictionaries
'''
ONLINE = {'online': True, 'offline': False}
"""
ONLINE = {"online": True, "offline": False}
'''
"""
OTHER
'''
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
"""
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2
SIZE_NOTIFICATION_KEY = 152
SIZE_UID = 16
+2 -2
View File
@@ -4,11 +4,11 @@ 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"
"Cannot authenticate since either password or username has not been set",
)
AUTH_TOKEN = (
3,
"Authentication header incorrect. Are you sure you received your token?"
"Authentication header incorrect. Are you sure you received your token?",
)
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
+56 -26
View File
@@ -2,6 +2,7 @@
import logging
import time
import secrets
from calendar import timegm
from functools import partial, wraps
from requests import Request, Session, exceptions
@@ -13,13 +14,18 @@ import blinkpy.helpers.errors as ERROR
_LOGGER = logging.getLogger(__name__)
def gen_uid(size):
"""Create a random sring."""
full_token = secrets.token_hex(size)
return full_token[0:size]
def time_to_seconds(timestamp):
"""Convert TIMESTAMP_FORMAT time to seconds."""
try:
dtime = dateutil.parser.isoparse(timestamp)
except ValueError:
_LOGGER.error("Incorrect timestamp format for conversion: %s.",
timestamp)
_LOGGER.error("Incorrect timestamp format for conversion: %s.", timestamp)
return False
return timegm(dtime.timetuple())
@@ -35,8 +41,10 @@ def merge_dicts(dict_a, dict_b):
"""Merge two dictionaries into one."""
duplicates = [val for val in dict_a if val in dict_b]
if duplicates:
_LOGGER.warning(("Duplicates found during merge: %s. "
"Renaming is recommended."), duplicates)
_LOGGER.warning(
("Duplicates found during merge: %s. " "Renaming is recommended."),
duplicates,
)
return {**dict_a, **dict_b}
@@ -59,8 +67,16 @@ def attempt_reauthorization(blink):
return headers
def http_req(blink, url='http://example.com', data=None, headers=None,
reqtype='get', stream=False, json_resp=True, is_retry=False):
def http_req(
blink,
url="http://example.com",
data=None,
headers=None,
reqtype="get",
stream=False,
json_resp=True,
is_retry=False,
):
"""
Perform server requests and check if reauthorization neccessary.
@@ -73,10 +89,10 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
:param json_resp: Return JSON response? TRUE/False
:param is_retry: Is this a retry attempt? True/FALSE
"""
if reqtype == 'post':
req = Request('POST', url, headers=headers, data=data)
elif reqtype == 'get':
req = Request('GET', url, headers=headers)
if reqtype == "post":
req = Request("POST", url, headers=headers, data=data)
elif reqtype == "get":
req = Request("GET", url, headers=headers)
else:
_LOGGER.error("Invalid request type: %s", reqtype)
raise BlinkException(ERROR.REQUEST)
@@ -85,10 +101,10 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
try:
response = blink.session.send(prepped, stream=stream)
if json_resp and 'code' in response.json():
if json_resp and "code" in response.json():
resp_dict = response.json()
code = resp_dict['code']
message = resp_dict['message']
code = resp_dict["code"]
message = resp_dict["message"]
if is_retry and code in ERROR.BLINK_ERRORS:
_LOGGER.error("Cannot obtain new token for server auth.")
return None
@@ -96,20 +112,33 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
headers = attempt_reauthorization(blink)
if not headers:
raise exceptions.ConnectionError
return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
return http_req(
blink,
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
_LOGGER.warning("Response from server: %s - %s", code, message)
except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry:
headers = attempt_reauthorization(blink)
return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.",
url)
return http_req(
blink,
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.", url)
return None
if json_resp:
@@ -132,14 +161,14 @@ class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception."""
class BlinkURLHandler():
class BlinkURLHandler:
"""Class that handles Blink URLS."""
def __init__(self, region_id, legacy=False):
"""Initialize the urls."""
self.subdomain = 'rest-{}'.format(region_id)
self.subdomain = "rest-{}".format(region_id)
if legacy:
self.subdomain = 'rest.{}'.format(region_id)
self.subdomain = "rest.{}".format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url)
@@ -149,7 +178,7 @@ class BlinkURLHandler():
_LOGGER.debug("Setting base url to %s.", self.base_url)
class Throttle():
class Throttle:
"""Class for throttling api calls."""
def __init__(self, seconds=10):
@@ -159,6 +188,7 @@ class Throttle():
def __call__(self, method):
"""Throttle caller method."""
def throttle_method():
"""Call when method is throttled."""
return None
@@ -166,7 +196,7 @@ class Throttle():
@wraps(method)
def wrapper(*args, **kwargs):
"""Wrap that checks for throttling."""
force = kwargs.pop('force', False)
force = kwargs.pop("force", False)
now = int(time.time())
last_call_delta = now - self.last_call
if force or last_call_delta > self.throttle_time:
+144
View File
@@ -0,0 +1,144 @@
"""Login handler for blink."""
import json
import logging
from os.path import isfile
from getpass import getpass
from blinkpy import api
from blinkpy.helpers import util
from blinkpy.helpers import constants as const
_LOGGER = logging.getLogger(__name__)
class LoginHandler:
"""Class to handle login communication."""
def __init__(self, username=None, password=None, cred_file=None, persist_key=None):
"""
Initialize login handler.
:param username: Blink username
:param password: Blink password
:param cred_file: JSON formatted credential file.
:param persist_key: File location of persistant key.
"""
self.login_url = None
self.login_urls = const.LOGIN_URLS
self.cred_file = cred_file
self.persist_key = persist_key
self.data = {
"username": username,
"password": password,
"uid": None,
"notification_key": None,
}
self.check_keys()
def check_keys(self):
"""Check if uid exists, if not create."""
uid = util.gen_uid(const.SIZE_UID)
notification_key = util.gen_uid(const.SIZE_NOTIFICATION_KEY)
data = {"uid": uid, "notification_key": notification_key}
if self.persist_key is None:
return data
if not isfile(self.persist_key):
with open(self.persist_key, "w") as json_file:
json.dump(data, json_file)
else:
with open(self.persist_key, "r") as json_file:
data = json.load(json_file)
return data
def check_cred_file(self):
"""Check if credential file supplied and use if so."""
if isfile(self.cred_file):
try:
with open(self.cred_file, "r") as json_file:
creds = json.load(json_file)
self.data["username"] = creds["username"]
self.data["password"] = creds["password"]
except ValueError:
_LOGGER.error(
"Improperly formatted json file %s.", self.cred_file, exc_info=True
)
return False
except KeyError:
_LOGGER.error("JSON file information incomplete %s.", exc_info=True)
return False
return True
return False
def check_login(self):
"""Check login information and prompt if not available."""
if self.data["username"] is None:
self.data["username"] = input("Username:")
if self.data["password"] is None:
self.data["password"] = getpass("Password:")
if self.data["username"] and self.data["password"]:
return True
return False
def validate_response(self, url, response):
"""Validate response from login endpoint."""
try:
if response.status_code != 200:
return False
except AttributeError:
_LOGGER.error(
"Response for %s did not return a status code. Deprecated endpoint?",
url,
)
return False
return True
def login(self, blink):
"""Attempt login to blink servers."""
if self.cred_file is not None:
self.check_cred_file()
if not self.check_login():
_LOGGER.error("Cannot login with username %s", self.data["username"])
return False
for url in self.login_urls:
_LOGGER.info("Attempting login with %s", url)
response = api.request_login(
blink,
url,
self.data["username"],
self.data["password"],
self.data["notification_key"],
self.data["uid"],
is_retry=False,
)
if self.validate_response(url, response):
self.login_url = url
return response.json()
_LOGGER.error("Failed to login to Blink servers. Last response: %s", response)
return False
def send_auth_key(self, blink, key):
"""Send 2FA key to blink servers."""
if key is not None:
response = api.request_verify(blink, key)
try:
json_resp = response.json()
blink.available = json_resp["valid"]
except (KeyError, TypeError):
_LOGGER.error("Did not receive valid response from server.")
return False
return True
def check_key_required(self, blink):
"""Check if 2FA key is required."""
try:
if blink.login_response["client"]["verification_required"]:
return True
except KeyError:
pass
return False
+60 -60
View File
@@ -11,7 +11,7 @@ from blinkpy.helpers.constants import ONLINE
_LOGGER = logging.getLogger(__name__)
class BlinkSyncModule():
class BlinkSyncModule:
"""Class to initialize sync module."""
def __init__(self, blink, network_name, network_id, camera_list):
@@ -38,18 +38,19 @@ class BlinkSyncModule():
self.motion = {}
self.last_record = {}
self.camera_list = camera_list
self.available = False
@property
def attributes(self):
"""Return sync attributes."""
attr = {
'name': self.name,
'id': self.sync_id,
'network_id': self.network_id,
'serial': self.serial,
'status': self.status,
'region': self.region,
'region_id': self.region_id,
"name": self.name,
"id": self.sync_id,
"network_id": self.network_id,
"serial": self.serial,
"status": self.status,
"region": self.region,
"region_id": self.region_id,
}
return attr
@@ -67,7 +68,7 @@ class BlinkSyncModule():
def arm(self):
"""Return status of sync module: armed/disarmed."""
try:
return self.network_info['network']['armed']
return self.network_info["network"]["armed"]
except (KeyError, TypeError):
return None
@@ -81,85 +82,86 @@ class BlinkSyncModule():
def start(self):
"""Initialize the system."""
response = api.request_syncmodule(self.blink,
self.network_id)
response = api.request_syncmodule(self.blink, self.network_id)
try:
self.summary = response['syncmodule']
self.network_id = self.summary['network_id']
self.summary = response["syncmodule"]
self.network_id = self.summary["network_id"]
except (TypeError, KeyError):
_LOGGER.error(("Could not retrieve sync module information "
"with response: %s"), response, exc_info=True)
_LOGGER.error(
("Could not retrieve sync module information " "with response: %s"),
response,
exc_info=True,
)
return False
try:
self.sync_id = self.summary['id']
self.serial = self.summary['serial']
self.status = self.summary['status']
self.sync_id = self.summary["id"]
self.serial = self.summary["serial"]
self.status = self.summary["status"]
except KeyError:
_LOGGER.error("Could not extract some sync module info: %s",
response,
exc_info=True)
self.network_info = api.request_network_status(self.blink,
self.network_id)
_LOGGER.error(
"Could not extract some sync module info: %s", response, exc_info=True
)
self.get_network_info()
self.check_new_videos()
try:
for camera_config in self.camera_list:
if 'name' not in camera_config:
if "name" not in camera_config:
break
name = camera_config['name']
name = camera_config["name"]
self.cameras[name] = BlinkCamera(self)
self.motion[name] = False
camera_info = self.get_camera_info(camera_config['id'])
self.cameras[name].update(camera_info,
force_cache=True,
force=True)
camera_info = self.get_camera_info(camera_config["id"])
self.cameras[name].update(camera_info, force_cache=True, force=True)
except KeyError:
_LOGGER.error("Could not create cameras instances for %s",
self.name,
exc_info=True)
_LOGGER.error(
"Could not create cameras instances for %s", self.name, exc_info=True
)
return False
self.available = True
return True
def get_events(self, **kwargs):
"""Retrieve events from server."""
force = kwargs.pop('force', False)
response = api.request_sync_events(self.blink,
self.network_id,
force=force)
force = kwargs.pop("force", False)
response = api.request_sync_events(self.blink, self.network_id, force=force)
try:
return response['event']
return response["event"]
except (TypeError, KeyError):
_LOGGER.error("Could not extract events: %s",
response,
exc_info=True)
_LOGGER.error("Could not extract events: %s", response, exc_info=True)
return False
def get_camera_info(self, camera_id):
"""Retrieve camera information."""
response = api.request_camera_info(self.blink,
self.network_id,
camera_id)
response = api.request_camera_info(self.blink, self.network_id, camera_id)
try:
return response['camera'][0]
return response["camera"][0]
except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s",
response,
exc_info=True)
_LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
return []
def get_network_info(self):
"""Retrieve network status."""
is_errored = False
self.network_info = api.request_network_status(self.blink, self.network_id)
try:
is_errored = self.network_info["network"]["sync_module_error"]
except KeyError:
is_errored = True
if is_errored:
self.available = False
def refresh(self, force_cache=False):
"""Get all blink cameras and pulls their most recent status."""
self.network_info = api.request_network_status(self.blink,
self.network_id)
self.get_network_info()
self.check_new_videos()
for camera_name in self.cameras.keys():
camera_id = self.cameras[camera_name].camera_id
camera_info = self.get_camera_info(camera_id)
self.cameras[camera_name].update(camera_info,
force_cache=force_cache)
self.cameras[camera_name].update(camera_info, force_cache=force_cache)
def check_new_videos(self):
"""Check if new videos since last refresh."""
@@ -170,27 +172,25 @@ class BlinkSyncModule():
# No need to check for motion.
return False
resp = api.request_videos(self.blink,
time=interval,
page=1)
resp = api.request_videos(self.blink, time=interval, page=1)
for camera in self.cameras.keys():
self.motion[camera] = False
try:
info = resp['media']
info = resp["media"]
except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp)
return False
for entry in info:
try:
name = entry['device_name']
clip = entry['media']
timestamp = entry['created_at']
name = entry["device_name"]
clip = entry["media"]
timestamp = entry["created_at"]
if self.check_new_video_time(timestamp):
self.motion[name] = True and self.arm
self.last_record[name] = {'clip': clip, 'time': timestamp}
self.last_record[name] = {"clip": clip, "time": timestamp}
except KeyError:
_LOGGER.debug("No new videos since last refresh.")
+1 -1
View File
@@ -4,7 +4,7 @@ coverage:
status:
project:
default:
threshold: 0.02
threshold: "2%"
comment: true
require_ci_to_pass: yes
+3
View File
@@ -12,9 +12,12 @@ reports=no
# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
disable=
format,
bad-continuation,
locally-disabled,
unused-argument,
duplicate-code,
implicit-str-concat,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
+4 -4
View File
@@ -1,4 +1,4 @@
python-dateutil~=2.8.0
requests==2.22.0
python-slugify~=3.0.2
testtools==2.3.0
python-dateutil~=2.8.1
requests~=2.23.0
python-slugify~=4.0.0
testtools==2.4.0
+4 -3
View File
@@ -1,9 +1,10 @@
coverage==4.5.4
black==19.10b0
coverage==5.1
flake8==3.7.9
flake8-docstrings==1.5.0
pylint==2.4.4
pylint==2.5.1
pydocstyle==5.0.2
pytest==5.3.5
pytest==5.4.1
pytest-cov==2.8.1
pytest-sugar>=0.9.2
pytest-timeout>=1.3.3
+18
View File
@@ -0,0 +1,18 @@
[flake8]
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
doctests = True
# To work with Black
max-line-length = 88
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# W504 line break after binary operator
ignore =
E501,
W503,
E203,
D202,
W504,
+26 -33
View File
@@ -1,48 +1,41 @@
"""Setup file for blinkpy."""
# -*- coding: utf-8 -*-
from os.path import abspath, dirname, join
from os.path import abspath, dirname
from setuptools import setup, find_packages
# Fixes import error due to breaking change in pip 10
# https://stackoverflow.com/a/49867265
# pip > 10
try:
from pip._internal.req import parse_requirements
# pip < 10
except ImportError:
from pip.req import parse_requirements
from blinkpy.helpers.constants import (
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
)
this_dir = abspath(dirname(__file__))
PROJECT_VERSION = __version__
install_reqs = parse_requirements('{}/requirements.txt'.format(this_dir), session=False)
test_reqs = parse_requirements('{}/requirements_test.txt'.format(this_dir), session=False)
REQUIRES = [str(ir.req) for ir in install_reqs]
TEST_REQUIRES = [str(ir.req) for ir in test_reqs]
THIS_DIR = abspath(dirname(__file__))
REQUIRES = [
"python-dateutil~=2.8.1",
"requests~=2.23.0",
"python-slugify~=4.0.0",
]
PACKAGES = find_packages(exclude=['tests*', 'docs'])
with open('{}/README.rst'.format(this_dir), encoding='utf-8') as readme_file:
long_description = readme_file.read()
name = PROJECT_PACKAGE_NAME
version = __version__
with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
LONG_DESCRIPTION = readme_file.read()
setup(
name = PROJECT_PACKAGE_NAME,
version = __version__,
description = PROJECT_DESCRIPTION,
long_description = long_description,
author = PROJECT_AUTHOR,
author_email = PROJECT_EMAIL,
license = PROJECT_LICENSE,
url = PROJECT_URL,
platforms = 'any',
py_modules = ['blinkpy'],
name=PROJECT_PACKAGE_NAME,
version=PROJECT_VERSION,
description=PROJECT_DESCRIPTION,
long_description=LONG_DESCRIPTION,
author=PROJECT_AUTHOR,
author_email=PROJECT_EMAIL,
license=PROJECT_LICENSE,
url=PROJECT_URL,
platforms='any',
py_modules=['blinkpy'],
packages=PACKAGES,
include_package_data = True,
install_requires = REQUIRES,
test_suite = 'tests',
classifiers = PROJECT_CLASSIFIERS
include_package_data=True,
install_requires=REQUIRES,
test_suite='tests',
classifiers=PROJECT_CLASSIFIERS
)
+19 -19
View File
@@ -4,11 +4,11 @@ from blinkpy.helpers.util import BlinkURLHandler
import blinkpy.helpers.constants as const
LOGIN_RESPONSE = {
'region': {'mock': 'Test'},
'networks': {
'1234': {'name': 'test', 'onboarded': True}
},
'authtoken': {'authtoken': 'foobar123', 'message': 'auth'}
"region": {"mock": "Test"},
"networks": {"1234": {"name": "test", "onboarded": True}},
"authtoken": {"authtoken": "foobar123", "message": "auth"},
"client": {"id": "5678"},
"account": {"id": "1337"},
}
@@ -37,29 +37,29 @@ def mocked_session_send(*args, **kwargs):
url = prepped.url
header = prepped.headers
method = prepped.method
if method == 'GET':
expected_token = LOGIN_RESPONSE['authtoken']['authtoken']
if header['TOKEN_AUTH'] != expected_token:
response = {'message': 'Not Authorized', 'code': 400}
if method == "GET":
expected_token = LOGIN_RESPONSE["authtoken"]["authtoken"]
if header["TOKEN_AUTH"] != expected_token:
response = {"message": "Not Authorized", "code": 400}
status = 400
elif url == 'use_bad_response':
response = {'foo': 'bar'}
elif url == "use_bad_response":
response = {"foo": "bar"}
status = 200
elif url == 'reauth':
response = {'message': 'REAUTH', 'code': 777}
elif url == "reauth":
response = {"message": "REAUTH", "code": 777}
status = 777
else:
response = {'test': 'foo'}
response = {"test": "foo"}
status = 200
elif method == 'POST':
if url in (const.LOGIN_URL, const.LOGIN_BACKUP_URL):
elif method == "POST":
if url in const.LOGIN_URLS:
response = LOGIN_RESPONSE
status = 200
elif url == 'http://wrong.url/' or url is None:
response = {'message': 'Error', 'code': 404}
elif url == "http://wrong.url/" or url is None:
response = {"message": "Error", "code": 404}
status = 404
else:
response = {'message': 'foo', 'code': 200}
response = {"message": "foo", "code": 200}
status = 200
return MockResponse(response, status)
-42
View File
@@ -1,42 +0,0 @@
"""Test various api functions."""
import unittest
from unittest import mock
from blinkpy import api
from blinkpy.blinkpy import Blink
from blinkpy.helpers.util import create_session
class TestBlinkAPI(unittest.TestCase):
"""Test the blink api module."""
def setUp(self):
"""Initialize the blink module."""
self.blink = Blink()
self.blink.session = create_session()
# pylint: disable=protected-access
self.blink._auth_header = {}
def tearDown(self):
"""Tear down blink module."""
self.blink = None
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
def test_http_req_connect_error(self, mock_auth):
"""Test http_get error condition."""
mock_auth.return_value = {'foo': 'bar'}
firstlog = ("INFO:blinkpy.helpers.util:"
"Cannot connect to server with url {}.").format(
'http://notreal.fake')
nextlog = ("INFO:blinkpy.helpers.util:"
"Auth token expired, attempting reauthorization.")
lastlog = ("ERROR:blinkpy.helpers.util:"
"Endpoint {} failed. Possible issue with "
"Blink servers.").format('http://notreal.fake')
expected = [firstlog, nextlog, firstlog, lastlog]
with self.assertLogs() as getlog:
api.http_get(self.blink, 'http://notreal.fake')
with self.assertLogs() as postlog:
api.http_post(self.blink, 'http://notreal.fake')
self.assertEqual(getlog.output, expected)
self.assertEqual(postlog.output, expected)
+47 -65
View File
@@ -8,8 +8,8 @@ from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import create_session, get_time
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
class MockSyncModule(BlinkSyncModule):
@@ -17,7 +17,7 @@ class MockSyncModule(BlinkSyncModule):
def __init__(self, blink, header):
"""Create mock sync module instance."""
super().__init__(blink, header)
super().__init__(blink, header, network_id=None, camera_list=None)
self.blink = blink
self.header = header
self.return_value = None
@@ -34,69 +34,54 @@ class MockSyncModule(BlinkSyncModule):
return self.return_value
@mock.patch('blinkpy.helpers.util.Session.send',
side_effect=mresp.mocked_session_send)
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
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.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
# pylint: disable=protected-access
self.blink._auth_header = {
'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123'
}
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
self.blink.urls = blinkpy.BlinkURLHandler("test")
self.blink.session = create_session()
def tearDown(self):
"""Clean up after test."""
self.blink = None
@mock.patch('blinkpy.blinkpy.api.request_login')
@mock.patch("blinkpy.login_handler.api.request_login")
def test_backup_url(self, req, mock_sess):
"""Test backup login method."""
json_resp = {
'authtoken': {'authtoken': 'foobar123'},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}
"authtoken": {"authtoken": "foobar123"},
"networks": {"1234": {"name": "foobar", "onboarded": True}},
}
bad_req = mresp.MockResponse({}, 404)
new_req = mresp.MockResponse(json_resp, 200)
req.side_effect = [
bad_req,
bad_req,
new_req
]
self.blink.login_urls = ['test1', 'test2', 'test3']
self.blink.login_request()
# pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test3')
req.side_effect = [bad_req, bad_req, new_req]
self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
self.blink.login_handler.login(self.blink)
self.assertEqual(self.blink.login_handler.login_url, "test3")
req.side_effect = [
bad_req,
new_req,
bad_req
]
self.blink.login_urls = ['test1', 'test2', 'test3']
self.blink.login_request()
# pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test2')
req.side_effect = [bad_req, new_req, bad_req]
self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
self.blink.login_handler.login(self.blink)
self.assertEqual(self.blink.login_handler.login_url, "test2")
def test_merge_cameras(self, mock_sess):
"""Test merge camera functionality."""
first_dict = {'foo': 'bar', 'test': 123}
next_dict = {'foobar': 456, 'bar': 'foo'}
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, [])
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, [])
self.blink.sync['foo'].cameras = first_dict
self.blink.sync['bar'].cameras = next_dict
first_dict = {"foo": "bar", "test": 123}
next_dict = {"foobar": 456, "bar": "foo"}
self.blink.sync["foo"] = BlinkSyncModule(self.blink, "foo", 1, [])
self.blink.sync["bar"] = BlinkSyncModule(self.blink, "bar", 2, [])
self.blink.sync["foo"].cameras = first_dict
self.blink.sync["bar"].cameras = next_dict
result = self.blink.merge_cameras()
expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'}
expected = {"foo": "bar", "test": 123, "foobar": 456, "bar": "foo"}
self.assertEqual(expected, result)
@mock.patch('blinkpy.blinkpy.api.request_videos')
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_download_video_exit(self, mock_req, mock_sess):
"""Test we exit method when provided bad response."""
blink = blinkpy.Blink()
@@ -106,63 +91,60 @@ class TestBlinkFunctions(unittest.TestCase):
mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting."
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting.",
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp')
blink.download_videos("/tmp")
self.assertEqual(dl_log.output, expected_log)
@mock.patch('blinkpy.blinkpy.api.request_videos')
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_parse_downloaded_items(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'device_name': 'foo',
'deleted': True,
'media': '/bar.mp4'
"created_at": "1970",
"device_name": "foo",
"deleted": True,
"media": "/bar.mp4",
}
result = [generic_entry]
mock_req.return_value = {'media': result}
mock_req.return_value = {"media": result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted."
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.",
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', stop=2)
blink.download_videos("/tmp", stop=2)
self.assertEqual(dl_log.output, expected_log)
@mock.patch('blinkpy.blinkpy.api.request_videos')
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_parse_camera_not_in_list(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'device_name': 'foo',
'deleted': True,
'media': '/bar.mp4'
"created_at": "1970",
"device_name": "foo",
"deleted": True,
"media": "/bar.mp4",
}
result = [generic_entry]
mock_req.return_value = {'media': result}
mock_req.return_value = {"media": result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:Skipping videos for foo."
"DEBUG:blinkpy.blinkpy:Skipping videos for foo.",
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', camera='bar', stop=2)
blink.download_videos("/tmp", camera="bar", stop=2)
self.assertEqual(dl_log.output, expected_log)
+75 -130
View File
@@ -11,174 +11,92 @@ from unittest import mock
from blinkpy import api
from blinkpy.blinkpy import Blink
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.login_handler import LoginHandler
from blinkpy.helpers.util import (
http_req, create_session, BlinkAuthenticationException,
BlinkException, BlinkURLHandler)
http_req,
create_session,
BlinkException,
BlinkURLHandler,
)
from blinkpy.helpers.constants import __version__
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
@mock.patch('blinkpy.helpers.util.Session.send',
side_effect=mresp.mocked_session_send)
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
class TestBlinkSetup(unittest.TestCase):
"""Test the Blink class in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink_no_cred = Blink()
self.blink = Blink(username=USERNAME,
password=PASSWORD)
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.blink.urls = BlinkURLHandler('test')
self.blink = Blink(username=USERNAME, password=PASSWORD)
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
self.blink.urls = BlinkURLHandler("test")
self.blink.session = create_session()
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.blink_no_cred = None
def test_initialization(self, mock_sess):
"""Verify we can initialize blink."""
self.assertEqual(self.blink.version, __version__)
# 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, mock_sess):
"""Check that we throw an exception when no username/password."""
with self.assertRaises(BlinkAuthenticationException):
self.blink_no_cred.get_auth_token()
# pylint: disable=protected-access
self.blink_no_cred._username = USERNAME
with self.assertRaises(BlinkAuthenticationException):
self.blink_no_cred.get_auth_token()
def test_no_auth_header(self, mock_sess):
"""Check that we throw an exception when no auth header given."""
# pylint: disable=unused-variable
(region_id, region), = mresp.LOGIN_RESPONSE['region'].items()
self.blink.urls = BlinkURLHandler(region_id)
with self.assertRaises(BlinkException):
self.blink.get_ids()
@mock.patch('blinkpy.blinkpy.getpass.getpass')
def test_manual_login(self, getpwd, mock_sess):
"""Check that we can manually use the login() function."""
getpwd.return_value = PASSWORD
with mock.patch('builtins.input', return_value=USERNAME):
self.assertTrue(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.blinkpy.getpass.getpass')
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
def test_no_cred_file(self, getpwd, getauth, mock_sess):
"""Check that normal login occurs when cred file doesn't exist."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
getpwd.return_value = PASSWORD
getauth.return_value = True
with mock.patch('builtins.input', return_value=USERNAME):
self.assertTrue(self.blink.login())
def test_exit_on_missing_json(self, mock_sess):
"""Test that we fail on missing json data."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data="{}")):
self.assertFalse(self.blink.login())
def test_exit_on_bad_json(self, mock_sess):
"""Test that we fail on bad json format."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data='{]')):
self.assertFalse(self.blink.login())
@mock.patch('blinkpy.blinkpy.json.load')
def test_cred_file(self, mockjson, mock_sess):
"""Test that loading credential file works."""
# pylint: disable=protected-access
self.blink_no_cred._cred_file = '/tmp/fake.file'
mockjson.return_value = {'username': 'foo', 'password': 'bar'}
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data='')):
self.assertTrue(self.blink_no_cred.login())
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._username, 'foo')
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._password, 'bar')
self.assertEqual(self.blink.login_handler.data["username"], USERNAME)
self.assertEqual(self.blink.login_handler.data["password"], PASSWORD)
def test_bad_request(self, mock_sess):
"""Check that we raise an Exception with a bad request."""
self.blink.session = create_session()
explog = ("WARNING:blinkpy.helpers.util:"
"Response from server: 200 - foo")
explog = "WARNING:blinkpy.helpers.util:" "Response from server: 200 - foo"
with self.assertRaises(BlinkException):
http_req(self.blink, reqtype='bad')
http_req(self.blink, reqtype="bad")
with self.assertLogs() as logrecord:
http_req(self.blink, reqtype='post', is_retry=True)
http_req(self.blink, reqtype="post", is_retry=True)
self.assertEqual(logrecord.output, [explog])
def test_authentication(self, mock_sess):
"""Check that we can authenticate Blink up properly."""
authtoken = self.blink.get_auth_token()['TOKEN_AUTH']
expected = mresp.LOGIN_RESPONSE['authtoken']['authtoken']
authtoken = self.blink.get_auth_token()["TOKEN_AUTH"]
expected = mresp.LOGIN_RESPONSE["authtoken"]["authtoken"]
self.assertEqual(authtoken, expected)
def test_reauthorization_attempt(self, mock_sess):
"""Check that we can reauthorize after first unsuccessful attempt."""
original_header = self.blink.get_auth_token()
# pylint: disable=protected-access
bad_header = {'Host': self.blink._host, 'TOKEN_AUTH': 'BADTOKEN'}
bad_header = {"Host": self.blink._host, "TOKEN_AUTH": "BADTOKEN"}
# pylint: disable=protected-access
self.blink._auth_header = bad_header
self.assertEqual(self.blink.auth_header, bad_header)
api.request_homescreen(self.blink)
self.assertEqual(self.blink.auth_header, original_header)
@mock.patch('blinkpy.api.request_networks')
def test_multiple_networks(self, mock_net, mock_sess):
def test_multiple_networks(self, mock_sess):
"""Check that we handle multiple networks appropriately."""
mock_net.return_value = {
'networks': [{'id': 1234, 'account_id': 1111},
{'id': 5678, 'account_id': 2222}]
self.blink.networks = {
"0000": {"onboarded": False, "name": "foo"},
"5678": {"onboarded": True, "name": "bar"},
"1234": {"onboarded": False, "name": "test"},
}
self.blink.networks = {'0000': {'onboarded': False, 'name': 'foo'},
'5678': {'onboarded': True, 'name': 'bar'},
'1234': {'onboarded': False, 'name': 'test'}}
self.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 2222)
self.assertTrue("5678" in self.blink.network_ids)
@mock.patch('blinkpy.api.request_networks')
def test_multiple_onboarded_networks(self, mock_net, mock_sess):
def test_multiple_onboarded_networks(self, mock_sess):
"""Check that we handle multiple networks appropriately."""
mock_net.return_value = {
'networks': [{'id': 0000, 'account_id': 2222},
{'id': 5678, 'account_id': 1111}]
self.blink.networks = {
"0000": {"onboarded": False, "name": "foo"},
"5678": {"onboarded": True, "name": "bar"},
"1234": {"onboarded": True, "name": "test"},
}
self.blink.networks = {'0000': {'onboarded': False, 'name': 'foo'},
'5678': {'onboarded': True, 'name': 'bar'},
'1234': {'onboarded': True, 'name': 'test'}}
self.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids)
self.assertTrue('1234' in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 1111)
self.assertTrue("0000" not in self.blink.network_ids)
self.assertTrue("5678" in self.blink.network_ids)
self.assertTrue("1234" in self.blink.network_ids)
@mock.patch('blinkpy.blinkpy.time.time')
@mock.patch("blinkpy.blinkpy.time.time")
def test_throttle(self, mock_time, mock_sess):
"""Check throttling functionality."""
now = self.blink.refresh_rate + 1
@@ -186,8 +104,9 @@ class TestBlinkSetup(unittest.TestCase):
self.assertEqual(self.blink.last_refresh, None)
self.assertEqual(self.blink.check_if_ok_to_update(), True)
self.assertEqual(self.blink.last_refresh, None)
with mock.patch('blinkpy.sync_module.BlinkSyncModule.refresh',
return_value=True):
with mock.patch(
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
):
self.blink.refresh()
self.assertEqual(self.blink.last_refresh, now)
@@ -196,31 +115,57 @@ class TestBlinkSetup(unittest.TestCase):
def test_sync_case_insensitive_dict(self, mock_sess):
"""Check that we can access sync modules ignoring case."""
self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['TEST'].name, 'test')
self.assertEqual(self.blink.sync["test"].name, "test")
self.assertEqual(self.blink.sync["TEST"].name, "test")
@mock.patch('blinkpy.api.request_login')
@mock.patch("blinkpy.api.request_login")
def test_unexpected_login(self, mock_login, mock_sess):
"""Check that we appropriately handle unexpected login info."""
mock_login.return_value = None
self.assertFalse(self.blink.get_auth_token())
@mock.patch('blinkpy.api.request_homescreen')
@mock.patch("blinkpy.api.request_homescreen")
def test_get_cameras(self, mock_home, mock_sess):
"""Check retrieval of camera information."""
mock_home.return_value = {
'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678},
{'name': 'bar', 'network_id': 1234, 'id': 5679},
{'name': 'test', 'network_id': 4321, 'id': 0000}]
"cameras": [
{"name": "foo", "network_id": 1234, "id": 5678},
{"name": "bar", "network_id": 1234, "id": 5679},
{"name": "test", "network_id": 4321, "id": 0000},
]
}
result = self.blink.get_cameras()
self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678},
{'name': 'bar', 'id': 5679}],
'4321': [{'name': 'test', 'id': 0000}]})
self.assertEqual(
result,
{
"1234": [{"name": "foo", "id": 5678}, {"name": "bar", "id": 5679}],
"4321": [{"name": "test", "id": 0000}],
},
)
@mock.patch('blinkpy.api.request_homescreen')
@mock.patch("blinkpy.api.request_homescreen")
def test_get_cameras_failure(self, mock_home, mock_sess):
"""Check that on failure we initialize empty info and move on."""
mock_home.return_value = {}
result = self.blink.get_cameras()
self.assertEqual(result, {})
@mock.patch.object(LoginHandler, "send_auth_key")
@mock.patch.object(Blink, "setup_post_verify")
def test_startup_prompt(self, mock_send_key, mock_verify, mock_sess):
"""Test startup logic with command-line prompt."""
mock_send_key.return_value = True
mock_verify.return_value = True
self.blink.no_prompt = False
self.blink.key_required = True
self.blink.available = True
with mock.patch("builtins.input", return_value="1234"):
self.blink.start()
self.assertFalse(self.blink.key_required)
def test_startup_no_prompt(self, mock_sess):
"""Test startup with no_prompt flag set."""
self.blink.key_required = True
self.blink.no_prompt = True
self.blink.start()
self.assertTrue(self.blink.key_required)
+80 -81
View File
@@ -14,42 +14,40 @@ from blinkpy.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
CAMERA_CFG = {
'camera': [
"camera": [
{
'battery_voltage': 90,
'motion_alert': True,
'wifi_strength': -30,
'temperature': 68
"battery_voltage": 90,
"motion_alert": True,
"wifi_strength": -30,
"temperature": 68,
}
]
}
@mock.patch('blinkpy.helpers.util.Session.send',
side_effect=mresp.mocked_session_send)
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
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)
self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
header = {
'Host': 'abc.zxc',
'TOKEN_AUTH': mresp.LOGIN_RESPONSE['authtoken']['authtoken']
"Host": "abc.zxc",
"TOKEN_AUTH": mresp.LOGIN_RESPONSE["authtoken"]["authtoken"],
}
# pylint: disable=protected-access
self.blink._auth_header = header
self.blink.session = create_session()
self.blink.urls = BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, [])
self.camera = BlinkCamera(self.blink.sync['test'])
self.camera.name = 'foobar'
self.blink.sync['test'].cameras['foobar'] = self.camera
self.blink.urls = BlinkURLHandler("test")
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", 1234, [])
self.camera = BlinkCamera(self.blink.sync["test"])
self.camera.name = "foobar"
self.blink.sync["test"].cameras["foobar"] = self.camera
def tearDown(self):
"""Clean up after test."""
@@ -58,98 +56,99 @@ class TestBlinkCameraSetup(unittest.TestCase):
def test_camera_update(self, mock_sess):
"""Test that we can properly update camera properties."""
config = {
'name': 'new',
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
'battery_voltage': 90,
'battery_state': 'ok',
'temperature': 68,
'wifi_strength': 4,
'thumbnail': '/thumb',
"name": "new",
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"enabled": False,
"battery_voltage": 90,
"battery_state": "ok",
"temperature": 68,
"wifi_strength": 4,
"thumbnail": "/thumb",
}
self.camera.last_record = ['1']
self.camera.last_record = ["1"]
self.camera.sync.last_record = {
'new': {
'clip': '/test.mp4',
'time': '1970-01-01T00:00:00'
}
"new": {"clip": "/test.mp4", "time": "1970-01-01T00:00:00"}
}
mock_sess.side_effect = [
mresp.MockResponse({'temp': 71}, 200),
'test',
'foobar'
mresp.MockResponse({"temp": 71}, 200),
"test",
"foobar",
]
self.camera.update(config)
self.assertEqual(self.camera.name, 'new')
self.assertEqual(self.camera.camera_id, '1234')
self.assertEqual(self.camera.network_id, '5678')
self.assertEqual(self.camera.serial, '12345678')
self.assertEqual(self.camera.name, "new")
self.assertEqual(self.camera.camera_id, "1234")
self.assertEqual(self.camera.network_id, "5678")
self.assertEqual(self.camera.serial, "12345678")
self.assertEqual(self.camera.motion_enabled, False)
self.assertEqual(self.camera.battery, 'ok')
self.assertEqual(self.camera.battery, "ok")
self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail,
'https://rest-test.immedia-semi.com/thumb.jpg')
self.assertEqual(self.camera.clip,
'https://rest-test.immedia-semi.com/test.mp4')
self.assertEqual(self.camera.image_from_cache, 'test')
self.assertEqual(self.camera.video_from_cache, 'foobar')
self.assertEqual(
self.camera.thumbnail, "https://rest-test.immedia-semi.com/thumb.jpg"
)
self.assertEqual(
self.camera.clip, "https://rest-test.immedia-semi.com/test.mp4"
)
self.assertEqual(self.camera.image_from_cache, "test")
self.assertEqual(self.camera.video_from_cache, "foobar")
def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found."""
mock_sess.return_value = 'foobar'
self.camera.last_record = ['1']
mock_sess.return_value = "foobar"
self.camera.last_record = ["1"]
config = {
'name': 'new',
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
'battery_voltage': 90,
'battery_state': 'ok',
'temperature': 68,
'wifi_strength': 4,
'thumbnail': '',
}
self.camera.sync.homescreen = {
'devices': []
"name": "new",
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"enabled": False,
"battery_voltage": 90,
"battery_state": "ok",
"temperature": 68,
"wifi_strength": 4,
"thumbnail": "",
}
self.camera.sync.homescreen = {"devices": []}
self.assertEqual(self.camera.temperature_calibrated, None)
with self.assertLogs() as logrecord:
self.camera.update(config, force=True)
self.assertEqual(self.camera.thumbnail, None)
self.assertEqual(self.camera.last_record, ['1'])
self.assertEqual(self.camera.last_record, ["1"])
self.assertEqual(self.camera.temperature_calibrated, 68)
self.assertEqual(
logrecord.output,
[("WARNING:blinkpy.camera:Could not retrieve calibrated "
"temperature."),
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None")]
[
(
"WARNING:blinkpy.camera:Could not retrieve calibrated "
"temperature."
),
(
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None"
),
],
)
def test_no_video_clips(self, mock_sess):
"""Tests that we still proceed with camera setup with no videos."""
mock_sess.return_value = 'foobar'
mock_sess.return_value = "foobar"
config = {
'name': 'new',
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
'battery_voltage': 90,
'battery_state': 'ok',
'temperature': 68,
'wifi_strength': 4,
'thumbnail': '/foobar',
}
self.camera.sync.homescreen = {
'devices': []
"name": "new",
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"enabled": False,
"battery_voltage": 90,
"battery_state": "ok",
"temperature": 68,
"wifi_strength": 4,
"thumbnail": "/foobar",
}
self.camera.sync.homescreen = {"devices": []}
self.camera.update(config, force_cache=True)
self.assertEqual(self.camera.clip, None)
self.assertEqual(self.camera.video_from_cache, None)
+135
View File
@@ -0,0 +1,135 @@
"""Test login handler."""
import unittest
from unittest import mock
from blinkpy.login_handler import LoginHandler
import tests.mock_responses as mresp
USERNAME = "foobar"
PASSWORD = "deadbeef"
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
class TestLoginHandler(unittest.TestCase):
"""Test the LoginHandler class in blinkpy."""
def setUp(self):
"""Set up Login Handler."""
self.login_handler = LoginHandler()
def tearDown(self):
"""Clean up after test."""
self.login_handler = None
@mock.patch("blinkpy.login_handler.getpass")
def test_manual_login(self, getpwd, mock_sess):
"""Check that we can manually use the login() function."""
getpwd.return_value = PASSWORD
with mock.patch("builtins.input", return_value=USERNAME):
self.assertTrue(self.login_handler.check_login())
self.assertEqual(self.login_handler.data["username"], USERNAME)
self.assertEqual(self.login_handler.data["password"], PASSWORD)
def test_no_cred_file(self, mock_sess):
"""Check that we return false when cred file doesn't exist."""
self.login_handler.cred_file = "/tmp/fake.file"
self.assertFalse(self.login_handler.check_cred_file())
@mock.patch("blinkpy.login_handler.isfile")
def test_exit_on_missing_json(self, mockisfile, mock_sess):
"""Test that we fail on missing json data."""
self.login_handler.cred_file = "/tmp/fake.file"
mockisfile.return_value = True
with mock.patch("builtins.open", mock.mock_open(read_data="{}")):
self.assertFalse(self.login_handler.check_cred_file())
@mock.patch("blinkpy.login_handler.json.load")
@mock.patch("blinkpy.login_handler.isfile")
def test_cred_file(self, mockisfile, mockjson, mock_sess):
"""Test that loading credential file works."""
self.login_handler.cred_file = "/tmp/fake.file"
mockjson.return_value = {"username": "foo", "password": "bar"}
mockisfile.return_value = True
with mock.patch("builtins.open", mock.mock_open(read_data="")):
self.assertTrue(self.login_handler.check_cred_file())
self.assertEqual(self.login_handler.data["username"], "foo")
self.assertEqual(self.login_handler.data["password"], "bar")
def test_bad_response(self, mock_sess):
"""Check bad response from server."""
self.assertFalse(self.login_handler.validate_response(None, None))
def test_bad_response_code(self, mock_sess):
"""Check bad response code from server."""
fake_resp = mresp.MockResponse(None, 404)
self.assertFalse(self.login_handler.validate_response(None, fake_resp))
def test_good_response_code(self, mock_sess):
"""Check good response code from server."""
fake_resp = mresp.MockResponse(None, 200)
self.assertTrue(self.login_handler.validate_response(None, fake_resp))
@mock.patch("blinkpy.login_handler.util.gen_uid")
def test_check_keys_no_persist(self, mock_uid, mock_sess):
"""Check key generation."""
uid_value = "abc123"
mock_uid.return_value = "abc123"
self.login_handler.persist_key = None
data = self.login_handler.check_keys()
self.assertEqual(data["uid"], uid_value)
self.assertEqual(data["notification_key"], uid_value)
@mock.patch("blinkpy.login_handler.util.gen_uid")
@mock.patch("blinkpy.login_handler.json.load")
@mock.patch("blinkpy.login_handler.isfile")
def test_check_keys_persist(self, mockisfile, mockjson, mock_uid, mock_sess):
"""Check key load from file."""
uid_value = "abc123"
mock_file = {"uid": "321cba", "notification_key": "foobar123"}
mock_uid.return_value = uid_value
mockjson.return_value = mock_file
mockisfile.return_value = True
self.login_handler.persist_key = True
data = self.login_handler.check_keys()
self.assertEqual(mock_file["uid"], data["uid"])
self.assertEqual(mock_file["notification_key"], data["notification_key"])
def test_check_key_required(self, mock_sess):
"""Check key required method."""
response_true = {"client": {"verification_required": True}}
response_false = {"client": {"verification_required": False}}
response_nokey = {}
mock_blink = MockBlink(response_nokey)
self.assertFalse(self.login_handler.check_key_required(mock_blink))
mock_blink = MockBlink(response_false)
self.assertFalse(self.login_handler.check_key_required(mock_blink))
mock_blink = MockBlink(response_true)
self.assertTrue(self.login_handler.check_key_required(mock_blink))
@mock.patch("blinkpy.login_handler.api.request_verify")
def test_send_auth_key(self, mock_req, mock_sess):
"""Check sending of auth key."""
mock_blink = MockBlink(None)
mock_req.return_value = mresp.MockResponse({"valid": True}, 200)
self.assertTrue(self.login_handler.send_auth_key(mock_blink, 1234))
self.assertTrue(mock_blink.available)
mock_req.return_value = mresp.MockResponse(None, 200)
self.assertFalse(self.login_handler.send_auth_key(mock_blink, 1234))
mock_req.return_value = mresp.MockResponse({}, 200)
self.assertFalse(self.login_handler.send_auth_key(mock_blink, 1234))
self.assertTrue(self.login_handler.send_auth_key(mock_blink, None))
class MockBlink:
"""Object to mock basic blink class."""
def __init__(self, login_response):
"""Initialize mock blink class."""
self.available = False
self.login_response = login_response
+109 -108
View File
@@ -6,44 +6,41 @@ from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
@mock.patch('blinkpy.api.http_req')
@mock.patch("blinkpy.api.http_req")
class TestBlinkSyncModule(unittest.TestCase):
"""Test BlinkSyncModule functions in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD,
motion_interval=0)
self.blink = blinkpy.Blink(
username=USERNAME, password=PASSWORD, motion_interval=0
)
# pylint: disable=protected-access
self.blink._auth_header = {
'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123'
}
self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.blink.urls = blinkpy.BlinkURLHandler("test")
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
self.camera = BlinkCamera(self.blink.sync)
self.mock_start = [
{'syncmodule': {
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'status': 'foobar'}},
{'event': True},
{
"syncmodule": {
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"status": "foobar",
}
},
{"event": True},
{},
{},
None,
{'devicestatus': {}},
{"devicestatus": {}},
]
self.blink.sync['test'].network_info = {'network': {'armed': True}}
self.blink.sync["test"].network_info = {"network": {"armed": True}}
def tearDown(self):
"""Clean up after test."""
@@ -53,173 +50,177 @@ class TestBlinkSyncModule(unittest.TestCase):
def test_get_events(self, mock_resp):
"""Test get events function."""
mock_resp.return_value = {'event': True}
self.assertEqual(self.blink.sync['test'].get_events(), True)
mock_resp.return_value = {"event": True}
self.assertEqual(self.blink.sync["test"].get_events(), True)
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'camera': ['foobar']}
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar')
mock_resp.return_value = {"camera": ["foobar"]}
self.assertEqual(self.blink.sync["test"].get_camera_info("1234"), "foobar")
def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup."""
sync_module = self.blink.sync['test']
sync_module = self.blink.sync["test"]
self.blink.last_refresh = None
self.assertFalse(sync_module.check_new_videos())
def test_check_new_videos(self, mock_resp):
"""Test recent video response."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1990-01-01T00:00:00+00:00'
}]
"media": [
{
"device_name": "foo",
"media": "/foo/bar.mp4",
"created_at": "1990-01-01T00:00:00+00:00",
}
]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module = self.blink.sync["test"]
sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 0
self.assertEqual(sync_module.motion, {})
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4',
'time': '1990-01-01T00:00:00+00:00'})
self.assertEqual(sync_module.motion, {'foo': True})
mock_resp.return_value = {'media': []}
self.assertEqual(
sync_module.last_record["foo"],
{"clip": "/foo/bar.mp4", "time": "1990-01-01T00:00:00+00:00"},
)
self.assertEqual(sync_module.motion, {"foo": True})
mock_resp.return_value = {"media": []}
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4',
'time': '1990-01-01T00:00:00+00:00'})
self.assertEqual(sync_module.motion, {"foo": False})
self.assertEqual(
sync_module.last_record["foo"],
{"clip": "/foo/bar.mp4", "time": "1990-01-01T00:00:00+00:00"},
)
def test_check_new_videos_old_date(self, mock_resp):
"""Test videos return response with old date."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+00:00'
}]
"media": [
{
"device_name": "foo",
"media": "/foo/bar.mp4",
"created_at": "1970-01-01T00:00:00+00:00",
}
]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module = self.blink.sync["test"]
sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.motion, {"foo": False})
def test_check_no_motion_if_not_armed(self, mock_resp):
"""Test that motion detection is not set if module unarmed."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1990-01-01T00:00:00+00:00'
}]
"media": [
{
"device_name": "foo",
"media": "/foo/bar.mp4",
"created_at": "1990-01-01T00:00:00+00:00",
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
]
}
sync_module = self.blink.sync["test"]
sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': True})
sync_module.network_info = {'network': {'armed': False}}
self.assertEqual(sync_module.motion, {"foo": True})
sync_module.network_info = {"network": {"armed": False}}
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.motion, {"foo": False})
def test_check_multiple_videos(self, mock_resp):
"""Test motion found even with multiple videos."""
mock_resp.return_value = {
'media': [
"media": [
{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+00:00'
"device_name": "foo",
"media": "/foo/bar.mp4",
"created_at": "1970-01-01T00:00:00+00:00",
},
{
'device_name': 'foo',
'media': '/bar/foo.mp4',
'created_at': '1990-01-01T00:00:00+00:00'
"device_name": "foo",
"media": "/bar/foo.mp4",
"created_at": "1990-01-01T00:00:00+00:00",
},
{
'device_name': 'foo',
'media': '/foobar.mp4',
'created_at': '1970-01-01T00:00:01+00:00'
}
"device_name": "foo",
"media": "/foobar.mp4",
"created_at": "1970-01-01T00:00:01+00:00",
},
]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module = self.blink.sync["test"]
sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': True})
self.assertEqual(sync_module.motion, {"foo": True})
expected_result = {
'foo': {
'clip': '/bar/foo.mp4',
'time': '1990-01-01T00:00:00+00:00'
}
"foo": {"clip": "/bar/foo.mp4", "time": "1990-01-01T00:00:00+00:00"}
}
self.assertEqual(sync_module.last_record, expected_result)
def test_check_new_videos_failed(self, mock_resp):
"""Test method when response is unexpected."""
mock_resp.side_effect = [None, 'just a string', {}]
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
mock_resp.side_effect = [None, "just a string", {}]
sync_module = self.blink.sync["test"]
sync_module.cameras = {"foo": None}
sync_module.motion['foo'] = True
sync_module.motion["foo"] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
self.assertFalse(sync_module.motion["foo"])
sync_module.motion['foo'] = True
sync_module.motion["foo"] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
self.assertFalse(sync_module.motion["foo"])
sync_module.motion['foo'] = True
sync_module.motion["foo"] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
self.assertFalse(sync_module.motion["foo"])
def test_sync_start(self, mock_resp):
"""Test sync start function."""
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['test'].sync_id, 1234)
self.assertEqual(self.blink.sync['test'].network_id, 5678)
self.assertEqual(self.blink.sync['test'].serial, '12345678')
self.assertEqual(self.blink.sync['test'].status, 'foobar')
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].name, "test")
self.assertEqual(self.blink.sync["test"].sync_id, 1234)
self.assertEqual(self.blink.sync["test"].network_id, 5678)
self.assertEqual(self.blink.sync["test"].serial, "12345678")
self.assertEqual(self.blink.sync["test"].status, "foobar")
def test_unexpected_summary(self, mock_resp):
"""Test unexpected summary response."""
self.mock_start[0] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync['test'].start())
self.assertFalse(self.blink.sync["test"].start())
def test_summary_with_no_network_id(self, mock_resp):
"""Test handling of bad summary."""
self.mock_start[0]['syncmodule'] = None
self.mock_start[0]["syncmodule"] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync['test'].start())
self.assertFalse(self.blink.sync["test"].start())
def test_summary_with_only_network_id(self, mock_resp):
"""Test handling of sparse summary."""
self.mock_start[0]['syncmodule'] = {'network_id': 8675309}
self.mock_start[0]["syncmodule"] = {"network_id": 8675309}
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].network_id, 8675309)
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].network_id, 8675309)
def test_unexpected_camera_info(self, mock_resp):
"""Test unexpected camera info response."""
self.blink.sync['test'].cameras['foo'] = None
self.blink.sync["test"].cameras["foo"] = None
self.mock_start[5] = None
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].cameras, {"foo": None})
def test_missing_camera_info(self, mock_resp):
"""Test missing key from camera info response."""
self.blink.sync['test'].cameras['foo'] = None
self.blink.sync["test"].cameras["foo"] = None
self.mock_start[5] = {}
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].cameras, {"foo": None})
+12 -10
View File
@@ -43,17 +43,18 @@ class TestUtil(unittest.TestCase):
self.assertEqual(2, len(calls))
# Fake time as 4 seconds from now
with mock.patch('time.time', return_value=now_plus_four):
with mock.patch("time.time", return_value=now_plus_four):
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 6 seconds from now
with mock.patch('time.time', return_value=now_plus_six):
with mock.patch("time.time", return_value=now_plus_six):
test_throttle()
self.assertEqual(3, len(calls))
def test_throttle_per_instance(self):
"""Test that throttle is done once per instance of class."""
class Tester:
"""A tester class for throttling."""
@@ -68,6 +69,7 @@ class TestUtil(unittest.TestCase):
def test_throttle_on_two_methods(self):
"""Test that throttle works for multiple methods."""
class Tester:
"""A tester class for throttling."""
@@ -91,24 +93,24 @@ class TestUtil(unittest.TestCase):
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_4):
with mock.patch("time.time", return_value=now_plus_4):
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_6):
with mock.patch("time.time", return_value=now_plus_6):
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler('test')
self.assertEqual(urls.subdomain, 'rest-test')
urls = BlinkURLHandler('test', legacy=True)
self.assertEqual(urls.subdomain, 'rest.test')
urls = BlinkURLHandler("test")
self.assertEqual(urls.subdomain, "rest-test")
urls = BlinkURLHandler("test", legacy=True)
self.assertEqual(urls.subdomain, "rest.test")
def test_time_to_seconds(self):
"""Test time to seconds conversion."""
correct_time = '1970-01-01T00:00:05+00:00'
wrong_time = '1/1/1970 00:00:03'
correct_time = "1970-01-01T00:00:05+00:00"
wrong_time = "1/1/1970 00:00:03"
self.assertEqual(time_to_seconds(correct_time), 5)
self.assertFalse(time_to_seconds(wrong_time))
+10 -2
View File
@@ -1,5 +1,5 @@
[tox]
envlist = build, py35, py36, py37, py38, lint
envlist = build, py36, py37, py38, lint
skip_missing_interpreters = True
skipsdist = True
@@ -23,7 +23,7 @@ deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
[testenv:lint]
[testenv:pylint]
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
@@ -31,8 +31,16 @@ basepython = python3
ignore_errors = True
commands =
pylint --rcfile={toxinidir}/pylintrc blinkpy tests app
[testenv:lint]
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
basepython = python3
commands =
flake8 blinkpy tests app
pydocstyle blinkpy tests app
black --check --diff blinkpy tests app
rst-lint README.rst
rst-lint CHANGES.rst