Compare commits

...
76 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub 295833efbf Merge pull request #254 from fronzbot/master
Merge changes.rst
2020-05-08 17:37:10 -04:00
Kevin FronczakandGitHub 5e98f59cdb Update for release 0.15.0 2020-05-08 17:35:54 -04:00
Kevin FronczakandGitHub 77649e0991 Merge pull request #253 from fronzbot/dev
v0.15.0
2020-05-08 17:28:18 -04:00
Kevin FronczakandGitHub 5c9ced057b Merge pull request #251 from fronzbot/dependabot/pip/restructuredtext-lint-1.3.0
Bump restructuredtext-lint from 1.0.1 to 1.3.0
2020-05-08 17:26:01 -04:00
dependabot-preview[bot]andGitHub 7a2b3b9967 Bump restructuredtext-lint from 1.0.1 to 1.3.0
Bumps [restructuredtext-lint](https://github.com/twolfson/restructuredtext-lint) from 1.0.1 to 1.3.0.
- [Release notes](https://github.com/twolfson/restructuredtext-lint/releases)
- [Changelog](https://github.com/twolfson/restructuredtext-lint/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/twolfson/restructuredtext-lint/compare/1.0.1...1.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 21:24:01 +00:00
Kevin FronczakandGitHub 4582600900 Merge pull request #252 from fronzbot/dependabot/pip/pygments-2.6.1
Bump pygments from 2.2.0 to 2.6.1
2020-05-08 17:22:54 -04:00
dependabot-preview[bot]andGitHub 78ec104392 Bump pygments from 2.2.0 to 2.6.1
Bumps [pygments](https://github.com/pygments/pygments) from 2.2.0 to 2.6.1.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.2.0...2.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 21:03:32 +00:00
Kevin FronczakandGitHub f15f00e8c5 Merge pull request #250 from fronzbot/dependabot/pip/pytest-sugar-0.9.3
Bump pytest-sugar from 0.9.2 to 0.9.3
2020-05-08 17:02:23 -04:00
Kevin FronczakandGitHub e953a2f918 Version bump 2020-05-08 17:02:07 -04:00
Kevin FronczakandGitHub 9d6075c0f7 Merge branch 'dev' into dependabot/pip/pytest-sugar-0.9.3 2020-05-08 16:58:37 -04:00
Kevin FronczakandGitHub 90ad0c7cc9 Merge pull request #249 from fronzbot/dependabot/pip/pytest-timeout-1.3.4
Bump pytest-timeout from 1.3.3 to 1.3.4
2020-05-08 16:57:52 -04:00
dependabot-preview[bot]andGitHub 62a74db683 Bump pytest-sugar from 0.9.2 to 0.9.3
Bumps [pytest-sugar](https://github.com/Teemu/pytest-sugar) from 0.9.2 to 0.9.3.
- [Release notes](https://github.com/Teemu/pytest-sugar/releases)
- [Changelog](https://github.com/Teemu/pytest-sugar/blob/master/CHANGES.rst)
- [Commits](https://github.com/Teemu/pytest-sugar/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 08:14:08 +00:00
dependabot-preview[bot]andGitHub 12b5dfdfd7 Bump pytest-timeout from 1.3.3 to 1.3.4
Bumps [pytest-timeout](https://github.com/pytest-dev/pytest-timeout) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/pytest-dev/pytest-timeout/releases)
- [Commits](https://github.com/pytest-dev/pytest-timeout/compare/1.3.3...1.3.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 08:13:45 +00:00
Kevin FronczakandGitHub 255812defc Merge pull request #246 from fronzbot/pin-requirements
Pin test requirements
2020-05-07 09:07:12 -04:00
Kevin FronczakandGitHub 7b2e015eed Pin test requirements 2020-05-07 09:04:42 -04:00
Kevin FronczakandGitHub a8605bd91f Dev version bump 2020-05-06 18:05:32 -04:00
Kevin FronczakandGitHub e3f9f4b0c1 Merge pull request #245 from fronzbot/add-device-id-param
Add device id param
2020-05-06 18:00:43 -04:00
Kevin Fronczak ec476bb9d6 quick codecov threshold fix...maybe 2020-05-06 11:19:45 -04:00
Kevin Fronczak 60cbcb7717 Update login methods to ensure platform is refreshed 2020-05-06 11:12:56 -04:00
Kevin Fronczak 4b11a8b16a Add ability to have unique device name per application 2020-05-06 10:59:01 -04:00
Kevin FronczakandGitHub dfb90b8c86 Dev version bump 2020-05-06 10:43:09 -04:00
Kevin FronczakandGitHub 2f0cec2724 Merge pull request #243 from fronzbot/dependabot/pip/pylint-2.5.2
Bump pylint from 2.5.1 to 2.5.2
2020-05-06 10:42:29 -04:00
Kevin FronczakandGitHub 887a1d0a2b Merge pull request #244 from fronzbot/fix-typos
Fix typos
2020-05-06 10:42:11 -04:00
Kevin FronczakandGitHub 1d65d558e8 Fix typo 2020-05-06 10:39:34 -04:00
Kevin FronczakandGitHub d49a1fe36c Fix typo, remove Python Version info 2020-05-06 10:37:25 -04:00
dependabot-preview[bot]andGitHub 33f0e16e7a Bump pylint from 2.5.1 to 2.5.2
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.5.1 to 2.5.2.
- [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.1...pylint-2.5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-06 08:37:35 +00:00
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
Kevin FronczakandGitHub deba0cbba6 Merge pull request #225 from fronzbot/dev
0.14.3
2020-04-22 10:22:48 -04:00
Kevin FronczakandGitHub 0c3359abcf Changes for release 0.14.3 2020-04-22 10:20:42 -04:00
Kevin FronczakandGitHub a76069174d Version bump to 0.14.3 2020-04-22 10:17:21 -04:00
31 changed files with 1216 additions and 907 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: build:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
strategy: strategy:
max-parallel: 4 max-parallel: 3
matrix: matrix:
platform: platform:
- ubuntu-latest - ubuntu-latest
python-version: [3.5, 3.6, 3.7, 3.8] python-version: [3.6, 3.7, 3.8]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
+1 -1
View File
@@ -3,7 +3,7 @@
.tox/* .tox/*
__pycache__/* __pycache__/*
htmlcov/* htmlcov/*
.coverage .coverage.*
coverage.xml coverage.xml
*.pyc *.pyc
*.egg*/* *.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
+33
View File
@@ -3,6 +3,39 @@ Changelog
A list of changes between each release A list of changes between each release
0.15.0 (2020-05-08)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking Changes:**
- Removed support for Python 3.5 (3.6 is now the minimum supported version)
- Deprecated ``Blink.login()`` method. Please only use the ``Blink.start()`` method for logging in.
**New Functions**
- Add ``device_id`` override when logging in (for debug and to differentiate applications) (`#245 <https://github.com/fronzbot/blinkpy/pull/245>`__)
This can be used by instantiating the Blink class with the ``device_id`` parameter.
**All Changes:**
- Fix setup.py use of internal pip structure (`#233 <https://github.com/fronzbot/blinkpy/pull/233>`__)
- Update python-slugify requirement from ~=3.0.2 to ~=4.0.0 (`#234 <https://github.com/fronzbot/blinkpy/pull/234>`__)
- Update python-dateutil requirement from ~=2.8.0 to ~=2.8.1 (`#230 <https://github.com/fronzbot/blinkpy/pull/230>`__)
- Bump requests from 2.22.0 to 2.23.0 (`#231 <https://github.com/fronzbot/blinkpy/pull/231>`__)
- Refactor login logic in preparation for 2FA (`#241 <https://github.com/fronzbot/blinkpy/pull/241>`__)
- Add 2FA Support (`#242 <https://github.com/fronzbot/blinkpy/pull/242>`__) (fixes (`#210 <https://github.com/fronzbot/blinkpy/pull/210>`__))
- Re-set key_required and available variables after setup (`#245 <https://github.com/fronzbot/blinkpy/pull/245>`__)
- Perform system refresh after setup (`#245 <https://github.com/fronzbot/blinkpy/pull/245>`__)
- Fix typos (`#244 <https://github.com/fronzbot/blinkpy/pull/244>`__)
0.14.3 (2020-04-22)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Add time check on recorded videos before determining motion
- Fix motion detection variable suck to ``True``
- Add ability to load credentials from a json file
- Only allow ``motion_detected`` variable to trigger if system was armed
- Log response message from server if not attempting a re-authorization
0.14.2 (2019-10-12) 0.14.2 (2019-10-12)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Update dependencies - Update dependencies
+12 -8
View File
@@ -1,6 +1,6 @@
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version| blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Codestyle|
================================================================================ =============================================================================================
A Python library for the Blink Camera system A Python library for the Blink Camera system (Python 3.6+)
Like the library? Consider buying me a cup of coffee! 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 = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD', refresh_rate=30)
blink.start() 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 be 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: In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
@@ -124,9 +129,8 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
.. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg .. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg
:target: https://pypi.python.org/pypi/blinkpy :target: https://pypi.python.org/pypi/blinkpy
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest .. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest :target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif .. |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 :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 from blinkpy import blinkpy
USERNAME = environ.get('USERNAME') USERNAME = environ.get("USERNAME")
PASSWORD = environ.get('PASSWORD') PASSWORD = environ.get("PASSWORD")
TIMEDELTA = timedelta(environ.get('TIMEDELTA', 1)) TIMEDELTA = timedelta(environ.get("TIMEDELTA", 1))
def get_date(): def get_date():
@@ -14,7 +14,7 @@ def get_date():
return (datetime.now() - TIMEDELTA).isoformat() return (datetime.now() - TIMEDELTA).isoformat()
def download_videos(blink, save_dir='/media'): def download_videos(blink, save_dir="/media"):
"""Make request to download videos.""" """Make request to download videos."""
blink.download_videos(save_dir, since=get_date()) blink.download_videos(save_dir, since=get_date())
@@ -26,6 +26,6 @@ def start():
return blink return blink
if __name__ == '__main__': if __name__ == "__main__":
BLINK = start() BLINK = start()
download_videos(BLINK) download_videos(BLINK)
+88 -41
View File
@@ -11,7 +11,16 @@ _LOGGER = logging.getLogger(__name__)
MIN_THROTTLE_TIME = 2 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,
device_id="Blinkpy",
):
""" """
Login request. Login request.
@@ -19,19 +28,52 @@ def request_login(blink, url, username, password, is_retry=False):
:param url: Login url. :param url: Login url.
:param username: Blink username. :param username: Blink username.
:param password: Blink password. :param password: Blink password.
:param notification_key: Randomly genereated key.
:param uid: Randomly generated unique id key.
:param is_retry: Is this part of a re-authorization attempt? :param is_retry: Is this part of a re-authorization attempt?
:param device_id: Name of application to send at login.
""" """
headers = { headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
'Host': DEFAULT_URL, data = dumps(
'Content-Type': 'application/json' {
} "email": username,
data = dumps({ "password": password,
'email': username, "notification_key": notification_key,
'password': password, "unique_id": uid,
'client_specifier': 'iPhone 9.2 | 2.2 | 222' "app_version": "6.0.7 (520300) #afb0be72a",
}) "client_name": "Computer",
return http_req(blink, url=url, headers=headers, data=data, "client_type": "android",
json_resp=False, reqtype='post', is_retry=is_retry) "device_identifier": device_id,
"device_name": "Blinkpy",
"os_version": "5.1.1",
"reauth": "true",
}
)
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): def request_networks(blink):
@@ -94,17 +136,16 @@ def request_command_status(blink, network, command_id):
:param network: Sync module network id. :param network: Sync module network id.
:param command_id: Command id to check. :param command_id: Command id to check.
""" """
url = "{}/network/{}/command/{}".format(blink.urls.base_url, url = "{}/network/{}/command/{}".format(blink.urls.base_url, network, command_id)
network,
command_id)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME) @Throttle(seconds=MIN_THROTTLE_TIME)
def request_homescreen(blink): def request_homescreen(blink):
"""Request homescreen info.""" """Request homescreen info."""
url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url, url = "{}/api/v3/accounts/{}/homescreen".format(
blink.account_id) blink.urls.base_url, blink.account_id
)
return http_get(blink, url) return http_get(blink, url)
@@ -129,9 +170,9 @@ def request_new_image(blink, network, camera_id):
:param network: Sync module network id. :param network: Sync module network id.
:param camera_id: Camera ID of camera to request new image from. :param camera_id: Camera ID of camera to request new image from.
""" """
url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/thumbnail".format(
network, blink.urls.base_url, network, camera_id
camera_id) )
return http_post(blink, url) return http_post(blink, url)
@@ -144,9 +185,7 @@ def request_new_video(blink, network, camera_id):
:param network: Sync module network id. :param network: Sync module network id.
:param camera_id: Camera ID of camera to request new video from. :param camera_id: Camera ID of camera to request new video from.
""" """
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url, network, camera_id)
network,
camera_id)
return http_post(blink, url) return http_post(blink, url)
@@ -167,7 +206,8 @@ def request_videos(blink, time=None, page=0):
""" """
timestamp = get_time(time) timestamp = get_time(time)
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format( 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) return http_get(blink, url)
@@ -190,9 +230,9 @@ def request_camera_info(blink, network, camera_id):
:param network: Sync module network id. :param network: Sync module network id.
:param camera_id: Camera ID of camera to request info from. :param camera_id: Camera ID of camera to request info from.
""" """
url = "{}/network/{}/camera/{}/config".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/config".format(
network, blink.urls.base_url, network, camera_id
camera_id) )
return http_get(blink, url) return http_get(blink, url)
@@ -204,9 +244,9 @@ def request_camera_sensors(blink, network, camera_id):
:param network: Sync module network id. :param network: Sync module network id.
:param camera_id: Camera ID of camera to request sesnor info from. :param camera_id: Camera ID of camera to request sesnor info from.
""" """
url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/signals".format(
network, blink.urls.base_url, network, camera_id
camera_id) )
return http_get(blink, url) return http_get(blink, url)
@@ -219,9 +259,9 @@ def request_motion_detection_enable(blink, network, camera_id):
:param network: Sync module network id. :param network: Sync module network id.
:param camera_id: Camera ID of camera to enable. :param camera_id: Camera ID of camera to enable.
""" """
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/enable".format(
network, blink.urls.base_url, network, camera_id
camera_id) )
return http_post(blink, url) return http_post(blink, url)
@@ -233,9 +273,9 @@ def request_motion_detection_disable(blink, network, camera_id):
:param network: Sync module network id. :param network: Sync module network id.
:param camera_id: Camera ID of camera to disable. :param camera_id: Camera ID of camera to disable.
""" """
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/disable".format(
network, blink.urls.base_url, network, camera_id
camera_id) )
return http_post(blink, url) return http_post(blink, url)
@@ -251,9 +291,15 @@ def http_get(blink, url, stream=False, json=True, is_retry=False):
if blink.auth_header is None: if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN) raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making GET request to %s", url) _LOGGER.debug("Making GET request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header, return http_req(
reqtype='get', stream=stream, json_resp=json, blink,
is_retry=is_retry) 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): def http_post(blink, url, is_retry=False):
@@ -266,5 +312,6 @@ def http_post(blink, url, is_retry=False):
if blink.auth_header is None: if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN) raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making POST request to %s", url) _LOGGER.debug("Making POST request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header, return http_req(
reqtype='post', is_retry=is_retry) blink, url=url, headers=blink.auth_header, reqtype="post", is_retry=is_retry
)
+121 -139
View File
@@ -15,9 +15,7 @@ blinkpy is in no way affiliated with Blink, nor Immedia Inc.
import os.path import os.path
import time import time
import getpass
import logging import logging
import json
from shutil import copyfileobj from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
@@ -26,27 +24,42 @@ from slugify import slugify
from blinkpy import api from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers import errors as ERROR
from blinkpy.helpers.util import ( from blinkpy.helpers.util import (
create_session, merge_dicts, get_time, BlinkURLHandler, create_session,
BlinkAuthenticationException, Throttle) merge_dicts,
get_time,
BlinkURLHandler,
Throttle,
)
from blinkpy.helpers.constants import ( from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL, BLINK_URL,
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME) DEFAULT_MOTION_INTERVAL,
DEFAULT_REFRESH,
MIN_THROTTLE_TIME,
LOGIN_URLS,
)
from blinkpy.helpers.constants import __version__ from blinkpy.helpers.constants import __version__
from blinkpy.login_handler import LoginHandler
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class Blink(): class Blink:
"""Class to initialize communication.""" """Class to initialize communication."""
def __init__(self, username=None, password=None, def __init__(
cred_file=None, self,
refresh_rate=DEFAULT_REFRESH, username=None,
motion_interval=DEFAULT_MOTION_INTERVAL, password=None,
legacy_subdomain=False): cred_file=None,
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False,
no_prompt=False,
persist_key=None,
device_id="Blinkpy",
):
""" """
Initialize Blink system. Initialize Blink system.
@@ -59,20 +72,32 @@ class Blink():
:param refresh_rate: Refresh rate of blink information. :param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds) Defaults to 15 (seconds)
:param motion_interval: How far back to register motion in minutes. :param motion_interval: How far back to register motion in minutes.
Defaults to last refresh time. Defaults to last refresh time.
Useful for preventing motion_detected property Useful for preventing motion_detected property
from de-asserting too quickly. from de-asserting too quickly.
:param legacy_subdomain: Set to TRUE to use old 'rest.region' :param legacy_subdomain: Set to TRUE to use old 'rest.region'
endpoints (only use if you are having endpoints (only use if you are having
api issues). 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.
:param device_id: Identifier for the application. Default is 'Blinkpy'.
This is used when logging in and should be changed to
fit the implementation (ie. "Home Assistant" in a
Home Assistant integration).
""" """
self._username = username self.login_handler = LoginHandler(
self._password = password username=username,
self._cred_file = cred_file password=password,
cred_file=cred_file,
persist_key=persist_key,
device_id=device_id,
)
self._token = None self._token = None
self._auth_header = None self._auth_header = None
self._host = None self._host = None
self.account_id = None self.account_id = None
self.client_id = None
self.network_ids = [] self.network_ids = []
self.urls = None self.urls = None
self.sync = CaseInsensitiveDict({}) self.sync = CaseInsensitiveDict({})
@@ -84,11 +109,15 @@ class Blink():
self.networks = [] self.networks = []
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({}) self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL self.login_url = LOGIN_URLS[0]
self.login_urls = [] self.login_urls = []
self.motion_interval = motion_interval self.motion_interval = motion_interval
self.version = __version__ self.version = __version__
self.legacy = legacy_subdomain self.legacy = legacy_subdomain
self.no_prompt = no_prompt
self.available = False
self.key_required = False
self.login_response = {}
@property @property
def auth_header(self): def auth_header(self):
@@ -102,126 +131,81 @@ class Blink():
Method logs in and sets auth token, urls, and ids for future requests. Method logs in and sets auth token, urls, and ids for future requests.
Essentially this is just a wrapper function for ease of use. Essentially this is just a wrapper function for ease of use.
""" """
if self._username is None or self._password is None: if not self.available:
if not self.login(): self.get_auth_token()
return
elif not self.get_auth_token():
return
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() camera_list = self.get_cameras()
networks = self.get_ids() networks = self.get_ids()
for network_name, network_id in networks.items(): for network_name, network_id in networks.items():
if network_id not in camera_list.keys(): if network_id not in camera_list.keys():
camera_list[network_id] = {} camera_list[network_id] = {}
_LOGGER.warning("No cameras found for %s", network_name) _LOGGER.warning("No cameras found for %s", network_name)
sync_module = BlinkSyncModule(self, sync_module = BlinkSyncModule(
network_name, self, network_name, network_id, camera_list[network_id]
network_id, )
camera_list[network_id])
sync_module.start() sync_module.start()
self.sync[network_name] = sync_module self.sync[network_name] = sync_module
self.cameras = self.merge_cameras() self.cameras = self.merge_cameras()
self.available = self.refresh()
self.key_required = False
def login(self): def login(self):
"""Prompt user for username and password.""" """Perform server login. DEPRECATED."""
if self._cred_file is not None and os.path.isfile(self._cred_file): _LOGGER.warning(
try: "Method is deprecated and will be removed in a future version. Please use the LoginHandler.login() method instead."
with open(self._cred_file, 'r') as json_file: )
creds = json.load(json_file) return self.login_handler.login(self)
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
def get_auth_token(self, is_retry=False): def get_auth_token(self, is_retry=False):
"""Retrieve the authentication token from Blink.""" """Retrieve the authentication token from Blink."""
if not isinstance(self._username, str): self.login_response = self.login_handler.login(self)
raise BlinkAuthenticationException(ERROR.USERNAME) if not self.login_response:
if not isinstance(self._password, str): self.available = False
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:
return False return False
self.setup_params(self.login_response)
self._host = "{}.{}".format(self.region_id, BLINK_URL) if self.login_handler.check_key_required(self):
self._token = response['authtoken']['authtoken'] self.key_required = True
self.networks = response['networks']
self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token}
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
return self._auth_header return self._auth_header
def login_request(self, is_retry=False): def setup_params(self, response):
"""Make a login request.""" """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: try:
login_url = self.login_urls.pop(0) return response["summary"]
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
except KeyError: except KeyError:
_LOGGER.warning("Could not extract region info.") return None
self.region_id = 'piri'
self.region = 'UNKNOWN'
self._login_url = login_url
return response
def get_ids(self): def get_ids(self):
"""Set the network ID and Account ID.""" """Set the network ID and Account ID."""
response = api.request_networks(self)
all_networks = [] all_networks = []
network_dict = {} network_dict = {}
for network, status in self.networks.items(): for network, status in self.networks.items():
if status['onboarded']: if status["onboarded"]:
all_networks.append('{}'.format(network)) all_networks.append("{}".format(network))
network_dict[status['name']] = 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
self.network_ids = all_networks self.network_ids = all_networks
return network_dict return network_dict
@@ -231,11 +215,11 @@ class Blink():
response = api.request_homescreen(self) response = api.request_homescreen(self)
try: try:
all_cameras = {} all_cameras = {}
for camera in response['cameras']: for camera in response["cameras"]:
camera_network = str(camera['network_id']) camera_network = str(camera["network_id"])
camera_name = camera['name'] camera_name = camera["name"]
camera_id = camera['id'] camera_id = camera["id"]
camera_info = {'name': camera_name, 'id': camera_id} camera_info = {"name": camera_name, "id": camera_id}
if camera_network not in all_cameras: if camera_network not in all_cameras:
all_cameras[camera_network] = [] all_cameras[camera_network] = []
@@ -279,8 +263,7 @@ class Blink():
combined = merge_dicts(combined, self.sync[sync].cameras) combined = merge_dicts(combined, self.sync[sync].cameras)
return combined return combined
def download_videos(self, path, since=None, def download_videos(self, path, since=None, camera="all", stop=10, debug=False):
camera='all', stop=10, debug=False):
""" """
Download all videos from server since specified time. Download all videos from server since specified time.
@@ -310,7 +293,7 @@ class Blink():
response = api.request_videos(self, time=since_epochs, page=page) response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page) _LOGGER.debug("Processing page %s", page)
try: try:
result = response['media'] result = response["media"]
if not result: if not result:
raise IndexError raise IndexError
except (KeyError, IndexError): except (KeyError, IndexError):
@@ -323,22 +306,20 @@ class Blink():
"""Parse downloaded videos.""" """Parse downloaded videos."""
for item in result: for item in result:
try: try:
created_at = item['created_at'] created_at = item["created_at"]
camera_name = item['device_name'] camera_name = item["device_name"]
is_deleted = item['deleted'] is_deleted = item["deleted"]
address = item['media'] address = item["media"]
except KeyError: except KeyError:
_LOGGER.info("Missing clip information, skipping...") _LOGGER.info("Missing clip information, skipping...")
continue 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) _LOGGER.debug("Skipping videos for %s.", camera_name)
continue continue
if is_deleted: if is_deleted:
_LOGGER.debug("%s: %s is marked as deleted.", _LOGGER.debug("%s: %s is marked as deleted.", camera_name, address)
camera_name,
address)
continue continue
clip_address = "{}{}".format(self.urls.base_url, address) clip_address = "{}{}".format(self.urls.base_url, address)
@@ -351,13 +332,14 @@ class Blink():
_LOGGER.info("%s already exists, skipping...", filename) _LOGGER.info("%s already exists, skipping...", filename)
continue continue
response = api.http_get(self, url=clip_address, response = api.http_get(self, url=clip_address, stream=True, json=False)
stream=True, json=False) with open(filename, "wb") as vidfile:
with open(filename, 'wb') as vidfile:
copyfileobj(response.raw, vidfile) copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename) _LOGGER.info("Downloaded video to %s", filename)
else: else:
print(("Camera: {}, Timestamp: {}, " print(
"Address: {}, Filename: {}").format( ("Camera: {}, Timestamp: {}, " "Address: {}, Filename: {}").format(
camera_name, created_at, address, filename)) camera_name, created_at, address, filename
)
)
+58 -64
View File
@@ -7,7 +7,7 @@ from blinkpy import api
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class BlinkCamera(): class BlinkCamera:
"""Class to initialize individual camera.""" """Class to initialize individual camera."""
def __init__(self, sync): def __init__(self, sync):
@@ -34,22 +34,22 @@ class BlinkCamera():
def attributes(self): def attributes(self):
"""Return dictionary of all camera attributes.""" """Return dictionary of all camera attributes."""
attributes = { attributes = {
'name': self.name, "name": self.name,
'camera_id': self.camera_id, "camera_id": self.camera_id,
'serial': self.serial, "serial": self.serial,
'temperature': self.temperature, "temperature": self.temperature,
'temperature_c': self.temperature_c, "temperature_c": self.temperature_c,
'temperature_calibrated': self.temperature_calibrated, "temperature_calibrated": self.temperature_calibrated,
'battery': self.battery, "battery": self.battery,
'battery_voltage': self.battery_voltage, "battery_voltage": self.battery_voltage,
'thumbnail': self.thumbnail, "thumbnail": self.thumbnail,
'video': self.clip, "video": self.clip,
'motion_enabled': self.motion_enabled, "motion_enabled": self.motion_enabled,
'motion_detected': self.motion_detected, "motion_detected": self.motion_detected,
'wifi_strength': self.wifi_strength, "wifi_strength": self.wifi_strength,
'network_id': self.sync.network_id, "network_id": self.sync.network_id,
'sync_module': self.sync.name, "sync_module": self.sync.name,
'last_record': self.last_record "last_record": self.last_record,
} }
return attributes return attributes
@@ -79,39 +79,37 @@ class BlinkCamera():
def snap_picture(self): def snap_picture(self):
"""Take a picture with camera to create a new thumbnail.""" """Take a picture with camera to create a new thumbnail."""
return api.request_new_image(self.sync.blink, return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
self.network_id,
self.camera_id)
def set_motion_detect(self, enable): def set_motion_detect(self, enable):
"""Set motion detection.""" """Set motion detection."""
if enable: if enable:
return api.request_motion_detection_enable(self.sync.blink, return api.request_motion_detection_enable(
self.network_id, self.sync.blink, self.network_id, self.camera_id
self.camera_id) )
return api.request_motion_detection_disable(self.sync.blink, return api.request_motion_detection_disable(
self.network_id, self.sync.blink, self.network_id, self.camera_id
self.camera_id) )
def update(self, config, force_cache=False, **kwargs): def update(self, config, force_cache=False, **kwargs):
"""Update camera info.""" """Update camera info."""
# force = kwargs.pop('force', False) # force = kwargs.pop('force', False)
self.name = config['name'] self.name = config["name"]
self.camera_id = str(config['id']) self.camera_id = str(config["id"])
self.network_id = str(config['network_id']) self.network_id = str(config["network_id"])
self.serial = config['serial'] self.serial = config["serial"]
self.motion_enabled = config['enabled'] self.motion_enabled = config["enabled"]
self.battery_voltage = config['battery_voltage'] self.battery_voltage = config["battery_voltage"]
self.battery_state = config['battery_state'] self.battery_state = config["battery_state"]
self.temperature = config['temperature'] self.temperature = config["temperature"]
self.wifi_strength = config['wifi_strength'] self.wifi_strength = config["wifi_strength"]
# Retrieve calibrated temperature from special endpoint # Retrieve calibrated temperature from special endpoint
resp = api.request_camera_sensors(self.sync.blink, resp = api.request_camera_sensors(
self.network_id, self.sync.blink, self.network_id, self.camera_id
self.camera_id) )
try: try:
self.temperature_calibrated = resp['temp'] self.temperature_calibrated = resp["temp"]
except KeyError: except KeyError:
self.temperature_calibrated = self.temperature self.temperature_calibrated = self.temperature
_LOGGER.warning("Could not retrieve calibrated temperature.") _LOGGER.warning("Could not retrieve calibrated temperature.")
@@ -121,16 +119,15 @@ class BlinkCamera():
# otherwise set it to None and log an error # otherwise set it to None and log an error
new_thumbnail = None new_thumbnail = None
thumb_addr = None thumb_addr = None
if config['thumbnail']: if config["thumbnail"]:
thumb_addr = config['thumbnail'] thumb_addr = config["thumbnail"]
else: else:
_LOGGER.warning("Could not find thumbnail for camera %s", _LOGGER.warning(
self.name, "Could not find thumbnail for camera %s", self.name, exc_info=True
exc_info=True) )
if thumb_addr is not None: if thumb_addr is not None:
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, thumb_addr)
thumb_addr)
try: try:
self.motion_detected = self.sync.motion[self.name] self.motion_detected = self.sync.motion[self.name]
@@ -139,10 +136,9 @@ class BlinkCamera():
clip_addr = None clip_addr = None
if self.name in self.sync.last_record: if self.name in self.sync.last_record:
clip_addr = self.sync.last_record[self.name]['clip'] clip_addr = self.sync.last_record[self.name]["clip"]
self.last_record = self.sync.last_record[self.name]['time'] self.last_record = self.sync.last_record[self.name]["time"]
self.clip = "{}{}".format(self.sync.urls.base_url, self.clip = "{}{}".format(self.sync.urls.base_url, clip_addr)
clip_addr)
# If the thumbnail or clip have changed, update the cache # If the thumbnail or clip have changed, update the cache
update_cached_image = False update_cached_image = False
@@ -155,15 +151,13 @@ class BlinkCamera():
update_cached_video = True update_cached_video = True
if new_thumbnail is not None and (update_cached_image or force_cache): if new_thumbnail is not None and (update_cached_image or force_cache):
self._cached_image = api.http_get(self.sync.blink, self._cached_image = api.http_get(
url=self.thumbnail, self.sync.blink, url=self.thumbnail, stream=True, json=False
stream=True, )
json=False)
if clip_addr is not None and (update_cached_video or force_cache): if clip_addr is not None and (update_cached_video or force_cache):
self._cached_video = api.http_get(self.sync.blink, self._cached_video = api.http_get(
url=self.clip, self.sync.blink, url=self.clip, stream=True, json=False
stream=True, )
json=False)
def image_to_file(self, path): def image_to_file(self, path):
""" """
@@ -174,11 +168,12 @@ class BlinkCamera():
_LOGGER.debug("Writing image from %s to %s", self.name, path) _LOGGER.debug("Writing image from %s to %s", self.name, path)
response = self._cached_image response = self._cached_image
if response.status_code == 200: if response.status_code == 200:
with open(path, 'wb') as imgfile: with open(path, "wb") as imgfile:
copyfileobj(response.raw, imgfile) copyfileobj(response.raw, imgfile)
else: else:
_LOGGER.error("Cannot write image to file, response %s", _LOGGER.error(
response.status_code) "Cannot write image to file, response %s", response.status_code
)
def video_to_file(self, path): def video_to_file(self, path):
"""Write video to file. """Write video to file.
@@ -188,8 +183,7 @@ class BlinkCamera():
_LOGGER.debug("Writing video from %s to %s", self.name, path) _LOGGER.debug("Writing video from %s to %s", self.name, path)
response = self._cached_video response = self._cached_video
if response is None: if response is None:
_LOGGER.error("No saved video exist for %s.", _LOGGER.error("No saved video exist for %s.", self.name)
self.name)
return return
with open(path, 'wb') as vidfile: with open(path, "wb") as vidfile:
copyfileobj(response.raw, vidfile) copyfileobj(response.raw, vidfile)
+47 -44
View File
@@ -3,65 +3,68 @@
import os import os
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 14 MINOR_VERSION = 15
PATCH_VERSION = '3.dev1' PATCH_VERSION = 0
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) __version__ = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3) REQUIRED_PYTHON_VER = (3, 5, 3)
PROJECT_NAME = 'blinkpy' PROJECT_NAME = "blinkpy"
PROJECT_PACKAGE_NAME = 'blinkpy' PROJECT_PACKAGE_NAME = "blinkpy"
PROJECT_LICENSE = 'MIT' PROJECT_LICENSE = "MIT"
PROJECT_AUTHOR = 'Kevin Fronczak' PROJECT_AUTHOR = "Kevin Fronczak"
PROJECT_COPYRIGHT = ' 2017, {}'.format(PROJECT_AUTHOR) PROJECT_COPYRIGHT = " 2017, {}".format(PROJECT_AUTHOR)
PROJECT_URL = 'https://github.com/fronzbot/blinkpy' PROJECT_URL = "https://github.com/fronzbot/blinkpy"
PROJECT_EMAIL = 'kfronczak@gmail.com' PROJECT_EMAIL = "kfronczak@gmail.com"
PROJECT_DESCRIPTION = ('A Blink camera Python library ' PROJECT_DESCRIPTION = "A Blink camera Python library " "running on Python 3."
'running on Python 3.') PROJECT_LONG_DESCRIPTION = (
PROJECT_LONG_DESCRIPTION = ('blinkpy is an open-source ' "blinkpy is an open-source "
'unofficial API for the Blink Camera ' "unofficial API for the Blink Camera "
'system with the intention for easy ' "system with the intention for easy "
'integration into various home ' "integration into various home "
'automation platforms.') "automation platforms."
if os.path.exists('README.rst'): )
PROJECT_LONG_DESCRIPTION = open('README.rst').read() if os.path.exists("README.rst"):
PROJECT_LONG_DESCRIPTION = open("README.rst").read()
PROJECT_CLASSIFIERS = [ PROJECT_CLASSIFIERS = [
'Intended Audience :: Developers', "Intended Audience :: Developers",
'License :: OSI Approved :: MIT License', "License :: OSI Approved :: MIT License",
'Operating System :: OS Independent', "Operating System :: OS Independent",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.7', "Programming Language :: Python :: 3.8",
'Programming Language :: Python :: 3.8', "Topic :: Home Automation",
'Topic :: Home Automation'
] ]
PROJECT_GITHUB_USERNAME = 'fronzbot' PROJECT_GITHUB_USERNAME = "fronzbot"
PROJECT_GITHUB_REPOSITORY = 'blinkpy' 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 URLS
''' """
BLINK_URL = 'immedia-semi.com' BLINK_URL = "immedia-semi.com"
DEFAULT_URL = "{}.{}".format('rest-prod', BLINK_URL) DEFAULT_URL = "{}.{}".format("rest-prod", BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL) BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URL = "{}/api/v2/login".format(BASE_URL) LOGIN_URLS = [
OLD_LOGIN_URL = "{}/login".format(BASE_URL) "{}/api/v4/login".format(BASE_URL),
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest-piri', BLINK_URL) "{}/api/v3/login".format(BASE_URL),
"{}/api/v2/login".format(BASE_URL),
]
''' """
Dictionaries Dictionaries
''' """
ONLINE = {'online': True, 'offline': False} ONLINE = {"online": True, "offline": False}
''' """
OTHER OTHER
''' """
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%z' TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
DEFAULT_MOTION_INTERVAL = 1 DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30 DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2 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") PASSWORD = (1, "Password must be a string")
AUTHENTICATE = ( AUTHENTICATE = (
2, 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 = ( AUTH_TOKEN = (
3, 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)") REQUEST = (4, "Cannot perform request (get/post type incorrect)")
+56 -26
View File
@@ -2,6 +2,7 @@
import logging import logging
import time import time
import secrets
from calendar import timegm from calendar import timegm
from functools import partial, wraps from functools import partial, wraps
from requests import Request, Session, exceptions from requests import Request, Session, exceptions
@@ -13,13 +14,18 @@ import blinkpy.helpers.errors as ERROR
_LOGGER = logging.getLogger(__name__) _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): def time_to_seconds(timestamp):
"""Convert TIMESTAMP_FORMAT time to seconds.""" """Convert TIMESTAMP_FORMAT time to seconds."""
try: try:
dtime = dateutil.parser.isoparse(timestamp) dtime = dateutil.parser.isoparse(timestamp)
except ValueError: except ValueError:
_LOGGER.error("Incorrect timestamp format for conversion: %s.", _LOGGER.error("Incorrect timestamp format for conversion: %s.", timestamp)
timestamp)
return False return False
return timegm(dtime.timetuple()) return timegm(dtime.timetuple())
@@ -35,8 +41,10 @@ def merge_dicts(dict_a, dict_b):
"""Merge two dictionaries into one.""" """Merge two dictionaries into one."""
duplicates = [val for val in dict_a if val in dict_b] duplicates = [val for val in dict_a if val in dict_b]
if duplicates: if duplicates:
_LOGGER.warning(("Duplicates found during merge: %s. " _LOGGER.warning(
"Renaming is recommended."), duplicates) ("Duplicates found during merge: %s. " "Renaming is recommended."),
duplicates,
)
return {**dict_a, **dict_b} return {**dict_a, **dict_b}
@@ -59,8 +67,16 @@ def attempt_reauthorization(blink):
return headers return headers
def http_req(blink, url='http://example.com', data=None, headers=None, def http_req(
reqtype='get', stream=False, json_resp=True, is_retry=False): 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. 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 json_resp: Return JSON response? TRUE/False
:param is_retry: Is this a retry attempt? True/FALSE :param is_retry: Is this a retry attempt? True/FALSE
""" """
if reqtype == 'post': if reqtype == "post":
req = Request('POST', url, headers=headers, data=data) req = Request("POST", url, headers=headers, data=data)
elif reqtype == 'get': elif reqtype == "get":
req = Request('GET', url, headers=headers) req = Request("GET", url, headers=headers)
else: else:
_LOGGER.error("Invalid request type: %s", reqtype) _LOGGER.error("Invalid request type: %s", reqtype)
raise BlinkException(ERROR.REQUEST) raise BlinkException(ERROR.REQUEST)
@@ -85,10 +101,10 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
try: try:
response = blink.session.send(prepped, stream=stream) 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() resp_dict = response.json()
code = resp_dict['code'] code = resp_dict["code"]
message = resp_dict['message'] message = resp_dict["message"]
if is_retry and code in ERROR.BLINK_ERRORS: if is_retry and code in ERROR.BLINK_ERRORS:
_LOGGER.error("Cannot obtain new token for server auth.") _LOGGER.error("Cannot obtain new token for server auth.")
return None return None
@@ -96,20 +112,33 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
headers = attempt_reauthorization(blink) headers = attempt_reauthorization(blink)
if not headers: if not headers:
raise exceptions.ConnectionError raise exceptions.ConnectionError
return http_req(blink, url=url, data=data, headers=headers, return http_req(
reqtype=reqtype, stream=stream, blink,
json_resp=json_resp, is_retry=True) 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) _LOGGER.warning("Response from server: %s - %s", code, message)
except (exceptions.ConnectionError, exceptions.Timeout): except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.info("Cannot connect to server with url %s.", url) _LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry: if not is_retry:
headers = attempt_reauthorization(blink) headers = attempt_reauthorization(blink)
return http_req(blink, url=url, data=data, headers=headers, return http_req(
reqtype=reqtype, stream=stream, blink,
json_resp=json_resp, is_retry=True) url=url,
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.", data=data,
url) 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 return None
if json_resp: if json_resp:
@@ -132,14 +161,14 @@ class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception.""" """Class to throw authentication exception."""
class BlinkURLHandler(): class BlinkURLHandler:
"""Class that handles Blink URLS.""" """Class that handles Blink URLS."""
def __init__(self, region_id, legacy=False): def __init__(self, region_id, legacy=False):
"""Initialize the urls.""" """Initialize the urls."""
self.subdomain = 'rest-{}'.format(region_id) self.subdomain = "rest-{}".format(region_id)
if legacy: if legacy:
self.subdomain = 'rest.{}'.format(region_id) self.subdomain = "rest.{}".format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL) self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url) self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".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) _LOGGER.debug("Setting base url to %s.", self.base_url)
class Throttle(): class Throttle:
"""Class for throttling api calls.""" """Class for throttling api calls."""
def __init__(self, seconds=10): def __init__(self, seconds=10):
@@ -159,6 +188,7 @@ class Throttle():
def __call__(self, method): def __call__(self, method):
"""Throttle caller method.""" """Throttle caller method."""
def throttle_method(): def throttle_method():
"""Call when method is throttled.""" """Call when method is throttled."""
return None return None
@@ -166,7 +196,7 @@ class Throttle():
@wraps(method) @wraps(method)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
"""Wrap that checks for throttling.""" """Wrap that checks for throttling."""
force = kwargs.pop('force', False) force = kwargs.pop("force", False)
now = int(time.time()) now = int(time.time())
last_call_delta = now - self.last_call last_call_delta = now - self.last_call
if force or last_call_delta > self.throttle_time: if force or last_call_delta > self.throttle_time:
+154
View File
@@ -0,0 +1,154 @@
"""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,
device_id="Blinkpy",
):
"""
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.
:param device_id: Name of application to send at login.
"""
self.login_url = None
self.login_urls = const.LOGIN_URLS
self.cred_file = cred_file
self.persist_key = persist_key
self.device_id = device_id
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,
device_id=self.device_id,
)
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__) _LOGGER = logging.getLogger(__name__)
class BlinkSyncModule(): class BlinkSyncModule:
"""Class to initialize sync module.""" """Class to initialize sync module."""
def __init__(self, blink, network_name, network_id, camera_list): def __init__(self, blink, network_name, network_id, camera_list):
@@ -38,18 +38,19 @@ class BlinkSyncModule():
self.motion = {} self.motion = {}
self.last_record = {} self.last_record = {}
self.camera_list = camera_list self.camera_list = camera_list
self.available = False
@property @property
def attributes(self): def attributes(self):
"""Return sync attributes.""" """Return sync attributes."""
attr = { attr = {
'name': self.name, "name": self.name,
'id': self.sync_id, "id": self.sync_id,
'network_id': self.network_id, "network_id": self.network_id,
'serial': self.serial, "serial": self.serial,
'status': self.status, "status": self.status,
'region': self.region, "region": self.region,
'region_id': self.region_id, "region_id": self.region_id,
} }
return attr return attr
@@ -67,7 +68,7 @@ class BlinkSyncModule():
def arm(self): def arm(self):
"""Return status of sync module: armed/disarmed.""" """Return status of sync module: armed/disarmed."""
try: try:
return self.network_info['network']['armed'] return self.network_info["network"]["armed"]
except (KeyError, TypeError): except (KeyError, TypeError):
return None return None
@@ -81,85 +82,86 @@ class BlinkSyncModule():
def start(self): def start(self):
"""Initialize the system.""" """Initialize the system."""
response = api.request_syncmodule(self.blink, response = api.request_syncmodule(self.blink, self.network_id)
self.network_id)
try: try:
self.summary = response['syncmodule'] self.summary = response["syncmodule"]
self.network_id = self.summary['network_id'] self.network_id = self.summary["network_id"]
except (TypeError, KeyError): except (TypeError, KeyError):
_LOGGER.error(("Could not retrieve sync module information " _LOGGER.error(
"with response: %s"), response, exc_info=True) ("Could not retrieve sync module information " "with response: %s"),
response,
exc_info=True,
)
return False return False
try: try:
self.sync_id = self.summary['id'] self.sync_id = self.summary["id"]
self.serial = self.summary['serial'] self.serial = self.summary["serial"]
self.status = self.summary['status'] self.status = self.summary["status"]
except KeyError: except KeyError:
_LOGGER.error("Could not extract some sync module info: %s", _LOGGER.error(
response, "Could not extract some sync module info: %s", response, exc_info=True
exc_info=True) )
self.network_info = api.request_network_status(self.blink,
self.network_id)
self.get_network_info()
self.check_new_videos() self.check_new_videos()
try: try:
for camera_config in self.camera_list: for camera_config in self.camera_list:
if 'name' not in camera_config: if "name" not in camera_config:
break break
name = camera_config['name'] name = camera_config["name"]
self.cameras[name] = BlinkCamera(self) self.cameras[name] = BlinkCamera(self)
self.motion[name] = False self.motion[name] = False
camera_info = self.get_camera_info(camera_config['id']) camera_info = self.get_camera_info(camera_config["id"])
self.cameras[name].update(camera_info, self.cameras[name].update(camera_info, force_cache=True, force=True)
force_cache=True,
force=True)
except KeyError: except KeyError:
_LOGGER.error("Could not create cameras instances for %s", _LOGGER.error(
self.name, "Could not create cameras instances for %s", self.name, exc_info=True
exc_info=True) )
return False return False
self.available = True
return True return True
def get_events(self, **kwargs): def get_events(self, **kwargs):
"""Retrieve events from server.""" """Retrieve events from server."""
force = kwargs.pop('force', False) force = kwargs.pop("force", False)
response = api.request_sync_events(self.blink, response = api.request_sync_events(self.blink, self.network_id, force=force)
self.network_id,
force=force)
try: try:
return response['event'] return response["event"]
except (TypeError, KeyError): except (TypeError, KeyError):
_LOGGER.error("Could not extract events: %s", _LOGGER.error("Could not extract events: %s", response, exc_info=True)
response,
exc_info=True)
return False return False
def get_camera_info(self, camera_id): def get_camera_info(self, camera_id):
"""Retrieve camera information.""" """Retrieve camera information."""
response = api.request_camera_info(self.blink, response = api.request_camera_info(self.blink, self.network_id, camera_id)
self.network_id,
camera_id)
try: try:
return response['camera'][0] return response["camera"][0]
except (TypeError, KeyError): except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s", _LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
response,
exc_info=True)
return [] 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): def refresh(self, force_cache=False):
"""Get all blink cameras and pulls their most recent status.""" """Get all blink cameras and pulls their most recent status."""
self.network_info = api.request_network_status(self.blink, self.get_network_info()
self.network_id)
self.check_new_videos() self.check_new_videos()
for camera_name in self.cameras.keys(): for camera_name in self.cameras.keys():
camera_id = self.cameras[camera_name].camera_id camera_id = self.cameras[camera_name].camera_id
camera_info = self.get_camera_info(camera_id) camera_info = self.get_camera_info(camera_id)
self.cameras[camera_name].update(camera_info, self.cameras[camera_name].update(camera_info, force_cache=force_cache)
force_cache=force_cache)
def check_new_videos(self): def check_new_videos(self):
"""Check if new videos since last refresh.""" """Check if new videos since last refresh."""
@@ -170,27 +172,25 @@ class BlinkSyncModule():
# No need to check for motion. # No need to check for motion.
return False return False
resp = api.request_videos(self.blink, resp = api.request_videos(self.blink, time=interval, page=1)
time=interval,
page=1)
for camera in self.cameras.keys(): for camera in self.cameras.keys():
self.motion[camera] = False self.motion[camera] = False
try: try:
info = resp['media'] info = resp["media"]
except (KeyError, TypeError): except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp) _LOGGER.warning("Could not check for motion. Response: %s", resp)
return False return False
for entry in info: for entry in info:
try: try:
name = entry['device_name'] name = entry["device_name"]
clip = entry['media'] clip = entry["media"]
timestamp = entry['created_at'] timestamp = entry["created_at"]
if self.check_new_video_time(timestamp): if self.check_new_video_time(timestamp):
self.motion[name] = True and self.arm 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: except KeyError:
_LOGGER.debug("No new videos since last refresh.") _LOGGER.debug("No new videos since last refresh.")
+1 -1
View File
@@ -4,7 +4,7 @@ coverage:
status: status:
project: project:
default: default:
threshold: 0.02 threshold: 2%
comment: true comment: true
require_ci_to_pass: yes 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 # unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
disable= disable=
format,
bad-continuation,
locally-disabled, locally-disabled,
unused-argument, unused-argument,
duplicate-code, duplicate-code,
implicit-str-concat,
too-many-arguments, too-many-arguments,
too-many-branches, too-many-branches,
too-many-instance-attributes, too-many-instance-attributes,
+4 -4
View File
@@ -1,4 +1,4 @@
python-dateutil~=2.8.0 python-dateutil~=2.8.1
requests==2.22.0 requests~=2.23.0
python-slugify~=3.0.2 python-slugify~=4.0.0
testtools==2.3.0 testtools==2.4.0
+8 -7
View File
@@ -1,11 +1,12 @@
coverage==4.5.4 black==19.10b0
coverage==5.1
flake8==3.7.9 flake8==3.7.9
flake8-docstrings==1.5.0 flake8-docstrings==1.5.0
pylint==2.4.4 pylint==2.5.2
pydocstyle==5.0.2 pydocstyle==5.0.2
pytest==5.3.5 pytest==5.4.1
pytest-cov==2.8.1 pytest-cov==2.8.1
pytest-sugar>=0.9.2 pytest-sugar==0.9.3
pytest-timeout>=1.3.3 pytest-timeout==1.3.4
restructuredtext-lint>=1.0.1 restructuredtext-lint==1.3.0
pygments>=2.2.0 pygments==2.6.1
+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 -*- # -*- coding: utf-8 -*-
from os.path import abspath, dirname, join from os.path import abspath, dirname
from setuptools import setup, find_packages 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 ( from blinkpy.helpers.constants import (
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL, __version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR, 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) THIS_DIR = abspath(dirname(__file__))
test_reqs = parse_requirements('{}/requirements_test.txt'.format(this_dir), session=False)
REQUIRES = [str(ir.req) for ir in install_reqs] REQUIRES = [
TEST_REQUIRES = [str(ir.req) for ir in test_reqs] "python-dateutil~=2.8.1",
"requests~=2.23.0",
"python-slugify~=4.0.0",
]
PACKAGES = find_packages(exclude=['tests*', 'docs']) PACKAGES = find_packages(exclude=['tests*', 'docs'])
with open('{}/README.rst'.format(this_dir), encoding='utf-8') as readme_file: with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
long_description = readme_file.read() LONG_DESCRIPTION = readme_file.read()
name = PROJECT_PACKAGE_NAME
version = __version__
setup( setup(
name = PROJECT_PACKAGE_NAME, name=PROJECT_PACKAGE_NAME,
version = __version__, version=PROJECT_VERSION,
description = PROJECT_DESCRIPTION, description=PROJECT_DESCRIPTION,
long_description = long_description, long_description=LONG_DESCRIPTION,
author = PROJECT_AUTHOR, author=PROJECT_AUTHOR,
author_email = PROJECT_EMAIL, author_email=PROJECT_EMAIL,
license = PROJECT_LICENSE, license=PROJECT_LICENSE,
url = PROJECT_URL, url=PROJECT_URL,
platforms = 'any', platforms='any',
py_modules = ['blinkpy'], py_modules=['blinkpy'],
packages=PACKAGES, packages=PACKAGES,
include_package_data = True, include_package_data=True,
install_requires = REQUIRES, install_requires=REQUIRES,
test_suite = 'tests', test_suite='tests',
classifiers = PROJECT_CLASSIFIERS classifiers=PROJECT_CLASSIFIERS
) )
+19 -19
View File
@@ -4,11 +4,11 @@ from blinkpy.helpers.util import BlinkURLHandler
import blinkpy.helpers.constants as const import blinkpy.helpers.constants as const
LOGIN_RESPONSE = { LOGIN_RESPONSE = {
'region': {'mock': 'Test'}, "region": {"mock": "Test"},
'networks': { "networks": {"1234": {"name": "test", "onboarded": True}},
'1234': {'name': 'test', 'onboarded': True} "authtoken": {"authtoken": "foobar123", "message": "auth"},
}, "client": {"id": "5678"},
'authtoken': {'authtoken': 'foobar123', 'message': 'auth'} "account": {"id": "1337"},
} }
@@ -37,29 +37,29 @@ def mocked_session_send(*args, **kwargs):
url = prepped.url url = prepped.url
header = prepped.headers header = prepped.headers
method = prepped.method method = prepped.method
if method == 'GET': if method == "GET":
expected_token = LOGIN_RESPONSE['authtoken']['authtoken'] expected_token = LOGIN_RESPONSE["authtoken"]["authtoken"]
if header['TOKEN_AUTH'] != expected_token: if header["TOKEN_AUTH"] != expected_token:
response = {'message': 'Not Authorized', 'code': 400} response = {"message": "Not Authorized", "code": 400}
status = 400 status = 400
elif url == 'use_bad_response': elif url == "use_bad_response":
response = {'foo': 'bar'} response = {"foo": "bar"}
status = 200 status = 200
elif url == 'reauth': elif url == "reauth":
response = {'message': 'REAUTH', 'code': 777} response = {"message": "REAUTH", "code": 777}
status = 777 status = 777
else: else:
response = {'test': 'foo'} response = {"test": "foo"}
status = 200 status = 200
elif method == 'POST': elif method == "POST":
if url in (const.LOGIN_URL, const.LOGIN_BACKUP_URL): if url in const.LOGIN_URLS:
response = LOGIN_RESPONSE response = LOGIN_RESPONSE
status = 200 status = 200
elif url == 'http://wrong.url/' or url is None: elif url == "http://wrong.url/" or url is None:
response = {'message': 'Error', 'code': 404} response = {"message": "Error", "code": 404}
status = 404 status = 404
else: else:
response = {'message': 'foo', 'code': 200} response = {"message": "foo", "code": 200}
status = 200 status = 200
return MockResponse(response, status) 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 from blinkpy.helpers.util import create_session, get_time
import tests.mock_responses as mresp import tests.mock_responses as mresp
USERNAME = 'foobar' USERNAME = "foobar"
PASSWORD = 'deadbeef' PASSWORD = "deadbeef"
class MockSyncModule(BlinkSyncModule): class MockSyncModule(BlinkSyncModule):
@@ -17,7 +17,7 @@ class MockSyncModule(BlinkSyncModule):
def __init__(self, blink, header): def __init__(self, blink, header):
"""Create mock sync module instance.""" """Create mock sync module instance."""
super().__init__(blink, header) super().__init__(blink, header, network_id=None, camera_list=None)
self.blink = blink self.blink = blink
self.header = header self.header = header
self.return_value = None self.return_value = None
@@ -34,69 +34,54 @@ class MockSyncModule(BlinkSyncModule):
return self.return_value return self.return_value
@mock.patch('blinkpy.helpers.util.Session.send', @mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
side_effect=mresp.mocked_session_send)
class TestBlinkFunctions(unittest.TestCase): class TestBlinkFunctions(unittest.TestCase):
"""Test Blink and BlinkCamera functions in blinkpy.""" """Test Blink and BlinkCamera functions in blinkpy."""
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME, self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
password=PASSWORD)
# pylint: disable=protected-access # pylint: disable=protected-access
self.blink._auth_header = { self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
'Host': 'test.url.tld', self.blink.urls = blinkpy.BlinkURLHandler("test")
'TOKEN_AUTH': 'foobar123'
}
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.session = create_session() self.blink.session = create_session()
def tearDown(self): def tearDown(self):
"""Clean up after test.""" """Clean up after test."""
self.blink = None 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): def test_backup_url(self, req, mock_sess):
"""Test backup login method.""" """Test backup login method."""
json_resp = { json_resp = {
'authtoken': {'authtoken': 'foobar123'}, "authtoken": {"authtoken": "foobar123"},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}} "networks": {"1234": {"name": "foobar", "onboarded": True}},
} }
bad_req = mresp.MockResponse({}, 404) bad_req = mresp.MockResponse({}, 404)
new_req = mresp.MockResponse(json_resp, 200) new_req = mresp.MockResponse(json_resp, 200)
req.side_effect = [ req.side_effect = [bad_req, bad_req, new_req]
bad_req, self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
bad_req, self.blink.login_handler.login(self.blink)
new_req self.assertEqual(self.blink.login_handler.login_url, "test3")
]
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 = [ req.side_effect = [bad_req, new_req, bad_req]
bad_req, self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
new_req, self.blink.login_handler.login(self.blink)
bad_req self.assertEqual(self.blink.login_handler.login_url, "test2")
]
self.blink.login_urls = ['test1', 'test2', 'test3']
self.blink.login_request()
# pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test2')
def test_merge_cameras(self, mock_sess): def test_merge_cameras(self, mock_sess):
"""Test merge camera functionality.""" """Test merge camera functionality."""
first_dict = {'foo': 'bar', 'test': 123} first_dict = {"foo": "bar", "test": 123}
next_dict = {'foobar': 456, 'bar': 'foo'} next_dict = {"foobar": 456, "bar": "foo"}
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, []) self.blink.sync["foo"] = BlinkSyncModule(self.blink, "foo", 1, [])
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, []) self.blink.sync["bar"] = BlinkSyncModule(self.blink, "bar", 2, [])
self.blink.sync['foo'].cameras = first_dict self.blink.sync["foo"].cameras = first_dict
self.blink.sync['bar'].cameras = next_dict self.blink.sync["bar"].cameras = next_dict
result = self.blink.merge_cameras() 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) 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): def test_download_video_exit(self, mock_req, mock_sess):
"""Test we exit method when provided bad response.""" """Test we exit method when provided bad response."""
blink = blinkpy.Blink() blink = blinkpy.Blink()
@@ -106,63 +91,60 @@ class TestBlinkFunctions(unittest.TestCase):
mock_req.return_value = {} mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh) formatted_date = get_time(blink.last_refresh)
expected_log = [ expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format( "INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1", "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: with self.assertLogs() as dl_log:
blink.download_videos('/tmp') blink.download_videos("/tmp")
self.assertEqual(dl_log.output, expected_log) 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): def test_parse_downloaded_items(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list.""" """Test ability to parse downloaded items list."""
blink = blinkpy.Blink() blink = blinkpy.Blink()
# pylint: disable=protected-access # pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG) blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = { generic_entry = {
'created_at': '1970', "created_at": "1970",
'device_name': 'foo', "device_name": "foo",
'deleted': True, "deleted": True,
'media': '/bar.mp4' "media": "/bar.mp4",
} }
result = [generic_entry] result = [generic_entry]
mock_req.return_value = {'media': result} mock_req.return_value = {"media": result}
blink.last_refresh = 0 blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh) formatted_date = get_time(blink.last_refresh)
expected_log = [ expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format( "INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1", "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: 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) 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): def test_parse_camera_not_in_list(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list.""" """Test ability to parse downloaded items list."""
blink = blinkpy.Blink() blink = blinkpy.Blink()
# pylint: disable=protected-access # pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG) blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = { generic_entry = {
'created_at': '1970', "created_at": "1970",
'device_name': 'foo', "device_name": "foo",
'deleted': True, "deleted": True,
'media': '/bar.mp4' "media": "/bar.mp4",
} }
result = [generic_entry] result = [generic_entry]
mock_req.return_value = {'media': result} mock_req.return_value = {"media": result}
blink.last_refresh = 0 blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh) formatted_date = get_time(blink.last_refresh)
expected_log = [ expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format( "INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1", "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: 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) 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 import api
from blinkpy.blinkpy import Blink from blinkpy.blinkpy import Blink
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
from blinkpy.login_handler import LoginHandler
from blinkpy.helpers.util import ( from blinkpy.helpers.util import (
http_req, create_session, BlinkAuthenticationException, http_req,
BlinkException, BlinkURLHandler) create_session,
BlinkException,
BlinkURLHandler,
)
from blinkpy.helpers.constants import __version__ from blinkpy.helpers.constants import __version__
import tests.mock_responses as mresp import tests.mock_responses as mresp
USERNAME = 'foobar' USERNAME = "foobar"
PASSWORD = 'deadbeef' PASSWORD = "deadbeef"
@mock.patch('blinkpy.helpers.util.Session.send', @mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
side_effect=mresp.mocked_session_send)
class TestBlinkSetup(unittest.TestCase): class TestBlinkSetup(unittest.TestCase):
"""Test the Blink class in blinkpy.""" """Test the Blink class in blinkpy."""
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink_no_cred = Blink() self.blink = Blink(username=USERNAME, password=PASSWORD)
self.blink = Blink(username=USERNAME, self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
password=PASSWORD) self.blink.urls = BlinkURLHandler("test")
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.blink.urls = BlinkURLHandler('test')
self.blink.session = create_session() self.blink.session = create_session()
def tearDown(self): def tearDown(self):
"""Clean up after test.""" """Clean up after test."""
self.blink = None self.blink = None
self.blink_no_cred = None
def test_initialization(self, mock_sess): def test_initialization(self, mock_sess):
"""Verify we can initialize blink.""" """Verify we can initialize blink."""
self.assertEqual(self.blink.version, __version__) self.assertEqual(self.blink.version, __version__)
# pylint: disable=protected-access self.assertEqual(self.blink.login_handler.data["username"], USERNAME)
self.assertEqual(self.blink._username, USERNAME) self.assertEqual(self.blink.login_handler.data["password"], PASSWORD)
# 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')
def test_bad_request(self, mock_sess): def test_bad_request(self, mock_sess):
"""Check that we raise an Exception with a bad request.""" """Check that we raise an Exception with a bad request."""
self.blink.session = create_session() self.blink.session = create_session()
explog = ("WARNING:blinkpy.helpers.util:" explog = "WARNING:blinkpy.helpers.util:" "Response from server: 200 - foo"
"Response from server: 200 - foo")
with self.assertRaises(BlinkException): with self.assertRaises(BlinkException):
http_req(self.blink, reqtype='bad') http_req(self.blink, reqtype="bad")
with self.assertLogs() as logrecord: 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]) self.assertEqual(logrecord.output, [explog])
def test_authentication(self, mock_sess): def test_authentication(self, mock_sess):
"""Check that we can authenticate Blink up properly.""" """Check that we can authenticate Blink up properly."""
authtoken = self.blink.get_auth_token()['TOKEN_AUTH'] authtoken = self.blink.get_auth_token()["TOKEN_AUTH"]
expected = mresp.LOGIN_RESPONSE['authtoken']['authtoken'] expected = mresp.LOGIN_RESPONSE["authtoken"]["authtoken"]
self.assertEqual(authtoken, expected) self.assertEqual(authtoken, expected)
def test_reauthorization_attempt(self, mock_sess): def test_reauthorization_attempt(self, mock_sess):
"""Check that we can reauthorize after first unsuccessful attempt.""" """Check that we can reauthorize after first unsuccessful attempt."""
original_header = self.blink.get_auth_token() original_header = self.blink.get_auth_token()
# pylint: disable=protected-access # 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 # pylint: disable=protected-access
self.blink._auth_header = bad_header self.blink._auth_header = bad_header
self.assertEqual(self.blink.auth_header, bad_header) self.assertEqual(self.blink.auth_header, bad_header)
api.request_homescreen(self.blink) api.request_homescreen(self.blink)
self.assertEqual(self.blink.auth_header, original_header) self.assertEqual(self.blink.auth_header, original_header)
@mock.patch('blinkpy.api.request_networks') def test_multiple_networks(self, mock_sess):
def test_multiple_networks(self, mock_net, mock_sess):
"""Check that we handle multiple networks appropriately.""" """Check that we handle multiple networks appropriately."""
mock_net.return_value = { self.blink.networks = {
'networks': [{'id': 1234, 'account_id': 1111}, "0000": {"onboarded": False, "name": "foo"},
{'id': 5678, 'account_id': 2222}] "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.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids) self.assertTrue("5678" in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 2222)
@mock.patch('blinkpy.api.request_networks') def test_multiple_onboarded_networks(self, mock_sess):
def test_multiple_onboarded_networks(self, mock_net, mock_sess):
"""Check that we handle multiple networks appropriately.""" """Check that we handle multiple networks appropriately."""
mock_net.return_value = { self.blink.networks = {
'networks': [{'id': 0000, 'account_id': 2222}, "0000": {"onboarded": False, "name": "foo"},
{'id': 5678, 'account_id': 1111}] "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.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids) self.assertTrue("0000" not in self.blink.network_ids)
self.assertTrue('1234' in self.blink.network_ids) self.assertTrue("5678" in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 1111) 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): def test_throttle(self, mock_time, mock_sess):
"""Check throttling functionality.""" """Check throttling functionality."""
now = self.blink.refresh_rate + 1 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.last_refresh, None)
self.assertEqual(self.blink.check_if_ok_to_update(), True) self.assertEqual(self.blink.check_if_ok_to_update(), True)
self.assertEqual(self.blink.last_refresh, None) self.assertEqual(self.blink.last_refresh, None)
with mock.patch('blinkpy.sync_module.BlinkSyncModule.refresh', with mock.patch(
return_value=True): "blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
):
self.blink.refresh() self.blink.refresh()
self.assertEqual(self.blink.last_refresh, now) self.assertEqual(self.blink.last_refresh, now)
@@ -196,31 +115,57 @@ class TestBlinkSetup(unittest.TestCase):
def test_sync_case_insensitive_dict(self, mock_sess): def test_sync_case_insensitive_dict(self, mock_sess):
"""Check that we can access sync modules ignoring case.""" """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): def test_unexpected_login(self, mock_login, mock_sess):
"""Check that we appropriately handle unexpected login info.""" """Check that we appropriately handle unexpected login info."""
mock_login.return_value = None mock_login.return_value = None
self.assertFalse(self.blink.get_auth_token()) 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): def test_get_cameras(self, mock_home, mock_sess):
"""Check retrieval of camera information.""" """Check retrieval of camera information."""
mock_home.return_value = { mock_home.return_value = {
'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678}, "cameras": [
{'name': 'bar', 'network_id': 1234, 'id': 5679}, {"name": "foo", "network_id": 1234, "id": 5678},
{'name': 'test', 'network_id': 4321, 'id': 0000}] {"name": "bar", "network_id": 1234, "id": 5679},
{"name": "test", "network_id": 4321, "id": 0000},
]
} }
result = self.blink.get_cameras() result = self.blink.get_cameras()
self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678}, self.assertEqual(
{'name': 'bar', 'id': 5679}], result,
'4321': [{'name': 'test', 'id': 0000}]}) {
"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): def test_get_cameras_failure(self, mock_home, mock_sess):
"""Check that on failure we initialize empty info and move on.""" """Check that on failure we initialize empty info and move on."""
mock_home.return_value = {} mock_home.return_value = {}
result = self.blink.get_cameras() result = self.blink.get_cameras()
self.assertEqual(result, {}) 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 from blinkpy.camera import BlinkCamera
import tests.mock_responses as mresp import tests.mock_responses as mresp
USERNAME = 'foobar' USERNAME = "foobar"
PASSWORD = 'deadbeef' PASSWORD = "deadbeef"
CAMERA_CFG = { CAMERA_CFG = {
'camera': [ "camera": [
{ {
'battery_voltage': 90, "battery_voltage": 90,
'motion_alert': True, "motion_alert": True,
'wifi_strength': -30, "wifi_strength": -30,
'temperature': 68 "temperature": 68,
} }
] ]
} }
@mock.patch('blinkpy.helpers.util.Session.send', @mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
side_effect=mresp.mocked_session_send)
class TestBlinkCameraSetup(unittest.TestCase): class TestBlinkCameraSetup(unittest.TestCase):
"""Test the Blink class in blinkpy.""" """Test the Blink class in blinkpy."""
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME, self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
password=PASSWORD)
header = { header = {
'Host': 'abc.zxc', "Host": "abc.zxc",
'TOKEN_AUTH': mresp.LOGIN_RESPONSE['authtoken']['authtoken'] "TOKEN_AUTH": mresp.LOGIN_RESPONSE["authtoken"]["authtoken"],
} }
# pylint: disable=protected-access # pylint: disable=protected-access
self.blink._auth_header = header self.blink._auth_header = header
self.blink.session = create_session() self.blink.session = create_session()
self.blink.urls = BlinkURLHandler('test') self.blink.urls = BlinkURLHandler("test")
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, []) self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", 1234, [])
self.camera = BlinkCamera(self.blink.sync['test']) self.camera = BlinkCamera(self.blink.sync["test"])
self.camera.name = 'foobar' self.camera.name = "foobar"
self.blink.sync['test'].cameras['foobar'] = self.camera self.blink.sync["test"].cameras["foobar"] = self.camera
def tearDown(self): def tearDown(self):
"""Clean up after test.""" """Clean up after test."""
@@ -58,98 +56,99 @@ class TestBlinkCameraSetup(unittest.TestCase):
def test_camera_update(self, mock_sess): def test_camera_update(self, mock_sess):
"""Test that we can properly update camera properties.""" """Test that we can properly update camera properties."""
config = { config = {
'name': 'new', "name": "new",
'id': 1234, "id": 1234,
'network_id': 5678, "network_id": 5678,
'serial': '12345678', "serial": "12345678",
'enabled': False, "enabled": False,
'battery_voltage': 90, "battery_voltage": 90,
'battery_state': 'ok', "battery_state": "ok",
'temperature': 68, "temperature": 68,
'wifi_strength': 4, "wifi_strength": 4,
'thumbnail': '/thumb', "thumbnail": "/thumb",
} }
self.camera.last_record = ['1'] self.camera.last_record = ["1"]
self.camera.sync.last_record = { self.camera.sync.last_record = {
'new': { "new": {"clip": "/test.mp4", "time": "1970-01-01T00:00:00"}
'clip': '/test.mp4',
'time': '1970-01-01T00:00:00'
}
} }
mock_sess.side_effect = [ mock_sess.side_effect = [
mresp.MockResponse({'temp': 71}, 200), mresp.MockResponse({"temp": 71}, 200),
'test', "test",
'foobar' "foobar",
] ]
self.camera.update(config) self.camera.update(config)
self.assertEqual(self.camera.name, 'new') self.assertEqual(self.camera.name, "new")
self.assertEqual(self.camera.camera_id, '1234') self.assertEqual(self.camera.camera_id, "1234")
self.assertEqual(self.camera.network_id, '5678') self.assertEqual(self.camera.network_id, "5678")
self.assertEqual(self.camera.serial, '12345678') self.assertEqual(self.camera.serial, "12345678")
self.assertEqual(self.camera.motion_enabled, False) 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, 68)
self.assertEqual(self.camera.temperature_c, 20) self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71) self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4) self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail, self.assertEqual(
'https://rest-test.immedia-semi.com/thumb.jpg') 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.assertEqual(self.camera.image_from_cache, 'test') self.camera.clip, "https://rest-test.immedia-semi.com/test.mp4"
self.assertEqual(self.camera.video_from_cache, 'foobar') )
self.assertEqual(self.camera.image_from_cache, "test")
self.assertEqual(self.camera.video_from_cache, "foobar")
def test_no_thumbnails(self, mock_sess): def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found.""" """Tests that thumbnail is 'None' if none found."""
mock_sess.return_value = 'foobar' mock_sess.return_value = "foobar"
self.camera.last_record = ['1'] self.camera.last_record = ["1"]
config = { config = {
'name': 'new', "name": "new",
'id': 1234, "id": 1234,
'network_id': 5678, "network_id": 5678,
'serial': '12345678', "serial": "12345678",
'enabled': False, "enabled": False,
'battery_voltage': 90, "battery_voltage": 90,
'battery_state': 'ok', "battery_state": "ok",
'temperature': 68, "temperature": 68,
'wifi_strength': 4, "wifi_strength": 4,
'thumbnail': '', "thumbnail": "",
}
self.camera.sync.homescreen = {
'devices': []
} }
self.camera.sync.homescreen = {"devices": []}
self.assertEqual(self.camera.temperature_calibrated, None) self.assertEqual(self.camera.temperature_calibrated, None)
with self.assertLogs() as logrecord: with self.assertLogs() as logrecord:
self.camera.update(config, force=True) self.camera.update(config, force=True)
self.assertEqual(self.camera.thumbnail, None) 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(self.camera.temperature_calibrated, 68)
self.assertEqual( self.assertEqual(
logrecord.output, logrecord.output,
[("WARNING:blinkpy.camera:Could not retrieve calibrated " [
"temperature."), (
("WARNING:blinkpy.camera:Could not find thumbnail for camera new" "WARNING:blinkpy.camera:Could not retrieve calibrated "
"\nNoneType: None")] "temperature."
),
(
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None"
),
],
) )
def test_no_video_clips(self, mock_sess): def test_no_video_clips(self, mock_sess):
"""Tests that we still proceed with camera setup with no videos.""" """Tests that we still proceed with camera setup with no videos."""
mock_sess.return_value = 'foobar' mock_sess.return_value = "foobar"
config = { config = {
'name': 'new', "name": "new",
'id': 1234, "id": 1234,
'network_id': 5678, "network_id": 5678,
'serial': '12345678', "serial": "12345678",
'enabled': False, "enabled": False,
'battery_voltage': 90, "battery_voltage": 90,
'battery_state': 'ok', "battery_state": "ok",
'temperature': 68, "temperature": 68,
'wifi_strength': 4, "wifi_strength": 4,
'thumbnail': '/foobar', "thumbnail": "/foobar",
}
self.camera.sync.homescreen = {
'devices': []
} }
self.camera.sync.homescreen = {"devices": []}
self.camera.update(config, force_cache=True) self.camera.update(config, force_cache=True)
self.assertEqual(self.camera.clip, None) self.assertEqual(self.camera.clip, None)
self.assertEqual(self.camera.video_from_cache, 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.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera from blinkpy.camera import BlinkCamera
USERNAME = 'foobar' USERNAME = "foobar"
PASSWORD = 'deadbeef' PASSWORD = "deadbeef"
@mock.patch('blinkpy.api.http_req') @mock.patch("blinkpy.api.http_req")
class TestBlinkSyncModule(unittest.TestCase): class TestBlinkSyncModule(unittest.TestCase):
"""Test BlinkSyncModule functions in blinkpy.""" """Test BlinkSyncModule functions in blinkpy."""
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME, self.blink = blinkpy.Blink(
password=PASSWORD, username=USERNAME, password=PASSWORD, motion_interval=0
motion_interval=0) )
# pylint: disable=protected-access # pylint: disable=protected-access
self.blink._auth_header = { self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123'
}
self.blink.last_refresh = 0 self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler('test') self.blink.urls = blinkpy.BlinkURLHandler("test")
self.blink.sync['test'] = BlinkSyncModule(self.blink, self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
'test',
'1234',
[])
self.camera = BlinkCamera(self.blink.sync) self.camera = BlinkCamera(self.blink.sync)
self.mock_start = [ self.mock_start = [
{'syncmodule': { {
'id': 1234, "syncmodule": {
'network_id': 5678, "id": 1234,
'serial': '12345678', "network_id": 5678,
'status': 'foobar'}}, "serial": "12345678",
{'event': True}, "status": "foobar",
}
},
{"event": True},
{}, {},
{}, {},
None, None,
{'devicestatus': {}}, {"devicestatus": {}},
] ]
self.blink.sync['test'].network_info = {'network': {'armed': True}} self.blink.sync["test"].network_info = {"network": {"armed": True}}
def tearDown(self): def tearDown(self):
"""Clean up after test.""" """Clean up after test."""
@@ -53,173 +50,177 @@ class TestBlinkSyncModule(unittest.TestCase):
def test_get_events(self, mock_resp): def test_get_events(self, mock_resp):
"""Test get events function.""" """Test get events function."""
mock_resp.return_value = {'event': True} mock_resp.return_value = {"event": True}
self.assertEqual(self.blink.sync['test'].get_events(), True) self.assertEqual(self.blink.sync["test"].get_events(), True)
def test_get_camera_info(self, mock_resp): def test_get_camera_info(self, mock_resp):
"""Test get camera info function.""" """Test get camera info function."""
mock_resp.return_value = {'camera': ['foobar']} mock_resp.return_value = {"camera": ["foobar"]}
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'), self.assertEqual(self.blink.sync["test"].get_camera_info("1234"), "foobar")
'foobar')
def test_check_new_videos_startup(self, mock_resp): def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup.""" """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.blink.last_refresh = None
self.assertFalse(sync_module.check_new_videos()) self.assertFalse(sync_module.check_new_videos())
def test_check_new_videos(self, mock_resp): def test_check_new_videos(self, mock_resp):
"""Test recent video response.""" """Test recent video response."""
mock_resp.return_value = { mock_resp.return_value = {
'media': [{ "media": [
'device_name': 'foo', {
'media': '/foo/bar.mp4', "device_name": "foo",
'created_at': '1990-01-01T00:00:00+00:00' "media": "/foo/bar.mp4",
}] "created_at": "1990-01-01T00:00:00+00:00",
}
]
} }
sync_module = self.blink.sync['test'] sync_module = self.blink.sync["test"]
sync_module.cameras = {'foo': None} sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 0 sync_module.blink.last_refresh = 0
self.assertEqual(sync_module.motion, {}) self.assertEqual(sync_module.motion, {})
self.assertTrue(sync_module.check_new_videos()) self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.last_record['foo'], self.assertEqual(
{'clip': '/foo/bar.mp4', sync_module.last_record["foo"],
'time': '1990-01-01T00:00:00+00:00'}) {"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.motion, {"foo": True})
mock_resp.return_value = {"media": []}
self.assertTrue(sync_module.check_new_videos()) self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False}) self.assertEqual(sync_module.motion, {"foo": False})
self.assertEqual(sync_module.last_record['foo'], self.assertEqual(
{'clip': '/foo/bar.mp4', sync_module.last_record["foo"],
'time': '1990-01-01T00:00:00+00:00'}) {"clip": "/foo/bar.mp4", "time": "1990-01-01T00:00:00+00:00"},
)
def test_check_new_videos_old_date(self, mock_resp): def test_check_new_videos_old_date(self, mock_resp):
"""Test videos return response with old date.""" """Test videos return response with old date."""
mock_resp.return_value = { mock_resp.return_value = {
'media': [{ "media": [
'device_name': 'foo', {
'media': '/foo/bar.mp4', "device_name": "foo",
'created_at': '1970-01-01T00:00:00+00:00' "media": "/foo/bar.mp4",
}] "created_at": "1970-01-01T00:00:00+00:00",
}
]
} }
sync_module = self.blink.sync['test'] sync_module = self.blink.sync["test"]
sync_module.cameras = {'foo': None} sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 1000 sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos()) 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): def test_check_no_motion_if_not_armed(self, mock_resp):
"""Test that motion detection is not set if module unarmed.""" """Test that motion detection is not set if module unarmed."""
mock_resp.return_value = { mock_resp.return_value = {
'media': [{ "media": [
'device_name': 'foo', {
'media': '/foo/bar.mp4', "device_name": "foo",
'created_at': '1990-01-01T00:00:00+00:00' "media": "/foo/bar.mp4",
}] "created_at": "1990-01-01T00:00:00+00:00",
}
]
} }
sync_module = self.blink.sync['test'] sync_module = self.blink.sync["test"]
sync_module.cameras = {'foo': None} sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 1000 sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos()) self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': True}) self.assertEqual(sync_module.motion, {"foo": True})
sync_module.network_info = {'network': {'armed': False}} sync_module.network_info = {"network": {"armed": False}}
self.assertTrue(sync_module.check_new_videos()) 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): def test_check_multiple_videos(self, mock_resp):
"""Test motion found even with multiple videos.""" """Test motion found even with multiple videos."""
mock_resp.return_value = { mock_resp.return_value = {
'media': [ "media": [
{ {
'device_name': 'foo', "device_name": "foo",
'media': '/foo/bar.mp4', "media": "/foo/bar.mp4",
'created_at': '1970-01-01T00:00:00+00:00' "created_at": "1970-01-01T00:00:00+00:00",
}, },
{ {
'device_name': 'foo', "device_name": "foo",
'media': '/bar/foo.mp4', "media": "/bar/foo.mp4",
'created_at': '1990-01-01T00:00:00+00:00' "created_at": "1990-01-01T00:00:00+00:00",
}, },
{ {
'device_name': 'foo', "device_name": "foo",
'media': '/foobar.mp4', "media": "/foobar.mp4",
'created_at': '1970-01-01T00:00:01+00:00' "created_at": "1970-01-01T00:00:01+00:00",
} },
] ]
} }
sync_module = self.blink.sync['test'] sync_module = self.blink.sync["test"]
sync_module.cameras = {'foo': None} sync_module.cameras = {"foo": None}
sync_module.blink.last_refresh = 1000 sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos()) self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': True}) self.assertEqual(sync_module.motion, {"foo": True})
expected_result = { expected_result = {
'foo': { "foo": {"clip": "/bar/foo.mp4", "time": "1990-01-01T00:00:00+00:00"}
'clip': '/bar/foo.mp4',
'time': '1990-01-01T00:00:00+00:00'
}
} }
self.assertEqual(sync_module.last_record, expected_result) self.assertEqual(sync_module.last_record, expected_result)
def test_check_new_videos_failed(self, mock_resp): def test_check_new_videos_failed(self, mock_resp):
"""Test method when response is unexpected.""" """Test method when response is unexpected."""
mock_resp.side_effect = [None, 'just a string', {}] mock_resp.side_effect = [None, "just a string", {}]
sync_module = self.blink.sync['test'] sync_module = self.blink.sync["test"]
sync_module.cameras = {'foo': None} 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.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.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.check_new_videos())
self.assertFalse(sync_module.motion['foo']) self.assertFalse(sync_module.motion["foo"])
def test_sync_start(self, mock_resp): def test_sync_start(self, mock_resp):
"""Test sync start function.""" """Test sync start function."""
mock_resp.side_effect = self.mock_start mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start() self.blink.sync["test"].start()
self.assertEqual(self.blink.sync['test'].name, 'test') self.assertEqual(self.blink.sync["test"].name, "test")
self.assertEqual(self.blink.sync['test'].sync_id, 1234) self.assertEqual(self.blink.sync["test"].sync_id, 1234)
self.assertEqual(self.blink.sync['test'].network_id, 5678) self.assertEqual(self.blink.sync["test"].network_id, 5678)
self.assertEqual(self.blink.sync['test'].serial, '12345678') self.assertEqual(self.blink.sync["test"].serial, "12345678")
self.assertEqual(self.blink.sync['test'].status, 'foobar') self.assertEqual(self.blink.sync["test"].status, "foobar")
def test_unexpected_summary(self, mock_resp): def test_unexpected_summary(self, mock_resp):
"""Test unexpected summary response.""" """Test unexpected summary response."""
self.mock_start[0] = None self.mock_start[0] = None
mock_resp.side_effect = self.mock_start 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): def test_summary_with_no_network_id(self, mock_resp):
"""Test handling of bad summary.""" """Test handling of bad summary."""
self.mock_start[0]['syncmodule'] = None self.mock_start[0]["syncmodule"] = None
mock_resp.side_effect = self.mock_start 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): def test_summary_with_only_network_id(self, mock_resp):
"""Test handling of sparse summary.""" """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 mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start() self.blink.sync["test"].start()
self.assertEqual(self.blink.sync['test'].network_id, 8675309) self.assertEqual(self.blink.sync["test"].network_id, 8675309)
def test_unexpected_camera_info(self, mock_resp): def test_unexpected_camera_info(self, mock_resp):
"""Test unexpected camera info response.""" """Test unexpected camera info response."""
self.blink.sync['test'].cameras['foo'] = None self.blink.sync["test"].cameras["foo"] = None
self.mock_start[5] = None self.mock_start[5] = None
mock_resp.side_effect = self.mock_start mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start() self.blink.sync["test"].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None}) self.assertEqual(self.blink.sync["test"].cameras, {"foo": None})
def test_missing_camera_info(self, mock_resp): def test_missing_camera_info(self, mock_resp):
"""Test missing key from camera info response.""" """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.mock_start[5] = {}
self.blink.sync['test'].start() self.blink.sync["test"].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None}) 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)) self.assertEqual(2, len(calls))
# Fake time as 4 seconds from now # 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() test_throttle()
self.assertEqual(2, len(calls)) self.assertEqual(2, len(calls))
# Fake time as 6 seconds from now # 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() test_throttle()
self.assertEqual(3, len(calls)) self.assertEqual(3, len(calls))
def test_throttle_per_instance(self): def test_throttle_per_instance(self):
"""Test that throttle is done once per instance of class.""" """Test that throttle is done once per instance of class."""
class Tester: class Tester:
"""A tester class for throttling.""" """A tester class for throttling."""
@@ -68,6 +69,7 @@ class TestUtil(unittest.TestCase):
def test_throttle_on_two_methods(self): def test_throttle_on_two_methods(self):
"""Test that throttle works for multiple methods.""" """Test that throttle works for multiple methods."""
class Tester: class Tester:
"""A tester class for throttling.""" """A tester class for throttling."""
@@ -91,24 +93,24 @@ class TestUtil(unittest.TestCase):
self.assertEqual(tester.test1(), None) self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), 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.test1(), True)
self.assertEqual(tester.test2(), None) 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.test1(), None)
self.assertEqual(tester.test2(), True) self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self): def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode.""" """Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler('test') urls = BlinkURLHandler("test")
self.assertEqual(urls.subdomain, 'rest-test') self.assertEqual(urls.subdomain, "rest-test")
urls = BlinkURLHandler('test', legacy=True) urls = BlinkURLHandler("test", legacy=True)
self.assertEqual(urls.subdomain, 'rest.test') self.assertEqual(urls.subdomain, "rest.test")
def test_time_to_seconds(self): def test_time_to_seconds(self):
"""Test time to seconds conversion.""" """Test time to seconds conversion."""
correct_time = '1970-01-01T00:00:05+00:00' correct_time = "1970-01-01T00:00:05+00:00"
wrong_time = '1/1/1970 00:00:03' wrong_time = "1/1/1970 00:00:03"
self.assertEqual(time_to_seconds(correct_time), 5) self.assertEqual(time_to_seconds(correct_time), 5)
self.assertFalse(time_to_seconds(wrong_time)) self.assertFalse(time_to_seconds(wrong_time))
+10 -2
View File
@@ -1,5 +1,5 @@
[tox] [tox]
envlist = build, py35, py36, py37, py38, lint envlist = build, py36, py37, py38, lint
skip_missing_interpreters = True skip_missing_interpreters = True
skipsdist = True skipsdist = True
@@ -23,7 +23,7 @@ deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt -r{toxinidir}/requirements_test.txt
[testenv:lint] [testenv:pylint]
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt -r{toxinidir}/requirements_test.txt
@@ -31,8 +31,16 @@ basepython = python3
ignore_errors = True ignore_errors = True
commands = commands =
pylint --rcfile={toxinidir}/pylintrc blinkpy tests app 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 flake8 blinkpy tests app
pydocstyle blinkpy tests app pydocstyle blinkpy tests app
black --check --diff blinkpy tests app
rst-lint README.rst rst-lint README.rst
rst-lint CHANGES.rst rst-lint CHANGES.rst